bluesky post

This commit is contained in:
Your Name 2024-07-04 12:43:45 -04:00 committed by Henri Dickson
parent bf92528331
commit 9ec737c8df
7 changed files with 81 additions and 35 deletions

View file

@ -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 += [

View file

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

View file

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

View file

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

View file

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

View file

@ -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"]}

View file

@ -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"))