Populate content with HTML, and source with markdown (ref #1220) #141

Merged
dessalines merged 4 commits from apub-media-type2 into main 2020-11-25 19:54:54 +00:00
20 changed files with 341 additions and 120 deletions

View File

@ -1,8 +1,8 @@
# build folders and similar which are not needed for the docker build # build folders and similar which are not needed for the docker build
target target
docker/dev/volumes docker
docker/prod/volumes api_tests
docker/federation/volumes
docker/travis/volumes
.git
ansible ansible
tests
.git
*.sh

4
Cargo.lock generated
View File

@ -2,9 +2,9 @@
# It is not intended for manual editing. # It is not intended for manual editing.
[[package]] [[package]]
name = "activitystreams" name = "activitystreams"
version = "0.7.0-alpha.4" version = "0.7.0-alpha.6"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "261b423734cca2a170d7a76936f1f0f9e6c6fc297d36cfc5ea6aa15f9017f996" checksum = "0b1afe32371e466a791ced0d6ef6e6b97822bb1a279ee4cc41c4324e61cd0b2b"
dependencies = [ dependencies = [
"chrono", "chrono",
"mime", "mime",

View File

@ -1,35 +1,72 @@
# syntax=docker/dockerfile:experimental ARG RUST_BUILDER_IMAGE=ekidd/rust-musl-builder:1.47.0
FROM rust:1.47-buster as rust
ENV HOME=/home/root # Cargo chef plan
FROM $RUST_BUILDER_IMAGE as planner
WORKDIR /app
RUN cargo install cargo-chef --version 0.1.6
# Copy dirs
COPY ./ ./
RUN sudo chown -R rust:rust .
RUN cargo chef prepare --recipe-path recipe.json
# Cargo chef cache dependencies
FROM $RUST_BUILDER_IMAGE as cacher
ARG CARGO_BUILD_TARGET=x86_64-unknown-linux-musl
WORKDIR /app
RUN cargo install cargo-chef --version 0.1.6
COPY --from=planner /app/recipe.json ./recipe.json
RUN sudo chown -R rust:rust .
RUN cargo chef cook --target ${CARGO_BUILD_TARGET} --recipe-path recipe.json
# Build the project
FROM $RUST_BUILDER_IMAGE as builder
ARG CARGO_BUILD_TARGET=x86_64-unknown-linux-musl
ARG RUSTRELEASEDIR="debug"
WORKDIR /app WORKDIR /app
# Copy the source folders # Copy over the cached dependencies
COPY . ./ COPY --from=cacher /app/target target
COPY --from=cacher /home/rust/.cargo /home/rust/.cargo
# Build for debug # Copy the rest of the dirs
RUN --mount=type=cache,target=/usr/local/cargo/registry \ COPY ./ ./
--mount=type=cache,target=/app/target \
cargo build
RUN --mount=type=cache,target=/app/target \
cp target/debug/lemmy_server lemmy_server
FROM peaceiris/mdbook:v0.3.7 as docs RUN sudo chown -R rust:rust .
RUN cargo build
# reduce binary size
RUN strip ./target/$CARGO_BUILD_TARGET/$RUSTRELEASEDIR/lemmy_server
RUN cp ./target/$CARGO_BUILD_TARGET/$RUSTRELEASEDIR/lemmy_server /app/lemmy_server
# Build the docs
FROM $RUST_BUILDER_IMAGE as docs
WORKDIR /app WORKDIR /app
COPY docs ./docs COPY --chown=rust:rust docs ./docs
RUN mdbook build docs/ RUN mdbook build docs/
FROM ubuntu:20.10 # The alpine runner
FROM alpine:3.12 as lemmy
# Install libpq for postgres and espeak # Install libpq for postgres
RUN apt-get update -y RUN apk add libpq
RUN apt-get install -y libpq-dev espeak
# Install Espeak for captchas
RUN apk add espeak
RUN addgroup -g 1000 lemmy
RUN adduser -D -s /bin/sh -u 1000 -G lemmy lemmy
# Copy resources # Copy resources
COPY config/defaults.hjson /config/defaults.hjson COPY --chown=lemmy:lemmy config/defaults.hjson /config/defaults.hjson
COPY --from=rust /app/lemmy_server /app/lemmy COPY --chown=lemmy:lemmy --from=builder /app/lemmy_server /app/lemmy
COPY --from=docs /app/docs/book/ /app/documentation/ COPY --chown=lemmy:lemmy --from=docs /app/docs/book/ /app/documentation/
RUN chown lemmy:lemmy /app/lemmy
USER lemmy
EXPOSE 8536 EXPOSE 8536
CMD ["/app/lemmy"] CMD ["/app/lemmy"]

View File

@ -1,4 +1,8 @@
#!/bin/sh #!/bin/sh
# This script uses a docker file that builds with musl, and runs on linux alpine
# Its a bit slower for development than the volume mount.
set -e set -e
mkdir -p volumes/pictrs mkdir -p volumes/pictrs

View File

@ -0,0 +1,11 @@
#!/bin/sh
set -e
# This script uses a Dockerfile that takes advantage of docker volume mounts,
# And runs on an ubuntu image. A little faster for development than the other
# script
mkdir -p volumes/pictrs
sudo chown -R 991:991 volumes/pictrs
sudo docker build ../../ --file ../dev/volume_mount.dockerfile -t lemmy-dev:latest
sudo docker-compose up -d

View File

@ -0,0 +1,35 @@
# syntax=docker/dockerfile:experimental
FROM rust:1.47-buster as rust
ENV HOME=/home/root
WORKDIR /app
# Copy the source folders
COPY . ./
# Build for debug
RUN --mount=type=cache,target=/usr/local/cargo/registry \
--mount=type=cache,target=/app/target \
cargo build
RUN --mount=type=cache,target=/app/target \
cp target/debug/lemmy_server lemmy_server
FROM peaceiris/mdbook:v0.3.7 as docs
WORKDIR /app
COPY docs ./docs
RUN mdbook build docs/
FROM ubuntu:20.10
# Install libpq for postgres and espeak
RUN apt-get update -y
RUN apt-get install -y libpq-dev espeak
# Copy resources
COPY config/defaults.hjson /config/defaults.hjson
COPY --from=rust /app/lemmy_server /app/lemmy
COPY --from=docs /app/docs/book/ /app/documentation/
EXPOSE 8536
CMD ["/app/lemmy"]

View File

@ -1,25 +1,55 @@
ARG RUST_BUILDER_IMAGE=ekidd/rust-musl-builder:stable ARG RUST_BUILDER_IMAGE=ekidd/rust-musl-builder:1.47.0
FROM $RUST_BUILDER_IMAGE as rust # Cargo chef plan
FROM $RUST_BUILDER_IMAGE as planner
WORKDIR /app
RUN cargo install cargo-chef --version 0.1.6
# Copy dirs
COPY ./ ./
RUN sudo chown -R rust:rust .
RUN cargo chef prepare --recipe-path recipe.json
# Cargo chef cache dependencies
FROM $RUST_BUILDER_IMAGE as cacher
ARG CARGO_BUILD_TARGET=x86_64-unknown-linux-musl
WORKDIR /app
RUN cargo install cargo-chef --version 0.1.6
COPY --from=planner /app/recipe.json ./recipe.json
RUN sudo chown -R rust:rust .
RUN cargo chef cook --release --target ${CARGO_BUILD_TARGET} --recipe-path recipe.json
# Build the project
FROM $RUST_BUILDER_IMAGE as builder
ARG CARGO_BUILD_TARGET=x86_64-unknown-linux-musl ARG CARGO_BUILD_TARGET=x86_64-unknown-linux-musl
ARG RUSTRELEASEDIR="release" ARG RUSTRELEASEDIR="release"
WORKDIR /app/server WORKDIR /app
# Copy over the cached dependencies
COPY --from=cacher /app/target target
COPY --from=cacher /home/rust/.cargo /home/rust/.cargo
# Copy the rest of the dirs
COPY ./ ./
RUN sudo chown -R rust:rust . RUN sudo chown -R rust:rust .
COPY . ./
RUN cargo build --release RUN cargo build --release
# reduce binary size # reduce binary size
RUN strip ./target/$CARGO_BUILD_TARGET/$RUSTRELEASEDIR/lemmy_server RUN strip ./target/$CARGO_BUILD_TARGET/$RUSTRELEASEDIR/lemmy_server
RUN cp ./target/$CARGO_BUILD_TARGET/$RUSTRELEASEDIR/lemmy_server /app/server/ RUN cp ./target/$CARGO_BUILD_TARGET/$RUSTRELEASEDIR/lemmy_server /app/lemmy_server
# Build the docs
FROM $RUST_BUILDER_IMAGE as docs FROM $RUST_BUILDER_IMAGE as docs
WORKDIR /app WORKDIR /app
COPY --chown=rust:rust docs ./docs COPY --chown=rust:rust docs ./docs
RUN mdbook build docs/ RUN mdbook build docs/
# The alpine runner
FROM alpine:3.12 as lemmy FROM alpine:3.12 as lemmy
# Install libpq for postgres # Install libpq for postgres
@ -33,7 +63,7 @@ RUN adduser -D -s /bin/sh -u 1000 -G lemmy lemmy
# Copy resources # Copy resources
COPY --chown=lemmy:lemmy config/defaults.hjson /config/defaults.hjson COPY --chown=lemmy:lemmy config/defaults.hjson /config/defaults.hjson
COPY --chown=lemmy:lemmy --from=rust /app/server/lemmy_server /app/lemmy COPY --chown=lemmy:lemmy --from=builder /app/lemmy_server /app/lemmy
COPY --chown=lemmy:lemmy --from=docs /app/docs/book/ /app/documentation/ COPY --chown=lemmy:lemmy --from=docs /app/docs/book/ /app/documentation/
RUN chown lemmy:lemmy /app/lemmy RUN chown lemmy:lemmy /app/lemmy

View File

@ -64,6 +64,10 @@ Receives activities from user: `Follow`, `Undo/Follow`, `Create`, `Update`, `Lik
"https://enterprise.lemmy.ml/u/riker" "https://enterprise.lemmy.ml/u/riker"
], ],
"content": "Welcome to the default community!", "content": "Welcome to the default community!",
"source": {
"content": "Welcome to the default community!",
"mediaType": "text/markdown"
},
"icon": { "icon": {
"type": "Image", "type": "Image",
"url": "https://enterprise.lemmy.ml/pictrs/image/Z8pFFb21cl.png" "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", "type": "Person",
"preferredUsername": "picard", "preferredUsername": "picard",
"name": "Jean-Luc Picard", "name": "Jean-Luc Picard",
"summary": "The user bio", "content": "The user bio",
"source": {
"content": "The user bio",
"mediaType": "text/markdown"
},
"icon": { "icon": {
"type": "Image", "type": "Image",
"url": "https://enterprise.lemmy.ml/pictrs/image/DS3q0colRA.jpg" "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 | | `preferredUsername` | yes | Name of the actor |
| `name` | no | The user's displayname | | `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 | | `icon` | no | The user's avatar, shown next to the username |
| `image` | no | The user's banner, shown on top of the profile | | `image` | no | The user's banner, shown on top of the profile |
| `inbox` | no | ActivityPub inbox URL | | `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", "to": "https://voyager.lemmy.ml/c/main",
"summary": "Test thumbnail 2", "summary": "Test thumbnail 2",
"content": "blub blub", "content": "blub blub",
"source": {
"content": "blub blub",
"mediaType": "text/markdown"
},
"url": "https://voyager.lemmy.ml:/pictrs/image/fzGwCsq7BJ.jpg", "url": "https://voyager.lemmy.ml:/pictrs/image/fzGwCsq7BJ.jpg",
"image": { "image": {
"type": "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", "attributedTo": "https://enterprise.lemmy.ml/u/picard",
"to": "https://enterprise.lemmy.ml/c/main", "to": "https://enterprise.lemmy.ml/c/main",
"content": "mmmk", "content": "mmmk",
"source": {
"content": "mmmk",
"mediaType": "text/markdown"
},
"inReplyTo": [ "inReplyTo": [
"https://enterprise.lemmy.ml/post/38", "https://enterprise.lemmy.ml/post/38",
"https://voyager.lemmy.ml/comment/73" "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", "attributedTo": "https://enterprise.lemmy.ml/u/picard",
"to": "https://voyager.lemmy.ml/u/janeway", "to": "https://voyager.lemmy.ml/u/janeway",
"content": "test", "content": "test",
"source": {
"content": "test",
"mediaType": "text/markdown"
},
"mediaType": "text/markdown",
"published": "2020-10-08T19:10:46.542820+00:00", "published": "2020-10-08T19:10:46.542820+00:00",
"updated": "2020-10-08T20:13:52.547156+00:00" "updated": "2020-10-08T20:13:52.547156+00:00"
} }

View File

@ -14,7 +14,7 @@ lemmy_db = { path = "../lemmy_db" }
lemmy_structs = { path = "../lemmy_structs" } lemmy_structs = { path = "../lemmy_structs" }
lemmy_websocket = { path = "../lemmy_websocket" } lemmy_websocket = { path = "../lemmy_websocket" }
diesel = "1.4" diesel = "1.4"
activitystreams = "0.7.0-alpha.4" activitystreams = "0.7.0-alpha.6"
activitystreams-ext = "0.1.0-alpha.2" activitystreams-ext = "0.1.0-alpha.2"
bcrypt = "0.8" bcrypt = "0.8"
chrono = { version = "0.4", features = ["serde"] } chrono = { version = "0.4", features = ["serde"] }

View File

@ -3,11 +3,11 @@ use crate::{
fetcher::get_or_fetch_and_insert_comment, fetcher::get_or_fetch_and_insert_comment,
ActorType, ActorType,
FromApub, FromApub,
NoteExt,
}; };
use activitystreams::{ use activitystreams::{
activity::{ActorAndObjectRefExt, Create, Dislike, Like, Remove, Update}, activity::{ActorAndObjectRefExt, Create, Dislike, Like, Remove, Update},
base::ExtendsExt, base::ExtendsExt,
object::Note,
}; };
use anyhow::{anyhow, Context}; use anyhow::{anyhow, Context};
use lemmy_db::{ use lemmy_db::{
@ -27,7 +27,7 @@ pub(crate) async fn receive_create_comment(
request_counter: &mut i32, request_counter: &mut i32,
) -> Result<(), LemmyError> { ) -> Result<(), LemmyError> {
let user = get_actor_as_user(&create, context, request_counter).await?; 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!())?; .context(location_info!())?;
let comment = let comment =
@ -83,7 +83,7 @@ pub(crate) async fn receive_update_comment(
context: &LemmyContext, context: &LemmyContext,
request_counter: &mut i32, request_counter: &mut i32,
) -> Result<(), LemmyError> { ) -> 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!())?; .context(location_info!())?;
let user = get_actor_as_user(&update, context, request_counter).await?; let user = get_actor_as_user(&update, context, request_counter).await?;
@ -140,7 +140,7 @@ pub(crate) async fn receive_like_comment(
context: &LemmyContext, context: &LemmyContext,
request_counter: &mut i32, request_counter: &mut i32,
) -> Result<(), LemmyError> { ) -> 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!())?; .context(location_info!())?;
let user = get_actor_as_user(&like, context, request_counter).await?; let user = get_actor_as_user(&like, context, request_counter).await?;
@ -191,7 +191,7 @@ pub(crate) async fn receive_dislike_comment(
context: &LemmyContext, context: &LemmyContext,
request_counter: &mut i32, request_counter: &mut i32,
) -> Result<(), LemmyError> { ) -> Result<(), LemmyError> {
let note = Note::from_any_base( let note = NoteExt::from_any_base(
dislike dislike
.object() .object()
.to_owned() .to_owned()

View File

@ -2,8 +2,9 @@ use crate::{
activities::receive::get_actor_as_user, activities::receive::get_actor_as_user,
fetcher::get_or_fetch_and_insert_comment, fetcher::get_or_fetch_and_insert_comment,
FromApub, FromApub,
NoteExt,
}; };
use activitystreams::{activity::*, object::Note, prelude::*}; use activitystreams::{activity::*, prelude::*};
use anyhow::Context; use anyhow::Context;
use lemmy_db::{ use lemmy_db::{
comment::{Comment, CommentForm, CommentLike}, comment::{Comment, CommentForm, CommentLike},
@ -20,7 +21,7 @@ pub(crate) async fn receive_undo_like_comment(
request_counter: &mut i32, request_counter: &mut i32,
) -> Result<(), LemmyError> { ) -> Result<(), LemmyError> {
let user = get_actor_as_user(like, context, request_counter).await?; 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!())?; .context(location_info!())?;
let comment = CommentForm::from_apub(&note, context, None, request_counter).await?; 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, request_counter: &mut i32,
) -> Result<(), LemmyError> { ) -> Result<(), LemmyError> {
let user = get_actor_as_user(dislike, context, request_counter).await?; let user = get_actor_as_user(dislike, context, request_counter).await?;
let note = Note::from_any_base( let note = NoteExt::from_any_base(
dislike dislike
.object() .object()
.to_owned() .to_owned()

View File

@ -4,11 +4,12 @@ use crate::{
fetcher::get_or_fetch_and_upsert_user, fetcher::get_or_fetch_and_upsert_user,
inbox::get_activity_to_and_cc, inbox::get_activity_to_and_cc,
FromApub, FromApub,
NoteExt,
}; };
use activitystreams::{ use activitystreams::{
activity::{ActorAndObjectRefExt, Create, Delete, Undo, Update}, activity::{ActorAndObjectRefExt, Create, Delete, Undo, Update},
base::{AsBase, ExtendsExt}, base::{AsBase, ExtendsExt},
object::{AsObject, Note}, object::AsObject,
public, public,
}; };
use anyhow::{anyhow, Context}; use anyhow::{anyhow, Context};
@ -30,7 +31,7 @@ pub(crate) async fn receive_create_private_message(
) -> Result<(), LemmyError> { ) -> Result<(), LemmyError> {
check_private_message_activity_valid(&create, context, request_counter).await?; check_private_message_activity_valid(&create, context, request_counter).await?;
let note = Note::from_any_base( let note = NoteExt::from_any_base(
create create
.object() .object()
.as_one() .as_one()
@ -79,7 +80,7 @@ pub(crate) async fn receive_update_private_message(
.as_one() .as_one()
.context(location_info!())? .context(location_info!())?
.to_owned(); .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 = let private_message_form =
PrivateMessageForm::from_apub(&note, context, Some(expected_domain), request_counter).await?; PrivateMessageForm::from_apub(&note, context, Some(expected_domain), request_counter).await?;

View File

@ -3,11 +3,12 @@ use crate::{
ActorType, ActorType,
FromApub, FromApub,
GroupExt, GroupExt,
NoteExt,
PageExt, PageExt,
PersonExt, PersonExt,
APUB_JSON_CONTENT_TYPE, 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 anyhow::{anyhow, Context};
use chrono::NaiveDateTime; use chrono::NaiveDateTime;
use diesel::result::Error::NotFound; use diesel::result::Error::NotFound;
@ -91,7 +92,7 @@ enum SearchAcceptedObjects {
Person(Box<PersonExt>), Person(Box<PersonExt>),
Group(Box<GroupExt>), Group(Box<GroupExt>),
Page(Box<PageExt>), Page(Box<PageExt>),
Comment(Box<Note>), Comment(Box<NoteExt>),
} }
/// Attempt to parse the query as URL, and fetch an ActivityPub object from it. /// 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 comment_ap_id
); );
let comment = 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( let comment_form = CommentForm::from_apub(
&comment, &comment,
context, context,

View File

@ -18,7 +18,7 @@ use activitystreams::{
activity::Follow, activity::Follow,
actor::{ApActor, Group, Person}, actor::{ApActor, Group, Person},
base::AnyBase, base::AnyBase,
object::{Page, Tombstone}, object::{ApObject, Note, Page, Tombstone},
}; };
use activitystreams_ext::{Ext1, Ext2}; use activitystreams_ext::{Ext1, Ext2};
use anyhow::{anyhow, Context}; use anyhow::{anyhow, Context};
@ -31,11 +31,12 @@ use std::net::IpAddr;
use url::{ParseError, Url}; use url::{ParseError, Url};
/// Activitystreams type for community /// Activitystreams type for community
type GroupExt = Ext2<ApActor<Group>, GroupExtension, PublicKeyExtension>; type GroupExt = Ext2<ApActor<ApObject<Group>>, GroupExtension, PublicKeyExtension>;
/// Activitystreams type for user /// Activitystreams type for user
type PersonExt = Ext1<ApActor<Person>, PublicKeyExtension>; type PersonExt = Ext1<ApActor<ApObject<Person>>, PublicKeyExtension>;
/// Activitystreams type for post /// 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"; pub static APUB_JSON_CONTENT_TYPE: &str = "application/activity+json";

View File

@ -4,12 +4,18 @@ use crate::{
get_or_fetch_and_insert_post, get_or_fetch_and_insert_post,
get_or_fetch_and_upsert_user, 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, FromApub,
NoteExt,
ToApub, ToApub,
}; };
use activitystreams::{ use activitystreams::{
object::{kind::NoteType, Note, Tombstone}, object::{kind::NoteType, ApObject, Note, Tombstone},
prelude::*, prelude::*,
}; };
use anyhow::Context; use anyhow::Context;
@ -32,10 +38,10 @@ use url::Url;
#[async_trait::async_trait(?Send)] #[async_trait::async_trait(?Send)]
impl ToApub for Comment { impl ToApub for Comment {
type ApubType = Note; type ApubType = NoteExt;
async fn to_apub(&self, pool: &DbPool) -> Result<Note, LemmyError> { async fn to_apub(&self, pool: &DbPool) -> Result<NoteExt, LemmyError> {
let mut comment = Note::new(); let mut comment = ApObject::new(Note::new());
let creator_id = self.creator_id; let creator_id = self.creator_id;
let creator = blocking(pool, move |conn| User_::read(conn, creator_id)).await??; 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_published(convert_datetime(self.published))
.set_to(community.actor_id) .set_to(community.actor_id)
.set_many_in_reply_tos(in_reply_to_vec) .set_many_in_reply_tos(in_reply_to_vec)
.set_content(self.content.to_owned())
.set_attributed_to(creator.actor_id); .set_attributed_to(creator.actor_id);
set_content_and_source(&mut comment, &self.content)?;
if let Some(u) = self.updated { if let Some(u) = self.updated {
comment.set_updated(convert_datetime(u)); comment.set_updated(convert_datetime(u));
} }
@ -80,13 +87,13 @@ impl ToApub for Comment {
#[async_trait::async_trait(?Send)] #[async_trait::async_trait(?Send)]
impl FromApub for CommentForm { impl FromApub for CommentForm {
type ApubType = Note; type ApubType = NoteExt;
/// Converts a `Note` to `CommentForm`. /// Converts a `Note` to `CommentForm`.
/// ///
/// If the parent community, post and comment(s) are not known locally, these are also fetched. /// If the parent community, post and comment(s) are not known locally, these are also fetched.
async fn from_apub( async fn from_apub(
note: &Note, note: &NoteExt,
context: &LemmyContext, context: &LemmyContext,
expected_domain: Option<Url>, expected_domain: Option<Url>,
request_counter: &mut i32, request_counter: &mut i32,
@ -124,12 +131,8 @@ impl FromApub for CommentForm {
} }
None => None, None => None,
}; };
let content = note
.content() let content = get_source_markdown_value(note)?.context(location_info!())?;
.context(location_info!())?
.as_single_xsd_string()
.context(location_info!())?
.to_string();
let content_slurs_removed = remove_slurs(&content); let content_slurs_removed = remove_slurs(&content);
Ok(CommentForm { Ok(CommentForm {

View File

@ -1,7 +1,12 @@
use crate::{ use crate::{
extensions::group_extensions::GroupExtension, extensions::group_extensions::GroupExtension,
fetcher::get_or_fetch_and_upsert_user, 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, ActorType,
FromApub, FromApub,
GroupExt, GroupExt,
@ -10,7 +15,7 @@ use crate::{
use activitystreams::{ use activitystreams::{
actor::{kind::GroupType, ApActor, Endpoints, Group}, actor::{kind::GroupType, ApActor, Endpoints, Group},
base::BaseExt, base::BaseExt,
object::{Image, Tombstone}, object::{ApObject, Image, Tombstone},
prelude::*, prelude::*,
}; };
use activitystreams_ext::Ext2; use activitystreams_ext::Ext2;
@ -46,7 +51,7 @@ impl ToApub for Community {
.await??; .await??;
let moderators: Vec<String> = moderators.into_iter().map(|m| m.user_actor_id).collect(); 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 group
.set_context(activitystreams::context()) .set_context(activitystreams::context())
.set_id(Url::parse(&self.actor_id)?) .set_id(Url::parse(&self.actor_id)?)
@ -58,9 +63,7 @@ impl ToApub for Community {
group.set_updated(convert_datetime(u)); group.set_updated(convert_datetime(u));
} }
if let Some(d) = self.description.to_owned() { if let Some(d) = self.description.to_owned() {
// TODO: this should be html, also add source field with raw markdown set_content_and_source(&mut group, &d)?;
// -> same for post.content and others
group.set_content(d);
} }
if let Some(icon_url) = &self.icon { if let Some(icon_url) = &self.icon {
@ -138,14 +141,9 @@ impl FromApub for CommunityForm {
.as_xsd_string() .as_xsd_string()
.context(location_info!())? .context(location_info!())?
.to_string(); .to_string();
// TODO: should be parsed as html and tags like <script> removed (or use markdown source)
// -> same for post.content etc let description = get_source_markdown_value(group)?;
let description = group
.inner
.content()
.map(|s| s.as_single_xsd_string())
.flatten()
.map(|s| s.to_string());
check_slurs(&name)?; check_slurs(&name)?;
check_slurs(&title)?; check_slurs(&title)?;
check_slurs_opt(&description)?; check_slurs_opt(&description)?;

View File

@ -1,8 +1,9 @@
use crate::check_is_apub_id_valid; use crate::check_is_apub_id_valid;
use activitystreams::{ use activitystreams::{
base::{AsBase, BaseExt}, base::{AsBase, BaseExt, ExtendsExt},
markers::Base, markers::Base,
object::{Tombstone, TombstoneExt}, mime::{FromStrError, Mime},
object::{ApObjectExt, Object, ObjectExt, Tombstone, TombstoneExt},
}; };
use anyhow::{anyhow, Context}; use anyhow::{anyhow, Context};
use chrono::NaiveDateTime; use chrono::NaiveDateTime;
@ -58,3 +59,73 @@ where
}; };
Ok(actor_id.to_string()) 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> + AsBase<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()?);
// set `content` to markdown for compatibility with older Lemmy versions
// TODO: change this to HTML in a while
object.set_content(markdown_text);
object.set_media_type(mime_markdown()?);

This seems really dangerous, why do we need to convert to html? Why not just content: mardownText, type: 'markdown'

This seems really dangerous, why do we need to convert to html? Why not just content: mardownText, type: 'markdown'

We could also put Markdown into the top-level content and set mediaType to text/markdown. But doing it this way ensures compatibility with software which doesnt support Markdown. Anyway, Lemmy doesnt read the HTML, only Markdown from source.content, so this doesnt change anything.

Except, now that I'm thinking about it, federation with instances without and with this commit will be broken.

We could also put Markdown into the top-level `content` and set [mediaType](https://www.w3.org/TR/activitystreams-vocabulary/#dfn-mediatype) to `text/markdown`. But doing it this way ensures compatibility with software which doesnt support Markdown. Anyway, Lemmy doesnt read the HTML, only Markdown from `source.content`, so this doesnt change anything. Except, now that I'm thinking about it, federation with instances without and with this commit will be broken.

I suppose for compatibility we could change both content and source.content to Markdown, then later change content to HTML. But that gets quite complicated really.

I suppose for compatibility we could change both `content` and `source.content` to Markdown, then later change `content` to HTML. But that gets quite complicated really.
//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> + AsBase<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();
// updated lemmy version, read markdown from `source.content`
if let Some(source) = source {
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));
}
// older lemmy version, read markdown from `content`
// TODO: remove this after a while
else {
return Ok(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(())
}
}

View File

@ -1,13 +1,18 @@
use crate::{ use crate::{
extensions::page_extension::PageExtension, extensions::page_extension::PageExtension,
fetcher::{get_or_fetch_and_upsert_community, get_or_fetch_and_upsert_user}, 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, FromApub,
PageExt, PageExt,
ToApub, ToApub,
}; };
use activitystreams::{ use activitystreams::{
object::{kind::PageType, Image, Page, Tombstone}, object::{kind::PageType, ApObject, Image, Page, Tombstone},
prelude::*, prelude::*,
}; };
use activitystreams_ext::Ext1; 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. // 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> { 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_id = self.creator_id;
let creator = blocking(pool, move |conn| User_::read(conn, creator_id)).await??; 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); .set_attributed_to(creator.actor_id);
if let Some(body) = &self.body { 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("") // TODO: hacky code because we get self.url == Some("")
@ -162,13 +167,8 @@ impl FromApub for PostForm {
.as_single_xsd_string() .as_single_xsd_string()
.context(location_info!())? .context(location_info!())?
.to_string(); .to_string();
let body = page let body = get_source_markdown_value(page)?;
.inner
.content()
.as_ref()
.map(|c| c.as_single_xsd_string())
.flatten()
.map(|s| s.to_string());
check_slurs(&name)?; check_slurs(&name)?;
let body_slurs_removed = body.map(|b| remove_slurs(&b)); let body_slurs_removed = body.map(|b| remove_slurs(&b));
Ok(PostForm { Ok(PostForm {

View File

@ -1,12 +1,18 @@
use crate::{ use crate::{
check_is_apub_id_valid, check_is_apub_id_valid,
fetcher::get_or_fetch_and_upsert_user, 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, FromApub,
NoteExt,
ToApub, ToApub,
}; };
use activitystreams::{ use activitystreams::{
object::{kind::NoteType, Note, Tombstone}, object::{kind::NoteType, ApObject, Note, Tombstone},
prelude::*, prelude::*,
}; };
use anyhow::Context; use anyhow::Context;
@ -23,10 +29,10 @@ use url::Url;
#[async_trait::async_trait(?Send)] #[async_trait::async_trait(?Send)]
impl ToApub for PrivateMessage { impl ToApub for PrivateMessage {
type ApubType = Note; type ApubType = NoteExt;
async fn to_apub(&self, pool: &DbPool) -> Result<Note, LemmyError> { async fn to_apub(&self, pool: &DbPool) -> Result<NoteExt, LemmyError> {
let mut private_message = Note::new(); let mut private_message = ApObject::new(Note::new());
let creator_id = self.creator_id; let creator_id = self.creator_id;
let creator = blocking(pool, move |conn| User_::read(conn, creator_id)).await??; 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_context(activitystreams::context())
.set_id(Url::parse(&self.ap_id.to_owned())?) .set_id(Url::parse(&self.ap_id.to_owned())?)
.set_published(convert_datetime(self.published)) .set_published(convert_datetime(self.published))
.set_content(self.content.to_owned())
.set_to(recipient.actor_id) .set_to(recipient.actor_id)
.set_attributed_to(creator.actor_id); .set_attributed_to(creator.actor_id);
set_content_and_source(&mut private_message, &self.content)?;
if let Some(u) = self.updated { if let Some(u) = self.updated {
private_message.set_updated(convert_datetime(u)); private_message.set_updated(convert_datetime(u));
} }
@ -56,10 +63,10 @@ impl ToApub for PrivateMessage {
#[async_trait::async_trait(?Send)] #[async_trait::async_trait(?Send)]
impl FromApub for PrivateMessageForm { impl FromApub for PrivateMessageForm {
type ApubType = Note; type ApubType = NoteExt;
async fn from_apub( async fn from_apub(
note: &Note, note: &NoteExt,
context: &LemmyContext, context: &LemmyContext,
expected_domain: Option<Url>, expected_domain: Option<Url>,
request_counter: &mut i32, request_counter: &mut i32,
@ -83,15 +90,12 @@ impl FromApub for PrivateMessageForm {
let ap_id = note.id_unchecked().context(location_info!())?.to_string(); let ap_id = note.id_unchecked().context(location_info!())?.to_string();
check_is_apub_id_valid(&Url::parse(&ap_id)?)?; check_is_apub_id_valid(&Url::parse(&ap_id)?)?;
let content = get_source_markdown_value(note)?.context(location_info!())?;
Ok(PrivateMessageForm { Ok(PrivateMessageForm {
creator_id: creator.id, creator_id: creator.id,
recipient_id: recipient.id, recipient_id: recipient.id,
content: note content,
.content()
.context(location_info!())?
.as_single_xsd_string()
.context(location_info!())?
.to_string(),
published: note.published().map(|u| u.to_owned().naive_local()), published: note.published().map(|u| u.to_owned().naive_local()),
updated: note.updated().map(|u| u.to_owned().naive_local()), updated: note.updated().map(|u| u.to_owned().naive_local()),
deleted: None, deleted: None,

View File

@ -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::{ use activitystreams::{
actor::{ApActor, Endpoints, Person}, actor::{ApActor, Endpoints, Person},
object::{Image, Tombstone}, object::{ApObject, Image, Tombstone},
prelude::*, prelude::*,
}; };
use activitystreams_ext::Ext1; use activitystreams_ext::Ext1;
@ -24,7 +30,7 @@ impl ToApub for User_ {
type ApubType = PersonExt; type ApubType = PersonExt;
async fn to_apub(&self, _pool: &DbPool) -> Result<PersonExt, LemmyError> { async fn to_apub(&self, _pool: &DbPool) -> Result<PersonExt, LemmyError> {
let mut person = Person::new(); let mut person = ApObject::new(Person::new());
person person
.set_context(activitystreams::context()) .set_context(activitystreams::context())
.set_id(Url::parse(&self.actor_id)?) .set_id(Url::parse(&self.actor_id)?)
@ -47,6 +53,9 @@ impl ToApub for User_ {
} }
if let Some(bio) = &self.bio { if let Some(bio) = &self.bio {
set_content_and_source(&mut person, bio)?;
// Also set summary for compatibility with older Lemmy versions.
// TODO: remove this after a while.

I had to change the user bio from summary to content, because the spec says that summary can only be HTML, with no option to specify a different mime type.

I had to change the user bio from `summary` to `content`, because the spec says that summary can only be HTML, with no option to specify a different mime type.
person.set_summary(bio.to_owned()); person.set_summary(bio.to_owned());
} }
@ -117,14 +126,8 @@ impl FromApub for UserForm {
.map(|n| n.to_owned().xsd_string()) .map(|n| n.to_owned().xsd_string())
.flatten(); .flatten();

Seems to be good, but we'll have to test manually before we merge.

Seems to be good, but we'll have to test manually before we merge.
// TODO a limit check (like the API does) might need to be done let bio = get_source_markdown_value(person)?;
// 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());
check_slurs(&name)?; check_slurs(&name)?;
check_slurs_opt(&preferred_username)?; check_slurs_opt(&preferred_username)?;
check_slurs_opt(&bio)?; check_slurs_opt(&bio)?;