diff --git a/common/templatetags/mastodon.py b/common/templatetags/mastodon.py index abdae9f1..0d31b7c5 100644 --- a/common/templatetags/mastodon.py +++ b/common/templatetags/mastodon.py @@ -28,7 +28,7 @@ def current_user_relationship(context, target_identity: "APIdentity"): "rejecting": False, "status": "", } - if target_identity and current_identity: + if target_identity and current_identity and not target_identity.restricted: if current_identity != target_identity: if current_identity.is_blocking( target_identity diff --git a/takahe/migrations/0001_initial.py b/takahe/migrations/0001_initial.py index bc938a14..96593a09 100644 --- a/takahe/migrations/0001_initial.py +++ b/takahe/migrations/0001_initial.py @@ -34,6 +34,11 @@ class Migration(migrations.Migration): ), ("state", models.CharField(default="outdated", max_length=100)), ("state_changed", models.DateTimeField(auto_now_add=True)), + ("state_next_attempt", models.DateTimeField(blank=True, null=True)), + ( + "state_locked_until", + models.DateTimeField(blank=True, db_index=True, null=True), + ), ("nodeinfo", models.JSONField(blank=True, null=True)), ("local", models.BooleanField()), ("blocked", models.BooleanField(default=False)), diff --git a/takahe/models.py b/takahe/models.py index 30e20331..bc31f28a 100644 --- a/takahe/models.py +++ b/takahe/models.py @@ -279,6 +279,8 @@ class Domain(models.Model): # state = StateField(DomainStates) state = models.CharField(max_length=100, default="outdated") state_changed = models.DateTimeField(auto_now_add=True) + state_next_attempt = models.DateTimeField(blank=True, null=True) + state_locked_until = models.DateTimeField(null=True, blank=True, db_index=True) # nodeinfo 2.0 detail about the remote server nodeinfo = models.JSONField(null=True, blank=True) @@ -352,6 +354,24 @@ class Domain(models.Model): def __str__(self): return self.domain + def recursively_blocked(self) -> bool: + """ + Checks for blocks on all right subsets of this domain, except the very + last part of the TLD. + + Yes, I know this weirdly lets you block ".co.uk" or whatever, but + people can do that if they want I guess. + """ + # Efficient short-circuit + if self.blocked: + return True + # Build domain list + domain_parts = [self.domain] + while "." in domain_parts[-1]: + domain_parts.append(domain_parts[-1].split(".", 1)[1]) + # See if any of those are blocked + return Domain.objects.filter(domain__in=domain_parts, blocked=True).exists() + def upload_store(): return FileSystemStorage( diff --git a/takahe/utils.py b/takahe/utils.py index ee8f3979..00553ab8 100644 --- a/takahe/utils.py +++ b/takahe/utils.py @@ -141,6 +141,10 @@ class Takahe: @staticmethod def fetch_remote_identity(handler: str) -> int | None: + d = handler.split("@")[-1] + domain = Domain.objects.filter(domain=d).first() + if domain and domain.recursively_blocked: + return InboxMessage.create_internal({"type": "FetchIdentity", "handle": handler}) @staticmethod @@ -670,7 +674,9 @@ class Takahe: return FediverseHtmlParser(linebreaks_filter(txt)).html @staticmethod - def update_state(obj: Post | PostInteraction | Relay | Identity, state: str): + def update_state( + obj: Post | PostInteraction | Relay | Identity | Domain, state: str + ): obj.state = state obj.state_changed = timezone.now() obj.state_next_attempt = None @@ -696,6 +702,7 @@ class Takahe: nodeinfo__protocols__contains="neodb", nodeinfo__metadata__nodeEnvironment="production", local=False, + blocked=False, ).values_list("pk", flat=True) ) cache.set(cache_key, peers, timeout=1800) diff --git a/users/management/commands/user.py b/users/management/commands/user.py index c7585762..8ccfc16d 100644 --- a/users/management/commands/user.py +++ b/users/management/commands/user.py @@ -1,7 +1,10 @@ from django.core.management.base import BaseCommand from tqdm import tqdm +import httpx from users.models import Preference, User +from takahe.models import Identity, Domain +from takahe.utils import Takahe class Command(BaseCommand): @@ -16,6 +19,11 @@ class Command(BaseCommand): action="store_true", help="check and fix integrity for missing data for user models", ) + parser.add_argument( + "--remote", + action="store_true", + help="reset state for remote domains/users with previous connection issues", + ) parser.add_argument( "--super", action="store", nargs="*", help="list or toggle superuser" ) @@ -32,6 +40,8 @@ class Command(BaseCommand): self.list(self.users) if options["integrity"]: self.integrity() + if options["remote"]: + self.check_remote() if options["super"] is not None: self.superuser(options["super"]) if options["staff"] is not None: @@ -50,6 +60,7 @@ class Command(BaseCommand): def integrity(self): count = 0 + self.stdout.write("Checking local users") for user in tqdm(User.objects.filter(is_active=True)): i = user.identity.takahe_identity if i.public_key is None: @@ -64,7 +75,60 @@ class Command(BaseCommand): if self.fix: Preference.objects.create(user=user) count += 1 - self.stdout.write(f"{count} issues") + + def check_remote(self): + headers = { + "Accept": "application/json,application/activity+json,application/ld+json" + } + with httpx.Client(timeout=0.5) as client: + count = 0 + self.stdout.write("Checking remote domains") + for d in tqdm( + Domain.objects.filter( + local=False, blocked=False, state="connection_issue" + ) + ): + try: + response = client.get( + f"https://{d.domain}/.well-known/nodeinfo", + follow_redirects=True, + headers=headers, + ) + if response.status_code == 200 and "json" in response.headers.get( + "content-type", "" + ): + count += 1 + if self.fix: + Takahe.update_state(d, "outdated") + except Exception: + pass + self.stdout.write(f"{count} issues") + count = 0 + self.stdout.write("Checking remote identities") + for i in tqdm( + Identity.objects.filter( + public_key__isnull=True, + local=False, + restriction=0, + state="connection_issue", + ) + ): + try: + response = client.request( + "get", + i.actor_uri, + headers=headers, + follow_redirects=True, + ) + if ( + response.status_code == 200 + and "json" in response.headers.get("content-type", "") + and "@context" in response.text + ): + Takahe.update_state(i, "outdated") + except Exception: + pass + self.stdout.write(f"{count} issues") def superuser(self, v): if v == []: diff --git a/users/migrations/0001_initial_0_10.py b/users/migrations/0001_initial_0_10.py index d2859191..c43c785f 100644 --- a/users/migrations/0001_initial_0_10.py +++ b/users/migrations/0001_initial_0_10.py @@ -562,9 +562,12 @@ class Migration(migrations.Migration): field=models.CharField( choices=[ ("en", "English"), + ("da", "Danish"), + ("de", "German"), + ("fr", "French"), + ("it", "Italian"), ("zh-hans", "Simplified Chinese"), ("zh-hant", "Traditional Chinese"), - ("da", "Danish"), ], default="en", max_length=10, diff --git a/users/models/apidentity.py b/users/models/apidentity.py index 7f031e23..6a9a3799 100644 --- a/users/models/apidentity.py +++ b/users/models/apidentity.py @@ -109,6 +109,10 @@ class APIdentity(models.Model): else: return f"{self.username}@{self.domain_name}" + @property + def restricted(self): + return self.takahe_identity.restriction != 2 + @property def following(self): return Takahe.get_following_ids(self.pk)