bluesky graph

This commit is contained in:
Your Name 2024-07-05 19:05:50 -04:00 committed by Henri Dickson
parent e29b921d34
commit 196ac2a616
6 changed files with 235 additions and 173 deletions

View file

@ -4,12 +4,14 @@ from functools import cached_property
from atproto import Client, SessionEvent, client_utils
from atproto_client import models
from atproto_client.exceptions import AtProtocolError
from atproto_identity.did.resolver import DidResolver
from atproto_identity.handle.resolver import HandleResolver
from django.utils import timezone
from loguru import logger
from catalog.common import jsondata
from takahe.utils import Takahe
from .common import SocialAccount
@ -172,6 +174,52 @@ class BlueskyAccount(SocialAccount):
)
return True
def refresh_graph(self, save=True) -> bool:
try:
r = self._client.get_followers(self.uid)
self.followers = [p.did for p in r.followers]
r = self._client.get_follows(self.uid)
self.following = [p.did for p in r.follows]
r = self._client.app.bsky.graph.get_mutes(
models.AppBskyGraphGetMutes.Params(cursor=None, limit=None)
)
self.mutes = [p.did for p in r.mutes]
except AtProtocolError as e:
logger.warning(f"{self} refresh_graph error: {e}")
return False
if save:
self.save(
update_fields=[
"followers",
"following",
"mutes",
]
)
return True
def sync_graph(self):
c = 0
def get_identity_ids(accts: list):
return set(
BlueskyAccount.objects.filter(
domain=Bluesky._DOMAIN, uid__in=accts
).values_list("user__identity", flat=True)
)
me = self.user.identity.pk
for target_identity in get_identity_ids(self.following):
if not Takahe.get_is_following(me, target_identity):
Takahe.follow(me, target_identity, True)
c += 1
for target_identity in get_identity_ids(self.mutes):
if not Takahe.get_is_muting(me, target_identity):
Takahe.mute(me, target_identity)
c += 1
return c
def post(
self,
content,

View file

@ -44,13 +44,13 @@ class SocialAccount(TypedModel):
last_refresh = models.DateTimeField(default=None, null=True)
last_reachable = models.DateTimeField(default=None, null=True)
sync_profile = jsondata.BooleanField(
json_field_name="preference_data", default=True
)
sync_graph = jsondata.BooleanField(json_field_name="preference_data", default=True)
sync_timeline = jsondata.BooleanField(
json_field_name="preference_data", default=True
)
# sync_profile = jsondata.BooleanField(
# json_field_name="preference_data", default=True
# )
# sync_graph = jsondata.BooleanField(json_field_name="preference_data", default=True)
# sync_timeline = jsondata.BooleanField(
# json_field_name="preference_data", default=True
# )
class Meta:
indexes = [
@ -124,3 +124,6 @@ class SocialAccount(TypedModel):
self.refresh_graph()
logger.debug(f"{self} refreshed")
return True
def sync_graph(self) -> int:
return 0

View file

@ -778,6 +778,46 @@ class MastodonAccount(SocialAccount):
)
return True
def sync_graph(self):
c = 0
def get_identity_ids(accts: list):
return set(
MastodonAccount.objects.filter(handle__in=accts).values_list(
"user__identity", flat=True
)
)
def get_identity_ids_in_domains(domains: list):
return set(
MastodonAccount.objects.filter(domain__in=domains).values_list(
"user__identity", flat=True
)
)
me = self.user.identity.pk
for target_identity in get_identity_ids(self.following):
if not Takahe.get_is_following(me, target_identity):
Takahe.follow(me, target_identity, True)
c += 1
for target_identity in get_identity_ids(self.blocks):
if not Takahe.get_is_blocking(me, target_identity):
Takahe.block(me, target_identity)
c += 1
for target_identity in get_identity_ids_in_domains(self.domain_blocks):
if not Takahe.get_is_blocking(me, target_identity):
Takahe.block(me, target_identity)
c += 1
for target_identity in get_identity_ids(self.mutes):
if not Takahe.get_is_muting(me, target_identity):
Takahe.mute(me, target_identity)
c += 1
return c
def boost(self, post_url: str):
boost_toot(self._api_domain, self.access_token, post_url)

View file

@ -254,40 +254,6 @@ class User(AbstractUser):
self.identity.save()
self.social_accounts.all().delete()
def sync_relationship(self):
def get_identity_ids(accts: list):
return set(
MastodonAccount.objects.filter(handle__in=accts).values_list(
"user__identity", flat=True
)
)
def get_identity_ids_in_domains(domains: list):
return set(
MastodonAccount.objects.filter(domain__in=domains).values_list(
"user__identity", flat=True
)
)
me = self.identity.pk
if not self.mastodon:
return
for target_identity in get_identity_ids(self.mastodon.following):
if not Takahe.get_is_following(me, target_identity):
Takahe.follow(me, target_identity, True)
for target_identity in get_identity_ids(self.mastodon.blocks):
if not Takahe.get_is_blocking(me, target_identity):
Takahe.block(me, target_identity)
for target_identity in get_identity_ids_in_domains(self.mastodon.domain_blocks):
if not Takahe.get_is_blocking(me, target_identity):
Takahe.block(me, target_identity)
for target_identity in get_identity_ids(self.mastodon.mutes):
if not Takahe.get_is_muting(me, target_identity):
Takahe.mute(me, target_identity)
def sync_identity(self):
"""sync display name, bio, and avatar from available sources"""
identity = self.identity.takahe_identity
@ -356,17 +322,18 @@ class User(AbstractUser):
if skip_graph:
return
if not self.preference.mastodon_skip_relationship:
self.sync_relationship()
return
c = 0
for account in self.social_accounts.all():
c += account.sync_graph()
if c:
logger.debug(f"{self} graph updated with {c} new relationship.")
@staticmethod
def sync_accounts_task(user_id):
user = User.objects.get(pk=user_id)
logger.info(f"{user} accounts sync start")
if user.sync_accounts():
logger.info(f"{user} accounts sync done")
else:
logger.warning(f"{user} accounts sync failed")
user.sync_accounts()
logger.info(f"{user} accounts sync end")
def sync_accounts_later(self):
django_rq.get_queue("mastodon").enqueue(User.sync_accounts_task, self.pk)

View file

@ -129,143 +129,144 @@
{% endif %}
</details>
</article>
{% if enable_threads %}
<article>
<details>
<summary>{% trans "Threads.net" %}</summary>
<form action="{% url 'mastodon:threads_reconnect' %}" method="post">
{% csrf_token %}
<fieldset>
{% if request.user.threads %}
<label>
<i class="fa-brands fa-threads"></i> {% trans "Verified threads.net account" %}
<input type="input"
aria-invalid="false"
value="{{ request.user.threads.handle }}"
readonly>
<small>
{% if request.user.threads.last_refresh %}
{% trans "Last updated" %} {{ request.user.threads.last_refresh }}
{% endif %}
</small>
</label>
{% endif %}
<input type="submit"
value="{% if request.user.threads %} {% trans 'Link with a different threads.net account' %} {% else %} {% trans "Link with a threads.net account" %} {% endif %} " />
</fieldset>
</form>
{% if request.user.threads %}
<form action="{% url 'mastodon:threads_disconnect' %}"
method="post"
onsubmit="return confirm('{% trans "Once disconnected, you will no longer be able login with this identity. Are you sure to continue?" %}')">
{% csrf_token %}
<input type="submit"
value="{% trans 'Disconnect with Threads' %}"
class="secondary" />
</form>
{% endif %}
</details>
</article>
{% endif %}
{% if enable_bluesky %}
<article>
<details>
<summary>{% trans "Bluesky (ATProto)" %}</summary>
<form action="{% url 'mastodon:bluesky_reconnect' %}" method="post">
{% csrf_token %}
<fieldset>
{% if request.user.bluesky %}
<label>
<i class="fa-brands fa-bluesky"></i> {% trans "Verified ATProto identity" %}
<input type="input"
aria-invalid="false"
value="@{{ request.user.bluesky.handle }} {{ request.user.bluesky.uid }}"
readonly>
<small>
{% if request.user.bluesky.last_refresh %}
{% trans "Last updated" %} {{ request.user.bluesky.last_refresh }}
{% endif %}
</small>
</label>
{% endif %}
<input required
name="username"
autofocus
placeholder="{% trans 'Bluesky Login ID' %}"
autocorrect="off"
autocapitalize="off"
autocomplete="off"
spellcheck="false" />
<input required
type="password"
name="password"
placeholder="{% trans 'Bluesky app password' %}"
autocorrect="off"
autocapitalize="off"
autocomplete="off"
spellcheck="false" />
<input type="submit"
value="{% if request.user.bluesky %} {% trans 'Link with a different ATProto identity' %} {% else %} {% trans "Link with an ATProto identity" %} {% endif %} " />
<small>{% blocktrans %}App password can be created on <a href="https://bsky.app/settings/app-passwords" target="_blank">bsky.app</a>.{% endblocktrans %}</small>
</fieldset>
</form>
{% if request.user.bluesky %}
<form action="{% url 'mastodon:bluesky_disconnect' %}"
method="post"
onsubmit="return confirm('{% trans "Once disconnected, you will no longer be able login with this identity. Are you sure to continue?" %}')">
{% csrf_token %}
<input type="submit"
value="{% trans 'Disconnect with ATProto identity' %}"
class="secondary" />
</form>
{% endif %}
</details>
</article>
{% endif %}
{% endif %}
{% if request.user.social_accounts.all %}
<article>
<details>
<summary>{% trans "Threads.net" %}</summary>
<form action="{% url 'mastodon:threads_reconnect' %}" method="post">
<summary>{% trans 'Sync and import social account' %}</summary>
<form action="{% url 'users:sync_mastodon_preference' %}"
method="post"
enctype="multipart/form-data">
{% csrf_token %}
<fieldset>
{% if request.user.threads %}
<label>
<i class="fa-brands fa-threads"></i> {% trans "Verified threads.net account" %}
<input type="input"
aria-invalid="false"
value="{{ request.user.threads.handle }}"
readonly>
<small>
{% if request.user.threads.last_refresh %}
{% trans "Last updated" %} {{ request.user.threads.last_refresh }}
{% endif %}
</small>
</label>
{% endif %}
<input type="submit"
value="{% if request.user.threads %} {% trans 'Link with a different threads.net account' %} {% else %} {% trans "Link with a threads.net account" %} {% endif %} " />
<label>
<input type="checkbox"
name="mastodon_sync_userinfo"
{% if not request.user.preference.mastodon_skip_userinfo %}checked{% endif %}>
{% trans 'Sync display name, bio and avatar' %}
</label>
</fieldset>
</form>
{% if request.user.threads %}
<form action="{% url 'mastodon:threads_disconnect' %}"
method="post"
onsubmit="return confirm('{% trans "Once disconnected, you will no longer be able login with this identity. Are you sure to continue?" %}')">
{% csrf_token %}
<input type="submit"
value="{% trans 'Disconnect with Threads' %}"
class="secondary" />
</form>
{% endif %}
</details>
</article>
<article>
<details>
<summary>{% trans "Bluesky (ATProto)" %}</summary>
<form action="{% url 'mastodon:bluesky_reconnect' %}" method="post">
{% csrf_token %}
<fieldset>
{% if request.user.bluesky %}
<label>
<i class="fa-brands fa-bluesky"></i> {% trans "Verified ATProto identity" %}
<input type="input"
aria-invalid="false"
value="@{{ request.user.bluesky.handle }} {{ request.user.bluesky.uid }}"
readonly>
<small>
{% if request.user.bluesky.last_refresh %}
{% trans "Last updated" %} {{ request.user.bluesky.last_refresh }}
{% endif %}
</small>
</label>
{% endif %}
<input required
type="email"
name="username"
autofocus
placeholder="{% trans 'Bluesky Login ID' %}"
autocorrect="off"
autocapitalize="off"
autocomplete="off"
spellcheck="false" />
<input required
type="password"
name="password"
placeholder="{% trans 'Bluesky app password' %}"
autocorrect="off"
autocapitalize="off"
autocomplete="off"
spellcheck="false" />
<input type="submit"
value="{% if request.user.bluesky %} {% trans 'Link with a different ATProto identity' %} {% else %} {% trans "Link with an ATProto identity" %} {% endif %} " />
<small>{% blocktrans %}App password can be created on <a href="https://bsky.app/settings/app-passwords" target="_blank">bsky.app</a>.{% endblocktrans %}</small>
<label>
<input type="checkbox"
name="mastodon_sync_relationship"
{% if not request.user.preference.mastodon_skip_relationship %}checked{% endif %}>
{% trans 'Sync follow, mute and block' %}
</label>
</fieldset>
<input type="submit" value="{% trans 'Save sync settings' %}" />
<small>
{% trans "New follow, mute and blocks in the associated identity may be automatically imported; removal has to be done manually." %}
</small>
</form>
<form action="{% url 'users:sync_mastodon' %}"
method="post"
enctype="multipart/form-data">
{% csrf_token %}
<small>{% trans "Click button below to start sync now." %}</small>
<input type="submit" value="{% trans 'Sync now' %}" />
<small>
{% if request.user.mastodon.last_refresh %}
{% trans "Last updated" %} {{ request.user.mastodon.last_refresh }}
{% endif %}
</small>
</form>
{% if request.user.bluesky %}
<form action="{% url 'mastodon:bluesky_disconnect' %}"
method="post"
onsubmit="return confirm('{% trans "Once disconnected, you will no longer be able login with this identity. Are you sure to continue?" %}')">
{% csrf_token %}
<input type="submit"
value="{% trans 'Disconnect with ATProto identity' %}"
class="secondary" />
</form>
{% endif %}
</details>
</article>
{% endif %}
<article>
<details>
<summary>{% trans 'Sync and import social account' %}</summary>
<form action="{% url 'users:sync_mastodon_preference' %}"
method="post"
enctype="multipart/form-data">
{% csrf_token %}
<fieldset>
<label>
<input type="checkbox"
name="mastodon_sync_userinfo"
{% if not request.user.preference.mastodon_skip_userinfo %}checked{% endif %}>
{% trans 'Sync display name, bio and avatar' %}
</label>
</fieldset>
<fieldset>
<label>
<input type="checkbox"
name="mastodon_sync_relationship"
{% if not request.user.preference.mastodon_skip_relationship %}checked{% endif %}>
{% trans 'Sync follow, mute and block' %}
</label>
</fieldset>
<input type="submit"
value="{% trans 'Save sync settings' %}"
{% if not request.user.mastodon_username %}disabled{% endif %} />
<small>
{% trans "New follow, mute and blocks in the associated identity may be automatically imported; removal has to be done manually." %}
</small>
</form>
<form action="{% url 'users:sync_mastodon' %}"
method="post"
enctype="multipart/form-data">
{% csrf_token %}
<small>{% trans "Click button below to start sync now." %}</small>
<input type="submit"
value="{% trans 'Sync now' %}"
{% if not request.user.mastodon_username %}disabled{% endif %} />
<small>
{% if request.user.mastodon.last_refresh %}
{% trans "Last updated" %} {{ request.user.mastodon.last_refresh }}
{% endif %}
</small>
</form>
</details>
</article>
<article>
<details>
<summary>{% trans 'Users you are following' %}</summary>

View file

@ -37,6 +37,9 @@ def account_info(request):
"users/account.html",
{
"allow_any_site": settings.MASTODON_ALLOW_ANY_SITE,
"enable_email": settings.ENABLE_LOGIN_EMAIL,
"enable_threads": settings.ENABLE_LOGIN_THREADS,
"enable_bluesky": settings.ENABLE_LOGIN_BLUESKY,
"profile_form": profile_form,
},
)