From cd3f20e49b81c6725232e792f4481210284e57df Mon Sep 17 00:00:00 2001
From: Felix Ableitner <me@nutomic.com>
Date: Tue, 24 Nov 2020 18:53:43 +0100
Subject: [PATCH] Populate `content` with HTML, and `source` with markdown (ref
 #1220)

---
 Cargo.lock                                    |  4 +-
 docs/src/contributing_apub_api_outline.md     | 25 ++++++-
 lemmy_apub/Cargo.toml                         |  2 +-
 lemmy_apub/src/activities/receive/comment.rs  | 10 +--
 .../src/activities/receive/comment_undo.rs    |  7 +-
 .../src/activities/receive/private_message.rs |  7 +-
 lemmy_apub/src/fetcher.rs                     |  7 +-
 lemmy_apub/src/lib.rs                         |  9 +--
 lemmy_apub/src/objects/comment.rs             | 31 +++++----
 lemmy_apub/src/objects/community.rs           | 26 ++++---
 lemmy_apub/src/objects/mod.rs                 | 68 ++++++++++++++++++-
 lemmy_apub/src/objects/post.rs                | 22 +++---
 lemmy_apub/src/objects/private_message.rs     | 32 +++++----
 lemmy_apub/src/objects/user.rs                | 24 ++++---
 14 files changed, 184 insertions(+), 90 deletions(-)

diff --git a/Cargo.lock b/Cargo.lock
index c5f98478..b2247c87 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -2,9 +2,9 @@
 # It is not intended for manual editing.
 [[package]]
 name = "activitystreams"
-version = "0.7.0-alpha.4"
+version = "0.7.0-alpha.6"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "261b423734cca2a170d7a76936f1f0f9e6c6fc297d36cfc5ea6aa15f9017f996"
+checksum = "0b1afe32371e466a791ced0d6ef6e6b97822bb1a279ee4cc41c4324e61cd0b2b"
 dependencies = [
  "chrono",
  "mime",
diff --git a/docs/src/contributing_apub_api_outline.md b/docs/src/contributing_apub_api_outline.md
index f5c7a595..4fac3cf8 100644
--- a/docs/src/contributing_apub_api_outline.md
+++ b/docs/src/contributing_apub_api_outline.md
@@ -64,6 +64,10 @@ Receives activities from user: `Follow`, `Undo/Follow`, `Create`, `Update`, `Lik
         "https://enterprise.lemmy.ml/u/riker"
     ],
     "content": "Welcome to the default community!",
+    "source": {
+        "content": "Welcome to the default community!",
+        "mediaType": "text/markdown"
+    },
     "icon": {
         "type": "Image",
         "url": "https://enterprise.lemmy.ml/pictrs/image/Z8pFFb21cl.png"
@@ -123,7 +127,11 @@ Sends and receives activities from/to other users: `Create/Note`, `Update/Note`,
     "type": "Person",
     "preferredUsername": "picard",
     "name": "Jean-Luc Picard",
-    "summary": "The user bio",
+    "content": "The user bio",
+    "source": {
+        "content": "The user bio",
+        "mediaType": "text/markdown"
+    },
     "icon": {
         "type": "Image",
         "url": "https://enterprise.lemmy.ml/pictrs/image/DS3q0colRA.jpg"
@@ -150,7 +158,7 @@ Sends and receives activities from/to other users: `Create/Note`, `Update/Note`,
 |---|---|---|
 | `preferredUsername` | yes | Name of the actor |
 | `name` | no | The user's displayname |
-| `summary` | no | User bio |
+| `content` | no | User bio |
 | `icon` | no | The user's avatar, shown next to the username |
 | `image` | no | The user's banner, shown on top of the profile |
 | `inbox` | no | ActivityPub inbox URL |
@@ -174,6 +182,10 @@ A page with title, and optional URL and text content. The URL often leads to an
     "to": "https://voyager.lemmy.ml/c/main",
     "summary": "Test thumbnail 2",
     "content": "blub blub",
+    "source": {
+        "content": "blub blub",
+        "mediaType": "text/markdown"
+    },
     "url": "https://voyager.lemmy.ml:/pictrs/image/fzGwCsq7BJ.jpg",
     "image": {
         "type": "Image",
@@ -213,6 +225,10 @@ A reply to a post, or reply to another comment. Contains only text (including re
     "attributedTo": "https://enterprise.lemmy.ml/u/picard",
     "to": "https://enterprise.lemmy.ml/c/main",
     "content": "mmmk",
+    "source": {
+        "content": "mmmk",
+        "mediaType": "text/markdown"
+    },
     "inReplyTo": [
         "https://enterprise.lemmy.ml/post/38",
         "https://voyager.lemmy.ml/comment/73"
@@ -243,6 +259,11 @@ A direct message from one user to another. Can not include additional users. Thr
     "attributedTo": "https://enterprise.lemmy.ml/u/picard",
     "to": "https://voyager.lemmy.ml/u/janeway",
     "content": "test",
+    "source": {
+        "content": "test",
+        "mediaType": "text/markdown"
+    },
+    "mediaType": "text/markdown",
     "published": "2020-10-08T19:10:46.542820+00:00",
     "updated": "2020-10-08T20:13:52.547156+00:00"
 }
diff --git a/lemmy_apub/Cargo.toml b/lemmy_apub/Cargo.toml
index 50bf62f0..6dd68bc8 100644
--- a/lemmy_apub/Cargo.toml
+++ b/lemmy_apub/Cargo.toml
@@ -14,7 +14,7 @@ lemmy_db = { path = "../lemmy_db" }
 lemmy_structs = { path = "../lemmy_structs" }
 lemmy_websocket = { path = "../lemmy_websocket" }
 diesel = "1.4"
-activitystreams = "0.7.0-alpha.4"
+activitystreams = "0.7.0-alpha.6"
 activitystreams-ext = "0.1.0-alpha.2"
 bcrypt = "0.8"
 chrono = { version = "0.4", features = ["serde"] }
diff --git a/lemmy_apub/src/activities/receive/comment.rs b/lemmy_apub/src/activities/receive/comment.rs
index d104d5e1..bf8de1eb 100644
--- a/lemmy_apub/src/activities/receive/comment.rs
+++ b/lemmy_apub/src/activities/receive/comment.rs
@@ -3,11 +3,11 @@ use crate::{
   fetcher::get_or_fetch_and_insert_comment,
   ActorType,
   FromApub,
+  NoteExt,
 };
 use activitystreams::{
   activity::{ActorAndObjectRefExt, Create, Dislike, Like, Remove, Update},
   base::ExtendsExt,
-  object::Note,
 };
 use anyhow::{anyhow, Context};
 use lemmy_db::{
@@ -27,7 +27,7 @@ pub(crate) async fn receive_create_comment(
   request_counter: &mut i32,
 ) -> Result<(), LemmyError> {
   let user = get_actor_as_user(&create, context, request_counter).await?;
-  let note = Note::from_any_base(create.object().to_owned().one().context(location_info!())?)?
+  let note = NoteExt::from_any_base(create.object().to_owned().one().context(location_info!())?)?
     .context(location_info!())?;
 
   let comment =
@@ -83,7 +83,7 @@ pub(crate) async fn receive_update_comment(
   context: &LemmyContext,
   request_counter: &mut i32,
 ) -> Result<(), LemmyError> {
-  let note = Note::from_any_base(update.object().to_owned().one().context(location_info!())?)?
+  let note = NoteExt::from_any_base(update.object().to_owned().one().context(location_info!())?)?
     .context(location_info!())?;
   let user = get_actor_as_user(&update, context, request_counter).await?;
 
@@ -140,7 +140,7 @@ pub(crate) async fn receive_like_comment(
   context: &LemmyContext,
   request_counter: &mut i32,
 ) -> Result<(), LemmyError> {
-  let note = Note::from_any_base(like.object().to_owned().one().context(location_info!())?)?
+  let note = NoteExt::from_any_base(like.object().to_owned().one().context(location_info!())?)?
     .context(location_info!())?;
   let user = get_actor_as_user(&like, context, request_counter).await?;
 
@@ -191,7 +191,7 @@ pub(crate) async fn receive_dislike_comment(
   context: &LemmyContext,
   request_counter: &mut i32,
 ) -> Result<(), LemmyError> {
-  let note = Note::from_any_base(
+  let note = NoteExt::from_any_base(
     dislike
       .object()
       .to_owned()
diff --git a/lemmy_apub/src/activities/receive/comment_undo.rs b/lemmy_apub/src/activities/receive/comment_undo.rs
index 709e8481..f44604cc 100644
--- a/lemmy_apub/src/activities/receive/comment_undo.rs
+++ b/lemmy_apub/src/activities/receive/comment_undo.rs
@@ -2,8 +2,9 @@ use crate::{
   activities::receive::get_actor_as_user,
   fetcher::get_or_fetch_and_insert_comment,
   FromApub,
+  NoteExt,
 };
-use activitystreams::{activity::*, object::Note, prelude::*};
+use activitystreams::{activity::*, prelude::*};
 use anyhow::Context;
 use lemmy_db::{
   comment::{Comment, CommentForm, CommentLike},
@@ -20,7 +21,7 @@ pub(crate) async fn receive_undo_like_comment(
   request_counter: &mut i32,
 ) -> Result<(), LemmyError> {
   let user = get_actor_as_user(like, context, request_counter).await?;
-  let note = Note::from_any_base(like.object().to_owned().one().context(location_info!())?)?
+  let note = NoteExt::from_any_base(like.object().to_owned().one().context(location_info!())?)?
     .context(location_info!())?;
 
   let comment = CommentForm::from_apub(&note, context, None, request_counter).await?;
@@ -64,7 +65,7 @@ pub(crate) async fn receive_undo_dislike_comment(
   request_counter: &mut i32,
 ) -> Result<(), LemmyError> {
   let user = get_actor_as_user(dislike, context, request_counter).await?;
-  let note = Note::from_any_base(
+  let note = NoteExt::from_any_base(
     dislike
       .object()
       .to_owned()
diff --git a/lemmy_apub/src/activities/receive/private_message.rs b/lemmy_apub/src/activities/receive/private_message.rs
index 31037d1f..8f1c95b9 100644
--- a/lemmy_apub/src/activities/receive/private_message.rs
+++ b/lemmy_apub/src/activities/receive/private_message.rs
@@ -4,11 +4,12 @@ use crate::{
   fetcher::get_or_fetch_and_upsert_user,
   inbox::get_activity_to_and_cc,
   FromApub,
+  NoteExt,
 };
 use activitystreams::{
   activity::{ActorAndObjectRefExt, Create, Delete, Undo, Update},
   base::{AsBase, ExtendsExt},
-  object::{AsObject, Note},
+  object::AsObject,
   public,
 };
 use anyhow::{anyhow, Context};
@@ -30,7 +31,7 @@ pub(crate) async fn receive_create_private_message(
 ) -> Result<(), LemmyError> {
   check_private_message_activity_valid(&create, context, request_counter).await?;
 
-  let note = Note::from_any_base(
+  let note = NoteExt::from_any_base(
     create
       .object()
       .as_one()
@@ -79,7 +80,7 @@ pub(crate) async fn receive_update_private_message(
     .as_one()
     .context(location_info!())?
     .to_owned();
-  let note = Note::from_any_base(object)?.context(location_info!())?;
+  let note = NoteExt::from_any_base(object)?.context(location_info!())?;
 
   let private_message_form =
     PrivateMessageForm::from_apub(&note, context, Some(expected_domain), request_counter).await?;
diff --git a/lemmy_apub/src/fetcher.rs b/lemmy_apub/src/fetcher.rs
index b4598ea3..ec44bce1 100644
--- a/lemmy_apub/src/fetcher.rs
+++ b/lemmy_apub/src/fetcher.rs
@@ -3,11 +3,12 @@ use crate::{
   ActorType,
   FromApub,
   GroupExt,
+  NoteExt,
   PageExt,
   PersonExt,
   APUB_JSON_CONTENT_TYPE,
 };
-use activitystreams::{base::BaseExt, collection::OrderedCollection, object::Note, prelude::*};
+use activitystreams::{base::BaseExt, collection::OrderedCollection, prelude::*};
 use anyhow::{anyhow, Context};
 use chrono::NaiveDateTime;
 use diesel::result::Error::NotFound;
@@ -91,7 +92,7 @@ enum SearchAcceptedObjects {
   Person(Box<PersonExt>),
   Group(Box<GroupExt>),
   Page(Box<PageExt>),
-  Comment(Box<Note>),
+  Comment(Box<NoteExt>),
 }
 
 /// Attempt to parse the query as URL, and fetch an ActivityPub object from it.
@@ -488,7 +489,7 @@ pub(crate) async fn get_or_fetch_and_insert_comment(
         comment_ap_id
       );
       let comment =
-        fetch_remote_object::<Note>(context.client(), comment_ap_id, recursion_counter).await?;
+        fetch_remote_object::<NoteExt>(context.client(), comment_ap_id, recursion_counter).await?;
       let comment_form = CommentForm::from_apub(
         &comment,
         context,
diff --git a/lemmy_apub/src/lib.rs b/lemmy_apub/src/lib.rs
index 4894b036..2e3f7bfc 100644
--- a/lemmy_apub/src/lib.rs
+++ b/lemmy_apub/src/lib.rs
@@ -18,7 +18,7 @@ use activitystreams::{
   activity::Follow,
   actor::{ApActor, Group, Person},
   base::AnyBase,
-  object::{Page, Tombstone},
+  object::{ApObject, Note, Page, Tombstone},
 };
 use activitystreams_ext::{Ext1, Ext2};
 use anyhow::{anyhow, Context};
@@ -31,11 +31,12 @@ use std::net::IpAddr;
 use url::{ParseError, Url};
 
 /// Activitystreams type for community
-type GroupExt = Ext2<ApActor<Group>, GroupExtension, PublicKeyExtension>;
+type GroupExt = Ext2<ApActor<ApObject<Group>>, GroupExtension, PublicKeyExtension>;
 /// Activitystreams type for user
-type PersonExt = Ext1<ApActor<Person>, PublicKeyExtension>;
+type PersonExt = Ext1<ApActor<ApObject<Person>>, PublicKeyExtension>;
 /// Activitystreams type for post
-type PageExt = Ext1<Page, PageExtension>;
+type PageExt = Ext1<ApObject<Page>, PageExtension>;
+type NoteExt = ApObject<Note>;
 
 pub static APUB_JSON_CONTENT_TYPE: &str = "application/activity+json";
 
diff --git a/lemmy_apub/src/objects/comment.rs b/lemmy_apub/src/objects/comment.rs
index ca0b0e85..c3d17e6e 100644
--- a/lemmy_apub/src/objects/comment.rs
+++ b/lemmy_apub/src/objects/comment.rs
@@ -4,12 +4,18 @@ use crate::{
     get_or_fetch_and_insert_post,
     get_or_fetch_and_upsert_user,
   },
-  objects::{check_object_domain, create_tombstone},
+  objects::{
+    check_object_domain,
+    create_tombstone,
+    get_source_markdown_value,
+    set_content_and_source,
+  },
   FromApub,
+  NoteExt,
   ToApub,
 };
 use activitystreams::{
-  object::{kind::NoteType, Note, Tombstone},
+  object::{kind::NoteType, ApObject, Note, Tombstone},
   prelude::*,
 };
 use anyhow::Context;
@@ -32,10 +38,10 @@ use url::Url;
 
 #[async_trait::async_trait(?Send)]
 impl ToApub for Comment {
-  type ApubType = Note;
+  type ApubType = NoteExt;
 
-  async fn to_apub(&self, pool: &DbPool) -> Result<Note, LemmyError> {
-    let mut comment = Note::new();
+  async fn to_apub(&self, pool: &DbPool) -> Result<NoteExt, LemmyError> {
+    let mut comment = ApObject::new(Note::new());
 
     let creator_id = self.creator_id;
     let creator = blocking(pool, move |conn| User_::read(conn, creator_id)).await??;
@@ -63,9 +69,10 @@ impl ToApub for Comment {
       .set_published(convert_datetime(self.published))
       .set_to(community.actor_id)
       .set_many_in_reply_tos(in_reply_to_vec)
-      .set_content(self.content.to_owned())
       .set_attributed_to(creator.actor_id);
 
+    set_content_and_source(&mut comment, &self.content)?;
+
     if let Some(u) = self.updated {
       comment.set_updated(convert_datetime(u));
     }
@@ -80,13 +87,13 @@ impl ToApub for Comment {
 
 #[async_trait::async_trait(?Send)]
 impl FromApub for CommentForm {
-  type ApubType = Note;
+  type ApubType = NoteExt;
 
   /// Converts a `Note` to `CommentForm`.
   ///
   /// If the parent community, post and comment(s) are not known locally, these are also fetched.
   async fn from_apub(
-    note: &Note,
+    note: &NoteExt,
     context: &LemmyContext,
     expected_domain: Option<Url>,
     request_counter: &mut i32,
@@ -124,12 +131,8 @@ impl FromApub for CommentForm {
       }
       None => None,
     };
-    let content = note
-      .content()
-      .context(location_info!())?
-      .as_single_xsd_string()
-      .context(location_info!())?
-      .to_string();
+
+    let content = get_source_markdown_value(note)?.context(location_info!())?;
     let content_slurs_removed = remove_slurs(&content);
 
     Ok(CommentForm {
diff --git a/lemmy_apub/src/objects/community.rs b/lemmy_apub/src/objects/community.rs
index d697c70b..c5a614ba 100644
--- a/lemmy_apub/src/objects/community.rs
+++ b/lemmy_apub/src/objects/community.rs
@@ -1,7 +1,12 @@
 use crate::{
   extensions::group_extensions::GroupExtension,
   fetcher::get_or_fetch_and_upsert_user,
-  objects::{check_object_domain, create_tombstone},
+  objects::{
+    check_object_domain,
+    create_tombstone,
+    get_source_markdown_value,
+    set_content_and_source,
+  },
   ActorType,
   FromApub,
   GroupExt,
@@ -10,7 +15,7 @@ use crate::{
 use activitystreams::{
   actor::{kind::GroupType, ApActor, Endpoints, Group},
   base::BaseExt,
-  object::{Image, Tombstone},
+  object::{ApObject, Image, Tombstone},
   prelude::*,
 };
 use activitystreams_ext::Ext2;
@@ -46,7 +51,7 @@ impl ToApub for Community {
     .await??;
     let moderators: Vec<String> = moderators.into_iter().map(|m| m.user_actor_id).collect();
 
-    let mut group = Group::new();
+    let mut group = ApObject::new(Group::new());
     group
       .set_context(activitystreams::context())
       .set_id(Url::parse(&self.actor_id)?)
@@ -58,9 +63,7 @@ impl ToApub for Community {
       group.set_updated(convert_datetime(u));
     }
     if let Some(d) = self.description.to_owned() {
-      // TODO: this should be html, also add source field with raw markdown
-      //       -> same for post.content and others
-      group.set_content(d);
+      set_content_and_source(&mut group, &d)?;
     }
 
     if let Some(icon_url) = &self.icon {
@@ -138,14 +141,9 @@ impl FromApub for CommunityForm {
       .as_xsd_string()
       .context(location_info!())?
       .to_string();
-    // TODO: should be parsed as html and tags like <script> removed (or use markdown source)
-    //       -> same for post.content etc
-    let description = group
-      .inner
-      .content()
-      .map(|s| s.as_single_xsd_string())
-      .flatten()
-      .map(|s| s.to_string());
+
+    let description = get_source_markdown_value(group)?;
+
     check_slurs(&name)?;
     check_slurs(&title)?;
     check_slurs_opt(&description)?;
diff --git a/lemmy_apub/src/objects/mod.rs b/lemmy_apub/src/objects/mod.rs
index 8fd0e567..0ae99877 100644
--- a/lemmy_apub/src/objects/mod.rs
+++ b/lemmy_apub/src/objects/mod.rs
@@ -1,12 +1,17 @@
 use crate::check_is_apub_id_valid;
 use activitystreams::{
-  base::{AsBase, BaseExt},
+  base::{AsBase, BaseExt, ExtendsExt},
   markers::Base,
-  object::{Tombstone, TombstoneExt},
+  mime::{FromStrError, Mime},
+  object::{ApObjectExt, Object, ObjectExt, Tombstone, TombstoneExt},
 };
 use anyhow::{anyhow, Context};
 use chrono::NaiveDateTime;
-use lemmy_utils::{location_info, utils::convert_datetime, LemmyError};
+use lemmy_utils::{
+  location_info,
+  utils::{convert_datetime, markdown_to_html},
+  LemmyError,
+};
 use url::Url;
 
 pub(crate) mod comment;
@@ -58,3 +63,60 @@ where
   };
   Ok(actor_id.to_string())
 }
+
+pub(in crate::objects) fn set_content_and_source<T, Kind1, Kind2>(
+  object: &mut T,
+  markdown_text: &str,
+) -> Result<(), LemmyError>
+where
+  T: ApObjectExt<Kind1> + ObjectExt<Kind2>,
+{
+  let mut source = Object::<()>::new_none_type();
+  source
+    .set_content(markdown_text)
+    .set_media_type(mime_markdown()?);
+  object.set_source(source.into_any_base()?);
+  object.set_content(markdown_to_html(markdown_text));
+  Ok(())
+}
+
+pub(in crate::objects) fn get_source_markdown_value<T, Kind1, Kind2>(
+  object: &T,
+) -> Result<Option<String>, LemmyError>
+where
+  T: ApObjectExt<Kind1> + ObjectExt<Kind2>,
+{
+  let content = object
+    .content()
+    .map(|s| s.as_single_xsd_string())
+    .flatten()
+    .map(|s| s.to_string());
+  if content.is_some() {
+    let source = object.source().context(location_info!())?;
+    let source = Object::<()>::from_any_base(source.to_owned())?.context(location_info!())?;
+    check_is_markdown(source.media_type())?;
+    let source_content = source
+      .content()
+      .map(|s| s.as_single_xsd_string())
+      .flatten()
+      .context(location_info!())?
+      .to_string();
+    return Ok(Some(source_content));
+  }
+  Ok(None)
+}
+
+pub(in crate::objects) fn mime_markdown() -> Result<Mime, FromStrError> {
+  "text/markdown".parse()
+}
+
+pub(in crate::objects) fn check_is_markdown(mime: Option<&Mime>) -> Result<(), LemmyError> {
+  let mime = mime.context(location_info!())?;
+  if !mime.eq(&mime_markdown()?) {
+    Err(LemmyError::from(anyhow!(
+      "Lemmy only supports markdown content"
+    )))
+  } else {
+    Ok(())
+  }
+}
diff --git a/lemmy_apub/src/objects/post.rs b/lemmy_apub/src/objects/post.rs
index 6b42e690..f2bb9add 100644
--- a/lemmy_apub/src/objects/post.rs
+++ b/lemmy_apub/src/objects/post.rs
@@ -1,13 +1,18 @@
 use crate::{
   extensions::page_extension::PageExtension,
   fetcher::{get_or_fetch_and_upsert_community, get_or_fetch_and_upsert_user},
-  objects::{check_object_domain, create_tombstone},
+  objects::{
+    check_object_domain,
+    create_tombstone,
+    get_source_markdown_value,
+    set_content_and_source,
+  },
   FromApub,
   PageExt,
   ToApub,
 };
 use activitystreams::{
-  object::{kind::PageType, Image, Page, Tombstone},
+  object::{kind::PageType, ApObject, Image, Page, Tombstone},
   prelude::*,
 };
 use activitystreams_ext::Ext1;
@@ -35,7 +40,7 @@ impl ToApub for Post {
 
   // Turn a Lemmy post into an ActivityPub page that can be sent out over the network.
   async fn to_apub(&self, pool: &DbPool) -> Result<PageExt, LemmyError> {
-    let mut page = Page::new();
+    let mut page = ApObject::new(Page::new());
 
     let creator_id = self.creator_id;
     let creator = blocking(pool, move |conn| User_::read(conn, creator_id)).await??;
@@ -57,7 +62,7 @@ impl ToApub for Post {
       .set_attributed_to(creator.actor_id);
 
     if let Some(body) = &self.body {
-      page.set_content(body.to_owned());
+      set_content_and_source(&mut page, &body)?;
     }
 
     // TODO: hacky code because we get self.url == Some("")
@@ -162,13 +167,8 @@ impl FromApub for PostForm {
       .as_single_xsd_string()
       .context(location_info!())?
       .to_string();
-    let body = page
-      .inner
-      .content()
-      .as_ref()
-      .map(|c| c.as_single_xsd_string())
-      .flatten()
-      .map(|s| s.to_string());
+    let body = get_source_markdown_value(page)?;
+
     check_slurs(&name)?;
     let body_slurs_removed = body.map(|b| remove_slurs(&b));
     Ok(PostForm {
diff --git a/lemmy_apub/src/objects/private_message.rs b/lemmy_apub/src/objects/private_message.rs
index 64047963..1a3b1587 100644
--- a/lemmy_apub/src/objects/private_message.rs
+++ b/lemmy_apub/src/objects/private_message.rs
@@ -1,12 +1,18 @@
 use crate::{
   check_is_apub_id_valid,
   fetcher::get_or_fetch_and_upsert_user,
-  objects::{check_object_domain, create_tombstone},
+  objects::{
+    check_object_domain,
+    create_tombstone,
+    get_source_markdown_value,
+    set_content_and_source,
+  },
   FromApub,
+  NoteExt,
   ToApub,
 };
 use activitystreams::{
-  object::{kind::NoteType, Note, Tombstone},
+  object::{kind::NoteType, ApObject, Note, Tombstone},
   prelude::*,
 };
 use anyhow::Context;
@@ -23,10 +29,10 @@ use url::Url;
 
 #[async_trait::async_trait(?Send)]
 impl ToApub for PrivateMessage {
-  type ApubType = Note;
+  type ApubType = NoteExt;
 
-  async fn to_apub(&self, pool: &DbPool) -> Result<Note, LemmyError> {
-    let mut private_message = Note::new();
+  async fn to_apub(&self, pool: &DbPool) -> Result<NoteExt, LemmyError> {
+    let mut private_message = ApObject::new(Note::new());
 
     let creator_id = self.creator_id;
     let creator = blocking(pool, move |conn| User_::read(conn, creator_id)).await??;
@@ -38,10 +44,11 @@ impl ToApub for PrivateMessage {
       .set_context(activitystreams::context())
       .set_id(Url::parse(&self.ap_id.to_owned())?)
       .set_published(convert_datetime(self.published))
-      .set_content(self.content.to_owned())
       .set_to(recipient.actor_id)
       .set_attributed_to(creator.actor_id);
 
+    set_content_and_source(&mut private_message, &self.content)?;
+
     if let Some(u) = self.updated {
       private_message.set_updated(convert_datetime(u));
     }
@@ -56,10 +63,10 @@ impl ToApub for PrivateMessage {
 
 #[async_trait::async_trait(?Send)]
 impl FromApub for PrivateMessageForm {
-  type ApubType = Note;
+  type ApubType = NoteExt;
 
   async fn from_apub(
-    note: &Note,
+    note: &NoteExt,
     context: &LemmyContext,
     expected_domain: Option<Url>,
     request_counter: &mut i32,
@@ -83,15 +90,12 @@ impl FromApub for PrivateMessageForm {
     let ap_id = note.id_unchecked().context(location_info!())?.to_string();
     check_is_apub_id_valid(&Url::parse(&ap_id)?)?;
 
+    let content = get_source_markdown_value(note)?.context(location_info!())?;
+
     Ok(PrivateMessageForm {
       creator_id: creator.id,
       recipient_id: recipient.id,
-      content: note
-        .content()
-        .context(location_info!())?
-        .as_single_xsd_string()
-        .context(location_info!())?
-        .to_string(),
+      content,
       published: note.published().map(|u| u.to_owned().naive_local()),
       updated: note.updated().map(|u| u.to_owned().naive_local()),
       deleted: None,
diff --git a/lemmy_apub/src/objects/user.rs b/lemmy_apub/src/objects/user.rs
index 49b7c9e5..5ec283cf 100644
--- a/lemmy_apub/src/objects/user.rs
+++ b/lemmy_apub/src/objects/user.rs
@@ -1,7 +1,13 @@
-use crate::{objects::check_object_domain, ActorType, FromApub, PersonExt, ToApub};
+use crate::{
+  objects::{check_object_domain, get_source_markdown_value, set_content_and_source},
+  ActorType,
+  FromApub,
+  PersonExt,
+  ToApub,
+};
 use activitystreams::{
   actor::{ApActor, Endpoints, Person},
-  object::{Image, Tombstone},
+  object::{ApObject, Image, Tombstone},
   prelude::*,
 };
 use activitystreams_ext::Ext1;
@@ -24,7 +30,7 @@ impl ToApub for User_ {
   type ApubType = PersonExt;
 
   async fn to_apub(&self, _pool: &DbPool) -> Result<PersonExt, LemmyError> {
-    let mut person = Person::new();
+    let mut person = ApObject::new(Person::new());
     person
       .set_context(activitystreams::context())
       .set_id(Url::parse(&self.actor_id)?)
@@ -47,6 +53,8 @@ impl ToApub for User_ {
     }
 
     if let Some(bio) = &self.bio {
+      set_content_and_source(&mut person, bio)?;
+      // Also set summary for compatibility with older Lemmy versions. Remove this after a while.
       person.set_summary(bio.to_owned());
     }
 
@@ -117,14 +125,8 @@ impl FromApub for UserForm {
       .map(|n| n.to_owned().xsd_string())
       .flatten();
 
-    // TODO a limit check (like the API does) might need to be done
-    // here when we federate to other platforms. Same for preferred_username
-    let bio = person
-      .inner
-      .summary()
-      .map(|s| s.as_single_xsd_string())
-      .flatten()
-      .map(|s| s.to_string());
+    let bio = get_source_markdown_value(person)?;
+
     check_slurs(&name)?;
     check_slurs_opt(&preferred_username)?;
     check_slurs_opt(&bio)?;
-- 
2.44.1