social media crossposting tool. 3rd time's the charm
mastodon misskey crossposting bluesky
at next 4.0 kB view raw
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 normalize_service_url 9 10 11def validate_and_transform(data: dict[str, Any]): 12 if "token" not in data or "instance" not in data: 13 raise KeyError("Missing required values 'token' or 'instance'") 14 15 data["instance"] = normalize_service_url(data["instance"]) 16 17 18@dataclass(kw_only=True) 19class InstanceInfo: 20 max_characters: int = 500 21 max_media_attachments: int = 4 22 characters_reserved_per_url: int = 23 23 24 image_size_limit: int = 16777216 25 video_size_limit: int = 103809024 26 27 text_format: str = "text/plain" 28 29 @classmethod 30 def from_api(cls, data: dict[str, Any]) -> "InstanceInfo": 31 config: dict[str, Any] = {} 32 33 if "statuses" in data: 34 statuses_config: dict[str, Any] = data.get("statuses", {}) 35 if "max_characters" in statuses_config: 36 config["max_characters"] = statuses_config["max_characters"] 37 if "max_media_attachments" in statuses_config: 38 config["max_media_attachments"] = statuses_config[ 39 "max_media_attachments" 40 ] 41 if "characters_reserved_per_url" in statuses_config: 42 config["characters_reserved_per_url"] = statuses_config[ 43 "characters_reserved_per_url" 44 ] 45 46 # glitch content type 47 if "supported_mime_types" in statuses_config: 48 text_mimes: list[str] = statuses_config["supported_mime_types"] 49 50 if "text/x.misskeymarkdown" in text_mimes: 51 config["text_format"] = "text/x.misskeymarkdown" 52 elif "text/markdown" in text_mimes: 53 config["text_format"] = "text/markdown" 54 55 if "media_attachments" in data: 56 media_config: dict[str, Any] = data["media_attachments"] 57 if "image_size_limit" in media_config: 58 config["image_size_limit"] = media_config["image_size_limit"] 59 if "video_size_limit" in media_config: 60 config["video_size_limit"] = media_config["video_size_limit"] 61 62 # *oma extensions 63 if "max_toot_chars" in data: 64 config["max_characters"] = data["max_toot_chars"] 65 if "upload_limit" in data: 66 config["image_size_limit"] = data["upload_limit"] 67 config["video_size_limit"] = data["upload_limit"] 68 69 if "pleroma" in data: 70 pleroma: dict[str, Any] = data["pleroma"] 71 if "metadata" in pleroma: 72 metadata: dict[str, Any] = pleroma["metadata"] 73 if "post_formats" in metadata: 74 post_formats: list[str] = metadata["post_formats"] 75 76 if "text/x.misskeymarkdown" in post_formats: 77 config["text_format"] = "text/x.misskeymarkdown" 78 elif "text/markdown" in post_formats: 79 config["text_format"] = "text/markdown" 80 81 return InstanceInfo(**config) 82 83 84class MastodonService(ABC, Service): 85 def verify_credentials(self): 86 token = self._get_token() 87 response = requests.get( 88 f"{self.url}/api/v1/accounts/verify_credentials", 89 headers={"Authorization": f"Bearer {token}"}, 90 ) 91 if response.status_code != 200: 92 self.log.error("Failed to validate user credentials!") 93 response.raise_for_status() 94 return dict(response.json()) 95 96 def fetch_instance_info(self): 97 token = self._get_token() 98 responce = requests.get( 99 f"{self.url}/api/v1/instance", 100 headers={"Authorization": f"Bearer {token}"}, 101 ) 102 if responce.status_code != 200: 103 self.log.error("Failed to get instance info!") 104 responce.raise_for_status() 105 return dict(responce.json()) 106 107 @abstractmethod 108 def _get_token(self) -> str: 109 pass