]> Untitled Git - lemmy.git/blob - lemmy_apub/src/objects/community.rs
Merge pull request #1209 from LemmyNet/fix-actor-name-confusion
[lemmy.git] / lemmy_apub / src / objects / community.rs
1 use crate::{
2   extensions::group_extensions::GroupExtension,
3   fetcher::get_or_fetch_and_upsert_user,
4   objects::{check_object_domain, create_tombstone},
5   ActorType,
6   FromApub,
7   GroupExt,
8   ToApub,
9 };
10 use activitystreams::{
11   actor::{kind::GroupType, ApActor, Endpoints, Group},
12   base::BaseExt,
13   object::{Image, Tombstone},
14   prelude::*,
15 };
16 use activitystreams_ext::Ext2;
17 use anyhow::Context;
18 use lemmy_db::{
19   community::{Community, CommunityForm},
20   community_view::CommunityModeratorView,
21   naive_now,
22   DbPool,
23 };
24 use lemmy_structs::blocking;
25 use lemmy_utils::{
26   location_info,
27   utils::{check_slurs, check_slurs_opt, convert_datetime},
28   LemmyError,
29 };
30 use lemmy_websocket::LemmyContext;
31 use url::Url;
32
33 #[async_trait::async_trait(?Send)]
34 impl ToApub for Community {
35   type ApubType = GroupExt;
36
37   async fn to_apub(&self, pool: &DbPool) -> Result<GroupExt, LemmyError> {
38     // The attributed to, is an ordered vector with the creator actor_ids first,
39     // then the rest of the moderators
40     // TODO Technically the instance admins can mod the community, but lets
41     // ignore that for now
42     let id = self.id;
43     let moderators = blocking(pool, move |conn| {
44       CommunityModeratorView::for_community(&conn, id)
45     })
46     .await??;
47     let moderators: Vec<String> = moderators.into_iter().map(|m| m.user_actor_id).collect();
48
49     let mut group = Group::new();
50     group
51       .set_context(activitystreams::context())
52       .set_id(Url::parse(&self.actor_id)?)
53       .set_name(self.title.to_owned())
54       .set_published(convert_datetime(self.published))
55       .set_many_attributed_tos(moderators);
56
57     if let Some(u) = self.updated.to_owned() {
58       group.set_updated(convert_datetime(u));
59     }
60     if let Some(d) = self.description.to_owned() {
61       // TODO: this should be html, also add source field with raw markdown
62       //       -> same for post.content and others
63       group.set_content(d);
64     }
65
66     if let Some(icon_url) = &self.icon {
67       let mut image = Image::new();
68       image.set_url(Url::parse(icon_url)?);
69       group.set_icon(image.into_any_base()?);
70     }
71
72     if let Some(banner_url) = &self.banner {
73       let mut image = Image::new();
74       image.set_url(Url::parse(banner_url)?);
75       group.set_image(image.into_any_base()?);
76     }
77
78     let mut ap_actor = ApActor::new(self.get_inbox_url()?, group);
79     ap_actor
80       .set_preferred_username(self.name.to_owned())
81       .set_outbox(self.get_outbox_url()?)
82       .set_followers(self.get_followers_url()?)
83       .set_endpoints(Endpoints {
84         shared_inbox: Some(self.get_shared_inbox_url()?),
85         ..Default::default()
86       });
87
88     let nsfw = self.nsfw;
89     let category_id = self.category_id;
90     let group_extension = blocking(pool, move |conn| {
91       GroupExtension::new(conn, category_id, nsfw)
92     })
93     .await??;
94
95     Ok(Ext2::new(
96       ap_actor,
97       group_extension,
98       self.get_public_key_ext()?,
99     ))
100   }
101
102   fn to_tombstone(&self) -> Result<Tombstone, LemmyError> {
103     create_tombstone(self.deleted, &self.actor_id, self.updated, GroupType::Group)
104   }
105 }
106 #[async_trait::async_trait(?Send)]
107 impl FromApub for CommunityForm {
108   type ApubType = GroupExt;
109
110   async fn from_apub(
111     group: &GroupExt,
112     context: &LemmyContext,
113     expected_domain: Option<Url>,
114   ) -> Result<Self, LemmyError> {
115     let creator_and_moderator_uris = group.inner.attributed_to().context(location_info!())?;
116     let creator_uri = creator_and_moderator_uris
117       .as_many()
118       .context(location_info!())?
119       .iter()
120       .next()
121       .context(location_info!())?
122       .as_xsd_any_uri()
123       .context(location_info!())?;
124
125     let creator = get_or_fetch_and_upsert_user(creator_uri, context).await?;
126     let name = group
127       .inner
128       .preferred_username()
129       .context(location_info!())?
130       .to_string();
131     let title = group
132       .inner
133       .name()
134       .context(location_info!())?
135       .as_one()
136       .context(location_info!())?
137       .as_xsd_string()
138       .context(location_info!())?
139       .to_string();
140     // TODO: should be parsed as html and tags like <script> removed (or use markdown source)
141     //       -> same for post.content etc
142     let description = group
143       .inner
144       .content()
145       .map(|s| s.as_single_xsd_string())
146       .flatten()
147       .map(|s| s.to_string());
148     check_slurs(&name)?;
149     check_slurs(&title)?;
150     check_slurs_opt(&description)?;
151
152     let icon = match group.icon() {
153       Some(any_image) => Some(
154         Image::from_any_base(any_image.as_one().context(location_info!())?.clone())
155           .context(location_info!())?
156           .context(location_info!())?
157           .url()
158           .context(location_info!())?
159           .as_single_xsd_any_uri()
160           .map(|u| u.to_string()),
161       ),
162       None => None,
163     };
164
165     let banner = match group.image() {
166       Some(any_image) => Some(
167         Image::from_any_base(any_image.as_one().context(location_info!())?.clone())
168           .context(location_info!())?
169           .context(location_info!())?
170           .url()
171           .context(location_info!())?
172           .as_single_xsd_any_uri()
173           .map(|u| u.to_string()),
174       ),
175       None => None,
176     };
177
178     Ok(CommunityForm {
179       name,
180       title,
181       description,
182       category_id: group.ext_one.category.identifier.parse::<i32>()?,
183       creator_id: creator.id,
184       removed: None,
185       published: group.inner.published().map(|u| u.to_owned().naive_local()),
186       updated: group.inner.updated().map(|u| u.to_owned().naive_local()),
187       deleted: None,
188       nsfw: group.ext_one.sensitive,
189       actor_id: Some(check_object_domain(group, expected_domain)?),
190       local: false,
191       private_key: None,
192       public_key: Some(group.ext_two.to_owned().public_key.public_key_pem),
193       last_refreshed_at: Some(naive_now()),
194       icon,
195       banner,
196     })
197   }
198 }