From 9ec737c8dfd6d73ea9081624ffb104bfd02c1817 Mon Sep 17 00:00:00 2001 From: Your Name Date: Thu, 4 Jul 2024 12:43:45 -0400 Subject: [PATCH] bluesky post --- boofilsic/settings.py | 4 +++- journal/models/common.py | 49 +++++++++++++++++++++++++++++++------- journal/models/note.py | 6 ++--- journal/models/shelf.py | 5 +--- mastodon/models/bluesky.py | 22 +++++++++++++---- mastodon/models/threads.py | 26 +++++++++++++------- mastodon/views/threads.py | 4 ---- 7 files changed, 81 insertions(+), 35 deletions(-) diff --git a/boofilsic/settings.py b/boofilsic/settings.py index 6e7cc88d..9879f7ee 100644 --- a/boofilsic/settings.py +++ b/boofilsic/settings.py @@ -229,7 +229,7 @@ ENABLE_LOCAL_ONLY = env("NEODB_ENABLE_LOCAL_ONLY") # Timeout of requests to Mastodon, in seconds MASTODON_TIMEOUT = env("NEODB_LOGIN_MASTODON_TIMEOUT", default=5) # type: ignore -THREADS_TIMEOUT = 10 # Threads is really slow when publishing post +THREADS_TIMEOUT = 30 # Threads is really slow when publishing post TAKAHE_REMOTE_TIMEOUT = MASTODON_TIMEOUT NEODB_USER_AGENT = f"NeoDB/{NEODB_VERSION} (+{SITE_INFO.get('site_url', 'undefined')})" @@ -389,6 +389,8 @@ LOGGING = { logging.getLogger("requests").setLevel(logging.WARNING) logging.getLogger("urllib3").setLevel(logging.WARNING) +logging.getLogger("httpx").setLevel(logging.WARNING) +logging.getLogger("httpcore").setLevel(logging.WARNING) if SLACK_TOKEN: INSTALLED_APPS += [ diff --git a/journal/models/common.py b/journal/models/common.py index 76e4a7ac..71c65237 100644 --- a/journal/models/common.py +++ b/journal/models/common.py @@ -3,6 +3,7 @@ import uuid from abc import abstractmethod from datetime import datetime from functools import cached_property +from operator import pos from typing import TYPE_CHECKING, Any, Self import django_rq @@ -279,9 +280,7 @@ class Piece(PolymorphicModel, UserOwnedObjectMixin): def get_crosspost_params(self): d = { "visibility": self.visibility, - "update_id": (self.metadata if hasattr(self, "metadata") else {}).get( - "mastodon_id" - ), + "update_ids": self.metadata.copy() if hasattr(self, "metadata") else {}, } d.update(self.to_crosspost_params()) return d @@ -293,20 +292,51 @@ class Piece(PolymorphicModel, UserOwnedObjectMixin): ) def _sync_to_social_accounts(self, update_mode: int): + def params_for_platform(params, platform): + p = params.copy() + for k in ["update_id", "reply_to_id"]: + ks = k + "s" + if ks in p: + d = p.pop(ks) + v = d.get(platform + "_id") + if v: + p[k] = v + return p + activate_language_for_user(self.owner.user) - params = self.get_crosspost_params() metadata = self.metadata.copy() - self.sync_to_mastodon(params, update_mode) - self.sync_to_threads(params, update_mode) + # TODO migrate + params = self.get_crosspost_params() + self.sync_to_mastodon(params_for_platform(params, "mastodon"), update_mode) + self.sync_to_threads(params_for_platform(params, "threads"), update_mode) + self.sync_to_bluesky(params_for_platform(params, "bluesky"), update_mode) if self.metadata != metadata: self.save(update_fields=["metadata"]) + def sync_to_bluesky(self, params, update_mode): + # skip non-public post as Bluesky does not support it + # update_mode 0 will act like 1 as bsky.app does not support edit + bluesky = self.owner.user.bluesky + if params["visibility"] != 0 or not bluesky: + return False + if update_mode in [0, 1]: + post_id = self.metadata.get("bluesky_id") + if post_id: + try: + bluesky.delete_post(post_id) + except Exception as e: + logger.warning(f"Delete {bluesky} post {post_id} error {e}") + r = bluesky.post(**params) + self.metadata.update({"bluesky_" + k: v for k, v in r.items()}) + return True + def sync_to_threads(self, params, update_mode): # skip non-public post as Threads does not support it # update_mode will be ignored as update/delete are not supported either threads = self.owner.user.threads + # return if params["visibility"] != 0 or not threads: - return + return False try: r = threads.post(**params) except RequestAborted: @@ -319,13 +349,13 @@ class Piece(PolymorphicModel, UserOwnedObjectMixin): def sync_to_mastodon(self, params, update_mode): mastodon = self.owner.user.mastodon if not mastodon: - return + return False if self.owner.user.preference.mastodon_repost_mode == 1: if update_mode == 1: toot_id = self.metadata.pop("mastodon_id", None) if toot_id: self.metadata.pop("mastodon_url", None) - mastodon.delete(toot_id) + mastodon.delete_post(toot_id) elif update_mode == 1: params.pop("update_id", None) return self.crosspost_to_mastodon(params) @@ -333,6 +363,7 @@ class Piece(PolymorphicModel, UserOwnedObjectMixin): mastodon.boost(self.latest_post.url) else: logger.warning("No post found for piece") + return True def crosspost_to_mastodon(self, params): mastodon = self.owner.user.mastodon diff --git a/journal/models/note.py b/journal/models/note.py index f848e697..321d1617 100644 --- a/journal/models/note.py +++ b/journal/models/note.py @@ -173,10 +173,8 @@ class Note(Content): "spoiler_text": self.title, "content": self.content + footer, "sensitive": self.sensitive, - "reply_to_id": ( - (self.shelfmember.metadata or {}).get("mastodon_id") - if self.shelfmember - else None + "reply_to_ids": ( + self.shelfmember.metadata.copy() if self.shelfmember else {} ), } if self.latest_post: diff --git a/journal/models/shelf.py b/journal/models/shelf.py index 43fc1b56..c7d56311 100644 --- a/journal/models/shelf.py +++ b/journal/models/shelf.py @@ -374,9 +374,7 @@ class ShelfMember(ListMember): else: spoiler, txt = None, "" stars = ( - self.owner.user.mastodon.rating_to_emoji(self.sibling_rating.grade) - if self.sibling_rating and self.owner.user.mastodon - else "" + render_rating(self.sibling_rating.grade, 1) if self.sibling_rating else "" ) content = f"{action} {stars} \n{self.item.absolute_url}\n{txt}\n{self.get_crosspost_postfix()}" params = {"content": content, "spoiler_text": spoiler} @@ -472,7 +470,6 @@ class ShelfMember(ListMember): def link_post_id(self, post_id: int): if self.local: self.ensure_log_entry().link_post_id(post_id) - print(self.ensure_log_entry(), post_id) return super().link_post_id(post_id) diff --git a/mastodon/models/bluesky.py b/mastodon/models/bluesky.py index da9f08c4..41a67b28 100644 --- a/mastodon/models/bluesky.py +++ b/mastodon/models/bluesky.py @@ -1,6 +1,8 @@ from functools import cached_property +from operator import pos from atproto import Client, SessionEvent, client_utils +from atproto_client import models from django.utils import timezone from loguru import logger @@ -45,7 +47,6 @@ class BlueskyAccount(SocialAccount): ) def on_session_change(self, event, session) -> None: - logger.debug("Bluesky session changed:", event, repr(session)) if event in (SessionEvent.CREATE, SessionEvent.REFRESH): session_string = session.export() if session_string != self.session_string: @@ -84,8 +85,21 @@ class BlueskyAccount(SocialAccount): ] ) - def post(self, content): + def post(self, content, reply_to_id=None, **kwargs): + reply_to = None + if reply_to_id: + posts = self._client.get_posts([reply_to_id]).posts + if posts: + root_post_ref = models.create_strong_ref(posts[0]) + reply_to = models.AppBskyFeedPost.ReplyRef( + parent=root_post_ref, root=root_post_ref + ) text = client_utils.TextBuilder().text(content) + # todo OpenGraph # .link("Python SDK", "https://atproto.blue") - post = self._client.send_post(text) - return {"id": post.cid, "url": post.uri} + post = self._client.send_post(text, reply_to=reply_to) + # return AT uri as id since it's used as so. + return {"cid": post.cid, "id": post.uri} + + def delete_post(self, post_uri): + self._client.delete_post(post_uri) diff --git a/mastodon/models/threads.py b/mastodon/models/threads.py index 0684424c..b554fe66 100644 --- a/mastodon/models/threads.py +++ b/mastodon/models/threads.py @@ -127,10 +127,14 @@ class Threads: return data @staticmethod - def post_single(token: str, user_id: str, text: str): + def post_single(token: str, user_id: str, text: str, reply_to_id=None): url = f"https://graph.threads.net/v1.0/{user_id}/threads?media_type=TEXT&access_token={token}&text={quote(text)}" + # TODO waiting for Meta to confirm it's bug or permission issue + # if reply_to_id: + # url += "&reply_to_id=" + reply_to_id response = post(url) if response.status_code != 200: + logger.debug(f"Error {url} {response.status_code} {response.content}") return None media_container_id = (response.json() or {}).get("id") if not media_container_id: @@ -138,8 +142,10 @@ class Threads: url = f"https://graph.threads.net/v1.0/{user_id}/threads_publish?creation_id={media_container_id}&access_token={token}" response = post(url) if response.status_code != 200: + logger.debug(f"Error {url} {response.status_code} {response.content}") return None - return (response.json() or {}).get("id") + media_id = (response.json() or {}).get("id") + return media_id @staticmethod def get_single(token: str, media_id: str) -> dict | None: @@ -233,15 +239,17 @@ class ThreadsAccount(SocialAccount): self.save(update_fields=["account_data", "handle", "last_refresh"]) return True - def post(self, content: str, **kwargs): + def post(self, content: str, reply_to_id=None, **kwargs): text = ( content.replace(settings.STAR_SOLID + " ", "🌕") .replace(settings.STAR_HALF + " ", "🌗") .replace(settings.STAR_EMPTY + " ", "🌑") ) - media_id = Threads.post_single(self.access_token, self.uid, text) - if media_id: - d = Threads.get_single(self.access_token, media_id) - if d: - return {"id": media_id, "url": d["permalink"]} - raise RequestAborted() + media_id = Threads.post_single(self.access_token, self.uid, text, reply_to_id) + if not media_id: + raise RequestAborted() + return {"id": media_id} + # if media_id: + # d = Threads.get_single(self.access_token, media_id) + # if d: + # return {"id": media_id, "url": d["permalink"]} diff --git a/mastodon/views/threads.py b/mastodon/views/threads.py index 10e4c79d..3d51c118 100644 --- a/mastodon/views/threads.py +++ b/mastodon/views/threads.py @@ -51,13 +51,9 @@ def threads_oauth(request: HttpRequest): @require_http_methods(["GET"]) def threads_uninstall(request: HttpRequest): - print(request.GET) - print(request.POST) return redirect(reverse("users:data")) @require_http_methods(["GET"]) def threads_delete(request: HttpRequest): - print(request.GET) - print(request.POST) return redirect(reverse("users:data"))