mirror of
https://github.com/ihabunek/toot.git
synced 2024-09-29 04:35:54 -04:00
wip
This commit is contained in:
parent
53ea989eea
commit
fd12591ee2
@ -9,7 +9,7 @@ from toot.exceptions import ApiError
|
|||||||
|
|
||||||
from .compose import StatusComposer
|
from .compose import StatusComposer
|
||||||
from .constants import PALETTE
|
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 ExceptionStackTrace, GotoMenu, Help, StatusSource, StatusLinks, StatusZoom
|
||||||
from .overlays import StatusDeleteConfirmation, Account
|
from .overlays import StatusDeleteConfirmation, Account
|
||||||
from .timeline import Timeline
|
from .timeline import Timeline
|
||||||
@ -258,8 +258,9 @@ class TUI(urwid.Frame):
|
|||||||
return timeline
|
return timeline
|
||||||
|
|
||||||
def make_status(self, status_data):
|
def make_status(self, status_data):
|
||||||
is_mine = self.user.username == status_data["account"]["acct"]
|
return from_dict(Status, status_data)
|
||||||
return Status(status_data, is_mine, self.app.instance)
|
# is_mine = self.user.username == status_data["account"]["acct"]
|
||||||
|
# return Status(status_data, is_mine, self.app.instance)
|
||||||
|
|
||||||
def show_thread(self, status):
|
def show_thread(self, status):
|
||||||
def _close(*args):
|
def _close(*args):
|
||||||
@ -379,14 +380,15 @@ class TUI(urwid.Frame):
|
|||||||
def clear_screen(self):
|
def clear_screen(self):
|
||||||
self.loop.screen.clear()
|
self.loop.screen.clear()
|
||||||
|
|
||||||
def show_links(self, status):
|
def show_links(self, status: Status):
|
||||||
links = parse_content_links(status.data["content"]) if status else []
|
links = parse_content_links(status.content)
|
||||||
post_attachments = status.data["media_attachments"] or []
|
post_attachments = status.media_attachments
|
||||||
reblog_attachments = (status.data["reblog"]["media_attachments"] if status.data["reblog"] else None) or []
|
reblog_attachments = status.reblog.media_attachments if status.reblog else []
|
||||||
|
|
||||||
for a in post_attachments + reblog_attachments:
|
for attachment in post_attachments + reblog_attachments:
|
||||||
url = a["remote_url"] or a["url"]
|
url = attachment.remote_url or attachment.url
|
||||||
links.append((url, a["description"] if a["description"] else url))
|
description = attachment.description if attachment.description else url
|
||||||
|
links.append((url, description))
|
||||||
|
|
||||||
def _clear(*args):
|
def _clear(*args):
|
||||||
self.clear_screen()
|
self.clear_screen()
|
||||||
@ -536,7 +538,7 @@ class TUI(urwid.Frame):
|
|||||||
done_callback=_done
|
done_callback=_done
|
||||||
)
|
)
|
||||||
|
|
||||||
def async_toggle_reblog(self, timeline, status):
|
def async_toggle_reblog(self, timeline: Timeline, status: Status):
|
||||||
def _reblog():
|
def _reblog():
|
||||||
logger.info("Reblogging {}".format(status))
|
logger.info("Reblogging {}".format(status))
|
||||||
api.reblog(self.app, self.user, status.id, visibility=get_default_visibility())
|
api.reblog(self.app, self.user, status.id, visibility=get_default_visibility())
|
||||||
|
@ -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://docs.joinmastodon.org/entities/Account/#Field
|
||||||
|
|
||||||
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.
|
|
||||||
"""
|
"""
|
||||||
|
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
|
@dataclass
|
||||||
Whether the status was created by the logged in user.
|
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
|
@dataclass
|
||||||
self.is_mine = is_mine
|
class Account:
|
||||||
self.default_instance = default_instance
|
"""
|
||||||
|
https://docs.joinmastodon.org/entities/Account/
|
||||||
# This can be toggled by the user
|
"""
|
||||||
self.show_sensitive = False
|
id: str
|
||||||
|
username: str
|
||||||
# Set when status is translated
|
acct: str
|
||||||
self.show_translation = False
|
url: str
|
||||||
self.translation = None
|
display_name: str
|
||||||
self.translated_from = None
|
note: str
|
||||||
|
avatar: str
|
||||||
# TODO: clean up
|
avatar_static: str
|
||||||
self.id = self.data["id"]
|
header: str
|
||||||
self.account = self._get_account()
|
header_static: str
|
||||||
self.created_at = parse_datetime(data["created_at"])
|
locked: bool
|
||||||
self.author = self._get_author()
|
fields: List[AccountField]
|
||||||
self.favourited = data.get("favourited", False)
|
emojis: List[CustomEmoji]
|
||||||
self.reblogged = data.get("reblogged", False)
|
bot: bool
|
||||||
self.bookmarked = data.get("bookmarked", False)
|
group: bool
|
||||||
self.in_reply_to = data.get("in_reply_to_id")
|
discoverable: Optional[bool]
|
||||||
self.url = data.get("url")
|
noindex: Optional[bool]
|
||||||
self.mentions = data.get("mentions")
|
moved: Optional["Account"]
|
||||||
self.reblog = self._get_reblog()
|
suspended: Optional[bool]
|
||||||
self.visibility = data.get("visibility")
|
limited: Optional[bool]
|
||||||
|
created_at: datetime
|
||||||
|
last_status_at: Optional[date]
|
||||||
|
statuses_count: int
|
||||||
|
followers_count: int
|
||||||
|
following_count: int
|
||||||
|
|
||||||
@property
|
@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
|
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 (
|
@dataclass
|
||||||
self.data["account"]["acct"] == reblog["account"]["acct"]
|
class InstanceV2:
|
||||||
)
|
domain: str
|
||||||
return Status(reblog, reblog_is_mine, self.default_instance)
|
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):
|
# Generic data class instance
|
||||||
acct = self.data['account']['acct']
|
T = TypeVar("T")
|
||||||
return acct if "@" in acct else "{}@{}".format(acct, self.default_instance)
|
|
||||||
|
|
||||||
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
90
toot/tui/old_entities.py
Normal 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)
|
@ -273,7 +273,7 @@ class Timeline(urwid.Columns):
|
|||||||
index = self.get_status_index(status.id)
|
index = self.get_status_index(status.id)
|
||||||
self.status_list.body.set_focus(index)
|
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."""
|
"""Overwrite status in list with the new instance and redraw."""
|
||||||
index = self.get_status_index(status.id)
|
index = self.get_status_index(status.id)
|
||||||
assert self.statuses[index].id == status.id # Sanity check
|
assert self.statuses[index].id == status.id # Sanity check
|
||||||
|
Loading…
Reference in New Issue
Block a user