Skip to content

Commit

Permalink
migrate and sync user relationship in Mastodon
Browse files Browse the repository at this point in the history
  • Loading branch information
Her Email committed Nov 8, 2023
1 parent edcd80d commit d38325c
Show file tree
Hide file tree
Showing 13 changed files with 181 additions and 53 deletions.
1 change: 1 addition & 0 deletions common/management/commands/cron.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@

from catalog.jobs import * # noqa
from common.models import JobManager
from users.jobs import * # noqa


class Command(BaseCommand):
Expand Down
2 changes: 1 addition & 1 deletion neodb-takahe
19 changes: 14 additions & 5 deletions users/data.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,15 +13,13 @@
from journal.importers.goodreads import GoodreadsImporter
from journal.importers.opml import OPMLImporter
from journal.models import reset_journal_visibility_for_user
from mastodon import mastodon_request_included
from mastodon.api import *
from social.models import reset_social_visibility_for_user

from .account import *
from .tasks import *


@mastodon_request_included
@login_required
def preferences(request):
preference = request.user.preference
Expand Down Expand Up @@ -54,7 +52,6 @@ def preferences(request):
return render(request, "users/preferences.html")


@mastodon_request_included
@login_required
def data(request):
return render(
Expand All @@ -79,15 +76,13 @@ def data_import_status(request):
)


@mastodon_request_included
@login_required
def export_reviews(request):
if request.method != "POST":
return redirect(reverse("users:data"))
return render(request, "users/data.html")


@mastodon_request_included
@login_required
def export_marks(request):
if request.method == "POST":
Expand Down Expand Up @@ -120,6 +115,20 @@ def sync_mastodon(request):
return redirect(reverse("users:info"))


@login_required
def sync_mastodon_preference(request):
if request.method == "POST":
request.user.preference.mastodon_skip_userinfo = (
request.POST.get("mastodon_sync_userinfo", "") == ""
)
request.user.preference.mastodon_skip_relationship = (
request.POST.get("mastodon_sync_relationship", "") == ""
)
request.user.preference.save()
messages.add_message(request, messages.INFO, _("同步设置已保存。"))
return redirect(reverse("users:info"))


@login_required
def reset_visibility(request):
if request.method == "POST":
Expand Down
1 change: 1 addition & 0 deletions users/jobs/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
from .sync import MastodonUserSync
35 changes: 35 additions & 0 deletions users/jobs/sync.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
import pprint
from datetime import timedelta
from time import sleep

from django.utils import timezone
from loguru import logger

from common.models import BaseJob, JobManager
from users.models import Preference, User


@JobManager.register
class MastodonUserSync(BaseJob):
interval = timedelta(hours=2)

def run(self):
logger.info("Mastodon User Sync start.")
count = 0
ttl_hours = 12
qs = (
User.objects.exclude(
preference__mastodon_skip_userinfo=True, mastodon_skip_relationship=True
)
.filter(
mastodon_last_refresh__lt=timezone.now() - timedelta(hours=ttl_hours)
)
.filter(
is_active=True,
)
.exclude(mastodon_token__isnull=True)
.exclude(mastodon_token="")
)
for user in qs:
user.refresh_mastodon_data()
logger.info(f"Mastodon User Sync finished.")
10 changes: 0 additions & 10 deletions users/management/commands/refresh_mastodon.py

This file was deleted.

Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
# Generated by Django 4.2.7 on 2023-11-06 01:46

from django.db import migrations, models
from loguru import logger
from tqdm import tqdm


def migrate_relationships(apps, schema_editor):
User = apps.get_model("users", "User")
APIdentity = apps.get_model("users", "APIdentity")
logger.info(f"Migrate user relationship")
for user in tqdm(User.objects.all()):
for target in user.local_following:
user.identity.follow(User.objects.get(pk=target).identity)
for target in user.local_blocking:
user.identity.block(User.objects.get(pk=target).identity)
for target in user.local_muting:
user.identity.block(User.objects.get(pk=target).identity)
user.sync_relationship()
for user in tqdm(User.objects.all()):
for req in user.identity.following_request:
target_identity = APIdentity.objects.get(pk=req)
target_identity.accept_follow_request(user.identity)


class Migration(migrations.Migration):

dependencies = [
("users", "0013_init_identity"),
]

operations = [
migrations.RunPython(migrate_relationships),
migrations.AddField(
model_name="preference",
name="mastodon_skip_relationship",
field=models.BooleanField(default=False),
),
migrations.AddField(
model_name="preference",
name="mastodon_skip_userinfo",
field=models.BooleanField(default=False),
),
]
2 changes: 1 addition & 1 deletion users/models/apidentity.py
Original file line number Diff line number Diff line change
Expand Up @@ -82,7 +82,7 @@ def avatar(self):
return (
self.takahe_identity.icon.url
if self.takahe_identity.icon
else settings.SITE_INFO["user_icon"]
else self.takahe_identity.icon_uri or settings.SITE_INFO["user_icon"]
)
else:
return f"/proxy/identity_icon/{self.pk}/"
Expand Down
2 changes: 2 additions & 0 deletions users/models/preference.py
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,8 @@ class Preference(models.Model):
show_last_edit = models.PositiveSmallIntegerField(default=0)
no_anonymous_view = models.PositiveSmallIntegerField(default=0)
hidden_categories = models.JSONField(default=list)
mastodon_skip_userinfo = models.BooleanField(null=False, default=False)
mastodon_skip_relationship = models.BooleanField(null=False, default=False)

def __str__(self):
return str(self.user)
50 changes: 43 additions & 7 deletions users/models/user.py
Original file line number Diff line number Diff line change
Expand Up @@ -231,12 +231,44 @@ def clear(self):
self.identity.deleted = timezone.now()
self.identity.save()

def sync_relationships(self):
# FIXME
pass
def sync_relationship(self):
for target in self.mastodon_followers:
t = target.split("@")
target_user = User.objects.filter(
mastodon_username=t[0], mastodon_site=t[1]
).first()
if target_user and not self.identity.is_following(target_user.identity):
self.identity.follow(target_user.identity)
for target in self.mastodon_blocks:
t = target.split("@")
target_user = User.objects.filter(
mastodon_username=t[0], mastodon_site=t[1]
).first()
if target_user and not self.identity.is_blocking(target_user.identity):
self.identity.block(target_user.identity)
for target in self.mastodon_mutes:
t = target.split("@")
target_user = User.objects.filter(
mastodon_username=t[0], mastodon_site=t[1]
).first()
if target_user and not self.identity.is_muting(target_user.identity):
self.identity.mute(target_user.identity)

def sync_identity(self):
identity = self.identity.takahe_identity
identity.name = (
self.mastodon_account.get("display_name")
or identity.name
or identity.username
)
identity.summary = self.mastodon_account.get("note") or identity.summary
identity.manually_approves_followers = self.mastodon_locked
identity.icon_uri = self.mastodon_account.get("avatar")
identity.save()

def refresh_mastodon_data(self):
"""Try refresh account data from mastodon server, return true if refreshed successfully, note it will not save to db"""
logger.debug(f"Refreshing Mastodon data for {self}")
self.mastodon_last_refresh = timezone.now()
code, mastodon_account = verify_account(self.mastodon_site, self.mastodon_token)
if code == 401 and self.mastodon_refresh_token:
Expand All @@ -247,7 +279,6 @@ def refresh_mastodon_data(self):
code, mastodon_account = verify_account(
self.mastodon_site, self.mastodon_token
)
updated = False
if mastodon_account:
self.mastodon_account = mastodon_account
self.mastodon_locked = mastodon_account["locked"]
Expand Down Expand Up @@ -277,12 +308,17 @@ def refresh_mastodon_data(self):
self.mastodon_domain_blocks = get_related_acct_list(
self.mastodon_site, self.mastodon_token, "/api/v1/domain_blocks"
)
self.sync_relationships()
updated = True
self.save()
if not self.preference.mastodon_skip_userinfo:
self.sync_identity()
if not self.preference.mastodon_skip_relationship:
self.sync_relationship()
return True
elif code == 401:
logger.error(f"Refresh mastodon data error 401 for {self}")
self.mastodon_token = ""
return updated
self.save(update_fields=["mastodon_token"])
return False

@property
def unread_announcements(self):
Expand Down
24 changes: 0 additions & 24 deletions users/tasks.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,30 +16,6 @@ def refresh_mastodon_data_task(user_id, token=None):
if token:
user.mastodon_token = token
if user.refresh_mastodon_data():
user.save()
logger.info(f"{user} mastodon data refreshed")
else:
logger.warning(f"{user} mastodon data refresh failed")


def refresh_all_mastodon_data_task(ttl_hours):
logger.info(f"Mastodon data refresh start")
count = 0
for user in tqdm(
User.objects.filter(
mastodon_last_refresh__lt=timezone.now() - timedelta(hours=ttl_hours),
is_active=True,
)
):
if user.mastodon_token or user.mastodon_refresh_token:
logger.info(f"Refreshing {user}")
if user.refresh_mastodon_data():
logger.info(f"Refreshed {user}")
count += 1
else:
logger.warning(f"Refresh failed for {user}")
user.save()
else:
logger.warning(f"Missing token for {user}")
logger.info(f"{count} users updated")
logger.info(f"Mastodon data refresh done")
39 changes: 34 additions & 5 deletions users/templates/users/account.html
Original file line number Diff line number Diff line change
Expand Up @@ -139,20 +139,49 @@
</article>
<article>
<details>
<summary>{% trans '同步联邦宇宙身份和社交关系数据' %}</summary>
<summary>{% trans '同步联邦宇宙信息和社交数据' %}</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 '自动同步用户昵称等基本信息' %}
</label>
</fieldset>
<fieldset>
<label>
<input type="checkbox"
name="mastodon_sync_relationship"
{% if not request.user.preference.mastodon_skip_relationship %}checked{% endif %}>
{% trans '自动导入新增的关注、屏蔽和隐藏列表' %}
</label>
</fieldset>
<input type="submit"
value="{% trans '保存同步设置' %}"
{% if not request.user.mastodon_username %}disabled{% endif %} />
<small>
{{ site_name }}会按照以上设置每天自动导入你在联邦宇宙实例中新增的关注、屏蔽和隐藏列表;
<br>
如果你在联邦宇宙实例中关注的用户加入了NeoDB,你会自动关注她;
<br>
如果你在联邦宇宙实例中取消了关注、屏蔽或隐藏,{{ site_name }}不会自动取消,但你可以手动移除。
</small>
</form>
<form action="{% url 'users:sync_mastodon' %}"
method="post"
enctype="multipart/form-data">
{% csrf_token %}
<small>如果希望立即开始同步,可以点击下方按钮。</small>
<input type="submit"
value="{% trans '同步' %}"
value="{% trans '立即同步' %}"
{% if not request.user.mastodon_username %}disabled{% endif %} />
<small>
{% if request.user.mastodon_last_refresh %}上次更新时间 {{ request.user.mastodon_last_refresh }}{% endif %}
</small>
<div>
为了正确高效的展示短评和评论,{{ site_name }}会缓存你在联邦宇宙的关注、屏蔽和隐藏列表。如果你刚刚更新过帐户的上锁状态、增减过关注、隐藏或屏蔽,希望立即生效,可以点击这里立刻更新;这类信息也会每天自动同步。
</div>
</form>
</details>
</article>
Expand Down
5 changes: 5 additions & 0 deletions users/urls.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,11 @@
path("data/export/reviews", export_reviews, name="export_reviews"),
path("data/export/marks", export_marks, name="export_marks"),
path("data/sync_mastodon", sync_mastodon, name="sync_mastodon"),
path(
"data/sync_mastodon_preference",
sync_mastodon_preference,
name="sync_mastodon_preference",
),
path("data/reset_visibility", reset_visibility, name="reset_visibility"),
path("data/clear_data", clear_data, name="clear_data"),
path("preferences", preferences, name="preferences"),
Expand Down

0 comments on commit d38325c

Please sign in to comment.