local mute and block
This commit is contained in:
parent
8feebf8e5a
commit
d601dde535
19 changed files with 621 additions and 112 deletions
|
@ -135,6 +135,10 @@ details {
|
|||
color: var(--pico-primary);
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.tag-list a:not(:hover) {
|
||||
color: white;
|
||||
}
|
||||
}
|
||||
|
||||
form img {
|
||||
|
|
|
@ -32,7 +32,6 @@
|
|||
</section>
|
||||
{% endif %}
|
||||
{% if show_profile %}
|
||||
{% current_user_relationship user as relationship %}
|
||||
<section class="profile">
|
||||
<article>
|
||||
<details class="auto-collapse" open>
|
||||
|
@ -56,46 +55,21 @@
|
|||
</a>
|
||||
</div>
|
||||
</hgroup>
|
||||
{% if relationship.status %}
|
||||
<div class="tag-list">
|
||||
<span><a>{{ relationship.status }}</a></span>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
</summary>
|
||||
<span class="action">
|
||||
{% if user.locked %}
|
||||
<span>
|
||||
<a title="用户已开启关注审核"
|
||||
hx-post="{% url 'users:locked' user.handler %}"
|
||||
hx-target="body"
|
||||
hx-swap="beforeend">
|
||||
<i class="fa-solid fa-user-shield"></i>
|
||||
</a>
|
||||
</span>
|
||||
{% elif user != request.user %}
|
||||
{% if not relationship.following %}
|
||||
<span class="action" id="profile_actions">
|
||||
{% if user == request.user %}
|
||||
{% if user.locked %}
|
||||
<span>
|
||||
<a title="关注ta"
|
||||
hx-post="{% url 'users:follow' user.handler %}"
|
||||
hx-swap="outerHTML">
|
||||
<i class="fa-solid fa-user-plus"></i>
|
||||
<a title="你已开启关注审核">
|
||||
<i class="fa-solid fa-user-shield"></i>
|
||||
</a>
|
||||
</span>
|
||||
{% elif relationship.unfollowable %}
|
||||
<span>
|
||||
<a title="取消关注"
|
||||
hx-post="{% url 'users:unfollow' user.handler %}"
|
||||
hx-swap="outerHTML">
|
||||
<i class="fa-solid fa-user-xmark"></i>
|
||||
</a>
|
||||
</span>
|
||||
{% else %}
|
||||
<span><a title="已在联邦宇宙关注了ta"><i class="fa-solid fa-user-check"></i></a></span>
|
||||
{% endif %}
|
||||
{% else %}
|
||||
{% include 'users/profile_actions.html' %}
|
||||
{% endif %}
|
||||
{% comment %} <span><a href="{% url 'users:report' %}?user_id={{ user.id }}">{% trans '投诉用户' %}</a></span> {% endcomment %}
|
||||
</span>
|
||||
<p>{{ user.mastodon_account.note|bleach:"a,p,span,br" }}</p>
|
||||
</details>
|
||||
|
|
|
@ -18,26 +18,28 @@ def current_user_relationship(context, user):
|
|||
current_user = context["request"].user
|
||||
r = {
|
||||
"following": False,
|
||||
"followable": False,
|
||||
"unfollowable": False,
|
||||
"muting": False,
|
||||
"unmutable": False,
|
||||
"rejecting": False,
|
||||
"status": "",
|
||||
}
|
||||
if (
|
||||
current_user
|
||||
and current_user.is_authenticated
|
||||
and current_user != user
|
||||
and not current_user.is_blocked_by(user)
|
||||
):
|
||||
if current_user.is_following(user):
|
||||
r["following"] = True
|
||||
if user in current_user.local_following.all():
|
||||
r["unfollowable"] = True
|
||||
if current_user.is_followed_by(user):
|
||||
r["status"] = _("互相关注")
|
||||
else:
|
||||
r["status"] = _("已关注")
|
||||
if current_user and current_user.is_authenticated and current_user != user:
|
||||
if current_user.is_blocking(user) or user.is_blocking(current_user):
|
||||
r["rejecting"] = True
|
||||
else:
|
||||
r["followable"] = True
|
||||
if current_user.is_followed_by(user):
|
||||
r["status"] = _("被ta关注")
|
||||
r["muting"] = current_user.is_muting(user)
|
||||
if user in current_user.local_muting.all():
|
||||
r["unmutable"] = current_user
|
||||
if current_user.is_following(user):
|
||||
r["following"] = True
|
||||
if user in current_user.local_following.all():
|
||||
r["unfollowable"] = True
|
||||
if current_user.is_followed_by(user):
|
||||
r["status"] = _("互相关注")
|
||||
else:
|
||||
r["status"] = _("已关注")
|
||||
else:
|
||||
if current_user.is_followed_by(user):
|
||||
r["status"] = _("被ta关注")
|
||||
return r
|
||||
|
|
|
@ -17,11 +17,7 @@ class UserOwnedObjectMixin:
|
|||
return self.visibility == 0
|
||||
if self.visibility == 2:
|
||||
return False
|
||||
if (
|
||||
viewer.is_blocking(owner)
|
||||
or owner.is_blocking(viewer)
|
||||
or viewer.is_muting(owner)
|
||||
):
|
||||
if viewer.is_blocking(owner) or owner.is_blocking(viewer):
|
||||
return False
|
||||
if self.visibility == 1:
|
||||
return viewer.is_following(owner)
|
||||
|
|
|
@ -61,7 +61,7 @@ def query_visible(user):
|
|||
Q(visibility=0)
|
||||
| Q(owner_id__in=user.following if user.is_authenticated else [], visibility=1)
|
||||
| Q(owner_id=user.id)
|
||||
)
|
||||
) & ~Q(owner_id__in=user.ignoring)
|
||||
|
||||
|
||||
def query_following(user):
|
||||
|
|
|
@ -55,7 +55,8 @@ class ActivityManager:
|
|||
self.owner = user
|
||||
|
||||
def get_timeline(self, before_time=None):
|
||||
q = Q(owner_id__in=self.owner.following, visibility__lt=2) | Q(owner=self.owner)
|
||||
following = [x for x in self.owner.following if x not in self.owner.ignoring]
|
||||
q = Q(owner_id__in=following, visibility__lt=2) | Q(owner=self.owner)
|
||||
if before_time:
|
||||
q = q & Q(created_time__lt=before_time)
|
||||
return (
|
||||
|
|
|
@ -42,7 +42,7 @@ class SocialTest(TestCase):
|
|||
# bob follows alice, see 2 activities
|
||||
self.bob.mastodon_following = ["Alice@MySpace"]
|
||||
self.alice.mastodon_follower = ["Bob@KKCity"]
|
||||
self.bob.following = self.bob.get_following_ids()
|
||||
self.bob.following = self.bob.merge_following_ids()
|
||||
timeline2 = self.bob.activity_manager.get_timeline()
|
||||
self.assertEqual(len(timeline2), 2)
|
||||
|
||||
|
|
|
@ -123,7 +123,7 @@ def sync_mastodon(request):
|
|||
refresh_mastodon_data_task, request.user.pk
|
||||
)
|
||||
messages.add_message(request, messages.INFO, _("同步已开始。"))
|
||||
return redirect(reverse("users:data"))
|
||||
return redirect(reverse("users:info"))
|
||||
|
||||
|
||||
@login_required
|
||||
|
|
|
@ -11,7 +11,7 @@ class Command(BaseCommand):
|
|||
def handle(self, *args, **options):
|
||||
count = 0
|
||||
for user in tqdm(User.objects.all()):
|
||||
user.following = user.get_following_ids()
|
||||
user.following = user.merge_following_ids()
|
||||
if user.following:
|
||||
count += 1
|
||||
user.save(update_fields=["following"])
|
||||
|
|
|
@ -1,30 +1,9 @@
|
|||
from django.core.management.base import BaseCommand
|
||||
from users.models import User
|
||||
from datetime import timedelta
|
||||
from django.utils import timezone
|
||||
from tqdm import tqdm
|
||||
from users.tasks import refresh_all_mastodon_data_task
|
||||
|
||||
|
||||
class Command(BaseCommand):
|
||||
help = "Refresh Mastodon data for all users if not updated in last 24h"
|
||||
|
||||
def handle(self, *args, **options):
|
||||
count = 0
|
||||
for user in tqdm(
|
||||
User.objects.filter(
|
||||
mastodon_last_refresh__lt=timezone.now() - timedelta(hours=24),
|
||||
is_active=True,
|
||||
)
|
||||
):
|
||||
if user.mastodon_token or user.mastodon_refresh_token:
|
||||
tqdm.write(f"Refreshing {user}")
|
||||
if user.refresh_mastodon_data():
|
||||
tqdm.write(f"Refreshed {user}")
|
||||
count += 1
|
||||
else:
|
||||
tqdm.write(f"Refresh failed for {user}")
|
||||
user.save()
|
||||
else:
|
||||
tqdm.write(f"Missing token for {user}")
|
||||
|
||||
print(f"{count} users updated")
|
||||
refresh_all_mastodon_data_task(24)
|
||||
|
|
104
users/migrations/0010_add_local_mute_block.py
Normal file
104
users/migrations/0010_add_local_mute_block.py
Normal file
|
@ -0,0 +1,104 @@
|
|||
# Generated by Django 4.2.3 on 2023-07-07 07:22
|
||||
|
||||
from django.conf import settings
|
||||
from django.db import migrations, models
|
||||
import django.db.models.deletion
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
dependencies = [
|
||||
("users", "0009_add_local_follow"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name="user",
|
||||
name="muting",
|
||||
field=models.JSONField(default=list),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name="user",
|
||||
name="rejecting",
|
||||
field=models.JSONField(default=list),
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name="Mute",
|
||||
fields=[
|
||||
(
|
||||
"id",
|
||||
models.BigAutoField(
|
||||
auto_created=True,
|
||||
primary_key=True,
|
||||
serialize=False,
|
||||
verbose_name="ID",
|
||||
),
|
||||
),
|
||||
("created_time", models.DateTimeField(auto_now_add=True)),
|
||||
("edited_time", models.DateTimeField(auto_now=True)),
|
||||
(
|
||||
"owner",
|
||||
models.ForeignKey(
|
||||
on_delete=django.db.models.deletion.CASCADE,
|
||||
related_name="+",
|
||||
to=settings.AUTH_USER_MODEL,
|
||||
),
|
||||
),
|
||||
(
|
||||
"target",
|
||||
models.ForeignKey(
|
||||
on_delete=django.db.models.deletion.CASCADE,
|
||||
related_name="+",
|
||||
to=settings.AUTH_USER_MODEL,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name="Block",
|
||||
fields=[
|
||||
(
|
||||
"id",
|
||||
models.BigAutoField(
|
||||
auto_created=True,
|
||||
primary_key=True,
|
||||
serialize=False,
|
||||
verbose_name="ID",
|
||||
),
|
||||
),
|
||||
("created_time", models.DateTimeField(auto_now_add=True)),
|
||||
("edited_time", models.DateTimeField(auto_now=True)),
|
||||
(
|
||||
"owner",
|
||||
models.ForeignKey(
|
||||
on_delete=django.db.models.deletion.CASCADE,
|
||||
related_name="+",
|
||||
to=settings.AUTH_USER_MODEL,
|
||||
),
|
||||
),
|
||||
(
|
||||
"target",
|
||||
models.ForeignKey(
|
||||
on_delete=django.db.models.deletion.CASCADE,
|
||||
related_name="+",
|
||||
to=settings.AUTH_USER_MODEL,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name="user",
|
||||
name="local_blocking",
|
||||
field=models.ManyToManyField(
|
||||
related_name="local_blocked_by",
|
||||
through="users.Block",
|
||||
to=settings.AUTH_USER_MODEL,
|
||||
),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name="user",
|
||||
name="local_muting",
|
||||
field=models.ManyToManyField(
|
||||
related_name="+", through="users.Mute", to=settings.AUTH_USER_MODEL
|
||||
),
|
||||
),
|
||||
]
|
208
users/models.py
208
users/models.py
|
@ -1,3 +1,4 @@
|
|||
from functools import cached_property
|
||||
import re
|
||||
from django.core import validators
|
||||
from django.core.exceptions import ValidationError
|
||||
|
@ -13,7 +14,8 @@ from django.conf import settings
|
|||
from management.models import Announcement
|
||||
from mastodon.api import *
|
||||
from django.urls import reverse
|
||||
from django.db.models import Q
|
||||
from django.db.models import Q, F, Value
|
||||
from django.db.models.functions import Concat
|
||||
from django.templatetags.static import static
|
||||
import hashlib
|
||||
|
||||
|
@ -78,8 +80,24 @@ class User(AbstractUser):
|
|||
symmetrical=False,
|
||||
related_name="local_followers",
|
||||
)
|
||||
local_blocking = models.ManyToManyField(
|
||||
through="Block",
|
||||
to="self",
|
||||
through_fields=("owner", "target"),
|
||||
symmetrical=False,
|
||||
related_name="local_blocked_by",
|
||||
)
|
||||
local_muting = models.ManyToManyField(
|
||||
through="Mute",
|
||||
to="self",
|
||||
through_fields=("owner", "target"),
|
||||
symmetrical=False,
|
||||
related_name="+",
|
||||
)
|
||||
following = models.JSONField(default=list)
|
||||
# followers = models.JSONField(default=list)
|
||||
muting = models.JSONField(default=list)
|
||||
# rejecting = local/external blocking + local/external blocked_by + domain_blocking + domain_blocked_by
|
||||
rejecting = models.JSONField(default=list)
|
||||
mastodon_id = models.CharField(max_length=100, default=None, null=True)
|
||||
mastodon_username = models.CharField(max_length=100, default=None, null=True)
|
||||
mastodon_site = models.CharField(max_length=100, default=None, null=True)
|
||||
|
@ -127,7 +145,7 @@ class User(AbstractUser):
|
|||
),
|
||||
]
|
||||
|
||||
@property
|
||||
@cached_property
|
||||
def mastodon_acct(self):
|
||||
return (
|
||||
f"{self.mastodon_username}@{self.mastodon_site}"
|
||||
|
@ -171,30 +189,98 @@ class User(AbstractUser):
|
|||
def __str__(self):
|
||||
return f'{self.pk}:{self.username or ""}:{self.mastodon_acct}'
|
||||
|
||||
@property
|
||||
def ignoring(self):
|
||||
return self.muting + self.rejecting
|
||||
|
||||
def follow(self, target: "User"):
|
||||
if (
|
||||
target is None
|
||||
or target.pk in self.following
|
||||
or target.locked
|
||||
or self.mastodon_acct in target.mastodon_blocks
|
||||
or target.mastodon_acct in self.mastodon_blocks
|
||||
or self.is_following(target)
|
||||
or self.is_blocking(target)
|
||||
or self.is_blocked_by(target)
|
||||
):
|
||||
return False
|
||||
self.local_following.add(target)
|
||||
self.following.append(target.pk)
|
||||
self.save()
|
||||
self.save(update_fields=["following"])
|
||||
return True
|
||||
|
||||
def unfollow(self, target: "User"):
|
||||
print(target)
|
||||
print(target in self.local_following.all())
|
||||
if target and target in self.local_following.all():
|
||||
self.local_following.remove(target)
|
||||
try:
|
||||
if (
|
||||
target.pk in self.following
|
||||
and target.mastodon_acct not in self.mastodon_following
|
||||
):
|
||||
self.following.remove(target.pk)
|
||||
self.save(update_fields=["following"])
|
||||
return True
|
||||
return False
|
||||
|
||||
def remove_follower(self, target: "User"):
|
||||
if target is None or self not in target.local_following.all():
|
||||
return False
|
||||
target.local_following.remove(self)
|
||||
if (
|
||||
self.pk in target.following
|
||||
and self.mastodon_acct not in target.mastodon_following
|
||||
):
|
||||
target.following.remove(self.pk)
|
||||
target.save(update_fields=["following"])
|
||||
return True
|
||||
|
||||
def block(self, target: "User"):
|
||||
if target is None or target in self.local_blocking.all():
|
||||
return False
|
||||
self.local_blocking.add(target)
|
||||
if target.pk in self.following:
|
||||
self.following.remove(target.pk)
|
||||
self.save(update_fields=["following"])
|
||||
if self.pk in target.following:
|
||||
target.following.remove(self.pk)
|
||||
target.save(update_fields=["following"])
|
||||
if target.pk not in self.rejecting:
|
||||
self.rejecting.append(target.pk)
|
||||
self.save(update_fields=["rejecting"])
|
||||
if self.pk not in target.rejecting:
|
||||
target.rejecting.append(self.pk)
|
||||
target.save(update_fields=["rejecting"])
|
||||
return True
|
||||
|
||||
def unblock(self, target: "User"):
|
||||
if target and target in self.local_blocking.all():
|
||||
self.local_blocking.remove(target)
|
||||
if not self.is_blocked_by(target):
|
||||
if target.pk in self.rejecting:
|
||||
self.rejecting.remove(target.pk)
|
||||
self.save(update_fields=["rejecting"])
|
||||
if self.pk in target.rejecting:
|
||||
target.rejecting.remove(self.pk)
|
||||
target.save(update_fields=["rejecting"])
|
||||
return True
|
||||
return False
|
||||
|
||||
def mute(self, target: "User"):
|
||||
if (
|
||||
target is None
|
||||
or target in self.local_muting.all()
|
||||
or target.mastodon_acct in self.mastodon_mutes
|
||||
):
|
||||
return False
|
||||
self.local_muting.add(target)
|
||||
if target.pk not in self.muting:
|
||||
self.muting.append(target.pk)
|
||||
self.save()
|
||||
return True
|
||||
|
||||
def unmute(self, target: "User"):
|
||||
if target and target in self.local_muting.all():
|
||||
self.local_muting.remove(target)
|
||||
if target.pk in self.muting:
|
||||
self.muting.remove(target.pk)
|
||||
self.save()
|
||||
except ValueError:
|
||||
pass
|
||||
return True
|
||||
return False
|
||||
|
||||
|
@ -225,6 +311,40 @@ class User(AbstractUser):
|
|||
self.mastodon_domain_blocks = []
|
||||
self.mastodon_account = {}
|
||||
|
||||
def merge_relationships(self):
|
||||
self.muting = self.merged_muting_ids()
|
||||
self.rejecting = self.merged_rejecting_ids()
|
||||
# caculate following after rejecting is merged
|
||||
self.following = self.merged_following_ids()
|
||||
|
||||
@classmethod
|
||||
def merge_rejected_by(cls):
|
||||
"""
|
||||
Caculate rejecting field to include blocked by for external users
|
||||
Should be invoked after invoking merge_relationships() for all users
|
||||
"""
|
||||
# FIXME this is quite inifficient, should only invoked in async task
|
||||
external_users = list(
|
||||
cls.objects.filter(mastodon_username__isnull=False, is_active=True)
|
||||
)
|
||||
reject_changed = []
|
||||
follow_changed = []
|
||||
for u in external_users:
|
||||
for v in external_users:
|
||||
if v.pk in u.rejecting and u.pk not in v.rejecting:
|
||||
v.rejecting.append(u.pk)
|
||||
if v not in reject_changed:
|
||||
reject_changed.append(v)
|
||||
if u.pk in v.following:
|
||||
v.following.remove(u.pk)
|
||||
if v not in follow_changed:
|
||||
follow_changed.append(v)
|
||||
for u in reject_changed:
|
||||
u.save(update_fields=["rejecting"])
|
||||
for u in follow_changed:
|
||||
u.save(update_fields=["following"])
|
||||
return len(follow_changed) + len(reject_changed)
|
||||
|
||||
def refresh_mastodon_data(self):
|
||||
"""Try refresh account data from mastodon server, return true if refreshed successfully, note it will not save to db"""
|
||||
self.mastodon_last_refresh = timezone.now()
|
||||
|
@ -267,14 +387,14 @@ class User(AbstractUser):
|
|||
self.mastodon_domain_blocks = get_related_acct_list(
|
||||
self.mastodon_site, self.mastodon_token, "/api/v1/domain_blocks"
|
||||
)
|
||||
self.following = self.get_following_ids()
|
||||
self.merge_relationships()
|
||||
updated = True
|
||||
elif code == 401:
|
||||
print(f"401 {self}")
|
||||
self.mastodon_token = ""
|
||||
return updated
|
||||
|
||||
def get_following_ids(self):
|
||||
def merged_following_ids(self):
|
||||
fl = []
|
||||
for m in self.mastodon_following:
|
||||
target = User.get(m)
|
||||
|
@ -286,12 +406,52 @@ class User(AbstractUser):
|
|||
for user in self.local_following.all():
|
||||
if user.pk not in fl and not user.locked and not user.is_blocking(self):
|
||||
fl.append(user.pk)
|
||||
return fl
|
||||
fl = [x for x in fl if x not in self.rejecting]
|
||||
return sorted(fl)
|
||||
|
||||
def merged_muting_ids(self):
|
||||
external_muting_user_ids = list(
|
||||
User.objects.all()
|
||||
.annotate(acct=Concat("mastodon_username", Value("@"), "mastodon_site"))
|
||||
.filter(acct__in=self.mastodon_mutes)
|
||||
.values_list("pk", flat=True)
|
||||
)
|
||||
l = list(
|
||||
set(
|
||||
external_muting_user_ids
|
||||
+ list(self.local_muting.all().values_list("pk", flat=True))
|
||||
)
|
||||
)
|
||||
return sorted(l)
|
||||
|
||||
def merged_rejecting_ids(self):
|
||||
domain_blocked_user_ids = list(
|
||||
User.objects.filter(
|
||||
mastodon_site__in=self.mastodon_domain_blocks
|
||||
).values_list("pk", flat=True)
|
||||
)
|
||||
external_blocking_user_ids = list(
|
||||
User.objects.all()
|
||||
.annotate(acct=Concat("mastodon_username", Value("@"), "mastodon_site"))
|
||||
.filter(acct__in=self.mastodon_blocks)
|
||||
.values_list("pk", flat=True)
|
||||
)
|
||||
l = list(
|
||||
set(
|
||||
domain_blocked_user_ids
|
||||
+ external_blocking_user_ids
|
||||
+ list(self.local_blocking.all().values_list("pk", flat=True))
|
||||
+ list(self.local_blocked_by.all().values_list("pk", flat=True)) # type: ignore
|
||||
+ list(self.local_muting.all().values_list("pk", flat=True))
|
||||
)
|
||||
)
|
||||
return sorted(l)
|
||||
|
||||
def is_blocking(self, target):
|
||||
return (
|
||||
(
|
||||
target.mastodon_acct in self.mastodon_blocks
|
||||
target in self.local_blocking.all()
|
||||
or target.mastodon_acct in self.mastodon_blocks
|
||||
or target.mastodon_site in self.mastodon_domain_blocks
|
||||
)
|
||||
if target.is_authenticated
|
||||
|
@ -302,7 +462,7 @@ class User(AbstractUser):
|
|||
return target.is_authenticated and target.is_blocking(self)
|
||||
|
||||
def is_muting(self, target):
|
||||
return target.mastodon_acct in self.mastodon_mutes
|
||||
return target.pk in self.muting or target.mastodon_acct in self.mastodon_mutes
|
||||
|
||||
def is_following(self, target):
|
||||
return (
|
||||
|
@ -397,6 +557,20 @@ class Follow(models.Model):
|
|||
edited_time = models.DateTimeField(auto_now=True)
|
||||
|
||||
|
||||
class Block(models.Model):
|
||||
owner = models.ForeignKey(User, on_delete=models.CASCADE, related_name="+")
|
||||
target = models.ForeignKey(User, on_delete=models.CASCADE, related_name="+")
|
||||
created_time = models.DateTimeField(auto_now_add=True)
|
||||
edited_time = models.DateTimeField(auto_now=True)
|
||||
|
||||
|
||||
class Mute(models.Model):
|
||||
owner = models.ForeignKey(User, on_delete=models.CASCADE, related_name="+")
|
||||
target = models.ForeignKey(User, on_delete=models.CASCADE, related_name="+")
|
||||
created_time = models.DateTimeField(auto_now_add=True)
|
||||
edited_time = models.DateTimeField(auto_now=True)
|
||||
|
||||
|
||||
class Report(models.Model):
|
||||
submit_user = models.ForeignKey(
|
||||
User, on_delete=models.SET_NULL, related_name="sumbitted_reports", null=True
|
||||
|
|
|
@ -1,5 +1,8 @@
|
|||
from django.conf import settings
|
||||
from .models import User
|
||||
from datetime import timedelta
|
||||
from django.utils import timezone
|
||||
from tqdm import tqdm
|
||||
from loguru import logger
|
||||
|
||||
|
||||
|
@ -15,3 +18,26 @@ def refresh_mastodon_data_task(user_id, token=None):
|
|||
logger.info(f"{user} mastodon data refreshed")
|
||||
else:
|
||||
logger.error(f"{user} mastodon data refresh failed")
|
||||
|
||||
|
||||
def refresh_all_mastodon_data_task(ttl_hours):
|
||||
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:
|
||||
tqdm.write(f"Refreshing {user}")
|
||||
if user.refresh_mastodon_data():
|
||||
tqdm.write(f"Refreshed {user}")
|
||||
count += 1
|
||||
else:
|
||||
tqdm.write(f"Refresh failed for {user}")
|
||||
user.save()
|
||||
else:
|
||||
tqdm.write(f"Missing token for {user}")
|
||||
logger.info(f"{count} users updated")
|
||||
c = User.merge_rejected_by()
|
||||
logger.info(f"{c} users's rejecting list updated")
|
||||
|
|
|
@ -1,3 +0,0 @@
|
|||
<a title="取消关注"
|
||||
hx-post="{% url 'users:unfollow' user.handler %}"
|
||||
hx-swap="outerHTML"><i class="fa-solid fa-user-xmark"></i></a>
|
84
users/templates/users/profile_actions.html
Normal file
84
users/templates/users/profile_actions.html
Normal file
|
@ -0,0 +1,84 @@
|
|||
{% load mastodon %}
|
||||
{% current_user_relationship user as relationship %}
|
||||
{% if relationship.rejecting %}
|
||||
<span class="tag-list">
|
||||
<span><a>已屏蔽</a></span>
|
||||
</span>
|
||||
{% else %}
|
||||
{% if relationship.status %}
|
||||
<span class="tag-list">
|
||||
<span><a>{{ relationship.status }}</a></span>
|
||||
</span>
|
||||
{% endif %}
|
||||
{% if relationship.following %}
|
||||
{% if relationship.unfollowable %}
|
||||
<span>
|
||||
<a title="已关注,点击可取消关注"
|
||||
class="activated"
|
||||
hx-post="{% url 'users:unfollow' user.handler %}"
|
||||
hx-target="#profile_actions"
|
||||
hx-swap="innerHTML">
|
||||
<i class="fa-solid fa-user-check"></i>
|
||||
</a>
|
||||
</span>
|
||||
{% else %}
|
||||
<span><a title="已在联邦宇宙关注该用户" class="activated"><i class="fa-solid fa-circle-check"></i></a></span>
|
||||
{% endif %}
|
||||
{% else %}
|
||||
{% if user.locked %}
|
||||
<span>
|
||||
<a title="用户已开启关注审核"
|
||||
hx-post="{% url 'users:locked' user.handler %}"
|
||||
hx-target="body"
|
||||
hx-swap="beforeend">
|
||||
<i class="fa-solid fa-user-shield"></i>
|
||||
</a>
|
||||
</span>
|
||||
{% else %}
|
||||
<span>
|
||||
<a title="点击可关注该用户"
|
||||
hx-post="{% url 'users:follow' user.handler %}"
|
||||
hx-target="#profile_actions"
|
||||
hx-swap="innerHTML">
|
||||
<i class="fa-solid fa-user-plus"></i>
|
||||
</a>
|
||||
</span>
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
{% if not relationship.muting %}
|
||||
<span>
|
||||
<a title="点击可忽略该用户"
|
||||
hx-post="{% url 'users:mute' user.handler %}"
|
||||
hx-target="#profile_actions"
|
||||
hx-swap="innerHTML">
|
||||
<i class="fa-solid fa-volume-high"></i>
|
||||
</a>
|
||||
</span>
|
||||
{% elif relationship.unmutable %}
|
||||
<span>
|
||||
<a title="已忽略,点击可取消忽略"
|
||||
class="activated"
|
||||
hx-post="{% url 'users:unmute' user.handler %}"
|
||||
hx-target="#profile_actions"
|
||||
hx-swap="innerHTML">
|
||||
<i class="fa-solid fa-volume-off"></i>
|
||||
</a>
|
||||
</span>
|
||||
{% else %}
|
||||
<span>
|
||||
<a title="已在联邦宇宙中忽略" class="activated">
|
||||
<i class="fa-solid fa-volume-xmark"></i>
|
||||
</a>
|
||||
</span>
|
||||
{% endif %}
|
||||
<span>
|
||||
<a title="点击可屏蔽该用户"
|
||||
hx-confirm="确定要屏蔽该用户吗?"
|
||||
hx-post="{% url 'users:block' user.handler %}"
|
||||
hx-target="#profile_actions"
|
||||
hx-swap="innerHTML">
|
||||
<i class="fa-solid fa-user-slash"></i>
|
||||
</a>
|
||||
</span>
|
||||
{% comment %} <span><a href="{% url 'users:report' %}?user_id={{ user.id }}">{% trans '投诉用户' %}</a></span> {% endcomment %}
|
||||
{% endif %}
|
|
@ -1,3 +0,0 @@
|
|||
<a title="关注ta"
|
||||
hx-post="{% url 'users:follow' user.handler %}"
|
||||
hx-swap="outerHTML"><i class="fa-solid fa-user-plus"></i></a>
|
123
users/tests.py
123
users/tests.py
|
@ -14,6 +14,7 @@ class UserTest(TestCase):
|
|||
self.assertTrue(
|
||||
Follow.objects.filter(owner=self.alice, target=self.bob).exists()
|
||||
)
|
||||
self.assertEqual(self.alice.merged_following_ids(), [self.bob.pk])
|
||||
self.assertEqual(self.alice.following, [self.bob.pk])
|
||||
self.assertTrue(self.alice.is_following(self.bob))
|
||||
self.assertTrue(self.bob.is_followed_by(self.alice))
|
||||
|
@ -31,3 +32,125 @@ class UserTest(TestCase):
|
|||
self.assertFalse(self.alice.is_following(self.bob))
|
||||
self.assertFalse(self.bob.is_followed_by(self.alice))
|
||||
self.assertEqual(self.alice.following, [])
|
||||
|
||||
def test_external_follow(self):
|
||||
self.alice.mastodon_following.append(self.bob.mastodon_acct)
|
||||
self.alice.merge_relationships()
|
||||
self.alice.save()
|
||||
self.assertTrue(self.alice.is_following(self.bob))
|
||||
self.assertEqual(self.alice.following, [self.bob.pk])
|
||||
self.assertFalse(self.alice.follow(self.bob))
|
||||
|
||||
self.alice.mastodon_following.remove(self.bob.mastodon_acct)
|
||||
self.alice.merge_relationships()
|
||||
self.alice.save()
|
||||
self.assertFalse(self.alice.is_following(self.bob))
|
||||
self.assertEqual(self.alice.following, [])
|
||||
self.assertTrue(self.alice.follow(self.bob))
|
||||
self.assertTrue(self.alice.is_following(self.bob))
|
||||
|
||||
def test_local_mute(self):
|
||||
self.alice.mute(self.bob)
|
||||
self.assertTrue(Mute.objects.filter(owner=self.alice, target=self.bob).exists())
|
||||
self.assertEqual(self.alice.merged_muting_ids(), [self.bob.pk])
|
||||
self.assertEqual(self.alice.ignoring, [self.bob.pk])
|
||||
self.assertTrue(self.alice.is_muting(self.bob))
|
||||
|
||||
self.alice.mute(self.bob)
|
||||
self.assertEqual(
|
||||
Mute.objects.filter(owner=self.alice, target=self.bob).count(), 1
|
||||
)
|
||||
self.assertEqual(self.alice.ignoring, [self.bob.pk])
|
||||
|
||||
self.alice.unmute(self.bob)
|
||||
self.assertFalse(
|
||||
Mute.objects.filter(owner=self.alice, target=self.bob).exists()
|
||||
)
|
||||
self.assertFalse(self.alice.is_muting(self.bob))
|
||||
self.assertEqual(self.alice.ignoring, [])
|
||||
self.assertEqual(self.alice.merged_muting_ids(), [])
|
||||
|
||||
def test_external_mute(self):
|
||||
self.alice.mastodon_mutes.append(self.bob.mastodon_acct)
|
||||
self.alice.save()
|
||||
self.assertTrue(self.alice.is_muting(self.bob))
|
||||
self.assertEqual(self.alice.merged_muting_ids(), [self.bob.pk])
|
||||
|
||||
self.alice.mastodon_mutes.remove(self.bob.mastodon_acct)
|
||||
self.assertFalse(self.alice.is_muting(self.bob))
|
||||
self.assertEqual(self.alice.merged_muting_ids(), [])
|
||||
|
||||
def test_local_block_follow(self):
|
||||
self.alice.block(self.bob)
|
||||
self.assertEqual(self.bob.follow(self.alice), False)
|
||||
self.alice.unblock(self.bob)
|
||||
self.assertEqual(self.bob.follow(self.alice), True)
|
||||
self.assertEqual(self.bob.following, [self.alice.pk])
|
||||
self.alice.block(self.bob)
|
||||
self.assertEqual(self.bob.following, [])
|
||||
|
||||
def test_local_block(self):
|
||||
self.alice.block(self.bob)
|
||||
self.assertTrue(
|
||||
Block.objects.filter(owner=self.alice, target=self.bob).exists()
|
||||
)
|
||||
self.assertEqual(self.alice.merged_rejecting_ids(), [self.bob.pk])
|
||||
self.assertEqual(self.alice.ignoring, [self.bob.pk])
|
||||
self.assertTrue(self.alice.is_blocking(self.bob))
|
||||
self.assertTrue(self.bob.is_blocked_by(self.alice))
|
||||
|
||||
self.alice.block(self.bob)
|
||||
self.assertEqual(
|
||||
Block.objects.filter(owner=self.alice, target=self.bob).count(), 1
|
||||
)
|
||||
self.assertEqual(self.alice.ignoring, [self.bob.pk])
|
||||
|
||||
self.alice.unblock(self.bob)
|
||||
self.assertFalse(
|
||||
Block.objects.filter(owner=self.alice, target=self.bob).exists()
|
||||
)
|
||||
self.assertFalse(self.alice.is_blocking(self.bob))
|
||||
self.assertFalse(self.bob.is_blocked_by(self.alice))
|
||||
self.assertEqual(self.alice.ignoring, [])
|
||||
self.assertEqual(self.alice.merged_rejecting_ids(), [])
|
||||
|
||||
def test_external_block(self):
|
||||
self.bob.follow(self.alice)
|
||||
self.assertEqual(self.bob.following, [self.alice.pk])
|
||||
self.alice.mastodon_blocks.append(self.bob.mastodon_acct)
|
||||
self.alice.save()
|
||||
self.assertTrue(self.alice.is_blocking(self.bob))
|
||||
self.assertTrue(self.bob.is_blocked_by(self.alice))
|
||||
self.assertEqual(self.alice.merged_rejecting_ids(), [self.bob.pk])
|
||||
self.alice.merge_relationships()
|
||||
self.assertEqual(self.alice.rejecting, [self.bob.pk])
|
||||
self.alice.save()
|
||||
self.assertEqual(self.bob.following, [self.alice.pk])
|
||||
self.assertEqual(self.bob.rejecting, [])
|
||||
self.assertEqual(User.merge_rejected_by(), 2)
|
||||
self.bob.refresh_from_db()
|
||||
self.assertEqual(self.bob.rejecting, [self.alice.pk])
|
||||
self.assertEqual(self.bob.following, [])
|
||||
|
||||
self.alice.mastodon_blocks.remove(self.bob.mastodon_acct)
|
||||
self.assertFalse(self.alice.is_blocking(self.bob))
|
||||
self.assertFalse(self.bob.is_blocked_by(self.alice))
|
||||
self.assertEqual(self.alice.merged_rejecting_ids(), [])
|
||||
|
||||
def test_external_domain_block(self):
|
||||
self.alice.mastodon_domain_blocks.append(self.bob.mastodon_site)
|
||||
self.alice.save()
|
||||
self.assertTrue(self.alice.is_blocking(self.bob))
|
||||
self.assertTrue(self.bob.is_blocked_by(self.alice))
|
||||
self.assertEqual(self.alice.merged_rejecting_ids(), [self.bob.pk])
|
||||
self.alice.merge_relationships()
|
||||
self.assertEqual(self.alice.rejecting, [self.bob.pk])
|
||||
self.alice.save()
|
||||
self.assertEqual(User.merge_rejected_by(), 1)
|
||||
self.bob.refresh_from_db()
|
||||
self.assertEqual(self.bob.rejecting, [self.alice.pk])
|
||||
|
||||
self.alice.mastodon_domain_blocks.remove(self.bob.mastodon_site)
|
||||
self.assertFalse(self.alice.is_blocking(self.bob))
|
||||
self.assertFalse(self.bob.is_blocked_by(self.alice))
|
||||
self.assertEqual(self.alice.merged_rejecting_ids(), [])
|
||||
|
|
|
@ -28,6 +28,10 @@ urlpatterns = [
|
|||
path("locked/<str:user_name>", follow_locked, name="locked"),
|
||||
path("follow/<str:user_name>", follow, name="follow"),
|
||||
path("unfollow/<str:user_name>", unfollow, name="unfollow"),
|
||||
path("mute/<str:user_name>", mute, name="mute"),
|
||||
path("unmute/<str:user_name>", unmute, name="unmute"),
|
||||
path("block/<str:user_name>", block, name="block"),
|
||||
path("unblock/<str:user_name>", unblock, name="unblock"),
|
||||
path("<str:id>/followers", followers, name="followers"),
|
||||
path("<str:id>/following", following, name="following"),
|
||||
path("report", report, name="report"),
|
||||
|
|
|
@ -16,8 +16,8 @@ from discord import SyncWebhook
|
|||
|
||||
|
||||
def render_user_not_found(request):
|
||||
msg = _("😖哎呀,这位用户还没有加入本站,快去联邦宇宙呼唤TA来注册吧!")
|
||||
sec_msg = _("")
|
||||
sec_msg = _("😖哎呀,这位用户好像还没有加入本站,快去联邦宇宙呼唤TA来注册吧!")
|
||||
msg = _("未找到该用户")
|
||||
return render(
|
||||
request,
|
||||
"common/error.html",
|
||||
|
@ -29,7 +29,7 @@ def render_user_not_found(request):
|
|||
|
||||
|
||||
def render_user_blocked(request):
|
||||
msg = _("你没有访问TA主页的权限😥")
|
||||
msg = _("没有访问该用户主页的权限")
|
||||
return render(
|
||||
request,
|
||||
"common/error.html",
|
||||
|
@ -81,7 +81,7 @@ def follow(request, user_name):
|
|||
raise BadRequest()
|
||||
user = User.get(user_name)
|
||||
if request.user.follow(user):
|
||||
return render(request, "users/followed.html", context={"user": user})
|
||||
return render(request, "users/profile_actions.html", context={"user": user})
|
||||
else:
|
||||
raise BadRequest()
|
||||
|
||||
|
@ -92,7 +92,51 @@ def unfollow(request, user_name):
|
|||
raise BadRequest()
|
||||
user = User.get(user_name)
|
||||
if request.user.unfollow(user):
|
||||
return render(request, "users/unfollowed.html", context={"user": user})
|
||||
return render(request, "users/profile_actions.html", context={"user": user})
|
||||
else:
|
||||
raise BadRequest()
|
||||
|
||||
|
||||
@login_required
|
||||
def mute(request, user_name):
|
||||
if request.method != "POST":
|
||||
raise BadRequest()
|
||||
user = User.get(user_name)
|
||||
if request.user.mute(user):
|
||||
return render(request, "users/profile_actions.html", context={"user": user})
|
||||
else:
|
||||
raise BadRequest()
|
||||
|
||||
|
||||
@login_required
|
||||
def unmute(request, user_name):
|
||||
if request.method != "POST":
|
||||
raise BadRequest()
|
||||
user = User.get(user_name)
|
||||
if request.user.unmute(user):
|
||||
return render(request, "users/profile_actions.html", context={"user": user})
|
||||
else:
|
||||
raise BadRequest()
|
||||
|
||||
|
||||
@login_required
|
||||
def block(request, user_name):
|
||||
if request.method != "POST":
|
||||
raise BadRequest()
|
||||
user = User.get(user_name)
|
||||
if request.user.block(user):
|
||||
return render(request, "users/profile_actions.html", context={"user": user})
|
||||
else:
|
||||
raise BadRequest()
|
||||
|
||||
|
||||
@login_required
|
||||
def unblock(request, user_name):
|
||||
if request.method != "POST":
|
||||
raise BadRequest()
|
||||
user = User.get(user_name)
|
||||
if request.user.unblock(user):
|
||||
return render(request, "users/profile_actions.html", context={"user": user})
|
||||
else:
|
||||
raise BadRequest()
|
||||
|
||||
|
|
Loading…
Add table
Reference in a new issue