From d601dde53546fbf400fea74a7d17eac98b51d764 Mon Sep 17 00:00:00 2001 From: Your Name Date: Fri, 7 Jul 2023 16:54:15 -0400 Subject: [PATCH] local mute and block --- common/static/scss/_common.scss | 4 + common/templates/_sidebar.html | 40 +--- common/templatetags/mastodon.py | 38 ++-- journal/mixins.py | 6 +- journal/models.py | 2 +- social/models.py | 3 +- social/tests.py | 2 +- users/data.py | 2 +- .../management/commands/refresh_following.py | 2 +- users/management/commands/refresh_mastodon.py | 25 +-- users/migrations/0010_add_local_mute_block.py | 104 +++++++++ users/models.py | 208 ++++++++++++++++-- users/tasks.py | 26 +++ users/templates/users/followed.html | 3 - users/templates/users/profile_actions.html | 84 +++++++ users/templates/users/unfollowed.html | 3 - users/tests.py | 123 +++++++++++ users/urls.py | 4 + users/views.py | 54 ++++- 19 files changed, 621 insertions(+), 112 deletions(-) create mode 100644 users/migrations/0010_add_local_mute_block.py delete mode 100644 users/templates/users/followed.html create mode 100644 users/templates/users/profile_actions.html delete mode 100644 users/templates/users/unfollowed.html diff --git a/common/static/scss/_common.scss b/common/static/scss/_common.scss index 2eb7149a..2ba16a24 100644 --- a/common/static/scss/_common.scss +++ b/common/static/scss/_common.scss @@ -135,6 +135,10 @@ details { color: var(--pico-primary); opacity: 1; } + + .tag-list a:not(:hover) { + color: white; + } } form img { diff --git a/common/templates/_sidebar.html b/common/templates/_sidebar.html index 06d04953..34fcd4f3 100644 --- a/common/templates/_sidebar.html +++ b/common/templates/_sidebar.html @@ -32,7 +32,6 @@ {% endif %} {% if show_profile %} - {% current_user_relationship user as relationship %}
@@ -56,46 +55,21 @@ - {% if relationship.status %} - - {% endif %} - - {% if user.locked %} - - - - - - {% elif user != request.user %} - {% if not relationship.following %} + + {% if user == request.user %} + {% if user.locked %} - - + + - {% elif relationship.unfollowable %} - - - - - - {% else %} - {% endif %} + {% else %} + {% include 'users/profile_actions.html' %} {% endif %} - {% comment %} {% trans '投诉用户' %} {% endcomment %}

{{ user.mastodon_account.note|bleach:"a,p,span,br" }}

diff --git a/common/templatetags/mastodon.py b/common/templatetags/mastodon.py index 1dab1876..3befdb24 100644 --- a/common/templatetags/mastodon.py +++ b/common/templatetags/mastodon.py @@ -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 diff --git a/journal/mixins.py b/journal/mixins.py index 8664df1d..538d76b7 100644 --- a/journal/mixins.py +++ b/journal/mixins.py @@ -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) diff --git a/journal/models.py b/journal/models.py index fa57a098..2c6dba87 100644 --- a/journal/models.py +++ b/journal/models.py @@ -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): diff --git a/social/models.py b/social/models.py index 4243dbe4..a94aaa2b 100644 --- a/social/models.py +++ b/social/models.py @@ -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 ( diff --git a/social/tests.py b/social/tests.py index 7c2ee48c..52814257 100644 --- a/social/tests.py +++ b/social/tests.py @@ -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) diff --git a/users/data.py b/users/data.py index c4ad7b53..39b3409e 100644 --- a/users/data.py +++ b/users/data.py @@ -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 diff --git a/users/management/commands/refresh_following.py b/users/management/commands/refresh_following.py index 9352db7d..1f1f7754 100644 --- a/users/management/commands/refresh_following.py +++ b/users/management/commands/refresh_following.py @@ -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"]) diff --git a/users/management/commands/refresh_mastodon.py b/users/management/commands/refresh_mastodon.py index 4cd62594..22d7036f 100644 --- a/users/management/commands/refresh_mastodon.py +++ b/users/management/commands/refresh_mastodon.py @@ -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) diff --git a/users/migrations/0010_add_local_mute_block.py b/users/migrations/0010_add_local_mute_block.py new file mode 100644 index 00000000..3f9e9508 --- /dev/null +++ b/users/migrations/0010_add_local_mute_block.py @@ -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 + ), + ), + ] diff --git a/users/models.py b/users/models.py index d1860c09..7d91fdd6 100644 --- a/users/models.py +++ b/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 diff --git a/users/tasks.py b/users/tasks.py index c3f4abfc..563e7886 100644 --- a/users/tasks.py +++ b/users/tasks.py @@ -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") diff --git a/users/templates/users/followed.html b/users/templates/users/followed.html deleted file mode 100644 index b0a1ee6d..00000000 --- a/users/templates/users/followed.html +++ /dev/null @@ -1,3 +0,0 @@ - diff --git a/users/templates/users/profile_actions.html b/users/templates/users/profile_actions.html new file mode 100644 index 00000000..2c62c7f7 --- /dev/null +++ b/users/templates/users/profile_actions.html @@ -0,0 +1,84 @@ +{% load mastodon %} +{% current_user_relationship user as relationship %} +{% if relationship.rejecting %} + + 已屏蔽 + +{% else %} + {% if relationship.status %} + + {{ relationship.status }} + + {% endif %} + {% if relationship.following %} + {% if relationship.unfollowable %} + + + + + + {% else %} + + {% endif %} + {% else %} + {% if user.locked %} + + + + + + {% else %} + + + + + + {% endif %} + {% endif %} + {% if not relationship.muting %} + + + + + + {% elif relationship.unmutable %} + + + + + + {% else %} + + + + + + {% endif %} + + + + + + {% comment %} {% trans '投诉用户' %} {% endcomment %} +{% endif %} diff --git a/users/templates/users/unfollowed.html b/users/templates/users/unfollowed.html deleted file mode 100644 index 02545f85..00000000 --- a/users/templates/users/unfollowed.html +++ /dev/null @@ -1,3 +0,0 @@ - diff --git a/users/tests.py b/users/tests.py index 249ffa42..6cf77644 100644 --- a/users/tests.py +++ b/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(), []) diff --git a/users/urls.py b/users/urls.py index b005f330..01c32e9f 100644 --- a/users/urls.py +++ b/users/urls.py @@ -28,6 +28,10 @@ urlpatterns = [ path("locked/", follow_locked, name="locked"), path("follow/", follow, name="follow"), path("unfollow/", unfollow, name="unfollow"), + path("mute/", mute, name="mute"), + path("unmute/", unmute, name="unmute"), + path("block/", block, name="block"), + path("unblock/", unblock, name="unblock"), path("/followers", followers, name="followers"), path("/following", following, name="following"), path("report", report, name="report"), diff --git a/users/views.py b/users/views.py index 64a028d4..d326ca8e 100644 --- a/users/views.py +++ b/users/views.py @@ -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()