|
|
|
@ -1,101 +1,157 @@
|
|
|
|
|
use crate::{
|
|
|
|
|
extensions::{context::lemmy_context, group_extension::GroupExtension},
|
|
|
|
|
extensions::{context::lemmy_context, signatures::PublicKey},
|
|
|
|
|
fetcher::community::fetch_community_mods,
|
|
|
|
|
generate_moderators_url,
|
|
|
|
|
objects::{
|
|
|
|
|
check_object_domain,
|
|
|
|
|
create_tombstone,
|
|
|
|
|
get_object_from_apub,
|
|
|
|
|
get_source_markdown_value,
|
|
|
|
|
set_content_and_source,
|
|
|
|
|
FromApub,
|
|
|
|
|
FromApubToForm,
|
|
|
|
|
ToApub,
|
|
|
|
|
},
|
|
|
|
|
objects::{create_tombstone, FromApub, ImageObject, Source, ToApub},
|
|
|
|
|
ActorType,
|
|
|
|
|
GroupExt,
|
|
|
|
|
};
|
|
|
|
|
use activitystreams::{
|
|
|
|
|
actor::{kind::GroupType, ApActor, Endpoints, Group},
|
|
|
|
|
base::BaseExt,
|
|
|
|
|
object::{ApObject, Image, Tombstone},
|
|
|
|
|
prelude::*,
|
|
|
|
|
actor::{kind::GroupType, Endpoints},
|
|
|
|
|
base::AnyBase,
|
|
|
|
|
object::{kind::ImageType, Tombstone},
|
|
|
|
|
primitives::OneOrMany,
|
|
|
|
|
unparsed::Unparsed,
|
|
|
|
|
};
|
|
|
|
|
use activitystreams_ext::Ext2;
|
|
|
|
|
use anyhow::Context;
|
|
|
|
|
use chrono::{DateTime, FixedOffset};
|
|
|
|
|
use lemmy_api_common::blocking;
|
|
|
|
|
use lemmy_db_queries::DbPool;
|
|
|
|
|
use lemmy_apub_lib::{
|
|
|
|
|
values::{MediaTypeHtml, MediaTypeMarkdown},
|
|
|
|
|
verify_domains_match,
|
|
|
|
|
};
|
|
|
|
|
use lemmy_db_queries::{ApubObject, DbPool};
|
|
|
|
|
use lemmy_db_schema::{
|
|
|
|
|
naive_now,
|
|
|
|
|
source::community::{Community, CommunityForm},
|
|
|
|
|
};
|
|
|
|
|
use lemmy_db_views_actor::community_moderator_view::CommunityModeratorView;
|
|
|
|
|
use lemmy_utils::{
|
|
|
|
|
location_info,
|
|
|
|
|
utils::{check_slurs, check_slurs_opt, convert_datetime},
|
|
|
|
|
utils::{check_slurs, check_slurs_opt, convert_datetime, markdown_to_html},
|
|
|
|
|
LemmyError,
|
|
|
|
|
};
|
|
|
|
|
use lemmy_websocket::LemmyContext;
|
|
|
|
|
use serde::{Deserialize, Serialize};
|
|
|
|
|
use serde_with::skip_serializing_none;
|
|
|
|
|
use url::Url;
|
|
|
|
|
|
|
|
|
|
#[async_trait::async_trait(?Send)]
|
|
|
|
|
impl ToApub for Community {
|
|
|
|
|
type ApubType = GroupExt;
|
|
|
|
|
|
|
|
|
|
async fn to_apub(&self, pool: &DbPool) -> Result<GroupExt, LemmyError> {
|
|
|
|
|
let id = self.id;
|
|
|
|
|
let moderators = blocking(pool, move |conn| {
|
|
|
|
|
CommunityModeratorView::for_community(conn, id)
|
|
|
|
|
})
|
|
|
|
|
.await??;
|
|
|
|
|
let moderators: Vec<Url> = moderators
|
|
|
|
|
.into_iter()
|
|
|
|
|
.map(|m| m.moderator.actor_id.into_inner())
|
|
|
|
|
.collect();
|
|
|
|
|
|
|
|
|
|
let mut group = ApObject::new(Group::new());
|
|
|
|
|
group
|
|
|
|
|
.set_many_contexts(lemmy_context())
|
|
|
|
|
.set_id(self.actor_id.to_owned().into())
|
|
|
|
|
.set_name(self.title.to_owned())
|
|
|
|
|
.set_published(convert_datetime(self.published))
|
|
|
|
|
// NOTE: included attritubed_to field for compatibility with lemmy v0.9.9
|
|
|
|
|
.set_many_attributed_tos(moderators);
|
|
|
|
|
#[skip_serializing_none]
|
|
|
|
|
#[derive(Clone, Debug, Deserialize, Serialize)]
|
|
|
|
|
#[serde(rename_all = "camelCase")]
|
|
|
|
|
pub struct Group {
|
|
|
|
|
#[serde(rename = "@context")]
|
|
|
|
|
context: OneOrMany<AnyBase>,
|
|
|
|
|
#[serde(rename = "type")]
|
|
|
|
|
kind: GroupType,
|
|
|
|
|
id: Url,
|
|
|
|
|
/// username, set at account creation and can never be changed
|
|
|
|
|
preferred_username: String,
|
|
|
|
|
/// title (can be changed at any time)
|
|
|
|
|
name: String,
|
|
|
|
|
content: Option<String>,
|
|
|
|
|
media_type: Option<MediaTypeHtml>,
|
|
|
|
|
source: Option<Source>,
|
|
|
|
|
icon: Option<ImageObject>,
|
|
|
|
|
/// banner
|
|
|
|
|
image: Option<ImageObject>,
|
|
|
|
|
// lemmy extension
|
|
|
|
|
sensitive: Option<bool>,
|
|
|
|
|
// lemmy extension
|
|
|
|
|
pub(crate) moderators: Option<Url>,
|
|
|
|
|
inbox: Url,
|
|
|
|
|
pub(crate) outbox: Url,
|
|
|
|
|
followers: Url,
|
|
|
|
|
endpoints: Endpoints<Url>,
|
|
|
|
|
public_key: PublicKey,
|
|
|
|
|
published: DateTime<FixedOffset>,
|
|
|
|
|
updated: Option<DateTime<FixedOffset>>,
|
|
|
|
|
#[serde(flatten)]
|
|
|
|
|
unparsed: Unparsed,
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if let Some(u) = self.updated.to_owned() {
|
|
|
|
|
group.set_updated(convert_datetime(u));
|
|
|
|
|
}
|
|
|
|
|
if let Some(d) = self.description.to_owned() {
|
|
|
|
|
set_content_and_source(&mut group, &d)?;
|
|
|
|
|
}
|
|
|
|
|
impl Group {
|
|
|
|
|
pub(crate) fn id(&self, expected_domain: &Url) -> Result<&Url, LemmyError> {
|
|
|
|
|
verify_domains_match(&self.id, expected_domain)?;
|
|
|
|
|
Ok(&self.id)
|
|
|
|
|
}
|
|
|
|
|
pub(crate) async fn from_apub_to_form(
|
|
|
|
|
group: &Group,
|
|
|
|
|
expected_domain: &Url,
|
|
|
|
|
) -> Result<CommunityForm, LemmyError> {
|
|
|
|
|
let actor_id = Some(group.id(expected_domain)?.clone().into());
|
|
|
|
|
let name = group.preferred_username.clone();
|
|
|
|
|
let title = group.name.clone();
|
|
|
|
|
let description = group.source.clone().map(|s| s.content);
|
|
|
|
|
let shared_inbox = group.endpoints.shared_inbox.clone().map(|s| s.into());
|
|
|
|
|
|
|
|
|
|
if let Some(icon_url) = &self.icon {
|
|
|
|
|
let mut image = Image::new();
|
|
|
|
|
image.set_url::<Url>(icon_url.to_owned().into());
|
|
|
|
|
group.set_icon(image.into_any_base()?);
|
|
|
|
|
}
|
|
|
|
|
check_slurs(&name)?;
|
|
|
|
|
check_slurs(&title)?;
|
|
|
|
|
check_slurs_opt(&description)?;
|
|
|
|
|
|
|
|
|
|
if let Some(banner_url) = &self.banner {
|
|
|
|
|
let mut image = Image::new();
|
|
|
|
|
image.set_url::<Url>(banner_url.to_owned().into());
|
|
|
|
|
group.set_image(image.into_any_base()?);
|
|
|
|
|
}
|
|
|
|
|
Ok(CommunityForm {
|
|
|
|
|
name,
|
|
|
|
|
title,
|
|
|
|
|
description,
|
|
|
|
|
removed: None,
|
|
|
|
|
published: Some(group.published.naive_local()),
|
|
|
|
|
updated: group.updated.map(|u| u.naive_local()),
|
|
|
|
|
deleted: None,
|
|
|
|
|
nsfw: Some(group.sensitive.unwrap_or(false)),
|
|
|
|
|
actor_id,
|
|
|
|
|
local: Some(false),
|
|
|
|
|
private_key: None,
|
|
|
|
|
public_key: Some(group.public_key.public_key_pem.clone()),
|
|
|
|
|
last_refreshed_at: Some(naive_now()),
|
|
|
|
|
icon: Some(group.icon.clone().map(|i| i.url.into())),
|
|
|
|
|
banner: Some(group.image.clone().map(|i| i.url.into())),
|
|
|
|
|
followers_url: Some(group.followers.clone().into()),
|
|
|
|
|
inbox_url: Some(group.inbox.clone().into()),
|
|
|
|
|
shared_inbox_url: Some(shared_inbox),
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
let mut ap_actor = ApActor::new(self.inbox_url.clone().into(), group);
|
|
|
|
|
ap_actor
|
|
|
|
|
.set_preferred_username(self.name.to_owned())
|
|
|
|
|
.set_outbox(self.get_outbox_url()?)
|
|
|
|
|
.set_followers(self.followers_url.clone().into())
|
|
|
|
|
.set_endpoints(Endpoints {
|
|
|
|
|
shared_inbox: Some(self.get_shared_inbox_or_inbox_url()),
|
|
|
|
|
#[async_trait::async_trait(?Send)]
|
|
|
|
|
impl ToApub for Community {
|
|
|
|
|
type ApubType = Group;
|
|
|
|
|
|
|
|
|
|
async fn to_apub(&self, _pool: &DbPool) -> Result<Group, LemmyError> {
|
|
|
|
|
let source = self.description.clone().map(|bio| Source {
|
|
|
|
|
content: bio,
|
|
|
|
|
media_type: MediaTypeMarkdown::Markdown,
|
|
|
|
|
});
|
|
|
|
|
let icon = self.icon.clone().map(|url| ImageObject {
|
|
|
|
|
kind: ImageType::Image,
|
|
|
|
|
url: url.into(),
|
|
|
|
|
});
|
|
|
|
|
let image = self.banner.clone().map(|url| ImageObject {
|
|
|
|
|
kind: ImageType::Image,
|
|
|
|
|
url: url.into(),
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
let group = Group {
|
|
|
|
|
context: lemmy_context(),
|
|
|
|
|
kind: GroupType::Group,
|
|
|
|
|
id: self.actor_id(),
|
|
|
|
|
preferred_username: self.name.clone(),
|
|
|
|
|
name: self.title.clone(),
|
|
|
|
|
content: self.description.as_ref().map(|b| markdown_to_html(b)),
|
|
|
|
|
media_type: self.description.as_ref().map(|_| MediaTypeHtml::Html),
|
|
|
|
|
source,
|
|
|
|
|
icon,
|
|
|
|
|
image,
|
|
|
|
|
sensitive: Some(self.nsfw),
|
|
|
|
|
moderators: Some(generate_moderators_url(&self.actor_id)?.into()),
|
|
|
|
|
inbox: self.inbox_url.clone().into(),
|
|
|
|
|
outbox: self.get_outbox_url()?,
|
|
|
|
|
followers: self.followers_url.clone().into(),
|
|
|
|
|
endpoints: Endpoints {
|
|
|
|
|
shared_inbox: self.shared_inbox_url.clone().map(|s| s.into()),
|
|
|
|
|
..Default::default()
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
Ok(Ext2::new(
|
|
|
|
|
ap_actor,
|
|
|
|
|
GroupExtension::new(self.nsfw, generate_moderators_url(&self.actor_id)?.into())?,
|
|
|
|
|
self.get_public_key_ext()?,
|
|
|
|
|
))
|
|
|
|
|
},
|
|
|
|
|
public_key: self.get_public_key()?,
|
|
|
|
|
published: convert_datetime(self.published),
|
|
|
|
|
updated: self.updated.map(convert_datetime),
|
|
|
|
|
unparsed: Default::default(),
|
|
|
|
|
};
|
|
|
|
|
Ok(group)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fn to_tombstone(&self) -> Result<Tombstone, LemmyError> {
|
|
|
|
@ -110,116 +166,19 @@ impl ToApub for Community {
|
|
|
|
|
|
|
|
|
|
#[async_trait::async_trait(?Send)]
|
|
|
|
|
impl FromApub for Community {
|
|
|
|
|
type ApubType = GroupExt;
|
|
|
|
|
type ApubType = Group;
|
|
|
|
|
|
|
|
|
|
/// Converts a `Group` to `Community`, inserts it into the database and updates moderators.
|
|
|
|
|
async fn from_apub(
|
|
|
|
|
group: &GroupExt,
|
|
|
|
|
group: &Group,
|
|
|
|
|
context: &LemmyContext,
|
|
|
|
|
expected_domain: Url,
|
|
|
|
|
expected_domain: &Url,
|
|
|
|
|
request_counter: &mut i32,
|
|
|
|
|
mod_action_allowed: bool,
|
|
|
|
|
) -> Result<Community, LemmyError> {
|
|
|
|
|
get_object_from_apub(
|
|
|
|
|
group,
|
|
|
|
|
context,
|
|
|
|
|
expected_domain,
|
|
|
|
|
request_counter,
|
|
|
|
|
mod_action_allowed,
|
|
|
|
|
)
|
|
|
|
|
.await
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[async_trait::async_trait(?Send)]
|
|
|
|
|
impl FromApubToForm<GroupExt> for CommunityForm {
|
|
|
|
|
async fn from_apub(
|
|
|
|
|
group: &GroupExt,
|
|
|
|
|
context: &LemmyContext,
|
|
|
|
|
expected_domain: Url,
|
|
|
|
|
request_counter: &mut i32,
|
|
|
|
|
_mod_action_allowed: bool,
|
|
|
|
|
) -> Result<Self, LemmyError> {
|
|
|
|
|
fetch_community_mods(context, group, request_counter).await?;
|
|
|
|
|
let form = Group::from_apub_to_form(group, expected_domain).await?;
|
|
|
|
|
|
|
|
|
|
let name = group
|
|
|
|
|
.inner
|
|
|
|
|
.preferred_username()
|
|
|
|
|
.context(location_info!())?
|
|
|
|
|
.to_string();
|
|
|
|
|
let title = group
|
|
|
|
|
.inner
|
|
|
|
|
.name()
|
|
|
|
|
.context(location_info!())?
|
|
|
|
|
.as_one()
|
|
|
|
|
.context(location_info!())?
|
|
|
|
|
.as_xsd_string()
|
|
|
|
|
.context(location_info!())?
|
|
|
|
|
.to_string();
|
|
|
|
|
|
|
|
|
|
let description = get_source_markdown_value(group)?;
|
|
|
|
|
|
|
|
|
|
check_slurs(&name)?;
|
|
|
|
|
check_slurs(&title)?;
|
|
|
|
|
check_slurs_opt(&description)?;
|
|
|
|
|
|
|
|
|
|
let icon = match group.icon() {
|
|
|
|
|
Some(any_image) => Some(
|
|
|
|
|
Image::from_any_base(any_image.as_one().context(location_info!())?.clone())
|
|
|
|
|
.context(location_info!())?
|
|
|
|
|
.context(location_info!())?
|
|
|
|
|
.url()
|
|
|
|
|
.context(location_info!())?
|
|
|
|
|
.as_single_xsd_any_uri()
|
|
|
|
|
.map(|u| u.to_owned().into()),
|
|
|
|
|
),
|
|
|
|
|
None => None,
|
|
|
|
|
};
|
|
|
|
|
let banner = match group.image() {
|
|
|
|
|
Some(any_image) => Some(
|
|
|
|
|
Image::from_any_base(any_image.as_one().context(location_info!())?.clone())
|
|
|
|
|
.context(location_info!())?
|
|
|
|
|
.context(location_info!())?
|
|
|
|
|
.url()
|
|
|
|
|
.context(location_info!())?
|
|
|
|
|
.as_single_xsd_any_uri()
|
|
|
|
|
.map(|u| u.to_owned().into()),
|
|
|
|
|
),
|
|
|
|
|
None => None,
|
|
|
|
|
};
|
|
|
|
|
let shared_inbox = group
|
|
|
|
|
.inner
|
|
|
|
|
.endpoints()?
|
|
|
|
|
.map(|e| e.shared_inbox)
|
|
|
|
|
.flatten()
|
|
|
|
|
.map(|s| s.to_owned().into());
|
|
|
|
|
|
|
|
|
|
Ok(CommunityForm {
|
|
|
|
|
name,
|
|
|
|
|
title,
|
|
|
|
|
description,
|
|
|
|
|
removed: None,
|
|
|
|
|
published: group.inner.published().map(|u| u.to_owned().naive_local()),
|
|
|
|
|
updated: group.inner.updated().map(|u| u.to_owned().naive_local()),
|
|
|
|
|
deleted: None,
|
|
|
|
|
nsfw: Some(group.ext_one.sensitive.unwrap_or(false)),
|
|
|
|
|
actor_id: Some(check_object_domain(group, expected_domain, true)?),
|
|
|
|
|
local: Some(false),
|
|
|
|
|
private_key: None,
|
|
|
|
|
public_key: Some(group.ext_two.to_owned().public_key.public_key_pem),
|
|
|
|
|
last_refreshed_at: Some(naive_now()),
|
|
|
|
|
icon,
|
|
|
|
|
banner,
|
|
|
|
|
followers_url: Some(
|
|
|
|
|
group
|
|
|
|
|
.inner
|
|
|
|
|
.followers()?
|
|
|
|
|
.context(location_info!())?
|
|
|
|
|
.to_owned()
|
|
|
|
|
.into(),
|
|
|
|
|
),
|
|
|
|
|
inbox_url: Some(group.inner.inbox()?.to_owned().into()),
|
|
|
|
|
shared_inbox_url: Some(shared_inbox),
|
|
|
|
|
})
|
|
|
|
|
let community = blocking(context.pool(), move |conn| Community::upsert(conn, &form)).await??;
|
|
|
|
|
Ok(community)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|