diff --git a/bookwyrm/activitypub/__init__.py b/bookwyrm/activitypub/__init__.py
index 05ca44476..e2bd4261d 100644
--- a/bookwyrm/activitypub/__init__.py
+++ b/bookwyrm/activitypub/__init__.py
@@ -19,6 +19,7 @@ from .verbs import Create, Delete, Undo, Update
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
diff --git a/bookwyrm/activitypub/verbs.py b/bookwyrm/activitypub/verbs.py
index 4b7514b5a..3432da4d5 100644
--- a/bookwyrm/activitypub/verbs.py
+++ b/bookwyrm/activitypub/verbs.py
@@ -231,3 +231,28 @@ 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"""
+
+ # note the spec example for target and origin is an object but
+ # Mastodon uses a URI string and TBH this makes more sense
+ # Is there a way we can account for either format?
+
+ object: str
+ type: str = "Move"
+ target: str
+ origin: str
+
+ def action(self, allow_external_connections=True):
+ """move"""
+
+ # we need to work out whether the object is a user or something else.
+
+ object_is_user = True # TODO!
+
+ if object_is_user:
+ self.to_model(object_is_user=True allow_external_connections=allow_external_connections)
+ else:
+ self.to_model(object_is_user=False allow_external_connections=allow_external_connections)
\ No newline at end of file
diff --git a/bookwyrm/models/move.py b/bookwyrm/models/move.py
new file mode 100644
index 000000000..cb597e84a
--- /dev/null
+++ b/bookwyrm/models/move.py
@@ -0,0 +1,50 @@
+""" move an object including migrating a user account """
+from django.db import models
+
+from bookwyrm import activitypub
+from .activitypub_mixin import ActivityMixin
+from .base_model import BookWyrmModel
+from . import fields
+from .status import Status
+
+
+class Move(ActivityMixin, BookWyrmModel):
+ """migrating an activitypub user account"""
+
+ user = fields.ForeignKey(
+ "User", on_delete=models.PROTECT, activitypub_field="actor"
+ )
+
+ # TODO: can we just use the abstract class here?
+ activitypub_object = fields.ForeignKey(
+ "BookWyrmModel", on_delete=models.PROTECT,
+ activitypub_field="object",
+ blank=True,
+ null=True
+ )
+
+ target = fields.CharField(
+ max_length=255, blank=True, null=True, deduplication_field=True
+ )
+
+ origin = fields.CharField(
+ max_length=255, blank=True, null=True, deduplication_field=True
+ )
+
+ activity_serializer = activitypub.Move
+
+ # pylint: disable=unused-argument
+ @classmethod
+ def ignore_activity(cls, activity, allow_external_connections=True):
+ """don't bother with incoming moves of unknown objects"""
+ # TODO how do we check this for any conceivable object?
+ pass
+
+ def save(self, *args, **kwargs):
+ """update user active time"""
+ self.user.update_active_date()
+ super().save(*args, **kwargs)
+
+ # Ok what else? We can trigger a notification for followers of a user who sends a `Move` for themselves
+ # What about when a book is merged (i.e. moved from one id into another)? We could use that to send out a message
+ # to other Bookwyrm instances to update their remote_id for the book, but ...how do we trigger any action?
\ No newline at end of file
diff --git a/bookwyrm/models/notification.py b/bookwyrm/models/notification.py
index 522038f9a..48baacfaf 100644
--- a/bookwyrm/models/notification.py
+++ b/bookwyrm/models/notification.py
@@ -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)
@@ -326,3 +329,14 @@ def notify_user_on_follow(sender, instance, created, *args, **kwargs):
notification_type=Notification.FOLLOW,
read=False,
)
+
+@receiver(models.signals.post_save, sender=Move)
+# pylint: disable=unused-argument
+def notify_on_move(sender, instance, *args, **kwargs):
+ """someone moved something"""
+ Notification.notify(
+ instance.status.user,
+ instance.user,
+ related_object=instance.object,
+ notification_type=Notification.MOVE,
+ )
\ No newline at end of file
diff --git a/bookwyrm/templates/notifications/item.html b/bookwyrm/templates/notifications/item.html
index b53abe3d1..b2020839a 100644
--- a/bookwyrm/templates/notifications/item.html
+++ b/bookwyrm/templates/notifications/item.html
@@ -35,4 +35,6 @@
{% include 'notifications/items/update.html' %}
{% elif notification.notification_type == 'GROUP_DESCRIPTION' %}
{% include 'notifications/items/update.html' %}
+ {% elif notification.notification_type == 'MOVE' %}
+ {% include 'notifications/items/move.html' %}
{% endif %}
diff --git a/bookwyrm/templates/notifications/items/move.html b/bookwyrm/templates/notifications/items/move.html
new file mode 100644
index 000000000..ffa23829f
--- /dev/null
+++ b/bookwyrm/templates/notifications/items/move.html
@@ -0,0 +1,28 @@
+{% extends 'notifications/items/layout.html' %}
+
+{% load i18n %}
+{% load utilities %}
+
+{% block primary_link %}{% spaceless %}
+ {{ notification.related_object.local_path }}
+{% endspaceless %}{% endblock %}
+
+{% block icon %}
+
+{% endblock %}
+
+{% block description %}
+
+ {% blocktrans trimmed with object_name=notification.related_object.name object_path=notification.related_object.local_path %}
+ {{ related_user }}
+ moved {{ object_name }}
+ "{{ object_name }}"
+ {% endblocktrans %}
+
+
+
+{% endblock %}