diff --git a/activities/models/fan_out.py b/activities/models/fan_out.py index bb9ee1a..53cb4fb 100644 --- a/activities/models/fan_out.py +++ b/activities/models/fan_out.py @@ -5,15 +5,17 @@ from django.db import models from activities.models.timeline_event import TimelineEvent from core.ld import canonicalise from stator.models import State, StateField, StateGraph, StatorModel -from users.models import FollowStates +from users.models import Block, FollowStates class FanOutStates(StateGraph): new = State(try_interval=600) sent = State(delete_after=86400) + skipped = State(delete_after=86400) failed = State(delete_after=86400) new.transitions_to(sent) + new.transitions_to(skipped) new.times_out_to(failed, seconds=86400 * 3) @classmethod @@ -32,6 +34,13 @@ class FanOutStates(StateGraph): # Handle creating/updating local posts case ((FanOut.Types.post | FanOut.Types.post_edited), True): post = await fan_out.subject_post.afetch_full() + # If the author of the post is blocked or muted, skip out + if ( + await Block.objects.active() + .filter(source=fan_out.identity, target=post.author) + .aexists() + ): + return cls.skipped # Make a timeline event directly # If it's a reply, we only add it if we follow at least one # of the people mentioned AND the author, or we're mentioned, @@ -126,6 +135,28 @@ class FanOutStates(StateGraph): # Handle local boosts/likes case (FanOut.Types.interaction, True): interaction = await fan_out.subject_post_interaction.afetch_full() + # If the author of the interaction is blocked or their notifications + # are muted, skip out + if ( + await Block.objects.active() + .filter( + models.Q(mute=False) | models.Q(include_notifications=True), + source=fan_out.identity, + target=interaction.identity, + ) + .aexists() + ): + return cls.skipped + # If blocked/muted the underlying post author, skip out + if ( + await Block.objects.active() + .filter( + source=fan_out.identity, + target_id=interaction.post.author_id, + ) + .aexists() + ): + return cls.skipped # Make a timeline event directly await sync_to_async(TimelineEvent.add_post_interaction)( identity=fan_out.identity, diff --git a/activities/models/post.py b/activities/models/post.py index b18e652..06d8de7 100644 --- a/activities/models/post.py +++ b/activities/models/post.py @@ -712,6 +712,14 @@ class Post(StatorModel): # If it's a local post, include the author if self.local: targets.add(self.author) + # Fetch the author's full blocks and remove them as targets + blocks = ( + self.author.outbound_blocks.active() + .filter(mute=False) + .select_related("target") + ) + async for block in blocks: + targets.remove(block.target) # Now dedupe the targets based on shared inboxes (we only keep one per # shared inbox) deduped_targets = set() diff --git a/api/views/accounts.py b/api/views/accounts.py index f53ee50..aace5fd 100644 --- a/api/views/accounts.py +++ b/api/views/accounts.py @@ -1,7 +1,7 @@ from django.db.models import Q from django.http import HttpRequest, HttpResponse, JsonResponse from django.shortcuts import get_object_or_404 -from ninja import Field +from ninja import Field, Schema from activities.services import SearchService from api import schemas @@ -199,6 +199,51 @@ def account_unfollow(request, id: str): return service.mastodon_json_relationship(request.identity) +@api_router.post("/v1/accounts/{id}/block", response=schemas.Relationship) +@identity_required +def account_block(request, id: str): + identity = get_object_or_404(Identity, pk=id) + service = IdentityService(identity) + service.block_from(request.identity) + return service.mastodon_json_relationship(request.identity) + + +@api_router.post("/v1/accounts/{id}/unblock", response=schemas.Relationship) +@identity_required +def account_unblock(request, id: str): + identity = get_object_or_404(Identity, pk=id) + service = IdentityService(identity) + service.unblock_from(request.identity) + return service.mastodon_json_relationship(request.identity) + + +class MuteDetailsSchema(Schema): + notifications: bool = True + duration: int = 0 + + +@api_router.post("/v1/accounts/{id}/mute", response=schemas.Relationship) +@identity_required +def account_mute(request, id: str, details: MuteDetailsSchema): + identity = get_object_or_404(Identity, pk=id) + service = IdentityService(identity) + service.mute_from( + request.identity, + duration=details.duration, + include_notifications=details.notifications, + ) + return service.mastodon_json_relationship(request.identity) + + +@api_router.post("/v1/accounts/{id}/unmute", response=schemas.Relationship) +@identity_required +def account_unmute(request, id: str): + identity = get_object_or_404(Identity, pk=id) + service = IdentityService(identity) + service.unmute_from(request.identity) + return service.mastodon_json_relationship(request.identity) + + @api_router.get("/v1/accounts/{id}/following", response=list[schemas.Account]) def account_following( request: HttpRequest, diff --git a/api/views/instance.py b/api/views/instance.py index 7f932f1..9e2104c 100644 --- a/api/views/instance.py +++ b/api/views/instance.py @@ -20,7 +20,7 @@ def instance_info(request): "urls": {}, "stats": { "user_count": Identity.objects.filter(local=True).count(), - "status_count": Post.objects.filter(local=True).count(), + "status_count": Post.objects.filter(local=True).not_hidden().count(), "domain_count": Domain.objects.count(), }, "thumbnail": Config.system.site_banner, diff --git a/static/css/style.css b/static/css/style.css index 312a837..6fbe788 100644 --- a/static/css/style.css +++ b/static/css/style.css @@ -1078,6 +1078,11 @@ button.htmx-request::before, animation-timing-function: var(--fa-animation-timing, linear); } +button i:first-child, +.button i:first-child { + margin-right: 3px; +} + .right-column button, .right-column .button { padding: 2px 6px; diff --git a/templates/identity/_view_menu.html b/templates/identity/_view_menu.html index 3edfcc3..a88ff7e 100644 --- a/templates/identity/_view_menu.html +++ b/templates/identity/_view_menu.html @@ -1,19 +1,27 @@ -
+
diff --git a/templates/identity/view.html b/templates/identity/view.html index 3b4122a..f774958 100644 --- a/templates/identity/view.html +++ b/templates/identity/view.html @@ -40,68 +40,75 @@ - {% if identity.summary %} -
- {{ identity.safe_summary }} -
- {% endif %} + {% if inbound_block %} +

+ This user has blocked you. +

+ {% else %} - {% if identity.metadata %} -
- {% for entry in identity.safe_metadata %} - - {% endfor %} -
- {% endif %} - - {% if identity.local and identity.config_identity.visible_follows %} -
- {{ post_count }} posts - {{ following_count }} following - {{ followers_count }} follower{{ followers_count|pluralize }} -
- {% endif %} - - {% if not identity.local %} - {% if identity.outdated and not identity.name %} -

- The system is still fetching this profile. Refresh to see updates. -

- {% else %} -

- This is a member of another server. - See their original profile ➔ -

- {% endif %} - {% endif %} - - {% block subcontent %} - - {% for post in page_obj %} - {% include "activities/_post.html" %} - {% empty %} - - {% if identity.local %} - No posts yet. - {% else %} - No posts have been received/retrieved by this server yet. - - {% if identity.profile_uri %} - You might find historical posts at - their original profile ➔ - {% endif %} - {% endif %} - - {% endfor %} - - {% if page_obj.has_next %} -