social media crossposting tool. 3rd time's the charm
mastodon
misskey
crossposting
bluesky
1from abc import ABC, abstractmethod
2from dataclasses import dataclass
3from typing import Any
4
5import requests
6
7from cross.service import Service
8from util.util import LOGGER
9
10
11@dataclass(kw_only=True)
12class InstanceInfo:
13 max_characters: int = 500
14 max_media_attachments: int = 4
15 characters_reserved_per_url: int = 23
16
17 image_size_limit: int = 16777216
18 video_size_limit: int = 103809024
19
20 @classmethod
21 def from_api(cls, data: dict[str, Any]) -> "InstanceInfo":
22 config: dict[str, Any] = {}
23
24 if "statuses" in data:
25 statuses_config: dict[str, Any] = data.get("statuses", {})
26 if "max_characters" in statuses_config:
27 config["max_characters"] = statuses_config["max_characters"]
28 if "max_media_attachments" in statuses_config:
29 config["max_media_attachments"] = statuses_config[
30 "max_media_attachments"
31 ]
32 if "characters_reserved_per_url" in statuses_config:
33 config["characters_reserved_per_url"] = statuses_config[
34 "characters_reserved_per_url"
35 ]
36
37 if "media_attachments" in data:
38 media_config: dict[str, Any] = data.get("media_attachments", {})
39 if "image_size_limit" in media_config:
40 config["image_size_limit"] = media_config["image_size_limit"]
41 if "video_size_limit" in media_config:
42 config["video_size_limit"] = media_config["video_size_limit"]
43
44 # *oma extensions
45 if "max_toot_chars" in data:
46 config["max_characters"] = data["max_toot_chars"]
47 if "upload_limit" in data:
48 config["image_size_limit"] = data["upload_limit"]
49 config["video_size_limit"] = data["upload_limit"]
50
51 return InstanceInfo(**config)
52
53
54class MastodonService(ABC, Service):
55 def verify_credentials(self):
56 token = self._get_token()
57 responce = requests.get(
58 f"{self.url}/api/v1/accounts/verify_credentials",
59 headers={"Authorization": f"Bearer {token}"},
60 )
61 if responce.status_code != 200:
62 LOGGER.error("Failed to validate user credentials!")
63 responce.raise_for_status()
64 return dict(responce.json())
65
66 def fetch_instance_info(self):
67 token = self._get_token()
68 responce = requests.get(
69 f"{self.url}/api/v1/instance",
70 headers={"Authorization": f"Bearer {token}"},
71 )
72 if responce.status_code != 200:
73 LOGGER.error("Failed to get instance info!")
74 responce.raise_for_status()
75 return dict(responce.json())
76
77 @abstractmethod
78 def _get_token(self) -> str:
79 pass