1
0
mirror of https://github.com/ihabunek/toot.git synced 2024-06-23 06:25:26 +00:00
This commit is contained in:
Ivan Habunek 2023-02-14 11:47:23 +01:00
parent 53ea989eea
commit fd12591ee2
No known key found for this signature in database
GPG Key ID: F5F0623FF5EBCB3D
4 changed files with 370 additions and 84 deletions

View File

@ -9,7 +9,7 @@ from toot.exceptions import ApiError
from .compose import StatusComposer
from .constants import PALETTE
from .entities import Status
from .entities import Status, from_dict
from .overlays import ExceptionStackTrace, GotoMenu, Help, StatusSource, StatusLinks, StatusZoom
from .overlays import StatusDeleteConfirmation, Account
from .timeline import Timeline
@ -258,8 +258,9 @@ class TUI(urwid.Frame):
return timeline
def make_status(self, status_data):
is_mine = self.user.username == status_data["account"]["acct"]
return Status(status_data, is_mine, self.app.instance)
return from_dict(Status, status_data)
# is_mine = self.user.username == status_data["account"]["acct"]
# return Status(status_data, is_mine, self.app.instance)
def show_thread(self, status):
def _close(*args):
@ -379,14 +380,15 @@ class TUI(urwid.Frame):
def clear_screen(self):
self.loop.screen.clear()
def show_links(self, status):
links = parse_content_links(status.data["content"]) if status else []
post_attachments = status.data["media_attachments"] or []
reblog_attachments = (status.data["reblog"]["media_attachments"] if status.data["reblog"] else None) or []
def show_links(self, status: Status):
links = parse_content_links(status.content)
post_attachments = status.media_attachments
reblog_attachments = status.reblog.media_attachments if status.reblog else []
for a in post_attachments + reblog_attachments:
url = a["remote_url"] or a["url"]
links.append((url, a["description"] if a["description"] else url))
for attachment in post_attachments + reblog_attachments:
url = attachment.remote_url or attachment.url
description = attachment.description if attachment.description else url
links.append((url, description))
def _clear(*args):
self.clear_screen()
@ -536,7 +538,7 @@ class TUI(urwid.Frame):
done_callback=_done
)
def async_toggle_reblog(self, timeline, status):
def async_toggle_reblog(self, timeline: Timeline, status: Status):
def _reblog():
logger.info("Reblogging {}".format(status))
api.reblog(self.app, self.user, status.id, visibility=get_default_visibility())

View File

@ -1,90 +1,284 @@
from collections import namedtuple
import dataclasses
from .utils import parse_datetime
from dataclasses import dataclass, is_dataclass
from datetime import date, datetime
from typing import Dict, List, Optional, Type, TypeVar, Union
from typing import get_type_hints
Author = namedtuple("Author", ["account", "display_name", "username"])
from toot.typing_compat import get_args, get_origin
from toot.utils import get_text
class Status:
@dataclass
class AccountField:
"""
A wrapper around the Status entity data fetched from Mastodon.
https://github.com/tootsuite/documentation/blob/master/Using-the-API/API.md#status
Attributes
----------
reblog : Status or None
The reblogged status if it exists.
original : Status
If a reblog, the reblogged status, otherwise self.
https://docs.joinmastodon.org/entities/Account/#Field
"""
name: str
value: str
verified_at: Optional[datetime]
def __init__(self, data, is_mine, default_instance):
"""
Parameters
----------
data : dict
Status data as received from Mastodon.
https://docs.joinmastodon.org/api/entities/#status
is_mine : bool
Whether the status was created by the logged in user.
@dataclass
class CustomEmoji:
"""
https://docs.joinmastodon.org/entities/CustomEmoji/
"""
shortcode: str
url: str
static_url: str
visible_in_picker: bool
category: str
default_instance : str
The domain of the instance into which the user is logged in. Used to
create fully qualified account names for users on the same instance.
Mastodon only populates the name, not the domain.
"""
self.data = data
self.is_mine = is_mine
self.default_instance = default_instance
# This can be toggled by the user
self.show_sensitive = False
# Set when status is translated
self.show_translation = False
self.translation = None
self.translated_from = None
# TODO: clean up
self.id = self.data["id"]
self.account = self._get_account()
self.created_at = parse_datetime(data["created_at"])
self.author = self._get_author()
self.favourited = data.get("favourited", False)
self.reblogged = data.get("reblogged", False)
self.bookmarked = data.get("bookmarked", False)
self.in_reply_to = data.get("in_reply_to_id")
self.url = data.get("url")
self.mentions = data.get("mentions")
self.reblog = self._get_reblog()
self.visibility = data.get("visibility")
@dataclass
class Account:
"""
https://docs.joinmastodon.org/entities/Account/
"""
id: str
username: str
acct: str
url: str
display_name: str
note: str
avatar: str
avatar_static: str
header: str
header_static: str
locked: bool
fields: List[AccountField]
emojis: List[CustomEmoji]
bot: bool
group: bool
discoverable: Optional[bool]
noindex: Optional[bool]
moved: Optional["Account"]
suspended: Optional[bool]
limited: Optional[bool]
created_at: datetime
last_status_at: Optional[date]
statuses_count: int
followers_count: int
following_count: int
@property
def original(self):
def note_plaintext(self) -> str:
return get_text(self.note)
@dataclass
class Application:
name: str
website: Optional[str]
@dataclass
class MediaAttachment:
id: str
type: str
url: str
preview_url: str
remote_url: Optional[str]
meta: dict
description: str
blurhash: str
@dataclass
class StatusMention:
"""
https://docs.joinmastodon.org/entities/Status/#Mention
"""
id: str
username: str
url: str
acct: str
@dataclass
class StatusTag:
"""
https://docs.joinmastodon.org/entities/Status/#Tag
"""
name: str
url: str
@dataclass
class PollOption:
"""
https://docs.joinmastodon.org/entities/Poll/#Option
"""
title: str
votes_count: Optional[int]
@dataclass
class Poll:
"""
https://docs.joinmastodon.org/entities/Poll/
"""
id: str
expires_at: Optional[datetime]
expired: bool
multiple: bool
votes_count: int
voters_count: Optional[int]
options: List[PollOption]
emojis: List[CustomEmoji]
voted: Optional[bool]
own_votes: Optional[List[int]]
@dataclass
class PreviewCard:
url: str
title: str
description: str
type: str
author_name: str
author_url: str
provider_name: str
provider_url: str
html: str
width: int
height: int
image: Optional[str]
embed_url: str
blurhash: Optional[str]
@dataclass
class FilterKeyword:
id: str
keyword: str
whole_word: str
@dataclass
class FilterStatus:
id: str
status_id: str
@dataclass
class Filter:
id: str
title: str
context: List[str]
expires_at: Optional[datetime]
filter_action: str
keywords: List[FilterKeyword]
statuses: List[FilterStatus]
@dataclass
class FilterResult:
filter: Filter
keyword_matches: Optional[List[str]]
status_matches: Optional[str]
@dataclass
class Status:
id: str
uri: str
created_at: datetime
account: Account
content: str
visibility: str
sensitive: bool
spoiler_text: str
media_attachments: List[MediaAttachment]
application: Optional[Application]
mentions: List[StatusMention]
tags: List[StatusTag]
emojis: List[CustomEmoji]
reblogs_count: int
favourites_count: int
replies_count: int
url: Optional[str]
in_reply_to_id: Optional[str]
in_reply_to_account_id: Optional[str]
reblog: Optional["Status"]
poll: Optional[Poll]
card: Optional[PreviewCard]
language: Optional[str]
text: Optional[str]
edited_at: Optional[datetime]
favourited: Optional[bool]
reblogged: Optional[bool]
muted: Optional[bool]
bookmarked: Optional[bool]
pinned: Optional[bool]
filtered: Optional[List[FilterResult]]
@property
def original(self) -> "Status":
return self.reblog or self
def _get_reblog(self):
reblog = self.data.get("reblog")
if not reblog:
return None
reblog_is_mine = self.is_mine and (
self.data["account"]["acct"] == reblog["account"]["acct"]
)
return Status(reblog, reblog_is_mine, self.default_instance)
@dataclass
class InstanceV2:
domain: str
title: str
version: str
source_url: str
description: str
usage: dict # TODO expand
thumbnail: dict # TODO expand
languages: List[str]
configuration: dict # TODO expand
registrations: dict # TODO expand
contact: dict # TODO expand
rules: List[dict] # TODO expand
def _get_author(self):
acct = self.data['account']['acct']
acct = acct if "@" in acct else "{}@{}".format(acct, self.default_instance)
return Author(acct, self.data['account']['display_name'], self.data['account']['username'])
def _get_account(self):
acct = self.data['account']['acct']
return acct if "@" in acct else "{}@{}".format(acct, self.default_instance)
# Generic data class instance
T = TypeVar("T")
def __repr__(self):
return "<Status id={} account={}>".format(self.id, self.account)
def from_dict(cls: Type[T], data: Dict) -> T:
def _fields():
hints = get_type_hints(cls)
for field in dataclasses.fields(cls):
default = field.default if field.default is not dataclasses.MISSING else None
field_type = prune_optional(hints[field.name])
value = data.get(field.name, default)
yield convert(field_type, value)
return cls(*_fields())
def convert(field_type, value):
if value is None:
return None
if field_type in [str, int, bool, dict]:
return value
if field_type == datetime:
return datetime.strptime(value, "%Y-%m-%dT%H:%M:%S.%f%z")
if field_type == date:
return date.fromisoformat(value)
if get_origin(field_type) == list:
(inner_type,) = get_args(field_type)
return [convert(inner_type, x) for x in value]
if is_dataclass(field_type):
return from_dict(field_type, value)
raise ValueError(f"Not implemented for type '{field_type}'")
def prune_optional(field_type):
"""For `Optional[<type>]` returnes the encapsulated `<type>`."""
if get_origin(field_type) == Union:
args = get_args(field_type)
if len(args) == 2 and args[1] == type(None):
return args[0]
return field_type

90
toot/tui/old_entities.py Normal file
View File

@ -0,0 +1,90 @@
from collections import namedtuple
from .utils import parse_datetime
Author = namedtuple("Author", ["account", "display_name", "username"])
class Status:
"""
A wrapper around the Status entity data fetched from Mastodon.
https://github.com/tootsuite/documentation/blob/master/Using-the-API/API.md#status
Attributes
----------
reblog : Status or None
The reblogged status if it exists.
original : Status
If a reblog, the reblogged status, otherwise self.
"""
def __init__(self, data, is_mine, default_instance):
"""
Parameters
----------
data : dict
Status data as received from Mastodon.
https://docs.joinmastodon.org/api/entities/#status
is_mine : bool
Whether the status was created by the logged in user.
default_instance : str
The domain of the instance into which the user is logged in. Used to
create fully qualified account names for users on the same instance.
Mastodon only populates the name, not the domain.
"""
self.data = data
self.is_mine = is_mine
self.default_instance = default_instance
# This can be toggled by the user
self.show_sensitive = False
# Set when status is translated
self.show_translation = False
self.translation = None
self.translated_from = None
# TODO: clean up
self.id = self.data["id"]
self.account = self._get_account()
self.created_at = parse_datetime(data["created_at"])
self.author = self._get_author()
self.favourited = data.get("favourited", False)
self.reblogged = data.get("reblogged", False)
self.bookmarked = data.get("bookmarked", False)
self.in_reply_to = data.get("in_reply_to_id")
self.url = data.get("url")
self.mentions = data.get("mentions")
self.reblog = self._get_reblog()
self.visibility = data.get("visibility")
@property
def original(self):
return self.reblog or self
def _get_reblog(self):
reblog = self.data.get("reblog")
if not reblog:
return None
reblog_is_mine = self.is_mine and (
self.data["account"]["acct"] == reblog["account"]["acct"]
)
return Status(reblog, reblog_is_mine, self.default_instance)
def _get_author(self):
acct = self.data['account']['acct']
acct = acct if "@" in acct else "{}@{}".format(acct, self.default_instance)
return Author(acct, self.data['account']['display_name'], self.data['account']['username'])
def _get_account(self):
acct = self.data['account']['acct']
return acct if "@" in acct else "{}@{}".format(acct, self.default_instance)
def __repr__(self):
return "<Status id={} account={}>".format(self.id, self.account)

View File

@ -273,7 +273,7 @@ class Timeline(urwid.Columns):
index = self.get_status_index(status.id)
self.status_list.body.set_focus(index)
def update_status(self, status):
def update_status(self, status: Status):
"""Overwrite status in list with the new instance and redraw."""
index = self.get_status_index(status.id)
assert self.statuses[index].id == status.id # Sanity check