social media crossposting tool. 3rd time's the charm
mastodon misskey crossposting bluesky

migrate stuff to PostRef

zenfyr.dev 19170b82 5acecfcf

verified
+67 -52
+8 -5
bluesky/input.py
··· 18 18 RemoteUrlAttachment, 19 19 ) 20 20 from cross.media import Blob, download_blob 21 - from cross.post import Post 21 + from cross.post import Post, PostRef 22 22 from cross.service import InputService 23 23 from database.connection import DatabasePool 24 24 from util.util import normalize_service_url ··· 77 77 return 78 78 79 79 tokens = tokenize_post(record["text"], record.get('facets', {})) 80 - post = Post(id=post_uri, parent_id=parent_uri, tokens=tokens) 80 + post = Post(id=post_uri, author=self.did, service=self.url, parent_id=parent_uri, tokens=tokens) 81 81 82 82 did, _, rid = AtUri.record_uri(post_uri) 83 83 post.attachments.put( ··· 194 194 } 195 195 ) 196 196 197 + repost_ref = PostRef(id=post_uri, author=self.did, service=self.url) 198 + reposted_ref = PostRef(id=reposted_uri, author=self.did, service=self.url) 197 199 for out in self.outputs: 198 - self.submitter(lambda: out.accept_repost(post_uri, reposted_uri)) 200 + self.submitter(lambda: out.accept_repost(repost_ref, reposted_ref)) 199 201 200 202 def _on_delete_post(self, post_id: str, repost: bool): 201 203 post = self._get_post(self.url, self.did, post_id) 202 204 if not post: 203 205 return 204 206 207 + post_ref = PostRef(id=post_id, author=self.did, service=self.url) 205 208 if repost: 206 209 for output in self.outputs: 207 - self.submitter(lambda: output.delete_repost(post_id)) 210 + self.submitter(lambda: output.delete_repost(post_ref)) 208 211 else: 209 212 for output in self.outputs: 210 - self.submitter(lambda: output.delete_post(post_id)) 213 + self.submitter(lambda: output.delete_post(post_ref)) 211 214 self._delete_post_by_id(post["id"]) 212 215 213 216
+7 -3
cross/post.py
··· 25 25 def __repr__(self) -> str: 26 26 return f"AttachmentKeeper(_map={self._map.values()})" 27 27 28 - 29 - @dataclass 30 - class Post: 28 + @dataclass(kw_only=True) 29 + class PostRef: 31 30 id: str 31 + author: str 32 + service: str 33 + 34 + @dataclass(kw_only=True) 35 + class Post(PostRef): 32 36 parent_id: str | None 33 37 tokens: list[Token] 34 38 text_type: str = "text/plain"
+9 -9
cross/service.py
··· 3 3 from abc import ABC, abstractmethod 4 4 from typing import Any, Callable, cast 5 5 6 - from cross.post import Post 6 + from cross.post import Post, PostRef 7 7 from database.connection import DatabasePool 8 8 9 9 columns: list[str] = [ ··· 132 132 133 133 134 134 class OutputService(Service): 135 - def accept_post(self, service: str, user: str, post: Post): 135 + def accept_post(self, post: Post): 136 136 self.log.warning("NOT IMPLEMENTED (%s), accept_post %s", self.url, post.id) 137 137 138 - def delete_post(self, service: str, user: str, post_id: str): 139 - self.log.warning("NOT IMPLEMENTED (%s), delete_post %s", self.url, post_id) 138 + def delete_post(self, post: PostRef): 139 + self.log.warning("NOT IMPLEMENTED (%s), delete_post %s", self.url, post.id) 140 140 141 - def accept_repost(self, service: str, user: str, repost_id: str, reposted_id: str): 141 + def accept_repost(self, repost: PostRef, reposted: PostRef): 142 142 self.log.warning( 143 143 "NOT IMPLEMENTED (%s), accept_repost %s of %s", 144 144 self.url, 145 - repost_id, 146 - reposted_id, 145 + repost.id, 146 + reposted.id, 147 147 ) 148 148 149 - def delete_repost(self, service: str, user: str, repost_id: str): 150 - self.log.warning("NOT IMPLEMENTED (%s), delete_repost %s", self.url, repost_id) 149 + def delete_repost(self, repost: PostRef): 150 + self.log.warning("NOT IMPLEMENTED (%s), delete_repost %s", self.url, repost.id) 151 151 152 152 153 153 class InputService(ABC, Service):
+8 -5
mastodon/input.py
··· 15 15 SensitiveAttachment, 16 16 ) 17 17 from cross.media import Blob, download_blob 18 - from cross.post import Post 18 + from cross.post import Post, PostRef 19 19 from cross.service import InputService 20 20 from database.connection import DatabasePool 21 21 from mastodon.info import MastodonService, validate_and_transform ··· 113 113 parser.feed(status["content"]) 114 114 tokens = parser.get_result() 115 115 116 - post = Post(id=status["id"], parent_id=in_reply, tokens=tokens) 116 + post = Post(id=status["id"], author=self.user_id, service=self.url, parent_id=in_reply, tokens=tokens) 117 117 118 118 if quote: 119 119 post.attachments.put(QuoteAttachment(quoted_id=quote['id'], quoted_user=self.user_id)) ··· 183 183 } 184 184 ) 185 185 186 + repost_ref = PostRef(id=status["id"], author=self.user_id, service=self.url) 187 + reposted_ref = PostRef(id=reblog["id"], author=self.user_id, service=self.url) 186 188 for out in self.outputs: 187 - self.submitter(lambda: out.accept_repost(status["id"], reblog["id"])) 189 + self.submitter(lambda: out.accept_repost(repost_ref, reposted_ref)) 188 190 189 191 def _on_delete_post(self, status_id: str): 190 192 post = self._get_post(self.url, self.user_id, status_id) 191 193 if not post: 192 194 return 193 195 196 + post_ref = PostRef(id=status_id, author=self.user_id, service=self.url) 194 197 if post["reposted_id"]: 195 198 for output in self.outputs: 196 - self.submitter(lambda: output.delete_repost(status_id)) 199 + self.submitter(lambda: output.delete_repost(post_ref)) 197 200 else: 198 201 for output in self.outputs: 199 - self.submitter(lambda: output.delete_post(status_id)) 202 + self.submitter(lambda: output.delete_post(post_ref)) 200 203 self._delete_post_by_id(post["id"]) 201 204 202 205 def _accept_msg(self, msg: websockets.Data) -> None:
+23 -19
mastodon/output.py
··· 9 9 RemoteUrlAttachment, 10 10 SensitiveAttachment, 11 11 ) 12 - from cross.post import Post 12 + from cross.post import Post, PostRef 13 13 from cross.service import OutputService 14 14 from database.connection import DatabasePool 15 15 from mastodon.info import InstanceInfo, MastodonService, validate_and_transform ··· 48 48 response = self.fetch_instance_info() 49 49 self.instance_info: InstanceInfo = InstanceInfo.from_api(response) 50 50 51 - def accept_post(self, service: str, user: str, post: Post): 51 + @override 52 + def accept_post(self, post: Post): 52 53 new_root_id: int | None = None 53 54 new_parent_id: int | None = None 54 55 55 56 reply_ref: str | None = None 56 57 if post.parent_id: 57 58 thread = self._find_mapped_thread( 58 - post.parent_id, service, user, self.url, self.user_id 59 + post.parent_id, post.service, post.author, self.url, self.user_id 59 60 ) 60 61 61 62 if not thread: ··· 65 66 66 67 quote = post.attachments.get(QuoteAttachment) 67 68 if quote: 68 - if quote.quoted_user != user: 69 + if quote.quoted_user != post.author: 69 70 self.log.info("Quoted other user, skipping!") 70 71 return 71 72 72 - quoted_post = self._get_post(service, user, quote.quoted_id) 73 + quoted_post = self._get_post(post.service, post.author, quote.quoted_id) 73 74 if not quoted_post: 74 75 self.log.error("Failed to find quoted post in the database!") 75 76 return ··· 103 104 if sensitive and sensitive.sensitive: 104 105 pass # TODO 105 106 106 - def delete_post(self, service: str, user: str, post_id: str): 107 - post = self._get_post(service, user, post_id) 108 - if not post: 107 + @override 108 + def delete_post(self, post: PostRef): 109 + db_post = self._get_post(post.service, post.author, post.id) 110 + if not db_post: 109 111 self.log.info("Post not found in db, skipping delete..") 110 112 return 111 113 112 - mappings = self._get_mappings(post["id"], self.url, self.user_id) 114 + mappings = self._get_mappings(db_post["id"], self.url, self.user_id) 113 115 for mapping in mappings[::-1]: 114 116 self.log.info("Deleting '%s'...", mapping["identifier"]) 115 117 requests.delete( ··· 118 120 ) 119 121 self._delete_post_by_id(mapping["id"]) 120 122 121 - def accept_repost(self, service: str, user: str, repost_id: str, reposted_id: str): 122 - reposted = self._get_post(service, user, reposted_id) 123 - if not reposted: 123 + @override 124 + def accept_repost(self, repost: PostRef, reposted: PostRef): 125 + original = self._get_post(reposted.service, reposted.author, reposted.id) 126 + if not original: 124 127 self.log.info("Post not found in db, skipping repost..") 125 128 return 126 129 127 - mappings = self._get_mappings(reposted["id"], self.url, self.user_id) 130 + mappings = self._get_mappings(original["id"], self.url, self.user_id) 128 131 if mappings: 129 132 rsp = requests.post( 130 133 f"{self.url}/api/v1/statuses/{mappings[0]['identifier']}/reblog", ··· 150 153 inserted = self._get_post(self.url, self.user_id, rsp.json()["id"]) 151 154 if not inserted: 152 155 raise ValueError("Inserted post not found!") 153 - self._insert_post_mapping(reposted["id"], inserted["id"]) 156 + self._insert_post_mapping(original["id"], inserted["id"]) 154 157 155 - def delete_repost(self, service: str, user: str, repost_id: str): 156 - repost = self._get_post(service, user, repost_id) 157 - if not repost: 158 + @override 159 + def delete_repost(self, repost: PostRef): 160 + db_repost = self._get_post(repost.service, repost.author, repost.id) 161 + if not db_repost: 158 162 self.log.info("Repost not found in db, skipping delete..") 159 163 return 160 164 161 - mappings = self._get_mappings(repost["id"], self.url, self.user_id) 162 - rmappings = self._get_mappings(repost["reposted"], self.url, self.user_id) 165 + mappings = self._get_mappings(db_repost["id"], self.url, self.user_id) 166 + rmappings = self._get_mappings(db_repost["reposted"], self.url, self.user_id) 163 167 164 168 if mappings and rmappings: 165 169 self.log.info(
+5 -4
misskey/input.py
··· 15 15 SensitiveAttachment, 16 16 ) 17 17 from cross.media import Blob, download_blob 18 - from cross.post import Post 18 + from cross.post import Post, PostRef 19 19 from cross.service import InputService 20 20 from database.connection import DatabasePool 21 21 from misskey.info import MisskeyService ··· 113 113 114 114 parser = MarkdownParser() # TODO MFM parser 115 115 tokens = parser.parse(note.get("text", ""), tags, handles) 116 - post = Post(id=note["id"], parent_id=reply["id"] if reply else None, tokens=tokens) 116 + post = Post(id=note["id"], author=self.user_id, service=self.url, parent_id=reply["id"] if reply else None, tokens=tokens) 117 117 118 118 post.attachments.put(RemoteUrlAttachment(url=self.url + "/notes/" + note["id"])) 119 119 if renote: ··· 180 180 } 181 181 ) 182 182 183 + repost_ref = PostRef(id=note["id"], author=self.user_id, service=self.url) 184 + reposted_ref = PostRef(id=renote["id"], author=self.user_id, service=self.url) 183 185 for out in self.outputs: 184 - self.submitter(lambda: out.accept_repost(note["id"], renote["id"])) 186 + self.submitter(lambda: out.accept_repost(repost_ref, reposted_ref)) 185 187 186 188 def _accept_msg(self, msg: websockets.Data) -> None: 187 189 data: dict[str, Any] = cast(dict[str, Any], json.loads(msg)) ··· 191 193 if type == "note" or type == "reply": 192 194 note_body = data["body"]["body"] 193 195 self._on_note(note_body) 194 - return 195 196 196 197 async def _subscribe_to_home(self, ws: websockets.ClientConnection) -> None: 197 198 await ws.send(
+7 -7
util/dummy.py
··· 1 1 from typing import override 2 - from cross.post import Post 2 + from cross.post import Post, PostRef 3 3 from cross.service import OutputService 4 4 from database.connection import DatabasePool 5 5 ··· 17 17 self.log.info("%s", post) 18 18 19 19 @override 20 - def accept_repost(self, repost_id: str, reposted_id: str): 21 - self.log.info("%s, %s", repost_id, reposted_id) 20 + def accept_repost(self, repost: PostRef, reposted: PostRef): 21 + self.log.info("%s, %s", repost.id, reposted.id) 22 22 23 23 @override 24 - def delete_post(self, post_id: str): 25 - self.log.info("%s", post_id) 24 + def delete_post(self, post: PostRef): 25 + self.log.info("%s", post.id) 26 26 27 27 @override 28 - def delete_repost(self, repost_id: str): 29 - self.log.info("%s", repost_id) 28 + def delete_repost(self, repost: PostRef): 29 + self.log.info("%s", repost.id)