We are currently solving the following issue within our repository. Here is the issue text:
--- BEGIN ISSUE ---
Wrong baseurl in remote @-mention notification
When someone on a remote server @-mentions me on Bookwyrm, the "status" link in notifications has the wrong baseUrl.

Example:

![image](https://user-images.githubusercontent.com/266454/106845345-999c0f80-665f-11eb-8825-7415fb412497.png)

The url that the username `Flancian` links to is `https://bookwyrm.social/user/flancian@social.coop`. The url that `status` links to is `https://social.coop/users/flancian/status/14794`, which takes me to a 404 on the `social.coop` server. The correct url that status should be linking to is `https://bookwyrm.social/user/flancian@social.coop/status/14794`.

I've confirmed this happens for @-mentions from any remote server.

--- END ISSUE ---

Below are some code segments, each from a relevant file. One or more of these files may contain bugs.
--- BEGIN FILES ---
Path: `bookwyrm/models/base_model.py`
Content:
```
1 ''' base model with default fields '''
2 from base64 import b64encode
3 from functools import reduce
4 import operator
5 from uuid import uuid4
6 
7 from Crypto.PublicKey import RSA
8 from Crypto.Signature import pkcs1_15
9 from Crypto.Hash import SHA256
10 from django.core.paginator import Paginator
11 from django.db import models
12 from django.db.models import Q
13 from django.dispatch import receiver
14 
15 from bookwyrm import activitypub
16 from bookwyrm.settings import DOMAIN, PAGE_LENGTH
17 from .fields import ImageField, ManyToManyField, RemoteIdField
18 
19 
20 class BookWyrmModel(models.Model):
21     ''' shared fields '''
22     created_date = models.DateTimeField(auto_now_add=True)
23     updated_date = models.DateTimeField(auto_now=True)
24     remote_id = RemoteIdField(null=True, activitypub_field='id')
25 
26     def get_remote_id(self):
27         ''' generate a url that resolves to the local object '''
28         base_path = 'https://%s' % DOMAIN
29         if hasattr(self, 'user'):
30             base_path = self.user.remote_id
31         model_name = type(self).__name__.lower()
32         return '%s/%s/%d' % (base_path, model_name, self.id)
33 
34     class Meta:
35         ''' this is just here to provide default fields for other models '''
36         abstract = True
37 
38     @property
39     def local_path(self):
40         ''' how to link to this object in the local app '''
41         return self.get_remote_id().replace('https://%s' % DOMAIN, '')
42 
43 
44 @receiver(models.signals.post_save)
45 #pylint: disable=unused-argument
46 def execute_after_save(sender, instance, created, *args, **kwargs):
47     ''' set the remote_id after save (when the id is available) '''
48     if not created or not hasattr(instance, 'get_remote_id'):
49         return
50     if not instance.remote_id:
51         instance.remote_id = instance.get_remote_id()
52         instance.save()
53 
54 
55 def unfurl_related_field(related_field, sort_field=None):
56     ''' load reverse lookups (like public key owner or Status attachment '''
57     if hasattr(related_field, 'all'):
58         return [unfurl_related_field(i) for i in related_field.order_by(
59             sort_field).all()]
60     if related_field.reverse_unfurl:
61         return related_field.field_to_activity()
62     return related_field.remote_id
63 
64 
65 class ActivitypubMixin:
66     ''' add this mixin for models that are AP serializable '''
67     activity_serializer = lambda: {}
68     reverse_unfurl = False
69 
70     def __init__(self, *args, **kwargs):
71         ''' collect some info on model fields '''
72         self.image_fields = []
73         self.many_to_many_fields = []
74         self.simple_fields = [] # "simple"
75         for field in self._meta.get_fields():
76             if not hasattr(field, 'field_to_activity'):
77                 continue
78 
79             if isinstance(field, ImageField):
80                 self.image_fields.append(field)
81             elif isinstance(field, ManyToManyField):
82                 self.many_to_many_fields.append(field)
83             else:
84                 self.simple_fields.append(field)
85 
86         self.activity_fields = self.image_fields + \
87                 self.many_to_many_fields + self.simple_fields
88 
89         self.deserialize_reverse_fields = self.deserialize_reverse_fields \
90                 if hasattr(self, 'deserialize_reverse_fields') else []
91         self.serialize_reverse_fields = self.serialize_reverse_fields \
92                 if hasattr(self, 'serialize_reverse_fields') else []
93 
94         super().__init__(*args, **kwargs)
95 
96 
97     @classmethod
98     def find_existing_by_remote_id(cls, remote_id):
99         ''' look up a remote id in the db '''
100         return cls.find_existing({'id': remote_id})
101 
102     @classmethod
103     def find_existing(cls, data):
104         ''' compare data to fields that can be used for deduplation.
105         This always includes remote_id, but can also be unique identifiers
106         like an isbn for an edition '''
107         filters = []
108         for field in cls._meta.get_fields():
109             if not hasattr(field, 'deduplication_field') or \
110                     not field.deduplication_field:
111                 continue
112 
113             value = data.get(field.get_activitypub_field())
114             if not value:
115                 continue
116             filters.append({field.name: value})
117 
118         if hasattr(cls, 'origin_id') and 'id' in data:
119             # kinda janky, but this handles special case for books
120             filters.append({'origin_id': data['id']})
121 
122         if not filters:
123             # if there are no deduplication fields, it will match the first
124             # item no matter what. this shouldn't happen but just in case.
125             return None
126 
127         objects = cls.objects
128         if hasattr(objects, 'select_subclasses'):
129             objects = objects.select_subclasses()
130 
131         # an OR operation on all the match fields
132         match = objects.filter(
133             reduce(
134                 operator.or_, (Q(**f) for f in filters)
135             )
136         )
137         # there OUGHT to be only one match
138         return match.first()
139 
140 
141     def to_activity(self):
142         ''' convert from a model to an activity '''
143         activity = generate_activity(self)
144         return self.activity_serializer(**activity).serialize()
145 
146 
147     def to_create_activity(self, user, **kwargs):
148         ''' returns the object wrapped in a Create activity '''
149         activity_object = self.to_activity(**kwargs)
150 
151         signature = None
152         create_id = self.remote_id + '/activity'
153         if 'content' in activity_object:
154             signer = pkcs1_15.new(RSA.import_key(user.key_pair.private_key))
155             content = activity_object['content']
156             signed_message = signer.sign(SHA256.new(content.encode('utf8')))
157 
158             signature = activitypub.Signature(
159                 creator='%s#main-key' % user.remote_id,
160                 created=activity_object['published'],
161                 signatureValue=b64encode(signed_message).decode('utf8')
162             )
163 
164         return activitypub.Create(
165             id=create_id,
166             actor=user.remote_id,
167             to=activity_object['to'],
168             cc=activity_object['cc'],
169             object=activity_object,
170             signature=signature,
171         ).serialize()
172 
173 
174     def to_delete_activity(self, user):
175         ''' notice of deletion '''
176         return activitypub.Delete(
177             id=self.remote_id + '/activity',
178             actor=user.remote_id,
179             to=['%s/followers' % user.remote_id],
180             cc=['https://www.w3.org/ns/activitystreams#Public'],
181             object=self.to_activity(),
182         ).serialize()
183 
184 
185     def to_update_activity(self, user):
186         ''' wrapper for Updates to an activity '''
187         activity_id = '%s#update/%s' % (self.remote_id, uuid4())
188         return activitypub.Update(
189             id=activity_id,
190             actor=user.remote_id,
191             to=['https://www.w3.org/ns/activitystreams#Public'],
192             object=self.to_activity()
193         ).serialize()
194 
195 
196     def to_undo_activity(self, user):
197         ''' undo an action '''
198         return activitypub.Undo(
199             id='%s#undo' % self.remote_id,
200             actor=user.remote_id,
201             object=self.to_activity()
202         ).serialize()
203 
204 
205 class OrderedCollectionPageMixin(ActivitypubMixin):
206     ''' just the paginator utilities, so you don't HAVE to
207         override ActivitypubMixin's to_activity (ie, for outbox '''
208     @property
209     def collection_remote_id(self):
210         ''' this can be overriden if there's a special remote id, ie outbox '''
211         return self.remote_id
212 
213 
214     def to_ordered_collection(self, queryset, \
215             remote_id=None, page=False, collection_only=False, **kwargs):
216         ''' an ordered collection of whatevers '''
217         if not queryset.ordered:
218             raise RuntimeError('queryset must be ordered')
219 
220         remote_id = remote_id or self.remote_id
221         if page:
222             return to_ordered_collection_page(
223                 queryset, remote_id, **kwargs)
224 
225         if collection_only or not hasattr(self, 'activity_serializer'):
226             serializer = activitypub.OrderedCollection
227             activity = {}
228         else:
229             serializer = self.activity_serializer
230             # a dict from the model fields
231             activity = generate_activity(self)
232 
233         if remote_id:
234             activity['id'] = remote_id
235 
236         paginated = Paginator(queryset, PAGE_LENGTH)
237         # add computed fields specific to orderd collections
238         activity['totalItems'] = paginated.count
239         activity['first'] = '%s?page=1' % remote_id
240         activity['last'] = '%s?page=%d' % (remote_id, paginated.num_pages)
241 
242         return serializer(**activity).serialize()
243 
244 
245 # pylint: disable=unused-argument
246 def to_ordered_collection_page(
247         queryset, remote_id, id_only=False, page=1, **kwargs):
248     ''' serialize and pagiante a queryset '''
249     paginated = Paginator(queryset, PAGE_LENGTH)
250 
251     activity_page = paginated.page(page)
252     if id_only:
253         items = [s.remote_id for s in activity_page.object_list]
254     else:
255         items = [s.to_activity() for s in activity_page.object_list]
256 
257     prev_page = next_page = None
258     if activity_page.has_next():
259         next_page = '%s?page=%d' % (remote_id, activity_page.next_page_number())
260     if activity_page.has_previous():
261         prev_page = '%s?page=%d' % \
262                 (remote_id, activity_page.previous_page_number())
263     return activitypub.OrderedCollectionPage(
264         id='%s?page=%s' % (remote_id, page),
265         partOf=remote_id,
266         orderedItems=items,
267         next=next_page,
268         prev=prev_page
269     ).serialize()
270 
271 
272 class OrderedCollectionMixin(OrderedCollectionPageMixin):
273     ''' extends activitypub models to work as ordered collections '''
274     @property
275     def collection_queryset(self):
276         ''' usually an ordered collection model aggregates a different model '''
277         raise NotImplementedError('Model must define collection_queryset')
278 
279     activity_serializer = activitypub.OrderedCollection
280 
281     def to_activity(self, **kwargs):
282         ''' an ordered collection of the specified model queryset  '''
283         return self.to_ordered_collection(self.collection_queryset, **kwargs)
284 
285 
286 def generate_activity(obj):
287     ''' go through the fields on an object '''
288     activity = {}
289     for field in obj.activity_fields:
290         field.set_activity_from_field(activity, obj)
291 
292     if hasattr(obj, 'serialize_reverse_fields'):
293         # for example, editions of a work
294         for model_field_name, activity_field_name, sort_field in \
295                 obj.serialize_reverse_fields:
296             related_field = getattr(obj, model_field_name)
297             activity[activity_field_name] = \
298                     unfurl_related_field(related_field, sort_field)
299 
300     if not activity.get('id'):
301         activity['id'] = obj.get_remote_id()
302     return activity
303
```
--- END FILES ---

Please first localize the bug based on the issue statement, and then generate a patch according to the `git diff` format fenced by three backticks.

Here is an example:

```diff
diff --git a/examples/server_async.py b/examples/server_async.py
--- a/examples/server_async.py
+++ b/examples/server_async.py
@@ -313,4 +313,4 @@
 
 
 if __name__ == "__main__":
-    asyncio.run(run_async_server("."), debug=True)
+    asyncio.run(run_async_server(), debug=True)
diff --git a/examples/server_sync.py b/examples/server_sync.py
--- a/examples/server_sync.py
+++ b/examples/server_sync.py
@@ -313,5 +313,5 @@
 
 
 if __name__ == "__main__":
-    server = run_sync_server(".")
+    server = run_sync_server()
     server.shutdown()

```

diff --git a/bookwyrm/models/base_model.py b/bookwyrm/models/base_model.py
--- a/bookwyrm/models/base_model.py
+++ b/bookwyrm/models/base_model.py
@@ -27,7 +27,7 @@
         ''' generate a url that resolves to the local object '''
         base_path = 'https://%s' % DOMAIN
         if hasattr(self, 'user'):
-            base_path = self.user.remote_id
+            base_path = '%s%s' % (base_path, self.user.local_path)
         model_name = type(self).__name__.lower()
         return '%s/%s/%d' % (base_path, model_name, self.id)

{"golden_diff": "diff --git a/bookwyrm/models/base_model.py b/bookwyrm/models/base_model.py\n--- a/bookwyrm/models/base_model.py\n+++ b/bookwyrm/models/base_model.py\n@@ -27,7 +27,7 @@\n         ''' generate a url that resolves to the local object '''\n         base_path = 'https://%s' % DOMAIN\n         if hasattr(self, 'user'):\n-            base_path = self.user.remote_id\n+            base_path = '%s%s' % (base_path, self.user.local_path)\n         model_name = type(self).__name__.lower()\n         return '%s/%s/%d' % (base_path, model_name, self.id)\n", "issue": "Wrong baseurl in remote @-mention notification\nWhen someone on a remote server @-mentions me on Bookwyrm, the \"status\" link in notifications has the wrong baseUrl.\r\n\r\nExample:\r\n\r\n![image](https://user-images.githubusercontent.com/266454/106845345-999c0f80-665f-11eb-8825-7415fb412497.png)\r\n\r\nThe url that the username `Flancian` links to is `https://bookwyrm.social/user/flancian@social.coop`. The url that `status` links to is `https://social.coop/users/flancian/status/14794`, which takes me to a 404 on the `social.coop` server. The correct url that status should be linking to is `https://bookwyrm.social/user/flancian@social.coop/status/14794`.\r\n\r\nI've confirmed this happens for @-mentions from any remote server.\n", "before_files": [{"content": "''' base model with default fields '''\nfrom base64 import b64encode\nfrom functools import reduce\nimport operator\nfrom uuid import uuid4\n\nfrom Crypto.PublicKey import RSA\nfrom Crypto.Signature import pkcs1_15\nfrom Crypto.Hash import SHA256\nfrom django.core.paginator import Paginator\nfrom django.db import models\nfrom django.db.models import Q\nfrom django.dispatch import receiver\n\nfrom bookwyrm import activitypub\nfrom bookwyrm.settings import DOMAIN, PAGE_LENGTH\nfrom .fields import ImageField, ManyToManyField, RemoteIdField\n\n\nclass BookWyrmModel(models.Model):\n    ''' shared fields '''\n    created_date = models.DateTimeField(auto_now_add=True)\n    updated_date = models.DateTimeField(auto_now=True)\n    remote_id = RemoteIdField(null=True, activitypub_field='id')\n\n    def get_remote_id(self):\n        ''' generate a url that resolves to the local object '''\n        base_path = 'https://%s' % DOMAIN\n        if hasattr(self, 'user'):\n            base_path = self.user.remote_id\n        model_name = type(self).__name__.lower()\n        return '%s/%s/%d' % (base_path, model_name, self.id)\n\n    class Meta:\n        ''' this is just here to provide default fields for other models '''\n        abstract = True\n\n    @property\n    def local_path(self):\n        ''' how to link to this object in the local app '''\n        return self.get_remote_id().replace('https://%s' % DOMAIN, '')\n\n\n@receiver(models.signals.post_save)\n#pylint: disable=unused-argument\ndef execute_after_save(sender, instance, created, *args, **kwargs):\n    ''' set the remote_id after save (when the id is available) '''\n    if not created or not hasattr(instance, 'get_remote_id'):\n        return\n    if not instance.remote_id:\n        instance.remote_id = instance.get_remote_id()\n        instance.save()\n\n\ndef unfurl_related_field(related_field, sort_field=None):\n    ''' load reverse lookups (like public key owner or Status attachment '''\n    if hasattr(related_field, 'all'):\n        return [unfurl_related_field(i) for i in related_field.order_by(\n            sort_field).all()]\n    if related_field.reverse_unfurl:\n        return related_field.field_to_activity()\n    return related_field.remote_id\n\n\nclass ActivitypubMixin:\n    ''' add this mixin for models that are AP serializable '''\n    activity_serializer = lambda: {}\n    reverse_unfurl = False\n\n    def __init__(self, *args, **kwargs):\n        ''' collect some info on model fields '''\n        self.image_fields = []\n        self.many_to_many_fields = []\n        self.simple_fields = [] # \"simple\"\n        for field in self._meta.get_fields():\n            if not hasattr(field, 'field_to_activity'):\n                continue\n\n            if isinstance(field, ImageField):\n                self.image_fields.append(field)\n            elif isinstance(field, ManyToManyField):\n                self.many_to_many_fields.append(field)\n            else:\n                self.simple_fields.append(field)\n\n        self.activity_fields = self.image_fields + \\\n                self.many_to_many_fields + self.simple_fields\n\n        self.deserialize_reverse_fields = self.deserialize_reverse_fields \\\n                if hasattr(self, 'deserialize_reverse_fields') else []\n        self.serialize_reverse_fields = self.serialize_reverse_fields \\\n                if hasattr(self, 'serialize_reverse_fields') else []\n\n        super().__init__(*args, **kwargs)\n\n\n    @classmethod\n    def find_existing_by_remote_id(cls, remote_id):\n        ''' look up a remote id in the db '''\n        return cls.find_existing({'id': remote_id})\n\n    @classmethod\n    def find_existing(cls, data):\n        ''' compare data to fields that can be used for deduplation.\n        This always includes remote_id, but can also be unique identifiers\n        like an isbn for an edition '''\n        filters = []\n        for field in cls._meta.get_fields():\n            if not hasattr(field, 'deduplication_field') or \\\n                    not field.deduplication_field:\n                continue\n\n            value = data.get(field.get_activitypub_field())\n            if not value:\n                continue\n            filters.append({field.name: value})\n\n        if hasattr(cls, 'origin_id') and 'id' in data:\n            # kinda janky, but this handles special case for books\n            filters.append({'origin_id': data['id']})\n\n        if not filters:\n            # if there are no deduplication fields, it will match the first\n            # item no matter what. this shouldn't happen but just in case.\n            return None\n\n        objects = cls.objects\n        if hasattr(objects, 'select_subclasses'):\n            objects = objects.select_subclasses()\n\n        # an OR operation on all the match fields\n        match = objects.filter(\n            reduce(\n                operator.or_, (Q(**f) for f in filters)\n            )\n        )\n        # there OUGHT to be only one match\n        return match.first()\n\n\n    def to_activity(self):\n        ''' convert from a model to an activity '''\n        activity = generate_activity(self)\n        return self.activity_serializer(**activity).serialize()\n\n\n    def to_create_activity(self, user, **kwargs):\n        ''' returns the object wrapped in a Create activity '''\n        activity_object = self.to_activity(**kwargs)\n\n        signature = None\n        create_id = self.remote_id + '/activity'\n        if 'content' in activity_object:\n            signer = pkcs1_15.new(RSA.import_key(user.key_pair.private_key))\n            content = activity_object['content']\n            signed_message = signer.sign(SHA256.new(content.encode('utf8')))\n\n            signature = activitypub.Signature(\n                creator='%s#main-key' % user.remote_id,\n                created=activity_object['published'],\n                signatureValue=b64encode(signed_message).decode('utf8')\n            )\n\n        return activitypub.Create(\n            id=create_id,\n            actor=user.remote_id,\n            to=activity_object['to'],\n            cc=activity_object['cc'],\n            object=activity_object,\n            signature=signature,\n        ).serialize()\n\n\n    def to_delete_activity(self, user):\n        ''' notice of deletion '''\n        return activitypub.Delete(\n            id=self.remote_id + '/activity',\n            actor=user.remote_id,\n            to=['%s/followers' % user.remote_id],\n            cc=['https://www.w3.org/ns/activitystreams#Public'],\n            object=self.to_activity(),\n        ).serialize()\n\n\n    def to_update_activity(self, user):\n        ''' wrapper for Updates to an activity '''\n        activity_id = '%s#update/%s' % (self.remote_id, uuid4())\n        return activitypub.Update(\n            id=activity_id,\n            actor=user.remote_id,\n            to=['https://www.w3.org/ns/activitystreams#Public'],\n            object=self.to_activity()\n        ).serialize()\n\n\n    def to_undo_activity(self, user):\n        ''' undo an action '''\n        return activitypub.Undo(\n            id='%s#undo' % self.remote_id,\n            actor=user.remote_id,\n            object=self.to_activity()\n        ).serialize()\n\n\nclass OrderedCollectionPageMixin(ActivitypubMixin):\n    ''' just the paginator utilities, so you don't HAVE to\n        override ActivitypubMixin's to_activity (ie, for outbox '''\n    @property\n    def collection_remote_id(self):\n        ''' this can be overriden if there's a special remote id, ie outbox '''\n        return self.remote_id\n\n\n    def to_ordered_collection(self, queryset, \\\n            remote_id=None, page=False, collection_only=False, **kwargs):\n        ''' an ordered collection of whatevers '''\n        if not queryset.ordered:\n            raise RuntimeError('queryset must be ordered')\n\n        remote_id = remote_id or self.remote_id\n        if page:\n            return to_ordered_collection_page(\n                queryset, remote_id, **kwargs)\n\n        if collection_only or not hasattr(self, 'activity_serializer'):\n            serializer = activitypub.OrderedCollection\n            activity = {}\n        else:\n            serializer = self.activity_serializer\n            # a dict from the model fields\n            activity = generate_activity(self)\n\n        if remote_id:\n            activity['id'] = remote_id\n\n        paginated = Paginator(queryset, PAGE_LENGTH)\n        # add computed fields specific to orderd collections\n        activity['totalItems'] = paginated.count\n        activity['first'] = '%s?page=1' % remote_id\n        activity['last'] = '%s?page=%d' % (remote_id, paginated.num_pages)\n\n        return serializer(**activity).serialize()\n\n\n# pylint: disable=unused-argument\ndef to_ordered_collection_page(\n        queryset, remote_id, id_only=False, page=1, **kwargs):\n    ''' serialize and pagiante a queryset '''\n    paginated = Paginator(queryset, PAGE_LENGTH)\n\n    activity_page = paginated.page(page)\n    if id_only:\n        items = [s.remote_id for s in activity_page.object_list]\n    else:\n        items = [s.to_activity() for s in activity_page.object_list]\n\n    prev_page = next_page = None\n    if activity_page.has_next():\n        next_page = '%s?page=%d' % (remote_id, activity_page.next_page_number())\n    if activity_page.has_previous():\n        prev_page = '%s?page=%d' % \\\n                (remote_id, activity_page.previous_page_number())\n    return activitypub.OrderedCollectionPage(\n        id='%s?page=%s' % (remote_id, page),\n        partOf=remote_id,\n        orderedItems=items,\n        next=next_page,\n        prev=prev_page\n    ).serialize()\n\n\nclass OrderedCollectionMixin(OrderedCollectionPageMixin):\n    ''' extends activitypub models to work as ordered collections '''\n    @property\n    def collection_queryset(self):\n        ''' usually an ordered collection model aggregates a different model '''\n        raise NotImplementedError('Model must define collection_queryset')\n\n    activity_serializer = activitypub.OrderedCollection\n\n    def to_activity(self, **kwargs):\n        ''' an ordered collection of the specified model queryset  '''\n        return self.to_ordered_collection(self.collection_queryset, **kwargs)\n\n\ndef generate_activity(obj):\n    ''' go through the fields on an object '''\n    activity = {}\n    for field in obj.activity_fields:\n        field.set_activity_from_field(activity, obj)\n\n    if hasattr(obj, 'serialize_reverse_fields'):\n        # for example, editions of a work\n        for model_field_name, activity_field_name, sort_field in \\\n                obj.serialize_reverse_fields:\n            related_field = getattr(obj, model_field_name)\n            activity[activity_field_name] = \\\n                    unfurl_related_field(related_field, sort_field)\n\n    if not activity.get('id'):\n        activity['id'] = obj.get_remote_id()\n    return activity\n", "path": "bookwyrm/models/base_model.py"}], "after_files": [{"content": "''' base model with default fields '''\nfrom base64 import b64encode\nfrom functools import reduce\nimport operator\nfrom uuid import uuid4\n\nfrom Crypto.PublicKey import RSA\nfrom Crypto.Signature import pkcs1_15\nfrom Crypto.Hash import SHA256\nfrom django.core.paginator import Paginator\nfrom django.db import models\nfrom django.db.models import Q\nfrom django.dispatch import receiver\n\nfrom bookwyrm import activitypub\nfrom bookwyrm.settings import DOMAIN, PAGE_LENGTH\nfrom .fields import ImageField, ManyToManyField, RemoteIdField\n\n\nclass BookWyrmModel(models.Model):\n    ''' shared fields '''\n    created_date = models.DateTimeField(auto_now_add=True)\n    updated_date = models.DateTimeField(auto_now=True)\n    remote_id = RemoteIdField(null=True, activitypub_field='id')\n\n    def get_remote_id(self):\n        ''' generate a url that resolves to the local object '''\n        base_path = 'https://%s' % DOMAIN\n        if hasattr(self, 'user'):\n            base_path = '%s%s' % (base_path, self.user.local_path)\n        model_name = type(self).__name__.lower()\n        return '%s/%s/%d' % (base_path, model_name, self.id)\n\n    class Meta:\n        ''' this is just here to provide default fields for other models '''\n        abstract = True\n\n    @property\n    def local_path(self):\n        ''' how to link to this object in the local app '''\n        return self.get_remote_id().replace('https://%s' % DOMAIN, '')\n\n\n@receiver(models.signals.post_save)\n#pylint: disable=unused-argument\ndef execute_after_save(sender, instance, created, *args, **kwargs):\n    ''' set the remote_id after save (when the id is available) '''\n    if not created or not hasattr(instance, 'get_remote_id'):\n        return\n    if not instance.remote_id:\n        instance.remote_id = instance.get_remote_id()\n        instance.save()\n\n\ndef unfurl_related_field(related_field, sort_field=None):\n    ''' load reverse lookups (like public key owner or Status attachment '''\n    if hasattr(related_field, 'all'):\n        return [unfurl_related_field(i) for i in related_field.order_by(\n            sort_field).all()]\n    if related_field.reverse_unfurl:\n        return related_field.field_to_activity()\n    return related_field.remote_id\n\n\nclass ActivitypubMixin:\n    ''' add this mixin for models that are AP serializable '''\n    activity_serializer = lambda: {}\n    reverse_unfurl = False\n\n    def __init__(self, *args, **kwargs):\n        ''' collect some info on model fields '''\n        self.image_fields = []\n        self.many_to_many_fields = []\n        self.simple_fields = [] # \"simple\"\n        for field in self._meta.get_fields():\n            if not hasattr(field, 'field_to_activity'):\n                continue\n\n            if isinstance(field, ImageField):\n                self.image_fields.append(field)\n            elif isinstance(field, ManyToManyField):\n                self.many_to_many_fields.append(field)\n            else:\n                self.simple_fields.append(field)\n\n        self.activity_fields = self.image_fields + \\\n                self.many_to_many_fields + self.simple_fields\n\n        self.deserialize_reverse_fields = self.deserialize_reverse_fields \\\n                if hasattr(self, 'deserialize_reverse_fields') else []\n        self.serialize_reverse_fields = self.serialize_reverse_fields \\\n                if hasattr(self, 'serialize_reverse_fields') else []\n\n        super().__init__(*args, **kwargs)\n\n\n    @classmethod\n    def find_existing_by_remote_id(cls, remote_id):\n        ''' look up a remote id in the db '''\n        return cls.find_existing({'id': remote_id})\n\n    @classmethod\n    def find_existing(cls, data):\n        ''' compare data to fields that can be used for deduplation.\n        This always includes remote_id, but can also be unique identifiers\n        like an isbn for an edition '''\n        filters = []\n        for field in cls._meta.get_fields():\n            if not hasattr(field, 'deduplication_field') or \\\n                    not field.deduplication_field:\n                continue\n\n            value = data.get(field.get_activitypub_field())\n            if not value:\n                continue\n            filters.append({field.name: value})\n\n        if hasattr(cls, 'origin_id') and 'id' in data:\n            # kinda janky, but this handles special case for books\n            filters.append({'origin_id': data['id']})\n\n        if not filters:\n            # if there are no deduplication fields, it will match the first\n            # item no matter what. this shouldn't happen but just in case.\n            return None\n\n        objects = cls.objects\n        if hasattr(objects, 'select_subclasses'):\n            objects = objects.select_subclasses()\n\n        # an OR operation on all the match fields\n        match = objects.filter(\n            reduce(\n                operator.or_, (Q(**f) for f in filters)\n            )\n        )\n        # there OUGHT to be only one match\n        return match.first()\n\n\n    def to_activity(self):\n        ''' convert from a model to an activity '''\n        activity = generate_activity(self)\n        return self.activity_serializer(**activity).serialize()\n\n\n    def to_create_activity(self, user, **kwargs):\n        ''' returns the object wrapped in a Create activity '''\n        activity_object = self.to_activity(**kwargs)\n\n        signature = None\n        create_id = self.remote_id + '/activity'\n        if 'content' in activity_object:\n            signer = pkcs1_15.new(RSA.import_key(user.key_pair.private_key))\n            content = activity_object['content']\n            signed_message = signer.sign(SHA256.new(content.encode('utf8')))\n\n            signature = activitypub.Signature(\n                creator='%s#main-key' % user.remote_id,\n                created=activity_object['published'],\n                signatureValue=b64encode(signed_message).decode('utf8')\n            )\n\n        return activitypub.Create(\n            id=create_id,\n            actor=user.remote_id,\n            to=activity_object['to'],\n            cc=activity_object['cc'],\n            object=activity_object,\n            signature=signature,\n        ).serialize()\n\n\n    def to_delete_activity(self, user):\n        ''' notice of deletion '''\n        return activitypub.Delete(\n            id=self.remote_id + '/activity',\n            actor=user.remote_id,\n            to=['%s/followers' % user.remote_id],\n            cc=['https://www.w3.org/ns/activitystreams#Public'],\n            object=self.to_activity(),\n        ).serialize()\n\n\n    def to_update_activity(self, user):\n        ''' wrapper for Updates to an activity '''\n        activity_id = '%s#update/%s' % (self.remote_id, uuid4())\n        return activitypub.Update(\n            id=activity_id,\n            actor=user.remote_id,\n            to=['https://www.w3.org/ns/activitystreams#Public'],\n            object=self.to_activity()\n        ).serialize()\n\n\n    def to_undo_activity(self, user):\n        ''' undo an action '''\n        return activitypub.Undo(\n            id='%s#undo' % self.remote_id,\n            actor=user.remote_id,\n            object=self.to_activity()\n        ).serialize()\n\n\nclass OrderedCollectionPageMixin(ActivitypubMixin):\n    ''' just the paginator utilities, so you don't HAVE to\n        override ActivitypubMixin's to_activity (ie, for outbox '''\n    @property\n    def collection_remote_id(self):\n        ''' this can be overriden if there's a special remote id, ie outbox '''\n        return self.remote_id\n\n\n    def to_ordered_collection(self, queryset, \\\n            remote_id=None, page=False, collection_only=False, **kwargs):\n        ''' an ordered collection of whatevers '''\n        if not queryset.ordered:\n            raise RuntimeError('queryset must be ordered')\n\n        remote_id = remote_id or self.remote_id\n        if page:\n            return to_ordered_collection_page(\n                queryset, remote_id, **kwargs)\n\n        if collection_only or not hasattr(self, 'activity_serializer'):\n            serializer = activitypub.OrderedCollection\n            activity = {}\n        else:\n            serializer = self.activity_serializer\n            # a dict from the model fields\n            activity = generate_activity(self)\n\n        if remote_id:\n            activity['id'] = remote_id\n\n        paginated = Paginator(queryset, PAGE_LENGTH)\n        # add computed fields specific to orderd collections\n        activity['totalItems'] = paginated.count\n        activity['first'] = '%s?page=1' % remote_id\n        activity['last'] = '%s?page=%d' % (remote_id, paginated.num_pages)\n\n        return serializer(**activity).serialize()\n\n\n# pylint: disable=unused-argument\ndef to_ordered_collection_page(\n        queryset, remote_id, id_only=False, page=1, **kwargs):\n    ''' serialize and pagiante a queryset '''\n    paginated = Paginator(queryset, PAGE_LENGTH)\n\n    activity_page = paginated.page(page)\n    if id_only:\n        items = [s.remote_id for s in activity_page.object_list]\n    else:\n        items = [s.to_activity() for s in activity_page.object_list]\n\n    prev_page = next_page = None\n    if activity_page.has_next():\n        next_page = '%s?page=%d' % (remote_id, activity_page.next_page_number())\n    if activity_page.has_previous():\n        prev_page = '%s?page=%d' % \\\n                (remote_id, activity_page.previous_page_number())\n    return activitypub.OrderedCollectionPage(\n        id='%s?page=%s' % (remote_id, page),\n        partOf=remote_id,\n        orderedItems=items,\n        next=next_page,\n        prev=prev_page\n    ).serialize()\n\n\nclass OrderedCollectionMixin(OrderedCollectionPageMixin):\n    ''' extends activitypub models to work as ordered collections '''\n    @property\n    def collection_queryset(self):\n        ''' usually an ordered collection model aggregates a different model '''\n        raise NotImplementedError('Model must define collection_queryset')\n\n    activity_serializer = activitypub.OrderedCollection\n\n    def to_activity(self, **kwargs):\n        ''' an ordered collection of the specified model queryset  '''\n        return self.to_ordered_collection(self.collection_queryset, **kwargs)\n\n\ndef generate_activity(obj):\n    ''' go through the fields on an object '''\n    activity = {}\n    for field in obj.activity_fields:\n        field.set_activity_from_field(activity, obj)\n\n    if hasattr(obj, 'serialize_reverse_fields'):\n        # for example, editions of a work\n        for model_field_name, activity_field_name, sort_field in \\\n                obj.serialize_reverse_fields:\n            related_field = getattr(obj, model_field_name)\n            activity[activity_field_name] = \\\n                    unfurl_related_field(related_field, sort_field)\n\n    if not activity.get('id'):\n        activity['id'] = obj.get_remote_id()\n    return activity\n", "path": "bookwyrm/models/base_model.py"}]}