From: Nutomic <me@nutomic.com>
Date: Fri, 6 May 2022 23:53:33 +0000 (+0000)
Subject: Federate with Peertube (#2244)
X-Git-Url: http://these/git/%22https:/image.com/static/%7Bicon?a=commitdiff_plain;h=7b86441bab1a60f48d7bd4ff1ac37d6788d3cbf9;p=lemmy.git

Federate with Peertube (#2244)
---

diff --git a/crates/apub/assets/peertube/activities/announce_video.json b/crates/apub/assets/peertube/activities/announce_video.json
new file mode 100644
index 00000000..28f06e8c
--- /dev/null
+++ b/crates/apub/assets/peertube/activities/announce_video.json
@@ -0,0 +1,107 @@
+{
+  "type": "Announce",
+  "id": "https://framatube.org/videos/watch/60c4bea4-6bb2-4fce-8d9f-8a522575419d/announces/395533",
+  "actor": "https://framatube.org/video-channels/joinpeertube",
+  "object": "https://framatube.org/videos/watch/60c4bea4-6bb2-4fce-8d9f-8a522575419d",
+  "to": [
+    "https://www.w3.org/ns/activitystreams#Public"
+  ],
+  "cc": [
+    "https://framatube.org/accounts/framasoft/followers"
+  ],
+  "@context": [
+    "https://www.w3.org/ns/activitystreams",
+    "https://w3id.org/security/v1",
+    {
+      "RsaSignature2017": "https://w3id.org/security#RsaSignature2017"
+    },
+    {
+      "pt": "https://joinpeertube.org/ns#",
+      "sc": "http://schema.org#",
+      "Hashtag": "as:Hashtag",
+      "uuid": "sc:identifier",
+      "category": "sc:category",
+      "licence": "sc:license",
+      "subtitleLanguage": "sc:subtitleLanguage",
+      "sensitive": "as:sensitive",
+      "language": "sc:inLanguage",
+      "isLiveBroadcast": "sc:isLiveBroadcast",
+      "liveSaveReplay": {
+        "@type": "sc:Boolean",
+        "@id": "pt:liveSaveReplay"
+      },
+      "permanentLive": {
+        "@type": "sc:Boolean",
+        "@id": "pt:permanentLive"
+      },
+      "Infohash": "pt:Infohash",
+      "Playlist": "pt:Playlist",
+      "PlaylistElement": "pt:PlaylistElement",
+      "originallyPublishedAt": "sc:datePublished",
+      "views": {
+        "@type": "sc:Number",
+        "@id": "pt:views"
+      },
+      "state": {
+        "@type": "sc:Number",
+        "@id": "pt:state"
+      },
+      "size": {
+        "@type": "sc:Number",
+        "@id": "pt:size"
+      },
+      "fps": {
+        "@type": "sc:Number",
+        "@id": "pt:fps"
+      },
+      "startTimestamp": {
+        "@type": "sc:Number",
+        "@id": "pt:startTimestamp"
+      },
+      "stopTimestamp": {
+        "@type": "sc:Number",
+        "@id": "pt:stopTimestamp"
+      },
+      "position": {
+        "@type": "sc:Number",
+        "@id": "pt:position"
+      },
+      "commentsEnabled": {
+        "@type": "sc:Boolean",
+        "@id": "pt:commentsEnabled"
+      },
+      "downloadEnabled": {
+        "@type": "sc:Boolean",
+        "@id": "pt:downloadEnabled"
+      },
+      "waitTranscoding": {
+        "@type": "sc:Boolean",
+        "@id": "pt:waitTranscoding"
+      },
+      "support": {
+        "@type": "sc:Text",
+        "@id": "pt:support"
+      },
+      "likes": {
+        "@id": "as:likes",
+        "@type": "@id"
+      },
+      "dislikes": {
+        "@id": "as:dislikes",
+        "@type": "@id"
+      },
+      "playlists": {
+        "@id": "pt:playlists",
+        "@type": "@id"
+      },
+      "shares": {
+        "@id": "as:shares",
+        "@type": "@id"
+      },
+      "comments": {
+        "@id": "as:comments",
+        "@type": "@id"
+      }
+    }
+  ]
+}
\ No newline at end of file
diff --git a/crates/apub/assets/peertube/objects/group.json b/crates/apub/assets/peertube/objects/group.json
new file mode 100644
index 00000000..cf4e216c
--- /dev/null
+++ b/crates/apub/assets/peertube/objects/group.json
@@ -0,0 +1,131 @@
+{
+  "type": "Group",
+  "id": "https://framatube.org/video-channels/joinpeertube",
+  "following": "https://framatube.org/video-channels/joinpeertube/following",
+  "followers": "https://framatube.org/video-channels/joinpeertube/followers",
+  "playlists": "https://framatube.org/video-channels/joinpeertube/playlists",
+  "inbox": "https://framatube.org/video-channels/joinpeertube/inbox",
+  "outbox": "https://framatube.org/video-channels/joinpeertube/outbox",
+  "preferredUsername": "joinpeertube",
+  "url": "https://framatube.org/video-channels/joinpeertube",
+  "name": "A propos de PeerTube",
+  "endpoints": {
+    "sharedInbox": "https://framatube.org/inbox"
+  },
+  "publicKey": {
+    "id": "https://framatube.org/video-channels/joinpeertube#main-key",
+    "owner": "https://framatube.org/video-channels/joinpeertube",
+    "publicKeyPem": "-----BEGIN PUBLIC KEY-----\nMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAsJCIZJga+4Kumb9Wrmpy\ntyV7kWdINImoXBiFkGG+6OHreHN2C3UPwTu9IkX/e20NaX6Ly6c0busieW7yh//q\nomHl2U8zz2Z5xQHUN/2ljQjUNO+89OV6cFIGyEvcwc6QhuqGvrcxonjrEkux7xSv\nxQM4kZ3YW1Sii4piFpGGIm1pcUkOxFab8PWVB5Hzpg/df2/XOmH8UECT5vaMRPE6\ns6hNiQNE34z9QmPiG6nUlaWb/WDcMYbma3sUVWW3DI008ukLlwLaLIm30ax8CEYt\nHEv2jOQb1E1sXtBPe1FI+dXRgTIk40KF50KLqcgwJH1y5ck7c8IEeooj+tYGVqPr\npQIDAQAB\n-----END PUBLIC KEY-----"
+  },
+  "published": "2021-08-09T14:26:09.514Z",
+  "icon": {
+    "type": "Image",
+    "mediaType": "image/png",
+    "height": 120,
+    "width": 120,
+    "url": "https://framatube.org/lazy-static/avatars/a2c2ff10-9da6-4c6c-9b25-2e557fa74b66.png"
+  },
+  "@context": [
+    "https://www.w3.org/ns/activitystreams",
+    "https://w3id.org/security/v1",
+    {
+      "RsaSignature2017": "https://w3id.org/security#RsaSignature2017"
+    },
+    {
+      "pt": "https://joinpeertube.org/ns#",
+      "sc": "http://schema.org#",
+      "Hashtag": "as:Hashtag",
+      "uuid": "sc:identifier",
+      "category": "sc:category",
+      "licence": "sc:license",
+      "subtitleLanguage": "sc:subtitleLanguage",
+      "sensitive": "as:sensitive",
+      "language": "sc:inLanguage",
+      "isLiveBroadcast": "sc:isLiveBroadcast",
+      "liveSaveReplay": {
+        "@type": "sc:Boolean",
+        "@id": "pt:liveSaveReplay"
+      },
+      "permanentLive": {
+        "@type": "sc:Boolean",
+        "@id": "pt:permanentLive"
+      },
+      "Infohash": "pt:Infohash",
+      "Playlist": "pt:Playlist",
+      "PlaylistElement": "pt:PlaylistElement",
+      "originallyPublishedAt": "sc:datePublished",
+      "views": {
+        "@type": "sc:Number",
+        "@id": "pt:views"
+      },
+      "state": {
+        "@type": "sc:Number",
+        "@id": "pt:state"
+      },
+      "size": {
+        "@type": "sc:Number",
+        "@id": "pt:size"
+      },
+      "fps": {
+        "@type": "sc:Number",
+        "@id": "pt:fps"
+      },
+      "startTimestamp": {
+        "@type": "sc:Number",
+        "@id": "pt:startTimestamp"
+      },
+      "stopTimestamp": {
+        "@type": "sc:Number",
+        "@id": "pt:stopTimestamp"
+      },
+      "position": {
+        "@type": "sc:Number",
+        "@id": "pt:position"
+      },
+      "commentsEnabled": {
+        "@type": "sc:Boolean",
+        "@id": "pt:commentsEnabled"
+      },
+      "downloadEnabled": {
+        "@type": "sc:Boolean",
+        "@id": "pt:downloadEnabled"
+      },
+      "waitTranscoding": {
+        "@type": "sc:Boolean",
+        "@id": "pt:waitTranscoding"
+      },
+      "support": {
+        "@type": "sc:Text",
+        "@id": "pt:support"
+      },
+      "likes": {
+        "@id": "as:likes",
+        "@type": "@id"
+      },
+      "dislikes": {
+        "@id": "as:dislikes",
+        "@type": "@id"
+      },
+      "playlists": {
+        "@id": "pt:playlists",
+        "@type": "@id"
+      },
+      "shares": {
+        "@id": "as:shares",
+        "@type": "@id"
+      },
+      "comments": {
+        "@id": "as:comments",
+        "@type": "@id"
+      }
+    }
+  ],
+  "summary": "Un logiciel libre pour reprendre le contrôle de vos vidéos",
+  "support": null,
+  "attributedTo": [
+    {
+      "type": "Person",
+      "id": "https://framatube.org/accounts/framasoft"
+    }
+  ]
+}
diff --git a/crates/apub/assets/peertube/objects/note.json b/crates/apub/assets/peertube/objects/note.json
new file mode 100644
index 00000000..8df8d586
--- /dev/null
+++ b/crates/apub/assets/peertube/objects/note.json
@@ -0,0 +1,119 @@
+{
+  "type": "Note",
+  "id": "https://video.antopie.org/videos/watch/4294a720-f263-4ea4-9392-cf9cea4d5277/comments/200873",
+  "content": "@af2@bae.st idk",
+  "mediaType": "text/markdown",
+  "inReplyTo": "https://bae.st/objects/87c1cbf5-542a-491d-af57-0414c8648381",
+  "updated": "2022-04-29T07:52:32.555Z",
+  "published": "2022-04-29T07:52:32.548Z",
+  "url": "https://video.antopie.org/videos/watch/4294a720-f263-4ea4-9392-cf9cea4d5277/comments/200873",
+  "attributedTo": "https://video.antopie.org/accounts/yoge6785555",
+  "tag": [
+    {
+      "type": "Mention",
+      "href": "https://bae.st/users/af2",
+      "name": "@af2@bae.st"
+    }
+  ],
+  "to": [
+    "https://www.w3.org/ns/activitystreams#Public"
+  ],
+  "cc": [
+    "https://video.antopie.org/accounts/yoge6785555/followers"
+  ],
+  "@context": [
+    "https://www.w3.org/ns/activitystreams",
+    "https://w3id.org/security/v1",
+    {
+      "RsaSignature2017": "https://w3id.org/security#RsaSignature2017"
+    },
+    {
+      "pt": "https://joinpeertube.org/ns#",
+      "sc": "http://schema.org#",
+      "Hashtag": "as:Hashtag",
+      "uuid": "sc:identifier",
+      "category": "sc:category",
+      "licence": "sc:license",
+      "subtitleLanguage": "sc:subtitleLanguage",
+      "sensitive": "as:sensitive",
+      "language": "sc:inLanguage",
+      "isLiveBroadcast": "sc:isLiveBroadcast",
+      "liveSaveReplay": {
+        "@type": "sc:Boolean",
+        "@id": "pt:liveSaveReplay"
+      },
+      "permanentLive": {
+        "@type": "sc:Boolean",
+        "@id": "pt:permanentLive"
+      },
+      "Infohash": "pt:Infohash",
+      "Playlist": "pt:Playlist",
+      "PlaylistElement": "pt:PlaylistElement",
+      "originallyPublishedAt": "sc:datePublished",
+      "views": {
+        "@type": "sc:Number",
+        "@id": "pt:views"
+      },
+      "state": {
+        "@type": "sc:Number",
+        "@id": "pt:state"
+      },
+      "size": {
+        "@type": "sc:Number",
+        "@id": "pt:size"
+      },
+      "fps": {
+        "@type": "sc:Number",
+        "@id": "pt:fps"
+      },
+      "startTimestamp": {
+        "@type": "sc:Number",
+        "@id": "pt:startTimestamp"
+      },
+      "stopTimestamp": {
+        "@type": "sc:Number",
+        "@id": "pt:stopTimestamp"
+      },
+      "position": {
+        "@type": "sc:Number",
+        "@id": "pt:position"
+      },
+      "commentsEnabled": {
+        "@type": "sc:Boolean",
+        "@id": "pt:commentsEnabled"
+      },
+      "downloadEnabled": {
+        "@type": "sc:Boolean",
+        "@id": "pt:downloadEnabled"
+      },
+      "waitTranscoding": {
+        "@type": "sc:Boolean",
+        "@id": "pt:waitTranscoding"
+      },
+      "support": {
+        "@type": "sc:Text",
+        "@id": "pt:support"
+      },
+      "likes": {
+        "@id": "as:likes",
+        "@type": "@id"
+      },
+      "dislikes": {
+        "@id": "as:dislikes",
+        "@type": "@id"
+      },
+      "playlists": {
+        "@id": "pt:playlists",
+        "@type": "@id"
+      },
+      "shares": {
+        "@id": "as:shares",
+        "@type": "@id"
+      },
+      "comments": {
+        "@id": "as:comments",
+        "@type": "@id"
+      }
+    }
+  ]
+}
diff --git a/crates/apub/assets/peertube/objects/person.json b/crates/apub/assets/peertube/objects/person.json
new file mode 100644
index 00000000..871e88e1
--- /dev/null
+++ b/crates/apub/assets/peertube/objects/person.json
@@ -0,0 +1,124 @@
+{
+  "type": "Person",
+  "id": "https://framatube.org/accounts/framasoft",
+  "following": "https://framatube.org/accounts/framasoft/following",
+  "followers": "https://framatube.org/accounts/framasoft/followers",
+  "playlists": "https://framatube.org/accounts/framasoft/playlists",
+  "inbox": "https://framatube.org/accounts/framasoft/inbox",
+  "outbox": "https://framatube.org/accounts/framasoft/outbox",
+  "preferredUsername": "framasoft",
+  "url": "https://framatube.org/accounts/framasoft",
+  "name": "Framasoft",
+  "endpoints": {
+    "sharedInbox": "https://framatube.org/inbox"
+  },
+  "publicKey": {
+    "id": "https://framatube.org/accounts/framasoft#main-key",
+    "owner": "https://framatube.org/accounts/framasoft",
+    "publicKeyPem": "-----BEGIN PUBLIC KEY-----\nMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAuRh3frgIg866D0y0FThp\nSUkJImMcHGkUvpYQYv2iUgarZZtEbwT8PfQf0bJazy+cP8KqQmMDf5PBhT7dfdny\nf/GKGMw9Olc+QISeKDj3sqZ3Csrm4KV4avMGCfth6eSU7LozojeSGCXdUFz/8UgE\nfhV4mJjEX/FbwRYoKlagv5rY9mkX5XomzZU+z9j6ZVXyofwOwJvmI1hq0SYDv2bc\neB/RgIh/H0nyMtF8o+0CT42FNEET9j9m1BKOBtPzwZHmitKRkEmui5cK256s1laB\nT61KHpcD9gQKkQ+I3sFEzCBUJYfVo6fUe+GehBZuAfq4qDhd15SfE4K9veDscDFI\nTwIDAQAB\n-----END PUBLIC KEY-----"
+  },
+  "published": "2018-03-01T15:16:17.118Z",
+  "icon": {
+    "type": "Image",
+    "mediaType": "image/png",
+    "height": null,
+    "width": null,
+    "url": "https://framatube.org/lazy-static/avatars/f73876f5-1d45-4f8a-942a-d3d5d5ac5dc1.png"
+  },
+  "@context": [
+    "https://www.w3.org/ns/activitystreams",
+    "https://w3id.org/security/v1",
+    {
+      "RsaSignature2017": "https://w3id.org/security#RsaSignature2017"
+    },
+    {
+      "pt": "https://joinpeertube.org/ns#",
+      "sc": "http://schema.org#",
+      "Hashtag": "as:Hashtag",
+      "uuid": "sc:identifier",
+      "category": "sc:category",
+      "licence": "sc:license",
+      "subtitleLanguage": "sc:subtitleLanguage",
+      "sensitive": "as:sensitive",
+      "language": "sc:inLanguage",
+      "isLiveBroadcast": "sc:isLiveBroadcast",
+      "liveSaveReplay": {
+        "@type": "sc:Boolean",
+        "@id": "pt:liveSaveReplay"
+      },
+      "permanentLive": {
+        "@type": "sc:Boolean",
+        "@id": "pt:permanentLive"
+      },
+      "Infohash": "pt:Infohash",
+      "Playlist": "pt:Playlist",
+      "PlaylistElement": "pt:PlaylistElement",
+      "originallyPublishedAt": "sc:datePublished",
+      "views": {
+        "@type": "sc:Number",
+        "@id": "pt:views"
+      },
+      "state": {
+        "@type": "sc:Number",
+        "@id": "pt:state"
+      },
+      "size": {
+        "@type": "sc:Number",
+        "@id": "pt:size"
+      },
+      "fps": {
+        "@type": "sc:Number",
+        "@id": "pt:fps"
+      },
+      "startTimestamp": {
+        "@type": "sc:Number",
+        "@id": "pt:startTimestamp"
+      },
+      "stopTimestamp": {
+        "@type": "sc:Number",
+        "@id": "pt:stopTimestamp"
+      },
+      "position": {
+        "@type": "sc:Number",
+        "@id": "pt:position"
+      },
+      "commentsEnabled": {
+        "@type": "sc:Boolean",
+        "@id": "pt:commentsEnabled"
+      },
+      "downloadEnabled": {
+        "@type": "sc:Boolean",
+        "@id": "pt:downloadEnabled"
+      },
+      "waitTranscoding": {
+        "@type": "sc:Boolean",
+        "@id": "pt:waitTranscoding"
+      },
+      "support": {
+        "@type": "sc:Text",
+        "@id": "pt:support"
+      },
+      "likes": {
+        "@id": "as:likes",
+        "@type": "@id"
+      },
+      "dislikes": {
+        "@id": "as:dislikes",
+        "@type": "@id"
+      },
+      "playlists": {
+        "@id": "pt:playlists",
+        "@type": "@id"
+      },
+      "shares": {
+        "@id": "as:shares",
+        "@type": "@id"
+      },
+      "comments": {
+        "@id": "as:comments",
+        "@type": "@id"
+      }
+    }
+  ],
+  "summary": null
+}
diff --git a/crates/apub/assets/peertube/objects/video.json b/crates/apub/assets/peertube/objects/video.json
new file mode 100644
index 00000000..4325c8fa
--- /dev/null
+++ b/crates/apub/assets/peertube/objects/video.json
@@ -0,0 +1,493 @@
+{
+  "type": "Video",
+  "id": "https://framatube.org/videos/watch/4294a720-f263-4ea4-9392-cf9cea4d5277",
+  "name": "What is the Fediverse?",
+  "duration": "PT98S",
+  "uuid": "4294a720-f263-4ea4-9392-cf9cea4d5277",
+  "tag": [
+    {
+      "type": "Hashtag",
+      "name": "fediverse"
+    },
+    {
+      "type": "Hashtag",
+      "name": "framasoft"
+    },
+    {
+      "type": "Hashtag",
+      "name": "Mastodon"
+    },
+    {
+      "type": "Hashtag",
+      "name": "PeerTube "
+    }
+  ],
+  "category": {
+    "identifier": "15",
+    "name": "Science & Technology"
+  },
+  "licence": {
+    "identifier": "2",
+    "name": "Attribution - Share Alike"
+  },
+  "language": {
+    "identifier": "en",
+    "name": "English"
+  },
+  "views": 4805,
+  "sensitive": false,
+  "waitTranscoding": true,
+  "isLiveBroadcast": false,
+  "liveSaveReplay": null,
+  "permanentLive": null,
+  "state": 1,
+  "commentsEnabled": true,
+  "downloadEnabled": true,
+  "published": "2022-04-28T11:51:16.293Z",
+  "originallyPublishedAt": null,
+  "updated": "2022-05-03T11:39:02.489Z",
+  "mediaType": "text/markdown",
+  "content": "Help us translate the subtitles [on our translation tool](https://weblate.framasoft.org/projects/what-is-the-fediverse-video/subtitles/).\r\n\r\n**Animation Produced by** [LILA](https://libreart.info/) - [ZeMarmot Team](https://film.zemarmot.net/)\r\n**Direction & Animation** by Aryeom\r\n**Script & Technology** by Jehan\r\n**Voice by** Paul Peterson\r\n**Licence**: [CC-By-SA 4.0](https://creativecommons.org/licenses/by-sa/4.0/)\r\n\r\n**Sponsored by** [Framasoft](https://framasoft.org/)\r\n\r\n**Sound by** ORL - [AMMD](https://ammd.net/)\r\n\r\n**Music**: \"Dolling\" by CyberSDF - [CC-BY 4.0](https://creativecommons.org/licenses/by/4.0/)",
+  "support": null,
+  "subtitleLanguage": [
+    {
+      "identifier": "ca",
+      "name": "Catalan",
+      "url": "https://framatube.org/lazy-static/video-captions/6f8aedd2-c61b-47f6-a2c9-75b15af24d14-ca.vtt"
+    },
+    {
+      "identifier": "en",
+      "name": "English",
+      "url": "https://framatube.org/lazy-static/video-captions/2f199e59-5cf8-4529-a033-9d6dd4a858ca-en.vtt"
+    },
+    {
+      "identifier": "es",
+      "name": "Spanish",
+      "url": "https://framatube.org/lazy-static/video-captions/3f74c16b-925f-45e1-8388-e358428c2436-es.vtt"
+    },
+    {
+      "identifier": "eu",
+      "name": "Basque",
+      "url": "https://framatube.org/lazy-static/video-captions/c4c88e7e-b9d4-4192-bcf2-caf025ddc9fd-eu.vtt"
+    },
+    {
+      "identifier": "fr",
+      "name": "French",
+      "url": "https://framatube.org/lazy-static/video-captions/c18906e3-6257-43e7-90e4-fa2c8ded258b-fr.vtt"
+    },
+    {
+      "identifier": "hu",
+      "name": "Hungarian",
+      "url": "https://framatube.org/lazy-static/video-captions/0a8a295d-a288-404b-b7b3-a2272bc2a6fb-hu.vtt"
+    },
+    {
+      "identifier": "it",
+      "name": "Italian",
+      "url": "https://framatube.org/lazy-static/video-captions/cf857bd9-8b04-4018-af9a-23fa1ff7662d-it.vtt"
+    },
+    {
+      "identifier": "nb",
+      "name": "Norwegian Bokmål",
+      "url": "https://framatube.org/lazy-static/video-captions/12e3a0e9-a29e-4b06-8538-91bed2a11242-nb.vtt"
+    },
+    {
+      "identifier": "oc",
+      "name": "Occitan",
+      "url": "https://framatube.org/lazy-static/video-captions/d841af30-97bf-4a0c-b1f9-e163ba77f23f-oc.vtt"
+    },
+    {
+      "identifier": "sh",
+      "name": "Serbo-Croatian",
+      "url": "https://framatube.org/lazy-static/video-captions/7afe4dae-745f-4769-9f17-9c3a079235cf-sh.vtt"
+    },
+    {
+      "identifier": "tr",
+      "name": "Turkish",
+      "url": "https://framatube.org/lazy-static/video-captions/1b2ea189-760c-4a3e-98d3-16f596c151f0-tr.vtt"
+    },
+    {
+      "identifier": "vi",
+      "name": "Vietnamese",
+      "url": "https://framatube.org/lazy-static/video-captions/552b4086-54ab-4eb3-a8b3-7611a2175e77-vi.vtt"
+    }
+  ],
+  "icon": [
+    {
+      "type": "Image",
+      "url": "https://framatube.org/static/thumbnails/1f9eb76e-c089-4bdd-af14-602935a6db72.jpg",
+      "mediaType": "image/jpeg",
+      "width": 280,
+      "height": 157
+    },
+    {
+      "type": "Image",
+      "url": "https://framatube.org/lazy-static/previews/8f89d4d8-696f-4512-9a1a-72f1d12caede.jpg",
+      "mediaType": "image/jpeg",
+      "width": 850,
+      "height": 480
+    }
+  ],
+  "url": [
+    {
+      "type": "Link",
+      "mediaType": "text/html",
+      "href": "https://framatube.org/videos/watch/4294a720-f263-4ea4-9392-cf9cea4d5277"
+    },
+    {
+      "type": "Link",
+      "mediaType": "application/x-mpegURL",
+      "href": "https://framatube.org/static/streaming-playlists/hls/4294a720-f263-4ea4-9392-cf9cea4d5277/adc259cb-06f7-496c-8a50-599e58358b29-master.m3u8",
+      "tag": [
+        {
+          "type": "Infohash",
+          "name": "caf7178ddd2013e28c9fbcbb7be28df25d03a023"
+        },
+        {
+          "type": "Infohash",
+          "name": "cc18bb140f51f64090ba41c951fba85705cafa38"
+        },
+        {
+          "type": "Infohash",
+          "name": "595513d823a1aecc18abacac94a1ebb0c31ec009"
+        },
+        {
+          "type": "Infohash",
+          "name": "6ae0ce749a57d0f8ff70286878ea7661f85eebf7"
+        },
+        {
+          "type": "Infohash",
+          "name": "4eb799f42d461929ed8dd4befae274c9a4404b99"
+        },
+        {
+          "type": "Infohash",
+          "name": "b48d1ea795657668783544fd1c9baf637198a323"
+        },
+        {
+          "type": "Link",
+          "name": "sha256",
+          "mediaType": "application/json",
+          "href": "https://framatube.org/static/streaming-playlists/hls/4294a720-f263-4ea4-9392-cf9cea4d5277/b414eda3-c8af-4271-8dde-253db28aacd1-segments-sha256.json"
+        },
+        {
+          "type": "Link",
+          "mediaType": "video/mp4",
+          "href": "https://framatube.org/static/streaming-playlists/hls/4294a720-f263-4ea4-9392-cf9cea4d5277/64147344-1957-480d-9106-59dd7bbf5661-1080-fragmented.mp4",
+          "height": 1080,
+          "size": 14653991,
+          "fps": 24
+        },
+        {
+          "type": "Link",
+          "rel": [
+            "metadata",
+            "video/mp4"
+          ],
+          "mediaType": "application/json",
+          "href": "https://framatube.org/api/v1/videos/4294a720-f263-4ea4-9392-cf9cea4d5277/metadata/1421492",
+          "height": 1080,
+          "fps": 24
+        },
+        {
+          "type": "Link",
+          "mediaType": "application/x-bittorrent",
+          "href": "https://framatube.org/lazy-static/torrents/83fa27e3-aba7-4e01-9e66-931086374176-1080-hls.torrent",
+          "height": 1080
+        },
+        {
+          "type": "Link",
+          "mediaType": "application/x-bittorrent;x-scheme-handler/magnet",
+          "href": "magnet:?xs=https%3A%2F%2Fframatube.org%2Flazy-static%2Ftorrents%2F83fa27e3-aba7-4e01-9e66-931086374176-1080-hls.torrent&xt=urn:btih:5651916e4301c812412f51381c5af0c1f627bfcb&dn=What+is+the+Fediverse%3F&tr=https%3A%2F%2Fframatube.org%2Ftracker%2Fannounce&tr=wss%3A%2F%2Fframatube.org%3A443%2Ftracker%2Fsocket&ws=https%3A%2F%2Fframatube.org%2Fstatic%2Fstreaming-playlists%2Fhls%2F4294a720-f263-4ea4-9392-cf9cea4d5277%2F64147344-1957-480d-9106-59dd7bbf5661-1080-fragmented.mp4",
+          "height": 1080
+        },
+        {
+          "type": "Link",
+          "mediaType": "video/mp4",
+          "href": "https://framatube.org/static/streaming-playlists/hls/4294a720-f263-4ea4-9392-cf9cea4d5277/0efaeae5-7468-4c45-ade5-d3b6c732621f-720-fragmented.mp4",
+          "height": 720,
+          "size": 9939723,
+          "fps": 24
+        },
+        {
+          "type": "Link",
+          "rel": [
+            "metadata",
+            "video/mp4"
+          ],
+          "mediaType": "application/json",
+          "href": "https://framatube.org/api/v1/videos/4294a720-f263-4ea4-9392-cf9cea4d5277/metadata/1421496",
+          "height": 720,
+          "fps": 24
+        },
+        {
+          "type": "Link",
+          "mediaType": "application/x-bittorrent",
+          "href": "https://framatube.org/lazy-static/torrents/b325c824-c052-46e2-9b46-887595055521-720-hls.torrent",
+          "height": 720
+        },
+        {
+          "type": "Link",
+          "mediaType": "application/x-bittorrent;x-scheme-handler/magnet",
+          "href": "magnet:?xs=https%3A%2F%2Fframatube.org%2Flazy-static%2Ftorrents%2Fb325c824-c052-46e2-9b46-887595055521-720-hls.torrent&xt=urn:btih:b5a1db245fe156edab7f1981693178dcd47075d2&dn=What+is+the+Fediverse%3F&tr=https%3A%2F%2Fframatube.org%2Ftracker%2Fannounce&tr=wss%3A%2F%2Fframatube.org%3A443%2Ftracker%2Fsocket&ws=https%3A%2F%2Fframatube.org%2Fstatic%2Fstreaming-playlists%2Fhls%2F4294a720-f263-4ea4-9392-cf9cea4d5277%2F0efaeae5-7468-4c45-ade5-d3b6c732621f-720-fragmented.mp4",
+          "height": 720
+        },
+        {
+          "type": "Link",
+          "mediaType": "video/mp4",
+          "href": "https://framatube.org/static/streaming-playlists/hls/4294a720-f263-4ea4-9392-cf9cea4d5277/201f9772-4971-4bc3-8356-9b85b405ae5d-480-fragmented.mp4",
+          "height": 480,
+          "size": 7398758,
+          "fps": 24
+        },
+        {
+          "type": "Link",
+          "rel": [
+            "metadata",
+            "video/mp4"
+          ],
+          "mediaType": "application/json",
+          "href": "https://framatube.org/api/v1/videos/4294a720-f263-4ea4-9392-cf9cea4d5277/metadata/1421494",
+          "height": 480,
+          "fps": 24
+        },
+        {
+          "type": "Link",
+          "mediaType": "application/x-bittorrent",
+          "href": "https://framatube.org/lazy-static/torrents/bd99f84e-e9bc-4d36-bea6-6f06000f87c5-480-hls.torrent",
+          "height": 480
+        },
+        {
+          "type": "Link",
+          "mediaType": "application/x-bittorrent;x-scheme-handler/magnet",
+          "href": "magnet:?xs=https%3A%2F%2Fframatube.org%2Flazy-static%2Ftorrents%2Fbd99f84e-e9bc-4d36-bea6-6f06000f87c5-480-hls.torrent&xt=urn:btih:6cbe09b50cf7788923a2ec4852a3b2bfd1cd1907&dn=What+is+the+Fediverse%3F&tr=https%3A%2F%2Fframatube.org%2Ftracker%2Fannounce&tr=wss%3A%2F%2Fframatube.org%3A443%2Ftracker%2Fsocket&ws=https%3A%2F%2Fframatube.org%2Fstatic%2Fstreaming-playlists%2Fhls%2F4294a720-f263-4ea4-9392-cf9cea4d5277%2F201f9772-4971-4bc3-8356-9b85b405ae5d-480-fragmented.mp4",
+          "height": 480
+        },
+        {
+          "type": "Link",
+          "mediaType": "video/mp4",
+          "href": "https://framatube.org/static/streaming-playlists/hls/4294a720-f263-4ea4-9392-cf9cea4d5277/b2313ae6-da36-4fe3-bec5-aa352824a38a-360-fragmented.mp4",
+          "height": 360,
+          "size": 6133890,
+          "fps": 24
+        },
+        {
+          "type": "Link",
+          "rel": [
+            "metadata",
+            "video/mp4"
+          ],
+          "mediaType": "application/json",
+          "href": "https://framatube.org/api/v1/videos/4294a720-f263-4ea4-9392-cf9cea4d5277/metadata/1421495",
+          "height": 360,
+          "fps": 24
+        },
+        {
+          "type": "Link",
+          "mediaType": "application/x-bittorrent",
+          "href": "https://framatube.org/lazy-static/torrents/b939430a-fdfd-4da7-a030-759ecafa6ac7-360-hls.torrent",
+          "height": 360
+        },
+        {
+          "type": "Link",
+          "mediaType": "application/x-bittorrent;x-scheme-handler/magnet",
+          "href": "magnet:?xs=https%3A%2F%2Fframatube.org%2Flazy-static%2Ftorrents%2Fb939430a-fdfd-4da7-a030-759ecafa6ac7-360-hls.torrent&xt=urn:btih:16693f14ad9e53fc41d335e3fa409c2f943d7b68&dn=What+is+the+Fediverse%3F&tr=https%3A%2F%2Fframatube.org%2Ftracker%2Fannounce&tr=wss%3A%2F%2Fframatube.org%3A443%2Ftracker%2Fsocket&ws=https%3A%2F%2Fframatube.org%2Fstatic%2Fstreaming-playlists%2Fhls%2F4294a720-f263-4ea4-9392-cf9cea4d5277%2Fb2313ae6-da36-4fe3-bec5-aa352824a38a-360-fragmented.mp4",
+          "height": 360
+        },
+        {
+          "type": "Link",
+          "mediaType": "video/mp4",
+          "href": "https://framatube.org/static/streaming-playlists/hls/4294a720-f263-4ea4-9392-cf9cea4d5277/06a866f2-0527-4d68-93b7-c656d7374e86-240-fragmented.mp4",
+          "height": 240,
+          "size": 4861464,
+          "fps": 24
+        },
+        {
+          "type": "Link",
+          "rel": [
+            "metadata",
+            "video/mp4"
+          ],
+          "mediaType": "application/json",
+          "href": "https://framatube.org/api/v1/videos/4294a720-f263-4ea4-9392-cf9cea4d5277/metadata/1421497",
+          "height": 240,
+          "fps": 24
+        },
+        {
+          "type": "Link",
+          "mediaType": "application/x-bittorrent",
+          "href": "https://framatube.org/lazy-static/torrents/072001ee-18ad-4859-af10-9d7bf12d640c-240-hls.torrent",
+          "height": 240
+        },
+        {
+          "type": "Link",
+          "mediaType": "application/x-bittorrent;x-scheme-handler/magnet",
+          "href": "magnet:?xs=https%3A%2F%2Fframatube.org%2Flazy-static%2Ftorrents%2F072001ee-18ad-4859-af10-9d7bf12d640c-240-hls.torrent&xt=urn:btih:b823f54d8cd73f9d7a55266ce683f43bf772d26a&dn=What+is+the+Fediverse%3F&tr=https%3A%2F%2Fframatube.org%2Ftracker%2Fannounce&tr=wss%3A%2F%2Fframatube.org%3A443%2Ftracker%2Fsocket&ws=https%3A%2F%2Fframatube.org%2Fstatic%2Fstreaming-playlists%2Fhls%2F4294a720-f263-4ea4-9392-cf9cea4d5277%2F06a866f2-0527-4d68-93b7-c656d7374e86-240-fragmented.mp4",
+          "height": 240
+        },
+        {
+          "type": "Link",
+          "mediaType": "video/mp4",
+          "href": "https://framatube.org/static/streaming-playlists/hls/4294a720-f263-4ea4-9392-cf9cea4d5277/f8a1caed-057f-4700-a28e-004efc158b15-0-fragmented.mp4",
+          "height": 0,
+          "size": 3141179,
+          "fps": 0
+        },
+        {
+          "type": "Link",
+          "rel": [
+            "metadata",
+            "video/mp4"
+          ],
+          "mediaType": "application/json",
+          "href": "https://framatube.org/api/v1/videos/4294a720-f263-4ea4-9392-cf9cea4d5277/metadata/1421493",
+          "height": 0,
+          "fps": 0
+        },
+        {
+          "type": "Link",
+          "mediaType": "application/x-bittorrent",
+          "href": "https://framatube.org/lazy-static/torrents/77cb6940-7e90-48d1-a391-bfa463b9600c-0-hls.torrent",
+          "height": 0
+        },
+        {
+          "type": "Link",
+          "mediaType": "application/x-bittorrent;x-scheme-handler/magnet",
+          "href": "magnet:?xs=https%3A%2F%2Fframatube.org%2Flazy-static%2Ftorrents%2F77cb6940-7e90-48d1-a391-bfa463b9600c-0-hls.torrent&xt=urn:btih:9bc7717ed01869507041e31a7e65baffa78ba651&dn=What+is+the+Fediverse%3F&tr=https%3A%2F%2Fframatube.org%2Ftracker%2Fannounce&tr=wss%3A%2F%2Fframatube.org%3A443%2Ftracker%2Fsocket&ws=https%3A%2F%2Fframatube.org%2Fstatic%2Fstreaming-playlists%2Fhls%2F4294a720-f263-4ea4-9392-cf9cea4d5277%2Ff8a1caed-057f-4700-a28e-004efc158b15-0-fragmented.mp4",
+          "height": 0
+        }
+      ]
+    },
+    {
+      "type": "Link",
+      "name": "tracker-http",
+      "rel": [
+        "tracker",
+        "http"
+      ],
+      "href": "https://framatube.org/tracker/announce"
+    },
+    {
+      "type": "Link",
+      "name": "tracker-websocket",
+      "rel": [
+        "tracker",
+        "websocket"
+      ],
+      "href": "wss://framatube.org:443/tracker/socket"
+    }
+  ],
+  "likes": "https://framatube.org/videos/watch/4294a720-f263-4ea4-9392-cf9cea4d5277/likes",
+  "dislikes": "https://framatube.org/videos/watch/4294a720-f263-4ea4-9392-cf9cea4d5277/dislikes",
+  "shares": "https://framatube.org/videos/watch/4294a720-f263-4ea4-9392-cf9cea4d5277/announces",
+  "comments": "https://framatube.org/videos/watch/4294a720-f263-4ea4-9392-cf9cea4d5277/comments",
+  "attributedTo": [
+    {
+      "type": "Person",
+      "id": "https://framatube.org/accounts/framasoft"
+    },
+    {
+      "type": "Group",
+      "id": "https://framatube.org/video-channels/joinpeertube"
+    }
+  ],
+  "to": [
+    "https://www.w3.org/ns/activitystreams#Public"
+  ],
+  "cc": [
+    "https://framatube.org/accounts/framasoft/followers"
+  ],
+  "@context": [
+    "https://www.w3.org/ns/activitystreams",
+    "https://w3id.org/security/v1",
+    {
+      "RsaSignature2017": "https://w3id.org/security#RsaSignature2017"
+    },
+    {
+      "pt": "https://joinpeertube.org/ns#",
+      "sc": "http://schema.org#",
+      "Hashtag": "as:Hashtag",
+      "uuid": "sc:identifier",
+      "category": "sc:category",
+      "licence": "sc:license",
+      "subtitleLanguage": "sc:subtitleLanguage",
+      "sensitive": "as:sensitive",
+      "language": "sc:inLanguage",
+      "isLiveBroadcast": "sc:isLiveBroadcast",
+      "liveSaveReplay": {
+        "@type": "sc:Boolean",
+        "@id": "pt:liveSaveReplay"
+      },
+      "permanentLive": {
+        "@type": "sc:Boolean",
+        "@id": "pt:permanentLive"
+      },
+      "Infohash": "pt:Infohash",
+      "Playlist": "pt:Playlist",
+      "PlaylistElement": "pt:PlaylistElement",
+      "originallyPublishedAt": "sc:datePublished",
+      "views": {
+        "@type": "sc:Number",
+        "@id": "pt:views"
+      },
+      "state": {
+        "@type": "sc:Number",
+        "@id": "pt:state"
+      },
+      "size": {
+        "@type": "sc:Number",
+        "@id": "pt:size"
+      },
+      "fps": {
+        "@type": "sc:Number",
+        "@id": "pt:fps"
+      },
+      "startTimestamp": {
+        "@type": "sc:Number",
+        "@id": "pt:startTimestamp"
+      },
+      "stopTimestamp": {
+        "@type": "sc:Number",
+        "@id": "pt:stopTimestamp"
+      },
+      "position": {
+        "@type": "sc:Number",
+        "@id": "pt:position"
+      },
+      "commentsEnabled": {
+        "@type": "sc:Boolean",
+        "@id": "pt:commentsEnabled"
+      },
+      "downloadEnabled": {
+        "@type": "sc:Boolean",
+        "@id": "pt:downloadEnabled"
+      },
+      "waitTranscoding": {
+        "@type": "sc:Boolean",
+        "@id": "pt:waitTranscoding"
+      },
+      "support": {
+        "@type": "sc:Text",
+        "@id": "pt:support"
+      },
+      "likes": {
+        "@id": "as:likes",
+        "@type": "@id"
+      },
+      "dislikes": {
+        "@id": "as:dislikes",
+        "@type": "@id"
+      },
+      "playlists": {
+        "@id": "pt:playlists",
+        "@type": "@id"
+      },
+      "shares": {
+        "@id": "as:shares",
+        "@type": "@id"
+      },
+      "comments": {
+        "@id": "as:comments",
+        "@type": "@id"
+      }
+    }
+  ]
+}
diff --git a/crates/apub/src/activities/community/announce.rs b/crates/apub/src/activities/community/announce.rs
index 5bfcb931..a6612ca1 100644
--- a/crates/apub/src/activities/community/announce.rs
+++ b/crates/apub/src/activities/community/announce.rs
@@ -4,7 +4,10 @@ use crate::{
   http::ActivityCommonFields,
   insert_activity,
   objects::community::ApubCommunity,
-  protocol::activities::{community::announce::AnnounceActivity, CreateOrUpdateType},
+  protocol::{
+    activities::{community::announce::AnnounceActivity, CreateOrUpdateType},
+    IdOrNestedObject,
+  },
 };
 use activitystreams_kinds::{activity::AnnounceType, public};
 use lemmy_apub_lib::{
@@ -34,7 +37,7 @@ impl AnnounceActivity {
     Ok(AnnounceActivity {
       actor: ObjectId::new(community.actor_id()),
       to: vec![public()],
-      object,
+      object: IdOrNestedObject::NestedObject(object),
       cc: vec![community.followers_url.clone().into()],
       kind: AnnounceType::Announce,
       id: generate_activity_id(
@@ -92,11 +95,10 @@ impl ActivityHandler for AnnounceActivity {
   async fn verify(
     &self,
     context: &Data<LemmyContext>,
-    request_counter: &mut i32,
+    _request_counter: &mut i32,
   ) -> Result<(), LemmyError> {
     verify_is_public(&self.to, &self.cc)?;
     verify_activity(&self.id, self.actor.inner(), &context.settings())?;
-    self.object.verify(context, request_counter).await?;
     Ok(())
   }
 
@@ -106,12 +108,16 @@ impl ActivityHandler for AnnounceActivity {
     context: &Data<LemmyContext>,
     request_counter: &mut i32,
   ) -> Result<(), LemmyError> {
+    let object = self.object.object(context, request_counter).await?;
+    // we have to verify this here in order to avoid fetching the object twice over http
+    object.verify(context, request_counter).await?;
+
     // TODO: this can probably be implemented in a cleaner way
-    match self.object {
+    match object {
       // Dont insert these into activities table, as they are not activities.
       AnnouncableActivities::Page(_) => {}
       _ => {
-        let object_value = serde_json::to_value(&self.object)?;
+        let object_value = serde_json::to_value(&object)?;
         let object_data: ActivityCommonFields = serde_json::from_value(object_value.to_owned())?;
 
         let insert =
@@ -125,6 +131,6 @@ impl ActivityHandler for AnnounceActivity {
         }
       }
     }
-    self.object.receive(context, request_counter).await
+    object.receive(context, request_counter).await
   }
 }
diff --git a/crates/apub/src/activities/create_or_update/post.rs b/crates/apub/src/activities/create_or_update/post.rs
index 70c59c35..28600182 100644
--- a/crates/apub/src/activities/create_or_update/post.rs
+++ b/crates/apub/src/activities/create_or_update/post.rs
@@ -93,7 +93,7 @@ impl ActivityHandler for CreateOrUpdatePost {
     match self.kind {
       CreateOrUpdateType::Create => {
         verify_domains_match(self.actor.inner(), self.object.id.inner())?;
-        verify_urls_match(self.actor.inner(), self.object.attributed_to.inner())?;
+        verify_urls_match(self.actor.inner(), self.object.creator()?.inner())?;
         // Check that the post isnt locked or stickied, as that isnt possible for newly created posts.
         // However, when fetching a remote post we generate a new create activity with the current
         // locked/stickied value, so this check may fail. So only check if its a local community,
@@ -119,7 +119,7 @@ impl ActivityHandler for CreateOrUpdatePost {
           .await?;
         } else {
           verify_domains_match(self.actor.inner(), self.object.id.inner())?;
-          verify_urls_match(self.actor.inner(), self.object.attributed_to.inner())?;
+          verify_urls_match(self.actor.inner(), self.object.creator()?.inner())?;
         }
       }
     }
diff --git a/crates/apub/src/activities/deletion/delete.rs b/crates/apub/src/activities/deletion/delete.rs
index 3f021587..f442494a 100644
--- a/crates/apub/src/activities/deletion/delete.rs
+++ b/crates/apub/src/activities/deletion/delete.rs
@@ -6,7 +6,11 @@ use crate::{
     verify_activity,
   },
   objects::{community::ApubCommunity, person::ApubPerson},
-  protocol::activities::deletion::delete::{Delete, IdOrNestedObject, NestedObject},
+  protocol::{
+    activities::deletion::delete::Delete,
+    objects::tombstone::Tombstone,
+    IdOrNestedObject,
+  },
 };
 use activitystreams_kinds::activity::DeleteType;
 use anyhow::anyhow;
@@ -106,7 +110,7 @@ impl Delete {
     Ok(Delete {
       actor: ObjectId::new(actor.actor_id.clone()),
       to: vec![to],
-      object: IdOrNestedObject::NestedObject(NestedObject {
+      object: IdOrNestedObject::NestedObject(Tombstone {
         id: object.id(),
         kind: Default::default(),
       }),
diff --git a/crates/apub/src/activity_lists.rs b/crates/apub/src/activity_lists.rs
index 80d37fc6..c9debefa 100644
--- a/crates/apub/src/activity_lists.rs
+++ b/crates/apub/src/activity_lists.rs
@@ -25,12 +25,14 @@ use crate::{
       voting::{undo_vote::UndoVote, vote::Vote},
     },
     objects::page::Page,
+    Id,
   },
 };
 use lemmy_apub_lib::traits::ActivityHandler;
 use lemmy_utils::LemmyError;
 use lemmy_websocket::LemmyContext;
 use serde::{Deserialize, Serialize};
+use url::Url;
 
 #[derive(Clone, Debug, Deserialize, Serialize, ActivityHandler)]
 #[serde(untagged)]
@@ -120,3 +122,23 @@ impl GetCommunity for AnnouncableActivities {
     Ok(community)
   }
 }
+
+impl Id for AnnouncableActivities {
+  fn id(&self) -> &Url {
+    use AnnouncableActivities::*;
+    match self {
+      CreateOrUpdateComment(c) => &c.id,
+      CreateOrUpdatePost(c) => &c.id,
+      Vote(v) => &v.id,
+      UndoVote(u) => &u.id,
+      Delete(d) => &d.id,
+      UndoDelete(u) => &u.id,
+      UpdateCommunity(u) => &u.id,
+      BlockUser(b) => &b.id,
+      UndoBlockUser(u) => &u.id,
+      AddMod(a) => &a.id,
+      RemoveMod(r) => &r.id,
+      Page(p) => p.id.inner(),
+    }
+  }
+}
diff --git a/crates/apub/src/collections/community_moderators.rs b/crates/apub/src/collections/community_moderators.rs
index 4e084a16..028174ae 100644
--- a/crates/apub/src/collections/community_moderators.rs
+++ b/crates/apub/src/collections/community_moderators.rs
@@ -184,10 +184,7 @@ mod tests {
       file_to_json_object("assets/lemmy/collections/group_moderators.json").unwrap();
     let url = Url::parse("https://enterprise.lemmy.ml/c/tenforward").unwrap();
     let mut request_counter = 0;
-    let community_context = CommunityContext {
-      0: community,
-      1: context,
-    };
+    let community_context = CommunityContext(community, context);
     ApubCommunityModerators::verify(&json, &url, &community_context, &mut request_counter)
       .await
       .unwrap();
diff --git a/crates/apub/src/fetcher/user_or_community.rs b/crates/apub/src/fetcher/user_or_community.rs
index ee86adce..7ec5e9dd 100644
--- a/crates/apub/src/fetcher/user_or_community.rs
+++ b/crates/apub/src/fetcher/user_or_community.rs
@@ -6,7 +6,7 @@ use chrono::NaiveDateTime;
 use lemmy_apub_lib::traits::{ActorType, ApubObject};
 use lemmy_utils::LemmyError;
 use lemmy_websocket::LemmyContext;
-use serde::Deserialize;
+use serde::{Deserialize, Serialize};
 use url::Url;
 
 #[derive(Clone, Debug)]
@@ -15,13 +15,19 @@ pub enum UserOrCommunity {
   Community(ApubCommunity),
 }
 
-#[derive(Deserialize)]
+#[derive(Serialize, Deserialize, Clone, Debug)]
 #[serde(untagged)]
 pub enum PersonOrGroup {
   Person(Person),
   Group(Group),
 }
 
+#[derive(Serialize, Deserialize, Clone, Debug, PartialEq)]
+pub enum PersonOrGroupType {
+  Person,
+  Group,
+}
+
 #[async_trait::async_trait(?Send)]
 impl ApubObject for UserOrCommunity {
   type DataType = LemmyContext;
diff --git a/crates/apub/src/objects/comment.rs b/crates/apub/src/objects/comment.rs
index c987ff8b..96c9f05a 100644
--- a/crates/apub/src/objects/comment.rs
+++ b/crates/apub/src/objects/comment.rs
@@ -15,7 +15,7 @@ use lemmy_api_common::utils::blocking;
 use lemmy_apub_lib::{
   object_id::ObjectId,
   traits::ApubObject,
-  values::MediaTypeHtml,
+  values::MediaTypeMarkdownOrHtml,
   verify::verify_domains_match,
 };
 use lemmy_db_schema::{
@@ -117,7 +117,7 @@ impl ApubObject for ApubComment {
       to: vec![public()],
       cc: maa.ccs,
       content: markdown_to_html(&self.content),
-      media_type: Some(MediaTypeHtml::Html),
+      media_type: Some(MediaTypeMarkdownOrHtml::Html),
       source: Some(Source::new(self.content.clone())),
       in_reply_to,
       published: Some(convert_datetime(self.published)),
@@ -178,7 +178,7 @@ impl ApubObject for ApubComment {
       .await?;
     let (post, parent_comment_id) = note.get_parents(context, request_counter).await?;
 
-    let content = read_from_string_or_source(&note.content, &note.source);
+    let content = read_from_string_or_source(&note.content, &note.media_type, &note.source);
     let content_slurs_removed = remove_slurs(&content, &context.settings().slur_regex());
 
     let form = CommentForm {
diff --git a/crates/apub/src/objects/instance.rs b/crates/apub/src/objects/instance.rs
index 225a1091..57e115f6 100644
--- a/crates/apub/src/objects/instance.rs
+++ b/crates/apub/src/objects/instance.rs
@@ -124,7 +124,11 @@ impl ApubObject for ApubSite {
   ) -> Result<Self, LemmyError> {
     let site_form = SiteForm {
       name: apub.name.clone(),
-      sidebar: Some(read_from_string_or_source_opt(&apub.content, &apub.source)),
+      sidebar: Some(read_from_string_or_source_opt(
+        &apub.content,
+        &None,
+        &apub.source,
+      )),
       updated: apub.updated.map(|u| u.clone().naive_local()),
       icon: Some(apub.icon.clone().map(|i| i.url.into())),
       banner: Some(apub.image.clone().map(|i| i.url.into())),
diff --git a/crates/apub/src/objects/mod.rs b/crates/apub/src/objects/mod.rs
index 0e55c49d..7d5fd26e 100644
--- a/crates/apub/src/objects/mod.rs
+++ b/crates/apub/src/objects/mod.rs
@@ -1,7 +1,7 @@
 use crate::protocol::{ImageObject, Source};
 use anyhow::anyhow;
 use html2md::parse_html;
-use lemmy_apub_lib::verify::verify_domains_match;
+use lemmy_apub_lib::{values::MediaTypeMarkdownOrHtml, verify::verify_domains_match};
 use lemmy_utils::{settings::structs::Settings, LemmyError};
 use url::Url;
 
@@ -12,23 +12,31 @@ pub mod person;
 pub mod post;
 pub mod private_message;
 
-pub(crate) fn read_from_string_or_source(raw: &str, source: &Option<Source>) -> String {
+pub(crate) fn read_from_string_or_source(
+  content: &str,
+  media_type: &Option<MediaTypeMarkdownOrHtml>,
+  source: &Option<Source>,
+) -> String {
   if let Some(s) = source {
+    // markdown sent by lemmy in source field
     s.content.clone()
+  } else if media_type == &Some(MediaTypeMarkdownOrHtml::Markdown) {
+    // markdown sent by peertube in content field
+    content.to_string()
   } else {
-    parse_html(raw)
+    // otherwise, convert content html to markdown
+    parse_html(content)
   }
 }
 
 pub(crate) fn read_from_string_or_source_opt(
-  raw: &Option<String>,
+  content: &Option<String>,
+  media_type: &Option<MediaTypeMarkdownOrHtml>,
   source: &Option<Source>,
 ) -> Option<String> {
-  if let Some(s2) = source {
-    Some(s2.content.clone())
-  } else {
-    raw.as_ref().map(|s| parse_html(s))
-  }
+  content
+    .as_ref()
+    .map(|content| read_from_string_or_source(content, media_type, source))
 }
 
 pub(crate) fn verify_image_domain_matches(
diff --git a/crates/apub/src/objects/person.rs b/crates/apub/src/objects/person.rs
index e1938ed3..8304322c 100644
--- a/crates/apub/src/objects/person.rs
+++ b/crates/apub/src/objects/person.rs
@@ -134,7 +134,7 @@ impl ApubObject for ApubPerson {
     let slur_regex = &context.settings().slur_regex();
     check_slurs(&person.preferred_username, slur_regex)?;
     check_slurs_opt(&person.name, slur_regex)?;
-    let bio = read_from_string_or_source_opt(&person.summary, &person.source);
+    let bio = read_from_string_or_source_opt(&person.summary, &None, &person.source);
     check_slurs_opt(&bio, slur_regex)?;
     Ok(())
   }
@@ -158,6 +158,7 @@ impl ApubObject for ApubPerson {
       actor_id: Some(person.id.into()),
       bio: Some(read_from_string_or_source_opt(
         &person.summary,
+        &None,
         &person.source,
       )),
       local: Some(false),
diff --git a/crates/apub/src/objects/post.rs b/crates/apub/src/objects/post.rs
index f6938dd6..49797036 100644
--- a/crates/apub/src/objects/post.rs
+++ b/crates/apub/src/objects/post.rs
@@ -4,7 +4,7 @@ use crate::{
   objects::{read_from_string_or_source_opt, verify_is_remote_object},
   protocol::{
     objects::{
-      page::{Attachment, Page, PageType},
+      page::{Attachment, AttributedTo, Page, PageType},
       tombstone::Tombstone,
     },
     ImageObject,
@@ -17,7 +17,7 @@ use lemmy_api_common::{request::fetch_site_data, utils::blocking};
 use lemmy_apub_lib::{
   object_id::ObjectId,
   traits::ApubObject,
-  values::MediaTypeHtml,
+  values::MediaTypeMarkdownOrHtml,
   verify::verify_domains_match,
 };
 use lemmy_db_schema::{
@@ -102,14 +102,14 @@ impl ApubObject for ApubPost {
     .await??;
 
     let page = Page {
-      r#type: PageType::Page,
+      kind: PageType::Page,
       id: ObjectId::new(self.ap_id.clone()),
-      attributed_to: ObjectId::new(creator.actor_id),
+      attributed_to: AttributedTo::Lemmy(ObjectId::new(creator.actor_id)),
       to: vec![community.actor_id.into(), public()],
       cc: vec![],
       name: self.name.clone(),
       content: self.body.as_ref().map(|b| markdown_to_html(b)),
-      media_type: Some(MediaTypeHtml::Html),
+      media_type: Some(MediaTypeMarkdownOrHtml::Html),
       source: self.body.clone().map(Source::new),
       url: self.url.clone().map(|u| u.into()),
       attachment: self.url.clone().map(Attachment::new).into_iter().collect(),
@@ -143,9 +143,9 @@ impl ApubObject for ApubPost {
 
     let community = page.extract_community(context, request_counter).await?;
     check_is_apub_id_valid(page.id.inner(), community.local, &context.settings())?;
-    verify_person_in_community(&page.attributed_to, &community, context, request_counter).await?;
+    verify_person_in_community(&page.creator()?, &community, context, request_counter).await?;
     check_slurs(&page.name, &context.settings().slur_regex())?;
-    verify_domains_match(page.attributed_to.inner(), page.id.inner())?;
+    verify_domains_match(page.creator()?.inner(), page.id.inner())?;
     verify_is_public(&page.to, &page.cc)?;
     Ok(())
   }
@@ -157,15 +157,20 @@ impl ApubObject for ApubPost {
     request_counter: &mut i32,
   ) -> Result<ApubPost, LemmyError> {
     let creator = page
-      .attributed_to
+      .creator()?
       .dereference(context, context.client(), request_counter)
       .await?;
     let community = page.extract_community(context, request_counter).await?;
 
     let form = if !page.is_mod_action(context).await? {
       let url = if let Some(attachment) = page.attachment.first() {
+        // url as sent by Lemmy (new)
         Some(attachment.href.clone())
+      } else if page.kind == PageType::Video {
+        // we cant display videos directly, so insert a link to external video page
+        Some(page.id.inner().clone())
       } else {
+        // url sent by lemmy (old)
         page.url
       };
       let thumbnail_url: Option<Url> = page.image.map(|i| i.url);
@@ -177,8 +182,9 @@ impl ApubObject for ApubPost {
       let (embed_title, embed_description, embed_html) = metadata_res
         .map(|u| (u.title, u.description, u.html))
         .unwrap_or((None, None, None));
-      let body_slurs_removed = read_from_string_or_source_opt(&page.content, &page.source)
-        .map(|s| remove_slurs(&s, &context.settings().slur_regex()));
+      let body_slurs_removed =
+        read_from_string_or_source_opt(&page.content, &page.media_type, &page.source)
+          .map(|s| remove_slurs(&s, &context.settings().slur_regex()));
 
       PostForm {
         name: page.name.clone(),
diff --git a/crates/apub/src/objects/private_message.rs b/crates/apub/src/objects/private_message.rs
index 7a638af3..a984180d 100644
--- a/crates/apub/src/objects/private_message.rs
+++ b/crates/apub/src/objects/private_message.rs
@@ -137,7 +137,7 @@ impl ApubObject for ApubPrivateMessage {
     let form = PrivateMessageForm {
       creator_id: creator.id,
       recipient_id: recipient.id,
-      content: read_from_string_or_source(&note.content, &note.source),
+      content: read_from_string_or_source(&note.content, &None, &note.source),
       published: note.published.map(|u| u.naive_local()),
       updated: note.updated.map(|u| u.naive_local()),
       deleted: None,
diff --git a/crates/apub/src/protocol/activities/community/announce.rs b/crates/apub/src/protocol/activities/community/announce.rs
index d693ed6f..75a7d9af 100644
--- a/crates/apub/src/protocol/activities/community/announce.rs
+++ b/crates/apub/src/protocol/activities/community/announce.rs
@@ -1,7 +1,7 @@
 use crate::{
   activity_lists::AnnouncableActivities,
   objects::community::ApubCommunity,
-  protocol::Unparsed,
+  protocol::{IdOrNestedObject, Unparsed},
 };
 use activitystreams_kinds::activity::AnnounceType;
 use lemmy_apub_lib::object_id::ObjectId;
@@ -14,7 +14,7 @@ pub struct AnnounceActivity {
   pub(crate) actor: ObjectId<ApubCommunity>,
   #[serde(deserialize_with = "crate::deserialize_one_or_many")]
   pub(crate) to: Vec<Url>,
-  pub(crate) object: AnnouncableActivities,
+  pub(crate) object: IdOrNestedObject<AnnouncableActivities>,
   #[serde(deserialize_with = "crate::deserialize_one_or_many")]
   pub(crate) cc: Vec<Url>,
   #[serde(rename = "type")]
diff --git a/crates/apub/src/protocol/activities/deletion/delete.rs b/crates/apub/src/protocol/activities/deletion/delete.rs
index d3ec53be..3fb5984d 100644
--- a/crates/apub/src/protocol/activities/deletion/delete.rs
+++ b/crates/apub/src/protocol/activities/deletion/delete.rs
@@ -1,5 +1,8 @@
-use crate::{objects::person::ApubPerson, protocol::Unparsed};
-use activitystreams_kinds::{activity::DeleteType, object::TombstoneType};
+use crate::{
+  objects::person::ApubPerson,
+  protocol::{objects::tombstone::Tombstone, IdOrNestedObject, Unparsed},
+};
+use activitystreams_kinds::activity::DeleteType;
 use lemmy_apub_lib::object_id::ObjectId;
 use serde::{Deserialize, Serialize};
 use serde_with::skip_serializing_none;
@@ -12,7 +15,7 @@ pub struct Delete {
   pub(crate) actor: ObjectId<ApubPerson>,
   #[serde(deserialize_with = "crate::deserialize_one_or_many")]
   pub(crate) to: Vec<Url>,
-  pub(crate) object: IdOrNestedObject,
+  pub(crate) object: IdOrNestedObject<Tombstone>,
   #[serde(rename = "type")]
   pub(crate) kind: DeleteType,
   pub(crate) id: Url,
@@ -27,29 +30,3 @@ pub struct Delete {
   #[serde(flatten)]
   pub(crate) unparsed: Unparsed,
 }
-
-/// Instead of a simple ID string as object, Mastodon sends a nested tombstone for some reason,
-/// so we need to handle that as well.
-#[derive(Clone, Debug, Deserialize, Serialize)]
-#[serde(untagged)]
-pub(crate) enum IdOrNestedObject {
-  Id(Url),
-  NestedObject(NestedObject),
-}
-
-#[derive(Clone, Debug, Deserialize, Serialize)]
-pub(crate) struct NestedObject {
-  pub(crate) id: Url,
-  // Backwards compatibility with Lemmy 0.15
-  #[serde(rename = "type")]
-  pub(crate) kind: TombstoneType,
-}
-
-impl IdOrNestedObject {
-  pub(crate) fn id(&self) -> &Url {
-    match self {
-      IdOrNestedObject::Id(i) => i,
-      IdOrNestedObject::NestedObject(n) => &n.id,
-    }
-  }
-}
diff --git a/crates/apub/src/protocol/activities/deletion/delete_user.rs b/crates/apub/src/protocol/activities/deletion/delete_user.rs
index 22d215eb..a45bfbdb 100644
--- a/crates/apub/src/protocol/activities/deletion/delete_user.rs
+++ b/crates/apub/src/protocol/activities/deletion/delete_user.rs
@@ -17,8 +17,7 @@ pub struct DeleteUser {
   pub(crate) kind: DeleteType,
   pub(crate) id: Url,
 
-  #[serde(deserialize_with = "crate::deserialize_one_or_many")]
-  #[serde(default)]
+  #[serde(deserialize_with = "crate::deserialize_one_or_many", default)]
   #[serde(skip_serializing_if = "Vec::is_empty")]
   pub(crate) cc: Vec<Url>,
 }
diff --git a/crates/apub/src/protocol/activities/deletion/undo_delete.rs b/crates/apub/src/protocol/activities/deletion/undo_delete.rs
index bc5b942f..e676ab00 100644
--- a/crates/apub/src/protocol/activities/deletion/undo_delete.rs
+++ b/crates/apub/src/protocol/activities/deletion/undo_delete.rs
@@ -18,8 +18,7 @@ pub struct UndoDelete {
   pub(crate) kind: UndoType,
   pub(crate) id: Url,
 
-  #[serde(deserialize_with = "crate::deserialize_one_or_many")]
-  #[serde(default)]
+  #[serde(deserialize_with = "crate::deserialize_one_or_many", default)]
   #[serde(skip_serializing_if = "Vec::is_empty")]
   pub(crate) cc: Vec<Url>,
   #[serde(flatten)]
diff --git a/crates/apub/src/protocol/activities/mod.rs b/crates/apub/src/protocol/activities/mod.rs
index 4301ed59..35d14f8f 100644
--- a/crates/apub/src/protocol/activities/mod.rs
+++ b/crates/apub/src/protocol/activities/mod.rs
@@ -18,6 +18,7 @@ pub enum CreateOrUpdateType {
 mod tests {
   use crate::protocol::{
     activities::{
+      community::announce::AnnounceActivity,
       create_or_update::{comment::CreateOrUpdateComment, post::CreateOrUpdatePost},
       deletion::delete::Delete,
       following::{follow::FollowCommunity, undo_follow::UndoFollowCommunity},
@@ -70,4 +71,9 @@ mod tests {
     test_json::<CreateOrUpdateComment>("assets/gnusocial/activities/create_note.json").unwrap();
     test_json::<Vote>("assets/gnusocial/activities/like_note.json").unwrap();
   }
+
+  #[test]
+  fn test_parse_peertube_activities() {
+    test_json::<AnnounceActivity>("assets/peertube/activities/announce_video.json").unwrap();
+  }
 }
diff --git a/crates/apub/src/protocol/mod.rs b/crates/apub/src/protocol/mod.rs
index ea56cda1..3206ac23 100644
--- a/crates/apub/src/protocol/mod.rs
+++ b/crates/apub/src/protocol/mod.rs
@@ -1,7 +1,9 @@
 use activitystreams_kinds::object::ImageType;
-use lemmy_apub_lib::values::MediaTypeMarkdown;
+use lemmy_apub_lib::{utils::fetch_object_http, values::MediaTypeMarkdown};
 use lemmy_db_schema::newtypes::DbUrl;
-use serde::{Deserialize, Serialize};
+use lemmy_utils::LemmyError;
+use lemmy_websocket::LemmyContext;
+use serde::{de::DeserializeOwned, Deserialize, Serialize};
 use std::collections::HashMap;
 use url::Url;
 
@@ -42,10 +44,40 @@ impl ImageObject {
   }
 }
 
-#[derive(Clone, Debug, Default, serde::Deserialize, serde::Serialize)]
+#[derive(Clone, Debug, Default, Deserialize, Serialize)]
 #[serde(transparent)]
 pub struct Unparsed(HashMap<String, serde_json::Value>);
 
+pub(crate) trait Id {
+  fn id(&self) -> &Url;
+}
+
+#[derive(Clone, Debug, Deserialize, Serialize)]
+#[serde(untagged)]
+pub(crate) enum IdOrNestedObject<Kind: Id> {
+  Id(Url),
+  NestedObject(Kind),
+}
+
+impl<Kind: Id + DeserializeOwned> IdOrNestedObject<Kind> {
+  pub(crate) fn id(&self) -> &Url {
+    match self {
+      IdOrNestedObject::Id(i) => i,
+      IdOrNestedObject::NestedObject(n) => n.id(),
+    }
+  }
+  pub(crate) async fn object(
+    self,
+    context: &LemmyContext,
+    request_counter: &mut i32,
+  ) -> Result<Kind, LemmyError> {
+    match self {
+      IdOrNestedObject::Id(i) => fetch_object_http(&i, context.client(), request_counter).await,
+      IdOrNestedObject::NestedObject(o) => Ok(o),
+    }
+  }
+}
+
 #[cfg(test)]
 pub(crate) mod tests {
   use crate::context::WithContext;
diff --git a/crates/apub/src/protocol/objects/chat_message.rs b/crates/apub/src/protocol/objects/chat_message.rs
index 163bff3a..e0ac8db9 100644
--- a/crates/apub/src/protocol/objects/chat_message.rs
+++ b/crates/apub/src/protocol/objects/chat_message.rs
@@ -19,8 +19,7 @@ pub struct ChatMessage {
   pub(crate) content: String,
 
   pub(crate) media_type: Option<MediaTypeHtml>,
-  #[serde(default)]
-  #[serde(deserialize_with = "crate::deserialize_skip_error")]
+  #[serde(deserialize_with = "crate::deserialize_skip_error", default)]
   pub(crate) source: Option<Source>,
   pub(crate) published: Option<DateTime<FixedOffset>>,
   pub(crate) updated: Option<DateTime<FixedOffset>>,
diff --git a/crates/apub/src/protocol/objects/group.rs b/crates/apub/src/protocol/objects/group.rs
index c0b544f3..5f03814a 100644
--- a/crates/apub/src/protocol/objects/group.rs
+++ b/crates/apub/src/protocol/objects/group.rs
@@ -40,8 +40,7 @@ pub struct Group {
   /// title
   pub(crate) name: Option<String>,
   pub(crate) summary: Option<String>,
-  #[serde(default)]
-  #[serde(deserialize_with = "crate::deserialize_skip_error")]
+  #[serde(deserialize_with = "crate::deserialize_skip_error", default)]
   pub(crate) source: Option<Source>,
   pub(crate) icon: Option<ImageObject>,
   /// banner
@@ -72,7 +71,7 @@ impl Group {
     let slur_regex = &context.settings().slur_regex();
     check_slurs(&self.preferred_username, slur_regex)?;
     check_slurs_opt(&self.name, slur_regex)?;
-    let description = read_from_string_or_source_opt(&self.summary, &self.source);
+    let description = read_from_string_or_source_opt(&self.summary, &None, &self.source);
     check_slurs_opt(&description, slur_regex)?;
     Ok(())
   }
@@ -81,7 +80,7 @@ impl Group {
     CommunityForm {
       name: self.preferred_username.clone(),
       title: self.name.unwrap_or(self.preferred_username),
-      description: read_from_string_or_source_opt(&self.summary, &self.source),
+      description: read_from_string_or_source_opt(&self.summary, &None, &self.source),
       removed: None,
       published: self.published.map(|u| u.naive_local()),
       updated: self.updated.map(|u| u.naive_local()),
diff --git a/crates/apub/src/protocol/objects/instance.rs b/crates/apub/src/protocol/objects/instance.rs
index 353d6500..a18b7279 100644
--- a/crates/apub/src/protocol/objects/instance.rs
+++ b/crates/apub/src/protocol/objects/instance.rs
@@ -30,8 +30,7 @@ pub struct Instance {
 
   // sidebar
   pub(crate) content: Option<String>,
-  #[serde(default)]
-  #[serde(deserialize_with = "crate::deserialize_skip_error")]
+  #[serde(deserialize_with = "crate::deserialize_skip_error", default)]
   pub(crate) source: Option<Source>,
   // short instance description
   pub(crate) summary: Option<String>,
diff --git a/crates/apub/src/protocol/objects/mod.rs b/crates/apub/src/protocol/objects/mod.rs
index 864c13d9..8bacae95 100644
--- a/crates/apub/src/protocol/objects/mod.rs
+++ b/crates/apub/src/protocol/objects/mod.rs
@@ -86,4 +86,12 @@ mod tests {
     test_json::<Page>("assets/gnusocial/objects/page.json").unwrap();
     test_json::<Note>("assets/gnusocial/objects/note.json").unwrap();
   }
+
+  #[test]
+  fn test_parse_object_peertube() {
+    test_json::<Person>("assets/peertube/objects/person.json").unwrap();
+    test_json::<Group>("assets/peertube/objects/group.json").unwrap();
+    test_json::<Page>("assets/peertube/objects/video.json").unwrap();
+    test_json::<Note>("assets/peertube/objects/note.json").unwrap();
+  }
 }
diff --git a/crates/apub/src/protocol/objects/note.rs b/crates/apub/src/protocol/objects/note.rs
index 779675cc..ac3e722c 100644
--- a/crates/apub/src/protocol/objects/note.rs
+++ b/crates/apub/src/protocol/objects/note.rs
@@ -7,7 +7,7 @@ use crate::{
 use activitystreams_kinds::object::NoteType;
 use chrono::{DateTime, FixedOffset};
 use lemmy_api_common::utils::blocking;
-use lemmy_apub_lib::{object_id::ObjectId, values::MediaTypeHtml};
+use lemmy_apub_lib::{object_id::ObjectId, values::MediaTypeMarkdownOrHtml};
 use lemmy_db_schema::{newtypes::CommentId, source::post::Post, traits::Crud};
 use lemmy_utils::LemmyError;
 use lemmy_websocket::LemmyContext;
@@ -25,15 +25,13 @@ pub struct Note {
   pub(crate) attributed_to: ObjectId<ApubPerson>,
   #[serde(deserialize_with = "crate::deserialize_one_or_many")]
   pub(crate) to: Vec<Url>,
-  #[serde(default)]
-  #[serde(deserialize_with = "crate::deserialize_one_or_many")]
+  #[serde(deserialize_with = "crate::deserialize_one_or_many", default)]
   pub(crate) cc: Vec<Url>,
   pub(crate) content: String,
   pub(crate) in_reply_to: ObjectId<PostOrComment>,
 
-  pub(crate) media_type: Option<MediaTypeHtml>,
-  #[serde(default)]
-  #[serde(deserialize_with = "crate::deserialize_skip_error")]
+  pub(crate) media_type: Option<MediaTypeMarkdownOrHtml>,
+  #[serde(deserialize_with = "crate::deserialize_skip_error", default)]
   pub(crate) source: Option<Source>,
   pub(crate) published: Option<DateTime<FixedOffset>>,
   pub(crate) updated: Option<DateTime<FixedOffset>>,
diff --git a/crates/apub/src/protocol/objects/page.rs b/crates/apub/src/protocol/objects/page.rs
index c799c520..426de96f 100644
--- a/crates/apub/src/protocol/objects/page.rs
+++ b/crates/apub/src/protocol/objects/page.rs
@@ -1,4 +1,5 @@
 use crate::{
+  fetcher::user_or_community::{PersonOrGroupType, UserOrCommunity},
   objects::{community::ApubCommunity, person::ApubPerson, post::ApubPost},
   protocol::{ImageObject, Source},
 };
@@ -9,7 +10,7 @@ use lemmy_apub_lib::{
   data::Data,
   object_id::ObjectId,
   traits::{ActivityHandler, ApubObject},
-  values::MediaTypeHtml,
+  values::MediaTypeMarkdownOrHtml,
 };
 use lemmy_db_schema::newtypes::DbUrl;
 use lemmy_utils::LemmyError;
@@ -18,33 +19,34 @@ use serde::{Deserialize, Serialize};
 use serde_with::skip_serializing_none;
 use url::Url;
 
-#[derive(Clone, Debug, Deserialize, Serialize)]
+#[derive(Clone, Debug, Deserialize, Serialize, PartialEq)]
 pub enum PageType {
   Page,
   Article,
   Note,
+  Video,
 }
 
 #[skip_serializing_none]
 #[derive(Clone, Debug, Deserialize, Serialize)]
 #[serde(rename_all = "camelCase")]
 pub struct Page {
-  pub(crate) r#type: PageType,
+  #[serde(rename = "type")]
+  pub(crate) kind: PageType,
   pub(crate) id: ObjectId<ApubPost>,
-  pub(crate) attributed_to: ObjectId<ApubPerson>,
+  pub(crate) attributed_to: AttributedTo,
   #[serde(deserialize_with = "crate::deserialize_one_or_many")]
   pub(crate) to: Vec<Url>,
   pub(crate) name: String,
 
-  #[serde(default)]
-  #[serde(deserialize_with = "crate::deserialize_one_or_many")]
+  #[serde(deserialize_with = "crate::deserialize_one_or_many", default)]
   pub(crate) cc: Vec<Url>,
   pub(crate) content: Option<String>,
-  pub(crate) media_type: Option<MediaTypeHtml>,
-  #[serde(default)]
-  #[serde(deserialize_with = "crate::deserialize_skip_error")]
+  pub(crate) media_type: Option<MediaTypeMarkdownOrHtml>,
+  #[serde(deserialize_with = "crate::deserialize_skip_error", default)]
   pub(crate) source: Option<Source>,
   /// deprecated, use attachment field
+  #[serde(deserialize_with = "crate::deserialize_skip_error", default)]
   pub(crate) url: Option<Url>,
   /// most software uses array type for attachment field, so we do the same. nevertheless, we only
   /// use the first item
@@ -60,11 +62,26 @@ pub struct Page {
 
 #[derive(Clone, Debug, Deserialize, Serialize)]
 #[serde(rename_all = "camelCase")]
-pub struct Attachment {
+pub(crate) struct Attachment {
   pub(crate) href: Url,
   pub(crate) r#type: LinkType,
 }
 
+#[derive(Clone, Debug, Deserialize, Serialize)]
+#[serde(untagged)]
+pub(crate) enum AttributedTo {
+  Lemmy(ObjectId<ApubPerson>),
+  Peertube([AttributedToPeertube; 2]),
+}
+
+#[derive(Clone, Debug, Deserialize, Serialize)]
+#[serde(rename_all = "camelCase")]
+pub(crate) struct AttributedToPeertube {
+  #[serde(rename = "type")]
+  pub kind: PersonOrGroupType,
+  pub id: ObjectId<UserOrCommunity>,
+}
+
 impl Page {
   /// Only mods can change the post's stickied/locked status. So if either of these is changed from
   /// the current value, it is a mod action and needs to be verified as such.
@@ -111,21 +128,44 @@ impl Page {
     context: &LemmyContext,
     request_counter: &mut i32,
   ) -> Result<ApubCommunity, LemmyError> {
-    let mut iter = self.to.iter().merge(self.cc.iter());
-    loop {
-      if let Some(cid) = iter.next() {
-        let cid = ObjectId::new(cid.clone());
-        if let Ok(c) = cid
+    match &self.attributed_to {
+      AttributedTo::Lemmy(_) => {
+        let mut iter = self.to.iter().merge(self.cc.iter());
+        loop {
+          if let Some(cid) = iter.next() {
+            let cid = ObjectId::new(cid.clone());
+            if let Ok(c) = cid
+              .dereference(context, context.client(), request_counter)
+              .await
+            {
+              break Ok(c);
+            }
+          } else {
+            return Err(LemmyError::from_message("No community found in cc"));
+          }
+        }
+      }
+      AttributedTo::Peertube(p) => {
+        p.iter()
+          .find(|a| a.kind == PersonOrGroupType::Group)
+          .map(|a| ObjectId::<ApubCommunity>::new(a.id.clone().into_inner()))
+          .ok_or_else(|| LemmyError::from_message("page does not specify group"))?
           .dereference(context, context.client(), request_counter)
           .await
-        {
-          break Ok(c);
-        }
-      } else {
-        return Err(LemmyError::from_message("No community found in cc"));
       }
     }
   }
+
+  pub(crate) fn creator(&self) -> Result<ObjectId<ApubPerson>, LemmyError> {
+    match &self.attributed_to {
+      AttributedTo::Lemmy(l) => Ok(l.clone()),
+      AttributedTo::Peertube(p) => p
+        .iter()
+        .find(|a| a.kind == PersonOrGroupType::Person)
+        .map(|a| ObjectId::<ApubPerson>::new(a.id.clone().into_inner()))
+        .ok_or_else(|| LemmyError::from_message("page does not specify creator person")),
+    }
+  }
 }
 
 impl Attachment {
diff --git a/crates/apub/src/protocol/objects/person.rs b/crates/apub/src/protocol/objects/person.rs
index 1fe072a3..75d20cf6 100644
--- a/crates/apub/src/protocol/objects/person.rs
+++ b/crates/apub/src/protocol/objects/person.rs
@@ -32,8 +32,7 @@ pub struct Person {
   /// displayname
   pub(crate) name: Option<String>,
   pub(crate) summary: Option<String>,
-  #[serde(default)]
-  #[serde(deserialize_with = "crate::deserialize_skip_error")]
+  #[serde(deserialize_with = "crate::deserialize_skip_error", default)]
   pub(crate) source: Option<Source>,
   /// user avatar
   pub(crate) icon: Option<ImageObject>,
diff --git a/crates/apub/src/protocol/objects/tombstone.rs b/crates/apub/src/protocol/objects/tombstone.rs
index 6eb81217..0e60d624 100644
--- a/crates/apub/src/protocol/objects/tombstone.rs
+++ b/crates/apub/src/protocol/objects/tombstone.rs
@@ -1,3 +1,4 @@
+use crate::protocol::Id;
 use activitystreams_kinds::object::TombstoneType;
 use serde::{Deserialize, Serialize};
 use serde_with::skip_serializing_none;
@@ -9,7 +10,7 @@ use url::Url;
 pub struct Tombstone {
   pub(crate) id: Url,
   #[serde(rename = "type")]
-  kind: TombstoneType,
+  pub(crate) kind: TombstoneType,
 }
 
 impl Tombstone {
@@ -20,3 +21,9 @@ impl Tombstone {
     }
   }
 }
+
+impl Id for Tombstone {
+  fn id(&self) -> &Url {
+    &self.id
+  }
+}
diff --git a/crates/apub_lib/src/lib.rs b/crates/apub_lib/src/lib.rs
index 3c11fcea..7c110169 100644
--- a/crates/apub_lib/src/lib.rs
+++ b/crates/apub_lib/src/lib.rs
@@ -3,6 +3,7 @@ pub mod data;
 pub mod object_id;
 pub mod signatures;
 pub mod traits;
+pub mod utils;
 pub mod values;
 pub mod verify;
 
diff --git a/crates/apub_lib/src/object_id.rs b/crates/apub_lib/src/object_id.rs
index c68b5b37..da389616 100644
--- a/crates/apub_lib/src/object_id.rs
+++ b/crates/apub_lib/src/object_id.rs
@@ -1,16 +1,14 @@
-use crate::{traits::ApubObject, APUB_JSON_CONTENT_TYPE};
+use crate::{traits::ApubObject, utils::fetch_object_http};
 use anyhow::anyhow;
 use chrono::{Duration as ChronoDuration, NaiveDateTime, Utc};
 use diesel::NotFound;
-use lemmy_utils::{request::retry, settings::structs::Settings, LemmyError, REQWEST_TIMEOUT};
-use reqwest::StatusCode;
+use lemmy_utils::{settings::structs::Settings, LemmyError};
 use reqwest_middleware::ClientWithMiddleware;
 use serde::{Deserialize, Serialize};
 use std::{
   fmt::{Debug, Display, Formatter},
   marker::PhantomData,
 };
-use tracing::info;
 use url::Url;
 
 /// We store Url on the heap because it is quite large (88 bytes).
@@ -37,6 +35,10 @@ where
     &self.0
   }
 
+  pub fn into_inner(self) -> Url {
+    *self.0
+  }
+
   /// Fetches an activitypub object, either from local database (if possible), or over http.
   pub async fn dereference(
     &self,
@@ -100,32 +102,19 @@ where
     request_counter: &mut i32,
     db_object: Option<Kind>,
   ) -> Result<Kind, LemmyError> {
-    // dont fetch local objects this way
-    debug_assert!(self.0.domain() != Some(&Settings::get().hostname));
-    info!("Fetching remote object {}", self.to_string());
+    let res = fetch_object_http(&self.0, client, request_counter).await;
 
-    *request_counter += 1;
-    if *request_counter > Settings::get().http_fetch_retry_limit {
-      return Err(LemmyError::from(anyhow!("Request retry limit reached")));
-    }
-
-    let res = retry(|| {
-      client
-        .get(self.0.as_str())
-        .header("Accept", APUB_JSON_CONTENT_TYPE)
-        .timeout(REQWEST_TIMEOUT)
-        .send()
-    })
-    .await?;
-
-    if res.status() == StatusCode::GONE {
-      if let Some(db_object) = db_object {
-        db_object.delete(data).await?;
+    if let Err(e) = &res {
+      // TODO: very ugly
+      if e.message == Some("410".to_string()) {
+        if let Some(db_object) = db_object {
+          db_object.delete(data).await?;
+        }
+        return Err(anyhow!("Fetched remote object {} which was deleted", self).into());
       }
-      return Err(anyhow!("Fetched remote object {} which was deleted", self).into());
     }
 
-    let res2: Kind::ApubType = res.json().await?;
+    let res2 = res?;
 
     Kind::verify(&res2, self.inner(), data, request_counter).await?;
     Kind::from_apub(res2, data, request_counter).await
diff --git a/crates/apub_lib/src/utils.rs b/crates/apub_lib/src/utils.rs
new file mode 100644
index 00000000..391e01df
--- /dev/null
+++ b/crates/apub_lib/src/utils.rs
@@ -0,0 +1,38 @@
+use crate::APUB_JSON_CONTENT_TYPE;
+use anyhow::anyhow;
+use http::StatusCode;
+use lemmy_utils::{request::retry, settings::structs::Settings, LemmyError, REQWEST_TIMEOUT};
+use reqwest_middleware::ClientWithMiddleware;
+use serde::de::DeserializeOwned;
+use tracing::log::info;
+use url::Url;
+
+pub async fn fetch_object_http<Kind: DeserializeOwned>(
+  url: &Url,
+  client: &ClientWithMiddleware,
+  request_counter: &mut i32,
+) -> Result<Kind, LemmyError> {
+  // dont fetch local objects this way
+  debug_assert!(url.domain() != Some(&Settings::get().hostname));
+  info!("Fetching remote object {}", url.to_string());
+
+  *request_counter += 1;
+  if *request_counter > Settings::get().http_fetch_retry_limit {
+    return Err(LemmyError::from(anyhow!("Request retry limit reached")));
+  }
+
+  let res = retry(|| {
+    client
+      .get(url.as_str())
+      .header("Accept", APUB_JSON_CONTENT_TYPE)
+      .timeout(REQWEST_TIMEOUT)
+      .send()
+  })
+  .await?;
+
+  if res.status() == StatusCode::GONE {
+    return Err(LemmyError::from_message("410"));
+  }
+
+  Ok(res.json().await?)
+}
diff --git a/crates/apub_lib/src/values.rs b/crates/apub_lib/src/values.rs
index bd014c12..bf1780a8 100644
--- a/crates/apub_lib/src/values.rs
+++ b/crates/apub_lib/src/values.rs
@@ -42,7 +42,7 @@ pub enum MediaTypeMarkdown {
   Markdown,
 }
 
-/// Media type for HTML text/
+/// Media type for HTML text.
 ///
 /// <https://www.iana.org/assignments/media-types/media-types.xhtml>
 #[derive(Clone, Debug, Deserialize, Serialize)]
@@ -50,3 +50,11 @@ pub enum MediaTypeHtml {
   #[serde(rename = "text/html")]
   Html,
 }
+/// Media type which allows both markdown and HTML.
+#[derive(Clone, Debug, Deserialize, Serialize, PartialEq)]
+pub enum MediaTypeMarkdownOrHtml {
+  #[serde(rename = "text/markdown")]
+  Markdown,
+  #[serde(rename = "text/html")]
+  Html,
+}