Merge branch 'main' into simplify-config

pull/1686/head
Dessalines 3 years ago
commit 65acc29ae4

74
Cargo.lock generated

@ -81,12 +81,10 @@ dependencies = [
"ahash 0.7.4",
"base64 0.13.0",
"bitflags",
"brotli2",
"bytes",
"bytestring",
"derive_more",
"encoding_rs",
"flate2",
"futures-core",
"futures-util",
"h2",
@ -108,7 +106,6 @@ dependencies = [
"smallvec",
"time 0.2.27",
"tokio",
"zstd",
]
[[package]]
@ -398,7 +395,6 @@ dependencies = [
"base64 0.13.0",
"bytes",
"cfg-if",
"cookie",
"derive_more",
"futures-core",
"itoa",
@ -565,26 +561,6 @@ dependencies = [
"opaque-debug 0.3.0",
]
[[package]]
name = "brotli-sys"
version = "0.3.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4445dea95f4c2b41cde57cc9fee236ae4dbae88d8fcbdb4750fc1bb5d86aaecd"
dependencies = [
"cc",
"libc",
]
[[package]]
name = "brotli2"
version = "0.3.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0cb036c3eade309815c15ddbacec5b22c4d1f3983a774ab2eac2e3e9ea85568e"
dependencies = [
"brotli-sys",
"libc",
]
[[package]]
name = "bumpalo"
version = "3.7.0"
@ -649,9 +625,6 @@ name = "cc"
version = "1.0.68"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4a72c244c1ff497a746a7e1fb3d14bd08420ecda70c8f25c7112f2781652d787"
dependencies = [
"jobserver",
]
[[package]]
name = "cfg-if"
@ -729,9 +702,9 @@ checksum = "6245d59a3e82a7fc217c5828a6692dbc6dfb63a0c8c90495621f7b9d79704a0e"
[[package]]
name = "cookie"
version = "0.15.0"
version = "0.15.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ffdf8865bac3d9a3bde5bde9088ca431b11f5d37c7a578b8086af77248b76627"
checksum = "d5f1c7727e460397e56abc4bddc1d49e07a1ad78fc98eb2e1c8f032a58a2f80d"
dependencies = [
"percent-encoding",
"time 0.2.27",
@ -1367,9 +1340,9 @@ dependencies = [
[[package]]
name = "http-signature-normalization-actix"
version = "0.5.0-beta.6"
version = "0.5.0-beta.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4fddb5f0de5059c337a00cfe5b224768e1821327da9df4b6f27762cbf305126d"
checksum = "aa7cf7b03512ba7341b4252794751c5ff46635e0ff33eb864a929a5b7381e17a"
dependencies = [
"actix-web",
"awc",
@ -1531,15 +1504,6 @@ version = "0.4.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dd25036021b0de88a0aff6b850051563c6516d0bf53f8638938edbb9de732736"
[[package]]
name = "jobserver"
version = "0.1.22"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "972f5ae5d1cb9c6ae417789196c803205313edde988685da5e3aae0827b9e7fd"
dependencies = [
"libc",
]
[[package]]
name = "jpeg-decoder"
version = "0.1.22"
@ -1751,6 +1715,7 @@ dependencies = [
"lemmy_utils",
"lemmy_websocket",
"serde",
"serde_json",
"url",
]
@ -3657,32 +3622,3 @@ name = "xdg"
version = "2.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d089681aa106a86fade1b0128fb5daf07d5867a509ab036d99988dec80429a57"
[[package]]
name = "zstd"
version = "0.7.0+zstd.1.4.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9428752481d8372e15b1bf779ea518a179ad6c771cca2d2c60e4fbff3cc2cd52"
dependencies = [
"zstd-safe",
]
[[package]]
name = "zstd-safe"
version = "3.1.0+zstd.1.4.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5aa1926623ad7fe406e090555387daf73db555b948134b4d73eac5eb08fb666d"
dependencies = [
"libc",
"zstd-sys",
]
[[package]]
name = "zstd-sys"
version = "1.5.0+zstd.1.4.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4e6c094340240369025fc6b731b054ee2a834328fa584310ac96aa4baebdc465"
dependencies = [
"cc",
"libc",
]

@ -51,7 +51,7 @@ env_logger = "0.8.4"
strum = "0.21.0"
url = { version = "2.2.2", features = ["serde"] }
openssl = "0.10.35"
http-signature-normalization-actix = { version = "0.5.0-beta.6", default-features = false, features = ["sha-2"] }
http-signature-normalization-actix = { version = "0.5.0-beta.7", default-features = false, features = ["sha-2"] }
tokio = { version = "1.8.0", features = ["sync"] }
anyhow = "1.0.41"
reqwest = { version = "0.11.4", features = ["json"] }

@ -1,3 +1,68 @@
# Lemmy v0.11.3 Release (2021-07-30)
## Changes
Since our last release, we've had [~30](https://github.com/LemmyNet/lemmy/compare/0.11.0...main) commits to Lemmy, and [~60](https://github.com/LemmyNet/lemmy-ui/compare/0.11.0...main) to Lemmy UI.
### Lemmy Server
- Blank out extra info for deleted or removed content. Fixes [#1679](https://github.com/LemmyNet/Lemmy/issues/1679)
- Add show_new_posts_notifs setting. Fixes [#1664](https://github.com/LemmyNet/Lemmy/issues/1664)
- Fix issue with protocol string in actor id generation [#1668](https://github.com/LemmyNet/Lemmy/issues/1668)
- Adding shortname fetching for users and communities. Fixes [#1662](https://github.com/LemmyNet/Lemmy/issues/1662)
- Make captcha case-insensitive.
- Remove tracking params from post url (fixes [#768](https://github.com/LemmyNet/Lemmy/issues/768))
- Upgrade pictrs. Fixes [#1599](https://github.com/LemmyNet/Lemmy/issues/1599)
- Invalidate current logins on account deletion. Fixes [#1602](https://github.com/LemmyNet/Lemmy/issues/1602)
- Fix nsfw posts showing for non-logged in users. Fixes [#1614](https://github.com/LemmyNet/Lemmy/issues/1614)
- Add additional slurs configuration option. Closes [#1464](https://github.com/LemmyNet/Lemmy/issues/1464).
- Updating to rust 1.51.0
### Lemmy UI
- Have setting to disable notifs for new posts. Fixes [#132](https://github.com/LemmyNet/lemmy-ui/issues/132)
- Remove max length constraints on actors. Fixes [#350](https://github.com/LemmyNet/lemmy-ui/issues/350)
- Fix captcha replay bug. Fixes [#348](https://github.com/LemmyNet/lemmy-ui/issues/348)
- Removing community and user routes in favor of shortnames. Fixes [#317](https://github.com/LemmyNet/lemmy-ui/issues/317)
- Add front end helpers 1 [(#346](https://github.com/LemmyNet/lemmy-ui/issues/346))
- Don't use default subscribed for communities page.
- Adding Listing type to communities page, default local. [#190](https://github.com/LemmyNet/lemmy-ui/issues/190)
- Fix language bug on mobile browsers.
- Collapse sidebar on mobile. Fixes [#335](https://github.com/LemmyNet/lemmy-ui/issues/335)
- Re-organized components folder. [(#339](https://github.com/LemmyNet/lemmy-ui/issues/339))
- Moving comment link to top bar. Fixes [#307](https://github.com/LemmyNet/lemmy-ui/issues/307)
- Make spinner bigger. Fixes [#203](https://github.com/LemmyNet/lemmy-ui/issues/203)
- Fix preview description html. Fixes [#110](https://github.com/LemmyNet/lemmy-ui/issues/110)
- Update darkly, make danger darker. Fixes [#16](https://github.com/LemmyNet/lemmy-ui/issues/16)
- Always show previous paginator, extract paginator component.
- Use better comment collapse icon, and add text. Fixes [#318](https://github.com/LemmyNet/lemmy-ui/issues/318)
- Fix symbols issue. Fixes [#319](https://github.com/LemmyNet/lemmy-ui/issues/319)
- Don't restore scroll position on page refresh. Fixes [#186](https://github.com/LemmyNet/lemmy-ui/issues/186)
- Insert triple backticks for 'code' button when multiple lines are selected. [(#311](https://github.com/LemmyNet/lemmy-ui/issues/311))
- Adding a comment here placeholder. Fixes [#301](https://github.com/LemmyNet/lemmy-ui/issues/301)
- Fix non-local community and person links. Fixes [#290](https://github.com/LemmyNet/lemmy-ui/issues/290)
- Fix navbar bug. Fixes [#289](https://github.com/LemmyNet/lemmy-ui/issues/289)
- Hide names of mods / admins without priveleges. Fixes [#285](https://github.com/LemmyNet/lemmy-ui/issues/285)
- Adding URL search type. Fixes [#286](https://github.com/LemmyNet/lemmy-ui/issues/286)
- Add a link to joinlemmy on lemmy.ml signup. Fixes [#235](https://github.com/LemmyNet/lemmy-ui/issues/235)
- Fix duped site description. Fixes [#281](https://github.com/LemmyNet/lemmy-ui/issues/281)
### API
- Added `show_new_posts_notifs` boolean to `SaveUserSettings`, and `LocalUserSettings`.
- A full list of the API changes can be seen on this diff of [lemmy-js-client: 0.11.0 -> 0.11.3](https://github.com/LemmyNet/lemmy-js-client/compare/0.11.0...0.11.3-rc.4) .
### Federation
- No changes in this release, but there will be many soon.
## Upgrade notes
To upgrade your instance to `0.11.3`, simply follow the instructions in the documentation:
- [Upgrade with manual Docker installation](https://join-lemmy.org/docs/en/administration/install_docker.html#updating)
- [Upgrade with Ansible installation](https://join-lemmy.org/docs/en/administration/install_ansible.html)
# Lemmy v0.11.0 Release (2021-04-27)
## Changes
@ -49,7 +114,6 @@ Since our last release this month, we've had [~60](https://github.com/LemmyNet/l
- Fix html notif bug. Fixes [#254](https://github.com/LemmyNet/lemmy-ui/issues/254)
- Fixing issue with debounce. Fixes [#236](https://github.com/LemmyNet/lemmy-ui/issues/236)
## Upgrade notes
### Servers

@ -1 +1 @@
0.11.2
0.11.3

@ -115,6 +115,7 @@ test('Delete a comment', async () => {
commentRes.comment_view.comment.id
);
expect(deleteCommentRes.comment_view.comment.deleted).toBe(true);
expect(deleteCommentRes.comment_view.comment.content).toBe("");
// Make sure that comment is undefined on beta
let searchBeta = await searchComment(beta, commentRes.comment_view.comment);
@ -149,6 +150,7 @@ test('Remove a comment from admin and community on the same instance', async ()
// The beta admin removes it (the community lives on beta)
let removeCommentRes = await removeComment(beta, true, betaCommentId);
expect(removeCommentRes.comment_view.comment.removed).toBe(true);
expect(removeCommentRes.comment_view.comment.content).toBe("");
// Make sure that comment is removed on alpha (it gets pushed since an admin from beta removed it)
let refetchedPost = await getPost(alpha, postRes.post_view.post.id);

@ -77,6 +77,7 @@ test('Delete community', async () => {
communityRes.community_view.community.id
);
expect(deleteCommunityRes.community_view.community.deleted).toBe(true);
expect(deleteCommunityRes.community_view.community.title).toBe("");
// Make sure it got deleted on A
let communityOnAlphaDeleted = await getCommunity(
@ -128,6 +129,7 @@ test('Remove community', async () => {
communityRes.community_view.community.id
);
expect(removeCommunityRes.community_view.community.removed).toBe(true);
expect(removeCommunityRes.community_view.community.title).toBe("");
// Make sure it got Removed on A
let communityOnAlphaRemoved = await getCommunity(

@ -210,6 +210,7 @@ test('Delete a post', async () => {
let deletedPost = await deletePost(alpha, true, postRes.post_view.post);
expect(deletedPost.post_view.post.deleted).toBe(true);
expect(deletedPost.post_view.post.name).toBe("");
// Make sure lemmy beta sees post is deleted
let searchBeta = await searchPost(beta, postRes.post_view.post);
@ -237,6 +238,7 @@ test('Remove a post from admin and community on different instance', async () =>
let removedPost = await removePost(alpha, true, postRes.post_view.post);
expect(removedPost.post_view.post.removed).toBe(true);
expect(removedPost.post_view.post.name).toBe("");
// Make sure lemmy beta sees post is NOT removed
let searchBeta = await searchPost(beta, postRes.post_view.post);

@ -64,6 +64,7 @@ test('Delete a private message', async () => {
pmRes.private_message_view.private_message.id
);
expect(deletedPmRes.private_message_view.private_message.deleted).toBe(true);
expect(deletedPmRes.private_message_view.private_message.content).toBe("");
// The GetPrivateMessages filters out deleted,
// even though they are in the actual database.

@ -35,7 +35,7 @@ lazy_static = "1.4.0"
url = { version = "2.2.2", features = ["serde"] }
openssl = "0.10.35"
http = "0.2.4"
http-signature-normalization-actix = { version = "0.5.0-beta.6", default-features = false, features = ["sha-2"] }
http-signature-normalization-actix = { version = "0.5.0-beta.7", default-features = false, features = ["sha-2"] }
base64 = "0.13.0"
tokio = "1.8.0"
futures = "0.3.15"

@ -7,12 +7,19 @@ use lemmy_api_common::{
comment::*,
get_local_user_view_from_jwt,
};
use lemmy_apub::ApubLikeableType;
use lemmy_apub::{
activities::voting::{
undo_vote::UndoVote,
vote::{Vote, VoteType},
},
PostOrComment,
};
use lemmy_db_queries::{source::comment::Comment_, Likeable, Saveable};
use lemmy_db_schema::{source::comment::*, LocalUserId};
use lemmy_db_views::{comment_view::CommentView, local_user_view::LocalUserView};
use lemmy_utils::{ApiError, ConnectionId, LemmyError};
use lemmy_websocket::{messages::SendComment, LemmyContext, UserOperation};
use std::convert::TryInto;
#[async_trait::async_trait(?Send)]
impl Perform for MarkCommentAsRead {
@ -170,6 +177,7 @@ impl Perform for CreateCommentLike {
// Only add the like if the score isnt 0
let comment = orig_comment.comment;
let object = PostOrComment::Comment(Box::new(comment));
let do_add = like_form.score != 0 && (like_form.score == 1 || like_form.score == -1);
if do_add {
let like_form2 = like_form.clone();
@ -178,17 +186,24 @@ impl Perform for CreateCommentLike {
return Err(ApiError::err("couldnt_like_comment").into());
}
if like_form.score == 1 {
comment.send_like(&local_user_view.person, context).await?;
} else if like_form.score == -1 {
comment
.send_dislike(&local_user_view.person, context)
.await?;
}
Vote::send(
&object,
&local_user_view.person,
orig_comment.community.id,
like_form.score.try_into()?,
context,
)
.await?;
} else {
comment
.send_undo_like(&local_user_view.person, context)
.await?;
// API doesn't distinguish between Undo/Like and Undo/Dislike
UndoVote::send(
&object,
&local_user_view.person,
orig_comment.community.id,
VoteType::Like,
context,
)
.await?;
}
// Have to refetch the comment to get the current state

@ -9,12 +9,23 @@ use lemmy_api_common::{
mark_post_as_read,
post::*,
};
use lemmy_apub::{ApubLikeableType, ApubObjectType};
use lemmy_apub::{
activities::{
post::create_or_update::CreateOrUpdatePost,
voting::{
undo_vote::UndoVote,
vote::{Vote, VoteType},
},
CreateOrUpdateType,
},
PostOrComment,
};
use lemmy_db_queries::{source::post::Post_, Crud, Likeable, Saveable};
use lemmy_db_schema::source::{moderator::*, post::*};
use lemmy_db_views::post_view::PostView;
use lemmy_utils::{ApiError, ConnectionId, LemmyError};
use lemmy_websocket::{messages::SendPost, LemmyContext, UserOperation};
use std::convert::TryInto;
#[async_trait::async_trait(?Send)]
impl Perform for CreatePostLike {
@ -50,6 +61,9 @@ impl Perform for CreatePostLike {
})
.await??;
let community_id = post.community_id;
let object = PostOrComment::Post(Box::new(post));
// Only add the like if the score isnt 0
let do_add = like_form.score != 0 && (like_form.score == 1 || like_form.score == -1);
if do_add {
@ -59,15 +73,24 @@ impl Perform for CreatePostLike {
return Err(ApiError::err("couldnt_like_post").into());
}
if like_form.score == 1 {
post.send_like(&local_user_view.person, context).await?;
} else if like_form.score == -1 {
post.send_dislike(&local_user_view.person, context).await?;
}
Vote::send(
&object,
&local_user_view.person,
community_id,
like_form.score.try_into()?,
context,
)
.await?;
} else {
post
.send_undo_like(&local_user_view.person, context)
.await?;
// API doesn't distinguish between Undo/Like and Undo/Dislike
UndoVote::send(
&object,
&local_user_view.person,
community_id,
VoteType::Like,
context,
)
.await?;
}
// Mark the post as read
@ -140,9 +163,13 @@ impl Perform for LockPost {
blocking(context.pool(), move |conn| ModLockPost::create(conn, &form)).await??;
// apub updates
updated_post
.send_update(&local_user_view.person, context)
.await?;
CreateOrUpdatePost::send(
&updated_post,
&local_user_view.person,
CreateOrUpdateType::Update,
context,
)
.await?;
// Refetch the post
let post_id = data.post_id;
@ -214,9 +241,13 @@ impl Perform for StickyPost {
// Apub updates
// TODO stickied should pry work like locked for ease of use
updated_post
.send_update(&local_user_view.person, context)
.await?;
CreateOrUpdatePost::send(
&updated_post,
&local_user_view.person,
CreateOrUpdateType::Update,
context,
)
.await?;
// Refetch the post
let post_id = data.post_id;

@ -15,6 +15,7 @@ use lemmy_db_queries::{
from_opt_str_to_opt_enum,
source::site::Site_,
Crud,
DeleteableOrRemoveable,
ListingType,
SearchType,
SortType,
@ -332,6 +333,28 @@ impl Perform for Search {
}
};
// Blank out deleted or removed info
for cv in comments
.iter_mut()
.filter(|cv| cv.comment.deleted || cv.comment.removed)
{
cv.comment = cv.to_owned().comment.blank_out_deleted_or_removed_info();
}
for cv in communities
.iter_mut()
.filter(|cv| cv.community.deleted || cv.community.removed)
{
cv.community = cv.to_owned().community.blank_out_deleted_or_removed_info();
}
for pv in posts
.iter_mut()
.filter(|p| p.post.deleted || p.post.removed)
{
pv.post = pv.to_owned().post.blank_out_deleted_or_removed_info();
}
// Return the jwt
Ok(SearchResponse {
type_: search_type.to_string(),

@ -18,7 +18,7 @@ lemmy_utils = { path = "../utils" }
serde = { version = "1.0.126", features = ["derive"] }
log = "0.4.14"
diesel = "1.4.7"
actix-web = "4.0.0-beta.8"
actix-web = {version = "4.0.0-beta.8", default-features = false, features = ["cookies"] }
chrono = { version = "0.4.19", features = ["serde"] }
serde_json = { version = "1.0.64", features = ["preserve_order"] }
url = "2.2.2"

@ -30,7 +30,7 @@ lazy_static = "1.4.0"
url = { version = "2.2.2", features = ["serde"] }
openssl = "0.10.35"
http = "0.2.4"
http-signature-normalization-actix = { version = "0.5.0-beta.6", default-features = false, features = ["sha-2"] }
http-signature-normalization-actix = { version = "0.5.0-beta.7", default-features = false, features = ["sha-2"] }
base64 = "0.13.0"
tokio = "1.8.0"
futures = "0.3.15"

@ -8,7 +8,16 @@ use lemmy_api_common::{
get_post,
send_local_notifs,
};
use lemmy_apub::{generate_apub_endpoint, ApubLikeableType, ApubObjectType, EndpointType};
use lemmy_apub::{
activities::{
comment::create_or_update::CreateOrUpdateComment,
voting::vote::{Vote, VoteType},
CreateOrUpdateType,
},
generate_apub_endpoint,
EndpointType,
PostOrComment,
};
use lemmy_db_queries::{source::comment::Comment_, Crud, Likeable};
use lemmy_db_schema::source::comment::*;
use lemmy_db_views::comment_view::CommentView;
@ -37,8 +46,9 @@ impl PerformCrud for CreateComment {
// Check for a community ban
let post_id = data.post_id;
let post = get_post(post_id, context.pool()).await?;
let community_id = post.community_id;
check_community_ban(local_user_view.person.id, post.community_id, context.pool()).await?;
check_community_ban(local_user_view.person.id, community_id, context.pool()).await?;
// Check if post is locked, no new comments
if post.locked {
@ -83,9 +93,13 @@ impl PerformCrud for CreateComment {
.await?
.map_err(|_| ApiError::err("couldnt_create_comment"))?;
updated_comment
.send_create(&local_user_view.person, context)
.await?;
CreateOrUpdateComment::send(
&updated_comment,
&local_user_view.person,
CreateOrUpdateType::Create,
context,
)
.await?;
// Scan the comment for user mentions, add those rows
let post_id = post.id;
@ -113,9 +127,15 @@ impl PerformCrud for CreateComment {
return Err(ApiError::err("couldnt_like_comment").into());
}
updated_comment
.send_like(&local_user_view.person, context)
.await?;
let object = PostOrComment::Comment(Box::new(updated_comment));
Vote::send(
&object,
&local_user_view.person,
community_id,
VoteType::Like,
context,
)
.await?;
let person_id = local_user_view.person.id;
let mut comment_view = blocking(context.pool(), move |conn| {

@ -9,7 +9,7 @@ use lemmy_api_common::{
send_local_notifs,
};
use lemmy_apub::ApubObjectType;
use lemmy_db_queries::{source::comment::Comment_, Crud};
use lemmy_db_queries::{source::comment::Comment_, Crud, DeleteableOrRemoveable};
use lemmy_db_schema::source::{comment::*, moderator::*};
use lemmy_db_views::comment_view::CommentView;
use lemmy_utils::{ApiError, ConnectionId, LemmyError};
@ -47,7 +47,7 @@ impl PerformCrud for DeleteComment {
// Do the delete
let deleted = data.deleted;
let updated_comment = blocking(context.pool(), move |conn| {
let mut updated_comment = blocking(context.pool(), move |conn| {
Comment::update_deleted(conn, comment_id, deleted)
})
.await?
@ -55,6 +55,7 @@ impl PerformCrud for DeleteComment {
// Send the apub message
if deleted {
updated_comment = updated_comment.blank_out_deleted_or_removed_info();
updated_comment
.send_delete(&local_user_view.person, context)
.await?;
@ -67,11 +68,16 @@ impl PerformCrud for DeleteComment {
// Refetch it
let comment_id = data.comment_id;
let person_id = local_user_view.person.id;
let comment_view = blocking(context.pool(), move |conn| {
let mut comment_view = blocking(context.pool(), move |conn| {
CommentView::read(conn, comment_id, Some(person_id))
})
.await??;
// Blank out deleted or removed info
if deleted {
comment_view.comment = comment_view.comment.blank_out_deleted_or_removed_info();
}
// Build the recipients
let comment_view_2 = comment_view.clone();
let mentions = vec![];
@ -136,7 +142,7 @@ impl PerformCrud for RemoveComment {
// Do the remove
let removed = data.removed;
let updated_comment = blocking(context.pool(), move |conn| {
let mut updated_comment = blocking(context.pool(), move |conn| {
Comment::update_removed(conn, comment_id, removed)
})
.await?
@ -156,6 +162,7 @@ impl PerformCrud for RemoveComment {
// Send the apub message
if removed {
updated_comment = updated_comment.blank_out_deleted_or_removed_info();
updated_comment
.send_remove(&local_user_view.person, context)
.await?;
@ -168,11 +175,16 @@ impl PerformCrud for RemoveComment {
// Refetch it
let comment_id = data.comment_id;
let person_id = local_user_view.person.id;
let comment_view = blocking(context.pool(), move |conn| {
let mut comment_view = blocking(context.pool(), move |conn| {
CommentView::read(conn, comment_id, Some(person_id))
})
.await??;
// Blank out deleted or removed info
if removed {
comment_view.comment = comment_view.comment.blank_out_deleted_or_removed_info();
}
// Build the recipients
let comment_view_2 = comment_view.clone();

@ -2,7 +2,7 @@ use crate::PerformCrud;
use actix_web::web::Data;
use lemmy_api_common::{blocking, comment::*, get_local_user_view_from_jwt_opt};
use lemmy_apub::{build_actor_id_from_shortname, EndpointType};
use lemmy_db_queries::{from_opt_str_to_opt_enum, ListingType, SortType};
use lemmy_db_queries::{from_opt_str_to_opt_enum, DeleteableOrRemoveable, ListingType, SortType};
use lemmy_db_views::comment_view::CommentQueryBuilder;
use lemmy_utils::{ApiError, ConnectionId, LemmyError};
use lemmy_websocket::LemmyContext;
@ -36,7 +36,7 @@ impl PerformCrud for GetComments {
let saved_only = data.saved_only;
let page = data.page;
let limit = data.limit;
let comments = blocking(context.pool(), move |conn| {
let mut comments = blocking(context.pool(), move |conn| {
CommentQueryBuilder::create(conn)
.listing_type(listing_type)
.sort(sort)
@ -52,6 +52,14 @@ impl PerformCrud for GetComments {
.await?
.map_err(|_| ApiError::err("couldnt_get_comments"))?;
// Blank out deleted or removed info
for cv in comments
.iter_mut()
.filter(|cv| cv.comment.deleted || cv.comment.removed)
{
cv.comment = cv.to_owned().comment.blank_out_deleted_or_removed_info();
}
Ok(GetCommentsResponse { comments })
}
}

@ -7,8 +7,11 @@ use lemmy_api_common::{
get_local_user_view_from_jwt,
send_local_notifs,
};
use lemmy_apub::ApubObjectType;
use lemmy_db_queries::source::comment::Comment_;
use lemmy_apub::activities::{
comment::create_or_update::CreateOrUpdateComment,
CreateOrUpdateType,
};
use lemmy_db_queries::{source::comment::Comment_, DeleteableOrRemoveable};
use lemmy_db_schema::source::comment::*;
use lemmy_db_views::comment_view::CommentView;
use lemmy_utils::{
@ -59,9 +62,13 @@ impl PerformCrud for EditComment {
.map_err(|_| ApiError::err("couldnt_update_comment"))?;
// Send the apub update
updated_comment
.send_update(&local_user_view.person, context)
.await?;
CreateOrUpdateComment::send(
&updated_comment,
&local_user_view.person,
CreateOrUpdateType::Update,
context,
)
.await?;
// Do the mentions / recipients
let updated_comment_content = updated_comment.content.to_owned();
@ -78,11 +85,16 @@ impl PerformCrud for EditComment {
let comment_id = data.comment_id;
let person_id = local_user_view.person.id;
let comment_view = blocking(context.pool(), move |conn| {
let mut comment_view = blocking(context.pool(), move |conn| {
CommentView::read(conn, comment_id, Some(person_id))
})
.await??;
// Blank out deleted or removed info
if comment_view.comment.deleted || comment_view.comment.removed {
comment_view.comment = comment_view.comment.blank_out_deleted_or_removed_info();
}
let res = CommentResponse {
comment_view,
recipient_ids,

@ -2,7 +2,7 @@ use crate::{community::send_community_websocket, PerformCrud};
use actix_web::web::Data;
use lemmy_api_common::{blocking, community::*, get_local_user_view_from_jwt, is_admin};
use lemmy_apub::CommunityType;
use lemmy_db_queries::{source::community::Community_, Crud};
use lemmy_db_queries::{source::community::Community_, Crud, DeleteableOrRemoveable};
use lemmy_db_schema::source::{
community::*,
moderator::{ModRemoveCommunity, ModRemoveCommunityForm},
@ -50,6 +50,7 @@ impl PerformCrud for DeleteCommunity {
// Send apub messages
if deleted {
updated_community
.blank_out_deleted_or_removed_info()
.send_delete(local_user_view.person.to_owned(), context)
.await?;
} else {
@ -60,11 +61,16 @@ impl PerformCrud for DeleteCommunity {
let community_id = data.community_id;
let person_id = local_user_view.person.id;
let community_view = blocking(context.pool(), move |conn| {
let mut community_view = blocking(context.pool(), move |conn| {
CommunityView::read(conn, community_id, Some(person_id))
})
.await??;
// Blank out deleted or removed info
if deleted {
community_view.community = community_view.community.blank_out_deleted_or_removed_info();
}
let res = CommunityResponse { community_view };
send_community_websocket(
@ -118,18 +124,26 @@ impl PerformCrud for RemoveCommunity {
// Apub messages
if removed {
updated_community.send_remove(context).await?;
updated_community
.blank_out_deleted_or_removed_info()
.send_remove(context)
.await?;
} else {
updated_community.send_undo_remove(context).await?;
}
let community_id = data.community_id;
let person_id = local_user_view.person.id;
let community_view = blocking(context.pool(), move |conn| {
let mut community_view = blocking(context.pool(), move |conn| {
CommunityView::read(conn, community_id, Some(person_id))
})
.await??;
// Blank out deleted or removed info
if removed {
community_view.community = community_view.community.blank_out_deleted_or_removed_info();
}
let res = CommunityResponse { community_view };
send_community_websocket(

@ -2,7 +2,13 @@ use crate::PerformCrud;
use actix_web::web::Data;
use lemmy_api_common::{blocking, community::*, get_local_user_view_from_jwt_opt};
use lemmy_apub::{build_actor_id_from_shortname, EndpointType};
use lemmy_db_queries::{from_opt_str_to_opt_enum, ApubObject, ListingType, SortType};
use lemmy_db_queries::{
from_opt_str_to_opt_enum,
ApubObject,
DeleteableOrRemoveable,
ListingType,
SortType,
};
use lemmy_db_schema::source::community::*;
use lemmy_db_views_actor::{
community_moderator_view::CommunityModeratorView,
@ -39,12 +45,17 @@ impl PerformCrud for GetCommunity {
}
};
let community_view = blocking(context.pool(), move |conn| {
let mut community_view = blocking(context.pool(), move |conn| {
CommunityView::read(conn, community_id, person_id)
})
.await?
.map_err(|_| ApiError::err("couldnt_find_community"))?;
// Blank out deleted or removed info
if community_view.community.deleted || community_view.community.removed {
community_view.community = community_view.community.blank_out_deleted_or_removed_info();
}
let moderators: Vec<CommunityModeratorView> = blocking(context.pool(), move |conn| {
CommunityModeratorView::for_community(conn, community_id)
})
@ -93,7 +104,7 @@ impl PerformCrud for ListCommunities {
let page = data.page;
let limit = data.limit;
let communities = blocking(context.pool(), move |conn| {
let mut communities = blocking(context.pool(), move |conn| {
CommunityQueryBuilder::create(conn)
.listing_type(listing_type)
.sort(sort)
@ -105,6 +116,14 @@ impl PerformCrud for ListCommunities {
})
.await??;
// Blank out deleted or removed info
for cv in communities
.iter_mut()
.filter(|cv| cv.community.deleted || cv.community.removed)
{
cv.community = cv.to_owned().community.blank_out_deleted_or_removed_info();
}
// Return the jwt
Ok(ListCommunitiesResponse { communities })
}

@ -6,7 +6,7 @@ use lemmy_api_common::{
get_local_user_view_from_jwt,
};
use lemmy_apub::CommunityType;
use lemmy_db_queries::{diesel_option_overwrite_to_url, Crud};
use lemmy_db_queries::{diesel_option_overwrite_to_url, Crud, DeleteableOrRemoveable};
use lemmy_db_schema::{
naive_now,
source::community::{Community, CommunityForm},
@ -78,11 +78,16 @@ impl PerformCrud for EditCommunity {
let community_id = data.community_id;
let person_id = local_user_view.person.id;
let community_view = blocking(context.pool(), move |conn| {
let mut community_view = blocking(context.pool(), move |conn| {
CommunityView::read(conn, community_id, Some(person_id))
})
.await??;
// Blank out deleted or removed info
if community_view.community.deleted || community_view.community.removed {
community_view.community = community_view.community.blank_out_deleted_or_removed_info();
}
let res = CommunityResponse { community_view };
send_community_websocket(

@ -7,7 +7,16 @@ use lemmy_api_common::{
mark_post_as_read,
post::*,
};
use lemmy_apub::{generate_apub_endpoint, ApubLikeableType, ApubObjectType, EndpointType};
use lemmy_apub::{
activities::{
post::create_or_update::CreateOrUpdatePost,
voting::vote::{Vote, VoteType},
CreateOrUpdateType,
},
generate_apub_endpoint,
EndpointType,
PostOrComment,
};
use lemmy_db_queries::{source::post::Post_, Crud, Likeable};
use lemmy_db_schema::source::post::*;
use lemmy_db_views::post_view::PostView;
@ -45,6 +54,9 @@ impl PerformCrud for CreatePost {
let data_url = data.url.as_ref();
let (iframely_response, pictrs_thumbnail) =
fetch_iframely_and_pictrs_data(context.client(), data_url).await?;
let (embed_title, embed_description, embed_html) = iframely_response
.map(|u| (u.title, u.description, u.html))
.unwrap_or((None, None, None));
let post_form = PostForm {
name: data.name.trim().to_owned(),
@ -53,9 +65,9 @@ impl PerformCrud for CreatePost {
community_id: data.community_id,
creator_id: local_user_view.person.id,
nsfw: data.nsfw,
embed_title: iframely_response.title,
embed_description: iframely_response.description,
embed_html: iframely_response.html,
embed_title,
embed_description,
embed_html,
thumbnail_url: pictrs_thumbnail.map(|u| u.into()),
..PostForm::default()
};
@ -82,9 +94,13 @@ impl PerformCrud for CreatePost {
.await?
.map_err(|_| ApiError::err("couldnt_create_post"))?;
updated_post
.send_create(&local_user_view.person, context)
.await?;
CreateOrUpdatePost::send(
&updated_post,
&local_user_view.person,
CreateOrUpdateType::Create,
context,
)
.await?;
// They like their own post by default
let person_id = local_user_view.person.id;
@ -103,9 +119,15 @@ impl PerformCrud for CreatePost {
// Mark the post as read
mark_post_as_read(person_id, post_id, context.pool()).await?;
updated_post
.send_like(&local_user_view.person, context)
.await?;
let object = PostOrComment::Post(Box::new(updated_post));
Vote::send(
&object,
&local_user_view.person,
inserted_post.community_id,
VoteType::Like,
context,
)
.await?;
// Refetch the view
let inserted_post_id = inserted_post.id;

@ -8,7 +8,7 @@ use lemmy_api_common::{
post::*,
};
use lemmy_apub::ApubObjectType;
use lemmy_db_queries::{source::post::Post_, Crud};
use lemmy_db_queries::{source::post::Post_, Crud, DeleteableOrRemoveable};
use lemmy_db_schema::source::{moderator::*, post::*};
use lemmy_db_views::post_view::PostView;
use lemmy_utils::{ApiError, ConnectionId, LemmyError};
@ -52,6 +52,7 @@ impl PerformCrud for DeletePost {
// apub updates
if deleted {
updated_post
.blank_out_deleted_or_removed_info()
.send_delete(&local_user_view.person, context)
.await?;
} else {
@ -62,11 +63,15 @@ impl PerformCrud for DeletePost {
// Refetch the post
let post_id = data.post_id;
let post_view = blocking(context.pool(), move |conn| {
let mut post_view = blocking(context.pool(), move |conn| {
PostView::read(conn, post_id, Some(local_user_view.person.id))
})
.await??;
if deleted {
post_view.post = post_view.post.blank_out_deleted_or_removed_info();
}
let res = PostResponse { post_view };
context.chat_server().do_send(SendPost {
@ -132,6 +137,7 @@ impl PerformCrud for RemovePost {
// apub updates
if removed {
updated_post
.blank_out_deleted_or_removed_info()
.send_remove(&local_user_view.person, context)
.await?;
} else {
@ -143,11 +149,16 @@ impl PerformCrud for RemovePost {
// Refetch the post
let post_id = data.post_id;
let person_id = local_user_view.person.id;
let post_view = blocking(context.pool(), move |conn| {
let mut post_view = blocking(context.pool(), move |conn| {
PostView::read(conn, post_id, Some(person_id))
})
.await??;
// Blank out deleted or removed info
if removed {
post_view.post = post_view.post.blank_out_deleted_or_removed_info();
}
let res = PostResponse { post_view };
context.chat_server().do_send(SendPost {

@ -2,7 +2,7 @@ use crate::PerformCrud;
use actix_web::web::Data;
use lemmy_api_common::{blocking, get_local_user_view_from_jwt_opt, mark_post_as_read, post::*};
use lemmy_apub::{build_actor_id_from_shortname, EndpointType};
use lemmy_db_queries::{from_opt_str_to_opt_enum, ListingType, SortType};
use lemmy_db_queries::{from_opt_str_to_opt_enum, DeleteableOrRemoveable, ListingType, SortType};
use lemmy_db_views::{
comment_view::CommentQueryBuilder,
post_view::{PostQueryBuilder, PostView},
@ -32,19 +32,24 @@ impl PerformCrud for GetPost {
let person_id = local_user_view.map(|u| u.person.id);
let id = data.id;
let post_view = blocking(context.pool(), move |conn| {
let mut post_view = blocking(context.pool(), move |conn| {
PostView::read(conn, id, person_id)
})
.await?
.map_err(|_| ApiError::err("couldnt_find_post"))?;
// Blank out deleted info
if post_view.post.deleted || post_view.post.removed {
post_view.post = post_view.post.blank_out_deleted_or_removed_info();
}
// Mark the post as read
if let Some(person_id) = person_id {
mark_post_as_read(person_id, id, context.pool()).await?;
}
let id = data.id;
let comments = blocking(context.pool(), move |conn| {
let mut comments = blocking(context.pool(), move |conn| {
CommentQueryBuilder::create(conn)
.my_person_id(person_id)
.show_bot_accounts(show_bot_accounts)
@ -54,6 +59,14 @@ impl PerformCrud for GetPost {
})
.await??;
// Blank out deleted or removed info
for cv in comments
.iter_mut()
.filter(|cv| cv.comment.deleted || cv.comment.removed)
{
cv.comment = cv.to_owned().comment.blank_out_deleted_or_removed_info();
}
let community_id = post_view.community.id;
let moderators = blocking(context.pool(), move |conn| {
CommunityModeratorView::for_community(conn, community_id)
@ -61,12 +74,17 @@ impl PerformCrud for GetPost {
.await??;
// Necessary for the sidebar
let community_view = blocking(context.pool(), move |conn| {
let mut community_view = blocking(context.pool(), move |conn| {
CommunityView::read(conn, community_id, person_id)
})
.await?
.map_err(|_| ApiError::err("couldnt_find_community"))?;
// Blank out deleted or removed info
if community_view.community.deleted || community_view.community.removed {
community_view.community = community_view.community.blank_out_deleted_or_removed_info();
}
let online = context
.chat_server()
.send(GetPostUsersOnline { post_id: data.id })
@ -119,7 +137,7 @@ impl PerformCrud for GetPosts {
.unwrap_or(None);
let saved_only = data.saved_only;
let posts = blocking(context.pool(), move |conn| {
let mut posts = blocking(context.pool(), move |conn| {
PostQueryBuilder::create(conn)
.listing_type(listing_type)
.sort(sort)
@ -137,6 +155,14 @@ impl PerformCrud for GetPosts {
.await?
.map_err(|_| ApiError::err("couldnt_get_posts"))?;
// Blank out deleted or removed info
for pv in posts
.iter_mut()
.filter(|p| p.post.deleted || p.post.removed)
{
pv.post = pv.to_owned().post.blank_out_deleted_or_removed_info();
}
Ok(GetPostsResponse { posts })
}
}

@ -1,8 +1,8 @@
use crate::PerformCrud;
use actix_web::web::Data;
use lemmy_api_common::{blocking, check_community_ban, get_local_user_view_from_jwt, post::*};
use lemmy_apub::ApubObjectType;
use lemmy_db_queries::{source::post::Post_, Crud};
use lemmy_apub::activities::{post::create_or_update::CreateOrUpdatePost, CreateOrUpdateType};
use lemmy_db_queries::{source::post::Post_, Crud, DeleteableOrRemoveable};
use lemmy_db_schema::{naive_now, source::post::*};
use lemmy_db_views::post_view::PostView;
use lemmy_utils::{
@ -54,6 +54,9 @@ impl PerformCrud for EditPost {
let data_url = data.url.as_ref();
let (iframely_response, pictrs_thumbnail) =
fetch_iframely_and_pictrs_data(context.client(), data_url).await?;
let (embed_title, embed_description, embed_html) = iframely_response
.map(|u| (u.title, u.description, u.html))
.unwrap_or((None, None, None));
let post_form = PostForm {
creator_id: orig_post.creator_id.to_owned(),
@ -63,9 +66,9 @@ impl PerformCrud for EditPost {
body: data.body.to_owned(),
nsfw: data.nsfw,
updated: Some(naive_now()),
embed_title: iframely_response.title,
embed_description: iframely_response.description,
embed_html: iframely_response.html,
embed_title,
embed_description,
embed_html,
thumbnail_url: pictrs_thumbnail.map(|u| u.into()),
..PostForm::default()
};
@ -89,16 +92,25 @@ impl PerformCrud for EditPost {
};
// Send apub update
updated_post
.send_update(&local_user_view.person, context)
.await?;
CreateOrUpdatePost::send(
&updated_post,
&local_user_view.person,
CreateOrUpdateType::Update,
context,
)
.await?;
let post_id = data.post_id;
let post_view = blocking(context.pool(), move |conn| {
let mut post_view = blocking(context.pool(), move |conn| {
PostView::read(conn, post_id, Some(local_user_view.person.id))
})
.await??;
// Blank out deleted info
if post_view.post.deleted || post_view.post.removed {
post_view.post = post_view.post.blank_out_deleted_or_removed_info();
}
let res = PostResponse { post_view };
context.chat_server().do_send(SendPost {

@ -6,7 +6,14 @@ use lemmy_api_common::{
person::{CreatePrivateMessage, PrivateMessageResponse},
send_email_to_user,
};
use lemmy_apub::{generate_apub_endpoint, ApubObjectType, EndpointType};
use lemmy_apub::{
activities::{
private_message::create_or_update::CreateOrUpdatePrivateMessage,
CreateOrUpdateType,
},
generate_apub_endpoint,
EndpointType,
};
use lemmy_db_queries::{source::private_message::PrivateMessage_, Crud};
use lemmy_db_schema::source::private_message::{PrivateMessage, PrivateMessageForm};
use lemmy_db_views::{local_user_view::LocalUserView, private_message_view::PrivateMessageView};
@ -63,9 +70,13 @@ impl PerformCrud for CreatePrivateMessage {
.await?
.map_err(|_| ApiError::err("couldnt_create_private_message"))?;
updated_private_message
.send_create(&local_user_view.person, context)
.await?;
CreateOrUpdatePrivateMessage::send(
&updated_private_message,
&local_user_view.person,
CreateOrUpdateType::Create,
context,
)
.await?;
let private_message_view = blocking(context.pool(), move |conn| {
PrivateMessageView::read(conn, inserted_private_message.id)

@ -6,7 +6,7 @@ use lemmy_api_common::{
person::{DeletePrivateMessage, PrivateMessageResponse},
};
use lemmy_apub::ApubObjectType;
use lemmy_db_queries::{source::private_message::PrivateMessage_, Crud};
use lemmy_db_queries::{source::private_message::PrivateMessage_, Crud, DeleteableOrRemoveable};
use lemmy_db_schema::source::private_message::PrivateMessage;
use lemmy_db_views::{local_user_view::LocalUserView, private_message_view::PrivateMessageView};
use lemmy_utils::{ApiError, ConnectionId, LemmyError};
@ -46,6 +46,7 @@ impl PerformCrud for DeletePrivateMessage {
// Send the apub update
if data.deleted {
updated_private_message
.blank_out_deleted_or_removed_info()
.send_delete(&local_user_view.person, context)
.await?;
} else {
@ -55,11 +56,18 @@ impl PerformCrud for DeletePrivateMessage {
}
let private_message_id = data.private_message_id;
let private_message_view = blocking(context.pool(), move |conn| {
let mut private_message_view = blocking(context.pool(), move |conn| {
PrivateMessageView::read(conn, private_message_id)
})
.await??;
// Blank out deleted or removed info
if deleted {
private_message_view.private_message = private_message_view
.private_message
.blank_out_deleted_or_removed_info();
}
let res = PrivateMessageResponse {
private_message_view,
};

@ -5,6 +5,7 @@ use lemmy_api_common::{
get_local_user_view_from_jwt,
person::{GetPrivateMessages, PrivateMessagesResponse},
};
use lemmy_db_queries::DeleteableOrRemoveable;
use lemmy_db_views::private_message_view::PrivateMessageQueryBuilder;
use lemmy_utils::{ConnectionId, LemmyError};
use lemmy_websocket::LemmyContext;
@ -25,7 +26,7 @@ impl PerformCrud for GetPrivateMessages {
let page = data.page;
let limit = data.limit;
let unread_only = data.unread_only;
let messages = blocking(context.pool(), move |conn| {
let mut messages = blocking(context.pool(), move |conn| {
PrivateMessageQueryBuilder::create(conn, person_id)
.page(page)
.limit(limit)
@ -34,6 +35,17 @@ impl PerformCrud for GetPrivateMessages {
})
.await??;
// Blank out deleted or removed info
for pmv in messages
.iter_mut()
.filter(|pmv| pmv.private_message.deleted)
{
pmv.private_message = pmv
.to_owned()
.private_message
.blank_out_deleted_or_removed_info();
}
Ok(PrivateMessagesResponse {
private_messages: messages,
})

@ -5,8 +5,11 @@ use lemmy_api_common::{
get_local_user_view_from_jwt,
person::{EditPrivateMessage, PrivateMessageResponse},
};
use lemmy_apub::ApubObjectType;
use lemmy_db_queries::{source::private_message::PrivateMessage_, Crud};
use lemmy_apub::activities::{
private_message::create_or_update::CreateOrUpdatePrivateMessage,
CreateOrUpdateType,
};
use lemmy_db_queries::{source::private_message::PrivateMessage_, Crud, DeleteableOrRemoveable};
use lemmy_db_schema::source::private_message::PrivateMessage;
use lemmy_db_views::{local_user_view::LocalUserView, private_message_view::PrivateMessageView};
use lemmy_utils::{utils::remove_slurs, ApiError, ConnectionId, LemmyError};
@ -44,16 +47,27 @@ impl PerformCrud for EditPrivateMessage {
.map_err(|_| ApiError::err("couldnt_update_private_message"))?;
// Send the apub update
updated_private_message
.send_update(&local_user_view.person, context)
.await?;
CreateOrUpdatePrivateMessage::send(
&updated_private_message,
&local_user_view.person,
CreateOrUpdateType::Update,
context,
)
.await?;
let private_message_id = data.private_message_id;
let private_message_view = blocking(context.pool(), move |conn| {
let mut private_message_view = blocking(context.pool(), move |conn| {
PrivateMessageView::read(conn, private_message_id)
})
.await??;
// Blank out deleted or removed info
if private_message_view.private_message.deleted {
private_message_view.private_message = private_message_view
.private_message
.blank_out_deleted_or_removed_info();
}
let res = PrivateMessageResponse {
private_message_view,
};

@ -36,7 +36,7 @@ url = { version = "2.2.2", features = ["serde"] }
percent-encoding = "2.1.0"
openssl = "0.10.35"
http = "0.2.4"
http-signature-normalization-actix = { version = "0.5.0-beta.6", default-features = false, features = ["sha-2"] }
http-signature-normalization-actix = { version = "0.5.0-beta.7", default-features = false, features = ["sha-2"] }
http-signature-normalization-reqwest = { version = "0.2.0", default-features = false, features = ["sha-2"] }
base64 = "0.13.0"
tokio = "1.8.0"

@ -1,71 +0,0 @@
use crate::{
activities::{
comment::{get_notif_recipients, send_websocket_message},
verify_activity,
verify_person_in_community,
},
objects::FromApub,
NoteExt,
};
use activitystreams::{activity::kind::CreateType, base::BaseExt};
use lemmy_apub_lib::{verify_domains_match_opt, ActivityCommonFields, ActivityHandler, PublicUrl};
use lemmy_db_schema::source::comment::Comment;
use lemmy_utils::LemmyError;
use lemmy_websocket::{LemmyContext, UserOperationCrud};
use url::Url;
#[derive(Clone, Debug, serde::Deserialize, serde::Serialize)]
#[serde(rename_all = "camelCase")]
pub struct CreateComment {
to: PublicUrl,
object: NoteExt,
cc: Vec<Url>,
#[serde(rename = "type")]
kind: CreateType,
#[serde(flatten)]
common: ActivityCommonFields,
}
#[async_trait::async_trait(?Send)]
impl ActivityHandler for CreateComment {
async fn verify(
&self,
context: &LemmyContext,
request_counter: &mut i32,
) -> Result<(), LemmyError> {
verify_activity(self.common())?;
verify_person_in_community(&self.common.actor, &self.cc, context, request_counter).await?;
verify_domains_match_opt(&self.common.actor, self.object.id_unchecked())?;
// TODO: should add a check that the correct community is in cc (probably needs changes to
// comment deserialization)
Ok(())
}
async fn receive(
&self,
context: &LemmyContext,
request_counter: &mut i32,
) -> Result<(), LemmyError> {
let comment = Comment::from_apub(
&self.object,
context,
self.common.actor.clone(),
request_counter,
false,
)
.await?;
let recipients =
get_notif_recipients(&self.common.actor, &comment, context, request_counter).await?;
send_websocket_message(
comment.id,
recipients,
UserOperationCrud::CreateComment,
context,
)
.await
}
fn common(&self) -> &ActivityCommonFields {
&self.common
}
}

@ -0,0 +1,131 @@
use crate::{
activities::{
comment::{collect_non_local_mentions, get_notif_recipients, send_websocket_message},
community::announce::AnnouncableActivities,
extract_community,
generate_activity_id,
verify_activity,
verify_person_in_community,
CreateOrUpdateType,
},
activity_queue::send_to_community_new,
extensions::context::lemmy_context,
objects::{comment::Note, FromApub, ToApub},
ActorType,
};
use activitystreams::link::Mention;
use lemmy_api_common::blocking;
use lemmy_apub_lib::{
values::PublicUrl,
verify_domains_match,
ActivityCommonFields,
ActivityHandler,
};
use lemmy_db_queries::Crud;
use lemmy_db_schema::source::{comment::Comment, community::Community, person::Person, post::Post};
use lemmy_utils::LemmyError;
use lemmy_websocket::{LemmyContext, UserOperationCrud};
use serde::{Deserialize, Serialize};
use url::Url;
#[derive(Clone, Debug, Deserialize, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct CreateOrUpdateComment {
to: PublicUrl,
object: Note,
cc: Vec<Url>,
tag: Vec<Mention>,
#[serde(rename = "type")]
kind: CreateOrUpdateType,
#[serde(flatten)]
common: ActivityCommonFields,
}
impl CreateOrUpdateComment {
pub async fn send(
comment: &Comment,
actor: &Person,
kind: CreateOrUpdateType,
context: &LemmyContext,
) -> Result<(), LemmyError> {
// TODO: might be helpful to add a comment method to retrieve community directly
let post_id = comment.post_id;
let post = blocking(context.pool(), move |conn| Post::read(conn, post_id)).await??;
let community_id = post.community_id;
let community = blocking(context.pool(), move |conn| {
Community::read(conn, community_id)
})
.await??;
let id = generate_activity_id(kind.clone())?;
let maa = collect_non_local_mentions(comment, &community, context).await?;
let create_or_update = CreateOrUpdateComment {
to: PublicUrl::Public,
object: comment.to_apub(context.pool()).await?,
cc: maa.ccs,
tag: maa.tags,
kind,
common: ActivityCommonFields {
context: lemmy_context(),
id: id.clone(),
actor: actor.actor_id(),
unparsed: Default::default(),
},
};
let activity = AnnouncableActivities::CreateOrUpdateComment(create_or_update);
send_to_community_new(activity, &id, actor, &community, maa.inboxes, context).await
}
}
#[async_trait::async_trait(?Send)]
impl ActivityHandler for CreateOrUpdateComment {
async fn verify(
&self,
context: &LemmyContext,
request_counter: &mut i32,
) -> Result<(), LemmyError> {
let community = extract_community(&self.cc, context, request_counter).await?;
verify_activity(self.common())?;
verify_person_in_community(
&self.common.actor,
&community.actor_id(),
context,
request_counter,
)
.await?;
verify_domains_match(&self.common.actor, &self.object.id)?;
// TODO: should add a check that the correct community is in cc (probably needs changes to
// comment deserialization)
self.object.verify(context, request_counter).await?;
Ok(())
}
async fn receive(
&self,
context: &LemmyContext,
request_counter: &mut i32,
) -> Result<(), LemmyError> {
let comment = Comment::from_apub(
&self.object,
context,
self.common.actor.clone(),
request_counter,
false,
)
.await?;
let recipients =
get_notif_recipients(&self.common.actor, &comment, context, request_counter).await?;
let notif_type = match self.kind {
CreateOrUpdateType::Create => UserOperationCrud::CreateComment,
CreateOrUpdateType::Update => UserOperationCrud::EditComment,
};
send_websocket_message(comment.id, recipients, notif_type, context).await
}
fn common(&self) -> &ActivityCommonFields {
&self.common
}
}

@ -1,18 +1,30 @@
use crate::fetcher::person::get_or_fetch_and_upsert_person;
use lemmy_api_common::{blocking, comment::CommentResponse, send_local_notifs};
use lemmy_db_queries::Crud;
use crate::{fetcher::person::get_or_fetch_and_upsert_person, ActorType};
use activitystreams::{
base::BaseExt,
link::{LinkExt, Mention},
};
use anyhow::anyhow;
use itertools::Itertools;
use lemmy_api_common::{blocking, comment::CommentResponse, send_local_notifs, WebFingerResponse};
use lemmy_db_queries::{Crud, DbPool};
use lemmy_db_schema::{
source::{comment::Comment, post::Post},
source::{comment::Comment, community::Community, person::Person, post::Post},
CommentId,
LocalUserId,
};
use lemmy_db_views::comment_view::CommentView;
use lemmy_utils::{utils::scrape_text_for_mentions, LemmyError};
use lemmy_utils::{
request::{retry, RecvError},
settings::structs::Settings,
utils::{scrape_text_for_mentions, MentionData},
LemmyError,
};
use lemmy_websocket::{messages::SendComment, LemmyContext};
use log::debug;
use reqwest::Client;
use url::Url;
pub mod create;
pub mod update;
pub mod create_or_update;
async fn get_notif_recipients(
actor: &Url,
@ -63,3 +75,104 @@ pub(crate) async fn send_websocket_message<
Ok(())
}
pub struct MentionsAndAddresses {
pub ccs: Vec<Url>,
pub inboxes: Vec<Url>,
pub tags: Vec<Mention>,
}
/// This takes a comment, and builds a list of to_addresses, inboxes,
/// and mention tags, so they know where to be sent to.
/// Addresses are the persons / addresses that go in the cc field.
pub async fn collect_non_local_mentions(
comment: &Comment,
community: &Community,
context: &LemmyContext,
) -> Result<MentionsAndAddresses, LemmyError> {
let parent_creator = get_comment_parent_creator(context.pool(), comment).await?;
let mut addressed_ccs = vec![community.actor_id(), parent_creator.actor_id()];
// Note: dont include community inbox here, as we send to it separately with `send_to_community()`
let mut inboxes = vec![parent_creator.get_shared_inbox_or_inbox_url()];
// Add the mention tag
let mut tags = Vec::new();
// Get the person IDs for any mentions
let mentions = scrape_text_for_mentions(&comment.content)
.into_iter()
// Filter only the non-local ones
.filter(|m| !m.is_local())
.collect::<Vec<MentionData>>();
for mention in &mentions {
// TODO should it be fetching it every time?
if let Ok(actor_id) = fetch_webfinger_url(mention, context.client()).await {
debug!("mention actor_id: {}", actor_id);
addressed_ccs.push(actor_id.to_owned().to_string().parse()?);
let mention_person = get_or_fetch_and_upsert_person(&actor_id, context, &mut 0).await?;
inboxes.push(mention_person.get_shared_inbox_or_inbox_url());
let mut mention_tag = Mention::new();
mention_tag.set_href(actor_id).set_name(mention.full_name());
tags.push(mention_tag);
}
}
let inboxes = inboxes.into_iter().unique().collect();
Ok(MentionsAndAddresses {
ccs: addressed_ccs,
inboxes,
tags,
})
}
/// Returns the apub ID of the person this comment is responding to. Meaning, in case this is a
/// top-level comment, the creator of the post, otherwise the creator of the parent comment.
async fn get_comment_parent_creator(
pool: &DbPool,
comment: &Comment,
) -> Result<Person, LemmyError> {
let parent_creator_id = if let Some(parent_comment_id) = comment.parent_id {
let parent_comment =
blocking(pool, move |conn| Comment::read(conn, parent_comment_id)).await??;
parent_comment.creator_id
} else {
let parent_post_id = comment.post_id;
let parent_post = blocking(pool, move |conn| Post::read(conn, parent_post_id)).await??;
parent_post.creator_id
};
Ok(blocking(pool, move |conn| Person::read(conn, parent_creator_id)).await??)
}
/// Turns a person id like `@name@example.com` into an apub ID, like `https://example.com/user/name`,
/// using webfinger.
async fn fetch_webfinger_url(mention: &MentionData, client: &Client) -> Result<Url, LemmyError> {
let fetch_url = format!(
"{}://{}/.well-known/webfinger?resource=acct:{}@{}",
Settings::get().get_protocol_string(),
mention.domain,
mention.name,
mention.domain
);
debug!("Fetching webfinger url: {}", &fetch_url);
let response = retry(|| client.get(&fetch_url).send()).await?;
let res: WebFingerResponse = response
.json()
.await
.map_err(|e| RecvError(e.to_string()))?;
let link = res
.links
.iter()
.find(|l| l.type_.eq(&Some("application/activity+json".to_string())))
.ok_or_else(|| anyhow!("No application/activity+json link found."))?;
link
.href
.to_owned()
.ok_or_else(|| anyhow!("No href found.").into())
}

@ -1,56 +0,0 @@
use crate::activities::{comment::send_websocket_message, verify_mod_action};
use activitystreams::activity::kind::RemoveType;
use lemmy_api_common::blocking;
use crate::{check_is_apub_id_valid, fetcher::objects::get_or_fetch_and_insert_comment};
use lemmy_apub_lib::{verify_domains_match, ActivityCommonFields, ActivityHandlerNew, PublicUrl};
use lemmy_db_queries::source::comment::Comment_;
use lemmy_db_schema::source::comment::Comment;
use lemmy_utils::LemmyError;
use lemmy_websocket::{LemmyContext, UserOperationCrud};
use url::Url;
#[derive(Clone, Debug, serde::Deserialize, serde::Serialize)]
#[serde(rename_all = "camelCase")]
pub struct RemoveComment {
to: PublicUrl,
pub(in crate::activities::comment) object: Url,
cc: [Url; 1],
#[serde(rename = "type")]
kind: RemoveType,
#[serde(flatten)]
common: ActivityCommonFields,
}
#[async_trait::async_trait(?Send)]
impl ActivityHandlerNew for RemoveComment {
async fn verify(&self, context: &LemmyContext, _: &mut i32) -> Result<(), LemmyError> {
verify_domains_match(&self.common.actor, self.common.id_unchecked())?;
check_is_apub_id_valid(&self.common.actor, false)?;
verify_mod_action(&self.common.actor, self.cc[0].clone(), context).await
}
async fn receive(
&self,
context: &LemmyContext,
request_counter: &mut i32,
) -> Result<(), LemmyError> {
let comment = get_or_fetch_and_insert_comment(&self.object, context, request_counter).await?;
let removed_comment = blocking(context.pool(), move |conn| {
Comment::update_removed(conn, comment.id, true)
})
.await??;
send_websocket_message(
removed_comment.id,
vec![],
UserOperationCrud::EditComment,
context,
)
.await
}
fn common(&self) -> &ActivityCommonFields {
&self.common
}
}

@ -1,65 +0,0 @@
use crate::activities::{
comment::{remove::RemoveComment, send_websocket_message},
verify_mod_action,
};
use activitystreams::activity::kind::UndoType;
use lemmy_api_common::blocking;
use crate::{check_is_apub_id_valid, fetcher::objects::get_or_fetch_and_insert_comment};
use lemmy_apub_lib::{verify_domains_match, ActivityCommonFields, ActivityHandlerNew, PublicUrl};
use lemmy_db_queries::source::comment::Comment_;
use lemmy_db_schema::source::comment::Comment;
use lemmy_utils::LemmyError;
use lemmy_websocket::{LemmyContext, UserOperationCrud};
use url::Url;
#[derive(Clone, Debug, serde::Deserialize, serde::Serialize)]
#[serde(rename_all = "camelCase")]
pub struct UndoRemoveComment {
to: PublicUrl,
object: RemoveComment,
cc: [Url; 1],
#[serde(rename = "type")]
kind: UndoType,
#[serde(flatten)]
common: ActivityCommonFields,
}
#[async_trait::async_trait(?Send)]
impl ActivityHandlerNew for UndoRemoveComment {
async fn verify(
&self,
context: &LemmyContext,
request_counter: &mut i32,
) -> Result<(), LemmyError> {
verify_domains_match(&self.common.actor, self.common.id_unchecked())?;
check_is_apub_id_valid(&self.common.actor, false)?;
verify_mod_action(&self.common.actor, self.cc[0].clone(), context).await?;
self.object.verify(context, request_counter).await
}
async fn receive(
&self,
context: &LemmyContext,
request_counter: &mut i32,
) -> Result<(), LemmyError> {
let comment =
get_or_fetch_and_insert_comment(&self.object.object, context, request_counter).await?;
let removed_comment = blocking(context.pool(), move |conn| {
Comment::update_removed(conn, comment.id, false)
})
.await??;
send_websocket_message(
removed_comment.id,
vec![],
UserOperationCrud::EditComment,
context,
)
.await
}
fn common(&self) -> &ActivityCommonFields {
&self.common
}
}

@ -1,70 +0,0 @@
use crate::{
activities::{
comment::{get_notif_recipients, send_websocket_message},
verify_activity,
verify_person_in_community,
},
objects::FromApub,
NoteExt,
};
use activitystreams::{activity::kind::UpdateType, base::BaseExt};
use lemmy_apub_lib::{verify_domains_match_opt, ActivityCommonFields, ActivityHandler, PublicUrl};
use lemmy_db_schema::source::comment::Comment;
use lemmy_utils::LemmyError;
use lemmy_websocket::{LemmyContext, UserOperationCrud};
use url::Url;
#[derive(Clone, Debug, serde::Deserialize, serde::Serialize)]
#[serde(rename_all = "camelCase")]
pub struct UpdateComment {
to: PublicUrl,
object: NoteExt,
cc: Vec<Url>,
#[serde(rename = "type")]
kind: UpdateType,
#[serde(flatten)]
common: ActivityCommonFields,
}
#[async_trait::async_trait(?Send)]
impl ActivityHandler for UpdateComment {
async fn verify(
&self,
context: &LemmyContext,
request_counter: &mut i32,
) -> Result<(), LemmyError> {
verify_activity(self.common())?;
verify_person_in_community(&self.common.actor, &self.cc, context, request_counter).await?;
verify_domains_match_opt(&self.common.actor, self.object.id_unchecked())?;
Ok(())
}
async fn receive(
&self,
context: &LemmyContext,
request_counter: &mut i32,
) -> Result<(), LemmyError> {
let comment = Comment::from_apub(
&self.object,
context,
self.common.actor.clone(),
request_counter,
false,
)
.await?;
let recipients =
get_notif_recipients(&self.common.actor, &comment, context, request_counter).await?;
send_websocket_message(
comment.id,
recipients,
UserOperationCrud::EditComment,
context,
)
.await
}
fn common(&self) -> &ActivityCommonFields {
&self.common
}
}

@ -10,7 +10,7 @@ use crate::{
};
use activitystreams::{activity::kind::AddType, base::AnyBase};
use lemmy_api_common::blocking;
use lemmy_apub_lib::{ActivityCommonFields, ActivityHandler, PublicUrl};
use lemmy_apub_lib::{values::PublicUrl, ActivityCommonFields, ActivityHandler};
use lemmy_db_queries::{source::community::CommunityModerator_, Joinable};
use lemmy_db_schema::source::community::{CommunityModerator, CommunityModeratorForm};
use lemmy_utils::LemmyError;
@ -38,7 +38,7 @@ impl ActivityHandler for AddMod {
request_counter: &mut i32,
) -> Result<(), LemmyError> {
verify_activity(self.common())?;
verify_person_in_community(&self.common.actor, &self.cc, context, request_counter).await?;
verify_person_in_community(&self.common.actor, &self.cc[0], context, request_counter).await?;
verify_mod_action(&self.common.actor, self.cc[0].clone(), context).await?;
verify_add_remove_moderator_target(&self.target, self.cc[0].clone())?;
Ok(())

@ -1,34 +1,36 @@
use crate::{
activities::{
comment::{create::CreateComment, update::UpdateComment},
comment::create_or_update::CreateOrUpdateComment,
community::{
add_mod::AddMod,
block_user::BlockUserFromCommunity,
list_community_follower_inboxes,
undo_block_user::UndoBlockUserFromCommunity,
},
deletion::{
delete::DeletePostCommentOrCommunity,
undo_delete::UndoDeletePostCommentOrCommunity,
},
post::{create::CreatePost, update::UpdatePost},
generate_activity_id,
post::create_or_update::CreateOrUpdatePost,
removal::{
remove::RemovePostCommentCommunityOrMod,
undo_remove::UndoRemovePostCommentOrCommunity,
},
verify_activity,
verify_community,
voting::{
dislike::DislikePostOrComment,
like::LikePostOrComment,
undo_dislike::UndoDislikePostOrComment,
undo_like::UndoLikePostOrComment,
},
voting::{undo_vote::UndoVote, vote::Vote},
},
activity_queue::send_activity_new,
extensions::context::lemmy_context,
http::is_activity_already_known,
insert_activity,
ActorType,
CommunityType,
};
use activitystreams::activity::kind::AnnounceType;
use lemmy_apub_lib::{ActivityCommonFields, ActivityHandler, PublicUrl};
use lemmy_apub_lib::{values::PublicUrl, ActivityCommonFields, ActivityHandler};
use lemmy_db_schema::source::community::Community;
use lemmy_utils::LemmyError;
use lemmy_websocket::LemmyContext;
use serde::{Deserialize, Serialize};
@ -37,14 +39,10 @@ use url::Url;
#[derive(Clone, Debug, Deserialize, Serialize, ActivityHandler)]
#[serde(untagged)]
pub enum AnnouncableActivities {
CreateComment(CreateComment),
UpdateComment(UpdateComment),
CreatePost(CreatePost),
UpdatePost(UpdatePost),
LikePostOrComment(LikePostOrComment),
DislikePostOrComment(DislikePostOrComment),
UndoLikePostOrComment(UndoLikePostOrComment),
UndoDislikePostOrComment(UndoDislikePostOrComment),
CreateOrUpdateComment(CreateOrUpdateComment),
CreateOrUpdatePost(Box<CreateOrUpdatePost>),
Vote(Vote),
UndoVote(UndoVote),
DeletePostCommentOrCommunity(DeletePostCommentOrCommunity),
UndoDeletePostCommentOrCommunity(UndoDeletePostCommentOrCommunity),
RemovePostCommentCommunityOrMod(RemovePostCommentCommunityOrMod),
@ -66,6 +64,38 @@ pub struct AnnounceActivity {
common: ActivityCommonFields,
}
impl AnnounceActivity {
pub async fn send(
object: AnnouncableActivities,
community: &Community,
additional_inboxes: Vec<Url>,
context: &LemmyContext,
) -> Result<(), LemmyError> {
let announce = AnnounceActivity {
to: PublicUrl::Public,
object,
cc: vec![community.followers_url()],
kind: AnnounceType::Announce,
common: ActivityCommonFields {
context: lemmy_context(),
id: generate_activity_id(&AnnounceType::Announce)?,
actor: community.actor_id(),
unparsed: Default::default(),
},
};
let inboxes = list_community_follower_inboxes(community, additional_inboxes, context).await?;
send_activity_new(
context,
&announce,
&announce.common.id,
community,
inboxes,
false,
)
.await
}
}
#[async_trait::async_trait(?Send)]
impl ActivityHandler for AnnounceActivity {
async fn verify(

@ -4,7 +4,7 @@ use crate::{
};
use activitystreams::activity::kind::BlockType;
use lemmy_api_common::blocking;
use lemmy_apub_lib::{ActivityCommonFields, ActivityHandler, PublicUrl};
use lemmy_apub_lib::{values::PublicUrl, ActivityCommonFields, ActivityHandler};
use lemmy_db_queries::{Bannable, Followable};
use lemmy_db_schema::source::community::{
CommunityFollower,
@ -36,7 +36,7 @@ impl ActivityHandler for BlockUserFromCommunity {
request_counter: &mut i32,
) -> Result<(), LemmyError> {
verify_activity(self.common())?;
verify_person_in_community(&self.common.actor, &self.cc, context, request_counter).await?;
verify_person_in_community(&self.common.actor, &self.cc[0], context, request_counter).await?;
verify_mod_action(&self.common.actor, self.cc[0].clone(), context).await?;
Ok(())
}

@ -1,8 +1,11 @@
use crate::{check_is_apub_id_valid, CommunityType};
use itertools::Itertools;
use lemmy_api_common::{blocking, community::CommunityResponse};
use lemmy_db_schema::CommunityId;
use lemmy_db_schema::{source::community::Community, CommunityId};
use lemmy_db_views_actor::community_view::CommunityView;
use lemmy_utils::LemmyError;
use lemmy_utils::{settings::structs::Settings, LemmyError};
use lemmy_websocket::{messages::SendCommunityRoomMessage, LemmyContext};
use url::Url;
pub mod add_mod;
pub mod announce;
@ -33,3 +36,23 @@ pub(crate) async fn send_websocket_message<
Ok(())
}
async fn list_community_follower_inboxes(
community: &Community,
additional_inboxes: Vec<Url>,
context: &LemmyContext,
) -> Result<Vec<Url>, LemmyError> {
Ok(
vec![
community.get_follower_inboxes(context.pool()).await?,
additional_inboxes,
]
.iter()
.flatten()
.unique()
.filter(|inbox| inbox.host_str() != Some(&Settings::get().hostname))
.filter(|inbox| check_is_apub_id_valid(inbox, false).is_ok())
.map(|inbox| inbox.to_owned())
.collect(),
)
}

@ -9,7 +9,7 @@ use crate::{
};
use activitystreams::activity::kind::UndoType;
use lemmy_api_common::blocking;
use lemmy_apub_lib::{ActivityCommonFields, ActivityHandler, PublicUrl};
use lemmy_apub_lib::{values::PublicUrl, ActivityCommonFields, ActivityHandler};
use lemmy_db_queries::Bannable;
use lemmy_db_schema::source::community::{CommunityPersonBan, CommunityPersonBanForm};
use lemmy_utils::LemmyError;
@ -36,7 +36,7 @@ impl ActivityHandler for UndoBlockUserFromCommunity {
request_counter: &mut i32,
) -> Result<(), LemmyError> {
verify_activity(self.common())?;
verify_person_in_community(&self.common.actor, &self.cc, context, request_counter).await?;
verify_person_in_community(&self.common.actor, &self.cc[0], context, request_counter).await?;
verify_mod_action(&self.common.actor, self.cc[0].clone(), context).await?;
self.object.verify(context, request_counter).await?;
Ok(())

@ -10,7 +10,7 @@ use crate::{
};
use activitystreams::activity::kind::UpdateType;
use lemmy_api_common::blocking;
use lemmy_apub_lib::{ActivityCommonFields, ActivityHandler, PublicUrl};
use lemmy_apub_lib::{values::PublicUrl, ActivityCommonFields, ActivityHandler};
use lemmy_db_queries::{ApubObject, Crud};
use lemmy_db_schema::source::community::{Community, CommunityForm};
use lemmy_utils::LemmyError;
@ -39,7 +39,7 @@ impl ActivityHandler for UpdateCommunity {
request_counter: &mut i32,
) -> Result<(), LemmyError> {
verify_activity(self.common())?;
verify_person_in_community(&self.common.actor, &self.cc, context, request_counter).await?;
verify_person_in_community(&self.common.actor, &self.cc[0], context, request_counter).await?;
verify_mod_action(&self.common.actor, self.cc[0].clone(), context).await?;
Ok(())
}

@ -18,7 +18,7 @@ use crate::{
};
use activitystreams::activity::kind::DeleteType;
use lemmy_api_common::blocking;
use lemmy_apub_lib::{verify_urls_match, ActivityCommonFields, ActivityHandler, PublicUrl};
use lemmy_apub_lib::{values::PublicUrl, verify_urls_match, ActivityCommonFields, ActivityHandler};
use lemmy_db_queries::{
source::{comment::Comment_, community::Community_, post::Post_},
Crud,
@ -64,7 +64,8 @@ impl ActivityHandler for DeletePostCommentOrCommunity {
}
// deleting a post or comment
else {
verify_person_in_community(&self.common().actor, &self.cc, context, request_counter).await?;
verify_person_in_community(&self.common().actor, &self.cc[0], context, request_counter)
.await?;
let object_creator =
get_post_or_comment_actor_id(&self.object, context, request_counter).await?;
verify_urls_match(&self.common.actor, &object_creator)?;
@ -83,7 +84,7 @@ impl ActivityHandler for DeletePostCommentOrCommunity {
if let Ok(community) = object_community {
if community.local {
// repeat these checks just to be sure
verify_person_in_community(&self.common().actor, &self.cc, context, request_counter)
verify_person_in_community(&self.common().actor, &self.cc[0], context, request_counter)
.await?;
verify_mod_action(&self.common.actor, self.object.clone(), context).await?;
let mod_ =

@ -18,7 +18,7 @@ use crate::{
};
use activitystreams::activity::kind::UndoType;
use lemmy_api_common::blocking;
use lemmy_apub_lib::{verify_urls_match, ActivityCommonFields, ActivityHandler, PublicUrl};
use lemmy_apub_lib::{values::PublicUrl, verify_urls_match, ActivityCommonFields, ActivityHandler};
use lemmy_db_queries::source::{comment::Comment_, community::Community_, post::Post_};
use lemmy_db_schema::source::{comment::Comment, community::Community, post::Post};
use lemmy_utils::LemmyError;
@ -54,7 +54,8 @@ impl ActivityHandler for UndoDeletePostCommentOrCommunity {
}
// restoring a post or comment
else {
verify_person_in_community(&self.common().actor, &self.cc, context, request_counter).await?;
verify_person_in_community(&self.common().actor, &self.cc[0], context, request_counter)
.await?;
verify_urls_match(&self.common.actor, &self.object.common().actor)?;
}
Ok(())
@ -71,7 +72,7 @@ impl ActivityHandler for UndoDeletePostCommentOrCommunity {
if let Ok(community) = object_community {
if community.local {
// repeat these checks just to be sure
verify_person_in_community(&self.common().actor, &self.cc, context, request_counter)
verify_person_in_community(&self.common().actor, &self.cc[0], context, request_counter)
.await?;
verify_mod_action(&self.common.actor, self.object.object.clone(), context).await?;
let mod_ =

@ -13,9 +13,12 @@ use lemmy_db_schema::{
DbUrl,
};
use lemmy_db_views_actor::community_view::CommunityView;
use lemmy_utils::LemmyError;
use lemmy_utils::{settings::structs::Settings, LemmyError};
use lemmy_websocket::LemmyContext;
use url::Url;
use serde::{Deserialize, Serialize};
use strum_macros::ToString;
use url::{ParseError, Url};
use uuid::Uuid;
pub mod comment;
pub mod community;
@ -27,6 +30,12 @@ pub mod removal;
pub mod send;
pub mod voting;
#[derive(Clone, Debug, ToString, Deserialize, Serialize)]
pub enum CreateOrUpdateType {
Create,
Update,
}
/// Checks that the specified Url actually identifies a Person (by fetching it), and that the person
/// doesn't have a site ban.
async fn verify_person(
@ -41,27 +50,34 @@ async fn verify_person(
Ok(())
}
/// Fetches the person and community to verify their type, then checks if person is banned from site
/// or community.
async fn verify_person_in_community(
person_id: &Url,
pub(crate) async fn extract_community(
cc: &[Url],
context: &LemmyContext,
request_counter: &mut i32,
) -> Result<Community, LemmyError> {
let person = get_or_fetch_and_upsert_person(person_id, context, request_counter).await?;
let mut cc_iter = cc.iter();
let community: Community = loop {
loop {
if let Some(cid) = cc_iter.next() {
if let Ok(c) = get_or_fetch_and_upsert_community(cid, context, request_counter).await {
break c;
break Ok(c);
}
} else {
return Err(anyhow!("No community found in cc").into());
}
};
check_community_or_site_ban(&person, community.id, context.pool()).await?;
Ok(community)
}
}
/// Fetches the person and community to verify their type, then checks if person is banned from site
/// or community.
pub(crate) async fn verify_person_in_community(
person_id: &Url,
community_id: &Url,
context: &LemmyContext,
request_counter: &mut i32,
) -> Result<(), LemmyError> {
let community = get_or_fetch_and_upsert_community(community_id, context, request_counter).await?;
let person = get_or_fetch_and_upsert_person(person_id, context, request_counter).await?;
check_community_or_site_ban(&person, community.id, context.pool()).await
}
/// Simply check that the url actually refers to a valid group.
@ -80,13 +96,16 @@ fn verify_activity(common: &ActivityCommonFields) -> Result<(), LemmyError> {
Ok(())
}
async fn verify_mod_action(
/// Verify that the actor is a community mod. This check is only run if the community is local,
/// because in case of remote communities, admins can also perform mod actions. As admin status
/// is not federated, we cant verify their actions remotely.
pub(crate) async fn verify_mod_action(
actor_id: &Url,
activity_cc: Url,
community: Url,
context: &LemmyContext,
) -> Result<(), LemmyError> {
let community = blocking(context.pool(), move |conn| {
Community::read_from_apub_id(conn, &activity_cc.into())
Community::read_from_apub_id(conn, &community.into())
})
.await??;
@ -120,3 +139,18 @@ fn verify_add_remove_moderator_target(target: &Url, community: Url) -> Result<()
}
Ok(())
}
/// Generate a unique ID for an activity, in the format:
/// `http(s)://example.com/receive/create/202daf0a-1489-45df-8d2e-c8a3173fed36`
fn generate_activity_id<T>(kind: T) -> Result<Url, ParseError>
where
T: ToString,
{
let id = format!(
"{}/activities/{}/{}",
Settings::get().get_protocol_and_hostname(),
kind.to_string().to_lowercase(),
Uuid::new_v4()
);
Url::parse(&id)
}

@ -1,62 +0,0 @@
use crate::{
activities::{post::send_websocket_message, verify_activity, verify_person_in_community},
fetcher::person::get_or_fetch_and_upsert_person,
objects::FromApub,
ActorType,
PageExt,
};
use activitystreams::{activity::kind::CreateType, base::BaseExt};
use lemmy_apub_lib::{verify_domains_match_opt, ActivityCommonFields, ActivityHandler, PublicUrl};
use lemmy_db_schema::source::post::Post;
use lemmy_utils::LemmyError;
use lemmy_websocket::{LemmyContext, UserOperationCrud};
use url::Url;
#[derive(Clone, Debug, serde::Deserialize, serde::Serialize)]
#[serde(rename_all = "camelCase")]
pub struct CreatePost {
to: PublicUrl,
object: PageExt,
cc: Vec<Url>,
#[serde(rename = "type")]
kind: CreateType,
#[serde(flatten)]
common: ActivityCommonFields,
}
#[async_trait::async_trait(?Send)]
impl ActivityHandler for CreatePost {
async fn verify(
&self,
context: &LemmyContext,
request_counter: &mut i32,
) -> Result<(), LemmyError> {
verify_activity(self.common())?;
verify_person_in_community(&self.common.actor, &self.cc, context, request_counter).await?;
verify_domains_match_opt(&self.common.actor, self.object.id_unchecked())?;
Ok(())
}
async fn receive(
&self,
context: &LemmyContext,
request_counter: &mut i32,
) -> Result<(), LemmyError> {
let actor =
get_or_fetch_and_upsert_person(&self.common.actor, context, request_counter).await?;
let post = Post::from_apub(
&self.object,
context,
actor.actor_id(),
request_counter,
false,
)
.await?;
send_websocket_message(post.id, UserOperationCrud::CreatePost, context).await
}
fn common(&self) -> &ActivityCommonFields {
&self.common
}
}

@ -0,0 +1,142 @@
use crate::{
activities::{
community::announce::AnnouncableActivities,
extract_community,
generate_activity_id,
post::send_websocket_message,
verify_activity,
verify_mod_action,
verify_person_in_community,
CreateOrUpdateType,
},
activity_queue::send_to_community_new,
extensions::context::lemmy_context,
fetcher::person::get_or_fetch_and_upsert_person,
objects::{post::Page, FromApub, ToApub},
ActorType,
};
use anyhow::anyhow;
use lemmy_api_common::blocking;
use lemmy_apub_lib::{
values::PublicUrl,
verify_domains_match,
verify_urls_match,
ActivityCommonFields,
ActivityHandler,
};
use lemmy_db_queries::Crud;
use lemmy_db_schema::source::{community::Community, person::Person, post::Post};
use lemmy_utils::LemmyError;
use lemmy_websocket::{LemmyContext, UserOperationCrud};
use url::Url;
#[derive(Clone, Debug, serde::Deserialize, serde::Serialize)]
#[serde(rename_all = "camelCase")]
pub struct CreateOrUpdatePost {
to: PublicUrl,
object: Page,
cc: [Url; 1],
#[serde(rename = "type")]
kind: CreateOrUpdateType,
#[serde(flatten)]
common: ActivityCommonFields,
}
impl CreateOrUpdatePost {
pub async fn send(
post: &Post,
actor: &Person,
kind: CreateOrUpdateType,
context: &LemmyContext,
) -> Result<(), LemmyError> {
let community_id = post.community_id;
let community = blocking(context.pool(), move |conn| {
Community::read(conn, community_id)
})
.await??;
let id = generate_activity_id(kind.clone())?;
let create_or_update = CreateOrUpdatePost {
to: PublicUrl::Public,
object: post.to_apub(context.pool()).await?,
cc: [community.actor_id()],
kind,
common: ActivityCommonFields {
context: lemmy_context(),
id: id.clone(),
actor: actor.actor_id(),
unparsed: Default::default(),
},
};
let activity = AnnouncableActivities::CreateOrUpdatePost(Box::new(create_or_update));
send_to_community_new(activity, &id, actor, &community, vec![], context).await
}
}
#[async_trait::async_trait(?Send)]
impl ActivityHandler for CreateOrUpdatePost {
async fn verify(
&self,
context: &LemmyContext,
request_counter: &mut i32,
) -> Result<(), LemmyError> {
verify_activity(self.common())?;
let community = extract_community(&self.cc, context, request_counter).await?;
let community_id = community.actor_id();
verify_person_in_community(&self.common.actor, &community_id, context, request_counter).await?;
match self.kind {
CreateOrUpdateType::Create => {
verify_domains_match(&self.common.actor, &self.object.id)?;
verify_urls_match(&self.common.actor, &self.object.attributed_to)?;
// Check that the post isnt locked or stickied, as that isnt possible for newly created posts.
// However, when fetching a remote post we generate a new create activity with the current
// locked/stickied value, so this check may fail. So only check if its a local community,
// because then we will definitely receive all create and update activities separately.
let is_stickied_or_locked =
self.object.stickied == Some(true) || self.object.comments_enabled == Some(false);
if community.local && is_stickied_or_locked {
return Err(anyhow!("New post cannot be stickied or locked").into());
}
}
CreateOrUpdateType::Update => {
let is_mod_action = self.object.is_mod_action(context.pool()).await?;
if is_mod_action {
verify_mod_action(&self.common.actor, community_id, context).await?;
} else {
verify_domains_match(&self.common.actor, &self.object.id)?;
verify_urls_match(&self.common.actor, &self.object.attributed_to)?;
}
}
}
self.object.verify(context, request_counter).await?;
Ok(())
}
async fn receive(
&self,
context: &LemmyContext,
request_counter: &mut i32,
) -> Result<(), LemmyError> {
let actor =
get_or_fetch_and_upsert_person(&self.common.actor, context, request_counter).await?;
let post = Post::from_apub(
&self.object,
context,
actor.actor_id(),
request_counter,
false,
)
.await?;
let notif_type = match self.kind {
CreateOrUpdateType::Create => UserOperationCrud::CreatePost,
CreateOrUpdateType::Update => UserOperationCrud::EditPost,
};
send_websocket_message(post.id, notif_type, context).await
}
fn common(&self) -> &ActivityCommonFields {
&self.common
}
}

@ -4,8 +4,7 @@ use lemmy_db_views::post_view::PostView;
use lemmy_utils::LemmyError;
use lemmy_websocket::{messages::SendPost, LemmyContext};
pub mod create;
pub mod update;
pub mod create_or_update;
pub(crate) async fn send_websocket_message<
OP: ToString + Send + lemmy_websocket::OperationType + 'static,

@ -1,96 +0,0 @@
use crate::{
activities::{
post::send_websocket_message,
verify_activity,
verify_mod_action,
verify_person_in_community,
},
objects::{FromApub, FromApubToForm},
ActorType,
PageExt,
};
use activitystreams::{activity::kind::UpdateType, base::BaseExt};
use anyhow::Context;
use lemmy_api_common::blocking;
use lemmy_apub_lib::{verify_domains_match_opt, ActivityCommonFields, ActivityHandler, PublicUrl};
use lemmy_db_queries::ApubObject;
use lemmy_db_schema::{
source::post::{Post, PostForm},
DbUrl,
};
use lemmy_utils::{location_info, LemmyError};
use lemmy_websocket::{LemmyContext, UserOperationCrud};
use url::Url;
#[derive(Clone, Debug, serde::Deserialize, serde::Serialize)]
#[serde(rename_all = "camelCase")]
pub struct UpdatePost {
to: PublicUrl,
object: PageExt,
cc: Vec<Url>,
#[serde(rename = "type")]
kind: UpdateType,
#[serde(flatten)]
common: ActivityCommonFields,
}
#[async_trait::async_trait(?Send)]
impl ActivityHandler for UpdatePost {
async fn verify(
&self,
context: &LemmyContext,
request_counter: &mut i32,
) -> Result<(), LemmyError> {
verify_activity(self.common())?;
let community =
verify_person_in_community(&self.common.actor, &self.cc, context, request_counter).await?;
let temp_post = PostForm::from_apub(
&self.object,
context,
self.common.actor.clone(),
request_counter,
true,
)
.await?;
let post_id: DbUrl = temp_post.ap_id.context(location_info!())?;
let old_post = blocking(context.pool(), move |conn| {
Post::read_from_apub_id(conn, &post_id)
})
.await??;
let stickied = temp_post.stickied.context(location_info!())?;
let locked = temp_post.locked.context(location_info!())?;
// community mod changed locked/sticky status
if (stickied != old_post.stickied) || (locked != old_post.locked) {
verify_mod_action(&self.common.actor, community.actor_id(), context).await?;
}
// user edited their own post
else {
verify_domains_match_opt(&self.common.actor, self.object.id_unchecked())?;
}
Ok(())
}
async fn receive(
&self,
context: &LemmyContext,
request_counter: &mut i32,
) -> Result<(), LemmyError> {
let post = Post::from_apub(
&self.object,
context,
self.common.actor.clone(),
request_counter,
// TODO: we already check here if the mod action is valid, can remove that check param
true,
)
.await?;
send_websocket_message(post.id, UserOperationCrud::EditPost, context).await
}
fn common(&self) -> &ActivityCommonFields {
&self.common
}
}

@ -1,64 +0,0 @@
use crate::{
activities::{private_message::send_websocket_message, verify_activity, verify_person},
objects::FromApub,
NoteExt,
};
use activitystreams::{activity::kind::CreateType, base::BaseExt};
use lemmy_apub_lib::{verify_domains_match_opt, ActivityCommonFields, ActivityHandler};
use lemmy_db_schema::source::private_message::PrivateMessage;
use lemmy_utils::LemmyError;
use lemmy_websocket::{LemmyContext, UserOperationCrud};
use url::Url;
#[derive(Clone, Debug, serde::Deserialize, serde::Serialize)]
#[serde(rename_all = "camelCase")]
pub struct CreatePrivateMessage {
to: Url,
object: NoteExt,
#[serde(rename = "type")]
kind: CreateType,
#[serde(flatten)]
common: ActivityCommonFields,
}
#[async_trait::async_trait(?Send)]
impl ActivityHandler for CreatePrivateMessage {
async fn verify(
&self,
context: &LemmyContext,
request_counter: &mut i32,
) -> Result<(), LemmyError> {
verify_activity(self.common())?;
verify_person(&self.common.actor, context, request_counter).await?;
verify_domains_match_opt(&self.common.actor, self.object.id_unchecked())?;
Ok(())
}
async fn receive(
&self,
context: &LemmyContext,
request_counter: &mut i32,
) -> Result<(), LemmyError> {
let private_message = PrivateMessage::from_apub(
&self.object,
context,
self.common.actor.clone(),
request_counter,
false,
)
.await?;
send_websocket_message(
private_message.id,
UserOperationCrud::CreatePrivateMessage,
context,
)
.await?;
Ok(())
}
fn common(&self) -> &ActivityCommonFields {
&self.common
}
}

@ -0,0 +1,107 @@
use crate::{
activities::{
generate_activity_id,
private_message::send_websocket_message,
verify_activity,
verify_person,
CreateOrUpdateType,
},
activity_queue::send_activity_new,
extensions::context::lemmy_context,
objects::{private_message::Note, FromApub, ToApub},
ActorType,
};
use lemmy_api_common::blocking;
use lemmy_apub_lib::{verify_domains_match, ActivityCommonFields, ActivityHandler};
use lemmy_db_queries::Crud;
use lemmy_db_schema::source::{person::Person, private_message::PrivateMessage};
use lemmy_utils::LemmyError;
use lemmy_websocket::{LemmyContext, UserOperationCrud};
use url::Url;
#[derive(Clone, Debug, serde::Deserialize, serde::Serialize)]
#[serde(rename_all = "camelCase")]
pub struct CreateOrUpdatePrivateMessage {
to: Url,
object: Note,
#[serde(rename = "type")]
kind: CreateOrUpdateType,
#[serde(flatten)]
common: ActivityCommonFields,
}
impl CreateOrUpdatePrivateMessage {
pub async fn send(
private_message: &PrivateMessage,
actor: &Person,
kind: CreateOrUpdateType,
context: &LemmyContext,
) -> Result<(), LemmyError> {
let recipient_id = private_message.recipient_id;
let recipient =
blocking(context.pool(), move |conn| Person::read(conn, recipient_id)).await??;
let id = generate_activity_id(kind.clone())?;
let create_or_update = CreateOrUpdatePrivateMessage {
to: recipient.actor_id(),
object: private_message.to_apub(context.pool()).await?,
kind,
common: ActivityCommonFields {
context: lemmy_context(),
id: id.clone(),
actor: actor.actor_id(),
unparsed: Default::default(),
},
};
send_activity_new(
context,
&create_or_update,
&id,
actor,
vec![recipient.get_shared_inbox_or_inbox_url()],
true,
)
.await
}
}
#[async_trait::async_trait(?Send)]
impl ActivityHandler for CreateOrUpdatePrivateMessage {
async fn verify(
&self,
context: &LemmyContext,
request_counter: &mut i32,
) -> Result<(), LemmyError> {
verify_activity(self.common())?;
verify_person(&self.common.actor, context, request_counter).await?;
verify_domains_match(&self.common.actor, &self.object.id)?;
self.object.verify(context, request_counter).await?;
Ok(())
}
async fn receive(
&self,
context: &LemmyContext,
request_counter: &mut i32,
) -> Result<(), LemmyError> {
let private_message = PrivateMessage::from_apub(
&self.object,
context,
self.common.actor.clone(),
request_counter,
false,
)
.await?;
let notif_type = match self.kind {
CreateOrUpdateType::Create => UserOperationCrud::CreatePrivateMessage,
CreateOrUpdateType::Update => UserOperationCrud::EditPrivateMessage,
};
send_websocket_message(private_message.id, notif_type, context).await?;
Ok(())
}
fn common(&self) -> &ActivityCommonFields {
&self.common
}
}

@ -4,10 +4,9 @@ use lemmy_db_views::{local_user_view::LocalUserView, private_message_view::Priva
use lemmy_utils::LemmyError;
use lemmy_websocket::{messages::SendUserRoomMessage, LemmyContext, UserOperationCrud};
pub mod create;
pub mod create_or_update;
pub mod delete;
pub mod undo_delete;
pub mod update;
async fn send_websocket_message(
private_message_id: PrivateMessageId,

@ -1,64 +0,0 @@
use crate::{
activities::{private_message::send_websocket_message, verify_activity, verify_person},
objects::FromApub,
NoteExt,
};
use activitystreams::{activity::kind::UpdateType, base::BaseExt};
use lemmy_apub_lib::{verify_domains_match_opt, ActivityCommonFields, ActivityHandler};
use lemmy_db_schema::source::private_message::PrivateMessage;
use lemmy_utils::LemmyError;
use lemmy_websocket::{LemmyContext, UserOperationCrud};
use url::Url;
#[derive(Clone, Debug, serde::Deserialize, serde::Serialize)]
#[serde(rename_all = "camelCase")]
pub struct UpdatePrivateMessage {
to: Url,
object: NoteExt,
#[serde(rename = "type")]
kind: UpdateType,
#[serde(flatten)]
common: ActivityCommonFields,
}
#[async_trait::async_trait(?Send)]
impl ActivityHandler for UpdatePrivateMessage {
async fn verify(
&self,
context: &LemmyContext,
request_counter: &mut i32,
) -> Result<(), LemmyError> {
verify_activity(self.common())?;
verify_person(&self.common.actor, context, request_counter).await?;
verify_domains_match_opt(&self.common.actor, self.object.id_unchecked())?;
Ok(())
}
async fn receive(
&self,
context: &LemmyContext,
request_counter: &mut i32,
) -> Result<(), LemmyError> {
let private_message = PrivateMessage::from_apub(
&self.object,
context,
self.common.actor.clone(),
request_counter,
false,
)
.await?;
send_websocket_message(
private_message.id,
UserOperationCrud::EditPrivateMessage,
context,
)
.await?;
Ok(())
}
fn common(&self) -> &ActivityCommonFields {
&self.common
}
}

@ -19,7 +19,7 @@ use crate::{
use activitystreams::{activity::kind::RemoveType, base::AnyBase};
use anyhow::anyhow;
use lemmy_api_common::blocking;
use lemmy_apub_lib::{ActivityCommonFields, ActivityHandler, PublicUrl};
use lemmy_apub_lib::{values::PublicUrl, ActivityCommonFields, ActivityHandler};
use lemmy_db_queries::{
source::{comment::Comment_, community::Community_, post::Post_},
Joinable,
@ -64,13 +64,13 @@ impl ActivityHandler for RemovePostCommentCommunityOrMod {
}
// removing community mod
else if let Some(target) = &self.target {
verify_person_in_community(&self.common.actor, &self.cc, context, request_counter).await?;
verify_person_in_community(&self.common.actor, &self.cc[0], context, request_counter).await?;
verify_mod_action(&self.common.actor, self.cc[0].clone(), context).await?;
verify_add_remove_moderator_target(target, self.cc[0].clone())?;
}
// removing a post or comment
else {
verify_person_in_community(&self.common.actor, &self.cc, context, request_counter).await?;
verify_person_in_community(&self.common.actor, &self.cc[0], context, request_counter).await?;
verify_mod_action(&self.common.actor, self.cc[0].clone(), context).await?;
}
Ok(())

@ -17,7 +17,7 @@ use crate::{
use activitystreams::activity::kind::UndoType;
use anyhow::anyhow;
use lemmy_api_common::blocking;
use lemmy_apub_lib::{ActivityCommonFields, ActivityHandler, PublicUrl};
use lemmy_apub_lib::{values::PublicUrl, ActivityCommonFields, ActivityHandler};
use lemmy_db_queries::source::{comment::Comment_, community::Community_, post::Post_};
use lemmy_db_schema::source::{comment::Comment, community::Community, post::Post};
use lemmy_utils::LemmyError;
@ -52,7 +52,7 @@ impl ActivityHandler for UndoRemovePostCommentOrCommunity {
}
// removing a post or comment
else {
verify_person_in_community(&self.common.actor, &self.cc, context, request_counter).await?;
verify_person_in_community(&self.common.actor, &self.cc[0], context, request_counter).await?;
verify_mod_action(&self.common.actor, self.cc[0].clone(), context).await?;
}
self.object.verify(context, request_counter).await?;

@ -1,114 +1,28 @@
use crate::{
activities::send::generate_activity_id,
activity_queue::{send_comment_mentions, send_to_community},
activities::generate_activity_id,
activity_queue::send_to_community,
extensions::context::lemmy_context,
fetcher::person::get_or_fetch_and_upsert_person,
objects::ToApub,
ActorType,
ApubLikeableType,
ApubObjectType,
};
use activitystreams::{
activity::{
kind::{CreateType, DeleteType, DislikeType, LikeType, RemoveType, UndoType, UpdateType},
Create,
kind::{DeleteType, RemoveType, UndoType},
Delete,
Dislike,
Like,
Remove,
Undo,
Update,
},
base::AnyBase,
link::Mention,
prelude::*,
public,
};
use anyhow::anyhow;
use itertools::Itertools;
use lemmy_api_common::{blocking, WebFingerResponse};
use lemmy_db_queries::{Crud, DbPool};
use lemmy_api_common::blocking;
use lemmy_db_queries::Crud;
use lemmy_db_schema::source::{comment::Comment, community::Community, person::Person, post::Post};
use lemmy_utils::{
request::{retry, RecvError},
settings::structs::Settings,
utils::{scrape_text_for_mentions, MentionData},
LemmyError,
};
use lemmy_utils::LemmyError;
use lemmy_websocket::LemmyContext;
use log::debug;
use reqwest::Client;
use serde_json::Error;
use url::Url;
#[async_trait::async_trait(?Send)]
impl ApubObjectType for Comment {
/// Send out information about a newly created comment, to the followers of the community and
/// mentioned persons.
async fn send_create(&self, creator: &Person, context: &LemmyContext) -> Result<(), LemmyError> {
let note = self.to_apub(context.pool()).await?;
let post_id = self.post_id;
let post = blocking(context.pool(), move |conn| Post::read(conn, post_id)).await??;
let community_id = post.community_id;
let community = blocking(context.pool(), move |conn| {
Community::read(conn, community_id)
})
.await??;
let maa = collect_non_local_mentions(self, &community, context).await?;
let mut create = Create::new(
creator.actor_id.to_owned().into_inner(),
note.into_any_base()?,
);
create
.set_many_contexts(lemmy_context()?)
.set_id(generate_activity_id(CreateType::Create)?)
.set_to(public())
.set_many_ccs(maa.ccs.to_owned())
// Set the mention tags
.set_many_tags(maa.get_tags()?);
send_to_community(create.clone(), creator, &community, None, context).await?;
send_comment_mentions(creator, maa.inboxes, create, context).await?;
Ok(())
}
/// Send out information about an edited post, to the followers of the community and mentioned
/// persons.
async fn send_update(&self, creator: &Person, context: &LemmyContext) -> Result<(), LemmyError> {
let note = self.to_apub(context.pool()).await?;
let post_id = self.post_id;
let post = blocking(context.pool(), move |conn| Post::read(conn, post_id)).await??;
let community_id = post.community_id;
let community = blocking(context.pool(), move |conn| {
Community::read(conn, community_id)
})
.await??;
let maa = collect_non_local_mentions(self, &community, context).await?;
let mut update = Update::new(
creator.actor_id.to_owned().into_inner(),
note.into_any_base()?,
);
update
.set_many_contexts(lemmy_context()?)
.set_id(generate_activity_id(UpdateType::Update)?)
.set_to(public())
.set_many_ccs(maa.ccs.to_owned())
// Set the mention tags
.set_many_tags(maa.get_tags()?);
send_to_community(update.clone(), creator, &community, None, context).await?;
send_comment_mentions(creator, maa.inboxes, update, context).await?;
Ok(())
}
async fn send_delete(&self, creator: &Person, context: &LemmyContext) -> Result<(), LemmyError> {
let post_id = self.post_id;
let post = blocking(context.pool(), move |conn| Post::read(conn, post_id)).await??;
@ -124,7 +38,7 @@ impl ApubObjectType for Comment {
self.ap_id.to_owned().into_inner(),
);
delete
.set_many_contexts(lemmy_context()?)
.set_many_contexts(lemmy_context())
.set_id(generate_activity_id(DeleteType::Delete)?)
.set_to(public())
.set_many_ccs(vec![community.actor_id()]);
@ -153,7 +67,7 @@ impl ApubObjectType for Comment {
self.ap_id.to_owned().into_inner(),
);
delete
.set_many_contexts(lemmy_context()?)
.set_many_contexts(lemmy_context())
.set_id(generate_activity_id(DeleteType::Delete)?)
.set_to(public())
.set_many_ccs(vec![community.actor_id()]);
@ -164,7 +78,7 @@ impl ApubObjectType for Comment {
delete.into_any_base()?,
);
undo
.set_many_contexts(lemmy_context()?)
.set_many_contexts(lemmy_context())
.set_id(generate_activity_id(UndoType::Undo)?)
.set_to(public())
.set_many_ccs(vec![community.actor_id()]);
@ -188,7 +102,7 @@ impl ApubObjectType for Comment {
self.ap_id.to_owned().into_inner(),
);
remove
.set_many_contexts(lemmy_context()?)
.set_many_contexts(lemmy_context())
.set_id(generate_activity_id(RemoveType::Remove)?)
.set_to(public())
.set_many_ccs(vec![community.actor_id()]);
@ -217,7 +131,7 @@ impl ApubObjectType for Comment {
self.ap_id.to_owned().into_inner(),
);
remove
.set_many_contexts(lemmy_context()?)
.set_many_contexts(lemmy_context())
.set_id(generate_activity_id(RemoveType::Remove)?)
.set_to(public())
.set_many_ccs(vec![community.actor_id()]);
@ -228,7 +142,7 @@ impl ApubObjectType for Comment {
remove.into_any_base()?,
);
undo
.set_many_contexts(lemmy_context()?)
.set_many_contexts(lemmy_context())
.set_id(generate_activity_id(UndoType::Undo)?)
.set_to(public())
.set_many_ccs(vec![community.actor_id()]);
@ -237,204 +151,3 @@ impl ApubObjectType for Comment {
Ok(())
}
}
#[async_trait::async_trait(?Send)]
impl ApubLikeableType for Comment {
async fn send_like(&self, creator: &Person, context: &LemmyContext) -> Result<(), LemmyError> {
let post_id = self.post_id;
let post = blocking(context.pool(), move |conn| Post::read(conn, post_id)).await??;
let community_id = post.community_id;
let community = blocking(context.pool(), move |conn| {
Community::read(conn, community_id)
})
.await??;
let mut like = Like::new(
creator.actor_id.to_owned().into_inner(),
self.ap_id.to_owned().into_inner(),
);
like
.set_many_contexts(lemmy_context()?)
.set_id(generate_activity_id(LikeType::Like)?)
.set_to(public())
.set_many_ccs(vec![community.actor_id()]);
send_to_community(like, creator, &community, None, context).await?;
Ok(())
}
async fn send_dislike(&self, creator: &Person, context: &LemmyContext) -> Result<(), LemmyError> {
let post_id = self.post_id;
let post = blocking(context.pool(), move |conn| Post::read(conn, post_id)).await??;
let community_id = post.community_id;
let community = blocking(context.pool(), move |conn| {
Community::read(conn, community_id)
})
.await??;
let mut dislike = Dislike::new(
creator.actor_id.to_owned().into_inner(),
self.ap_id.to_owned().into_inner(),
);
dislike
.set_many_contexts(lemmy_context()?)
.set_id(generate_activity_id(DislikeType::Dislike)?)
.set_to(public())
.set_many_ccs(vec![community.actor_id()]);
send_to_community(dislike, creator, &community, None, context).await?;
Ok(())
}
async fn send_undo_like(
&self,
creator: &Person,
context: &LemmyContext,
) -> Result<(), LemmyError> {
let post_id = self.post_id;
let post = blocking(context.pool(), move |conn| Post::read(conn, post_id)).await??;
let community_id = post.community_id;
let community = blocking(context.pool(), move |conn| {
Community::read(conn, community_id)
})
.await??;
let mut like = Like::new(
creator.actor_id.to_owned().into_inner(),
self.ap_id.to_owned().into_inner(),
);
like
.set_many_contexts(lemmy_context()?)
.set_id(generate_activity_id(DislikeType::Dislike)?)
.set_to(public())
.set_many_ccs(vec![community.actor_id()]);
// Undo that fake activity
let mut undo = Undo::new(
creator.actor_id.to_owned().into_inner(),
like.into_any_base()?,
);
undo
.set_many_contexts(lemmy_context()?)
.set_id(generate_activity_id(UndoType::Undo)?)
.set_to(public())
.set_many_ccs(vec![community.actor_id()]);
send_to_community(undo, creator, &community, None, context).await?;
Ok(())
}
}
struct MentionsAndAddresses {
ccs: Vec<Url>,
inboxes: Vec<Url>,
tags: Vec<Mention>,
}
impl MentionsAndAddresses {
fn get_tags(&self) -> Result<Vec<AnyBase>, Error> {
self
.tags
.iter()
.map(|t| t.to_owned().into_any_base())
.collect::<Result<Vec<AnyBase>, Error>>()
}
}
/// This takes a comment, and builds a list of to_addresses, inboxes,
/// and mention tags, so they know where to be sent to.
/// Addresses are the persons / addresses that go in the cc field.
async fn collect_non_local_mentions(
comment: &Comment,
community: &Community,
context: &LemmyContext,
) -> Result<MentionsAndAddresses, LemmyError> {
let parent_creator = get_comment_parent_creator(context.pool(), comment).await?;
let mut addressed_ccs = vec![community.actor_id(), parent_creator.actor_id()];
// Note: dont include community inbox here, as we send to it separately with `send_to_community()`
let mut inboxes = vec![parent_creator.get_shared_inbox_or_inbox_url()];
// Add the mention tag
let mut tags = Vec::new();
// Get the person IDs for any mentions
let mentions = scrape_text_for_mentions(&comment.content)
.into_iter()
// Filter only the non-local ones
.filter(|m| !m.is_local())
.collect::<Vec<MentionData>>();
for mention in &mentions {
// TODO should it be fetching it every time?
if let Ok(actor_id) = fetch_webfinger_url(mention, context.client()).await {
debug!("mention actor_id: {}", actor_id);
addressed_ccs.push(actor_id.to_owned().to_string().parse()?);
let mention_person = get_or_fetch_and_upsert_person(&actor_id, context, &mut 0).await?;
inboxes.push(mention_person.get_shared_inbox_or_inbox_url());
let mut mention_tag = Mention::new();
mention_tag.set_href(actor_id).set_name(mention.full_name());
tags.push(mention_tag);
}
}
let inboxes = inboxes.into_iter().unique().collect();
Ok(MentionsAndAddresses {
ccs: addressed_ccs,
inboxes,
tags,
})
}
/// Returns the apub ID of the person this comment is responding to. Meaning, in case this is a
/// top-level comment, the creator of the post, otherwise the creator of the parent comment.
async fn get_comment_parent_creator(
pool: &DbPool,
comment: &Comment,
) -> Result<Person, LemmyError> {
let parent_creator_id = if let Some(parent_comment_id) = comment.parent_id {
let parent_comment =
blocking(pool, move |conn| Comment::read(conn, parent_comment_id)).await??;
parent_comment.creator_id
} else {
let parent_post_id = comment.post_id;
let parent_post = blocking(pool, move |conn| Post::read(conn, parent_post_id)).await??;
parent_post.creator_id
};
Ok(blocking(pool, move |conn| Person::read(conn, parent_creator_id)).await??)
}
/// Turns a person id like `@name@example.com` into an apub ID, like `https://example.com/user/name`,
/// using webfinger.
async fn fetch_webfinger_url(mention: &MentionData, client: &Client) -> Result<Url, LemmyError> {
let fetch_url = format!(
"{}://{}/.well-known/webfinger?resource=acct:{}@{}",
Settings::get().get_protocol_string(),
mention.domain,
mention.name,
mention.domain
);
debug!("Fetching webfinger url: {}", &fetch_url);
let response = retry(|| client.get(&fetch_url).send()).await?;
let res: WebFingerResponse = response
.json()
.await
.map_err(|e| RecvError(e.to_string()))?;
let link = res
.links
.iter()
.find(|l| l.type_.eq(&Some("application/activity+json".to_string())))
.ok_or_else(|| anyhow!("No application/activity+json link found."))?;
link
.href
.to_owned()
.ok_or_else(|| anyhow!("No href found.").into())
}

@ -1,5 +1,5 @@
use crate::{
activities::send::generate_activity_id,
activities::generate_activity_id,
activity_queue::{send_activity_single_dest, send_to_community, send_to_community_followers},
check_is_apub_id_valid,
extensions::context::lemmy_context,
@ -54,7 +54,7 @@ impl ActorType for Community {
self.local
}
fn actor_id(&self) -> Url {
self.actor_id.to_owned().into_inner()
self.actor_id.to_owned().into()
}
fn name(&self) -> String {
self.name.clone()
@ -78,7 +78,7 @@ impl ActorType for Community {
#[async_trait::async_trait(?Send)]
impl CommunityType for Community {
fn followers_url(&self) -> Url {
self.followers_url.clone().into_inner()
self.followers_url.clone().into()
}
/// As a local community, accept the follow request from a remote person.
@ -98,7 +98,7 @@ impl CommunityType for Community {
follow.into_any_base()?,
);
accept
.set_many_contexts(lemmy_context()?)
.set_many_contexts(lemmy_context())
.set_id(generate_activity_id(AcceptType::Accept)?)
.set_to(person.actor_id());
@ -117,7 +117,7 @@ impl CommunityType for Community {
self.to_apub(context.pool()).await?.into_any_base()?,
);
update
.set_many_contexts(lemmy_context()?)
.set_many_contexts(lemmy_context())
.set_id(generate_activity_id(UpdateType::Update)?)
.set_to(public())
.set_many_ccs(vec![self.actor_id()]);
@ -134,7 +134,7 @@ impl CommunityType for Community {
if self.local {
let mut delete = Delete::new(self.actor_id(), self.actor_id());
delete
.set_many_contexts(lemmy_context()?)
.set_many_contexts(lemmy_context())
.set_id(generate_activity_id(DeleteType::Delete)?)
.set_to(public())
.set_many_ccs(vec![self.followers_url()]);
@ -145,7 +145,7 @@ impl CommunityType for Community {
else {
let mut delete = Delete::new(mod_.actor_id(), self.actor_id());
delete
.set_many_contexts(lemmy_context()?)
.set_many_contexts(lemmy_context())
.set_id(generate_activity_id(DeleteType::Delete)?)
.set_to(public())
.set_many_ccs(vec![self.actor_id()]);
@ -163,14 +163,14 @@ impl CommunityType for Community {
if self.local {
let mut delete = Delete::new(self.actor_id(), self.actor_id());
delete
.set_many_contexts(lemmy_context()?)
.set_many_contexts(lemmy_context())
.set_id(generate_activity_id(DeleteType::Delete)?)
.set_to(public())
.set_many_ccs(vec![self.followers_url()]);
let mut undo = Undo::new(self.actor_id(), delete.into_any_base()?);
undo
.set_many_contexts(lemmy_context()?)
.set_many_contexts(lemmy_context())
.set_id(generate_activity_id(UndoType::Undo)?)
.set_to(public())
.set_many_ccs(vec![self.followers_url()]);
@ -181,14 +181,14 @@ impl CommunityType for Community {
else {
let mut delete = Delete::new(mod_.actor_id(), self.actor_id());
delete
.set_many_contexts(lemmy_context()?)
.set_many_contexts(lemmy_context())
.set_id(generate_activity_id(DeleteType::Delete)?)
.set_to(public())
.set_many_ccs(vec![self.actor_id()]);
let mut undo = Undo::new(mod_.actor_id(), delete.into_any_base()?);
undo
.set_many_contexts(lemmy_context()?)
.set_many_contexts(lemmy_context())
.set_id(generate_activity_id(UndoType::Undo)?)
.set_to(public())
.set_many_ccs(vec![self.actor_id()]);
@ -202,7 +202,7 @@ impl CommunityType for Community {
async fn send_remove(&self, context: &LemmyContext) -> Result<(), LemmyError> {
let mut remove = Remove::new(self.actor_id(), self.actor_id());
remove
.set_many_contexts(lemmy_context()?)
.set_many_contexts(lemmy_context())
.set_id(generate_activity_id(RemoveType::Remove)?)
.set_to(public())
.set_many_ccs(vec![self.followers_url()]);
@ -215,7 +215,7 @@ impl CommunityType for Community {
async fn send_undo_remove(&self, context: &LemmyContext) -> Result<(), LemmyError> {
let mut remove = Remove::new(self.actor_id(), self.actor_id());
remove
.set_many_contexts(lemmy_context()?)
.set_many_contexts(lemmy_context())
.set_id(generate_activity_id(RemoveType::Remove)?)
.set_to(public())
.set_many_ccs(vec![self.followers_url()]);
@ -223,7 +223,7 @@ impl CommunityType for Community {
// Undo that fake activity
let mut undo = Undo::new(self.actor_id(), remove.into_any_base()?);
undo
.set_many_contexts(lemmy_context()?)
.set_many_contexts(lemmy_context())
.set_id(generate_activity_id(LikeType::Like)?)
.set_to(public())
.set_many_ccs(vec![self.followers_url()]);
@ -267,7 +267,7 @@ impl CommunityType for Community {
}
let mut announce = Announce::new(self.actor_id(), activity);
announce
.set_many_contexts(lemmy_context()?)
.set_many_contexts(lemmy_context())
.set_id(generate_activity_id(AnnounceType::Announce)?)
.set_to(public())
.set_many_ccs(ccs);
@ -306,7 +306,7 @@ impl CommunityType for Community {
) -> Result<(), LemmyError> {
let mut add = Add::new(actor.actor_id(), added_mod.actor_id());
add
.set_many_contexts(lemmy_context()?)
.set_many_contexts(lemmy_context())
.set_id(generate_activity_id(AddType::Add)?)
.set_to(public())
.set_many_ccs(vec![self.actor_id()])
@ -324,7 +324,7 @@ impl CommunityType for Community {
) -> Result<(), LemmyError> {
let mut remove = Remove::new(actor.actor_id(), removed_mod.actor_id());
remove
.set_many_contexts(lemmy_context()?)
.set_many_contexts(lemmy_context())
.set_id(generate_activity_id(RemoveType::Remove)?)
.set_to(public())
.set_many_ccs(vec![self.actor_id()])
@ -342,7 +342,7 @@ impl CommunityType for Community {
) -> Result<(), LemmyError> {
let mut block = Block::new(actor.actor_id(), blocked_user.actor_id());
block
.set_many_contexts(lemmy_context()?)
.set_many_contexts(lemmy_context())
.set_id(generate_activity_id(BlockType::Block)?)
.set_to(public())
.set_many_ccs(vec![self.actor_id()]);
@ -359,7 +359,7 @@ impl CommunityType for Community {
) -> Result<(), LemmyError> {
let mut block = Block::new(actor.actor_id(), unblocked_user.actor_id());
block
.set_many_contexts(lemmy_context()?)
.set_many_contexts(lemmy_context())
.set_id(generate_activity_id(BlockType::Block)?)
.set_to(public())
.set_many_ccs(vec![self.actor_id()]);
@ -367,7 +367,7 @@ impl CommunityType for Community {
// Undo that fake activity
let mut undo = Undo::new(actor.actor_id(), block.into_any_base()?);
undo
.set_many_contexts(lemmy_context()?)
.set_many_contexts(lemmy_context())
.set_id(generate_activity_id(UndoType::Undo)?)
.set_to(public())
.set_many_ccs(vec![self.actor_id()]);

@ -1,24 +1,5 @@
use lemmy_utils::settings::structs::Settings;
use url::{ParseError, Url};
use uuid::Uuid;
pub(crate) mod comment;
pub(crate) mod community;
pub(crate) mod person;
pub(crate) mod post;
pub(crate) mod private_message;
/// Generate a unique ID for an activity, in the format:
/// `http(s)://example.com/receive/create/202daf0a-1489-45df-8d2e-c8a3173fed36`
fn generate_activity_id<T>(kind: T) -> Result<Url, ParseError>
where
T: ToString,
{
let id = format!(
"{}/activities/{}/{}",
Settings::get().get_protocol_and_hostname(),
kind.to_string().to_lowercase(),
Uuid::new_v4()
);
Url::parse(&id)
}

@ -1,5 +1,5 @@
use crate::{
activities::send::generate_activity_id,
activities::generate_activity_id,
activity_queue::send_activity_single_dest,
extensions::context::lemmy_context,
ActorType,
@ -78,7 +78,7 @@ impl UserType for Person {
let mut follow = Follow::new(self.actor_id(), community.actor_id());
follow
.set_many_contexts(lemmy_context()?)
.set_many_contexts(lemmy_context())
.set_id(generate_activity_id(FollowType::Follow)?)
.set_to(community.actor_id());
@ -99,7 +99,7 @@ impl UserType for Person {
let mut follow = Follow::new(self.actor_id(), community.actor_id());
follow
.set_many_contexts(lemmy_context()?)
.set_many_contexts(lemmy_context())
.set_id(generate_activity_id(FollowType::Follow)?)
.set_to(community.actor_id());
@ -109,7 +109,7 @@ impl UserType for Person {
follow.into_any_base()?,
);
undo
.set_many_contexts(lemmy_context()?)
.set_many_contexts(lemmy_context())
.set_id(generate_activity_id(UndoType::Undo)?)
.set_to(community.actor_id());

@ -1,22 +1,16 @@
use crate::{
activities::send::generate_activity_id,
activities::generate_activity_id,
activity_queue::send_to_community,
extensions::context::lemmy_context,
objects::ToApub,
ActorType,
ApubLikeableType,
ApubObjectType,
};
use activitystreams::{
activity::{
kind::{CreateType, DeleteType, DislikeType, LikeType, RemoveType, UndoType, UpdateType},
Create,
kind::{DeleteType, RemoveType, UndoType},
Delete,
Dislike,
Like,
Remove,
Undo,
Update,
},
prelude::*,
public,
@ -29,54 +23,6 @@ use lemmy_websocket::LemmyContext;
#[async_trait::async_trait(?Send)]
impl ApubObjectType for Post {
/// Send out information about a newly created post, to the followers of the community.
async fn send_create(&self, creator: &Person, context: &LemmyContext) -> Result<(), LemmyError> {
let page = self.to_apub(context.pool()).await?;
let community_id = self.community_id;
let community = blocking(context.pool(), move |conn| {
Community::read(conn, community_id)
})
.await??;
let mut create = Create::new(
creator.actor_id.to_owned().into_inner(),
page.into_any_base()?,
);
create
.set_many_contexts(lemmy_context()?)
.set_id(generate_activity_id(CreateType::Create)?)
.set_to(public())
.set_many_ccs(vec![community.actor_id()]);
send_to_community(create, creator, &community, None, context).await?;
Ok(())
}
/// Send out information about an edited post, to the followers of the community.
async fn send_update(&self, creator: &Person, context: &LemmyContext) -> Result<(), LemmyError> {
let page = self.to_apub(context.pool()).await?;
let community_id = self.community_id;
let community = blocking(context.pool(), move |conn| {
Community::read(conn, community_id)
})
.await??;
let mut update = Update::new(
creator.actor_id.to_owned().into_inner(),
page.into_any_base()?,
);
update
.set_many_contexts(lemmy_context()?)
.set_id(generate_activity_id(UpdateType::Update)?)
.set_to(public())
.set_many_ccs(vec![community.actor_id()]);
send_to_community(update, creator, &community, None, context).await?;
Ok(())
}
async fn send_delete(&self, creator: &Person, context: &LemmyContext) -> Result<(), LemmyError> {
let community_id = self.community_id;
let community = blocking(context.pool(), move |conn| {
@ -89,7 +35,7 @@ impl ApubObjectType for Post {
self.ap_id.to_owned().into_inner(),
);
delete
.set_many_contexts(lemmy_context()?)
.set_many_contexts(lemmy_context())
.set_id(generate_activity_id(DeleteType::Delete)?)
.set_to(public())
.set_many_ccs(vec![community.actor_id()]);
@ -114,7 +60,7 @@ impl ApubObjectType for Post {
self.ap_id.to_owned().into_inner(),
);
delete
.set_many_contexts(lemmy_context()?)
.set_many_contexts(lemmy_context())
.set_id(generate_activity_id(DeleteType::Delete)?)
.set_to(public())
.set_many_ccs(vec![community.actor_id()]);
@ -125,7 +71,7 @@ impl ApubObjectType for Post {
delete.into_any_base()?,
);
undo
.set_many_contexts(lemmy_context()?)
.set_many_contexts(lemmy_context())
.set_id(generate_activity_id(UndoType::Undo)?)
.set_to(public())
.set_many_ccs(vec![community.actor_id()]);
@ -146,7 +92,7 @@ impl ApubObjectType for Post {
self.ap_id.to_owned().into_inner(),
);
remove
.set_many_contexts(lemmy_context()?)
.set_many_contexts(lemmy_context())
.set_id(generate_activity_id(RemoveType::Remove)?)
.set_to(public())
.set_many_ccs(vec![community.actor_id()]);
@ -171,7 +117,7 @@ impl ApubObjectType for Post {
self.ap_id.to_owned().into_inner(),
);
remove
.set_many_contexts(lemmy_context()?)
.set_many_contexts(lemmy_context())
.set_id(generate_activity_id(RemoveType::Remove)?)
.set_to(public())
.set_many_ccs(vec![community.actor_id()]);
@ -182,7 +128,7 @@ impl ApubObjectType for Post {
remove.into_any_base()?,
);
undo
.set_many_contexts(lemmy_context()?)
.set_many_contexts(lemmy_context())
.set_id(generate_activity_id(UndoType::Undo)?)
.set_to(public())
.set_many_ccs(vec![community.actor_id()]);
@ -191,84 +137,3 @@ impl ApubObjectType for Post {
Ok(())
}
}
#[async_trait::async_trait(?Send)]
impl ApubLikeableType for Post {
async fn send_like(&self, creator: &Person, context: &LemmyContext) -> Result<(), LemmyError> {
let community_id = self.community_id;
let community = blocking(context.pool(), move |conn| {
Community::read(conn, community_id)
})
.await??;
let mut like = Like::new(
creator.actor_id.to_owned().into_inner(),
self.ap_id.to_owned().into_inner(),
);
like
.set_many_contexts(lemmy_context()?)
.set_id(generate_activity_id(LikeType::Like)?)
.set_to(public())
.set_many_ccs(vec![community.actor_id()]);
send_to_community(like, creator, &community, None, context).await?;
Ok(())
}
async fn send_dislike(&self, creator: &Person, context: &LemmyContext) -> Result<(), LemmyError> {
let community_id = self.community_id;
let community = blocking(context.pool(), move |conn| {
Community::read(conn, community_id)
})
.await??;
let mut dislike = Dislike::new(
creator.actor_id.to_owned().into_inner(),
self.ap_id.to_owned().into_inner(),
);
dislike
.set_many_contexts(lemmy_context()?)
.set_id(generate_activity_id(DislikeType::Dislike)?)
.set_to(public())
.set_many_ccs(vec![community.actor_id()]);
send_to_community(dislike, creator, &community, None, context).await?;
Ok(())
}
async fn send_undo_like(
&self,
creator: &Person,
context: &LemmyContext,
) -> Result<(), LemmyError> {
let community_id = self.community_id;
let community = blocking(context.pool(), move |conn| {
Community::read(conn, community_id)
})
.await??;
let mut like = Like::new(
creator.actor_id.to_owned().into_inner(),
self.ap_id.to_owned().into_inner(),
);
like
.set_many_contexts(lemmy_context()?)
.set_id(generate_activity_id(LikeType::Like)?)
.set_to(public())
.set_many_ccs(vec![community.actor_id()]);
// Undo that fake activity
let mut undo = Undo::new(
creator.actor_id.to_owned().into_inner(),
like.into_any_base()?,
);
undo
.set_many_contexts(lemmy_context()?)
.set_id(generate_activity_id(UndoType::Undo)?)
.set_to(public())
.set_many_ccs(vec![community.actor_id()]);
send_to_community(undo, creator, &community, None, context).await?;
Ok(())
}
}

@ -1,20 +1,18 @@
use crate::{
activities::send::generate_activity_id,
activities::generate_activity_id,
activity_queue::send_activity_single_dest,
extensions::context::lemmy_context,
objects::ToApub,
ActorType,
ApubObjectType,
};
use activitystreams::{
activity::{
kind::{CreateType, DeleteType, UndoType, UpdateType},
Create,
kind::{DeleteType, UndoType},
Delete,
Undo,
Update,
},
prelude::*,
base::{BaseExt, ExtendsExt},
object::ObjectExt,
};
use lemmy_api_common::blocking;
use lemmy_db_queries::Crud;
@ -24,49 +22,6 @@ use lemmy_websocket::LemmyContext;
#[async_trait::async_trait(?Send)]
impl ApubObjectType for PrivateMessage {
/// Send out information about a newly created private message
async fn send_create(&self, creator: &Person, context: &LemmyContext) -> Result<(), LemmyError> {
let note = self.to_apub(context.pool()).await?;
let recipient_id = self.recipient_id;
let recipient =
blocking(context.pool(), move |conn| Person::read(conn, recipient_id)).await??;
let mut create = Create::new(
creator.actor_id.to_owned().into_inner(),
note.into_any_base()?,
);
create
.set_many_contexts(lemmy_context()?)
.set_id(generate_activity_id(CreateType::Create)?)
.set_to(recipient.actor_id());
send_activity_single_dest(create, creator, recipient.inbox_url.into(), context).await?;
Ok(())
}
/// Send out information about an edited private message, to the followers of the community.
async fn send_update(&self, creator: &Person, context: &LemmyContext) -> Result<(), LemmyError> {
let note = self.to_apub(context.pool()).await?;
let recipient_id = self.recipient_id;
let recipient =
blocking(context.pool(), move |conn| Person::read(conn, recipient_id)).await??;
let mut update = Update::new(
creator.actor_id.to_owned().into_inner(),
note.into_any_base()?,
);
update
.set_many_contexts(lemmy_context()?)
.set_id(generate_activity_id(UpdateType::Update)?)
.set_to(recipient.actor_id());
send_activity_single_dest(update, creator, recipient.inbox_url.into(), context).await?;
Ok(())
}
async fn send_delete(&self, creator: &Person, context: &LemmyContext) -> Result<(), LemmyError> {
let recipient_id = self.recipient_id;
let recipient =
@ -77,7 +32,7 @@ impl ApubObjectType for PrivateMessage {
self.ap_id.to_owned().into_inner(),
);
delete
.set_many_contexts(lemmy_context()?)
.set_many_contexts(lemmy_context())
.set_id(generate_activity_id(DeleteType::Delete)?)
.set_to(recipient.actor_id());
@ -99,7 +54,7 @@ impl ApubObjectType for PrivateMessage {
self.ap_id.to_owned().into_inner(),
);
delete
.set_many_contexts(lemmy_context()?)
.set_many_contexts(lemmy_context())
.set_id(generate_activity_id(DeleteType::Delete)?)
.set_to(recipient.actor_id());
@ -109,7 +64,7 @@ impl ApubObjectType for PrivateMessage {
delete.into_any_base()?,
);
undo
.set_many_contexts(lemmy_context()?)
.set_many_contexts(lemmy_context())
.set_id(generate_activity_id(UndoType::Undo)?)
.set_to(recipient.actor_id());

@ -1,54 +0,0 @@
use crate::activities::{
verify_activity,
verify_person_in_community,
voting::receive_like_or_dislike,
};
use activitystreams::activity::kind::DislikeType;
use lemmy_apub_lib::{ActivityCommonFields, ActivityHandler, PublicUrl};
use lemmy_utils::LemmyError;
use lemmy_websocket::LemmyContext;
use url::Url;
#[derive(Clone, Debug, serde::Deserialize, serde::Serialize)]
#[serde(rename_all = "camelCase")]
pub struct DislikePostOrComment {
to: PublicUrl,
pub(in crate::activities) object: Url,
cc: [Url; 1],
#[serde(rename = "type")]
kind: DislikeType,
#[serde(flatten)]
common: ActivityCommonFields,
}
#[async_trait::async_trait(?Send)]
impl ActivityHandler for DislikePostOrComment {
async fn verify(
&self,
context: &LemmyContext,
request_counter: &mut i32,
) -> Result<(), LemmyError> {
verify_activity(self.common())?;
verify_person_in_community(&self.common.actor, &self.cc, context, request_counter).await?;
Ok(())
}
async fn receive(
&self,
context: &LemmyContext,
request_counter: &mut i32,
) -> Result<(), LemmyError> {
receive_like_or_dislike(
-1,
&self.common.actor,
&self.object,
context,
request_counter,
)
.await
}
fn common(&self) -> &ActivityCommonFields {
&self.common
}
}

@ -1,54 +0,0 @@
use crate::activities::{
verify_activity,
verify_person_in_community,
voting::receive_like_or_dislike,
};
use activitystreams::activity::kind::LikeType;
use lemmy_apub_lib::{ActivityCommonFields, ActivityHandler, PublicUrl};
use lemmy_utils::LemmyError;
use lemmy_websocket::LemmyContext;
use url::Url;
#[derive(Clone, Debug, serde::Deserialize, serde::Serialize)]
#[serde(rename_all = "camelCase")]
pub struct LikePostOrComment {
to: PublicUrl,
pub(in crate::activities::voting) object: Url,
cc: [Url; 1],
#[serde(rename = "type")]
kind: LikeType,
#[serde(flatten)]
common: ActivityCommonFields,
}
#[async_trait::async_trait(?Send)]
impl ActivityHandler for LikePostOrComment {
async fn verify(
&self,
context: &LemmyContext,
request_counter: &mut i32,
) -> Result<(), LemmyError> {
verify_activity(self.common())?;
verify_person_in_community(&self.common.actor, &self.cc, context, request_counter).await?;
Ok(())
}
async fn receive(
&self,
context: &LemmyContext,
request_counter: &mut i32,
) -> Result<(), LemmyError> {
receive_like_or_dislike(
1,
&self.common.actor,
&self.object,
context,
request_counter,
)
.await
}
fn common(&self) -> &ActivityCommonFields {
&self.common
}
}

@ -1,62 +1,33 @@
use crate::{
activities::{
comment::send_websocket_message as send_comment_message,
post::send_websocket_message as send_post_message,
},
fetcher::{
objects::get_or_fetch_and_insert_post_or_comment,
person::get_or_fetch_and_upsert_person,
},
PostOrComment,
use crate::activities::{
comment::send_websocket_message as send_comment_message,
post::send_websocket_message as send_post_message,
voting::vote::VoteType,
};
use lemmy_api_common::blocking;
use lemmy_db_queries::Likeable;
use lemmy_db_schema::source::{
comment::{Comment, CommentLike, CommentLikeForm},
person::Person,
post::{Post, PostLike, PostLikeForm},
};
use lemmy_utils::LemmyError;
use lemmy_websocket::{LemmyContext, UserOperation};
use std::ops::Deref;
use url::Url;
pub mod dislike;
pub mod like;
pub mod undo_dislike;
pub mod undo_like;
pub mod undo_vote;
pub mod vote;
pub(in crate::activities::voting) async fn receive_like_or_dislike(
score: i16,
actor: &Url,
object: &Url,
context: &LemmyContext,
request_counter: &mut i32,
) -> Result<(), LemmyError> {
match get_or_fetch_and_insert_post_or_comment(object, context, request_counter).await? {
PostOrComment::Post(p) => {
like_or_dislike_post(score, actor, p.deref(), context, request_counter).await
}
PostOrComment::Comment(c) => {
like_or_dislike_comment(score, actor, c.deref(), context, request_counter).await
}
}
}
async fn like_or_dislike_comment(
score: i16,
actor: &Url,
async fn vote_comment(
vote_type: &VoteType,
actor: Person,
comment: &Comment,
context: &LemmyContext,
request_counter: &mut i32,
) -> Result<(), LemmyError> {
let actor = get_or_fetch_and_upsert_person(actor, context, request_counter).await?;
let comment_id = comment.id;
let like_form = CommentLikeForm {
comment_id,
post_id: comment.post_id,
person_id: actor.id,
score,
score: vote_type.into(),
};
let person_id = actor.id;
blocking(context.pool(), move |conn| {
@ -74,20 +45,17 @@ async fn like_or_dislike_comment(
.await
}
async fn like_or_dislike_post(
score: i16,
actor: &Url,
async fn vote_post(
vote_type: &VoteType,
actor: Person,
post: &Post,
context: &LemmyContext,
request_counter: &mut i32,
) -> Result<(), LemmyError> {
let actor = get_or_fetch_and_upsert_person(actor, context, request_counter).await?;
let post_id = post.id;
let like_form = PostLikeForm {
post_id: post.id,
person_id: actor.id,
score,
score: vote_type.into(),
};
let person_id = actor.id;
blocking(context.pool(), move |conn| {
@ -99,30 +67,11 @@ async fn like_or_dislike_post(
send_post_message(post.id, UserOperation::CreatePostLike, context).await
}
pub(in crate::activities::voting) async fn receive_undo_like_or_dislike(
actor: &Url,
object: &Url,
context: &LemmyContext,
request_counter: &mut i32,
) -> Result<(), LemmyError> {
match get_or_fetch_and_insert_post_or_comment(object, context, request_counter).await? {
PostOrComment::Post(p) => {
undo_like_or_dislike_post(actor, p.deref(), context, request_counter).await
}
PostOrComment::Comment(c) => {
undo_like_or_dislike_comment(actor, c.deref(), context, request_counter).await
}
}
}
async fn undo_like_or_dislike_comment(
actor: &Url,
async fn undo_vote_comment(
actor: Person,
comment: &Comment,
context: &LemmyContext,
request_counter: &mut i32,
) -> Result<(), LemmyError> {
let actor = get_or_fetch_and_upsert_person(actor, context, request_counter).await?;
let comment_id = comment.id;
let person_id = actor.id;
blocking(context.pool(), move |conn| {
@ -139,14 +88,11 @@ async fn undo_like_or_dislike_comment(
.await
}
async fn undo_like_or_dislike_post(
actor: &Url,
async fn undo_vote_post(
actor: Person,
post: &Post,
context: &LemmyContext,
request_counter: &mut i32,
) -> Result<(), LemmyError> {
let actor = get_or_fetch_and_upsert_person(actor, context, request_counter).await?;
let post_id = post.id;
let person_id = actor.id;
blocking(context.pool(), move |conn| {

@ -1,55 +0,0 @@
use crate::activities::{
verify_activity,
verify_person_in_community,
voting::{dislike::DislikePostOrComment, receive_undo_like_or_dislike},
};
use activitystreams::activity::kind::UndoType;
use lemmy_apub_lib::{verify_urls_match, ActivityCommonFields, ActivityHandler, PublicUrl};
use lemmy_utils::LemmyError;
use lemmy_websocket::LemmyContext;
use url::Url;
#[derive(Clone, Debug, serde::Deserialize, serde::Serialize)]
#[serde(rename_all = "camelCase")]
pub struct UndoDislikePostOrComment {
to: PublicUrl,
object: DislikePostOrComment,
cc: [Url; 1],
#[serde(rename = "type")]
kind: UndoType,
#[serde(flatten)]
common: ActivityCommonFields,
}
#[async_trait::async_trait(?Send)]
impl ActivityHandler for UndoDislikePostOrComment {
async fn verify(
&self,
context: &LemmyContext,
request_counter: &mut i32,
) -> Result<(), LemmyError> {
verify_activity(self.common())?;
verify_person_in_community(&self.common.actor, &self.cc, context, request_counter).await?;
verify_urls_match(&self.common.actor, &self.object.common().actor)?;
self.object.verify(context, request_counter).await?;
Ok(())
}
async fn receive(
&self,
context: &LemmyContext,
request_counter: &mut i32,
) -> Result<(), LemmyError> {
receive_undo_like_or_dislike(
&self.common.actor,
&self.object.object,
context,
request_counter,
)
.await
}
fn common(&self) -> &ActivityCommonFields {
&self.common
}
}

@ -1,55 +0,0 @@
use crate::activities::{
verify_activity,
verify_person_in_community,
voting::{like::LikePostOrComment, receive_undo_like_or_dislike},
};
use activitystreams::activity::kind::UndoType;
use lemmy_apub_lib::{verify_urls_match, ActivityCommonFields, ActivityHandler, PublicUrl};
use lemmy_utils::LemmyError;
use lemmy_websocket::LemmyContext;
use url::Url;
#[derive(Clone, Debug, serde::Deserialize, serde::Serialize)]
#[serde(rename_all = "camelCase")]
pub struct UndoLikePostOrComment {
to: PublicUrl,
object: LikePostOrComment,
cc: [Url; 1],
#[serde(rename = "type")]
kind: UndoType,
#[serde(flatten)]
common: ActivityCommonFields,
}
#[async_trait::async_trait(?Send)]
impl ActivityHandler for UndoLikePostOrComment {
async fn verify(
&self,
context: &LemmyContext,
request_counter: &mut i32,
) -> Result<(), LemmyError> {
verify_activity(self.common())?;
verify_person_in_community(&self.common.actor, &self.cc, context, request_counter).await?;
verify_urls_match(&self.common.actor, &self.object.common().actor)?;
self.object.verify(context, request_counter).await?;
Ok(())
}
async fn receive(
&self,
context: &LemmyContext,
request_counter: &mut i32,
) -> Result<(), LemmyError> {
receive_undo_like_or_dislike(
&self.common.actor,
&self.object.object,
context,
request_counter,
)
.await
}
fn common(&self) -> &ActivityCommonFields {
&self.common
}
}

@ -0,0 +1,122 @@
use crate::{
activities::{
community::announce::AnnouncableActivities,
generate_activity_id,
verify_activity,
verify_person_in_community,
voting::{
undo_vote_comment,
undo_vote_post,
vote::{Vote, VoteType},
},
},
activity_queue::send_to_community_new,
extensions::context::lemmy_context,
fetcher::{
objects::get_or_fetch_and_insert_post_or_comment,
person::get_or_fetch_and_upsert_person,
},
ActorType,
PostOrComment,
};
use activitystreams::activity::kind::UndoType;
use lemmy_api_common::blocking;
use lemmy_apub_lib::{values::PublicUrl, verify_urls_match, ActivityCommonFields, ActivityHandler};
use lemmy_db_queries::Crud;
use lemmy_db_schema::{
source::{community::Community, person::Person},
CommunityId,
};
use lemmy_utils::LemmyError;
use lemmy_websocket::LemmyContext;
use std::ops::Deref;
use url::Url;
#[derive(Clone, Debug, serde::Deserialize, serde::Serialize)]
#[serde(rename_all = "camelCase")]
pub struct UndoVote {
to: PublicUrl,
object: Vote,
cc: [Url; 1],
#[serde(rename = "type")]
kind: UndoType,
#[serde(flatten)]
common: ActivityCommonFields,
}
impl UndoVote {
pub async fn send(
object: &PostOrComment,
actor: &Person,
community_id: CommunityId,
kind: VoteType,
context: &LemmyContext,
) -> Result<(), LemmyError> {
let community = blocking(context.pool(), move |conn| {
Community::read(conn, community_id)
})
.await??;
let id = generate_activity_id(UndoType::Undo)?;
let undo_vote = UndoVote {
to: PublicUrl::Public,
object: Vote {
to: PublicUrl::Public,
object: object.ap_id(),
cc: [community.actor_id()],
kind: kind.clone(),
common: ActivityCommonFields {
context: lemmy_context(),
id: generate_activity_id(kind)?,
actor: actor.actor_id(),
unparsed: Default::default(),
},
},
cc: [community.actor_id()],
kind: UndoType::Undo,
common: ActivityCommonFields {
context: lemmy_context(),
id: id.clone(),
actor: actor.actor_id(),
unparsed: Default::default(),
},
};
let activity = AnnouncableActivities::UndoVote(undo_vote);
send_to_community_new(activity, &id, actor, &community, vec![], context).await
}
}
#[async_trait::async_trait(?Send)]
impl ActivityHandler for UndoVote {
async fn verify(
&self,
context: &LemmyContext,
request_counter: &mut i32,
) -> Result<(), LemmyError> {
verify_activity(self.common())?;
verify_person_in_community(&self.common.actor, &self.cc[0], context, request_counter).await?;
verify_urls_match(&self.common.actor, &self.object.common().actor)?;
self.object.verify(context, request_counter).await?;
Ok(())
}
async fn receive(
&self,
context: &LemmyContext,
request_counter: &mut i32,
) -> Result<(), LemmyError> {
let actor =
get_or_fetch_and_upsert_person(&self.common.actor, context, request_counter).await?;
let object =
get_or_fetch_and_insert_post_or_comment(&self.object.object, context, request_counter)
.await?;
match object {
PostOrComment::Post(p) => undo_vote_post(actor, p.deref(), context).await,
PostOrComment::Comment(c) => undo_vote_comment(actor, c.deref(), context).await,
}
}
fn common(&self) -> &ActivityCommonFields {
&self.common
}
}

@ -0,0 +1,133 @@
use crate::{
activities::{
community::announce::AnnouncableActivities,
generate_activity_id,
verify_activity,
verify_person_in_community,
voting::{vote_comment, vote_post},
},
activity_queue::send_to_community_new,
extensions::context::lemmy_context,
fetcher::{
objects::get_or_fetch_and_insert_post_or_comment,
person::get_or_fetch_and_upsert_person,
},
ActorType,
PostOrComment,
};
use anyhow::anyhow;
use lemmy_api_common::blocking;
use lemmy_apub_lib::{values::PublicUrl, ActivityCommonFields, ActivityHandler};
use lemmy_db_queries::Crud;
use lemmy_db_schema::{
source::{community::Community, person::Person},
CommunityId,
};
use lemmy_utils::LemmyError;
use lemmy_websocket::LemmyContext;
use serde::{Deserialize, Serialize};
use std::{convert::TryFrom, ops::Deref};
use strum_macros::ToString;
use url::Url;
#[derive(Clone, Debug, ToString, Deserialize, Serialize)]
pub enum VoteType {
Like,
Dislike,
}
impl TryFrom<i16> for VoteType {
type Error = LemmyError;
fn try_from(value: i16) -> Result<Self, Self::Error> {
match value {
1 => Ok(VoteType::Like),
-1 => Ok(VoteType::Dislike),
_ => Err(anyhow!("invalid vote value").into()),
}
}
}
impl From<&VoteType> for i16 {
fn from(value: &VoteType) -> i16 {
match value {
VoteType::Like => 1,
VoteType::Dislike => -1,
}
}
}
#[derive(Clone, Debug, Deserialize, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct Vote {
pub(in crate::activities::voting) to: PublicUrl,
pub(in crate::activities::voting) object: Url,
pub(in crate::activities::voting) cc: [Url; 1],
#[serde(rename = "type")]
pub(in crate::activities::voting) kind: VoteType,
#[serde(flatten)]
pub(in crate::activities::voting) common: ActivityCommonFields,
}
impl Vote {
pub async fn send(
object: &PostOrComment,
actor: &Person,
community_id: CommunityId,
kind: VoteType,
context: &LemmyContext,
) -> Result<(), LemmyError> {
let community = blocking(context.pool(), move |conn| {
Community::read(conn, community_id)
})
.await??;
let id = generate_activity_id(kind.clone())?;
let vote = Vote {
to: PublicUrl::Public,
object: object.ap_id(),
cc: [community.actor_id()],
kind,
common: ActivityCommonFields {
context: lemmy_context(),
id: id.clone(),
actor: actor.actor_id(),
unparsed: Default::default(),
},
};
let activity = AnnouncableActivities::Vote(vote);
send_to_community_new(activity, &id, actor, &community, vec![], context).await
}
}
#[async_trait::async_trait(?Send)]
impl ActivityHandler for Vote {
async fn verify(
&self,
context: &LemmyContext,
request_counter: &mut i32,
) -> Result<(), LemmyError> {
verify_activity(self.common())?;
verify_person_in_community(&self.common.actor, &self.cc[0], context, request_counter).await?;
Ok(())
}
async fn receive(
&self,
context: &LemmyContext,
request_counter: &mut i32,
) -> Result<(), LemmyError> {
let actor =
get_or_fetch_and_upsert_person(&self.common.actor, context, request_counter).await?;
let object =
get_or_fetch_and_insert_post_or_comment(&self.object, context, request_counter).await?;
match object {
PostOrComment::Post(p) => vote_post(&self.kind, actor, p.deref(), context).await,
PostOrComment::Comment(c) => vote_comment(&self.kind, actor, c.deref(), context).await,
}
}
fn common(&self) -> &ActivityCommonFields {
&self.common
}
}

@ -1,4 +1,5 @@
use crate::{
activities::community::announce::{AnnouncableActivities, AnnounceActivity},
check_is_apub_id_valid,
extensions::signatures::sign_and_send,
insert_activity,
@ -24,7 +25,7 @@ use itertools::Itertools;
use lemmy_db_schema::source::{community::Community, person::Person};
use lemmy_utils::{location_info, settings::structs::Settings, LemmyError};
use lemmy_websocket::LemmyContext;
use log::{debug, warn};
use log::{debug, info, warn};
use reqwest::Client;
use serde::{Deserialize, Serialize};
use std::{collections::BTreeMap, env, fmt::Debug, future::Future, pin::Pin};
@ -137,37 +138,77 @@ where
Ok(())
}
/// Sends notification to any persons mentioned in a comment
///
/// * `creator` person who created the comment
/// * `mentions` list of inboxes of persons which are mentioned in the comment
/// * `activity` either a `Create/Note` or `Update/Note`
pub(crate) async fn send_comment_mentions<T, Kind>(
creator: &Person,
mentions: Vec<Url>,
activity: T,
pub(crate) async fn send_to_community_new(
activity: AnnouncableActivities,
activity_id: &Url,
actor: &dyn ActorType,
community: &Community,
additional_inboxes: Vec<Url>,
context: &LemmyContext,
) -> Result<(), LemmyError> {
// if this is a local community, we need to do an announce from the community instead
if community.local {
insert_activity(activity_id, activity.clone(), true, false, context.pool()).await?;
AnnounceActivity::send(activity, community, additional_inboxes, context).await?;
} else {
let mut inboxes = additional_inboxes;
inboxes.push(community.get_shared_inbox_or_inbox_url());
send_activity_new(context, &activity, activity_id, actor, inboxes, false).await?;
}
Ok(())
}
pub(crate) async fn send_activity_new<T>(
context: &LemmyContext,
activity: &T,
activity_id: &Url,
actor: &dyn ActorType,
inboxes: Vec<Url>,
sensitive: bool,
) -> Result<(), LemmyError>
where
T: AsObject<Kind> + Extends<Kind> + Debug + BaseExt<Kind>,
Kind: Serialize,
<T as Extends<Kind>>::Error: From<serde_json::Error> + Send + Sync + 'static,
T: Serialize,
{
debug!(
"Sending mentions activity {:?} to {:?}",
&activity.id_unchecked(),
&mentions
);
let mentions = mentions
if !Settings::get().federation.enabled || inboxes.is_empty() {
return Ok(());
}
info!("Sending activity {}", activity_id.to_string());
// Don't send anything to ourselves
// TODO: this should be a debug assert
let hostname = Settings::get().get_hostname_without_port()?;
let inboxes: Vec<&Url> = inboxes
.iter()
.filter(|inbox| check_is_apub_id_valid(inbox, false).is_ok())
.map(|i| i.to_owned())
.filter(|i| i.domain().expect("valid inbox url") != hostname)
.collect();
send_activity_internal(
context, activity, creator, mentions, false, // Don't create a new DB row
false,
let serialised_activity = serde_json::to_string(&activity)?;
insert_activity(
activity_id,
serialised_activity.clone(),
true,
sensitive,
context.pool(),
)
.await?;
for i in inboxes {
let message = SendActivityTask {
activity: serialised_activity.to_owned(),
inbox: i.to_owned(),
actor_id: actor.actor_id(),
private_key: actor.private_key().context(location_info!())?,
};
if env::var("LEMMY_TEST_SEND_SYNC").is_ok() {
do_send(message, &Client::default()).await?;
} else {
context.activity_queue.queue::<SendActivityTask>(message)?;
}
}
Ok(())
}

@ -1,9 +1,8 @@
use activitystreams::{base::AnyBase, context};
use lemmy_utils::LemmyError;
use activitystreams::{base::AnyBase, context, primitives::OneOrMany};
use serde_json::json;
use url::Url;
pub fn lemmy_context() -> Result<Vec<AnyBase>, LemmyError> {
pub fn lemmy_context() -> OneOrMany<AnyBase> {
let context_ext = AnyBase::from_arbitrary_json(json!(
{
"sc": "http://schema.org#",
@ -19,10 +18,11 @@ pub fn lemmy_context() -> Result<Vec<AnyBase>, LemmyError> {
"type": "sc:Text",
"id": "as:alsoKnownAs"
},
}))?;
Ok(vec![
}))
.expect("parse context");
OneOrMany::from(vec![
AnyBase::from(context()),
context_ext,
AnyBase::from(Url::parse("https://w3id.org/security/v1")?),
AnyBase::from(Url::parse("https://w3id.org/security/v1").expect("parse context")),
])
}

@ -1,5 +1,4 @@
pub mod context;
pub(crate) mod group_extension;
pub(crate) mod page_extension;
pub(crate) mod person_extension;
pub mod signatures;

@ -1,36 +0,0 @@
use activitystreams::unparsed::UnparsedMutExt;
use activitystreams_ext::UnparsedExtension;
use serde::{Deserialize, Serialize};
/// Activitystreams extension to allow (de)serializing additional Post fields
/// `comemnts_enabled` (called 'locked' in Lemmy),
/// `sensitive` (called 'nsfw') and `stickied`.
#[derive(Clone, Debug, Default, Deserialize, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct PageExtension {
pub comments_enabled: Option<bool>,
pub sensitive: Option<bool>,
pub stickied: Option<bool>,
}
impl<U> UnparsedExtension<U> for PageExtension
where
U: UnparsedMutExt,
{
type Error = serde_json::Error;
fn try_from_unparsed(unparsed_mut: &mut U) -> Result<Self, Self::Error> {
Ok(PageExtension {
comments_enabled: unparsed_mut.remove("commentsEnabled")?,
sensitive: unparsed_mut.remove("sensitive")?,
stickied: unparsed_mut.remove("stickied")?,
})
}
fn try_into_unparsed(self, unparsed_mut: &mut U) -> Result<(), Self::Error> {
unparsed_mut.insert("commentsEnabled", self.comments_enabled)?;
unparsed_mut.insert("sensitive", self.sensitive)?;
unparsed_mut.insert("stickied", self.stickied)?;
Ok(())
}
}

@ -1,8 +1,6 @@
use crate::{
fetcher::fetch::fetch_remote_object,
objects::FromApub,
NoteExt,
PageExt,
objects::{comment::Note, post::Page, FromApub},
PostOrComment,
};
use anyhow::anyhow;
@ -35,7 +33,7 @@ pub async fn get_or_fetch_and_insert_post(
Err(NotFound {}) => {
debug!("Fetching and creating remote post: {}", post_ap_id);
let page =
fetch_remote_object::<PageExt>(context.client(), post_ap_id, recursion_counter).await?;
fetch_remote_object::<Page>(context.client(), post_ap_id, recursion_counter).await?;
let post = Post::from_apub(
&page,
context,
@ -74,7 +72,7 @@ pub async fn get_or_fetch_and_insert_comment(
comment_ap_id
);
let comment =
fetch_remote_object::<NoteExt>(context.client(), comment_ap_id, recursion_counter).await?;
fetch_remote_object::<Note>(context.client(), comment_ap_id, recursion_counter).await?;
let comment = Comment::from_apub(
&comment,
context,

@ -6,11 +6,9 @@ use crate::{
is_deleted,
},
find_object_by_id,
objects::FromApub,
objects::{comment::Note, post::Page, FromApub},
GroupExt,
NoteExt,
Object,
PageExt,
PersonExt,
};
use activitystreams::base::BaseExt;
@ -46,8 +44,8 @@ use url::Url;
enum SearchAcceptedObjects {
Person(Box<PersonExt>),
Group(Box<GroupExt>),
Page(Box<PageExt>),
Comment(Box<NoteExt>),
Page(Box<Page>),
Comment(Box<Note>),
}
/// Attempt to parse the query as URL, and fetch an ActivityPub object from it.

@ -81,7 +81,7 @@ pub(crate) async fn get_apub_community_followers(
let mut collection = UnorderedCollection::new();
collection
.set_many_contexts(lemmy_context()?)
.set_many_contexts(lemmy_context())
.set_id(community.followers_url.into())
.set_total_items(community_followers.len() as u64);
Ok(create_apub_response(&collection))
@ -112,7 +112,7 @@ pub(crate) async fn get_apub_community_outbox(
let mut collection = OrderedCollection::new();
collection
.set_many_items(activities)
.set_many_contexts(lemmy_context()?)
.set_many_contexts(lemmy_context())
.set_id(community.get_outbox_url()?)
.set_total_items(len as u64);
Ok(create_apub_response(&collection))
@ -130,7 +130,7 @@ pub(crate) async fn get_apub_community_inbox(
let mut collection = OrderedCollection::new();
collection
.set_id(community.inbox_url.into())
.set_many_contexts(lemmy_context()?);
.set_many_contexts(lemmy_context());
Ok(create_apub_response(&collection))
}
@ -155,13 +155,13 @@ pub(crate) async fn get_apub_community_moderators(
let moderators: Vec<Url> = moderators
.into_iter()
.map(|m| m.moderator.actor_id.into_inner())
.map(|m| m.moderator.actor_id.into())
.collect();
let mut collection = OrderedCollection::new();
collection
.set_id(generate_moderators_url(&community.actor_id)?.into())
.set_total_items(moderators.len() as u64)
.set_many_items(moderators)
.set_many_contexts(lemmy_context()?);
.set_many_contexts(lemmy_context());
Ok(create_apub_response(&collection))
}

@ -1,5 +1,5 @@
use crate::activities::{
comment::{create::CreateComment, update::UpdateComment},
comment::create_or_update::CreateOrUpdateComment,
community::{
add_mod::AddMod,
announce::AnnounceActivity,
@ -9,23 +9,17 @@ use crate::activities::{
},
deletion::{delete::DeletePostCommentOrCommunity, undo_delete::UndoDeletePostCommentOrCommunity},
following::{accept::AcceptFollowCommunity, follow::FollowCommunity, undo::UndoFollowCommunity},
post::{create::CreatePost, update::UpdatePost},
post::create_or_update::CreateOrUpdatePost,
private_message::{
create::CreatePrivateMessage,
create_or_update::CreateOrUpdatePrivateMessage,
delete::DeletePrivateMessage,
undo_delete::UndoDeletePrivateMessage,
update::UpdatePrivateMessage,
},
removal::{
remove::RemovePostCommentCommunityOrMod,
undo_remove::UndoRemovePostCommentOrCommunity,
},
voting::{
dislike::DislikePostOrComment,
like::LikePostOrComment,
undo_dislike::UndoDislikePostOrComment,
undo_like::UndoLikePostOrComment,
},
voting::{undo_vote::UndoVote, vote::Vote},
};
use lemmy_apub_lib::{ActivityCommonFields, ActivityHandler};
use lemmy_utils::LemmyError;
@ -36,8 +30,7 @@ use serde::{Deserialize, Serialize};
#[serde(untagged)]
pub enum PersonInboxActivities {
AcceptFollowCommunity(AcceptFollowCommunity),
CreatePrivateMessage(CreatePrivateMessage),
UpdatePrivateMessage(UpdatePrivateMessage),
CreateOrUpdatePrivateMessage(CreateOrUpdatePrivateMessage),
DeletePrivateMessage(DeletePrivateMessage),
UndoDeletePrivateMessage(UndoDeletePrivateMessage),
AnnounceActivity(Box<AnnounceActivity>),
@ -48,14 +41,10 @@ pub enum PersonInboxActivities {
pub enum GroupInboxActivities {
FollowCommunity(FollowCommunity),
UndoFollowCommunity(UndoFollowCommunity),
CreateComment(CreateComment),
UpdateComment(UpdateComment),
CreatePost(CreatePost),
UpdatePost(UpdatePost),
LikePostOrComment(LikePostOrComment),
DislikePostOrComment(DislikePostOrComment),
UndoLikePostOrComment(UndoLikePostOrComment),
UndoDislikePostOrComment(UndoDislikePostOrComment),
CreateOrUpdateComment(CreateOrUpdateComment),
CreateOrUpdatePost(Box<CreateOrUpdatePost>),
Vote(Vote),
UndoVote(UndoVote),
DeletePostCommentOrCommunity(DeletePostCommentOrCommunity),
UndoDeletePostCommentOrCommunity(UndoDeletePostCommentOrCommunity),
RemovePostCommentOrCommunity(RemovePostCommentCommunityOrMod),
@ -72,14 +61,10 @@ pub enum SharedInboxActivities {
// received by group
FollowCommunity(FollowCommunity),
UndoFollowCommunity(UndoFollowCommunity),
CreateComment(CreateComment),
UpdateComment(UpdateComment),
CreatePost(CreatePost),
UpdatePost(UpdatePost),
LikePostOrComment(LikePostOrComment),
DislikePostOrComment(DislikePostOrComment),
UndoDislikePostOrComment(UndoDislikePostOrComment),
UndoLikePostOrComment(UndoLikePostOrComment),
CreateOrUpdateComment(CreateOrUpdateComment),
CreateOrUpdatePost(Box<CreateOrUpdatePost>),
Vote(Vote),
UndoVote(UndoVote),
DeletePostCommentOrCommunity(DeletePostCommentOrCommunity),
UndoDeletePostCommentOrCommunity(UndoDeletePostCommentOrCommunity),
RemovePostCommentOrCommunity(RemovePostCommentCommunityOrMod),
@ -92,8 +77,7 @@ pub enum SharedInboxActivities {
AcceptFollowCommunity(AcceptFollowCommunity),
// Note, pm activities need to be at the end, otherwise comments will end up here. We can probably
// avoid this problem by replacing createpm.object with our own struct, instead of NoteExt.
CreatePrivateMessage(CreatePrivateMessage),
UpdatePrivateMessage(UpdatePrivateMessage),
CreateOrUpdatePrivateMessage(CreateOrUpdatePrivateMessage),
DeletePrivateMessage(DeletePrivateMessage),
UndoDeletePrivateMessage(UndoDeletePrivateMessage),
AnnounceActivity(Box<AnnounceActivity>),

@ -71,7 +71,7 @@ pub(crate) async fn get_apub_person_outbox(
let mut collection = OrderedCollection::new();
collection
.set_many_items(Vec::<Url>::new())
.set_many_contexts(lemmy_context()?)
.set_many_contexts(lemmy_context())
.set_id(person.get_outbox_url()?)
.set_total_items(0_u64);
Ok(create_apub_response(&collection))
@ -89,6 +89,6 @@ pub(crate) async fn get_apub_person_inbox(
let mut collection = OrderedCollection::new();
collection
.set_id(person.inbox_url.into())
.set_many_contexts(lemmy_context()?);
.set_many_contexts(lemmy_context());
Ok(create_apub_response(&collection))
}

@ -11,7 +11,6 @@ pub mod objects;
use crate::{
extensions::{
group_extension::GroupExtension,
page_extension::PageExtension,
person_extension::PersonExtension,
signatures::{PublicKey, PublicKeyExtension},
},
@ -21,9 +20,9 @@ use activitystreams::{
activity::Follow,
actor,
base::AnyBase,
object::{ApObject, AsObject, Note, ObjectExt, Page},
object::{ApObject, AsObject, ObjectExt},
};
use activitystreams_ext::{Ext1, Ext2};
use activitystreams_ext::Ext2;
use anyhow::{anyhow, Context};
use diesel::NotFound;
use lemmy_api_common::blocking;
@ -54,9 +53,6 @@ pub type GroupExt =
type PersonExt =
Ext2<actor::ApActor<ApObject<actor::Actor<UserTypes>>>, PersonExtension, PublicKeyExtension>;
pub type SiteExt = actor::ApActor<ApObject<actor::Service>>;
/// Activitystreams type for post
pub type PageExt = Ext1<ApObject<Page>, PageExtension>;
pub type NoteExt = ApObject<Note>;
#[derive(Clone, Copy, Debug, serde::Deserialize, serde::Serialize, PartialEq)]
pub enum UserTypes {
@ -133,10 +129,6 @@ pub fn check_is_apub_id_valid(apub_id: &Url, use_strict_allowlist: bool) -> Resu
/// and actors in Lemmy.
#[async_trait::async_trait(?Send)]
pub trait ApubObjectType {
async fn send_create(&self, creator: &DbPerson, context: &LemmyContext)
-> Result<(), LemmyError>;
async fn send_update(&self, creator: &DbPerson, context: &LemmyContext)
-> Result<(), LemmyError>;
async fn send_delete(&self, creator: &DbPerson, context: &LemmyContext)
-> Result<(), LemmyError>;
async fn send_undo_delete(
@ -152,21 +144,6 @@ pub trait ApubObjectType {
) -> Result<(), LemmyError>;
}
#[async_trait::async_trait(?Send)]
pub trait ApubLikeableType {
async fn send_like(&self, creator: &DbPerson, context: &LemmyContext) -> Result<(), LemmyError>;
async fn send_dislike(
&self,
creator: &DbPerson,
context: &LemmyContext,
) -> Result<(), LemmyError>;
async fn send_undo_like(
&self,
creator: &DbPerson,
context: &LemmyContext,
) -> Result<(), LemmyError>;
}
/// Common methods provided by ActivityPub actors (community and person). Not all methods are
/// implemented by all actors.
pub trait ActorType {
@ -314,7 +291,7 @@ pub fn generate_inbox_url(actor_id: &DbUrl) -> Result<DbUrl, ParseError> {
}
pub fn generate_shared_inbox_url(actor_id: &DbUrl) -> Result<DbUrl, LemmyError> {
let actor_id = actor_id.clone().into_inner();
let actor_id: Url = actor_id.clone().into();
let url = format!(
"{}://{}{}/inbox",
&actor_id.scheme(),
@ -377,6 +354,16 @@ pub enum PostOrComment {
Post(Box<Post>),
}
impl PostOrComment {
pub(crate) fn ap_id(&self) -> Url {
match self {
PostOrComment::Post(p) => p.ap_id.clone(),
PostOrComment::Comment(c) => c.ap_id.clone(),
}
.into()
}
}
/// Tries to find a post or comment in the local database, without any network requests.
/// This is used to handle deletions and removals, because in case we dont have the object, we can
/// simply ignore the activity.

@ -1,29 +1,24 @@
use crate::{
activities::verify_person_in_community,
extensions::context::lemmy_context,
fetcher::objects::{get_or_fetch_and_insert_comment, get_or_fetch_and_insert_post},
get_community_from_to_or_cc,
objects::{
check_object_domain,
check_object_for_community_or_site_ban,
create_tombstone,
get_object_from_apub,
get_or_fetch_and_upsert_person,
get_source_markdown_value,
set_content_and_source,
FromApub,
FromApubToForm,
ToApub,
},
NoteExt,
objects::{create_tombstone, get_or_fetch_and_upsert_person, FromApub, Source, ToApub},
ActorType,
};
use activitystreams::{
object::{kind::NoteType, ApObject, Note, Tombstone},
prelude::*,
public,
base::AnyBase,
object::{kind::NoteType, Tombstone},
primitives::OneOrMany,
unparsed::Unparsed,
};
use anyhow::{anyhow, Context};
use chrono::{DateTime, FixedOffset};
use lemmy_api_common::blocking;
use lemmy_db_queries::{Crud, DbPool};
use lemmy_apub_lib::{
values::{MediaTypeHtml, MediaTypeMarkdown, PublicUrl},
verify_domains_match,
};
use lemmy_db_queries::{ApubObject, Crud, DbPool};
use lemmy_db_schema::{
source::{
comment::{Comment, CommentForm},
@ -39,24 +34,103 @@ use lemmy_utils::{
LemmyError,
};
use lemmy_websocket::LemmyContext;
use serde::{Deserialize, Serialize};
use url::Url;
#[derive(Clone, Debug, Deserialize, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct Note {
#[serde(rename = "@context")]
context: OneOrMany<AnyBase>,
r#type: NoteType,
pub(crate) id: Url,
pub(crate) attributed_to: Url,
/// Indicates that the object is publicly readable. Unlike [`Post.to`], this one doesn't contain
/// the community ID, as it would be incompatible with Pleroma (and we can get the community from
/// the post in [`in_reply_to`]).
to: PublicUrl,
content: String,
media_type: MediaTypeHtml,
source: Source,
in_reply_to: Vec<Url>,
published: DateTime<FixedOffset>,
updated: Option<DateTime<FixedOffset>>,
#[serde(flatten)]
unparsed: Unparsed,
}
impl Note {
async fn get_parents(
&self,
context: &LemmyContext,
request_counter: &mut i32,
) -> Result<(Post, Option<CommentId>), LemmyError> {
// This post, or the parent comment might not yet exist on this server yet, fetch them.
let post_id = self.in_reply_to.get(0).context(location_info!())?;
let post = Box::pin(get_or_fetch_and_insert_post(
post_id,
context,
request_counter,
))
.await?;
// The 2nd item, if it exists, is the parent comment apub_id
// Nested comments will automatically get fetched recursively
let parent_id: Option<CommentId> = match self.in_reply_to.get(1) {
Some(parent_comment_uri) => {
let parent_comment = Box::pin(get_or_fetch_and_insert_comment(
parent_comment_uri,
context,
request_counter,
))
.await?;
Some(parent_comment.id)
}
None => None,
};
Ok((post, parent_id))
}
pub(crate) async fn verify(
&self,
context: &LemmyContext,
request_counter: &mut i32,
) -> Result<(), LemmyError> {
let (post, _parent_comment_id) = self.get_parents(context, request_counter).await?;
let community_id = post.community_id;
let community = blocking(context.pool(), move |conn| {
Community::read(conn, community_id)
})
.await??;
if post.locked {
return Err(anyhow!("Post is locked").into());
}
verify_domains_match(&self.attributed_to, &self.id)?;
verify_person_in_community(
&self.attributed_to,
&community.actor_id(),
context,
request_counter,
)
.await?;
Ok(())
}
}
#[async_trait::async_trait(?Send)]
impl ToApub for Comment {
type ApubType = NoteExt;
async fn to_apub(&self, pool: &DbPool) -> Result<NoteExt, LemmyError> {
let mut comment = ApObject::new(Note::new());
type ApubType = Note;
async fn to_apub(&self, pool: &DbPool) -> Result<Note, LemmyError> {
let creator_id = self.creator_id;
let creator = blocking(pool, move |conn| Person::read(conn, creator_id)).await??;
let post_id = self.post_id;
let post = blocking(pool, move |conn| Post::read(conn, post_id)).await??;
let community_id = post.community_id;
let community = blocking(pool, move |conn| Community::read(conn, community_id)).await??;
// Add a vector containing some important info to the "in_reply_to" field
// [post_ap_id, Option(parent_comment_ap_id)]
let mut in_reply_to_vec = vec![post.ap_id.into_inner()];
@ -67,23 +141,25 @@ impl ToApub for Comment {
in_reply_to_vec.push(parent_comment.ap_id.into_inner());
}
comment
// Not needed when the Post is embedded in a collection (like for community outbox)
.set_many_contexts(lemmy_context()?)
.set_id(self.ap_id.to_owned().into_inner())
.set_published(convert_datetime(self.published))
// NOTE: included community id for compatibility with lemmy v0.9.9
.set_many_tos(vec![community.actor_id.into_inner(), public()])
.set_many_in_reply_tos(in_reply_to_vec)
.set_attributed_to(creator.actor_id.into_inner());
set_content_and_source(&mut comment, &self.content)?;
if let Some(u) = self.updated {
comment.set_updated(convert_datetime(u));
}
let note = Note {
context: lemmy_context(),
r#type: NoteType::Note,
id: self.ap_id.to_owned().into_inner(),
attributed_to: creator.actor_id.into_inner(),
to: PublicUrl::Public,
content: self.content.clone(),
media_type: MediaTypeHtml::Html,
source: Source {
content: self.content.clone(),
media_type: MediaTypeMarkdown::Markdown,
},
in_reply_to: in_reply_to_vec,
published: convert_datetime(self.published),
updated: self.updated.map(convert_datetime),
unparsed: Default::default(),
};
Ok(comment)
Ok(note)
}
fn to_tombstone(&self) -> Result<Tombstone, LemmyError> {
@ -98,108 +174,38 @@ impl ToApub for Comment {
#[async_trait::async_trait(?Send)]
impl FromApub for Comment {
type ApubType = NoteExt;
type ApubType = Note;
/// Converts a `Note` to `Comment`.
///
/// If the parent community, post and comment(s) are not known locally, these are also fetched.
async fn from_apub(
note: &NoteExt,
note: &Note,
context: &LemmyContext,
expected_domain: Url,
request_counter: &mut i32,
mod_action_allowed: bool,
) -> Result<Comment, LemmyError> {
let comment: Comment = get_object_from_apub(
note,
context,
expected_domain,
request_counter,
mod_action_allowed,
)
.await?;
let post_id = comment.post_id;
let post = blocking(context.pool(), move |conn| Post::read(conn, post_id)).await??;
check_object_for_community_or_site_ban(note, post.community_id, context, request_counter)
.await?;
Ok(comment)
}
}
#[async_trait::async_trait(?Send)]
impl FromApubToForm<NoteExt> for CommentForm {
async fn from_apub(
note: &NoteExt,
context: &LemmyContext,
expected_domain: Url,
_expected_domain: Url,
request_counter: &mut i32,
_mod_action_allowed: bool,
) -> Result<CommentForm, LemmyError> {
let community = get_community_from_to_or_cc(note, context, request_counter).await?;
let ap_id = Some(check_object_domain(note, expected_domain, community.local)?);
let creator_actor_id = &note
.attributed_to()
.context(location_info!())?
.as_single_xsd_any_uri()
.context(location_info!())?;
) -> Result<Comment, LemmyError> {
let creator =
get_or_fetch_and_upsert_person(creator_actor_id, context, request_counter).await?;
let mut in_reply_tos = note
.in_reply_to()
.as_ref()
.context(location_info!())?
.as_many()
.context(location_info!())?
.iter()
.map(|i| i.as_xsd_any_uri().context(""));
let post_ap_id = in_reply_tos.next().context(location_info!())??;
get_or_fetch_and_upsert_person(&note.attributed_to, context, request_counter).await?;
let (post, parent_comment_id) = note.get_parents(context, request_counter).await?;
// This post, or the parent comment might not yet exist on this server yet, fetch them.
let post = Box::pin(get_or_fetch_and_insert_post(
post_ap_id,
context,
request_counter,
))
.await?;
if post.locked {
return Err(anyhow!("Post is locked").into());
}
// The 2nd item, if it exists, is the parent comment apub_id
// For deeply nested comments, FromApub automatically gets called recursively
let parent_id: Option<CommentId> = match in_reply_tos.next() {
Some(parent_comment_uri) => {
let parent_comment_ap_id = &parent_comment_uri?;
let parent_comment = Box::pin(get_or_fetch_and_insert_comment(
parent_comment_ap_id,
context,
request_counter,
))
.await?;
let content = &note.source.content;
let content_slurs_removed = remove_slurs(content);
Some(parent_comment.id)
}
None => None,
};
let content = get_source_markdown_value(note)?.context(location_info!())?;
let content_slurs_removed = remove_slurs(&content);
Ok(CommentForm {
let form = CommentForm {
creator_id: creator.id,
post_id: post.id,
parent_id,
parent_id: parent_comment_id,
content: content_slurs_removed,
removed: None,
read: None,
published: note.published().map(|u| u.to_owned().naive_local()),
updated: note.updated().map(|u| u.to_owned().naive_local()),
published: Some(note.published.naive_local()),
updated: note.updated.map(|u| u.to_owned().naive_local()),
deleted: None,
ap_id,
ap_id: Some(note.id.clone().into()),
local: Some(false),
})
};
Ok(blocking(context.pool(), move |conn| Comment::upsert(conn, &form)).await??)
}
}

@ -55,7 +55,7 @@ impl ToApub for Community {
let mut group = ApObject::new(Group::new());
group
.set_many_contexts(lemmy_context()?)
.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))

@ -1,8 +1,4 @@
use crate::{
check_community_or_site_ban,
check_is_apub_id_valid,
fetcher::person::get_or_fetch_and_upsert_person,
};
use crate::{check_is_apub_id_valid, fetcher::person::get_or_fetch_and_upsert_person};
use activitystreams::{
base::{AsBase, BaseExt, ExtendsExt},
markers::Base,
@ -12,8 +8,9 @@ use activitystreams::{
use anyhow::{anyhow, Context};
use chrono::NaiveDateTime;
use lemmy_api_common::blocking;
use lemmy_apub_lib::values::MediaTypeMarkdown;
use lemmy_db_queries::{ApubObject, Crud, DbPool};
use lemmy_db_schema::{CommunityId, DbUrl};
use lemmy_db_schema::DbUrl;
use lemmy_utils::{
location_info,
settings::structs::Settings,
@ -70,6 +67,13 @@ pub trait FromApubToForm<ApubType> {
Self: Sized;
}
#[derive(Clone, Debug, serde::Deserialize, serde::Serialize)]
#[serde(rename_all = "camelCase")]
pub struct Source {
content: String,
media_type: MediaTypeMarkdown,
}
/// Updated is actually the deletion time
fn create_tombstone<T>(
deleted: bool,
@ -211,21 +215,3 @@ where
Ok(to)
}
}
pub(in crate::objects) async fn check_object_for_community_or_site_ban<T, Kind>(
object: &T,
community_id: CommunityId,
context: &LemmyContext,
request_counter: &mut i32,
) -> Result<(), LemmyError>
where
T: ObjectExt<Kind>,
{
let person_id = object
.attributed_to()
.context(location_info!())?
.as_single_xsd_any_uri()
.context(location_info!())?;
let person = get_or_fetch_and_upsert_person(person_id, context, request_counter).await?;
check_community_or_site_ban(&person, community_id, context.pool()).await
}

@ -50,7 +50,7 @@ impl ToApub for DbPerson {
let mut person = ApObject::new(actor);
person
.set_many_contexts(lemmy_context()?)
.set_many_contexts(lemmy_context())
.set_id(self.actor_id.to_owned().into_inner())
.set_published(convert_datetime(self.published));

@ -1,30 +1,27 @@
use crate::{
check_is_apub_id_valid,
extensions::{context::lemmy_context, page_extension::PageExtension},
activities::{extract_community, verify_person_in_community},
extensions::context::lemmy_context,
fetcher::person::get_or_fetch_and_upsert_person,
get_community_from_to_or_cc,
objects::{
check_object_domain,
check_object_for_community_or_site_ban,
create_tombstone,
get_object_from_apub,
get_source_markdown_value,
set_content_and_source,
FromApub,
FromApubToForm,
ToApub,
},
PageExt,
objects::{create_tombstone, FromApub, Source, ToApub},
ActorType,
};
use activitystreams::{
object::{kind::PageType, ApObject, Image, Page, Tombstone},
prelude::*,
base::AnyBase,
object::{
kind::{ImageType, PageType},
Tombstone,
},
primitives::OneOrMany,
public,
unparsed::Unparsed,
};
use activitystreams_ext::Ext1;
use anyhow::Context;
use chrono::{DateTime, FixedOffset};
use lemmy_api_common::blocking;
use lemmy_db_queries::{Crud, DbPool};
use lemmy_apub_lib::{
values::{MediaTypeHtml, MediaTypeMarkdown},
verify_domains_match,
};
use lemmy_db_queries::{ApubObject, Crud, DbPool};
use lemmy_db_schema::{
self,
source::{
@ -34,66 +31,125 @@ use lemmy_db_schema::{
},
};
use lemmy_utils::{
location_info,
request::fetch_iframely_and_pictrs_data,
utils::{check_slurs, convert_datetime, remove_slurs},
utils::{check_slurs, convert_datetime, markdown_to_html, remove_slurs},
LemmyError,
};
use lemmy_websocket::LemmyContext;
use serde::{Deserialize, Serialize};
use url::Url;
#[derive(Clone, Debug, Deserialize, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct Page {
#[serde(rename = "@context")]
context: OneOrMany<AnyBase>,
r#type: PageType,
pub(crate) id: Url,
pub(crate) attributed_to: Url,
to: [Url; 2],
name: String,
content: Option<String>,
media_type: MediaTypeHtml,
source: Option<Source>,
url: Option<Url>,
image: Option<ImageObject>,
pub(crate) comments_enabled: Option<bool>,
sensitive: Option<bool>,
pub(crate) stickied: Option<bool>,
published: DateTime<FixedOffset>,
updated: Option<DateTime<FixedOffset>>,
#[serde(flatten)]
unparsed: Unparsed,
}
#[derive(Clone, Debug, serde::Deserialize, serde::Serialize)]
#[serde(rename_all = "camelCase")]
pub struct ImageObject {
content: ImageType,
url: Url,
}
impl Page {
/// Only mods can change the post's stickied/locked status. So if either of these is changed from
/// the current value, it is a mod action and needs to be verified as such.
///
/// Both stickied and locked need to be false on a newly created post (verified in [[CreatePost]].
pub(crate) async fn is_mod_action(&self, pool: &DbPool) -> Result<bool, LemmyError> {
let post_id = self.id.clone();
let old_post = blocking(pool, move |conn| {
Post::read_from_apub_id(conn, &post_id.into())
})
.await?;
let is_mod_action = if let Ok(old_post) = old_post {
self.stickied != Some(old_post.stickied) || self.comments_enabled != Some(!old_post.locked)
} else {
false
};
Ok(is_mod_action)
}
pub(crate) async fn verify(
&self,
context: &LemmyContext,
request_counter: &mut i32,
) -> Result<(), LemmyError> {
let community = extract_community(&self.to, context, request_counter).await?;
check_slurs(&self.name)?;
verify_domains_match(&self.attributed_to, &self.id)?;
verify_person_in_community(
&self.attributed_to,
&community.actor_id(),
context,
request_counter,
)
.await?;
Ok(())
}
}
#[async_trait::async_trait(?Send)]
impl ToApub for Post {
type ApubType = PageExt;
type ApubType = Page;
// 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 = ApObject::new(Page::new());
async fn to_apub(&self, pool: &DbPool) -> Result<Page, LemmyError> {
let creator_id = self.creator_id;
let creator = blocking(pool, move |conn| Person::read(conn, creator_id)).await??;
let community_id = self.community_id;
let community = blocking(pool, move |conn| Community::read(conn, community_id)).await??;
page
// Not needed when the Post is embedded in a collection (like for community outbox)
// TODO: need to set proper context defining sensitive/commentsEnabled fields
// https://git.asonix.dog/Aardwolf/activitystreams/issues/5
.set_many_contexts(lemmy_context()?)
.set_id(self.ap_id.to_owned().into_inner())
.set_name(self.name.to_owned())
// `summary` field for compatibility with lemmy v0.9.9 and older,
// TODO: remove this after some time
.set_summary(self.name.to_owned())
.set_published(convert_datetime(self.published))
.set_many_tos(vec![community.actor_id.into_inner(), public()])
.set_attributed_to(creator.actor_id.into_inner());
if let Some(body) = &self.body {
set_content_and_source(&mut page, body)?;
}
if let Some(url) = &self.url {
page.set_url::<Url>(url.to_owned().into());
}
if let Some(thumbnail_url) = &self.thumbnail_url {
let mut image = Image::new();
image.set_url::<Url>(thumbnail_url.to_owned().into());
page.set_image(image.into_any_base()?);
}
if let Some(u) = self.updated {
page.set_updated(convert_datetime(u));
}
let ext = PageExtension {
let source = self.body.clone().map(|body| Source {
content: body,
media_type: MediaTypeMarkdown::Markdown,
});
let image = self.thumbnail_url.clone().map(|thumb| ImageObject {
content: ImageType::Image,
url: thumb.into(),
});
let page = Page {
context: lemmy_context(),
r#type: PageType::Page,
id: self.ap_id.clone().into(),
attributed_to: creator.actor_id.into(),
to: [community.actor_id.into(), public()],
name: self.name.clone(),
content: self.body.as_ref().map(|b| markdown_to_html(b)),
media_type: MediaTypeHtml::Html,
source,
url: self.url.clone().map(|u| u.into()),
image,
comments_enabled: Some(!self.locked),
sensitive: Some(self.nsfw),
stickied: Some(self.stickied),
published: convert_datetime(self.published),
updated: self.updated.map(convert_datetime),
unparsed: Default::default(),
};
Ok(Ext1::new(page, ext))
Ok(page)
}
fn to_tombstone(&self) -> Result<Tombstone, LemmyError> {
@ -108,136 +164,50 @@ impl ToApub for Post {
#[async_trait::async_trait(?Send)]
impl FromApub for Post {
type ApubType = PageExt;
type ApubType = Page;
/// Converts a `PageExt` to `PostForm`.
///
/// If the post's community or creator are not known locally, these are also fetched.
async fn from_apub(
page: &PageExt,
page: &Page,
context: &LemmyContext,
expected_domain: Url,
_expected_domain: Url,
request_counter: &mut i32,
mod_action_allowed: bool,
_mod_action_allowed: bool,
) -> Result<Post, LemmyError> {
let post: Post = get_object_from_apub(
page,
context,
expected_domain,
request_counter,
mod_action_allowed,
)
.await?;
check_object_for_community_or_site_ban(page, post.community_id, context, request_counter)
.await?;
Ok(post)
}
}
#[async_trait::async_trait(?Send)]
impl FromApubToForm<PageExt> for PostForm {
async fn from_apub(
page: &PageExt,
context: &LemmyContext,
expected_domain: Url,
request_counter: &mut i32,
mod_action_allowed: bool,
) -> Result<PostForm, LemmyError> {
let community = get_community_from_to_or_cc(page, context, request_counter).await?;
let ap_id = if mod_action_allowed {
let id = page.id_unchecked().context(location_info!())?;
check_is_apub_id_valid(id, community.local)?;
id.to_owned().into()
} else {
check_object_domain(page, expected_domain, community.local)?
};
let ext = &page.ext_one;
let creator_actor_id = page
.inner
.attributed_to()
.as_ref()
.context(location_info!())?
.as_single_xsd_any_uri()
.context(location_info!())?;
let creator =
get_or_fetch_and_upsert_person(creator_actor_id, context, request_counter).await?;
get_or_fetch_and_upsert_person(&page.attributed_to, context, request_counter).await?;
let community = extract_community(&page.to, context, request_counter).await?;
let thumbnail_url: Option<Url> = match &page.inner.image() {
Some(any_image) => Image::from_any_base(
any_image
.to_owned()
.as_one()
.context(location_info!())?
.to_owned(),
)?
.context(location_info!())?
.url()
.context(location_info!())?
.as_single_xsd_any_uri()
.map(|url| url.to_owned()),
None => None,
};
let url = page
.inner
.url()
.map(|u| u.as_single_xsd_any_uri())
.flatten()
.map(|u| u.to_owned());
let (embed_title, embed_description, embed_html, pictrs_thumbnail) = if let Some(url) = &url {
let (embed, thumb) = fetch_iframely_and_pictrs_data(context.client(), Some(url)).await?;
(embed.title, embed.description, embed.html, thumb)
let thumbnail_url: Option<Url> = page.image.clone().map(|i| i.url);
let (iframely_response, pictrs_thumbnail) = if let Some(url) = &page.url {
fetch_iframely_and_pictrs_data(context.client(), Some(url)).await?
} else {
(None, None, None, thumbnail_url)
(None, thumbnail_url)
};
let name = page
.inner
.name()
// The following is for compatibility with lemmy v0.9.9 and older
// TODO: remove it after some time (along with the map above)
.or_else(|| page.inner.summary())
.context(location_info!())?
.as_single_xsd_string()
.context(location_info!())?
.to_string();
let body = get_source_markdown_value(page)?;
// TODO: expected_domain is wrong in this case, because it simply takes the domain of the actor
// maybe we need to take id_unchecked() if the activity is from community to user?
// why did this work before? -> i dont think it did?
// -> try to make expected_domain optional and set it null if it is a mod action
check_slurs(&name)?;
let body_slurs_removed = body.map(|b| remove_slurs(&b));
Ok(PostForm {
name,
url: url.map(|u| u.into()),
let (embed_title, embed_description, embed_html) = iframely_response
.map(|u| (u.title, u.description, u.html))
.unwrap_or((None, None, None));
let body_slurs_removed = page.source.as_ref().map(|s| remove_slurs(&s.content));
let form = PostForm {
name: page.name.clone(),
url: page.url.clone().map(|u| u.into()),
body: body_slurs_removed,
creator_id: creator.id,
community_id: community.id,
removed: None,
locked: ext.comments_enabled.map(|e| !e),
published: page
.inner
.published()
.as_ref()
.map(|u| u.to_owned().naive_local()),
updated: page
.inner
.updated()
.as_ref()
.map(|u| u.to_owned().naive_local()),
locked: page.comments_enabled.map(|e| !e),
published: Some(page.published.naive_local()),
updated: page.updated.map(|u| u.naive_local()),
deleted: None,
nsfw: ext.sensitive,
stickied: ext.stickied,
nsfw: page.sensitive,
stickied: page.stickied,
embed_title,
embed_description,
embed_html,
thumbnail_url: pictrs_thumbnail.map(|u| u.into()),
ap_id: Some(ap_id),
ap_id: Some(page.id.clone().into()),
local: Some(false),
})
};
Ok(blocking(context.pool(), move |conn| Post::upsert(conn, &form)).await??)
}
}

@ -1,60 +1,93 @@
use crate::{
extensions::context::lemmy_context,
fetcher::person::get_or_fetch_and_upsert_person,
objects::{
check_object_domain,
create_tombstone,
get_object_from_apub,
get_source_markdown_value,
set_content_and_source,
FromApub,
FromApubToForm,
ToApub,
},
NoteExt,
objects::{create_tombstone, FromApub, Source, ToApub},
};
use activitystreams::{
object::{kind::NoteType, ApObject, Note, Tombstone},
prelude::*,
base::AnyBase,
object::{kind::NoteType, Tombstone},
primitives::OneOrMany,
unparsed::Unparsed,
};
use anyhow::Context;
use anyhow::anyhow;
use chrono::{DateTime, FixedOffset};
use lemmy_api_common::blocking;
use lemmy_db_queries::{Crud, DbPool};
use lemmy_apub_lib::{
values::{MediaTypeHtml, MediaTypeMarkdown},
verify_domains_match,
};
use lemmy_db_queries::{ApubObject, Crud, DbPool};
use lemmy_db_schema::source::{
person::Person,
private_message::{PrivateMessage, PrivateMessageForm},
};
use lemmy_utils::{location_info, utils::convert_datetime, LemmyError};
use lemmy_utils::{utils::convert_datetime, LemmyError};
use lemmy_websocket::LemmyContext;
use serde::{Deserialize, Serialize};
use url::Url;
#[derive(Clone, Debug, Deserialize, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct Note {
#[serde(rename = "@context")]
context: OneOrMany<AnyBase>,
r#type: NoteType,
pub(crate) id: Url,
pub(crate) attributed_to: Url,
to: Url,
content: String,
media_type: MediaTypeHtml,
source: Source,
published: DateTime<FixedOffset>,
updated: Option<DateTime<FixedOffset>>,
#[serde(flatten)]
unparsed: Unparsed,
}
impl Note {
pub(crate) async fn verify(
&self,
context: &LemmyContext,
request_counter: &mut i32,
) -> Result<(), LemmyError> {
verify_domains_match(&self.attributed_to, &self.id)?;
let person =
get_or_fetch_and_upsert_person(&self.attributed_to, context, request_counter).await?;
if person.banned {
return Err(anyhow!("Person is banned from site").into());
}
Ok(())
}
}
#[async_trait::async_trait(?Send)]
impl ToApub for PrivateMessage {
type ApubType = NoteExt;
async fn to_apub(&self, pool: &DbPool) -> Result<NoteExt, LemmyError> {
let mut private_message = ApObject::new(Note::new());
type ApubType = Note;
async fn to_apub(&self, pool: &DbPool) -> Result<Note, LemmyError> {
let creator_id = self.creator_id;
let creator = blocking(pool, move |conn| Person::read(conn, creator_id)).await??;
let recipient_id = self.recipient_id;
let recipient = blocking(pool, move |conn| Person::read(conn, recipient_id)).await??;
private_message
.set_many_contexts(lemmy_context()?)
.set_id(self.ap_id.to_owned().into_inner())
.set_published(convert_datetime(self.published))
.set_to(recipient.actor_id.into_inner())
.set_attributed_to(creator.actor_id.into_inner());
set_content_and_source(&mut private_message, &self.content)?;
if let Some(u) = self.updated {
private_message.set_updated(convert_datetime(u));
}
Ok(private_message)
let note = Note {
context: lemmy_context(),
r#type: NoteType::Note,
id: self.ap_id.clone().into(),
attributed_to: creator.actor_id.into_inner(),
to: recipient.actor_id.into(),
content: self.content.clone(),
media_type: MediaTypeHtml::Html,
source: Source {
content: self.content.clone(),
media_type: MediaTypeMarkdown::Markdown,
},
published: convert_datetime(self.published),
updated: self.updated.map(convert_datetime),
unparsed: Default::default(),
};
Ok(note)
}
fn to_tombstone(&self) -> Result<Tombstone, LemmyError> {
@ -69,66 +102,35 @@ impl ToApub for PrivateMessage {
#[async_trait::async_trait(?Send)]
impl FromApub for PrivateMessage {
type ApubType = NoteExt;
type ApubType = Note;
async fn from_apub(
note: &NoteExt,
note: &Note,
context: &LemmyContext,
expected_domain: Url,
request_counter: &mut i32,
mod_action_allowed: bool,
) -> Result<PrivateMessage, LemmyError> {
get_object_from_apub(
note,
context,
expected_domain,
request_counter,
mod_action_allowed,
)
.await
}
}
#[async_trait::async_trait(?Send)]
impl FromApubToForm<NoteExt> for PrivateMessageForm {
async fn from_apub(
note: &NoteExt,
context: &LemmyContext,
expected_domain: Url,
_expected_domain: Url,
request_counter: &mut i32,
_mod_action_allowed: bool,
) -> Result<PrivateMessageForm, LemmyError> {
let creator_actor_id = note
.attributed_to()
.context(location_info!())?
.clone()
.single_xsd_any_uri()
.context(location_info!())?;
) -> Result<PrivateMessage, LemmyError> {
let creator =
get_or_fetch_and_upsert_person(&creator_actor_id, context, request_counter).await?;
let recipient_actor_id = note
.to()
.context(location_info!())?
.clone()
.single_xsd_any_uri()
.context(location_info!())?;
let recipient =
get_or_fetch_and_upsert_person(&recipient_actor_id, context, request_counter).await?;
let ap_id = Some(check_object_domain(note, expected_domain, false)?);
let content = get_source_markdown_value(note)?.context(location_info!())?;
get_or_fetch_and_upsert_person(&note.attributed_to, context, request_counter).await?;
let recipient = get_or_fetch_and_upsert_person(&note.to, context, request_counter).await?;
Ok(PrivateMessageForm {
let form = PrivateMessageForm {
creator_id: creator.id,
recipient_id: recipient.id,
content,
published: note.published().map(|u| u.to_owned().naive_local()),
updated: note.updated().map(|u| u.to_owned().naive_local()),
content: note.source.content.clone(),
published: Some(note.published.naive_local()),
updated: note.updated.map(|u| u.to_owned().naive_local()),
deleted: None,
read: None,
ap_id,
ap_id: Some(note.id.clone().into()),
local: Some(false),
})
};
Ok(
blocking(context.pool(), move |conn| {
PrivateMessage::upsert(conn, &form)
})
.await??,
)
}
}

@ -12,3 +12,4 @@ activitystreams-ext = "0.1.0-alpha.2"
serde = { version = "1.0.123", features = ["derive"] }
async-trait = "0.1.42"
url = { version = "2.2.1", features = ["serde"] }
serde_json = { version = "1.0.64", features = ["preserve_order"] }

@ -1,3 +1,5 @@
pub mod values;
use activitystreams::{
base::AnyBase,
error::DomainError,
@ -9,18 +11,12 @@ use lemmy_utils::LemmyError;
use lemmy_websocket::LemmyContext;
use url::Url;
#[derive(Debug, Clone, serde::Deserialize, serde::Serialize)]
pub enum PublicUrl {
#[serde(rename = "https://www.w3.org/ns/activitystreams#Public")]
Public,
}
#[derive(Clone, Debug, serde::Deserialize, serde::Serialize)]
#[serde(rename_all = "camelCase")]
pub struct ActivityCommonFields {
#[serde(rename = "@context")]
pub context: OneOrMany<AnyBase>,
id: Url,
pub id: Url,
pub actor: Url,
// unparsed fields

@ -0,0 +1,61 @@
//! The enums here serve to limit a json string value to a single, hardcoded value which can be
//! verified at compilation time. When using it as the type of a struct field, the struct can only
//! be constructed or deserialized if the field has the exact same value.
//!
//! If we used String as the field type, any value would be accepted, and we would have to check
//! manually at runtime that it contains the expected value.
//!
//! The enums in [`activitystreams::activity::kind`] work in the same way, and can be used to
//! distinguish different activity types.
//!
//! In the example below, `MyObject` can only be constructed or
//! deserialized if `media_type` is `text/markdown`, but not if it is `text/html`.
//!
//! ```
//! use lemmy_apub_lib::values::MediaTypeMarkdown;
//! use serde_json::from_str;
//! use serde::{Deserialize, Serialize};
//!
//! #[derive(Deserialize, Serialize)]
//! struct MyObject {
//! content: String,
//! media_type: MediaTypeMarkdown,
//! }
//!
//! let markdown_json = r#"{"content": "**test**", "media_type": "text/markdown"}"#;
//! let from_markdown = from_str::<MyObject>(markdown_json);
//! assert!(from_markdown.is_ok());
//!
//! let markdown_html = r#"{"content": "<b>test</b>", "media_type": "text/html"}"#;
//! let from_html = from_str::<MyObject>(markdown_html);
//! assert!(from_html.is_err());
//! ```
use serde::{Deserialize, Serialize};
/// The identifier used to address activities to the public.
///
/// <https://www.w3.org/TR/activitypub/#public-addressing>
#[derive(Debug, Clone, Deserialize, Serialize)]
pub enum PublicUrl {
#[serde(rename = "https://www.w3.org/ns/activitystreams#Public")]
Public,
}
/// Media type for markdown text.
///
/// <https://www.iana.org/assignments/media-types/media-types.xhtml>
#[derive(Clone, Debug, Deserialize, Serialize)]
pub enum MediaTypeMarkdown {
#[serde(rename = "text/markdown")]
Markdown,
}
/// Media type for HTML text/
///
/// <https://www.iana.org/assignments/media-types/media-types.xhtml>
#[derive(Clone, Debug, Deserialize, Serialize)]
pub enum MediaTypeHtml {
#[serde(rename = "text/html")]
Html,
}

@ -117,6 +117,10 @@ pub trait Reportable<Form> {
Self: Sized;
}
pub trait DeleteableOrRemoveable {
fn blank_out_deleted_or_removed_info(self) -> Self;
}
pub trait ApubObject<Form> {
fn read_from_apub_id(conn: &PgConnection, object_id: &DbUrl) -> Result<Self, Error>
where

@ -1,4 +1,4 @@
use crate::{ApubObject, Crud, Likeable, Saveable};
use crate::{ApubObject, Crud, DeleteableOrRemoveable, Likeable, Saveable};
use diesel::{dsl::*, result::Error, *};
use lemmy_db_schema::{
naive_now,
@ -228,6 +228,13 @@ impl Saveable<CommentSavedForm> for CommentSaved {
}
}
impl DeleteableOrRemoveable for Comment {
fn blank_out_deleted_or_removed_info(mut self) -> Self {
self.content = "".into();
self
}
}
#[cfg(test)]
mod tests {
use crate::{establish_unpooled_connection, Crud, Likeable, Saveable};

@ -1,4 +1,4 @@
use crate::{ApubObject, Bannable, Crud, Followable, Joinable};
use crate::{ApubObject, Bannable, Crud, DeleteableOrRemoveable, Followable, Joinable};
use diesel::{dsl::*, result::Error, *};
use lemmy_db_schema::{
naive_now,
@ -11,6 +11,7 @@ use lemmy_db_schema::{
CommunityModeratorForm,
CommunityPersonBan,
CommunityPersonBanForm,
CommunitySafe,
},
CommunityId,
DbUrl,
@ -199,6 +200,26 @@ impl Joinable<CommunityModeratorForm> for CommunityModerator {
}
}
impl DeleteableOrRemoveable for CommunitySafe {
fn blank_out_deleted_or_removed_info(mut self) -> Self {
self.title = "".into();
self.description = None;
self.icon = None;
self.banner = None;
self
}
}
impl DeleteableOrRemoveable for Community {
fn blank_out_deleted_or_removed_info(mut self) -> Self {
self.title = "".into();
self.description = None;
self.icon = None;
self.banner = None;
self
}
}
pub trait CommunityModerator_ {
fn delete_for_community(
conn: &PgConnection,

@ -1,4 +1,4 @@
use crate::{ApubObject, Crud, Likeable, Readable, Saveable};
use crate::{ApubObject, Crud, DeleteableOrRemoveable, Likeable, Readable, Saveable};
use diesel::{dsl::*, result::Error, *};
use lemmy_db_schema::{
naive_now,
@ -260,6 +260,20 @@ impl Readable<PostReadForm> for PostRead {
}
}
impl DeleteableOrRemoveable for Post {
fn blank_out_deleted_or_removed_info(mut self) -> Self {
self.name = "".into();
self.url = None;
self.body = None;
self.embed_title = None;
self.embed_description = None;
self.embed_html = None;
self.thumbnail_url = None;
self
}
}
#[cfg(test)]
mod tests {
use crate::{establish_unpooled_connection, source::post::*};

@ -1,4 +1,4 @@
use crate::{ApubObject, Crud};
use crate::{ApubObject, Crud, DeleteableOrRemoveable};
use diesel::{dsl::*, result::Error, *};
use lemmy_db_schema::{naive_now, source::private_message::*, DbUrl, PersonId, PrivateMessageId};
@ -137,6 +137,13 @@ impl PrivateMessage_ for PrivateMessage {
}
}
impl DeleteableOrRemoveable for PrivateMessage {
fn blank_out_deleted_or_removed_info(mut self) -> Self {
self.content = "".into();
self
}
}
#[cfg(test)]
mod tests {
use crate::{establish_unpooled_connection, source::private_message::PrivateMessage_, Crud};

@ -92,6 +92,7 @@ where
}
impl DbUrl {
// TODO: remove this method and just use into()
pub fn into_inner(self) -> Url {
self.0
}
@ -99,7 +100,7 @@ impl DbUrl {
impl Display for DbUrl {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
self.to_owned().into_inner().fmt(f)
self.to_owned().0.fmt(f)
}
}

@ -47,7 +47,7 @@ where
response.expect("retry http request")
}
#[derive(Deserialize, Debug, Default)]
#[derive(Deserialize, Debug)]
pub struct IframelyResponse {
pub title: Option<String>,
pub description: Option<String>,
@ -70,7 +70,7 @@ pub(crate) async fn fetch_iframely(
.map_err(|e| RecvError(e.to_string()))?;
Ok(res)
} else {
Ok(IframelyResponse::default())
Err(anyhow!("Missing Iframely URL in config.").into())
}
}
@ -119,18 +119,23 @@ pub(crate) async fn fetch_pictrs(
pub async fn fetch_iframely_and_pictrs_data(
client: &Client,
url: Option<&Url>,
) -> Result<(IframelyResponse, Option<Url>), LemmyError> {
) -> Result<(Option<IframelyResponse>, Option<Url>), LemmyError> {
match &url {
Some(url) => {
// Fetch iframely data
let iframely_response = fetch_iframely(client, url).await?;
let iframely_res_option = fetch_iframely(client, url).await.ok();
// Fetch pictrs thumbnail
let pictrs_hash = match &iframely_response.thumbnail_url {
Some(iframely_thumbnail_url) => fetch_pictrs(client, iframely_thumbnail_url)
.await?
.map(|r| r.files[0].file.to_owned()),
// Try to generate a small thumbnail if iframely is not supported
let pictrs_hash = match &iframely_res_option {
Some(iframely_res) => match &iframely_res.thumbnail_url {
Some(iframely_thumbnail_url) => fetch_pictrs(client, iframely_thumbnail_url)
.await?
.map(|r| r.files[0].file.to_owned()),
// Try to generate a small thumbnail if iframely is not supported
None => fetch_pictrs(client, url)
.await?
.map(|r| r.files[0].file.to_owned()),
},
None => fetch_pictrs(client, url)
.await?
.map(|r| r.files[0].file.to_owned()),
@ -147,22 +152,10 @@ pub async fn fetch_iframely_and_pictrs_data(
.ok()
})
.flatten();
/*
let pictrs_thumbnail = if let Some(pictrs_hash) = pictrs_hash {
Some(Url::parse(&format!(
"{}/pictrs/image/{}",
Settings::get().get_protocol_and_hostname(),
pictrs_hash
))?)
} else {
None
};
*/
Ok((iframely_response, pictrs_thumbnail))
Ok((iframely_res_option, pictrs_thumbnail))
}
None => Ok((IframelyResponse::default(), None)),
None => Ok((None, None)),
}
}

@ -12,7 +12,7 @@ services:
restart: always
lemmy:
image: dessalines/lemmy:0.11.2
image: dessalines/lemmy:0.11.3
ports:
- "127.0.0.1:8536:8536"
restart: always
@ -26,7 +26,7 @@ services:
- iframely
lemmy-ui:
image: dessalines/lemmy-ui:0.11.2
image: dessalines/lemmy-ui:0.11.3
ports:
- "127.0.0.1:1235:1234"
restart: always

@ -0,0 +1,28 @@
drop trigger post_aggregates_comment_set_deleted on comment;
drop function post_aggregates_comment_deleted;
create or replace function post_aggregates_comment_count()
returns trigger language plpgsql
as $$
begin
IF (TG_OP = 'INSERT') THEN
update post_aggregates pa
set comments = comments + 1,
newest_comment_time = NEW.published
where pa.post_id = NEW.post_id;
-- A 2 day necro-bump limit
update post_aggregates pa
set newest_comment_time_necro = NEW.published
where pa.post_id = NEW.post_id
and published > ('now'::timestamp - '2 days'::interval);
ELSIF (TG_OP = 'DELETE') THEN
-- Join to post because that post may not exist anymore
update post_aggregates pa
set comments = comments - 1
from post p
where pa.post_id = p.id
and pa.post_id = OLD.post_id;
END IF;
return null;
end $$;

@ -0,0 +1,61 @@
-- Creating a new trigger for when comment.deleted is updated
create or replace function post_aggregates_comment_deleted()
returns trigger language plpgsql
as $$
begin
IF NEW.deleted = TRUE THEN
update post_aggregates pa
set comments = comments - 1
where pa.post_id = NEW.post_id;
ELSE
update post_aggregates pa
set comments = comments + 1
where pa.post_id = NEW.post_id;
END IF;
return null;
end $$;
create trigger post_aggregates_comment_set_deleted
after update of deleted on comment
for each row
execute procedure post_aggregates_comment_deleted();
-- Fix issue with being able to necro-bump your own post
create or replace function post_aggregates_comment_count()
returns trigger language plpgsql
as $$
begin
IF (TG_OP = 'INSERT') THEN
update post_aggregates pa
set comments = comments + 1,
newest_comment_time = NEW.published
where pa.post_id = NEW.post_id;
-- A 2 day necro-bump limit
update post_aggregates pa
set newest_comment_time_necro = NEW.published
from post p
where pa.post_id = p.id
and pa.post_id = NEW.post_id
-- Fix issue with being able to necro-bump your own post
and NEW.creator_id != p.creator_id
and pa.published > ('now'::timestamp - '2 days'::interval);
ELSIF (TG_OP = 'DELETE') THEN
-- Join to post because that post may not exist anymore
update post_aggregates pa
set comments = comments - 1
from post p
where pa.post_id = p.id
and pa.post_id = OLD.post_id;
ELSIF (TG_OP = 'UPDATE') THEN
-- Join to post because that post may not exist anymore
update post_aggregates pa
set comments = comments - 1
from post p
where pa.post_id = p.id
and pa.post_id = OLD.post_id;
END IF;
return null;
end $$;
Loading…
Cancel
Save