2 check_is_apub_id_valid,
3 context::lemmy_context,
4 fetcher::community::{fetch_community_outbox, update_community_mods},
5 generate_moderators_url,
7 objects::{create_tombstone, FromApub, ImageObject, Source, ToApub},
10 use activitystreams::{
11 actor::{kind::GroupType, Endpoints},
13 object::{kind::ImageType, Tombstone},
14 primitives::OneOrMany,
17 use chrono::{DateTime, FixedOffset};
18 use itertools::Itertools;
19 use lemmy_api_common::blocking;
21 signatures::PublicKey,
23 values::{MediaTypeHtml, MediaTypeMarkdown},
24 verify::verify_domains_match,
26 use lemmy_db_queries::{source::community::Community_, DbPool};
27 use lemmy_db_schema::{
29 source::community::{Community, CommunityForm},
31 use lemmy_db_views_actor::community_follower_view::CommunityFollowerView;
33 settings::structs::Settings,
34 utils::{check_slurs, check_slurs_opt, convert_datetime, markdown_to_html},
37 use lemmy_websocket::LemmyContext;
38 use serde::{Deserialize, Serialize};
39 use serde_with::skip_serializing_none;
42 #[skip_serializing_none]
43 #[derive(Clone, Debug, Deserialize, Serialize)]
44 #[serde(rename_all = "camelCase")]
46 #[serde(rename = "@context")]
47 context: OneOrMany<AnyBase>,
48 #[serde(rename = "type")]
51 /// username, set at account creation and can never be changed
52 preferred_username: String,
53 /// title (can be changed at any time)
55 content: Option<String>,
56 media_type: Option<MediaTypeHtml>,
57 source: Option<Source>,
58 icon: Option<ImageObject>,
60 image: Option<ImageObject>,
62 sensitive: Option<bool>,
64 pub(crate) moderators: Option<Url>,
66 pub(crate) outbox: Url,
68 endpoints: Endpoints<Url>,
69 public_key: PublicKey,
70 published: DateTime<FixedOffset>,
71 updated: Option<DateTime<FixedOffset>>,
77 pub(crate) fn id(&self, expected_domain: &Url) -> Result<&Url, LemmyError> {
78 verify_domains_match(&self.id, expected_domain)?;
81 pub(crate) async fn from_apub_to_form(
83 expected_domain: &Url,
85 ) -> Result<CommunityForm, LemmyError> {
86 let actor_id = Some(group.id(expected_domain)?.clone().into());
87 let name = group.preferred_username.clone();
88 let title = group.name.clone();
89 let description = group.source.clone().map(|s| s.content);
90 let shared_inbox = group.endpoints.shared_inbox.clone().map(|s| s.into());
92 let slur_regex = &settings.slur_regex();
93 check_slurs(&name, slur_regex)?;
94 check_slurs(&title, slur_regex)?;
95 check_slurs_opt(&description, slur_regex)?;
102 published: Some(group.published.naive_local()),
103 updated: group.updated.map(|u| u.naive_local()),
105 nsfw: Some(group.sensitive.unwrap_or(false)),
109 public_key: Some(group.public_key.public_key_pem.clone()),
110 last_refreshed_at: Some(naive_now()),
111 icon: Some(group.icon.clone().map(|i| i.url.into())),
112 banner: Some(group.image.clone().map(|i| i.url.into())),
113 followers_url: Some(group.followers.clone().into()),
114 inbox_url: Some(group.inbox.clone().into()),
115 shared_inbox_url: Some(shared_inbox),
120 #[async_trait::async_trait(?Send)]
121 impl ToApub for Community {
122 type ApubType = Group;
124 async fn to_apub(&self, _pool: &DbPool) -> Result<Group, LemmyError> {
125 let source = self.description.clone().map(|bio| Source {
127 media_type: MediaTypeMarkdown::Markdown,
129 let icon = self.icon.clone().map(|url| ImageObject {
130 kind: ImageType::Image,
133 let image = self.banner.clone().map(|url| ImageObject {
134 kind: ImageType::Image,
139 context: lemmy_context(),
140 kind: GroupType::Group,
142 preferred_username: self.name.clone(),
143 name: self.title.clone(),
144 content: self.description.as_ref().map(|b| markdown_to_html(b)),
145 media_type: self.description.as_ref().map(|_| MediaTypeHtml::Html),
149 sensitive: Some(self.nsfw),
150 moderators: Some(generate_moderators_url(&self.actor_id)?.into()),
151 inbox: self.inbox_url.clone().into(),
152 outbox: generate_outbox_url(&self.actor_id)?.into(),
153 followers: self.followers_url.clone().into(),
154 endpoints: Endpoints {
155 shared_inbox: self.shared_inbox_url.clone().map(|s| s.into()),
158 public_key: self.get_public_key()?,
159 published: convert_datetime(self.published),
160 updated: self.updated.map(convert_datetime),
161 unparsed: Default::default(),
166 fn to_tombstone(&self) -> Result<Tombstone, LemmyError> {
169 self.actor_id.to_owned().into(),
176 #[async_trait::async_trait(?Send)]
177 impl FromApub for Community {
178 type ApubType = Group;
180 /// Converts a `Group` to `Community`, inserts it into the database and updates moderators.
183 context: &LemmyContext,
184 expected_domain: &Url,
185 request_counter: &mut i32,
186 ) -> Result<Community, LemmyError> {
187 let form = Group::from_apub_to_form(group, expected_domain, &context.settings()).await?;
189 let community = blocking(context.pool(), move |conn| Community::upsert(conn, &form)).await??;
190 update_community_mods(group, &community, context, request_counter).await?;
192 // TODO: doing this unconditionally might cause infinite loop for some reason
193 fetch_community_outbox(context, &group.outbox, request_counter).await?;
199 #[async_trait::async_trait(?Send)]
200 impl CommunityType for Community {
201 fn followers_url(&self) -> Url {
202 self.followers_url.clone().into()
205 /// For a given community, returns the inboxes of all followers.
206 async fn get_follower_inboxes(
210 ) -> Result<Vec<Url>, LemmyError> {
213 let follows = blocking(pool, move |conn| {
214 CommunityFollowerView::for_community(conn, id)
217 let inboxes = follows
219 .filter(|f| !f.follower.local)
220 .map(|f| f.follower.shared_inbox_url.unwrap_or(f.follower.inbox_url))
221 .map(|i| i.into_inner())
223 // Don't send to blocked instances
224 .filter(|inbox| check_is_apub_id_valid(inbox, false, settings).is_ok())