mastoposter-oss_images/mastoposter/types.py

373 lines
10 KiB
Python
Raw Normal View History

"""
mastoposter - configurable reposter from Mastodon-compatible Fediverse servers
Copyright (C) 2022-2023 hatkidchan <hatkidchan@gmail.com>
This program is free software; you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation; either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
"""
from dataclasses import dataclass, field, fields
2022-08-24 08:09:41 +03:00
from datetime import datetime
from typing import Any, Callable, Optional, List, Literal, TypeVar
2022-08-31 16:19:39 +03:00
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)
2022-08-24 08:09:41 +03:00
@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")),
2022-08-24 08:09:41 +03:00
)
@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":
2022-09-14 20:37:51 +03:00
return cls(
**{f.name: data[f.name] for f in fields(cls) if f.name in data}
)
2022-08-24 08:09:41 +03:00
@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: Optional[datetime]
2022-08-24 08:09:41 +03:00
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_or_none(data.get("last_status_at")),
2022-08-24 08:09:41 +03:00
statuses_count=data["statuses_count"],
followers_count=data["followers_count"],
following_count=data["following_count"],
moved=_fnil(Account.from_dict, data.get("moved")),
2022-08-24 08:09:41 +03:00
fields=list(map(Field.from_dict, data.get("fields", []))),
bot=bool(data.get("bot")),
)
2022-08-31 16:19:39 +03:00
@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
2022-08-24 08:09:41 +03:00
@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(
2022-09-14 20:37:51 +03:00
**{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"]),
2023-05-08 17:35:16 +03:00
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"]),
2023-05-08 17:35:16 +03:00
small=cls.AttachmentMetaVideoSmall(**data["small"]),
)
2022-08-24 08:09:41 +03:00
@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":
2022-09-14 20:37:51 +03:00
return cls(
**{f.name: data[f.name] for f in fields(cls) if f.name in data}
)
2022-08-24 08:09:41 +03:00
@dataclass
class Application:
name: str
website: Optional[str] = None
vapid_key: Optional[str] = None
@classmethod
def from_dict(cls, data: dict) -> "Application":
2022-09-14 20:37:51 +03:00
return cls(
**{f.name: data[f.name] for f in fields(cls) if f.name in data}
)
2022-08-24 08:09:41 +03:00
@dataclass
class Mention:
id: str
username: str
acct: str
url: str
@classmethod
def from_dict(cls, data: dict) -> "Mention":
2022-09-14 20:37:51 +03:00
return cls(
**{f.name: data[f.name] for f in fields(cls) if f.name in data}
)
2022-08-24 08:09:41 +03:00
@dataclass
class Tag:
name: str
url: str
@classmethod
def from_dict(cls, data: dict) -> "Tag":
2022-09-14 20:37:51 +03:00
return cls(
**{f.name: data[f.name] for f in fields(cls) if f.name in data}
)
2022-08-24 08:09:41 +03:00
@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"]],
)
2022-08-24 08:09:41 +03:00
@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
2022-08-27 16:16:36 +03:00
mentions: List[Mention]
2022-08-28 01:05:14 +03:00
tags: List[Tag]
2022-08-24 08:09:41 +03:00
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
2022-08-24 08:09:41 +03:00
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"]),
2022-08-24 08:09:41 +03:00
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")),
2022-08-24 08:09:41 +03:00
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")),
2022-08-24 08:09:41 +03:00
card=data.get("card"),
language=data.get("language"),
text=data.get("text"),
2022-08-27 16:16:36 +03:00
mentions=[Mention.from_dict(m) for m in data.get("mentions", [])],
2022-08-28 01:05:14 +03:00
tags=[Tag.from_dict(m) for m in data.get("tags", [])],
2022-08-24 08:09:41 +03:00
)
@property
def reblog_or_status(self) -> "Status":
return self.reblog or self
2022-08-24 08:09:41 +03:00
@property
def link(self) -> str:
return self.account.url + "/" + str(self.id)
2022-08-31 16:19:39 +03:00
@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()