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

fix: broken logs, broken threading logic on bsky output, missking types.

zenfyr.dev 100ee395 f2b23c64

verified
+73 -84
+2 -5
bluesky/input.py
··· 53 53 post_cid = cast(str, record["$xpost.strongRef"]["cid"]) 54 54 55 55 if self._is_post_crossposted(self.url, self.did, post_uri): 56 - self.log.info( 57 - "Skipping '%s': already crossposted", 58 - post_uri, 59 - ) 56 + self.log.info("Skipping '%s': already crossposted", post_uri) 60 57 return 61 58 62 59 parent_uri = cast( ··· 167 164 "service": self.url, 168 165 "identifier": post_uri, 169 166 "parent": parent["id"], 170 - "root": parent["id"] if not parent["root"] else parent["root"], 167 + "root": parent["root"] or parent["id"], 171 168 "extra_data": json.dumps({"cid": post_cid}), 172 169 } 173 170 )
+31 -42
bluesky/output.py
··· 195 195 new_parent_id: int | None = None 196 196 197 197 if post.parent_id: 198 - parent = self._get_post(post.service, post.author, post.parent_id) 199 - if not parent: 200 - self.log.error( 201 - "Skipping '%s': parent post not found in db", post.parent_id 202 - ) 203 - return 204 - 205 198 thread = self._find_mapped_thread( 206 - parent["identifier"], 207 - parent["service"], 208 - parent["user"], 199 + post.parent_id, 200 + post.service, 201 + post.author, 209 202 self.url, 210 203 self.did, 211 204 ) 212 205 if not thread: 213 206 self.log.error( 214 - "Skipping '%s': parent thread tuple not found in db", 215 - post.parent_id, 207 + "Skipping '%s': parent thread tuple not found in db", post.id 216 208 ) 217 209 return 218 210 219 - root_uri, reply_uri, root_db_id, reply_db_id = thread 211 + root_uri, reply_uri, new_root_id, new_parent_id = thread 220 212 221 - root_post = self._get_post(self.url, self.did, root_uri) 222 - reply_post = self._get_post(self.url, self.did, reply_uri) 213 + root_post = self._get_post_by_id(new_root_id) 214 + reply_post = self._get_post_by_id(new_parent_id) 223 215 224 216 if not root_post or not reply_post: 225 - self.log.error("Skipping '%s': failed to fetch parent posts from db") 217 + self.log.error( 218 + "Skipping '%s': failed to fetch parent posts from db", post.id 219 + ) 226 220 return 227 221 228 222 root_cid = cid_from_json(root_post["extra_data"]) 229 223 reply_cid = cid_from_json(reply_post["extra_data"]) 230 224 231 225 if not root_cid or not reply_cid: 232 - self.log.error("Skipping '%s': failed to parse CID from db") 226 + self.log.error("Skipping '%s': failed to parse CID from db", post.id) 233 227 return 234 228 235 229 root_ref = StrongRef(uri=root_uri, cid=root_cid) 236 230 reply_ref = StrongRef(uri=reply_uri, cid=reply_cid) 237 231 reply_to = ReplyRef(root=root_ref, parent=reply_ref) 238 - new_root_id = root_db_id 239 - new_parent_id = reply_db_id 240 232 241 233 labels_attachment = post.attachments.get(LabelsAttachment) 242 234 spoiler: str | None = ( ··· 305 297 quoted_uri: str | None = None 306 298 if quote_attachment: 307 299 if quote_attachment.quoted_user != post.author: 308 - self.log.info("Skipping '%s': quoted other user") 300 + self.log.info("Skipping '%s': quoted other user", post.id) 309 301 return 310 302 311 303 quoted_post = self._get_post( 312 304 post.service, post.author, quote_attachment.quoted_id 313 305 ) 314 306 if not quoted_post: 315 - self.log.error("Skipping '%s': quoted post not found in db!") 307 + self.log.error("Skipping '%s': quoted post not found in db!", post.id) 316 308 return 317 309 318 310 quoted_mappings = self._get_mappings(quoted_post["id"], self.url, self.did) 319 311 if not quoted_mappings: 320 - self.log.error("Skipping '%s': failed to find mappings for quoted post") 312 + self.log.error( 313 + "Skipping '%s': failed to find mappings for quoted post", post.id 314 + ) 321 315 return 322 316 323 317 quoted_cid = cid_from_json(quoted_mappings[0]["extra_data"]) 324 318 if not quoted_cid: 325 - self.log.error("Skipping '%s': failed to parse CID from db") 319 + self.log.error("Skipping '%s': failed to parse CID from db", post.id) 326 320 return 327 321 328 322 quoted_uri = quoted_mappings[0]["identifier"] ··· 374 368 created_records: list[tuple[str, str]] = [] 375 369 post_root_ref: StrongRef | None = None 376 370 previous_reply_ref: StrongRef | None = None 371 + original_thread_root: StrongRef | None = reply_to.root if reply_to else None 377 372 378 373 richtext_index = 0 379 374 ··· 389 384 if i == 0: 390 385 current_reply_to = reply_to 391 386 elif previous_reply_ref and post_root_ref: 392 - current_reply_to = ReplyRef( 393 - root=post_root_ref, parent=previous_reply_ref 394 - ) 387 + root_ref = original_thread_root or post_root_ref 388 + current_reply_to = ReplyRef(root=root_ref, parent=previous_reply_ref) 395 389 396 390 embed: dict[str, Any] | None = None 397 391 if i == 0 and quoted_uri and quoted_cid: ··· 496 490 self.options.quote_gate, 497 491 ) 498 492 493 + all_records = created_records 499 494 if new_root_id is None or new_parent_id is None: 500 - self._insert_post( 495 + new_root_id = self._insert_post( 501 496 { 502 497 "user": self.did, 503 498 "service": self.url, ··· 509 504 "crossposted": 1, 510 505 } 511 506 ) 512 - new_post = self._get_post(self.url, self.did, created_records[0][0]) 513 - if not new_post: 514 - raise ValueError("Inserted post not found!") 515 - new_root_id = new_post["id"] 516 507 new_parent_id = new_root_id 517 - 518 - self._insert_post_mapping(db_post["id"], new_parent_id) 508 + created_records = created_records[1:] 509 + self._insert_post_mapping(db_post["id"], new_parent_id) 519 510 520 - for uri, cid in created_records[1:]: 521 - self._insert_post( 511 + for uri, cid in created_records: 512 + new_parent_id = self._insert_post( 522 513 { 523 514 "user": self.did, 524 515 "service": self.url, ··· 530 521 "crossposted": 1, 531 522 } 532 523 ) 533 - reply_post = self._get_post(self.url, self.did, uri) 534 - if not reply_post: 535 - raise ValueError("Inserted reply post not found!") 536 - new_parent_id = reply_post["id"] 537 524 self._insert_post_mapping(db_post["id"], new_parent_id) 538 525 539 526 self.log.info( 540 527 "Post accepted successfully: %s -> %s", 541 528 post.id, 542 - [r[0] for r in created_records], 529 + [r[0] for r in all_records], 543 530 ) 544 531 545 532 @override ··· 560 547 db_repost = self._get_post(repost.service, repost.author, repost.id) 561 548 db_reposted = self._get_post(reposted.service, reposted.author, reposted.id) 562 549 if not db_repost or not db_reposted: 563 - self.log.info("Skipping repost '%s': post not found in db") 550 + self.log.info("Skipping repost '%s': post not found in db", repost.id) 564 551 return 565 552 566 553 mappings = self._get_mappings(db_reposted["id"], self.url, self.did) ··· 569 556 570 557 cid = cid_from_json(mappings[0]["extra_data"]) 571 558 if not cid: 572 - self.log.exception("Skipping '%s': failed to parse CID from extra_data") 559 + self.log.exception( 560 + "Skipping repost '%s': failed to parse CID from extra_data", repost.id 561 + ) 573 562 return 574 563 575 564 response = self._client.repost(mappings[0]["identifier"], cid)
+8 -6
cross/service.py
··· 60 60 WHERE m.original = ? 61 61 AND p.service = ? 62 62 AND p.user = ? 63 - ORDER BY p.id; 63 + ORDER BY p.id ASC; 64 64 """, 65 65 (original, service, user), 66 66 ) ··· 68 68 69 69 def _find_mapped_thread( 70 70 self, parent: str, iservice: str, iuser: str, oservice: str, ouser: str 71 - ): 71 + ) -> tuple[str, str, int, int] | None: 72 72 reply_data = self._get_post(iservice, iuser, parent) 73 73 if not reply_data: 74 74 return None ··· 95 95 return ( 96 96 root_identifier["identifier"], # real ids 97 97 reply_identifier["identifier"], 98 - reply_data["root"], # db ids 99 - reply_data["id"], 98 + root_identifier["id"], # db ids 99 + reply_identifier["id"], 100 100 ) 101 101 102 - def _insert_post(self, post_data: dict[str, Any]): 102 + def _insert_post(self, post_data: dict[str, Any]) -> int: 103 103 values = [post_data.get(col) for col in columns] 104 104 cursor = self.db.get_conn().cursor() 105 105 _ = cursor.execute( 106 - f"INSERT INTO posts ({column_names}) VALUES ({placeholders})", values 106 + f"INSERT INTO posts ({column_names}) VALUES ({placeholders}) RETURNING id", 107 + values, 107 108 ) 109 + return int(cast(sqlite3.Row, cursor.fetchone())["id"]) 108 110 109 111 def _insert_post_mapping(self, original: int, mapped: int): 110 112 cursor = self.db.get_conn().cursor()
+32 -31
mastodon/output.py
··· 250 250 def accept_post(self, post: Post): 251 251 db_post = self._get_post(post.service, post.author, post.id) 252 252 if not db_post: 253 - self.log.error("Skipping '%s': post not found in db") 253 + self.log.error("Skipping '%s': post not found in db", post.id) 254 254 return 255 255 256 256 new_root_id: int | None = None ··· 259 259 reply_ref: str | None = None 260 260 if post.parent_id: 261 261 thread = self._find_mapped_thread( 262 - post.parent_id, post.service, post.author, self.url, self.user_id 262 + post.parent_id, 263 + post.service, 264 + post.author, 265 + self.url, 266 + self.user_id, 263 267 ) 264 268 if not thread: 265 - self.log.error("Skipping '%s': parent thread tuple not found in db") 269 + self.log.error( 270 + "Skipping '%s': parent thread tuple not found in db", post.id 271 + ) 266 272 return 267 273 _, reply_ref, new_root_id, new_parent_id = thread 268 274 ··· 270 276 quote = post.attachments.get(QuoteAttachment) 271 277 if quote: 272 278 if quote.quoted_user != post.author: 273 - self.log.info("Skipping '%s': quote of other user") 279 + self.log.info("Skipping '%s': quote of other user", post.id) 274 280 return 275 281 276 282 quoted_post = self._get_post(post.service, post.author, quote.quoted_id) 277 283 if not quoted_post: 278 - self.log.error("Skipping '%s': quoted post not found in db") 284 + self.log.error("Skipping '%s': quoted post not found in db", post.id) 279 285 return 280 286 281 287 quoted_mappings = self._get_mappings( ··· 283 289 ) 284 290 if not quoted_mappings: 285 291 self.log.error( 286 - "Skipping '%s': mappings for quoted post not found in db" 292 + "Skipping '%s': mappings for quoted post not found in db", post.id 287 293 ) 288 294 return 289 295 ··· 316 322 317 323 raw_statuses = self._split_tokens_and_media(post_tokens, media_blobs) 318 324 if not raw_statuses: 319 - self.log.error("Skipping '%s': couldn't split post into statuses") 325 + self.log.error("Skipping '%s': couldn't split post into statuses", post.id) 320 326 return 321 327 322 328 baked_statuses: list[tuple[str, list[str] | None]] = [] ··· 325 331 if raw_media: 326 332 media_ids = self._upload_media(raw_media) 327 333 if not media_ids: 328 - self.log.error("Skipping '%s': failed to upload attachments") 334 + self.log.error( 335 + "Skipping '%s': failed to upload attachments", post.id 336 + ) 329 337 return 330 338 baked_statuses.append((status_text, media_ids)) 331 339 ··· 369 377 if i == 0: 370 378 reply_ref = status_id 371 379 380 + all_statuses = created_statuses 372 381 if new_root_id is None or new_parent_id is None: 373 - self._insert_post( 382 + new_root_id = self._insert_post( 374 383 { 375 384 "user": self.user_id, 376 385 "service": self.url, ··· 382 391 "crossposted": 1, 383 392 } 384 393 ) 385 - new_post = self._get_post(self.url, self.user_id, created_statuses[0]) 386 - if not new_post: 387 - raise ValueError("Inserted post not found!") 388 - new_root_id = new_post["id"] 389 394 new_parent_id = new_root_id 395 + created_statuses = created_statuses[1:] 396 + self._insert_post_mapping(db_post["id"], new_parent_id) 390 397 391 - self._insert_post_mapping(db_post["id"], new_parent_id) 392 - 393 - for status_id in created_statuses[1:]: 394 - self._insert_post( 398 + for status_id in created_statuses: 399 + new_parent_id = self._insert_post( 395 400 { 396 401 "user": self.user_id, 397 402 "service": self.url, ··· 403 408 "crossposted": 1, 404 409 } 405 410 ) 406 - reply_post = self._get_post(self.url, self.user_id, status_id) 407 - if not reply_post: 408 - raise ValueError("Inserted reply post not found!") 409 - new_parent_id = reply_post["id"] 410 411 self._insert_post_mapping(db_post["id"], new_parent_id) 411 412 412 - self.log.info("Post accepted successfully: %s -> %s", post.id, created_statuses) 413 + self.log.info("Post accepted successfully: %s -> %s", post.id, all_statuses) 413 414 414 415 @override 415 416 def delete_post(self, post: PostRef): 416 417 db_post = self._get_post(post.service, post.author, post.id) 417 418 if not db_post: 418 - self.log.warning("Skipping delete '%s': post not found in db: %s", post.id) 419 + self.log.warning("Skipping delete '%s': post not found in db", post.id) 419 420 return 420 421 421 422 mappings = self._get_mappings(db_post["id"], self.url, self.user_id) ··· 434 435 def accept_repost(self, repost: PostRef, reposted: PostRef): 435 436 original = self._get_post(reposted.service, reposted.author, reposted.id) 436 437 if not original: 437 - self.log.info("Skipping repost '%s': reposted post not found in db") 438 + self.log.info( 439 + "Skipping repost '%s': reposted post not found in db", repost.id 440 + ) 438 441 return 439 442 440 443 mappings = self._get_mappings(original["id"], self.url, self.user_id) 441 444 if not mappings: 442 - self.log.error("Skipping repost '%s': no mappings found for reposted post") 445 + self.log.error( 446 + "Skipping repost '%s': no mappings found for reposted post", repost.id 447 + ) 443 448 return 444 449 445 450 response = self.http.post( ··· 466 471 467 472 original_repost = self._get_post(repost.service, repost.author, repost.id) 468 473 if not original_repost: 469 - self.log.error( 470 - "Skipping repost '%s': repost not found in db: %s", repost.id 471 - ) 474 + self.log.error("Skipping repost '%s': repost not found in db", repost.id) 472 475 return 473 476 474 477 self._insert_post_mapping(original_repost["id"], inserted["id"]) ··· 491 494 return 492 495 if not rmappings: 493 496 self.log.warning( 494 - "Skipping delete '%s': no mappings found for post", 495 - repost.id, 496 - db_repost["reposted"], 497 + "Skipping delete '%s': no mappings found for post", repost.id 497 498 ) 498 499 return 499 500