mastoposter-oss_images/mastoposter/types.py

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()