bluesky login improvements
This commit is contained in:
parent
9ec737c8df
commit
4e97cb7083
5 changed files with 127 additions and 34 deletions
|
@ -114,13 +114,7 @@ class Piece(PolymorphicModel, UserOwnedObjectMixin):
|
|||
def delete(self, *args, **kwargs):
|
||||
if self.local:
|
||||
Takahe.delete_posts(self.all_post_ids)
|
||||
toot_id = (
|
||||
(self.metadata or {}).get("mastodon_id")
|
||||
if hasattr(self, "metadata")
|
||||
else None
|
||||
)
|
||||
if toot_id and self.owner.user.mastodon:
|
||||
self.owner.user.mastodon.delete_post_later(toot_id)
|
||||
self.delete_crossposts()
|
||||
return super().delete(*args, **kwargs)
|
||||
|
||||
@property
|
||||
|
@ -277,6 +271,22 @@ class Piece(PolymorphicModel, UserOwnedObjectMixin):
|
|||
# subclass may have to add additional code to update type_data in local post
|
||||
return p
|
||||
|
||||
@classmethod
|
||||
def _delete_crossposts(cls, user_pk, metadata: dict):
|
||||
user = User.objects.get(pk=user_pk)
|
||||
toot_id = metadata.get("mastodon_id")
|
||||
if toot_id and user.mastodon:
|
||||
user.mastodon.delete_post(toot_id)
|
||||
post_id = metadata.get("bluesky_id")
|
||||
if toot_id and user.bluesky:
|
||||
user.bluesky.delete_post(post_id)
|
||||
|
||||
def delete_crossposts(self):
|
||||
if hasattr(self, "metadata") and self.metadata:
|
||||
django_rq.get_queue("mastodon").enqueue(
|
||||
self._delete_crossposts, self.owner.user_id, self.metadata
|
||||
)
|
||||
|
||||
def get_crosspost_params(self):
|
||||
d = {
|
||||
"visibility": self.visibility,
|
||||
|
@ -305,7 +315,13 @@ class Piece(PolymorphicModel, UserOwnedObjectMixin):
|
|||
|
||||
activate_language_for_user(self.owner.user)
|
||||
metadata = self.metadata.copy()
|
||||
# TODO migrate
|
||||
|
||||
# backward compatible with previous way of storing mastodon id
|
||||
legacy_mastodon_url = self.metadata.pop("shared_link", None)
|
||||
if legacy_mastodon_url and not self.metadata.get("mastodon_id"):
|
||||
self.metadata["mastodon_id"] = legacy_mastodon_url.split("/")[-1]
|
||||
self.metadata["mastodon_url"] = legacy_mastodon_url
|
||||
|
||||
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)
|
||||
|
|
|
@ -1,8 +1,12 @@
|
|||
import re
|
||||
from functools import cached_property
|
||||
from operator import pos
|
||||
|
||||
from atproto import Client, SessionEvent, client_utils
|
||||
from atproto_client import models
|
||||
from atproto_identity.did.resolver import DidResolver
|
||||
from atproto_identity.handle.resolver import HandleResolver
|
||||
from django.db.models import base
|
||||
from django.utils import timezone
|
||||
from loguru import logger
|
||||
|
||||
|
@ -12,28 +16,60 @@ from .common import SocialAccount
|
|||
|
||||
|
||||
class Bluesky:
|
||||
BASE_DOMAIN = "bsky.app" # TODO support alternative servers
|
||||
_DOMAIN = "-"
|
||||
_RE_HANDLE = re.compile(
|
||||
r"/^([a-zA-Z0-9]([a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?\.)+[a-zA-Z]([a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?$/"
|
||||
)
|
||||
# for BlueskyAccount
|
||||
# uid is did and the only unique identifier
|
||||
# domain is not useful and will always be _DOMAIN
|
||||
# handle and base_url may change in BlueskyAccount.refresh()
|
||||
|
||||
@staticmethod
|
||||
def authenticate(username: str, password: str) -> "BlueskyAccount | None":
|
||||
def authenticate(handle: str, password: str) -> "BlueskyAccount | None":
|
||||
if not Bluesky._RE_HANDLE.match(handle) or len(handle) > 500:
|
||||
logger.warning(f"ATProto login failed: handle {handle} is invalid")
|
||||
return None
|
||||
try:
|
||||
client = Client()
|
||||
profile = client.login(username, password)
|
||||
handle_r = HandleResolver(timeout=5)
|
||||
did = handle_r.resolve(handle)
|
||||
if not did:
|
||||
logger.warning(
|
||||
f"ATProto login failed: handle {handle} -> <missing did>"
|
||||
)
|
||||
return
|
||||
did_r = DidResolver()
|
||||
did_doc = did_r.resolve(did)
|
||||
if not did_doc:
|
||||
logger.warning(
|
||||
f"ATProto login failed: handle {handle} -> did {did} -> <missing doc>"
|
||||
)
|
||||
return
|
||||
resolved_handle = did_doc.get_handle()
|
||||
if resolved_handle != handle:
|
||||
logger.warning(
|
||||
f"ATProto login failed: handle {handle} -> did {did} -> handle {resolved_handle}"
|
||||
)
|
||||
return
|
||||
base_url = did_doc.get_pds_endpoint()
|
||||
client = Client(base_url)
|
||||
profile = client.login(handle, password)
|
||||
session_string = client.export_session_string()
|
||||
except Exception as e:
|
||||
logger.debug(f"Bluesky login {username} exception {e}")
|
||||
return None
|
||||
existing_account = BlueskyAccount.objects.filter(
|
||||
uid=profile.did, domain=Bluesky.BASE_DOMAIN
|
||||
logger.debug(f"Bluesky login {handle} exception {e}")
|
||||
return
|
||||
account = BlueskyAccount.objects.filter(
|
||||
uid=profile.did, domain=Bluesky._DOMAIN
|
||||
).first()
|
||||
if existing_account:
|
||||
existing_account.session_string = session_string
|
||||
existing_account.save(update_fields=["access_data"])
|
||||
existing_account.refresh(save=True, profile=profile)
|
||||
return existing_account
|
||||
account = BlueskyAccount(uid=profile.did, domain=Bluesky.BASE_DOMAIN)
|
||||
if not account:
|
||||
account = BlueskyAccount(uid=profile.did, domain=Bluesky._DOMAIN)
|
||||
account._client = client
|
||||
account.session_string = session_string
|
||||
account.refresh(save=False, profile=profile)
|
||||
account.base_url = base_url
|
||||
if account.pk:
|
||||
account.refresh(save=True, did_refresh=False)
|
||||
else:
|
||||
account.refresh(save=False, did_refresh=False)
|
||||
return account
|
||||
|
||||
|
||||
|
@ -42,9 +78,13 @@ class BlueskyAccount(SocialAccount):
|
|||
# app_password = jsondata.EncryptedTextField(
|
||||
# json_field_name="access_data", default=""
|
||||
# )
|
||||
base_url = jsondata.CharField(json_field_name="access_data", default=None)
|
||||
session_string = jsondata.EncryptedTextField(
|
||||
json_field_name="access_data", default=""
|
||||
)
|
||||
display_name = jsondata.CharField(json_field_name="account_data", default="")
|
||||
description = jsondata.CharField(json_field_name="account_data", default="")
|
||||
avatar = jsondata.CharField(json_field_name="account_data", default="")
|
||||
|
||||
def on_session_change(self, event, session) -> None:
|
||||
if event in (SessionEvent.CREATE, SessionEvent.REFRESH):
|
||||
|
@ -63,13 +103,46 @@ class BlueskyAccount(SocialAccount):
|
|||
|
||||
@property
|
||||
def url(self):
|
||||
return f"https://bsky.app/profile/{self.handle}"
|
||||
return f"{self.base_url}/profile/{self.handle}"
|
||||
|
||||
def refresh(self, save=True, profile=None):
|
||||
def refresh(self, save=True, did_refresh=True):
|
||||
if did_refresh:
|
||||
did = self.uid
|
||||
did_r = DidResolver()
|
||||
handle_r = HandleResolver(timeout=5)
|
||||
did_doc = did_r.resolve(did)
|
||||
if not did_doc:
|
||||
logger.warning(f"ATProto refresh failed: did {did} -> <missing doc>")
|
||||
return False
|
||||
resolved_handle = did_doc.get_handle()
|
||||
if not resolved_handle:
|
||||
logger.warning(f"ATProto refresh failed: did {did} -> <missing handle>")
|
||||
return False
|
||||
resolved_did = handle_r.resolve(resolved_handle)
|
||||
resolved_pds = did_doc.get_pds_endpoint()
|
||||
if did != resolved_did:
|
||||
logger.warning(
|
||||
f"ATProto refresh failed: did {did} -> handle {resolved_handle} -> did {resolved_did}"
|
||||
)
|
||||
return False
|
||||
if resolved_handle != self.handle:
|
||||
logger.debug(
|
||||
f"ATProto refresh: handle changed for did {did}: handle {self.handle} -> {resolved_handle}"
|
||||
)
|
||||
self.handle = resolved_handle
|
||||
if resolved_pds != self.base_url:
|
||||
logger.debug(
|
||||
f"ATProto refresh: pds changed for did {did}: handle {self.base_url} -> {resolved_pds}"
|
||||
)
|
||||
self.base_url = resolved_pds
|
||||
profile = self._client.me
|
||||
if not profile:
|
||||
_ = self._client
|
||||
profile = self._profile
|
||||
self.handle = profile.handle
|
||||
logger.warning("Bluesky: client not logged in.") # this should not happen
|
||||
return None
|
||||
if self.handle != profile.handle:
|
||||
logger.warning(
|
||||
"ATProto refresh: handle mismatch {self.handle} from did doc -> {profile.handle} from PDS"
|
||||
)
|
||||
self.account_data = {
|
||||
k: v for k, v in profile.__dict__.items() if isinstance(v, (int, str))
|
||||
}
|
||||
|
@ -78,6 +151,7 @@ class BlueskyAccount(SocialAccount):
|
|||
if save:
|
||||
self.save(
|
||||
update_fields=[
|
||||
"access_data",
|
||||
"account_data",
|
||||
"handle",
|
||||
"last_refresh",
|
||||
|
|
|
@ -19,6 +19,7 @@ class APIdentity(models.Model):
|
|||
"""
|
||||
|
||||
user: User
|
||||
user_id: int
|
||||
user = models.OneToOneField(
|
||||
User, models.SET_NULL, related_name="identity", null=True
|
||||
) # type:ignore
|
||||
|
|
|
@ -295,12 +295,12 @@ class User(AbstractUser):
|
|||
return
|
||||
mastodon = self.mastodon
|
||||
threads = self.threads
|
||||
# bluesky = self.bluesky
|
||||
bluesky = self.bluesky
|
||||
changed = False
|
||||
name = (
|
||||
(mastodon.display_name if mastodon else "")
|
||||
or (threads.username if threads else "")
|
||||
# or (bluesky.display_name if bluesky else "")
|
||||
or (bluesky.display_name if bluesky else "")
|
||||
or identity.name
|
||||
or identity.username
|
||||
)
|
||||
|
@ -310,7 +310,7 @@ class User(AbstractUser):
|
|||
summary = (
|
||||
(mastodon.note if mastodon else "")
|
||||
or (threads.threads_biography if threads else "")
|
||||
# or (bluesky.note if bluesky else "")
|
||||
or (bluesky.description if bluesky else "")
|
||||
or identity.summary
|
||||
)
|
||||
if identity.summary != summary:
|
||||
|
@ -326,7 +326,8 @@ class User(AbstractUser):
|
|||
url = mastodon.avatar
|
||||
elif threads and threads.threads_profile_picture_url:
|
||||
url = threads.threads_profile_picture_url
|
||||
# elif bluesky and bluesky.
|
||||
elif bluesky and bluesky.avatar:
|
||||
url = bluesky.avatar
|
||||
if url:
|
||||
try:
|
||||
r = httpx.get(url)
|
||||
|
|
|
@ -145,14 +145,15 @@
|
|||
{% csrf_token %}
|
||||
<input name="method" value="bluesky" type="hidden" />
|
||||
<input required
|
||||
type="email"
|
||||
name="username"
|
||||
autofocus
|
||||
placeholder="{% trans 'Bluesky Login ID' %}"
|
||||
pattern="([a-zA-Z0-9]([a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?\.)+[a-zA-Z]([a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?"
|
||||
placeholder="{% trans 'Bluesky handle' %}"
|
||||
autocorrect="off"
|
||||
autocapitalize="off"
|
||||
autocomplete="off"
|
||||
spellcheck="false" />
|
||||
<small>{% trans "Please input your ATProto handle (e.g. neodb.bsky.social, without the leading @), do not use email." %}</small>
|
||||
<input required
|
||||
type="password"
|
||||
name="password"
|
||||
|
|
Loading…
Add table
Reference in a new issue