forked from hkc/mastoposter
359 lines
9.5 KiB
Python
359 lines
9.5 KiB
Python
from dataclasses import dataclass, field, fields
|
|
from datetime import datetime
|
|
from typing import Any, Callable, Optional, List, Literal, TypeVar
|
|
|
|
from bs4 import BeautifulSoup
|
|
|
|
from mastoposter.utils import node_to_html, node_to_markdown, node_to_plaintext
|
|
|
|
|
|
def _date(val: str) -> datetime:
|
|
return datetime.fromisoformat(val.rstrip("Z"))
|
|
|
|
|
|
T = TypeVar("T")
|
|
|
|
|
|
def _fnil(fn: Callable[[Any], T], val: Optional[Any]) -> Optional[T]:
|
|
return None if val is None else fn(val)
|
|
|
|
|
|
def _date_or_none(val: Optional[str]) -> Optional[datetime]:
|
|
return _fnil(_date, val)
|
|
|
|
|
|
def _int_or_none(val: Optional[str]) -> Optional[int]:
|
|
return _fnil(int, val)
|
|
|
|
|
|
@dataclass
|
|
class Field:
|
|
name: str
|
|
value: str
|
|
verified_at: Optional[datetime] = None
|
|
|
|
@classmethod
|
|
def from_dict(cls, data: dict) -> "Field":
|
|
return cls(
|
|
name=data["name"],
|
|
value=data["value"],
|
|
verified_at=_date_or_none(data.get("verified_at")),
|
|
)
|
|
|
|
|
|
@dataclass
|
|
class Emoji:
|
|
shortcode: str
|
|
url: str
|
|
static_url: str
|
|
visible_in_picker: bool
|
|
category: Optional[str] = None
|
|
|
|
@classmethod
|
|
def from_dict(cls, data: dict) -> "Emoji":
|
|
return cls(
|
|
**{f.name: data[f.name] for f in fields(cls) if f.name in data}
|
|
)
|
|
|
|
|
|
@dataclass
|
|
class 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
|
|
emojis: List[Emoji]
|
|
discoverable: bool
|
|
created_at: datetime
|
|
last_status_at: datetime
|
|
statuses_count: int
|
|
followers_count: int
|
|
following_count: int
|
|
moved: Optional["Account"] = None
|
|
fields: Optional[List[Field]] = None
|
|
bot: Optional[bool] = None
|
|
|
|
@classmethod
|
|
def from_dict(cls, data: dict) -> "Account":
|
|
return cls(
|
|
id=data["id"],
|
|
username=data["username"],
|
|
acct=data["acct"],
|
|
url=data["url"],
|
|
display_name=data["display_name"],
|
|
note=data["note"],
|
|
avatar=data["avatar"],
|
|
avatar_static=data["avatar_static"],
|
|
header=data["header"],
|
|
header_static=data["header_static"],
|
|
locked=data["locked"],
|
|
emojis=list(map(Emoji.from_dict, data["emojis"])),
|
|
discoverable=data.get("discoverable", False),
|
|
created_at=_date(data["created_at"]),
|
|
last_status_at=_date(data["last_status_at"]),
|
|
statuses_count=data["statuses_count"],
|
|
followers_count=data["followers_count"],
|
|
following_count=data["following_count"],
|
|
moved=_fnil(Account.from_dict, data.get("moved")),
|
|
fields=list(map(Field.from_dict, data.get("fields", []))),
|
|
bot=bool(data.get("bot")),
|
|
)
|
|
|
|
@property
|
|
def name(self) -> str:
|
|
return self.display_name or self.username
|
|
|
|
@property
|
|
def name_emojiless(self) -> str:
|
|
if not self.display_name:
|
|
return self.username
|
|
name = self.display_name
|
|
for emoji in self.emojis:
|
|
name = name.replace(":%s:" % emoji.shortcode, "")
|
|
return name.strip() or self.username
|
|
|
|
|
|
@dataclass
|
|
class AttachmentMetaImage:
|
|
@dataclass
|
|
class Vec2F:
|
|
x: float
|
|
y: float
|
|
|
|
@dataclass
|
|
class AttachmentMetaImageDimensions:
|
|
width: int
|
|
height: int
|
|
size: str
|
|
aspect: float
|
|
|
|
original: AttachmentMetaImageDimensions
|
|
small: AttachmentMetaImageDimensions
|
|
focus: Vec2F
|
|
|
|
@classmethod
|
|
def from_dict(cls, data: dict) -> "AttachmentMetaImage":
|
|
return cls(
|
|
**{f.name: data[f.name] for f in fields(cls) if f.name in data},
|
|
original=cls.AttachmentMetaImageDimensions(**data["original"]),
|
|
small=cls.AttachmentMetaImageDimensions(**data["small"]),
|
|
focus=cls.Vec2F(**data["focus"])
|
|
)
|
|
|
|
|
|
@dataclass
|
|
class AttachmentMetaVideo:
|
|
@dataclass
|
|
class AttachmentMetaVideoOriginal:
|
|
width: int
|
|
height: int
|
|
duration: float
|
|
bitrate: int
|
|
frame_rate: Optional[str] # XXX Gargron wtf?
|
|
|
|
@dataclass
|
|
class AttachmentMetaVideoSmall:
|
|
width: int
|
|
height: int
|
|
size: str
|
|
aspect: float
|
|
|
|
length: str
|
|
duration: float
|
|
fps: int
|
|
size: str
|
|
width: int
|
|
height: int
|
|
aspect: float
|
|
audio_encode: str
|
|
audio_bitrate: str # XXX GARGROOOOONNNNNN!!!!!!!
|
|
audio_channels: str # XXX I HATE YOU
|
|
original: AttachmentMetaVideoOriginal
|
|
small: AttachmentMetaVideoSmall
|
|
|
|
@classmethod
|
|
def from_dict(cls, data: dict) -> "AttachmentMetaVideo":
|
|
return cls(
|
|
**data,
|
|
original=cls.AttachmentMetaVideoOriginal(**data["original"]),
|
|
small=cls.AttachmentMetaVideoSmall(**data["small"])
|
|
)
|
|
|
|
|
|
@dataclass
|
|
class Attachment:
|
|
id: str
|
|
type: Literal["unknown", "image", "gifv", "video", "audio"]
|
|
url: str
|
|
preview_url: str
|
|
remote_url: Optional[str] = None
|
|
preview_remote_url: Optional[str] = None
|
|
meta: Optional[dict] = None
|
|
description: Optional[str] = None
|
|
blurhash: Optional[str] = None
|
|
text_url: Optional[str] = None # XXX: DEPRECATED
|
|
|
|
@classmethod
|
|
def from_dict(cls, data: dict) -> "Attachment":
|
|
return cls(
|
|
**{f.name: data[f.name] for f in fields(cls) if f.name in data}
|
|
)
|
|
|
|
|
|
@dataclass
|
|
class Application:
|
|
name: str
|
|
website: Optional[str] = None
|
|
vapid_key: Optional[str] = None
|
|
|
|
@classmethod
|
|
def from_dict(cls, data: dict) -> "Application":
|
|
return cls(
|
|
**{f.name: data[f.name] for f in fields(cls) if f.name in data}
|
|
)
|
|
|
|
|
|
@dataclass
|
|
class Mention:
|
|
id: str
|
|
username: str
|
|
acct: str
|
|
url: str
|
|
|
|
@classmethod
|
|
def from_dict(cls, data: dict) -> "Mention":
|
|
return cls(
|
|
**{f.name: data[f.name] for f in fields(cls) if f.name in data}
|
|
)
|
|
|
|
|
|
@dataclass
|
|
class Tag:
|
|
name: str
|
|
url: str
|
|
|
|
@classmethod
|
|
def from_dict(cls, data: dict) -> "Tag":
|
|
return cls(
|
|
**{f.name: data[f.name] for f in fields(cls) if f.name in data}
|
|
)
|
|
|
|
|
|
@dataclass
|
|
class Poll:
|
|
@dataclass
|
|
class PollOption:
|
|
title: str
|
|
votes_count: Optional[int] = None
|
|
|
|
id: str
|
|
expires_at: Optional[datetime]
|
|
expired: bool
|
|
multiple: bool
|
|
votes_count: int
|
|
voters_count: Optional[int] = None
|
|
options: List[PollOption] = field(default_factory=list)
|
|
emojis: List[Emoji] = field(default_factory=list)
|
|
|
|
@classmethod
|
|
def from_dict(cls, data: dict) -> "Poll":
|
|
return cls(
|
|
id=data["id"],
|
|
expires_at=_date_or_none(data.get("expires_at")),
|
|
expired=data["expired"],
|
|
multiple=data["multiple"],
|
|
votes_count=data["votes_count"],
|
|
voters_count=_int_or_none(data.get("voters_count")),
|
|
options=[cls.PollOption(**opt) for opt in data["options"]],
|
|
)
|
|
|
|
|
|
@dataclass
|
|
class Status:
|
|
id: str
|
|
uri: str
|
|
created_at: datetime
|
|
account: Account
|
|
content: str
|
|
visibility: Literal["public", "unlisted", "private", "direct"]
|
|
sensitive: bool
|
|
spoiler_text: str
|
|
media_attachments: List[Attachment]
|
|
reblogs_count: int
|
|
favourites_count: int
|
|
replies_count: int
|
|
mentions: List[Mention]
|
|
tags: List[Tag]
|
|
application: Optional[Application] = None
|
|
url: Optional[str] = None
|
|
in_reply_to_id: Optional[str] = None
|
|
in_reply_to_account_id: Optional[str] = None
|
|
reblog: Optional["Status"] = None
|
|
poll: Optional[Poll] = None
|
|
card: Optional[dict] = None
|
|
language: Optional[str] = None
|
|
text: Optional[str] = None
|
|
|
|
@classmethod
|
|
def from_dict(cls, data: dict) -> "Status":
|
|
return cls(
|
|
id=data["id"],
|
|
uri=data["uri"],
|
|
created_at=_date(data["created_at"]),
|
|
account=Account.from_dict(data["account"]),
|
|
content=data["content"],
|
|
visibility=data["visibility"],
|
|
sensitive=data["sensitive"],
|
|
spoiler_text=data["spoiler_text"],
|
|
media_attachments=list(
|
|
map(Attachment.from_dict, data["media_attachments"])
|
|
),
|
|
application=_fnil(Application.from_dict, data.get("application")),
|
|
reblogs_count=data["reblogs_count"],
|
|
favourites_count=data["favourites_count"],
|
|
replies_count=data["replies_count"],
|
|
url=data.get("url"),
|
|
in_reply_to_id=data.get("in_reply_to_id"),
|
|
in_reply_to_account_id=data.get("in_reply_to_account_id"),
|
|
reblog=_fnil(Status.from_dict, data.get("reblog")),
|
|
poll=_fnil(Poll.from_dict, data.get("poll")),
|
|
card=data.get("card"),
|
|
language=data.get("language"),
|
|
text=data.get("text"),
|
|
mentions=[Mention.from_dict(m) for m in data.get("mentions", [])],
|
|
tags=[Tag.from_dict(m) for m in data.get("tags", [])],
|
|
)
|
|
|
|
@property
|
|
def reblog_or_status(self) -> "Status":
|
|
return self.reblog or self
|
|
|
|
@property
|
|
def link(self) -> str:
|
|
return self.account.url + "/" + str(self.id)
|
|
|
|
@property
|
|
def content_flathtml(self) -> str:
|
|
return node_to_html(
|
|
BeautifulSoup(self.content, features="lxml")
|
|
).rstrip()
|
|
|
|
@property
|
|
def content_markdown(self) -> str:
|
|
return node_to_markdown(
|
|
BeautifulSoup(self.content, features="lxml")
|
|
).rstrip()
|
|
|
|
@property
|
|
def content_plaintext(self) -> str:
|
|
return node_to_plaintext(
|
|
BeautifulSoup(self.content, features="lxml")
|
|
).rstrip()
|