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