Skip to content

Commit

Permalink
Merge pull request #3076 from bookwyrm-social/move
Browse files Browse the repository at this point in the history
Add Move activity for user migration (with small change)
  • Loading branch information
mouse-reeve authored Nov 2, 2023
2 parents 86fd62a + 2137737 commit 0502f6b
Show file tree
Hide file tree
Showing 30 changed files with 788 additions and 81 deletions.
5 changes: 3 additions & 2 deletions FEDERATION.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,14 +13,15 @@ User relationship interactions follow the standard ActivityPub spec.
- `Block`: prevent users from seeing one another's statuses, and prevents the blocked user from viewing the actor's profile
- `Update`: updates a user's profile and settings
- `Delete`: deactivates a user
- `Undo`: reverses a `Follow` or `Block`
- `Undo`: reverses a `Block` or `Follow`

### Activities
- `Create/Status`: saves a new status in the database.
- `Delete/Status`: Removes a status
- `Like/Status`: Creates a favorite on the status
- `Announce/Status`: Boosts the status into the actor's timeline
- `Undo/*`,: Reverses a `Like` or `Announce`
- `Undo/*`,: Reverses an `Announce`, `Like`, or `Move`
- `Move/User`: Moves a user from one ActivityPub id to another.

### Collections
User's books and lists are represented by [`OrderedCollection`](https://www.w3.org/TR/activitystreams-vocabulary/#dfn-orderedcollection)
Expand Down
1 change: 1 addition & 0 deletions bookwyrm/activitypub/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@
from .verbs import Follow, Accept, Reject, Block
from .verbs import Add, Remove
from .verbs import Announce, Like
from .verbs import Move

# this creates a list of all the Activity types that we can serialize,
# so when an Activity comes in from outside, we can check if it's known
Expand Down
2 changes: 2 additions & 0 deletions bookwyrm/activitypub/person.py
Original file line number Diff line number Diff line change
Expand Up @@ -40,4 +40,6 @@ class Person(ActivityObject):
manuallyApprovesFollowers: str = False
discoverable: str = False
hideFollows: str = False
movedTo: str = None
alsoKnownAs: dict[str] = None
type: str = "Person"
27 changes: 27 additions & 0 deletions bookwyrm/activitypub/verbs.py
Original file line number Diff line number Diff line change
Expand Up @@ -231,3 +231,30 @@ class Announce(Verb):
def action(self, allow_external_connections=True):
"""boost"""
self.to_model(allow_external_connections=allow_external_connections)


@dataclass(init=False)
class Move(Verb):
"""a user moving an object"""

object: str
type: str = "Move"
origin: str = None
target: str = None

def action(self, allow_external_connections=True):
"""move"""

object_is_user = resolve_remote_id(remote_id=self.object, model="User")

if object_is_user:
model = apps.get_model("bookwyrm.MoveUser")

self.to_model(
model=model,
save=True,
allow_external_connections=allow_external_connections,
)
else:
# we might do something with this to move other objects at some point
pass
16 changes: 16 additions & 0 deletions bookwyrm/forms/edit_user.py
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,22 @@ class Meta:
fields = ["password"]


class MoveUserForm(CustomForm):
target = forms.CharField(widget=forms.TextInput)

class Meta:
model = models.User
fields = ["password"]


class AliasUserForm(CustomForm):
username = forms.CharField(widget=forms.TextInput)

class Meta:
model = models.User
fields = ["password"]


class ChangePasswordForm(CustomForm):
current_password = forms.CharField(widget=forms.PasswordInput)
confirm_password = forms.CharField(widget=forms.PasswordInput)
Expand Down
130 changes: 130 additions & 0 deletions bookwyrm/migrations/0182_auto_20231027_1122.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,130 @@
# Generated by Django 3.2.20 on 2023-10-27 11:22

import bookwyrm.models.activitypub_mixin
import bookwyrm.models.fields
from django.conf import settings
from django.db import migrations, models
import django.db.models.deletion


class Migration(migrations.Migration):

dependencies = [
("bookwyrm", "0181_merge_20230806_2302"),
]

operations = [
migrations.AddField(
model_name="user",
name="also_known_as",
field=bookwyrm.models.fields.ManyToManyField(to=settings.AUTH_USER_MODEL),
),
migrations.AddField(
model_name="user",
name="moved_to",
field=bookwyrm.models.fields.RemoteIdField(
max_length=255,
null=True,
validators=[bookwyrm.models.fields.validate_remote_id],
),
),
migrations.AlterField(
model_name="notification",
name="notification_type",
field=models.CharField(
choices=[
("FAVORITE", "Favorite"),
("REPLY", "Reply"),
("MENTION", "Mention"),
("TAG", "Tag"),
("FOLLOW", "Follow"),
("FOLLOW_REQUEST", "Follow Request"),
("BOOST", "Boost"),
("IMPORT", "Import"),
("ADD", "Add"),
("REPORT", "Report"),
("LINK_DOMAIN", "Link Domain"),
("INVITE", "Invite"),
("ACCEPT", "Accept"),
("JOIN", "Join"),
("LEAVE", "Leave"),
("REMOVE", "Remove"),
("GROUP_PRIVACY", "Group Privacy"),
("GROUP_NAME", "Group Name"),
("GROUP_DESCRIPTION", "Group Description"),
("MOVE", "Move"),
],
max_length=255,
),
),
migrations.CreateModel(
name="Move",
fields=[
(
"id",
models.AutoField(
auto_created=True,
primary_key=True,
serialize=False,
verbose_name="ID",
),
),
("created_date", models.DateTimeField(auto_now_add=True)),
("updated_date", models.DateTimeField(auto_now=True)),
(
"remote_id",
bookwyrm.models.fields.RemoteIdField(
max_length=255,
null=True,
validators=[bookwyrm.models.fields.validate_remote_id],
),
),
("object", bookwyrm.models.fields.CharField(max_length=255)),
(
"origin",
bookwyrm.models.fields.CharField(
blank=True, default="", max_length=255, null=True
),
),
(
"user",
bookwyrm.models.fields.ForeignKey(
on_delete=django.db.models.deletion.PROTECT,
to=settings.AUTH_USER_MODEL,
),
),
],
options={
"abstract": False,
},
bases=(bookwyrm.models.activitypub_mixin.ActivityMixin, models.Model),
),
migrations.CreateModel(
name="MoveUser",
fields=[
(
"move_ptr",
models.OneToOneField(
auto_created=True,
on_delete=django.db.models.deletion.CASCADE,
parent_link=True,
primary_key=True,
serialize=False,
to="bookwyrm.move",
),
),
(
"target",
bookwyrm.models.fields.ForeignKey(
on_delete=django.db.models.deletion.PROTECT,
related_name="move_target",
to=settings.AUTH_USER_MODEL,
),
),
],
options={
"abstract": False,
},
bases=("bookwyrm.move",),
),
]
2 changes: 2 additions & 0 deletions bookwyrm/models/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,8 @@

from .import_job import ImportJob, ImportItem

from .move import MoveUser

from .site import SiteSettings, Theme, SiteInvite
from .site import PasswordReset, InviteRequest
from .announcement import Announcement
Expand Down
72 changes: 72 additions & 0 deletions bookwyrm/models/move.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
""" move an object including migrating a user account """
from django.core.exceptions import PermissionDenied
from django.db import models

from bookwyrm import activitypub
from .activitypub_mixin import ActivityMixin
from .base_model import BookWyrmModel
from . import fields
from .notification import Notification


class Move(ActivityMixin, BookWyrmModel):
"""migrating an activitypub user account"""

user = fields.ForeignKey(
"User", on_delete=models.PROTECT, activitypub_field="actor"
)

object = fields.CharField(
max_length=255,
blank=False,
null=False,
activitypub_field="object",
)

origin = fields.CharField(
max_length=255,
blank=True,
null=True,
default="",
activitypub_field="origin",
)

activity_serializer = activitypub.Move


class MoveUser(Move):
"""migrating an activitypub user account"""

target = fields.ForeignKey(
"User",
on_delete=models.PROTECT,
related_name="move_target",
activitypub_field="target",
)

def save(self, *args, **kwargs):
"""update user info and broadcast it"""

# only allow if the source is listed in the target's alsoKnownAs
if self.user in self.target.also_known_as.all():

self.user.also_known_as.add(self.target.id)
self.user.update_active_date()
self.user.moved_to = self.target.remote_id
self.user.save(update_fields=["moved_to"])

if self.user.local:
kwargs[
"broadcast"
] = True # Only broadcast if we are initiating the Move

super().save(*args, **kwargs)

for follower in self.user.followers.all():
if follower.local:
Notification.notify(
follower, self.user, notification_type=Notification.MOVE
)

else:
raise PermissionDenied()
5 changes: 4 additions & 1 deletion bookwyrm/models/notification.py
Original file line number Diff line number Diff line change
Expand Up @@ -40,11 +40,14 @@ class Notification(BookWyrmModel):
GROUP_NAME = "GROUP_NAME"
GROUP_DESCRIPTION = "GROUP_DESCRIPTION"

# Migrations
MOVE = "MOVE"

# pylint: disable=line-too-long
NotificationType = models.TextChoices(
# there has got be a better way to do this
"NotificationType",
f"{FAVORITE} {REPLY} {MENTION} {TAG} {FOLLOW} {FOLLOW_REQUEST} {BOOST} {IMPORT} {ADD} {REPORT} {LINK_DOMAIN} {INVITE} {ACCEPT} {JOIN} {LEAVE} {REMOVE} {GROUP_PRIVACY} {GROUP_NAME} {GROUP_DESCRIPTION}",
f"{FAVORITE} {REPLY} {MENTION} {TAG} {FOLLOW} {FOLLOW_REQUEST} {BOOST} {IMPORT} {ADD} {REPORT} {LINK_DOMAIN} {INVITE} {ACCEPT} {JOIN} {LEAVE} {REMOVE} {GROUP_PRIVACY} {GROUP_NAME} {GROUP_DESCRIPTION} {MOVE}",
)

user = models.ForeignKey("User", on_delete=models.CASCADE)
Expand Down
15 changes: 15 additions & 0 deletions bookwyrm/models/user.py
Original file line number Diff line number Diff line change
Expand Up @@ -140,6 +140,19 @@ class User(OrderedCollectionPageMixin, AbstractUser):
theme = models.ForeignKey("Theme", null=True, blank=True, on_delete=models.SET_NULL)
hide_follows = fields.BooleanField(default=False)

# migration fields

moved_to = fields.RemoteIdField(
null=True, unique=False, activitypub_field="movedTo", deduplication_field=False
)
also_known_as = fields.ManyToManyField(
"self",
symmetrical=False,
unique=False,
activitypub_field="alsoKnownAs",
deduplication_field=False,
)

# options to turn features on and off
show_goal = models.BooleanField(default=True)
show_suggested_users = models.BooleanField(default=True)
Expand Down Expand Up @@ -314,6 +327,8 @@ def to_activity(self, **kwargs):
"schema": "http://schema.org#",
"PropertyValue": "schema:PropertyValue",
"value": "schema:value",
"alsoKnownAs": {"@id": "as:alsoKnownAs", "@type": "@id"},
"movedTo": {"@id": "as:movedTo", "@type": "@id"},
},
]
return activity_object
Expand Down
Loading

0 comments on commit 0502f6b

Please sign in to comment.