# 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",
"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"
"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"
|---|---|---|
| `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 |
"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",
"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"
"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"
}
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"] }
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::{
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 =
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?;
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?;
context: &LemmyContext,
request_counter: &mut i32,
) -> Result<(), LemmyError> {
- let note = Note::from_any_base(
+ let note = NoteExt::from_any_base(
dislike
.object()
.to_owned()
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},
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(¬e, context, None, request_counter).await?;
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()
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};
) -> 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()
.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(¬e, context, Some(expected_domain), request_counter).await?;
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;
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.
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,
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};
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";
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;
#[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??;
.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));
}
#[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,
}
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 {
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,
use activitystreams::{
actor::{kind::GroupType, ApActor, Endpoints, Group},
base::BaseExt,
- object::{Image, Tombstone},
+ object::{ApObject, Image, Tombstone},
prelude::*,
};
use activitystreams_ext::Ext2;
.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)?)
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 {
.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)?;
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;
};
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(())
+ }
+}
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;
// 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??;
.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("")
.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 {
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;
#[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??;
.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));
}
#[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,
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,
-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;
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)?)
}
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());
}
.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)?;