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):
|
def delete(self, *args, **kwargs):
|
||||||
if self.local:
|
if self.local:
|
||||||
Takahe.delete_posts(self.all_post_ids)
|
Takahe.delete_posts(self.all_post_ids)
|
||||||
toot_id = (
|
self.delete_crossposts()
|
||||||
(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)
|
|
||||||
return super().delete(*args, **kwargs)
|
return super().delete(*args, **kwargs)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
|
@ -277,6 +271,22 @@ class Piece(PolymorphicModel, UserOwnedObjectMixin):
|
||||||
# subclass may have to add additional code to update type_data in local post
|
# subclass may have to add additional code to update type_data in local post
|
||||||
return p
|
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):
|
def get_crosspost_params(self):
|
||||||
d = {
|
d = {
|
||||||
"visibility": self.visibility,
|
"visibility": self.visibility,
|
||||||
|
@ -305,7 +315,13 @@ class Piece(PolymorphicModel, UserOwnedObjectMixin):
|
||||||
|
|
||||||
activate_language_for_user(self.owner.user)
|
activate_language_for_user(self.owner.user)
|
||||||
metadata = self.metadata.copy()
|
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()
|
params = self.get_crosspost_params()
|
||||||
self.sync_to_mastodon(params_for_platform(params, "mastodon"), update_mode)
|
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_threads(params_for_platform(params, "threads"), update_mode)
|
||||||
|
|
|
@ -1,8 +1,12 @@
|
||||||
|
import re
|
||||||
from functools import cached_property
|
from functools import cached_property
|
||||||
from operator import pos
|
from operator import pos
|
||||||
|
|
||||||
from atproto import Client, SessionEvent, client_utils
|
from atproto import Client, SessionEvent, client_utils
|
||||||
from atproto_client import models
|
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 django.utils import timezone
|
||||||
from loguru import logger
|
from loguru import logger
|
||||||
|
|
||||||
|
@ -12,28 +16,60 @@ from .common import SocialAccount
|
||||||
|
|
||||||
|
|
||||||
class Bluesky:
|
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
|
@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:
|
try:
|
||||||
client = Client()
|
handle_r = HandleResolver(timeout=5)
|
||||||
profile = client.login(username, password)
|
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()
|
session_string = client.export_session_string()
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.debug(f"Bluesky login {username} exception {e}")
|
logger.debug(f"Bluesky login {handle} exception {e}")
|
||||||
return None
|
return
|
||||||
existing_account = BlueskyAccount.objects.filter(
|
account = BlueskyAccount.objects.filter(
|
||||||
uid=profile.did, domain=Bluesky.BASE_DOMAIN
|
uid=profile.did, domain=Bluesky._DOMAIN
|
||||||
).first()
|
).first()
|
||||||
if existing_account:
|
if not account:
|
||||||
existing_account.session_string = session_string
|
account = BlueskyAccount(uid=profile.did, domain=Bluesky._DOMAIN)
|
||||||
existing_account.save(update_fields=["access_data"])
|
account._client = client
|
||||||
existing_account.refresh(save=True, profile=profile)
|
|
||||||
return existing_account
|
|
||||||
account = BlueskyAccount(uid=profile.did, domain=Bluesky.BASE_DOMAIN)
|
|
||||||
account.session_string = session_string
|
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
|
return account
|
||||||
|
|
||||||
|
|
||||||
|
@ -42,9 +78,13 @@ class BlueskyAccount(SocialAccount):
|
||||||
# app_password = jsondata.EncryptedTextField(
|
# app_password = jsondata.EncryptedTextField(
|
||||||
# json_field_name="access_data", default=""
|
# json_field_name="access_data", default=""
|
||||||
# )
|
# )
|
||||||
|
base_url = jsondata.CharField(json_field_name="access_data", default=None)
|
||||||
session_string = jsondata.EncryptedTextField(
|
session_string = jsondata.EncryptedTextField(
|
||||||
json_field_name="access_data", default=""
|
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:
|
def on_session_change(self, event, session) -> None:
|
||||||
if event in (SessionEvent.CREATE, SessionEvent.REFRESH):
|
if event in (SessionEvent.CREATE, SessionEvent.REFRESH):
|
||||||
|
@ -63,13 +103,46 @@ class BlueskyAccount(SocialAccount):
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def url(self):
|
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:
|
if not profile:
|
||||||
_ = self._client
|
logger.warning("Bluesky: client not logged in.") # this should not happen
|
||||||
profile = self._profile
|
return None
|
||||||
self.handle = profile.handle
|
if self.handle != profile.handle:
|
||||||
|
logger.warning(
|
||||||
|
"ATProto refresh: handle mismatch {self.handle} from did doc -> {profile.handle} from PDS"
|
||||||
|
)
|
||||||
self.account_data = {
|
self.account_data = {
|
||||||
k: v for k, v in profile.__dict__.items() if isinstance(v, (int, str))
|
k: v for k, v in profile.__dict__.items() if isinstance(v, (int, str))
|
||||||
}
|
}
|
||||||
|
@ -78,6 +151,7 @@ class BlueskyAccount(SocialAccount):
|
||||||
if save:
|
if save:
|
||||||
self.save(
|
self.save(
|
||||||
update_fields=[
|
update_fields=[
|
||||||
|
"access_data",
|
||||||
"account_data",
|
"account_data",
|
||||||
"handle",
|
"handle",
|
||||||
"last_refresh",
|
"last_refresh",
|
||||||
|
|
|
@ -19,6 +19,7 @@ class APIdentity(models.Model):
|
||||||
"""
|
"""
|
||||||
|
|
||||||
user: User
|
user: User
|
||||||
|
user_id: int
|
||||||
user = models.OneToOneField(
|
user = models.OneToOneField(
|
||||||
User, models.SET_NULL, related_name="identity", null=True
|
User, models.SET_NULL, related_name="identity", null=True
|
||||||
) # type:ignore
|
) # type:ignore
|
||||||
|
|
|
@ -295,12 +295,12 @@ class User(AbstractUser):
|
||||||
return
|
return
|
||||||
mastodon = self.mastodon
|
mastodon = self.mastodon
|
||||||
threads = self.threads
|
threads = self.threads
|
||||||
# bluesky = self.bluesky
|
bluesky = self.bluesky
|
||||||
changed = False
|
changed = False
|
||||||
name = (
|
name = (
|
||||||
(mastodon.display_name if mastodon else "")
|
(mastodon.display_name if mastodon else "")
|
||||||
or (threads.username if threads 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.name
|
||||||
or identity.username
|
or identity.username
|
||||||
)
|
)
|
||||||
|
@ -310,7 +310,7 @@ class User(AbstractUser):
|
||||||
summary = (
|
summary = (
|
||||||
(mastodon.note if mastodon else "")
|
(mastodon.note if mastodon else "")
|
||||||
or (threads.threads_biography if threads else "")
|
or (threads.threads_biography if threads else "")
|
||||||
# or (bluesky.note if bluesky else "")
|
or (bluesky.description if bluesky else "")
|
||||||
or identity.summary
|
or identity.summary
|
||||||
)
|
)
|
||||||
if identity.summary != summary:
|
if identity.summary != summary:
|
||||||
|
@ -326,7 +326,8 @@ class User(AbstractUser):
|
||||||
url = mastodon.avatar
|
url = mastodon.avatar
|
||||||
elif threads and threads.threads_profile_picture_url:
|
elif threads and threads.threads_profile_picture_url:
|
||||||
url = 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:
|
if url:
|
||||||
try:
|
try:
|
||||||
r = httpx.get(url)
|
r = httpx.get(url)
|
||||||
|
|
|
@ -145,14 +145,15 @@
|
||||||
{% csrf_token %}
|
{% csrf_token %}
|
||||||
<input name="method" value="bluesky" type="hidden" />
|
<input name="method" value="bluesky" type="hidden" />
|
||||||
<input required
|
<input required
|
||||||
type="email"
|
|
||||||
name="username"
|
name="username"
|
||||||
autofocus
|
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"
|
autocorrect="off"
|
||||||
autocapitalize="off"
|
autocapitalize="off"
|
||||||
autocomplete="off"
|
autocomplete="off"
|
||||||
spellcheck="false" />
|
spellcheck="false" />
|
||||||
|
<small>{% trans "Please input your ATProto handle (e.g. neodb.bsky.social, without the leading @), do not use email." %}</small>
|
||||||
<input required
|
<input required
|
||||||
type="password"
|
type="password"
|
||||||
name="password"
|
name="password"
|
||||||
|
|
Loading…
Add table
Reference in a new issue