We are currently solving the following issue within our repository. Here is the issue text:
--- BEGIN ISSUE ---
Issues with federating book links
**Describe the bug**
This may be 2 bugs but I'm not sure:

**1)** Admitted links that come from another instance show up in my pending link domains but are not connected to a book.
![image](https://user-images.githubusercontent.com/66644064/152956488-dab12631-5174-4aae-a368-6ccd3ad8d201.png)

**2)** The other way round, when I import a book from wyrms to b.s, it doesn't have the links (this may be due to them not being approved for b.s though):
![image](https://user-images.githubusercontent.com/66644064/152956983-152d65c1-4e0e-428b-8dd5-dc1df4c900f5.png)



--- 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/activitypub/base_activity.py`
Content:
```
1 """ basics for an activitypub serializer """
2 from dataclasses import dataclass, fields, MISSING
3 from json import JSONEncoder
4 
5 from django.apps import apps
6 from django.db import IntegrityError, transaction
7 
8 from bookwyrm.connectors import ConnectorException, get_data
9 from bookwyrm.tasks import app
10 
11 
12 class ActivitySerializerError(ValueError):
13     """routine problems serializing activitypub json"""
14 
15 
16 class ActivityEncoder(JSONEncoder):
17     """used to convert an Activity object into json"""
18 
19     def default(self, o):
20         return o.__dict__
21 
22 
23 @dataclass
24 # pylint: disable=invalid-name
25 class Signature:
26     """public key block"""
27 
28     creator: str
29     created: str
30     signatureValue: str
31     type: str = "RsaSignature2017"
32 
33 
34 def naive_parse(activity_objects, activity_json, serializer=None):
35     """this navigates circular import issues by looking up models' serializers"""
36     if not serializer:
37         if activity_json.get("publicKeyPem"):
38             # ugh
39             activity_json["type"] = "PublicKey"
40 
41         activity_type = activity_json.get("type")
42         try:
43             serializer = activity_objects[activity_type]
44         except KeyError as err:
45             # we know this exists and that we can't handle it
46             if activity_type in ["Question"]:
47                 return None
48             raise ActivitySerializerError(err)
49 
50     return serializer(activity_objects=activity_objects, **activity_json)
51 
52 
53 @dataclass(init=False)
54 class ActivityObject:
55     """actor activitypub json"""
56 
57     id: str
58     type: str
59 
60     def __init__(self, activity_objects=None, **kwargs):
61         """this lets you pass in an object with fields that aren't in the
62         dataclass, which it ignores. Any field in the dataclass is required or
63         has a default value"""
64         for field in fields(self):
65             try:
66                 value = kwargs[field.name]
67                 if value in (None, MISSING, {}):
68                     raise KeyError()
69                 try:
70                     is_subclass = issubclass(field.type, ActivityObject)
71                 except TypeError:
72                     is_subclass = False
73                 # serialize a model obj
74                 if hasattr(value, "to_activity"):
75                     value = value.to_activity()
76                 # parse a dict into the appropriate activity
77                 elif is_subclass and isinstance(value, dict):
78                     if activity_objects:
79                         value = naive_parse(activity_objects, value)
80                     else:
81                         value = naive_parse(
82                             activity_objects, value, serializer=field.type
83                         )
84 
85             except KeyError:
86                 if field.default == MISSING and field.default_factory == MISSING:
87                     raise ActivitySerializerError(
88                         f"Missing required field: {field.name}"
89                     )
90                 value = field.default
91             setattr(self, field.name, value)
92 
93     # pylint: disable=too-many-locals,too-many-branches,too-many-arguments
94     def to_model(
95         self, model=None, instance=None, allow_create=True, save=True, overwrite=True
96     ):
97         """convert from an activity to a model instance"""
98         model = model or get_model_from_type(self.type)
99 
100         # only reject statuses if we're potentially creating them
101         if (
102             allow_create
103             and hasattr(model, "ignore_activity")
104             and model.ignore_activity(self)
105         ):
106             return None
107 
108         # check for an existing instance
109         instance = instance or model.find_existing(self.serialize())
110 
111         if not instance and not allow_create:
112             # so that we don't create when we want to delete or update
113             return None
114         instance = instance or model()
115 
116         # keep track of what we've changed
117         update_fields = []
118         # sets field on the model using the activity value
119         for field in instance.simple_fields:
120             try:
121                 changed = field.set_field_from_activity(
122                     instance, self, overwrite=overwrite
123                 )
124                 if changed:
125                     update_fields.append(field.name)
126             except AttributeError as e:
127                 raise ActivitySerializerError(e)
128 
129         # image fields have to be set after other fields because they can save
130         # too early and jank up users
131         for field in instance.image_fields:
132             changed = field.set_field_from_activity(
133                 instance, self, save=save, overwrite=overwrite
134             )
135             if changed:
136                 update_fields.append(field.name)
137 
138         if not save:
139             return instance
140 
141         with transaction.atomic():
142             # can't force an update on fields unless the object already exists in the db
143             if not instance.id:
144                 update_fields = None
145             # we can't set many to many and reverse fields on an unsaved object
146             try:
147                 try:
148                     instance.save(broadcast=False, update_fields=update_fields)
149                 except TypeError:
150                     instance.save(update_fields=update_fields)
151             except IntegrityError as e:
152                 raise ActivitySerializerError(e)
153 
154             # add many to many fields, which have to be set post-save
155             for field in instance.many_to_many_fields:
156                 # mention books/users, for example
157                 field.set_field_from_activity(instance, self)
158 
159         # reversed relationships in the models
160         for (
161             model_field_name,
162             activity_field_name,
163         ) in instance.deserialize_reverse_fields:
164             # attachments on Status, for example
165             values = getattr(self, activity_field_name)
166             if values is None or values is MISSING:
167                 continue
168 
169             model_field = getattr(model, model_field_name)
170             # creating a Work, model_field is 'editions'
171             # creating a User, model field is 'key_pair'
172             related_model = model_field.field.model
173             related_field_name = model_field.field.name
174 
175             for item in values:
176                 set_related_field.delay(
177                     related_model.__name__,
178                     instance.__class__.__name__,
179                     related_field_name,
180                     instance.remote_id,
181                     item,
182                 )
183         return instance
184 
185     def serialize(self, **kwargs):
186         """convert to dictionary with context attr"""
187         omit = kwargs.get("omit", ())
188         data = self.__dict__.copy()
189         # recursively serialize
190         for (k, v) in data.items():
191             try:
192                 if issubclass(type(v), ActivityObject):
193                     data[k] = v.serialize()
194             except TypeError:
195                 pass
196         data = {k: v for (k, v) in data.items() if v is not None and k not in omit}
197         if "@context" not in omit:
198             data["@context"] = "https://www.w3.org/ns/activitystreams"
199         return data
200 
201 
202 @app.task(queue="medium_priority")
203 @transaction.atomic
204 def set_related_field(
205     model_name, origin_model_name, related_field_name, related_remote_id, data
206 ):
207     """load reverse related fields (editions, attachments) without blocking"""
208     model = apps.get_model(f"bookwyrm.{model_name}", require_ready=True)
209     origin_model = apps.get_model(f"bookwyrm.{origin_model_name}", require_ready=True)
210 
211     if isinstance(data, str):
212         existing = model.find_existing_by_remote_id(data)
213         if existing:
214             data = existing.to_activity()
215         else:
216             data = get_data(data)
217     activity = model.activity_serializer(**data)
218 
219     # this must exist because it's the object that triggered this function
220     instance = origin_model.find_existing_by_remote_id(related_remote_id)
221     if not instance:
222         raise ValueError(f"Invalid related remote id: {related_remote_id}")
223 
224     # set the origin's remote id on the activity so it will be there when
225     # the model instance is created
226     # edition.parentWork = instance, for example
227     model_field = getattr(model, related_field_name)
228     if hasattr(model_field, "activitypub_field"):
229         setattr(activity, getattr(model_field, "activitypub_field"), instance.remote_id)
230     item = activity.to_model()
231 
232     # if the related field isn't serialized (attachments on Status), then
233     # we have to set it post-creation
234     if not hasattr(model_field, "activitypub_field"):
235         setattr(item, related_field_name, instance)
236         item.save()
237 
238 
239 def get_model_from_type(activity_type):
240     """given the activity, what type of model"""
241     models = apps.get_models()
242     model = [
243         m
244         for m in models
245         if hasattr(m, "activity_serializer")
246         and hasattr(m.activity_serializer, "type")
247         and m.activity_serializer.type == activity_type
248     ]
249     if not model:
250         raise ActivitySerializerError(
251             f'No model found for activity type "{activity_type}"'
252         )
253     return model[0]
254 
255 
256 def resolve_remote_id(
257     remote_id, model=None, refresh=False, save=True, get_activity=False
258 ):
259     """take a remote_id and return an instance, creating if necessary"""
260     if model:  # a bonus check we can do if we already know the model
261         if isinstance(model, str):
262             model = apps.get_model(f"bookwyrm.{model}", require_ready=True)
263         result = model.find_existing_by_remote_id(remote_id)
264         if result and not refresh:
265             return result if not get_activity else result.to_activity_dataclass()
266 
267     # load the data and create the object
268     try:
269         data = get_data(remote_id)
270     except ConnectorException:
271         raise ActivitySerializerError(
272             f"Could not connect to host for remote_id: {remote_id}"
273         )
274     # determine the model implicitly, if not provided
275     # or if it's a model with subclasses like Status, check again
276     if not model or hasattr(model.objects, "select_subclasses"):
277         model = get_model_from_type(data.get("type"))
278 
279     # check for existing items with shared unique identifiers
280     result = model.find_existing(data)
281     if result and not refresh:
282         return result if not get_activity else result.to_activity_dataclass()
283 
284     item = model.activity_serializer(**data)
285     if get_activity:
286         return item
287 
288     # if we're refreshing, "result" will be set and we'll update it
289     return item.to_model(model=model, instance=result, save=save)
290 
291 
292 @dataclass(init=False)
293 class Link(ActivityObject):
294     """for tagging a book in a status"""
295 
296     href: str
297     name: str = None
298     mediaType: str = None
299     id: str = None
300     attributedTo: str = None
301     type: str = "Link"
302 
303     def serialize(self, **kwargs):
304         """remove fields"""
305         omit = ("id", "type", "@context")
306         return super().serialize(omit=omit)
307 
308 
309 @dataclass(init=False)
310 class Mention(Link):
311     """a subtype of Link for mentioning an actor"""
312 
313     type: str = "Mention"
314
```
--- 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/activitypub/base_activity.py b/bookwyrm/activitypub/base_activity.py
--- a/bookwyrm/activitypub/base_activity.py
+++ b/bookwyrm/activitypub/base_activity.py
@@ -227,7 +227,7 @@
     model_field = getattr(model, related_field_name)
     if hasattr(model_field, "activitypub_field"):
         setattr(activity, getattr(model_field, "activitypub_field"), instance.remote_id)
-    item = activity.to_model()
+    item = activity.to_model(model=model)
 
     # if the related field isn't serialized (attachments on Status), then
     # we have to set it post-creation
@@ -298,6 +298,7 @@
     mediaType: str = None
     id: str = None
     attributedTo: str = None
+    availability: str = None
     type: str = "Link"
 
     def serialize(self, **kwargs):

{"golden_diff": "diff --git a/bookwyrm/activitypub/base_activity.py b/bookwyrm/activitypub/base_activity.py\n--- a/bookwyrm/activitypub/base_activity.py\n+++ b/bookwyrm/activitypub/base_activity.py\n@@ -227,7 +227,7 @@\n     model_field = getattr(model, related_field_name)\n     if hasattr(model_field, \"activitypub_field\"):\n         setattr(activity, getattr(model_field, \"activitypub_field\"), instance.remote_id)\n-    item = activity.to_model()\n+    item = activity.to_model(model=model)\n \n     # if the related field isn't serialized (attachments on Status), then\n     # we have to set it post-creation\n@@ -298,6 +298,7 @@\n     mediaType: str = None\n     id: str = None\n     attributedTo: str = None\n+    availability: str = None\n     type: str = \"Link\"\n \n     def serialize(self, **kwargs):\n", "issue": "Issues with federating book links\n**Describe the bug**\r\nThis may be 2 bugs but I'm not sure:\r\n\r\n**1)** Admitted links that come from another instance show up in my pending link domains but are not connected to a book.\r\n![image](https://user-images.githubusercontent.com/66644064/152956488-dab12631-5174-4aae-a368-6ccd3ad8d201.png)\r\n\r\n**2)** The other way round, when I import a book from wyrms to b.s, it doesn't have the links (this may be due to them not being approved for b.s though):\r\n![image](https://user-images.githubusercontent.com/66644064/152956983-152d65c1-4e0e-428b-8dd5-dc1df4c900f5.png)\r\n\r\n\n", "before_files": [{"content": "\"\"\" basics for an activitypub serializer \"\"\"\nfrom dataclasses import dataclass, fields, MISSING\nfrom json import JSONEncoder\n\nfrom django.apps import apps\nfrom django.db import IntegrityError, transaction\n\nfrom bookwyrm.connectors import ConnectorException, get_data\nfrom bookwyrm.tasks import app\n\n\nclass ActivitySerializerError(ValueError):\n    \"\"\"routine problems serializing activitypub json\"\"\"\n\n\nclass ActivityEncoder(JSONEncoder):\n    \"\"\"used to convert an Activity object into json\"\"\"\n\n    def default(self, o):\n        return o.__dict__\n\n\n@dataclass\n# pylint: disable=invalid-name\nclass Signature:\n    \"\"\"public key block\"\"\"\n\n    creator: str\n    created: str\n    signatureValue: str\n    type: str = \"RsaSignature2017\"\n\n\ndef naive_parse(activity_objects, activity_json, serializer=None):\n    \"\"\"this navigates circular import issues by looking up models' serializers\"\"\"\n    if not serializer:\n        if activity_json.get(\"publicKeyPem\"):\n            # ugh\n            activity_json[\"type\"] = \"PublicKey\"\n\n        activity_type = activity_json.get(\"type\")\n        try:\n            serializer = activity_objects[activity_type]\n        except KeyError as err:\n            # we know this exists and that we can't handle it\n            if activity_type in [\"Question\"]:\n                return None\n            raise ActivitySerializerError(err)\n\n    return serializer(activity_objects=activity_objects, **activity_json)\n\n\n@dataclass(init=False)\nclass ActivityObject:\n    \"\"\"actor activitypub json\"\"\"\n\n    id: str\n    type: str\n\n    def __init__(self, activity_objects=None, **kwargs):\n        \"\"\"this lets you pass in an object with fields that aren't in the\n        dataclass, which it ignores. Any field in the dataclass is required or\n        has a default value\"\"\"\n        for field in fields(self):\n            try:\n                value = kwargs[field.name]\n                if value in (None, MISSING, {}):\n                    raise KeyError()\n                try:\n                    is_subclass = issubclass(field.type, ActivityObject)\n                except TypeError:\n                    is_subclass = False\n                # serialize a model obj\n                if hasattr(value, \"to_activity\"):\n                    value = value.to_activity()\n                # parse a dict into the appropriate activity\n                elif is_subclass and isinstance(value, dict):\n                    if activity_objects:\n                        value = naive_parse(activity_objects, value)\n                    else:\n                        value = naive_parse(\n                            activity_objects, value, serializer=field.type\n                        )\n\n            except KeyError:\n                if field.default == MISSING and field.default_factory == MISSING:\n                    raise ActivitySerializerError(\n                        f\"Missing required field: {field.name}\"\n                    )\n                value = field.default\n            setattr(self, field.name, value)\n\n    # pylint: disable=too-many-locals,too-many-branches,too-many-arguments\n    def to_model(\n        self, model=None, instance=None, allow_create=True, save=True, overwrite=True\n    ):\n        \"\"\"convert from an activity to a model instance\"\"\"\n        model = model or get_model_from_type(self.type)\n\n        # only reject statuses if we're potentially creating them\n        if (\n            allow_create\n            and hasattr(model, \"ignore_activity\")\n            and model.ignore_activity(self)\n        ):\n            return None\n\n        # check for an existing instance\n        instance = instance or model.find_existing(self.serialize())\n\n        if not instance and not allow_create:\n            # so that we don't create when we want to delete or update\n            return None\n        instance = instance or model()\n\n        # keep track of what we've changed\n        update_fields = []\n        # sets field on the model using the activity value\n        for field in instance.simple_fields:\n            try:\n                changed = field.set_field_from_activity(\n                    instance, self, overwrite=overwrite\n                )\n                if changed:\n                    update_fields.append(field.name)\n            except AttributeError as e:\n                raise ActivitySerializerError(e)\n\n        # image fields have to be set after other fields because they can save\n        # too early and jank up users\n        for field in instance.image_fields:\n            changed = field.set_field_from_activity(\n                instance, self, save=save, overwrite=overwrite\n            )\n            if changed:\n                update_fields.append(field.name)\n\n        if not save:\n            return instance\n\n        with transaction.atomic():\n            # can't force an update on fields unless the object already exists in the db\n            if not instance.id:\n                update_fields = None\n            # we can't set many to many and reverse fields on an unsaved object\n            try:\n                try:\n                    instance.save(broadcast=False, update_fields=update_fields)\n                except TypeError:\n                    instance.save(update_fields=update_fields)\n            except IntegrityError as e:\n                raise ActivitySerializerError(e)\n\n            # add many to many fields, which have to be set post-save\n            for field in instance.many_to_many_fields:\n                # mention books/users, for example\n                field.set_field_from_activity(instance, self)\n\n        # reversed relationships in the models\n        for (\n            model_field_name,\n            activity_field_name,\n        ) in instance.deserialize_reverse_fields:\n            # attachments on Status, for example\n            values = getattr(self, activity_field_name)\n            if values is None or values is MISSING:\n                continue\n\n            model_field = getattr(model, model_field_name)\n            # creating a Work, model_field is 'editions'\n            # creating a User, model field is 'key_pair'\n            related_model = model_field.field.model\n            related_field_name = model_field.field.name\n\n            for item in values:\n                set_related_field.delay(\n                    related_model.__name__,\n                    instance.__class__.__name__,\n                    related_field_name,\n                    instance.remote_id,\n                    item,\n                )\n        return instance\n\n    def serialize(self, **kwargs):\n        \"\"\"convert to dictionary with context attr\"\"\"\n        omit = kwargs.get(\"omit\", ())\n        data = self.__dict__.copy()\n        # recursively serialize\n        for (k, v) in data.items():\n            try:\n                if issubclass(type(v), ActivityObject):\n                    data[k] = v.serialize()\n            except TypeError:\n                pass\n        data = {k: v for (k, v) in data.items() if v is not None and k not in omit}\n        if \"@context\" not in omit:\n            data[\"@context\"] = \"https://www.w3.org/ns/activitystreams\"\n        return data\n\n\n@app.task(queue=\"medium_priority\")\n@transaction.atomic\ndef set_related_field(\n    model_name, origin_model_name, related_field_name, related_remote_id, data\n):\n    \"\"\"load reverse related fields (editions, attachments) without blocking\"\"\"\n    model = apps.get_model(f\"bookwyrm.{model_name}\", require_ready=True)\n    origin_model = apps.get_model(f\"bookwyrm.{origin_model_name}\", require_ready=True)\n\n    if isinstance(data, str):\n        existing = model.find_existing_by_remote_id(data)\n        if existing:\n            data = existing.to_activity()\n        else:\n            data = get_data(data)\n    activity = model.activity_serializer(**data)\n\n    # this must exist because it's the object that triggered this function\n    instance = origin_model.find_existing_by_remote_id(related_remote_id)\n    if not instance:\n        raise ValueError(f\"Invalid related remote id: {related_remote_id}\")\n\n    # set the origin's remote id on the activity so it will be there when\n    # the model instance is created\n    # edition.parentWork = instance, for example\n    model_field = getattr(model, related_field_name)\n    if hasattr(model_field, \"activitypub_field\"):\n        setattr(activity, getattr(model_field, \"activitypub_field\"), instance.remote_id)\n    item = activity.to_model()\n\n    # if the related field isn't serialized (attachments on Status), then\n    # we have to set it post-creation\n    if not hasattr(model_field, \"activitypub_field\"):\n        setattr(item, related_field_name, instance)\n        item.save()\n\n\ndef get_model_from_type(activity_type):\n    \"\"\"given the activity, what type of model\"\"\"\n    models = apps.get_models()\n    model = [\n        m\n        for m in models\n        if hasattr(m, \"activity_serializer\")\n        and hasattr(m.activity_serializer, \"type\")\n        and m.activity_serializer.type == activity_type\n    ]\n    if not model:\n        raise ActivitySerializerError(\n            f'No model found for activity type \"{activity_type}\"'\n        )\n    return model[0]\n\n\ndef resolve_remote_id(\n    remote_id, model=None, refresh=False, save=True, get_activity=False\n):\n    \"\"\"take a remote_id and return an instance, creating if necessary\"\"\"\n    if model:  # a bonus check we can do if we already know the model\n        if isinstance(model, str):\n            model = apps.get_model(f\"bookwyrm.{model}\", require_ready=True)\n        result = model.find_existing_by_remote_id(remote_id)\n        if result and not refresh:\n            return result if not get_activity else result.to_activity_dataclass()\n\n    # load the data and create the object\n    try:\n        data = get_data(remote_id)\n    except ConnectorException:\n        raise ActivitySerializerError(\n            f\"Could not connect to host for remote_id: {remote_id}\"\n        )\n    # determine the model implicitly, if not provided\n    # or if it's a model with subclasses like Status, check again\n    if not model or hasattr(model.objects, \"select_subclasses\"):\n        model = get_model_from_type(data.get(\"type\"))\n\n    # check for existing items with shared unique identifiers\n    result = model.find_existing(data)\n    if result and not refresh:\n        return result if not get_activity else result.to_activity_dataclass()\n\n    item = model.activity_serializer(**data)\n    if get_activity:\n        return item\n\n    # if we're refreshing, \"result\" will be set and we'll update it\n    return item.to_model(model=model, instance=result, save=save)\n\n\n@dataclass(init=False)\nclass Link(ActivityObject):\n    \"\"\"for tagging a book in a status\"\"\"\n\n    href: str\n    name: str = None\n    mediaType: str = None\n    id: str = None\n    attributedTo: str = None\n    type: str = \"Link\"\n\n    def serialize(self, **kwargs):\n        \"\"\"remove fields\"\"\"\n        omit = (\"id\", \"type\", \"@context\")\n        return super().serialize(omit=omit)\n\n\n@dataclass(init=False)\nclass Mention(Link):\n    \"\"\"a subtype of Link for mentioning an actor\"\"\"\n\n    type: str = \"Mention\"\n", "path": "bookwyrm/activitypub/base_activity.py"}], "after_files": [{"content": "\"\"\" basics for an activitypub serializer \"\"\"\nfrom dataclasses import dataclass, fields, MISSING\nfrom json import JSONEncoder\n\nfrom django.apps import apps\nfrom django.db import IntegrityError, transaction\n\nfrom bookwyrm.connectors import ConnectorException, get_data\nfrom bookwyrm.tasks import app\n\n\nclass ActivitySerializerError(ValueError):\n    \"\"\"routine problems serializing activitypub json\"\"\"\n\n\nclass ActivityEncoder(JSONEncoder):\n    \"\"\"used to convert an Activity object into json\"\"\"\n\n    def default(self, o):\n        return o.__dict__\n\n\n@dataclass\n# pylint: disable=invalid-name\nclass Signature:\n    \"\"\"public key block\"\"\"\n\n    creator: str\n    created: str\n    signatureValue: str\n    type: str = \"RsaSignature2017\"\n\n\ndef naive_parse(activity_objects, activity_json, serializer=None):\n    \"\"\"this navigates circular import issues by looking up models' serializers\"\"\"\n    if not serializer:\n        if activity_json.get(\"publicKeyPem\"):\n            # ugh\n            activity_json[\"type\"] = \"PublicKey\"\n\n        activity_type = activity_json.get(\"type\")\n        try:\n            serializer = activity_objects[activity_type]\n        except KeyError as err:\n            # we know this exists and that we can't handle it\n            if activity_type in [\"Question\"]:\n                return None\n            raise ActivitySerializerError(err)\n\n    return serializer(activity_objects=activity_objects, **activity_json)\n\n\n@dataclass(init=False)\nclass ActivityObject:\n    \"\"\"actor activitypub json\"\"\"\n\n    id: str\n    type: str\n\n    def __init__(self, activity_objects=None, **kwargs):\n        \"\"\"this lets you pass in an object with fields that aren't in the\n        dataclass, which it ignores. Any field in the dataclass is required or\n        has a default value\"\"\"\n        for field in fields(self):\n            try:\n                value = kwargs[field.name]\n                if value in (None, MISSING, {}):\n                    raise KeyError()\n                try:\n                    is_subclass = issubclass(field.type, ActivityObject)\n                except TypeError:\n                    is_subclass = False\n                # serialize a model obj\n                if hasattr(value, \"to_activity\"):\n                    value = value.to_activity()\n                # parse a dict into the appropriate activity\n                elif is_subclass and isinstance(value, dict):\n                    if activity_objects:\n                        value = naive_parse(activity_objects, value)\n                    else:\n                        value = naive_parse(\n                            activity_objects, value, serializer=field.type\n                        )\n\n            except KeyError:\n                if field.default == MISSING and field.default_factory == MISSING:\n                    raise ActivitySerializerError(\n                        f\"Missing required field: {field.name}\"\n                    )\n                value = field.default\n            setattr(self, field.name, value)\n\n    # pylint: disable=too-many-locals,too-many-branches,too-many-arguments\n    def to_model(\n        self, model=None, instance=None, allow_create=True, save=True, overwrite=True\n    ):\n        \"\"\"convert from an activity to a model instance\"\"\"\n        model = model or get_model_from_type(self.type)\n\n        # only reject statuses if we're potentially creating them\n        if (\n            allow_create\n            and hasattr(model, \"ignore_activity\")\n            and model.ignore_activity(self)\n        ):\n            return None\n\n        # check for an existing instance\n        instance = instance or model.find_existing(self.serialize())\n\n        if not instance and not allow_create:\n            # so that we don't create when we want to delete or update\n            return None\n        instance = instance or model()\n\n        # keep track of what we've changed\n        update_fields = []\n        # sets field on the model using the activity value\n        for field in instance.simple_fields:\n            try:\n                changed = field.set_field_from_activity(\n                    instance, self, overwrite=overwrite\n                )\n                if changed:\n                    update_fields.append(field.name)\n            except AttributeError as e:\n                raise ActivitySerializerError(e)\n\n        # image fields have to be set after other fields because they can save\n        # too early and jank up users\n        for field in instance.image_fields:\n            changed = field.set_field_from_activity(\n                instance, self, save=save, overwrite=overwrite\n            )\n            if changed:\n                update_fields.append(field.name)\n\n        if not save:\n            return instance\n\n        with transaction.atomic():\n            # can't force an update on fields unless the object already exists in the db\n            if not instance.id:\n                update_fields = None\n            # we can't set many to many and reverse fields on an unsaved object\n            try:\n                try:\n                    instance.save(broadcast=False, update_fields=update_fields)\n                except TypeError:\n                    instance.save(update_fields=update_fields)\n            except IntegrityError as e:\n                raise ActivitySerializerError(e)\n\n            # add many to many fields, which have to be set post-save\n            for field in instance.many_to_many_fields:\n                # mention books/users, for example\n                field.set_field_from_activity(instance, self)\n\n        # reversed relationships in the models\n        for (\n            model_field_name,\n            activity_field_name,\n        ) in instance.deserialize_reverse_fields:\n            # attachments on Status, for example\n            values = getattr(self, activity_field_name)\n            if values is None or values is MISSING:\n                continue\n\n            model_field = getattr(model, model_field_name)\n            # creating a Work, model_field is 'editions'\n            # creating a User, model field is 'key_pair'\n            related_model = model_field.field.model\n            related_field_name = model_field.field.name\n\n            for item in values:\n                set_related_field.delay(\n                    related_model.__name__,\n                    instance.__class__.__name__,\n                    related_field_name,\n                    instance.remote_id,\n                    item,\n                )\n        return instance\n\n    def serialize(self, **kwargs):\n        \"\"\"convert to dictionary with context attr\"\"\"\n        omit = kwargs.get(\"omit\", ())\n        data = self.__dict__.copy()\n        # recursively serialize\n        for (k, v) in data.items():\n            try:\n                if issubclass(type(v), ActivityObject):\n                    data[k] = v.serialize()\n            except TypeError:\n                pass\n        data = {k: v for (k, v) in data.items() if v is not None and k not in omit}\n        if \"@context\" not in omit:\n            data[\"@context\"] = \"https://www.w3.org/ns/activitystreams\"\n        return data\n\n\n@app.task(queue=\"medium_priority\")\n@transaction.atomic\ndef set_related_field(\n    model_name, origin_model_name, related_field_name, related_remote_id, data\n):\n    \"\"\"load reverse related fields (editions, attachments) without blocking\"\"\"\n    model = apps.get_model(f\"bookwyrm.{model_name}\", require_ready=True)\n    origin_model = apps.get_model(f\"bookwyrm.{origin_model_name}\", require_ready=True)\n\n    if isinstance(data, str):\n        existing = model.find_existing_by_remote_id(data)\n        if existing:\n            data = existing.to_activity()\n        else:\n            data = get_data(data)\n    activity = model.activity_serializer(**data)\n\n    # this must exist because it's the object that triggered this function\n    instance = origin_model.find_existing_by_remote_id(related_remote_id)\n    if not instance:\n        raise ValueError(f\"Invalid related remote id: {related_remote_id}\")\n\n    # set the origin's remote id on the activity so it will be there when\n    # the model instance is created\n    # edition.parentWork = instance, for example\n    model_field = getattr(model, related_field_name)\n    if hasattr(model_field, \"activitypub_field\"):\n        setattr(activity, getattr(model_field, \"activitypub_field\"), instance.remote_id)\n    item = activity.to_model(model=model)\n\n    # if the related field isn't serialized (attachments on Status), then\n    # we have to set it post-creation\n    if not hasattr(model_field, \"activitypub_field\"):\n        setattr(item, related_field_name, instance)\n        item.save()\n\n\ndef get_model_from_type(activity_type):\n    \"\"\"given the activity, what type of model\"\"\"\n    models = apps.get_models()\n    model = [\n        m\n        for m in models\n        if hasattr(m, \"activity_serializer\")\n        and hasattr(m.activity_serializer, \"type\")\n        and m.activity_serializer.type == activity_type\n    ]\n    if not model:\n        raise ActivitySerializerError(\n            f'No model found for activity type \"{activity_type}\"'\n        )\n    return model[0]\n\n\ndef resolve_remote_id(\n    remote_id, model=None, refresh=False, save=True, get_activity=False\n):\n    \"\"\"take a remote_id and return an instance, creating if necessary\"\"\"\n    if model:  # a bonus check we can do if we already know the model\n        if isinstance(model, str):\n            model = apps.get_model(f\"bookwyrm.{model}\", require_ready=True)\n        result = model.find_existing_by_remote_id(remote_id)\n        if result and not refresh:\n            return result if not get_activity else result.to_activity_dataclass()\n\n    # load the data and create the object\n    try:\n        data = get_data(remote_id)\n    except ConnectorException:\n        raise ActivitySerializerError(\n            f\"Could not connect to host for remote_id: {remote_id}\"\n        )\n    # determine the model implicitly, if not provided\n    # or if it's a model with subclasses like Status, check again\n    if not model or hasattr(model.objects, \"select_subclasses\"):\n        model = get_model_from_type(data.get(\"type\"))\n\n    # check for existing items with shared unique identifiers\n    result = model.find_existing(data)\n    if result and not refresh:\n        return result if not get_activity else result.to_activity_dataclass()\n\n    item = model.activity_serializer(**data)\n    if get_activity:\n        return item\n\n    # if we're refreshing, \"result\" will be set and we'll update it\n    return item.to_model(model=model, instance=result, save=save)\n\n\n@dataclass(init=False)\nclass Link(ActivityObject):\n    \"\"\"for tagging a book in a status\"\"\"\n\n    href: str\n    name: str = None\n    mediaType: str = None\n    id: str = None\n    attributedTo: str = None\n    availability: str = None\n    type: str = \"Link\"\n\n    def serialize(self, **kwargs):\n        \"\"\"remove fields\"\"\"\n        omit = (\"id\", \"type\", \"@context\")\n        return super().serialize(omit=omit)\n\n\n@dataclass(init=False)\nclass Mention(Link):\n    \"\"\"a subtype of Link for mentioning an actor\"\"\"\n\n    type: str = \"Mention\"\n", "path": "bookwyrm/activitypub/base_activity.py"}]}