kopia lustrzana https://github.com/ihabunek/toot
Porównaj commity
15 Commity
e1be3a68bb
...
4b17e2e586
Autor | SHA1 | Data |
---|---|---|
Ivan Habunek | 4b17e2e586 | |
Daniel Schwarz | 20968fe87f | |
Ivan Habunek | 3bac9b2fb6 | |
Ivan Habunek | 3420f1466a | |
Ivan Habunek | 3eebbe35c9 | |
Ivan Habunek | 4d5ac3cc4e | |
Ivan Habunek | ee98ce3746 | |
Ivan Habunek | 0cbb8863b3 | |
Ivan Habunek | 1709a416b3 | |
Ivan Habunek | f324aa119d | |
Ivan Habunek | 43f51cbbb9 | |
Ivan Habunek | 225dfbfb2e | |
Ivan Habunek | 9ae205c548 | |
Ivan Habunek | 9875209b30 | |
Ivan Habunek | 965ffa1312 |
|
@ -3,6 +3,13 @@ Changelog
|
|||
|
||||
<!-- Do not edit. This file is automatically generated from changelog.yaml.-->
|
||||
|
||||
**0.42.0 (2024-03-09)**
|
||||
|
||||
* TUI: Add `toot tui --always-show-sensitive` option (thanks Lexi Winter)
|
||||
* TUI: Document missing shortcuts (thanks Denis Laxalde)
|
||||
* TUI: Use rounded boxes for nicer visuals (thanks Dan Schwarz)
|
||||
* TUI: Don't break if edited_at status field does not exist
|
||||
|
||||
**0.41.1 (2024-01-02)**
|
||||
|
||||
* Fix a crash in settings parsing code
|
||||
|
|
|
@ -1,3 +1,11 @@
|
|||
0.42.0:
|
||||
date: 2024-03-09
|
||||
changes:
|
||||
- "TUI: Add `toot tui --always-show-sensitive` option (thanks Lexi Winter)"
|
||||
- "TUI: Document missing shortcuts (thanks Denis Laxalde)"
|
||||
- "TUI: Use rounded boxes for nicer visuals (thanks Dan Schwarz)"
|
||||
- "TUI: Don't break if edited_at status field does not exist"
|
||||
|
||||
0.41.1:
|
||||
date: 2024-01-02
|
||||
changes:
|
||||
|
|
|
@ -3,6 +3,13 @@ Changelog
|
|||
|
||||
<!-- Do not edit. This file is automatically generated from changelog.yaml.-->
|
||||
|
||||
**0.42.0 (2024-03-09)**
|
||||
|
||||
* TUI: Add `toot tui --always-show-sensitive` option (thanks Lexi Winter)
|
||||
* TUI: Document missing shortcuts (thanks Denis Laxalde)
|
||||
* TUI: Use rounded boxes for nicer visuals (thanks Dan Schwarz)
|
||||
* TUI: Don't break if edited_at status field does not exist
|
||||
|
||||
**0.41.1 (2024-01-02)**
|
||||
|
||||
* Fix a crash in settings parsing code
|
||||
|
|
2
setup.py
2
setup.py
|
@ -12,7 +12,7 @@ and blocking accounts and other actions.
|
|||
|
||||
setup(
|
||||
name='toot',
|
||||
version='0.41.1',
|
||||
version='0.42.0',
|
||||
description='Mastodon CLI client',
|
||||
long_description=long_description.strip(),
|
||||
author='Ivan Habunek',
|
||||
|
|
|
@ -0,0 +1,42 @@
|
|||
Testing toot
|
||||
============
|
||||
|
||||
This document is WIP.
|
||||
|
||||
Mastodon
|
||||
--------
|
||||
|
||||
TODO
|
||||
|
||||
Pleroma
|
||||
-------
|
||||
|
||||
TODO
|
||||
|
||||
Akkoma
|
||||
------
|
||||
|
||||
Install using the guide here:
|
||||
https://docs.akkoma.dev/stable/installation/docker_en/
|
||||
|
||||
Disable captcha and throttling by adding this to `config/prod.exs`:
|
||||
|
||||
```ex
|
||||
# Disable captcha for testing
|
||||
config :pleroma, Pleroma.Captcha,
|
||||
enabled: false
|
||||
|
||||
# Disable rate limiting for testing
|
||||
config :pleroma, :rate_limit,
|
||||
authentication: nil,
|
||||
timeline: nil,
|
||||
search: nil,
|
||||
app_account_creation: nil,
|
||||
relations_actions: nil,
|
||||
relation_id_action: nil,
|
||||
statuses_actions: nil,
|
||||
status_id_action: nil,
|
||||
password_reset: nil,
|
||||
account_confirmation_resend: nil,
|
||||
ap_routes: nil
|
||||
```
|
|
@ -1,4 +1,5 @@
|
|||
import json
|
||||
from tests.integration.conftest import register_account
|
||||
|
||||
from toot import App, User, api, cli
|
||||
from toot.entities import Account, Relationship, from_dict
|
||||
|
@ -35,9 +36,8 @@ def test_whois(app: App, friend: User, run):
|
|||
assert f"@{friend.username}" in result.stdout
|
||||
|
||||
|
||||
def test_following(app: App, user: User, friend: User, friend_id, run):
|
||||
# Make sure we're not initially following friend
|
||||
api.unfollow(app, user, friend_id)
|
||||
def test_following(app: App, user: User, run):
|
||||
friend = register_account(app)
|
||||
|
||||
result = run(cli.accounts.following, user.username)
|
||||
assert result.exit_code == 0
|
||||
|
@ -84,9 +84,8 @@ def test_following_not_found(run):
|
|||
assert result.stderr.strip() == "Error: Account not found"
|
||||
|
||||
|
||||
def test_following_json(app: App, user: User, friend: User, user_id, friend_id, run_json):
|
||||
# Make sure we're not initially following friend
|
||||
api.unfollow(app, user, friend_id)
|
||||
def test_following_json(app: App, user: User, user_id, run_json):
|
||||
friend = register_account(app)
|
||||
|
||||
result = run_json(cli.accounts.following, user.username, "--json")
|
||||
assert result == []
|
||||
|
@ -96,24 +95,26 @@ def test_following_json(app: App, user: User, friend: User, user_id, friend_id,
|
|||
|
||||
result = run_json(cli.accounts.follow, friend.username, "--json")
|
||||
relationship = from_dict(Relationship, result)
|
||||
assert relationship.id == friend_id
|
||||
assert relationship.following is True
|
||||
|
||||
[result] = run_json(cli.accounts.following, user.username, "--json")
|
||||
relationship = from_dict(Relationship, result)
|
||||
assert relationship.id == friend_id
|
||||
account = from_dict(Account, result)
|
||||
assert account.acct == friend.username
|
||||
|
||||
# If no account is given defaults to logged in user
|
||||
[result] = run_json(cli.accounts.following, user.username, "--json")
|
||||
relationship = from_dict(Relationship, result)
|
||||
assert relationship.id == friend_id
|
||||
[result] = run_json(cli.accounts.following, "--json")
|
||||
account = from_dict(Account, result)
|
||||
assert account.acct == friend.username
|
||||
|
||||
assert relationship.following is True
|
||||
|
||||
[result] = run_json(cli.accounts.followers, friend.username, "--json")
|
||||
assert result["id"] == user_id
|
||||
account = from_dict(Account, result)
|
||||
assert account.acct == user.username
|
||||
|
||||
result = run_json(cli.accounts.unfollow, friend.username, "--json")
|
||||
assert result["id"] == friend_id
|
||||
assert result["following"] is False
|
||||
relationship = from_dict(Relationship, result)
|
||||
assert relationship.following is False
|
||||
|
||||
result = run_json(cli.accounts.following, user.username, "--json")
|
||||
assert result == []
|
||||
|
@ -200,9 +201,8 @@ def test_mute_json(app: App, user: User, friend: User, run_json, friend_id):
|
|||
assert result == []
|
||||
|
||||
|
||||
def test_block(app, user, friend, friend_id, run):
|
||||
# Make sure we're not initially blocking friend
|
||||
api.unblock(app, user, friend_id)
|
||||
def test_block(app, user, run):
|
||||
friend = register_account(app)
|
||||
|
||||
result = run(cli.accounts.blocked)
|
||||
assert result.exit_code == 0
|
||||
|
|
|
@ -4,7 +4,7 @@ import sys
|
|||
from os.path import join, expanduser
|
||||
from typing import NamedTuple
|
||||
|
||||
__version__ = '0.41.1'
|
||||
__version__ = '0.42.0'
|
||||
|
||||
|
||||
class App(NamedTuple):
|
||||
|
|
|
@ -3,6 +3,7 @@ import json as pyjson
|
|||
|
||||
from toot import api, config
|
||||
from toot.cli import Context, cli, pass_context, json_option
|
||||
from toot.entities import from_dict_list, List
|
||||
from toot.output import print_list_accounts, print_lists, print_warning
|
||||
|
||||
|
||||
|
@ -18,7 +19,8 @@ def lists(ctx: click.Context):
|
|||
if not user or not app:
|
||||
raise click.ClickException("This command requires you to be logged in.")
|
||||
|
||||
lists = api.get_lists(app, user)
|
||||
data = api.get_lists(app, user)
|
||||
lists = from_dict_list(List, data)
|
||||
if lists:
|
||||
print_lists(lists)
|
||||
else:
|
||||
|
@ -30,12 +32,13 @@ def lists(ctx: click.Context):
|
|||
@pass_context
|
||||
def list(ctx: Context, json: bool):
|
||||
"""List all your lists"""
|
||||
lists = api.get_lists(ctx.app, ctx.user)
|
||||
data = api.get_lists(ctx.app, ctx.user)
|
||||
|
||||
if json:
|
||||
click.echo(pyjson.dumps(lists))
|
||||
click.echo(pyjson.dumps(data))
|
||||
else:
|
||||
if lists:
|
||||
if data:
|
||||
lists = from_dict_list(List, data)
|
||||
print_lists(lists)
|
||||
else:
|
||||
click.echo("You have no lists defined.")
|
||||
|
|
|
@ -145,7 +145,7 @@ def post(
|
|||
else:
|
||||
user, app = ctx.user, ctx.app
|
||||
|
||||
media_ids = _upload_media(ctx.app, ctx.user, media, descriptions, thumbnails)
|
||||
media_ids = _upload_media(app, user, media, descriptions, thumbnails)
|
||||
status_text = _get_status_text(text, editor, media)
|
||||
scheduled_at = _get_scheduled_at(scheduled_at, scheduled_in)
|
||||
|
||||
|
|
|
@ -30,7 +30,7 @@ COLOR_OPTIONS = ", ".join(TUI_COLORS.keys())
|
|||
help="Default visibility when posting new toots; overrides the server-side preference"
|
||||
)
|
||||
@click.option(
|
||||
"-S", "--always-show-sensitive",
|
||||
"-s", "--always-show-sensitive",
|
||||
is_flag=True,
|
||||
help="Expand toots with content warnings automatically"
|
||||
)
|
||||
|
|
|
@ -17,11 +17,11 @@ def get_config_file_path():
|
|||
return join(get_config_dir(), TOOT_CONFIG_FILE_NAME)
|
||||
|
||||
|
||||
def user_id(user):
|
||||
def user_id(user: User):
|
||||
return "{}@{}".format(user.username, user.instance)
|
||||
|
||||
|
||||
def make_config(path):
|
||||
def make_config(path: str):
|
||||
"""Creates an empty toot configuration file."""
|
||||
config = {
|
||||
"apps": {},
|
||||
|
@ -58,7 +58,7 @@ def save_config(config):
|
|||
return json.dump(config, f, indent=True, sort_keys=True)
|
||||
|
||||
|
||||
def extract_user_app(config, user_id):
|
||||
def extract_user_app(config, user_id: str):
|
||||
if user_id not in config['users']:
|
||||
return None, None
|
||||
|
||||
|
@ -82,7 +82,7 @@ def get_active_user_app():
|
|||
return None, None
|
||||
|
||||
|
||||
def get_user_app(user_id):
|
||||
def get_user_app(user_id: str):
|
||||
"""Returns (User, App) for given user ID or (None, None) if user is not logged in."""
|
||||
return extract_user_app(load_config(), user_id)
|
||||
|
||||
|
@ -93,7 +93,7 @@ def load_app(instance: str) -> Optional[App]:
|
|||
return App(**config['apps'][instance])
|
||||
|
||||
|
||||
def load_user(user_id, throw=False):
|
||||
def load_user(user_id: str, throw=False):
|
||||
config = load_config()
|
||||
|
||||
if user_id in config['users']:
|
||||
|
@ -120,7 +120,7 @@ def save_app(app: App):
|
|||
config['apps'][app.instance] = app._asdict()
|
||||
|
||||
|
||||
def delete_app(config, app):
|
||||
def delete_app(config, app: App):
|
||||
with edit_config() as config:
|
||||
config['apps'].pop(app.instance, None)
|
||||
|
||||
|
|
|
@ -9,11 +9,12 @@ different versions of the Mastodon API.
|
|||
"""
|
||||
|
||||
import dataclasses
|
||||
import typing as t
|
||||
|
||||
from dataclasses import dataclass, is_dataclass
|
||||
from datetime import date, datetime
|
||||
from functools import lru_cache
|
||||
from typing import Any, Dict, List, Optional, Tuple, Type, TypeVar, Union
|
||||
from typing import Any, Dict, Optional, Tuple, Type, TypeVar, Union
|
||||
from typing import get_type_hints
|
||||
|
||||
from toot.typing_compat import get_args, get_origin
|
||||
|
@ -59,8 +60,8 @@ class Account:
|
|||
header: str
|
||||
header_static: str
|
||||
locked: bool
|
||||
fields: List[AccountField]
|
||||
emojis: List[CustomEmoji]
|
||||
fields: t.List[AccountField]
|
||||
emojis: t.List[CustomEmoji]
|
||||
bot: bool
|
||||
group: bool
|
||||
discoverable: Optional[bool]
|
||||
|
@ -154,10 +155,10 @@ class Poll:
|
|||
multiple: bool
|
||||
votes_count: int
|
||||
voters_count: Optional[int]
|
||||
options: List[PollOption]
|
||||
emojis: List[CustomEmoji]
|
||||
options: t.List[PollOption]
|
||||
emojis: t.List[CustomEmoji]
|
||||
voted: Optional[bool]
|
||||
own_votes: Optional[List[int]]
|
||||
own_votes: Optional[t.List[int]]
|
||||
|
||||
|
||||
@dataclass
|
||||
|
@ -207,11 +208,11 @@ class Filter:
|
|||
"""
|
||||
id: str
|
||||
title: str
|
||||
context: List[str]
|
||||
context: t.List[str]
|
||||
expires_at: Optional[datetime]
|
||||
filter_action: str
|
||||
keywords: List[FilterKeyword]
|
||||
statuses: List[FilterStatus]
|
||||
keywords: t.List[FilterKeyword]
|
||||
statuses: t.List[FilterStatus]
|
||||
|
||||
|
||||
@dataclass
|
||||
|
@ -220,7 +221,7 @@ class FilterResult:
|
|||
https://docs.joinmastodon.org/entities/FilterResult/
|
||||
"""
|
||||
filter: Filter
|
||||
keyword_matches: Optional[List[str]]
|
||||
keyword_matches: Optional[t.List[str]]
|
||||
status_matches: Optional[str]
|
||||
|
||||
|
||||
|
@ -237,11 +238,11 @@ class Status:
|
|||
visibility: str
|
||||
sensitive: bool
|
||||
spoiler_text: str
|
||||
media_attachments: List[MediaAttachment]
|
||||
media_attachments: t.List[MediaAttachment]
|
||||
application: Optional[Application]
|
||||
mentions: List[StatusMention]
|
||||
tags: List[StatusTag]
|
||||
emojis: List[CustomEmoji]
|
||||
mentions: t.List[StatusMention]
|
||||
tags: t.List[StatusTag]
|
||||
emojis: t.List[CustomEmoji]
|
||||
reblogs_count: int
|
||||
favourites_count: int
|
||||
replies_count: int
|
||||
|
@ -259,7 +260,7 @@ class Status:
|
|||
muted: Optional[bool]
|
||||
bookmarked: Optional[bool]
|
||||
pinned: Optional[bool]
|
||||
filtered: Optional[List[FilterResult]]
|
||||
filtered: Optional[t.List[FilterResult]]
|
||||
|
||||
@property
|
||||
def original(self) -> "Status":
|
||||
|
@ -289,8 +290,8 @@ class Report:
|
|||
comment: str
|
||||
forwarded: bool
|
||||
created_at: datetime
|
||||
status_ids: Optional[List[str]]
|
||||
rule_ids: Optional[List[str]]
|
||||
status_ids: Optional[t.List[str]]
|
||||
rule_ids: Optional[t.List[str]]
|
||||
target_account: Account
|
||||
|
||||
|
||||
|
@ -328,7 +329,7 @@ class InstanceConfigurationStatuses:
|
|||
|
||||
@dataclass
|
||||
class InstanceConfigurationMediaAttachments:
|
||||
supported_mime_types: List[str]
|
||||
supported_mime_types: t.List[str]
|
||||
image_size_limit: int
|
||||
image_matrix_limit: int
|
||||
video_size_limit: int
|
||||
|
@ -377,13 +378,13 @@ class Instance:
|
|||
urls: InstanceUrls
|
||||
stats: InstanceStats
|
||||
thumbnail: Optional[str]
|
||||
languages: List[str]
|
||||
languages: t.List[str]
|
||||
registrations: bool
|
||||
approval_required: bool
|
||||
invites_enabled: bool
|
||||
configuration: InstanceConfiguration
|
||||
contact_account: Optional[Account]
|
||||
rules: List[Rule]
|
||||
rules: t.List[Rule]
|
||||
|
||||
|
||||
@dataclass
|
||||
|
@ -397,7 +398,7 @@ class Relationship:
|
|||
following: bool
|
||||
showing_reblogs: bool
|
||||
notifying: bool
|
||||
languages: List[str]
|
||||
languages: t.List[str]
|
||||
followed_by: bool
|
||||
blocking: bool
|
||||
blocked_by: bool
|
||||
|
@ -428,7 +429,7 @@ class Tag:
|
|||
"""
|
||||
name: str
|
||||
url: str
|
||||
history: List[TagHistory]
|
||||
history: t.List[TagHistory]
|
||||
following: Optional[bool]
|
||||
|
||||
|
||||
|
@ -445,6 +446,19 @@ class FeaturedTag:
|
|||
last_status_at: datetime
|
||||
|
||||
|
||||
@dataclass
|
||||
class List:
|
||||
"""
|
||||
Represents a list of some users that the authenticated user follows.
|
||||
https://docs.joinmastodon.org/entities/List/
|
||||
"""
|
||||
id: str
|
||||
title: str
|
||||
# This is a required field on Mastodon, but not supported on Pleroma/Akkoma
|
||||
# see: https://git.pleroma.social/pleroma/pleroma/-/issues/2918
|
||||
replies_policy: Optional[str]
|
||||
|
||||
|
||||
# Generic data class instance
|
||||
T = TypeVar("T")
|
||||
|
||||
|
@ -481,7 +495,7 @@ def from_dict(cls: Type[T], data: Dict) -> T:
|
|||
|
||||
|
||||
@lru_cache(maxsize=100)
|
||||
def get_fields(cls: Type) -> List[Tuple[str, Type, Any]]:
|
||||
def get_fields(cls: Type) -> t.List[Tuple[str, Type, Any]]:
|
||||
hints = get_type_hints(cls)
|
||||
return [
|
||||
(
|
||||
|
@ -493,7 +507,7 @@ def get_fields(cls: Type) -> List[Tuple[str, Type, Any]]:
|
|||
]
|
||||
|
||||
|
||||
def from_dict_list(cls: Type[T], data: List[Dict]) -> List[T]:
|
||||
def from_dict_list(cls: Type[T], data: t.List[Dict]) -> t.List[T]:
|
||||
return [from_dict(cls, x) for x in data]
|
||||
|
||||
|
||||
|
|
|
@ -1,12 +1,12 @@
|
|||
import click
|
||||
import re
|
||||
import textwrap
|
||||
import shutil
|
||||
import textwrap
|
||||
import typing as t
|
||||
|
||||
from toot.entities import Account, Instance, Notification, Poll, Status
|
||||
from toot.entities import Account, Instance, Notification, Poll, Status, List
|
||||
from toot.utils import get_text, html_to_paragraphs
|
||||
from toot.wcstring import wc_wrap
|
||||
from typing import Any, Generator, Iterable, List
|
||||
from wcwidth import wcswidth
|
||||
|
||||
|
||||
|
@ -38,7 +38,7 @@ def instance_to_text(instance: Instance, width: int) -> str:
|
|||
return "\n".join(instance_lines(instance, width))
|
||||
|
||||
|
||||
def instance_lines(instance: Instance, width: int) -> Generator[str, None, None]:
|
||||
def instance_lines(instance: Instance, width: int) -> t.Generator[str, None, None]:
|
||||
yield f"{green(instance.title)}"
|
||||
yield f"{blue(instance.uri)}"
|
||||
yield f"running Mastodon {instance.version}"
|
||||
|
@ -78,7 +78,7 @@ def account_to_text(account: Account, width: int) -> str:
|
|||
return "\n".join(account_lines(account, width))
|
||||
|
||||
|
||||
def account_lines(account: Account, width: int) -> Generator[str, None, None]:
|
||||
def account_lines(account: Account, width: int) -> t.Generator[str, None, None]:
|
||||
acct = f"@{account.acct}"
|
||||
since = account.created_at.strftime("%Y-%m-%d")
|
||||
|
||||
|
@ -119,13 +119,13 @@ def print_tag_list(tags):
|
|||
click.echo(f"* {format_tag_name(tag)}\t{tag['url']}")
|
||||
|
||||
|
||||
def print_lists(lists):
|
||||
def print_lists(lists: t.List[List]):
|
||||
headers = ["ID", "Title", "Replies"]
|
||||
data = [[lst["id"], lst["title"], lst["replies_policy"]] for lst in lists]
|
||||
data = [[lst.id, lst.title, lst.replies_policy or ""] for lst in lists]
|
||||
print_table(headers, data)
|
||||
|
||||
|
||||
def print_table(headers: List[str], data: List[List[str]]):
|
||||
def print_table(headers: t.List[str], data: t.List[t.List[str]]):
|
||||
widths = [[len(cell) for cell in row] for row in data + [headers]]
|
||||
widths = [max(width) for width in zip(*widths)]
|
||||
|
||||
|
@ -178,7 +178,7 @@ def status_to_text(status: Status, width: int) -> str:
|
|||
return "\n".join(status_lines(status))
|
||||
|
||||
|
||||
def status_lines(status: Status) -> Generator[str, None, None]:
|
||||
def status_lines(status: Status) -> t.Generator[str, None, None]:
|
||||
width = get_width()
|
||||
status_id = status.id
|
||||
in_reply_to_id = status.in_reply_to_id
|
||||
|
@ -222,7 +222,7 @@ def status_lines(status: Status) -> Generator[str, None, None]:
|
|||
yield f"ID {yellow(status_id)} {reply} {boost}"
|
||||
|
||||
|
||||
def html_lines(html: str, width: int) -> Generator[str, None, None]:
|
||||
def html_lines(html: str, width: int) -> t.Generator[str, None, None]:
|
||||
first = True
|
||||
for paragraph in html_to_paragraphs(html):
|
||||
if not first:
|
||||
|
@ -233,7 +233,7 @@ def html_lines(html: str, width: int) -> Generator[str, None, None]:
|
|||
first = False
|
||||
|
||||
|
||||
def poll_lines(poll: Poll) -> Generator[str, None, None]:
|
||||
def poll_lines(poll: Poll) -> t.Generator[str, None, None]:
|
||||
for idx, option in enumerate(poll.options):
|
||||
perc = (round(100 * option.votes_count / poll.votes_count)
|
||||
if poll.votes_count and option.votes_count is not None else 0)
|
||||
|
@ -258,7 +258,7 @@ def poll_lines(poll: Poll) -> Generator[str, None, None]:
|
|||
yield poll_footer
|
||||
|
||||
|
||||
def print_timeline(items: Iterable[Status]):
|
||||
def print_timeline(items: t.Iterable[Status]):
|
||||
print_divider()
|
||||
for item in items:
|
||||
print_status(item)
|
||||
|
@ -272,7 +272,7 @@ def print_notification(notification: Notification):
|
|||
print_status(notification.status)
|
||||
|
||||
|
||||
def print_notifications(notifications: List[Notification]):
|
||||
def print_notifications(notifications: t.List[Notification]):
|
||||
for notification in notifications:
|
||||
if notification.type not in ['pleroma:emoji_reaction']:
|
||||
print_divider()
|
||||
|
@ -316,25 +316,25 @@ def format_account_name(account: Account) -> str:
|
|||
|
||||
# Shorthand functions for coloring output
|
||||
|
||||
def blue(text: Any) -> str:
|
||||
def blue(text: t.Any) -> str:
|
||||
return click.style(text, fg="blue")
|
||||
|
||||
|
||||
def bold(text: Any) -> str:
|
||||
def bold(text: t.Any) -> str:
|
||||
return click.style(text, bold=True)
|
||||
|
||||
|
||||
def cyan(text: Any) -> str:
|
||||
def cyan(text: t.Any) -> str:
|
||||
return click.style(text, fg="cyan")
|
||||
|
||||
|
||||
def dim(text: Any) -> str:
|
||||
def dim(text: t.Any) -> str:
|
||||
return click.style(text, dim=True)
|
||||
|
||||
|
||||
def green(text: Any) -> str:
|
||||
def green(text: t.Any) -> str:
|
||||
return click.style(text, fg="green")
|
||||
|
||||
|
||||
def yellow(text: Any) -> str:
|
||||
def yellow(text: t.Any) -> str:
|
||||
return click.style(text, fg="yellow")
|
||||
|
|
|
@ -35,7 +35,7 @@ class TuiOptions(NamedTuple):
|
|||
media_viewer: Optional[str]
|
||||
always_show_sensitive: bool
|
||||
relative_datetimes: bool
|
||||
default_visibility: Optional[bool]
|
||||
default_visibility: Optional[str]
|
||||
|
||||
|
||||
class Header(urwid.WidgetWrap):
|
||||
|
|
|
@ -53,7 +53,7 @@ class Status:
|
|||
self.id = self.data["id"]
|
||||
self.account = self._get_account()
|
||||
self.created_at = parse_datetime(data["created_at"])
|
||||
if data["edited_at"]:
|
||||
if data.get("edited_at"):
|
||||
self.edited_at = parse_datetime(data["edited_at"])
|
||||
else:
|
||||
self.edited_at = None
|
||||
|
|
|
@ -60,7 +60,10 @@ def html_to_widgets(html, recovery_attempt=False) -> List[urwid.Widget]:
|
|||
|
||||
|
||||
def url_to_widget(url: str):
|
||||
widget = len(url), urwid.Filler(Hyperlink(url, "link", url))
|
||||
try:
|
||||
widget = len(url), urwid.Filler(Hyperlink(url, "link", url))
|
||||
except ValueError:
|
||||
widget = len(url), urwid.Filler(urwid.Text(url)) # don't style as link
|
||||
return TextEmbed(widget)
|
||||
|
||||
|
||||
|
@ -98,10 +101,16 @@ def text_to_widget(attr, markup) -> urwid.Widget:
|
|||
if match:
|
||||
label, url = match.groups()
|
||||
anchor_attr = get_best_anchor_attr(attr_list)
|
||||
markup_list.append((
|
||||
len(label),
|
||||
urwid.Filler(Hyperlink(url, anchor_attr, label)),
|
||||
))
|
||||
try:
|
||||
markup_list.append((
|
||||
len(label),
|
||||
urwid.Filler(Hyperlink(url, anchor_attr, label)),
|
||||
))
|
||||
except ValueError:
|
||||
markup_list.append((
|
||||
len(label),
|
||||
urwid.Filler(urwid.Text(url)), # don't style as link
|
||||
))
|
||||
else:
|
||||
markup_list.append(run)
|
||||
else:
|
||||
|
|
|
@ -1,26 +1,22 @@
|
|||
import click
|
||||
import os
|
||||
import re
|
||||
import socket
|
||||
import subprocess
|
||||
import tempfile
|
||||
import unicodedata
|
||||
import warnings
|
||||
|
||||
from bs4 import BeautifulSoup
|
||||
from typing import Any, Dict, List
|
||||
|
||||
import click
|
||||
|
||||
from toot.exceptions import ConsoleError
|
||||
from typing import Any, Dict, Generator, List, Optional
|
||||
from urllib.parse import urlparse, urlencode, quote, unquote
|
||||
|
||||
|
||||
def str_bool(b):
|
||||
def str_bool(b: bool) -> str:
|
||||
"""Convert boolean to string, in the way expected by the API."""
|
||||
return "true" if b else "false"
|
||||
|
||||
|
||||
def str_bool_nullable(b):
|
||||
def str_bool_nullable(b: Optional[bool]) -> Optional[str]:
|
||||
"""Similar to str_bool, but leave None as None"""
|
||||
return None if b is None else str_bool(b)
|
||||
|
||||
|
@ -34,7 +30,7 @@ def parse_html(html: str) -> BeautifulSoup:
|
|||
return BeautifulSoup(html.replace("'", "'"), "html.parser")
|
||||
|
||||
|
||||
def get_text(html):
|
||||
def get_text(html: str) -> str:
|
||||
"""Converts html to text, strips all tags."""
|
||||
text = parse_html(html).get_text()
|
||||
return unicodedata.normalize("NFKC", text)
|
||||
|
@ -53,7 +49,7 @@ def html_to_paragraphs(html: str) -> List[List[str]]:
|
|||
return [[get_text(line) for line in p] for p in paragraphs]
|
||||
|
||||
|
||||
def format_content(content):
|
||||
def format_content(content: str) -> Generator[str, None, None]:
|
||||
"""Given a Status contents in HTML, converts it into lines of plain text.
|
||||
|
||||
Returns a generator yielding lines of content.
|
||||
|
@ -73,25 +69,12 @@ def format_content(content):
|
|||
first = False
|
||||
|
||||
|
||||
def domain_exists(name):
|
||||
try:
|
||||
socket.gethostbyname(name)
|
||||
return True
|
||||
except OSError:
|
||||
return False
|
||||
|
||||
|
||||
def assert_domain_exists(domain):
|
||||
if not domain_exists(domain):
|
||||
raise ConsoleError("Domain {} not found".format(domain))
|
||||
|
||||
|
||||
EOF_KEY = "Ctrl-Z" if os.name == 'nt' else "Ctrl-D"
|
||||
|
||||
|
||||
def multiline_input():
|
||||
def multiline_input() -> str:
|
||||
"""Lets user input multiple lines of text, terminated by EOF."""
|
||||
lines = []
|
||||
lines: List[str] = []
|
||||
while True:
|
||||
try:
|
||||
lines.append(input())
|
||||
|
|
|
@ -4,7 +4,7 @@ import os
|
|||
from datetime import datetime, timezone
|
||||
|
||||
|
||||
def parse_datetime(value):
|
||||
def parse_datetime(value: str) -> datetime:
|
||||
"""Returns an aware datetime in local timezone"""
|
||||
dttm = datetime.strptime(value, "%Y-%m-%dT%H:%M:%S.%f%z")
|
||||
|
||||
|
|
Ładowanie…
Reference in New Issue