Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add Move activity for user migration (with small change) #3076

Merged
merged 15 commits into from
Nov 2, 2023
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
Loading