2023-05-13 09:01:27 -07:00
|
|
|
from django.db import models, transaction
|
2022-12-21 12:48:39 -08:00
|
|
|
from django.template.defaultfilters import linebreaks_filter
|
2022-12-21 12:36:10 -08:00
|
|
|
|
2023-05-13 09:01:27 -07:00
|
|
|
from activities.models import FanOut, Post, PostInteraction, PostInteractionStates
|
2023-01-15 15:15:57 -08:00
|
|
|
from core.files import resize_image
|
2023-01-29 16:46:22 -08:00
|
|
|
from core.html import FediverseHtmlParser
|
2023-07-12 08:49:30 -07:00
|
|
|
from stator.exceptions import TryAgainLater
|
2023-01-15 13:48:17 -08:00
|
|
|
from users.models import (
|
|
|
|
Block,
|
|
|
|
BlockStates,
|
|
|
|
Domain,
|
|
|
|
Follow,
|
|
|
|
FollowStates,
|
|
|
|
Identity,
|
2023-01-16 10:53:40 -08:00
|
|
|
InboxMessage,
|
2023-01-15 13:48:17 -08:00
|
|
|
User,
|
|
|
|
)
|
2022-12-19 12:54:09 -08:00
|
|
|
|
|
|
|
|
|
|
|
class IdentityService:
|
|
|
|
"""
|
|
|
|
High-level helper methods for doing things to identities
|
|
|
|
"""
|
|
|
|
|
|
|
|
def __init__(self, identity: Identity):
|
|
|
|
self.identity = identity
|
|
|
|
|
2023-01-15 13:48:17 -08:00
|
|
|
@classmethod
|
|
|
|
def create(
|
|
|
|
cls,
|
|
|
|
user: User,
|
|
|
|
username: str,
|
|
|
|
domain: Domain,
|
|
|
|
name: str,
|
|
|
|
discoverable: bool = True,
|
|
|
|
) -> Identity:
|
|
|
|
identity = Identity.objects.create(
|
|
|
|
actor_uri=f"https://{domain.uri_domain}/@{username}@{domain.domain}/",
|
|
|
|
username=username,
|
|
|
|
domain=domain,
|
|
|
|
name=name,
|
|
|
|
local=True,
|
|
|
|
discoverable=discoverable,
|
|
|
|
)
|
|
|
|
identity.users.add(user)
|
|
|
|
identity.generate_keypair()
|
|
|
|
# Send fanouts to all admin identities
|
|
|
|
for admin_identity in cls.admin_identities():
|
|
|
|
FanOut.objects.create(
|
|
|
|
type=FanOut.Types.identity_created,
|
|
|
|
identity=admin_identity,
|
|
|
|
subject_identity=identity,
|
|
|
|
)
|
|
|
|
return identity
|
|
|
|
|
|
|
|
@classmethod
|
|
|
|
def admin_identities(cls) -> models.QuerySet[Identity]:
|
|
|
|
return Identity.objects.filter(users__admin=True).distinct()
|
|
|
|
|
2022-12-21 12:36:10 -08:00
|
|
|
def following(self) -> models.QuerySet[Identity]:
|
2022-12-21 12:56:52 -08:00
|
|
|
return (
|
2023-02-13 19:50:43 -08:00
|
|
|
Identity.objects.filter(
|
|
|
|
inbound_follows__source=self.identity,
|
|
|
|
inbound_follows__state__in=FollowStates.group_active(),
|
|
|
|
)
|
2022-12-21 12:56:52 -08:00
|
|
|
.not_deleted()
|
2023-02-15 09:44:02 -08:00
|
|
|
.distinct()
|
2022-12-21 12:56:52 -08:00
|
|
|
.order_by("username")
|
|
|
|
.select_related("domain")
|
|
|
|
)
|
2022-12-21 12:36:10 -08:00
|
|
|
|
|
|
|
def followers(self) -> models.QuerySet[Identity]:
|
2022-12-21 12:56:52 -08:00
|
|
|
return (
|
2023-02-13 19:50:43 -08:00
|
|
|
Identity.objects.filter(
|
|
|
|
outbound_follows__target=self.identity,
|
|
|
|
inbound_follows__state__in=FollowStates.group_active(),
|
|
|
|
)
|
2022-12-21 12:56:52 -08:00
|
|
|
.not_deleted()
|
2023-02-15 09:44:02 -08:00
|
|
|
.distinct()
|
2022-12-21 12:56:52 -08:00
|
|
|
.order_by("username")
|
|
|
|
.select_related("domain")
|
|
|
|
)
|
2022-12-21 12:36:10 -08:00
|
|
|
|
2023-02-13 19:50:43 -08:00
|
|
|
def follow(self, target_identity: Identity, boosts=True) -> Follow:
|
2022-12-19 12:54:09 -08:00
|
|
|
"""
|
|
|
|
Follows a user (or does nothing if already followed).
|
|
|
|
Returns the follow.
|
|
|
|
"""
|
2023-02-13 19:50:43 -08:00
|
|
|
if target_identity == self.identity:
|
2023-01-20 08:31:15 -08:00
|
|
|
raise ValueError("You cannot follow yourself")
|
2023-02-13 19:50:43 -08:00
|
|
|
return Follow.create_local(self.identity, target_identity, boosts=boosts)
|
2022-12-19 12:54:09 -08:00
|
|
|
|
2023-02-13 19:50:43 -08:00
|
|
|
def unfollow(self, target_identity: Identity):
|
2022-12-19 12:54:09 -08:00
|
|
|
"""
|
|
|
|
Unfollows a user (or does nothing if not followed).
|
|
|
|
"""
|
2023-02-13 19:50:43 -08:00
|
|
|
if target_identity == self.identity:
|
2023-01-20 08:31:15 -08:00
|
|
|
raise ValueError("You cannot unfollow yourself")
|
2023-02-13 19:50:43 -08:00
|
|
|
existing_follow = Follow.maybe_get(self.identity, target_identity)
|
2022-12-19 12:54:09 -08:00
|
|
|
if existing_follow:
|
|
|
|
existing_follow.transition_perform(FollowStates.undone)
|
2023-01-16 10:53:40 -08:00
|
|
|
InboxMessage.create_internal(
|
|
|
|
{
|
|
|
|
"type": "ClearTimeline",
|
2023-02-13 19:50:43 -08:00
|
|
|
"object": target_identity.pk,
|
|
|
|
"actor": self.identity.pk,
|
2023-01-16 10:53:40 -08:00
|
|
|
}
|
|
|
|
)
|
2022-12-19 12:54:09 -08:00
|
|
|
|
2023-02-13 19:50:43 -08:00
|
|
|
def block(self, target_identity: Identity) -> Block:
|
2023-01-15 12:35:45 -08:00
|
|
|
"""
|
|
|
|
Blocks a user.
|
|
|
|
"""
|
2023-02-13 19:50:43 -08:00
|
|
|
if target_identity == self.identity:
|
2023-01-20 08:31:15 -08:00
|
|
|
raise ValueError("You cannot block yourself")
|
2023-02-13 19:50:43 -08:00
|
|
|
self.unfollow(target_identity)
|
|
|
|
block = Block.create_local_block(self.identity, target_identity)
|
2023-01-16 10:53:40 -08:00
|
|
|
InboxMessage.create_internal(
|
|
|
|
{
|
|
|
|
"type": "ClearTimeline",
|
2023-02-13 19:50:43 -08:00
|
|
|
"actor": self.identity.pk,
|
|
|
|
"object": target_identity.pk,
|
2023-01-16 10:53:40 -08:00
|
|
|
"fullErase": True,
|
|
|
|
}
|
|
|
|
)
|
|
|
|
return block
|
2023-01-15 12:35:45 -08:00
|
|
|
|
2023-02-13 19:50:43 -08:00
|
|
|
def unblock(self, target_identity: Identity):
|
2023-01-15 12:35:45 -08:00
|
|
|
"""
|
|
|
|
Unlocks a user
|
|
|
|
"""
|
2023-02-13 19:50:43 -08:00
|
|
|
if target_identity == self.identity:
|
2023-01-20 08:31:15 -08:00
|
|
|
raise ValueError("You cannot unblock yourself")
|
2023-02-13 19:50:43 -08:00
|
|
|
existing_block = Block.maybe_get(self.identity, target_identity, mute=False)
|
2023-01-15 12:35:45 -08:00
|
|
|
if existing_block and existing_block.active:
|
|
|
|
existing_block.transition_perform(BlockStates.undone)
|
|
|
|
|
2023-02-13 19:50:43 -08:00
|
|
|
def mute(
|
2023-01-15 12:35:45 -08:00
|
|
|
self,
|
2023-02-13 19:50:43 -08:00
|
|
|
target_identity: Identity,
|
2023-01-15 12:35:45 -08:00
|
|
|
duration: int = 0,
|
|
|
|
include_notifications: bool = False,
|
|
|
|
) -> Block:
|
|
|
|
"""
|
|
|
|
Mutes a user.
|
|
|
|
"""
|
2023-02-13 19:50:43 -08:00
|
|
|
if target_identity == self.identity:
|
2023-01-20 08:31:15 -08:00
|
|
|
raise ValueError("You cannot mute yourself")
|
2023-01-15 12:35:45 -08:00
|
|
|
return Block.create_local_mute(
|
|
|
|
self.identity,
|
2023-02-13 19:50:43 -08:00
|
|
|
target_identity,
|
2023-01-15 12:35:45 -08:00
|
|
|
duration=duration or None,
|
|
|
|
include_notifications=include_notifications,
|
|
|
|
)
|
|
|
|
|
2023-02-13 19:50:43 -08:00
|
|
|
def unmute(self, target_identity: Identity):
|
2023-01-15 12:35:45 -08:00
|
|
|
"""
|
|
|
|
Unmutes a user
|
|
|
|
"""
|
2023-02-13 19:50:43 -08:00
|
|
|
if target_identity == self.identity:
|
2023-01-20 08:31:15 -08:00
|
|
|
raise ValueError("You cannot unmute yourself")
|
2023-02-13 19:50:43 -08:00
|
|
|
existing_block = Block.maybe_get(self.identity, target_identity, mute=True)
|
2023-01-15 12:35:45 -08:00
|
|
|
if existing_block and existing_block.active:
|
|
|
|
existing_block.transition_perform(BlockStates.undone)
|
|
|
|
|
|
|
|
def relationships(self, from_identity: Identity):
|
|
|
|
"""
|
|
|
|
Returns a dict of any active relationships from the given identity.
|
|
|
|
"""
|
|
|
|
return {
|
|
|
|
"outbound_follow": Follow.maybe_get(
|
|
|
|
from_identity, self.identity, require_active=True
|
|
|
|
),
|
|
|
|
"inbound_follow": Follow.maybe_get(
|
|
|
|
self.identity, from_identity, require_active=True
|
|
|
|
),
|
|
|
|
"outbound_block": Block.maybe_get(
|
|
|
|
from_identity, self.identity, mute=False, require_active=True
|
|
|
|
),
|
|
|
|
"inbound_block": Block.maybe_get(
|
|
|
|
self.identity, from_identity, mute=False, require_active=True
|
|
|
|
),
|
|
|
|
"outbound_mute": Block.maybe_get(
|
|
|
|
from_identity, self.identity, mute=True, require_active=True
|
|
|
|
),
|
|
|
|
}
|
|
|
|
|
2023-05-13 09:01:27 -07:00
|
|
|
def sync_pins(self, object_uris):
|
2023-05-15 09:54:32 -07:00
|
|
|
if not object_uris or self.identity.domain.blocked:
|
2023-05-13 09:01:27 -07:00
|
|
|
return
|
|
|
|
|
|
|
|
with transaction.atomic():
|
|
|
|
for object_uri in object_uris:
|
2023-05-15 10:36:33 -07:00
|
|
|
try:
|
|
|
|
post = Post.by_object_uri(object_uri, fetch=True)
|
|
|
|
PostInteraction.objects.get_or_create(
|
|
|
|
type=PostInteraction.Types.pin,
|
|
|
|
identity=self.identity,
|
|
|
|
post=post,
|
|
|
|
state__in=PostInteractionStates.group_active(),
|
|
|
|
)
|
|
|
|
except Post.DoesNotExist:
|
|
|
|
# ignore 404s...
|
|
|
|
pass
|
2023-07-12 08:49:30 -07:00
|
|
|
except TryAgainLater:
|
|
|
|
# when fetching a post -> author -> post we can
|
|
|
|
# get into a state. Ignore this round.
|
|
|
|
pass
|
2023-05-13 09:01:27 -07:00
|
|
|
for removed in PostInteraction.objects.filter(
|
|
|
|
type=PostInteraction.Types.pin,
|
|
|
|
identity=self.identity,
|
|
|
|
state__in=PostInteractionStates.group_active(),
|
|
|
|
).exclude(post__object_uri__in=object_uris):
|
|
|
|
removed.transition_perform(PostInteractionStates.undone_fanned_out)
|
|
|
|
|
2022-12-19 12:54:09 -08:00
|
|
|
def mastodon_json_relationship(self, from_identity: Identity):
|
|
|
|
"""
|
|
|
|
Returns a Relationship object for the from_identity's relationship
|
|
|
|
with this identity.
|
|
|
|
"""
|
2023-01-15 12:35:45 -08:00
|
|
|
relationships = self.relationships(from_identity)
|
2022-12-19 12:54:09 -08:00
|
|
|
return {
|
|
|
|
"id": self.identity.pk,
|
2023-01-15 12:35:45 -08:00
|
|
|
"following": relationships["outbound_follow"] is not None,
|
|
|
|
"followed_by": relationships["inbound_follow"] is not None,
|
|
|
|
"showing_reblogs": (
|
|
|
|
relationships["outbound_follow"]
|
|
|
|
and relationships["outbound_follow"].boosts
|
|
|
|
or False
|
|
|
|
),
|
2022-12-19 12:54:09 -08:00
|
|
|
"notifying": False,
|
2023-01-15 12:35:45 -08:00
|
|
|
"blocking": relationships["outbound_block"] is not None,
|
|
|
|
"blocked_by": relationships["inbound_block"] is not None,
|
|
|
|
"muting": relationships["outbound_mute"] is not None,
|
2022-12-19 12:54:09 -08:00
|
|
|
"muting_notifications": False,
|
|
|
|
"requested": False,
|
|
|
|
"domain_blocking": False,
|
|
|
|
"endorsed": False,
|
2023-01-15 12:35:45 -08:00
|
|
|
"note": (
|
|
|
|
relationships["outbound_follow"]
|
|
|
|
and relationships["outbound_follow"].note
|
|
|
|
or ""
|
|
|
|
),
|
2022-12-19 12:54:09 -08:00
|
|
|
}
|
2022-12-21 12:48:39 -08:00
|
|
|
|
|
|
|
def set_summary(self, summary: str):
|
|
|
|
"""
|
|
|
|
Safely sets a summary and turns linebreaks into HTML
|
|
|
|
"""
|
2022-12-21 13:56:45 -08:00
|
|
|
if summary:
|
2023-01-29 16:46:22 -08:00
|
|
|
self.identity.summary = FediverseHtmlParser(linebreaks_filter(summary)).html
|
2022-12-21 13:56:45 -08:00
|
|
|
else:
|
|
|
|
self.identity.summary = None
|
2022-12-21 12:48:39 -08:00
|
|
|
self.identity.save()
|
2023-01-15 15:15:57 -08:00
|
|
|
|
|
|
|
def set_icon(self, file):
|
|
|
|
"""
|
|
|
|
Sets the user's avatar image
|
|
|
|
"""
|
|
|
|
self.identity.icon.save(
|
|
|
|
file.name,
|
|
|
|
resize_image(file, size=(400, 400)),
|
|
|
|
)
|
|
|
|
|
|
|
|
def set_image(self, file):
|
|
|
|
"""
|
|
|
|
Sets the user's header image
|
|
|
|
"""
|
|
|
|
self.identity.image.save(
|
|
|
|
file.name,
|
|
|
|
resize_image(file, size=(1500, 500)),
|
|
|
|
)
|
2023-02-13 19:50:43 -08:00
|
|
|
|
|
|
|
@classmethod
|
|
|
|
def handle_internal_add_follow(cls, payload):
|
|
|
|
"""
|
|
|
|
Handles an inbox message saying we need to follow a handle
|
|
|
|
|
|
|
|
Message format:
|
|
|
|
{
|
|
|
|
"type": "AddFollow",
|
|
|
|
"source": "90310938129083",
|
|
|
|
"target_handle": "andrew@aeracode.org",
|
|
|
|
"boosts": true,
|
|
|
|
}
|
|
|
|
"""
|
|
|
|
# Retrieve ourselves
|
|
|
|
self = cls(Identity.objects.get(pk=payload["source"]))
|
|
|
|
# Get the remote end (may need a fetch)
|
|
|
|
username, domain = payload["target_handle"].split("@")
|
|
|
|
target_identity = Identity.by_username_and_domain(username, domain, fetch=True)
|
|
|
|
if target_identity is None:
|
|
|
|
raise ValueError(f"Cannot find identity to follow: {target_identity}")
|
|
|
|
# Follow!
|
|
|
|
self.follow(target_identity=target_identity, boosts=payload.get("boosts", True))
|