Compare commits

..

17 commits

Author SHA1 Message Date
05b07b8cbb Fix conflicts in docker-compose.yml 2021-03-19 18:09:05 +01:00
4f54108a9c Merge branch 'main' into federated-moderation 2021-03-19 17:11:34 +01:00
4f7dca7c2b Fix three federation test cases 2021-03-18 17:02:15 +01:00
b3a5b4eb82 Refactor activitypub code 2021-03-17 18:12:37 +01:00
71067a8cb5 For FromApub trait, use is_mod_action: bool instead 2021-03-16 18:26:19 +01:00
be00f63fb2 Allow remot moderators to do Remove/Post and Remove/Comment 2021-03-16 18:06:59 +01:00
de14636e10 Fix code to allow sticky/lock from remote moderators 2021-03-12 16:46:36 +01:00
de39d57592 WIP: check that modifications are made by same user, add docs 2021-03-12 14:47:55 +01:00
b1ca85b910 Fix clippy warning 2021-03-11 18:18:01 +01:00
50559de6d2 Add check so only mods can change stickied/locked state of posts 2021-03-11 18:11:59 +01:00
803aad3b3e Add check so only author or mods can edit posts/comments 2021-03-11 17:50:47 +01:00
a2698dea92 Allow for remote mods to remove posts/comments 2021-03-11 17:21:45 +01:00
3ffae1f5b8 Allow adding remote users as community mods (ref #1061) 2021-03-09 18:14:15 +01:00
dcf40db225 Update activitystreams to 0.7.0-alpha.11 2021-03-09 18:14:15 +01:00
9172eff65a Implemented receiving activities to add/remove remote mods 2021-03-09 18:14:15 +01:00
0c484e8c76 Move moderators collection to separate HTTP endpoint 2021-03-09 18:14:15 +01:00
beb8b9fe69 Use collection for moderators, instead of attributedTo (ref #1061) 2021-03-09 18:14:15 +01:00
52 changed files with 940 additions and 512 deletions

4
Cargo.lock generated
View file

@ -4,9 +4,9 @@ version = 3
[[package]] [[package]]
name = "activitystreams" name = "activitystreams"
version = "0.7.0-alpha.10" version = "0.7.0-alpha.11"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fe7ceed015dfca322d3bcec3653909c77557e7e57df72e98cb8806e2c93cc919" checksum = "3a5da1d857ec9ca65ef8d0469cdd64e7b93b59d6cad26f1444bf84b62f3eadd4"
dependencies = [ dependencies = [
"chrono", "chrono",
"mime", "mime",

View file

@ -1 +1 @@
0.10.0-rc.7 0.10.0-rc.5

View file

@ -22,6 +22,7 @@ import {
searchForUser, searchForUser,
banPersonFromSite, banPersonFromSite,
searchPostLocal, searchPostLocal,
followCommunity,
banPersonFromCommunity, banPersonFromCommunity,
} from './shared'; } from './shared';
import { PostView, CommunityView } from 'lemmy-js-client'; import { PostView, CommunityView } from 'lemmy-js-client';
@ -169,35 +170,38 @@ test('Sticky a post', async () => {
}); });
test('Lock a post', async () => { test('Lock a post', async () => {
await followCommunity(alpha, true, betaCommunity.community.id);
let postRes = await createPost(alpha, betaCommunity.community.id); let postRes = await createPost(alpha, betaCommunity.community.id);
// Lock the post // Lock the post
let lockedPostRes = await lockPost(alpha, true, postRes.post_view.post); let searchBeta = await searchPost(beta, postRes.post_view.post);
let betaPost1 = searchBeta.posts[0];
let lockedPostRes = await lockPost(beta, true, betaPost1.post);
expect(lockedPostRes.post_view.post.locked).toBe(true); expect(lockedPostRes.post_view.post.locked).toBe(true);
// Make sure that post is locked on beta // Make sure that post is locked on alpha
let searchBeta = await searchPostLocal(beta, postRes.post_view.post); let searchAlpha = await searchPostLocal(alpha, postRes.post_view.post);
let betaPost1 = searchBeta.posts[0]; let alphaPost1 = searchAlpha.posts[0];
expect(betaPost1.post.locked).toBe(true); expect(alphaPost1.post.locked).toBe(true);
// Try to make a new comment there, on alpha // Try to make a new comment there, on alpha
let comment: any = await createComment(alpha, postRes.post_view.post.id); let comment: any = await createComment(alpha, alphaPost1.post.id);
expect(comment['error']).toBe('locked'); expect(comment['error']).toBe('locked');
// Unlock a post // Unlock a post
let unlockedPost = await lockPost(alpha, false, postRes.post_view.post); let unlockedPost = await lockPost(beta, false, betaPost1.post);
expect(unlockedPost.post_view.post.locked).toBe(false); expect(unlockedPost.post_view.post.locked).toBe(false);
// Make sure that post is unlocked on beta // Make sure that post is unlocked on alpha
let searchBeta2 = await searchPost(beta, postRes.post_view.post); let searchAlpha2 = await searchPostLocal(alpha, postRes.post_view.post);
let betaPost2 = searchBeta2.posts[0]; let alphaPost2 = searchAlpha2.posts[0];
expect(betaPost2.community.local).toBe(true); expect(alphaPost2.community.local).toBe(false);
expect(betaPost2.creator.local).toBe(false); expect(alphaPost2.creator.local).toBe(true);
expect(betaPost2.post.locked).toBe(false); expect(alphaPost2.post.locked).toBe(false);
// Try to create a new comment, on beta // Try to create a new comment, on alpha
let commentBeta = await createComment(beta, betaPost2.post.id); let commentAlpha = await createComment(alpha, alphaPost1.post.id);
expect(commentBeta).toBeDefined(); expect(commentAlpha).toBeDefined();
}); });
test('Delete a post', async () => { test('Delete a post', async () => {

View file

@ -15,7 +15,9 @@ use lemmy_apub::{
generate_inbox_url, generate_inbox_url,
generate_shared_inbox_url, generate_shared_inbox_url,
ActorType, ActorType,
CommunityType,
EndpointType, EndpointType,
UserType,
}; };
use lemmy_db_queries::{ use lemmy_db_queries::{
diesel_option_overwrite_to_url, diesel_option_overwrite_to_url,
@ -34,7 +36,7 @@ use lemmy_db_queries::{
}; };
use lemmy_db_schema::{ use lemmy_db_schema::{
naive_now, naive_now,
source::{comment::Comment, community::*, moderator::*, post::Post, site::*}, source::{comment::Comment, community::*, moderator::*, person::Person, post::Post, site::*},
PersonId, PersonId,
}; };
use lemmy_db_views::comment_view::CommentQueryBuilder; use lemmy_db_views::comment_view::CommentQueryBuilder;
@ -707,16 +709,16 @@ impl Perform for AddModToCommunity {
let data: &AddModToCommunity = &self; let data: &AddModToCommunity = &self;
let local_user_view = get_local_user_view_from_jwt(&data.auth, context.pool()).await?; let local_user_view = get_local_user_view_from_jwt(&data.auth, context.pool()).await?;
let community_moderator_form = CommunityModeratorForm {
community_id: data.community_id,
person_id: data.person_id,
};
let community_id = data.community_id; let community_id = data.community_id;
// Verify that only mods or admins can add mod // Verify that only mods or admins can add mod
is_mod_or_admin(context.pool(), local_user_view.person.id, community_id).await?; is_mod_or_admin(context.pool(), local_user_view.person.id, community_id).await?;
// Update in local database
let community_moderator_form = CommunityModeratorForm {
community_id: data.community_id,
person_id: data.person_id,
};
if data.added { if data.added {
let join = move |conn: &'_ _| CommunityModerator::join(conn, &community_moderator_form); let join = move |conn: &'_ _| CommunityModerator::join(conn, &community_moderator_form);
if blocking(context.pool(), join).await?.is_err() { if blocking(context.pool(), join).await?.is_err() {
@ -741,6 +743,28 @@ impl Perform for AddModToCommunity {
}) })
.await??; .await??;
// Send to federated instances
let updated_mod_id = data.person_id;
let updated_mod = blocking(context.pool(), move |conn| {
Person::read(conn, updated_mod_id)
})
.await??;
let community = blocking(context.pool(), move |conn| {
Community::read(conn, community_id)
})
.await??;
if data.added {
community
.send_add_mod(&local_user_view.person, updated_mod, context)
.await?;
} else {
community
.send_remove_mod(&local_user_view.person, updated_mod, context)
.await?;
}
// Note: in case a remote mod is added, this returns the old moderators list, it will only get
// updated once we receive an activity from the community (like `Announce/Add/Moderator`)
let community_id = data.community_id; let community_id = data.community_id;
let moderators = blocking(context.pool(), move |conn| { let moderators = blocking(context.pool(), move |conn| {
CommunityModeratorView::for_community(conn, community_id) CommunityModeratorView::for_community(conn, community_id)
@ -748,18 +772,18 @@ impl Perform for AddModToCommunity {
.await??; .await??;
let res = AddModToCommunityResponse { moderators }; let res = AddModToCommunityResponse { moderators };
context.chat_server().do_send(SendCommunityRoomMessage { context.chat_server().do_send(SendCommunityRoomMessage {
op: UserOperation::AddModToCommunity, op: UserOperation::AddModToCommunity,
response: res.clone(), response: res.clone(),
community_id, community_id,
websocket_id, websocket_id,
}); });
Ok(res) Ok(res)
} }
} }
// TODO: we dont do anything for federation here, it should be updated the next time the community
// gets fetched. i hope we can get rid of the community creator role soon.
#[async_trait::async_trait(?Send)] #[async_trait::async_trait(?Send)]
impl Perform for TransferCommunity { impl Perform for TransferCommunity {
type Response = GetCommunityResponse; type Response = GetCommunityResponse;

View file

@ -41,7 +41,7 @@ use lemmy_utils::{
}; };
use lemmy_websocket::{serialize_websocket_message, LemmyContext, UserOperation}; use lemmy_websocket::{serialize_websocket_message, LemmyContext, UserOperation};
use serde::Deserialize; use serde::Deserialize;
use std::{env, process::Command}; use std::process::Command;
use url::Url; use url::Url;
pub mod comment; pub mod comment;
@ -100,32 +100,16 @@ pub(crate) async fn get_local_user_view_from_jwt(
Ok(claims) => claims.claims, Ok(claims) => claims.claims,
Err(_e) => return Err(ApiError::err("not_logged_in").into()), Err(_e) => return Err(ApiError::err("not_logged_in").into()),
}; };
let local_user_id = LocalUserId(claims.sub); let local_user_id = LocalUserId(claims.local_user_id);
let local_user_view = let local_user_view =
blocking(pool, move |conn| LocalUserView::read(conn, local_user_id)).await??; blocking(pool, move |conn| LocalUserView::read(conn, local_user_id)).await??;
// Check for a site ban // Check for a site ban
if local_user_view.person.banned { if local_user_view.person.banned {
return Err(ApiError::err("site_ban").into()); return Err(ApiError::err("site_ban").into());
} }
check_validator_time(&local_user_view.local_user.validator_time, &claims)?;
Ok(local_user_view) Ok(local_user_view)
} }
/// Checks if user's token was issued before user's password reset.
pub(crate) fn check_validator_time(
validator_time: &chrono::NaiveDateTime,
claims: &Claims,
) -> Result<(), LemmyError> {
let user_validation_time = validator_time.timestamp();
if user_validation_time > claims.iat {
Err(ApiError::err("not_logged_in").into())
} else {
Ok(())
}
}
pub(crate) async fn get_local_user_view_from_jwt_opt( pub(crate) async fn get_local_user_view_from_jwt_opt(
jwt: &Option<String>, jwt: &Option<String>,
pool: &DbPool, pool: &DbPool,
@ -144,7 +128,7 @@ pub(crate) async fn get_local_user_settings_view_from_jwt(
Ok(claims) => claims.claims, Ok(claims) => claims.claims,
Err(_e) => return Err(ApiError::err("not_logged_in").into()), Err(_e) => return Err(ApiError::err("not_logged_in").into()),
}; };
let local_user_id = LocalUserId(claims.sub); let local_user_id = LocalUserId(claims.local_user_id);
let local_user_view = blocking(pool, move |conn| { let local_user_view = blocking(pool, move |conn| {
LocalUserSettingsView::read(conn, local_user_id) LocalUserSettingsView::read(conn, local_user_id)
}) })
@ -153,9 +137,6 @@ pub(crate) async fn get_local_user_settings_view_from_jwt(
if local_user_view.person.banned { if local_user_view.person.banned {
return Err(ApiError::err("site_ban").into()); return Err(ApiError::err("site_ban").into());
} }
check_validator_time(&local_user_view.local_user.validator_time, &claims)?;
Ok(local_user_view) Ok(local_user_view)
} }
@ -478,11 +459,7 @@ pub(crate) fn captcha_espeak_wav_base64(captcha: &str) -> Result<String, LemmyEr
pub(crate) fn espeak_wav_base64(text: &str) -> Result<String, LemmyError> { pub(crate) fn espeak_wav_base64(text: &str) -> Result<String, LemmyError> {
// Make a temp file path // Make a temp file path
let uuid = uuid::Uuid::new_v4().to_string(); let uuid = uuid::Uuid::new_v4().to_string();
let file_path = format!( let file_path = format!("/tmp/lemmy_espeak_{}.wav", &uuid);
"{}/lemmy_espeak_{}.wav",
env::temp_dir().to_string_lossy(),
&uuid
);
// Write the wav file // Write the wav file
Command::new("espeak") Command::new("espeak")
@ -514,70 +491,7 @@ pub(crate) fn password_length_check(pass: &str) -> Result<(), LemmyError> {
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use crate::{captcha_espeak_wav_base64, check_validator_time}; use crate::captcha_espeak_wav_base64;
use lemmy_db_queries::{establish_unpooled_connection, source::local_user::LocalUser_, Crud};
use lemmy_db_schema::source::{
local_user::{LocalUser, LocalUserForm},
person::{Person, PersonForm},
};
use lemmy_utils::claims::Claims;
#[test]
fn test_should_not_validate_user_token_after_password_change() {
let conn = establish_unpooled_connection();
let new_person = PersonForm {
name: "Gerry9812".into(),
preferred_username: None,
avatar: None,
banner: None,
banned: None,
deleted: None,
published: None,
updated: None,
actor_id: None,
bio: None,
local: None,
private_key: None,
public_key: None,
last_refreshed_at: None,
inbox_url: None,
shared_inbox_url: None,
};
let inserted_person = Person::create(&conn, &new_person).unwrap();
let local_user_form = LocalUserForm {
person_id: inserted_person.id,
email: None,
matrix_user_id: None,
password_encrypted: "123456".to_string(),
admin: None,
show_nsfw: None,
theme: None,
default_sort_type: None,
default_listing_type: None,
lang: None,
show_avatars: None,
send_notifications_to_email: None,
};
let inserted_local_user = LocalUser::create(&conn, &local_user_form).unwrap();
let jwt = Claims::jwt(inserted_local_user.id.0).unwrap();
let claims = Claims::decode(&jwt).unwrap().claims;
let check = check_validator_time(&inserted_local_user.validator_time, &claims);
assert!(check.is_ok());
// The check should fail, since the validator time is now newer than the jwt issue time
let updated_local_user =
LocalUser::update_password(&conn, inserted_local_user.id, &"password111").unwrap();
let check_after = check_validator_time(&updated_local_user.validator_time, &claims);
assert!(check_after.is_err());
let num_deleted = Person::delete(&conn, inserted_person.id).unwrap();
assert_eq!(1, num_deleted);
}
#[test] #[test]
fn test_espeak() { fn test_espeak() {

View file

@ -130,7 +130,7 @@ impl Perform for Login {
// Return the jwt // Return the jwt
Ok(LoginResponse { Ok(LoginResponse {
jwt: Claims::jwt(local_user_view.local_user.id.0)?, jwt: Claims::jwt(local_user_view.local_user.id.0, Settings::get().hostname())?,
}) })
} }
} }
@ -336,7 +336,7 @@ impl Perform for Register {
// Return the jwt // Return the jwt
Ok(LoginResponse { Ok(LoginResponse {
jwt: Claims::jwt(inserted_local_user.id.0)?, jwt: Claims::jwt(inserted_local_user.id.0, Settings::get().hostname())?,
}) })
} }
} }
@ -526,7 +526,7 @@ impl Perform for SaveUserSettings {
// Return the jwt // Return the jwt
Ok(LoginResponse { Ok(LoginResponse {
jwt: Claims::jwt(updated_local_user.id.0)?, jwt: Claims::jwt(updated_local_user.id.0, Settings::get().hostname())?,
}) })
} }
} }
@ -1078,7 +1078,7 @@ impl Perform for PasswordChange {
// Return the jwt // Return the jwt
Ok(LoginResponse { Ok(LoginResponse {
jwt: Claims::jwt(updated_local_user.id.0)?, jwt: Claims::jwt(updated_local_user.id.0, Settings::get().hostname())?,
}) })
} }
} }

View file

@ -17,7 +17,7 @@ lemmy_db_views_actor = { path = "../db_views_actor" }
lemmy_api_structs = { path = "../api_structs" } lemmy_api_structs = { path = "../api_structs" }
lemmy_websocket = { path = "../websocket" } lemmy_websocket = { path = "../websocket" }
diesel = "1.4.5" diesel = "1.4.5"
activitystreams = "0.7.0-alpha.10" activitystreams = "0.7.0-alpha.11"
activitystreams-ext = "0.1.0-alpha.2" activitystreams-ext = "0.1.0-alpha.2"
bcrypt = "0.9.0" bcrypt = "0.9.0"
chrono = { version = "0.4.19", features = ["serde"] } chrono = { version = "0.4.19", features = ["serde"] }

View file

@ -1,2 +1,2 @@
pub(crate) mod receive; pub(crate) mod receive;
pub(crate) mod send; pub mod send;

View file

@ -1,6 +1,6 @@
use crate::{activities::receive::get_actor_as_person, objects::FromApub, ActorType, NoteExt}; use crate::{activities::receive::get_actor_as_person, objects::FromApub, ActorType, NoteExt};
use activitystreams::{ use activitystreams::{
activity::{ActorAndObjectRefExt, Create, Dislike, Like, Remove, Update}, activity::{ActorAndObjectRefExt, Create, Dislike, Like, Update},
base::ExtendsExt, base::ExtendsExt,
}; };
use anyhow::Context; use anyhow::Context;
@ -23,7 +23,8 @@ pub(crate) async fn receive_create_comment(
let note = NoteExt::from_any_base(create.object().to_owned().one().context(location_info!())?)? let note = NoteExt::from_any_base(create.object().to_owned().one().context(location_info!())?)?
.context(location_info!())?; .context(location_info!())?;
let comment = Comment::from_apub(&note, context, person.actor_id(), request_counter).await?; let comment =
Comment::from_apub(&note, context, person.actor_id(), request_counter, false).await?;
let post_id = comment.post_id; let post_id = comment.post_id;
let post = blocking(context.pool(), move |conn| Post::read(conn, post_id)).await??; let post = blocking(context.pool(), move |conn| Post::read(conn, post_id)).await??;
@ -73,7 +74,8 @@ pub(crate) async fn receive_update_comment(
.context(location_info!())?; .context(location_info!())?;
let person = get_actor_as_person(&update, context, request_counter).await?; let person = get_actor_as_person(&update, context, request_counter).await?;
let comment = Comment::from_apub(&note, context, person.actor_id(), request_counter).await?; let comment =
Comment::from_apub(&note, context, person.actor_id(), request_counter, false).await?;
let comment_id = comment.id; let comment_id = comment.id;
let post_id = comment.post_id; let post_id = comment.post_id;
@ -228,7 +230,6 @@ pub(crate) async fn receive_delete_comment(
pub(crate) async fn receive_remove_comment( pub(crate) async fn receive_remove_comment(
context: &LemmyContext, context: &LemmyContext,
_remove: Remove,
comment: Comment, comment: Comment,
) -> Result<(), LemmyError> { ) -> Result<(), LemmyError> {
let removed_comment = blocking(context.pool(), move |conn| { let removed_comment = blocking(context.pool(), move |conn| {

View file

@ -1,16 +1,9 @@
use crate::{activities::receive::verify_activity_domains_valid, inbox::is_addressed_to_public};
use activitystreams::{
activity::{ActorAndObjectRefExt, Delete, Remove, Undo},
base::{AnyBase, ExtendsExt},
};
use anyhow::Context;
use lemmy_api_structs::{blocking, community::CommunityResponse}; use lemmy_api_structs::{blocking, community::CommunityResponse};
use lemmy_db_queries::{source::community::Community_, ApubObject}; use lemmy_db_queries::source::community::Community_;
use lemmy_db_schema::source::community::Community; use lemmy_db_schema::source::community::Community;
use lemmy_db_views_actor::community_view::CommunityView; use lemmy_db_views_actor::community_view::CommunityView;
use lemmy_utils::{location_info, LemmyError}; use lemmy_utils::LemmyError;
use lemmy_websocket::{messages::SendCommunityRoomMessage, LemmyContext, UserOperation}; use lemmy_websocket::{messages::SendCommunityRoomMessage, LemmyContext, UserOperation};
use url::Url;
pub(crate) async fn receive_delete_community( pub(crate) async fn receive_delete_community(
context: &LemmyContext, context: &LemmyContext,
@ -42,23 +35,8 @@ pub(crate) async fn receive_delete_community(
pub(crate) async fn receive_remove_community( pub(crate) async fn receive_remove_community(
context: &LemmyContext, context: &LemmyContext,
activity: AnyBase, community: Community,
expected_domain: &Url,
) -> Result<(), LemmyError> { ) -> Result<(), LemmyError> {
let remove = Remove::from_any_base(activity)?.context(location_info!())?;
verify_activity_domains_valid(&remove, expected_domain, true)?;
is_addressed_to_public(&remove)?;
let community_uri = remove
.object()
.to_owned()
.single_xsd_any_uri()
.context(location_info!())?;
let community = blocking(context.pool(), move |conn| {
Community::read_from_apub_id(conn, &community_uri.into())
})
.await??;
let removed_community = blocking(context.pool(), move |conn| { let removed_community = blocking(context.pool(), move |conn| {
Community::update_removed(conn, community.id, true) Community::update_removed(conn, community.id, true)
}) })
@ -85,16 +63,8 @@ pub(crate) async fn receive_remove_community(
pub(crate) async fn receive_undo_delete_community( pub(crate) async fn receive_undo_delete_community(
context: &LemmyContext, context: &LemmyContext,
undo: Undo,
community: Community, community: Community,
expected_domain: &Url,
) -> Result<(), LemmyError> { ) -> Result<(), LemmyError> {
is_addressed_to_public(&undo)?;
let inner = undo.object().to_owned().one().context(location_info!())?;
let delete = Delete::from_any_base(inner)?.context(location_info!())?;
verify_activity_domains_valid(&delete, expected_domain, true)?;
is_addressed_to_public(&delete)?;
let deleted_community = blocking(context.pool(), move |conn| { let deleted_community = blocking(context.pool(), move |conn| {
Community::update_deleted(conn, community.id, false) Community::update_deleted(conn, community.id, false)
}) })
@ -121,26 +91,8 @@ pub(crate) async fn receive_undo_delete_community(
pub(crate) async fn receive_undo_remove_community( pub(crate) async fn receive_undo_remove_community(
context: &LemmyContext, context: &LemmyContext,
undo: Undo, community: Community,
expected_domain: &Url,
) -> Result<(), LemmyError> { ) -> Result<(), LemmyError> {
is_addressed_to_public(&undo)?;
let inner = undo.object().to_owned().one().context(location_info!())?;
let remove = Remove::from_any_base(inner)?.context(location_info!())?;
verify_activity_domains_valid(&remove, &expected_domain, true)?;
is_addressed_to_public(&remove)?;
let community_uri = remove
.object()
.to_owned()
.single_xsd_any_uri()
.context(location_info!())?;
let community = blocking(context.pool(), move |conn| {
Community::read_from_apub_id(conn, &community_uri.into())
})
.await??;
let removed_community = blocking(context.pool(), move |conn| { let removed_community = blocking(context.pool(), move |conn| {
Community::update_removed(conn, community.id, false) Community::update_removed(conn, community.id, false)
}) })

View file

@ -1,12 +1,24 @@
use crate::{activities::receive::get_actor_as_person, objects::FromApub, ActorType, PageExt}; use crate::{
activities::receive::get_actor_as_person,
inbox::receive_for_community::verify_mod_activity,
objects::FromApub,
ActorType,
PageExt,
};
use activitystreams::{ use activitystreams::{
activity::{Create, Dislike, Like, Remove, Update}, activity::{Announce, Create, Dislike, Like, Update},
prelude::*, prelude::*,
}; };
use anyhow::Context; use anyhow::Context;
use lemmy_api_structs::{blocking, post::PostResponse}; use lemmy_api_structs::{blocking, post::PostResponse};
use lemmy_db_queries::{source::post::Post_, Likeable}; use lemmy_db_queries::{source::post::Post_, ApubObject, Crud, Likeable};
use lemmy_db_schema::source::post::{Post, PostLike, PostLikeForm}; use lemmy_db_schema::{
source::{
community::Community,
post::{Post, PostLike, PostLikeForm},
},
DbUrl,
};
use lemmy_db_views::post_view::PostView; use lemmy_db_views::post_view::PostView;
use lemmy_utils::{location_info, LemmyError}; use lemmy_utils::{location_info, LemmyError};
use lemmy_websocket::{messages::SendPost, LemmyContext, UserOperation}; use lemmy_websocket::{messages::SendPost, LemmyContext, UserOperation};
@ -20,7 +32,7 @@ pub(crate) async fn receive_create_post(
let page = PageExt::from_any_base(create.object().to_owned().one().context(location_info!())?)? let page = PageExt::from_any_base(create.object().to_owned().one().context(location_info!())?)?
.context(location_info!())?; .context(location_info!())?;
let post = Post::from_apub(&page, context, person.actor_id(), request_counter).await?; let post = Post::from_apub(&page, context, person.actor_id(), request_counter, false).await?;
// Refetch the view // Refetch the view
let post_id = post.id; let post_id = post.id;
@ -42,6 +54,7 @@ pub(crate) async fn receive_create_post(
pub(crate) async fn receive_update_post( pub(crate) async fn receive_update_post(
update: Update, update: Update,
announce: Option<Announce>,
context: &LemmyContext, context: &LemmyContext,
request_counter: &mut i32, request_counter: &mut i32,
) -> Result<(), LemmyError> { ) -> Result<(), LemmyError> {
@ -49,7 +62,40 @@ pub(crate) async fn receive_update_post(
let page = PageExt::from_any_base(update.object().to_owned().one().context(location_info!())?)? let page = PageExt::from_any_base(update.object().to_owned().one().context(location_info!())?)?
.context(location_info!())?; .context(location_info!())?;
let post = Post::from_apub(&page, context, person.actor_id(), request_counter).await?; let post_id: DbUrl = page
.id_unchecked()
.context(location_info!())?
.to_owned()
.into();
let old_post = blocking(context.pool(), move |conn| {
Post::read_from_apub_id(conn, &post_id)
})
.await??;
// If sticked or locked state was changed, make sure the actor is a mod
let stickied = page.ext_one.stickied.context(location_info!())?;
let locked = !page.ext_one.comments_enabled.context(location_info!())?;
let mut mod_action_allowed = false;
if stickied != old_post.stickied || locked != old_post.locked {
let community = blocking(context.pool(), move |conn| {
Community::read(conn, old_post.community_id)
})
.await??;
// Only check mod status if the community is local, otherwise we trust that it was sent correctly.
if community.local {
verify_mod_activity(&update, announce, &community, context).await?;
}
mod_action_allowed = true;
}
let post = Post::from_apub(
&page,
context,
person.actor_id(),
request_counter,
mod_action_allowed,
)
.await?;
let post_id = post.id; let post_id = post.id;
// Refetch the view // Refetch the view
@ -173,7 +219,6 @@ pub(crate) async fn receive_delete_post(
pub(crate) async fn receive_remove_post( pub(crate) async fn receive_remove_post(
context: &LemmyContext, context: &LemmyContext,
_remove: Remove,
post: Post, post: Post,
) -> Result<(), LemmyError> { ) -> Result<(), LemmyError> {
let removed_post = blocking(context.pool(), move |conn| { let removed_post = blocking(context.pool(), move |conn| {

View file

@ -39,7 +39,7 @@ pub(crate) async fn receive_create_private_message(
.context(location_info!())?; .context(location_info!())?;
let private_message = let private_message =
PrivateMessage::from_apub(&note, context, expected_domain, request_counter).await?; PrivateMessage::from_apub(&note, context, expected_domain, request_counter, false).await?;
let message = blocking(&context.pool(), move |conn| { let message = blocking(&context.pool(), move |conn| {
PrivateMessageView::read(conn, private_message.id) PrivateMessageView::read(conn, private_message.id)
@ -85,7 +85,7 @@ pub(crate) async fn receive_update_private_message(
let note = NoteExt::from_any_base(object)?.context(location_info!())?; let note = NoteExt::from_any_base(object)?.context(location_info!())?;
let private_message = let private_message =
PrivateMessage::from_apub(&note, context, expected_domain, request_counter).await?; PrivateMessage::from_apub(&note, context, expected_domain, request_counter, false).await?;
let private_message_id = private_message.id; let private_message_id = private_message.id;
let message = blocking(&context.pool(), move |conn| { let message = blocking(&context.pool(), move |conn| {

View file

@ -1,20 +1,24 @@
use crate::{ use crate::{
activities::send::generate_activity_id, activities::send::generate_activity_id,
activity_queue::{send_activity_single_dest, send_to_community_followers}, activity_queue::{send_activity_single_dest, send_to_community, send_to_community_followers},
check_is_apub_id_valid, check_is_apub_id_valid,
extensions::context::lemmy_context, extensions::context::lemmy_context,
fetcher::person::get_or_fetch_and_upsert_person, fetcher::person::get_or_fetch_and_upsert_person,
generate_moderators_url,
insert_activity, insert_activity,
ActorType, ActorType,
CommunityType,
}; };
use activitystreams::{ use activitystreams::{
activity::{ activity::{
kind::{AcceptType, AnnounceType, DeleteType, LikeType, RemoveType, UndoType}, kind::{AcceptType, AddType, AnnounceType, DeleteType, LikeType, RemoveType, UndoType},
Accept, Accept,
ActorAndObjectRefExt, ActorAndObjectRefExt,
Add,
Announce, Announce,
Delete, Delete,
Follow, Follow,
OptTargetRefExt,
Remove, Remove,
Undo, Undo,
}, },
@ -26,7 +30,7 @@ use anyhow::Context;
use itertools::Itertools; use itertools::Itertools;
use lemmy_api_structs::blocking; use lemmy_api_structs::blocking;
use lemmy_db_queries::DbPool; use lemmy_db_queries::DbPool;
use lemmy_db_schema::source::community::Community; use lemmy_db_schema::source::{community::Community, person::Person};
use lemmy_db_views_actor::community_follower_view::CommunityFollowerView; use lemmy_db_views_actor::community_follower_view::CommunityFollowerView;
use lemmy_utils::{location_info, settings::structs::Settings, LemmyError}; use lemmy_utils::{location_info, settings::structs::Settings, LemmyError};
use lemmy_websocket::LemmyContext; use lemmy_websocket::LemmyContext;
@ -54,23 +58,10 @@ impl ActorType for Community {
.unwrap_or_else(|| self.inbox_url.to_owned()) .unwrap_or_else(|| self.inbox_url.to_owned())
.into() .into()
} }
async fn send_follow(
&self,
_follow_actor_id: &Url,
_context: &LemmyContext,
) -> Result<(), LemmyError> {
unimplemented!()
}
async fn send_unfollow(
&self,
_follow_actor_id: &Url,
_context: &LemmyContext,
) -> Result<(), LemmyError> {
unimplemented!()
} }
#[async_trait::async_trait(?Send)]
impl CommunityType for Community {
/// As a local community, accept the follow request from a remote person. /// As a local community, accept the follow request from a remote person.
async fn send_accept_follow( async fn send_accept_follow(
&self, &self,
@ -211,4 +202,46 @@ impl ActorType for Community {
Ok(inboxes) Ok(inboxes)
} }
async fn send_add_mod(
&self,
actor: &Person,
added_mod: Person,
context: &LemmyContext,
) -> Result<(), LemmyError> {
let mut add = Add::new(
actor.actor_id.clone().into_inner(),
added_mod.actor_id.into_inner(),
);
add
.set_many_contexts(lemmy_context()?)
.set_id(generate_activity_id(AddType::Add)?)
.set_to(public())
.set_many_ccs(vec![self.actor_id()])
.set_target(generate_moderators_url(&self.actor_id)?.into_inner());
send_to_community(add, actor, self, context).await?;
Ok(())
}
async fn send_remove_mod(
&self,
actor: &Person,
removed_mod: Person,
context: &LemmyContext,
) -> Result<(), LemmyError> {
let mut remove = Remove::new(
actor.actor_id.clone().into_inner(),
removed_mod.actor_id.into_inner(),
);
remove
.set_many_contexts(lemmy_context()?)
.set_id(generate_activity_id(RemoveType::Remove)?)
.set_to(public())
.set_many_ccs(vec![self.actor_id()])
.set_target(generate_moderators_url(&self.actor_id)?.into_inner());
send_to_community(remove, &actor, self, context).await?;
Ok(())
}
} }

View file

@ -3,6 +3,7 @@ use crate::{
activity_queue::send_activity_single_dest, activity_queue::send_activity_single_dest,
extensions::context::lemmy_context, extensions::context::lemmy_context,
ActorType, ActorType,
UserType,
}; };
use activitystreams::{ use activitystreams::{
activity::{ activity::{
@ -10,11 +11,11 @@ use activitystreams::{
Follow, Follow,
Undo, Undo,
}, },
base::{AnyBase, BaseExt, ExtendsExt}, base::{BaseExt, ExtendsExt},
object::ObjectExt, object::ObjectExt,
}; };
use lemmy_api_structs::blocking; use lemmy_api_structs::blocking;
use lemmy_db_queries::{ApubObject, DbPool, Followable}; use lemmy_db_queries::{ApubObject, Followable};
use lemmy_db_schema::source::{ use lemmy_db_schema::source::{
community::{Community, CommunityFollower, CommunityFollowerForm}, community::{Community, CommunityFollower, CommunityFollowerForm},
person::Person, person::Person,
@ -47,7 +48,10 @@ impl ActorType for Person {
.unwrap_or_else(|| self.inbox_url.to_owned()) .unwrap_or_else(|| self.inbox_url.to_owned())
.into() .into()
} }
}
#[async_trait::async_trait(?Send)]
impl UserType for Person {
/// As a given local person, send out a follow request to a remote community. /// As a given local person, send out a follow request to a remote community.
async fn send_follow( async fn send_follow(
&self, &self,
@ -110,40 +114,4 @@ impl ActorType for Person {
send_activity_single_dest(undo, self, community.inbox_url.into(), context).await?; send_activity_single_dest(undo, self, community.inbox_url.into(), context).await?;
Ok(()) Ok(())
} }
async fn send_accept_follow(
&self,
_follow: Follow,
_context: &LemmyContext,
) -> Result<(), LemmyError> {
unimplemented!()
}
async fn send_delete(&self, _context: &LemmyContext) -> Result<(), LemmyError> {
unimplemented!()
}
async fn send_undo_delete(&self, _context: &LemmyContext) -> Result<(), LemmyError> {
unimplemented!()
}
async fn send_remove(&self, _context: &LemmyContext) -> Result<(), LemmyError> {
unimplemented!()
}
async fn send_undo_remove(&self, _context: &LemmyContext) -> Result<(), LemmyError> {
unimplemented!()
}
async fn send_announce(
&self,
_activity: AnyBase,
_context: &LemmyContext,
) -> Result<(), LemmyError> {
unimplemented!()
}
async fn get_follower_inboxes(&self, _pool: &DbPool) -> Result<Vec<Url>, LemmyError> {
unimplemented!()
}
} }

View file

@ -3,6 +3,7 @@ use crate::{
extensions::signatures::sign_and_send, extensions::signatures::sign_and_send,
insert_activity, insert_activity,
ActorType, ActorType,
CommunityType,
APUB_JSON_CONTENT_TYPE, APUB_JSON_CONTENT_TYPE,
}; };
use activitystreams::{ use activitystreams::{

View file

@ -11,7 +11,8 @@ pub(crate) fn lemmy_context() -> Result<Vec<AnyBase>, LemmyError> {
"comments_enabled": { "comments_enabled": {
"kind": "sc:Boolean", "kind": "sc:Boolean",
"id": "pt:commentsEnabled" "id": "pt:commentsEnabled"
} },
"moderators": "as:moderators"
}))?; }))?;
Ok(vec![AnyBase::from(context()), context_ext]) Ok(vec![AnyBase::from(context()), context_ext])
} }

View file

@ -2,6 +2,7 @@ use activitystreams::unparsed::UnparsedMutExt;
use activitystreams_ext::UnparsedExtension; use activitystreams_ext::UnparsedExtension;
use lemmy_utils::LemmyError; use lemmy_utils::LemmyError;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use url::Url;
/// Activitystreams extension to allow (de)serializing additional Community field /// Activitystreams extension to allow (de)serializing additional Community field
/// `sensitive` (called 'nsfw' in Lemmy). /// `sensitive` (called 'nsfw' in Lemmy).
@ -9,12 +10,14 @@ use serde::{Deserialize, Serialize};
#[serde(rename_all = "camelCase")] #[serde(rename_all = "camelCase")]
pub struct GroupExtension { pub struct GroupExtension {
pub sensitive: Option<bool>, pub sensitive: Option<bool>,
pub moderators: Option<Url>,
} }
impl GroupExtension { impl GroupExtension {
pub fn new(sensitive: bool) -> Result<GroupExtension, LemmyError> { pub fn new(sensitive: bool, moderators_url: Url) -> Result<GroupExtension, LemmyError> {
Ok(GroupExtension { Ok(GroupExtension {
sensitive: Some(sensitive), sensitive: Some(sensitive),
moderators: Some(moderators_url),
}) })
} }
} }
@ -28,11 +31,13 @@ where
fn try_from_unparsed(unparsed_mut: &mut U) -> Result<Self, Self::Error> { fn try_from_unparsed(unparsed_mut: &mut U) -> Result<Self, Self::Error> {
Ok(GroupExtension { Ok(GroupExtension {
sensitive: unparsed_mut.remove("sensitive")?, sensitive: unparsed_mut.remove("sensitive")?,
moderators: unparsed_mut.remove("moderators")?,
}) })
} }
fn try_into_unparsed(self, unparsed_mut: &mut U) -> Result<(), Self::Error> { fn try_into_unparsed(self, unparsed_mut: &mut U) -> Result<(), Self::Error> {
unparsed_mut.insert("sensitive", self.sensitive)?; unparsed_mut.insert("sensitive", self.sensitive)?;
unparsed_mut.insert("moderators", self.moderators)?;
Ok(()) Ok(())
} }
} }

View file

@ -1,10 +1,5 @@
use crate::{ use crate::{
fetcher::{ fetcher::{fetch::fetch_remote_object, is_deleted, should_refetch_actor},
fetch::fetch_remote_object,
get_or_fetch_and_upsert_person,
is_deleted,
should_refetch_actor,
},
inbox::person_inbox::receive_announce, inbox::person_inbox::receive_announce,
objects::FromApub, objects::FromApub,
GroupExt, GroupExt,
@ -12,13 +7,12 @@ use crate::{
use activitystreams::{ use activitystreams::{
actor::ApActorExt, actor::ApActorExt,
collection::{CollectionExt, OrderedCollection}, collection::{CollectionExt, OrderedCollection},
object::ObjectExt,
}; };
use anyhow::Context; use anyhow::Context;
use diesel::result::Error::NotFound; use diesel::result::Error::NotFound;
use lemmy_api_structs::blocking; use lemmy_api_structs::blocking;
use lemmy_db_queries::{source::community::Community_, ApubObject, Joinable}; use lemmy_db_queries::{source::community::Community_, ApubObject};
use lemmy_db_schema::source::community::{Community, CommunityModerator, CommunityModeratorForm}; use lemmy_db_schema::source::community::Community;
use lemmy_utils::{location_info, LemmyError}; use lemmy_utils::{location_info, LemmyError};
use lemmy_websocket::LemmyContext; use lemmy_websocket::LemmyContext;
use log::debug; use log::debug;
@ -77,42 +71,14 @@ async fn fetch_remote_community(
} }
let group = group?; let group = group?;
let community = let community = Community::from_apub(
Community::from_apub(&group, context, apub_id.to_owned(), recursion_counter).await?; &group,
context,
// Also add the community moderators too apub_id.to_owned(),
let attributed_to = group.inner.attributed_to().context(location_info!())?; recursion_counter,
let creator_and_moderator_uris: Vec<&Url> = attributed_to false,
.as_many() )
.context(location_info!())? .await?;
.iter()
.map(|a| a.as_xsd_any_uri().context(""))
.collect::<Result<Vec<&Url>, anyhow::Error>>()?;
let mut creator_and_moderators = Vec::new();
for uri in creator_and_moderator_uris {
let c_or_m = get_or_fetch_and_upsert_person(uri, context, recursion_counter).await?;
creator_and_moderators.push(c_or_m);
}
// TODO: need to make this work to update mods of existing communities
if old_community.is_none() {
let community_id = community.id;
blocking(context.pool(), move |conn| {
for mod_ in creator_and_moderators {
let community_moderator_form = CommunityModeratorForm {
community_id,
person_id: mod_.id,
};
CommunityModerator::join(conn, &community_moderator_form)?;
}
Ok(()) as Result<(), LemmyError>
})
.await??;
}
// only fetch outbox for new communities, otherwise this can create an infinite loop // only fetch outbox for new communities, otherwise this can create an infinite loop
if old_community.is_none() { if old_community.is_none() {
@ -143,3 +109,27 @@ async fn fetch_community_outbox(
Ok(()) Ok(())
} }
pub(crate) async fn fetch_community_mods(
context: &LemmyContext,
group: &GroupExt,
recursion_counter: &mut i32,
) -> Result<Vec<Url>, LemmyError> {
if let Some(mods_url) = &group.ext_one.moderators {
let mods =
fetch_remote_object::<OrderedCollection>(context.client(), mods_url, recursion_counter)
.await?;
let mods = mods
.items()
.map(|i| i.as_many())
.flatten()
.context(location_info!())?
.iter()
.filter_map(|i| i.as_xsd_any_uri())
.map(|u| u.to_owned())
.collect();
Ok(mods)
} else {
Ok(vec![])
}
}

View file

@ -30,7 +30,14 @@ pub(crate) async fn get_or_fetch_and_insert_post(
debug!("Fetching and creating remote post: {}", post_ap_id); debug!("Fetching and creating remote post: {}", post_ap_id);
let page = let page =
fetch_remote_object::<PageExt>(context.client(), post_ap_id, recursion_counter).await?; fetch_remote_object::<PageExt>(context.client(), post_ap_id, recursion_counter).await?;
let post = Post::from_apub(&page, context, post_ap_id.to_owned(), recursion_counter).await?; let post = Post::from_apub(
&page,
context,
post_ap_id.to_owned(),
recursion_counter,
false,
)
.await?;
Ok(post) Ok(post)
} }
@ -67,6 +74,7 @@ pub(crate) async fn get_or_fetch_and_insert_comment(
context, context,
comment_ap_id.to_owned(), comment_ap_id.to_owned(),
recursion_counter, recursion_counter,
false,
) )
.await?; .await?;

View file

@ -46,8 +46,14 @@ pub(crate) async fn get_or_fetch_and_upsert_person(
return Ok(u); return Ok(u);
} }
let person = let person = Person::from_apub(
Person::from_apub(&person?, context, apub_id.to_owned(), recursion_counter).await?; &person?,
context,
apub_id.to_owned(),
recursion_counter,
false,
)
.await?;
let person_id = person.id; let person_id = person.id;
blocking(context.pool(), move |conn| { blocking(context.pool(), move |conn| {
@ -63,8 +69,14 @@ pub(crate) async fn get_or_fetch_and_upsert_person(
let person = let person =
fetch_remote_object::<PersonExt>(context.client(), apub_id, recursion_counter).await?; fetch_remote_object::<PersonExt>(context.client(), apub_id, recursion_counter).await?;
let person = let person = Person::from_apub(
Person::from_apub(&person, context, apub_id.to_owned(), recursion_counter).await?; &person,
context,
apub_id.to_owned(),
recursion_counter,
false,
)
.await?;
Ok(person) Ok(person)
} }

View file

@ -147,13 +147,13 @@ async fn build_response(
]; ];
} }
SearchAcceptedObjects::Page(p) => { SearchAcceptedObjects::Page(p) => {
let p = Post::from_apub(&p, context, query_url, recursion_counter).await?; let p = Post::from_apub(&p, context, query_url, recursion_counter, false).await?;
response.posts = response.posts =
vec![blocking(context.pool(), move |conn| PostView::read(conn, p.id, None)).await??]; vec![blocking(context.pool(), move |conn| PostView::read(conn, p.id, None)).await??];
} }
SearchAcceptedObjects::Comment(c) => { SearchAcceptedObjects::Comment(c) => {
let c = Comment::from_apub(&c, context, query_url, recursion_counter).await?; let c = Comment::from_apub(&c, context, query_url, recursion_counter, false).await?;
response.comments = vec![ response.comments = vec![
blocking(context.pool(), move |conn| { blocking(context.pool(), move |conn| {

View file

@ -12,12 +12,12 @@ use lemmy_websocket::LemmyContext;
use serde::Deserialize; use serde::Deserialize;
#[derive(Deserialize)] #[derive(Deserialize)]
pub struct CommentQuery { pub(crate) struct CommentQuery {
comment_id: String, comment_id: String,
} }
/// Return the ActivityPub json representation of a local comment over HTTP. /// Return the ActivityPub json representation of a local comment over HTTP.
pub async fn get_apub_comment( pub(crate) async fn get_apub_comment(
info: Path<CommentQuery>, info: Path<CommentQuery>,
context: web::Data<LemmyContext>, context: web::Data<LemmyContext>,
) -> Result<HttpResponse<Body>, LemmyError> { ) -> Result<HttpResponse<Body>, LemmyError> {

View file

@ -1,5 +1,6 @@
use crate::{ use crate::{
extensions::context::lemmy_context, extensions::context::lemmy_context,
generate_moderators_url,
http::{create_apub_response, create_apub_tombstone_response}, http::{create_apub_response, create_apub_tombstone_response},
objects::ToApub, objects::ToApub,
ActorType, ActorType,
@ -7,23 +8,27 @@ use crate::{
use activitystreams::{ use activitystreams::{
base::{AnyBase, BaseExt}, base::{AnyBase, BaseExt},
collection::{CollectionExt, OrderedCollection, UnorderedCollection}, collection::{CollectionExt, OrderedCollection, UnorderedCollection},
url::Url,
}; };
use actix_web::{body::Body, web, HttpResponse}; use actix_web::{body::Body, web, HttpResponse};
use lemmy_api_structs::blocking; use lemmy_api_structs::blocking;
use lemmy_db_queries::source::{activity::Activity_, community::Community_}; use lemmy_db_queries::source::{activity::Activity_, community::Community_};
use lemmy_db_schema::source::{activity::Activity, community::Community}; use lemmy_db_schema::source::{activity::Activity, community::Community};
use lemmy_db_views_actor::community_follower_view::CommunityFollowerView; use lemmy_db_views_actor::{
community_follower_view::CommunityFollowerView,
community_moderator_view::CommunityModeratorView,
};
use lemmy_utils::LemmyError; use lemmy_utils::LemmyError;
use lemmy_websocket::LemmyContext; use lemmy_websocket::LemmyContext;
use serde::Deserialize; use serde::Deserialize;
#[derive(Deserialize)] #[derive(Deserialize)]
pub struct CommunityQuery { pub(crate) struct CommunityQuery {
community_name: String, community_name: String,
} }
/// Return the ActivityPub json representation of a local community over HTTP. /// Return the ActivityPub json representation of a local community over HTTP.
pub async fn get_apub_community_http( pub(crate) async fn get_apub_community_http(
info: web::Path<CommunityQuery>, info: web::Path<CommunityQuery>,
context: web::Data<LemmyContext>, context: web::Data<LemmyContext>,
) -> Result<HttpResponse<Body>, LemmyError> { ) -> Result<HttpResponse<Body>, LemmyError> {
@ -42,7 +47,7 @@ pub async fn get_apub_community_http(
} }
/// Returns an empty followers collection, only populating the size (for privacy). /// Returns an empty followers collection, only populating the size (for privacy).
pub async fn get_apub_community_followers( pub(crate) async fn get_apub_community_followers(
info: web::Path<CommunityQuery>, info: web::Path<CommunityQuery>,
context: web::Data<LemmyContext>, context: web::Data<LemmyContext>,
) -> Result<HttpResponse<Body>, LemmyError> { ) -> Result<HttpResponse<Body>, LemmyError> {
@ -67,7 +72,7 @@ pub async fn get_apub_community_followers(
/// Returns the community outbox, which is populated by a maximum of 20 posts (but no other /// Returns the community outbox, which is populated by a maximum of 20 posts (but no other
/// activites like votes or comments). /// activites like votes or comments).
pub async fn get_apub_community_outbox( pub(crate) async fn get_apub_community_outbox(
info: web::Path<CommunityQuery>, info: web::Path<CommunityQuery>,
context: web::Data<LemmyContext>, context: web::Data<LemmyContext>,
) -> Result<HttpResponse<Body>, LemmyError> { ) -> Result<HttpResponse<Body>, LemmyError> {
@ -96,7 +101,7 @@ pub async fn get_apub_community_outbox(
Ok(create_apub_response(&collection)) Ok(create_apub_response(&collection))
} }
pub async fn get_apub_community_inbox( pub(crate) async fn get_apub_community_inbox(
info: web::Path<CommunityQuery>, info: web::Path<CommunityQuery>,
context: web::Data<LemmyContext>, context: web::Data<LemmyContext>,
) -> Result<HttpResponse<Body>, LemmyError> { ) -> Result<HttpResponse<Body>, LemmyError> {
@ -107,7 +112,39 @@ pub async fn get_apub_community_inbox(
let mut collection = OrderedCollection::new(); let mut collection = OrderedCollection::new();
collection collection
.set_id(format!("{}/inbox", community.actor_id).parse()?) .set_id(community.inbox_url.into())
.set_many_contexts(lemmy_context()?);
Ok(create_apub_response(&collection))
}
pub(crate) async fn get_apub_community_moderators(
info: web::Path<CommunityQuery>,
context: web::Data<LemmyContext>,
) -> Result<HttpResponse<Body>, LemmyError> {
let community = blocking(context.pool(), move |conn| {
Community::read_from_name(&conn, &info.community_name)
})
.await??;
// The attributed to, is an ordered vector with the creator actor_ids first,
// then the rest of the moderators
// TODO Technically the instance admins can mod the community, but lets
// ignore that for now
let cid = community.id;
let moderators = blocking(context.pool(), move |conn| {
CommunityModeratorView::for_community(&conn, cid)
})
.await??;
let moderators: Vec<Url> = moderators
.into_iter()
.map(|m| m.moderator.actor_id.into_inner())
.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)) Ok(create_apub_response(&collection))
} }

View file

@ -42,7 +42,7 @@ pub struct CommunityQuery {
} }
/// Return the ActivityPub json representation of a local community over HTTP. /// Return the ActivityPub json representation of a local community over HTTP.
pub async fn get_activity( pub(crate) async fn get_activity(
info: web::Path<CommunityQuery>, info: web::Path<CommunityQuery>,
context: web::Data<LemmyContext>, context: web::Data<LemmyContext>,
) -> Result<HttpResponse<Body>, LemmyError> { ) -> Result<HttpResponse<Body>, LemmyError> {

View file

@ -23,7 +23,7 @@ pub struct PersonQuery {
} }
/// Return the ActivityPub json representation of a local person over HTTP. /// Return the ActivityPub json representation of a local person over HTTP.
pub async fn get_apub_person_http( pub(crate) async fn get_apub_person_http(
info: web::Path<PersonQuery>, info: web::Path<PersonQuery>,
context: web::Data<LemmyContext>, context: web::Data<LemmyContext>,
) -> Result<HttpResponse<Body>, LemmyError> { ) -> Result<HttpResponse<Body>, LemmyError> {
@ -43,7 +43,7 @@ pub async fn get_apub_person_http(
} }
} }
pub async fn get_apub_person_outbox( pub(crate) async fn get_apub_person_outbox(
info: web::Path<PersonQuery>, info: web::Path<PersonQuery>,
context: web::Data<LemmyContext>, context: web::Data<LemmyContext>,
) -> Result<HttpResponse<Body>, LemmyError> { ) -> Result<HttpResponse<Body>, LemmyError> {
@ -61,7 +61,7 @@ pub async fn get_apub_person_outbox(
Ok(create_apub_response(&collection)) Ok(create_apub_response(&collection))
} }
pub async fn get_apub_person_inbox( pub(crate) async fn get_apub_person_inbox(
info: web::Path<PersonQuery>, info: web::Path<PersonQuery>,
context: web::Data<LemmyContext>, context: web::Data<LemmyContext>,
) -> Result<HttpResponse<Body>, LemmyError> { ) -> Result<HttpResponse<Body>, LemmyError> {
@ -72,7 +72,7 @@ pub async fn get_apub_person_inbox(
let mut collection = OrderedCollection::new(); let mut collection = OrderedCollection::new();
collection collection
.set_id(format!("{}/inbox", person.actor_id.into_inner()).parse()?) .set_id(person.inbox_url.into())
.set_many_contexts(lemmy_context()?); .set_many_contexts(lemmy_context()?);
Ok(create_apub_response(&collection)) Ok(create_apub_response(&collection))
} }

View file

@ -12,12 +12,12 @@ use lemmy_websocket::LemmyContext;
use serde::Deserialize; use serde::Deserialize;
#[derive(Deserialize)] #[derive(Deserialize)]
pub struct PostQuery { pub(crate) struct PostQuery {
post_id: String, post_id: String,
} }
/// Return the ActivityPub json representation of a local post over HTTP. /// Return the ActivityPub json representation of a local post over HTTP.
pub async fn get_apub_post( pub(crate) async fn get_apub_post(
info: web::Path<PostQuery>, info: web::Path<PostQuery>,
context: web::Data<LemmyContext>, context: web::Data<LemmyContext>,
) -> Result<HttpResponse<Body>, LemmyError> { ) -> Result<HttpResponse<Body>, LemmyError> {

View file

@ -6,18 +6,21 @@ use crate::{
get_activity_to_and_cc, get_activity_to_and_cc,
inbox_verify_http_signature, inbox_verify_http_signature,
is_activity_already_known, is_activity_already_known,
is_addressed_to_public,
receive_for_community::{ receive_for_community::{
receive_add_for_community,
receive_create_for_community, receive_create_for_community,
receive_delete_for_community, receive_delete_for_community,
receive_dislike_for_community, receive_dislike_for_community,
receive_like_for_community, receive_like_for_community,
receive_remove_for_community,
receive_undo_for_community, receive_undo_for_community,
receive_update_for_community, receive_update_for_community,
}, },
verify_is_addressed_to_public,
}, },
insert_activity, insert_activity,
ActorType, ActorType,
CommunityType,
}; };
use activitystreams::{ use activitystreams::{
activity::{kind::FollowType, ActorAndObject, Follow, Undo}, activity::{kind::FollowType, ActorAndObject, Follow, Undo},
@ -54,7 +57,8 @@ pub enum CommunityValidTypes {
Like, // upvote post or comment Like, // upvote post or comment
Dislike, // downvote post or comment Dislike, // downvote post or comment
Delete, // post or comment deleted by creator Delete, // post or comment deleted by creator
Remove, // post or comment removed by mod or admin Remove, // post or comment removed by mod or admin, or mod removed from community
Add, // mod added to community
} }
pub type CommunityAcceptedActivities = ActorAndObject<CommunityValidTypes>; pub type CommunityAcceptedActivities = ActorAndObject<CommunityValidTypes>;
@ -148,7 +152,8 @@ pub(crate) async fn community_receive_message(
true true
} }
CommunityValidTypes::Update => { CommunityValidTypes::Update => {
receive_update_for_community(context, any_base.clone(), &actor_url, request_counter).await?; receive_update_for_community(context, any_base.clone(), None, &actor_url, request_counter)
.await?;
true true
} }
CommunityValidTypes::Like => { CommunityValidTypes::Like => {
@ -160,19 +165,22 @@ pub(crate) async fn community_receive_message(
true true
} }
CommunityValidTypes::Delete => { CommunityValidTypes::Delete => {
receive_delete_for_community(context, any_base.clone(), &actor_url).await?; receive_delete_for_community(context, any_base.clone(), None, &actor_url).await?;
true
}
CommunityValidTypes::Add => {
receive_add_for_community(context, any_base.clone(), None, request_counter).await?;
true true
} }
CommunityValidTypes::Remove => { CommunityValidTypes::Remove => {
// TODO: we dont support remote mods, so this is ignored for now receive_remove_for_community(context, any_base.clone(), None, request_counter).await?;
//receive_remove_for_community(context, any_base.clone(), &person_url).await? true
false
} }
}; };
if do_announce { if do_announce {
// Check again that the activity is public, just to be sure // Check again that the activity is public, just to be sure
is_addressed_to_public(&activity)?; verify_is_addressed_to_public(&activity)?;
to_community to_community
.send_announce(activity.into_any_base()?, context) .send_announce(activity.into_any_base()?, context)
.await?; .await?;
@ -224,7 +232,7 @@ async fn handle_undo(
handle_undo_follow(any_base, actor_url, to_community, &context).await?; handle_undo_follow(any_base, actor_url, to_community, &context).await?;
Ok(false) Ok(false)
} else { } else {
receive_undo_for_community(context, any_base, &actor_url, request_counter).await?; receive_undo_for_community(context, any_base, None, &actor_url, request_counter).await?;
Ok(true) Ok(true)
} }
} }

View file

@ -27,7 +27,7 @@ use url::Url;
pub mod community_inbox; pub mod community_inbox;
pub mod person_inbox; pub mod person_inbox;
mod receive_for_community; pub(crate) mod receive_for_community;
pub mod shared_inbox; pub mod shared_inbox;
pub(crate) fn get_activity_id<T, Kind>(activity: &T, creator_uri: &Url) -> Result<Url, LemmyError> pub(crate) fn get_activity_id<T, Kind>(activity: &T, creator_uri: &Url) -> Result<Url, LemmyError>
@ -84,7 +84,7 @@ where
to_and_cc to_and_cc
} }
pub(crate) fn is_addressed_to_public<T, Kind>(activity: &T) -> Result<(), LemmyError> pub(crate) fn verify_is_addressed_to_public<T, Kind>(activity: &T) -> Result<(), LemmyError>
where where
T: AsBase<Kind> + AsObject<Kind> + ActorAndObjectRefExt, T: AsBase<Kind> + AsObject<Kind> + ActorAndObjectRefExt,
{ {

View file

@ -26,8 +26,8 @@ use crate::{
is_activity_already_known, is_activity_already_known,
is_addressed_to_community_followers, is_addressed_to_community_followers,
is_addressed_to_local_person, is_addressed_to_local_person,
is_addressed_to_public,
receive_for_community::{ receive_for_community::{
receive_add_for_community,
receive_create_for_community, receive_create_for_community,
receive_delete_for_community, receive_delete_for_community,
receive_dislike_for_community, receive_dislike_for_community,
@ -36,12 +36,13 @@ use crate::{
receive_undo_for_community, receive_undo_for_community,
receive_update_for_community, receive_update_for_community,
}, },
verify_is_addressed_to_public,
}, },
insert_activity, insert_activity,
ActorType, ActorType,
}; };
use activitystreams::{ use activitystreams::{
activity::{Accept, ActorAndObject, Announce, Create, Delete, Follow, Undo, Update}, activity::{Accept, ActorAndObject, Announce, Create, Delete, Follow, Remove, Undo, Update},
base::AnyBase, base::AnyBase,
prelude::*, prelude::*,
}; };
@ -165,7 +166,7 @@ pub(crate) async fn person_receive_message(
receive_delete(context, any_base, &actor_url, request_counter).await? receive_delete(context, any_base, &actor_url, request_counter).await?
} }
PersonValidTypes::Undo => receive_undo(context, any_base, &actor_url, request_counter).await?, PersonValidTypes::Undo => receive_undo(context, any_base, &actor_url, request_counter).await?,
PersonValidTypes::Remove => receive_remove_community(&context, any_base, &actor_url).await?, PersonValidTypes::Remove => receive_remove(context, any_base, &actor_url).await?,
}; };
// TODO: would be logical to move websocket notification code here // TODO: would be logical to move websocket notification code here
@ -252,6 +253,7 @@ enum AnnouncableActivities {
Delete, Delete,
Remove, Remove,
Undo, Undo,
Add,
} }
/// Takes an announce and passes the inner activity to the appropriate handler. /// Takes an announce and passes the inner activity to the appropriate handler.
@ -263,7 +265,7 @@ pub async fn receive_announce(
) -> Result<(), LemmyError> { ) -> Result<(), LemmyError> {
let announce = Announce::from_any_base(activity)?.context(location_info!())?; let announce = Announce::from_any_base(activity)?.context(location_info!())?;
verify_activity_domains_valid(&announce, &actor.actor_id(), false)?; verify_activity_domains_valid(&announce, &actor.actor_id(), false)?;
is_addressed_to_public(&announce)?; verify_is_addressed_to_public(&announce)?;
let kind = announce let kind = announce
.object() .object()
@ -287,7 +289,14 @@ pub async fn receive_announce(
receive_create_for_community(context, inner_activity, &inner_id, request_counter).await receive_create_for_community(context, inner_activity, &inner_id, request_counter).await
} }
Some(Update) => { Some(Update) => {
receive_update_for_community(context, inner_activity, &inner_id, request_counter).await receive_update_for_community(
context,
inner_activity,
Some(announce),
&inner_id,
request_counter,
)
.await
} }
Some(Like) => { Some(Like) => {
receive_like_for_community(context, inner_activity, &inner_id, request_counter).await receive_like_for_community(context, inner_activity, &inner_id, request_counter).await
@ -295,15 +304,31 @@ pub async fn receive_announce(
Some(Dislike) => { Some(Dislike) => {
receive_dislike_for_community(context, inner_activity, &inner_id, request_counter).await receive_dislike_for_community(context, inner_activity, &inner_id, request_counter).await
} }
Some(Delete) => receive_delete_for_community(context, inner_activity, &inner_id).await, Some(Delete) => {
Some(Remove) => receive_remove_for_community(context, inner_activity, &inner_id).await, receive_delete_for_community(context, inner_activity, Some(announce), &inner_id).await
}
Some(Remove) => {
receive_remove_for_community(context, inner_activity, Some(announce), request_counter).await
}
Some(Undo) => { Some(Undo) => {
receive_undo_for_community(context, inner_activity, &inner_id, request_counter).await receive_undo_for_community(
context,
inner_activity,
Some(announce),
&inner_id,
request_counter,
)
.await
}
Some(Add) => {
receive_add_for_community(context, inner_activity, Some(announce), request_counter).await
} }
_ => receive_unhandled_activity(inner_activity), _ => receive_unhandled_activity(inner_activity),
} }
} }
/// Receive either a new private message, or a new comment mention. We distinguish them by checking
/// whether the activity is public.
async fn receive_create( async fn receive_create(
context: &LemmyContext, context: &LemmyContext,
activity: AnyBase, activity: AnyBase,
@ -312,13 +337,15 @@ async fn receive_create(
) -> Result<(), LemmyError> { ) -> Result<(), LemmyError> {
let create = Create::from_any_base(activity)?.context(location_info!())?; let create = Create::from_any_base(activity)?.context(location_info!())?;
verify_activity_domains_valid(&create, &expected_domain, true)?; verify_activity_domains_valid(&create, &expected_domain, true)?;
if is_addressed_to_public(&create).is_ok() { if verify_is_addressed_to_public(&create).is_ok() {
receive_create_comment(create, context, request_counter).await receive_create_comment(create, context, request_counter).await
} else { } else {
receive_create_private_message(&context, create, expected_domain, request_counter).await receive_create_private_message(&context, create, expected_domain, request_counter).await
} }
} }
/// Receive either an updated private message, or an updated comment mention. We distinguish
/// them by checking whether the activity is public.
async fn receive_update( async fn receive_update(
context: &LemmyContext, context: &LemmyContext,
activity: AnyBase, activity: AnyBase,
@ -327,7 +354,7 @@ async fn receive_update(
) -> Result<(), LemmyError> { ) -> Result<(), LemmyError> {
let update = Update::from_any_base(activity)?.context(location_info!())?; let update = Update::from_any_base(activity)?.context(location_info!())?;
verify_activity_domains_valid(&update, &expected_domain, true)?; verify_activity_domains_valid(&update, &expected_domain, true)?;
if is_addressed_to_public(&update).is_ok() { if verify_is_addressed_to_public(&update).is_ok() {
receive_update_comment(update, context, request_counter).await receive_update_comment(update, context, request_counter).await
} else { } else {
receive_update_private_message(&context, update, expected_domain, request_counter).await receive_update_private_message(&context, update, expected_domain, request_counter).await
@ -356,13 +383,31 @@ async fn receive_delete(
} }
} }
async fn receive_remove(
context: &LemmyContext,
any_base: AnyBase,
expected_domain: &Url,
) -> Result<(), LemmyError> {
let remove = Remove::from_any_base(any_base.clone())?.context(location_info!())?;
verify_activity_domains_valid(&remove, expected_domain, true)?;
let object_uri = remove
.object()
.to_owned()
.single_xsd_any_uri()
.context(location_info!())?;
let community = blocking(context.pool(), move |conn| {
Community::read_from_apub_id(conn, &object_uri.into())
})
.await??;
receive_remove_community(&context, community).await
}
async fn receive_undo( async fn receive_undo(
context: &LemmyContext, context: &LemmyContext,
any_base: AnyBase, any_base: AnyBase,
expected_domain: &Url, expected_domain: &Url,
request_counter: &mut i32, request_counter: &mut i32,
) -> Result<(), LemmyError> { ) -> Result<(), LemmyError> {
use CommunityOrPrivateMessage::*;
let undo = Undo::from_any_base(any_base)?.context(location_info!())?; let undo = Undo::from_any_base(any_base)?.context(location_info!())?;
verify_activity_domains_valid(&undo, expected_domain, true)?; verify_activity_domains_valid(&undo, expected_domain, true)?;
@ -377,15 +422,28 @@ async fn receive_undo(
.to_owned() .to_owned()
.single_xsd_any_uri() .single_xsd_any_uri()
.context(location_info!())?; .context(location_info!())?;
use CommunityOrPrivateMessage::*;
match find_community_or_private_message_by_id(context, object_uri).await? { match find_community_or_private_message_by_id(context, object_uri).await? {
Community(c) => receive_undo_delete_community(context, undo, c, expected_domain).await, Community(c) => receive_undo_delete_community(context, c).await,
PrivateMessage(p) => { PrivateMessage(p) => {
receive_undo_delete_private_message(context, undo, expected_domain, p, request_counter) receive_undo_delete_private_message(context, undo, expected_domain, p, request_counter)
.await .await
} }
} }
} }
Some("Remove") => receive_undo_remove_community(context, undo, expected_domain).await, Some("Remove") => {
let remove = Remove::from_any_base(inner_activity)?.context(location_info!())?;
let object_uri = remove
.object()
.to_owned()
.single_xsd_any_uri()
.context(location_info!())?;
let community = blocking(context.pool(), move |conn| {
Community::read_from_apub_id(conn, &object_uri.into())
})
.await??;
receive_undo_remove_community(context, community).await
}
_ => receive_unhandled_activity(undo), _ => receive_unhandled_activity(undo),
} }
} }

View file

@ -31,21 +31,50 @@ use crate::{
receive_unhandled_activity, receive_unhandled_activity,
verify_activity_domains_valid, verify_activity_domains_valid,
}, },
fetcher::objects::{get_or_fetch_and_insert_comment, get_or_fetch_and_insert_post}, fetcher::{
objects::{get_or_fetch_and_insert_comment, get_or_fetch_and_insert_post},
person::get_or_fetch_and_upsert_person,
},
find_object_by_id,
find_post_or_comment_by_id, find_post_or_comment_by_id,
inbox::is_addressed_to_public, generate_moderators_url,
inbox::verify_is_addressed_to_public,
ActorType,
CommunityType,
Object,
PostOrComment, PostOrComment,
}; };
use activitystreams::{ use activitystreams::{
activity::{Create, Delete, Dislike, Like, Remove, Undo, Update}, activity::{
ActorAndObjectRef,
Add,
Announce,
Create,
Delete,
Dislike,
Like,
OptTargetRef,
Remove,
Undo,
Update,
},
base::AnyBase, base::AnyBase,
object::AsObject,
prelude::*, prelude::*,
}; };
use anyhow::Context; use anyhow::{anyhow, Context};
use diesel::result::Error::NotFound; use diesel::result::Error::NotFound;
use lemmy_api_structs::blocking; use lemmy_api_structs::blocking;
use lemmy_db_queries::Crud; use lemmy_db_queries::{source::community::CommunityModerator_, ApubObject, Crud, Joinable};
use lemmy_db_schema::source::site::Site; use lemmy_db_schema::{
source::{
community::{Community, CommunityModerator, CommunityModeratorForm},
person::Person,
site::Site,
},
DbUrl,
};
use lemmy_db_views_actor::community_view::CommunityView;
use lemmy_utils::{location_info, LemmyError}; use lemmy_utils::{location_info, LemmyError};
use lemmy_websocket::LemmyContext; use lemmy_websocket::LemmyContext;
use strum_macros::EnumString; use strum_macros::EnumString;
@ -69,7 +98,7 @@ pub(in crate::inbox) async fn receive_create_for_community(
) -> Result<(), LemmyError> { ) -> Result<(), LemmyError> {
let create = Create::from_any_base(activity)?.context(location_info!())?; let create = Create::from_any_base(activity)?.context(location_info!())?;
verify_activity_domains_valid(&create, &expected_domain, true)?; verify_activity_domains_valid(&create, &expected_domain, true)?;
is_addressed_to_public(&create)?; verify_is_addressed_to_public(&create)?;
let kind = create let kind = create
.object() .object()
@ -86,19 +115,21 @@ pub(in crate::inbox) async fn receive_create_for_community(
pub(in crate::inbox) async fn receive_update_for_community( pub(in crate::inbox) async fn receive_update_for_community(
context: &LemmyContext, context: &LemmyContext,
activity: AnyBase, activity: AnyBase,
announce: Option<Announce>,
expected_domain: &Url, expected_domain: &Url,
request_counter: &mut i32, request_counter: &mut i32,
) -> Result<(), LemmyError> { ) -> Result<(), LemmyError> {
let update = Update::from_any_base(activity)?.context(location_info!())?; let update = Update::from_any_base(activity)?.context(location_info!())?;
verify_activity_domains_valid(&update, &expected_domain, true)?; verify_activity_domains_valid(&update, &expected_domain, false)?;
is_addressed_to_public(&update)?; verify_is_addressed_to_public(&update)?;
verify_modification_actor_instance(&update, &announce, context).await?;
let kind = update let kind = update
.object() .object()
.as_single_kind_str() .as_single_kind_str()
.and_then(|s| s.parse().ok()); .and_then(|s| s.parse().ok());
match kind { match kind {
Some(PageOrNote::Page) => receive_update_post(update, context, request_counter).await, Some(PageOrNote::Page) => receive_update_post(update, announce, context, request_counter).await,
Some(PageOrNote::Note) => receive_update_comment(update, context, request_counter).await, Some(PageOrNote::Note) => receive_update_comment(update, context, request_counter).await,
_ => receive_unhandled_activity(update), _ => receive_unhandled_activity(update),
} }
@ -113,7 +144,7 @@ pub(in crate::inbox) async fn receive_like_for_community(
) -> Result<(), LemmyError> { ) -> Result<(), LemmyError> {
let like = Like::from_any_base(activity)?.context(location_info!())?; let like = Like::from_any_base(activity)?.context(location_info!())?;
verify_activity_domains_valid(&like, &expected_domain, false)?; verify_activity_domains_valid(&like, &expected_domain, false)?;
is_addressed_to_public(&like)?; verify_is_addressed_to_public(&like)?;
let object_id = like let object_id = like
.object() .object()
@ -144,7 +175,7 @@ pub(in crate::inbox) async fn receive_dislike_for_community(
let dislike = Dislike::from_any_base(activity)?.context(location_info!())?; let dislike = Dislike::from_any_base(activity)?.context(location_info!())?;
verify_activity_domains_valid(&dislike, &expected_domain, false)?; verify_activity_domains_valid(&dislike, &expected_domain, false)?;
is_addressed_to_public(&dislike)?; verify_is_addressed_to_public(&dislike)?;
let object_id = dislike let object_id = dislike
.object() .object()
@ -164,11 +195,13 @@ pub(in crate::inbox) async fn receive_dislike_for_community(
pub(in crate::inbox) async fn receive_delete_for_community( pub(in crate::inbox) async fn receive_delete_for_community(
context: &LemmyContext, context: &LemmyContext,
activity: AnyBase, activity: AnyBase,
announce: Option<Announce>,
expected_domain: &Url, expected_domain: &Url,
) -> Result<(), LemmyError> { ) -> Result<(), LemmyError> {
let delete = Delete::from_any_base(activity)?.context(location_info!())?; let delete = Delete::from_any_base(activity)?.context(location_info!())?;
verify_activity_domains_valid(&delete, &expected_domain, true)?; verify_activity_domains_valid(&delete, &expected_domain, true)?;
is_addressed_to_public(&delete)?; verify_is_addressed_to_public(&delete)?;
verify_modification_actor_instance(&delete, &announce, context).await?;
let object = delete let object = delete
.object() .object()
@ -187,40 +220,50 @@ pub(in crate::inbox) async fn receive_delete_for_community(
/// A post or comment being removed by a mod/admin /// A post or comment being removed by a mod/admin
pub(in crate::inbox) async fn receive_remove_for_community( pub(in crate::inbox) async fn receive_remove_for_community(
context: &LemmyContext, context: &LemmyContext,
activity: AnyBase, remove_any_base: AnyBase,
expected_domain: &Url, announce: Option<Announce>,
request_counter: &mut i32,
) -> Result<(), LemmyError> { ) -> Result<(), LemmyError> {
let remove = Remove::from_any_base(activity)?.context(location_info!())?; let remove = Remove::from_any_base(remove_any_base.to_owned())?.context(location_info!())?;
verify_activity_domains_valid(&remove, &expected_domain, false)?; let community = extract_community_from_cc(&remove, context).await?;
is_addressed_to_public(&remove)?;
let cc = remove verify_mod_activity(&remove, announce, &community, context).await?;
.cc() verify_is_addressed_to_public(&remove)?;
.map(|c| c.as_many())
.flatten()
.context(location_info!())?;
let community_id = cc
.first()
.map(|c| c.as_xsd_any_uri())
.flatten()
.context(location_info!())?;
if remove.target().is_some() {
let remove_mod = remove
.object()
.as_single_xsd_any_uri()
.context(location_info!())?;
let remove_mod = get_or_fetch_and_upsert_person(&remove_mod, context, request_counter).await?;
let form = CommunityModeratorForm {
community_id: community.id,
person_id: remove_mod.id,
};
blocking(context.pool(), move |conn| {
CommunityModerator::leave(conn, &form)
})
.await??;
community.send_announce(remove_any_base, context).await?;
// TODO: send websocket notification about removed mod
Ok(())
}
// Remove a post or comment
else {
let object = remove let object = remove
.object() .object()
.to_owned() .to_owned()
.single_xsd_any_uri() .single_xsd_any_uri()
.context(location_info!())?; .context(location_info!())?;
// Ensure that remove activity comes from the same domain as the community
remove.id(community_id.domain().context(location_info!())?)?;
match find_post_or_comment_by_id(context, object).await { match find_post_or_comment_by_id(context, object).await {
Ok(PostOrComment::Post(p)) => receive_remove_post(context, remove, *p).await, Ok(PostOrComment::Post(p)) => receive_remove_post(context, *p).await,
Ok(PostOrComment::Comment(c)) => receive_remove_comment(context, remove, *c).await, Ok(PostOrComment::Comment(c)) => receive_remove_comment(context, *c).await,
// if we dont have the object, no need to do anything // if we dont have the object, no need to do anything
Err(_) => Ok(()), Err(_) => Ok(()),
} }
} }
}
#[derive(EnumString)] #[derive(EnumString)]
enum UndoableActivities { enum UndoableActivities {
@ -234,12 +277,13 @@ enum UndoableActivities {
pub(in crate::inbox) async fn receive_undo_for_community( pub(in crate::inbox) async fn receive_undo_for_community(
context: &LemmyContext, context: &LemmyContext,
activity: AnyBase, activity: AnyBase,
announce: Option<Announce>,
expected_domain: &Url, expected_domain: &Url,
request_counter: &mut i32, request_counter: &mut i32,
) -> Result<(), LemmyError> { ) -> Result<(), LemmyError> {
let undo = Undo::from_any_base(activity)?.context(location_info!())?; let undo = Undo::from_any_base(activity)?.context(location_info!())?;
verify_activity_domains_valid(&undo, &expected_domain.to_owned(), true)?; verify_activity_domains_valid(&undo, &expected_domain.to_owned(), true)?;
is_addressed_to_public(&undo)?; verify_is_addressed_to_public(&undo)?;
use UndoableActivities::*; use UndoableActivities::*;
match undo match undo
@ -248,7 +292,9 @@ pub(in crate::inbox) async fn receive_undo_for_community(
.and_then(|s| s.parse().ok()) .and_then(|s| s.parse().ok())
{ {
Some(Delete) => receive_undo_delete_for_community(context, undo, expected_domain).await, Some(Delete) => receive_undo_delete_for_community(context, undo, expected_domain).await,
Some(Remove) => receive_undo_remove_for_community(context, undo, expected_domain).await, Some(Remove) => {
receive_undo_remove_for_community(context, undo, announce, expected_domain).await
}
Some(Like) => { Some(Like) => {
receive_undo_like_for_community(context, undo, expected_domain, request_counter).await receive_undo_like_for_community(context, undo, expected_domain, request_counter).await
} }
@ -268,7 +314,7 @@ pub(in crate::inbox) async fn receive_undo_delete_for_community(
let delete = Delete::from_any_base(undo.object().to_owned().one().context(location_info!())?)? let delete = Delete::from_any_base(undo.object().to_owned().one().context(location_info!())?)?
.context(location_info!())?; .context(location_info!())?;
verify_activity_domains_valid(&delete, &expected_domain, true)?; verify_activity_domains_valid(&delete, &expected_domain, true)?;
is_addressed_to_public(&delete)?; verify_is_addressed_to_public(&delete)?;
let object = delete let object = delete
.object() .object()
@ -287,12 +333,14 @@ pub(in crate::inbox) async fn receive_undo_delete_for_community(
pub(in crate::inbox) async fn receive_undo_remove_for_community( pub(in crate::inbox) async fn receive_undo_remove_for_community(
context: &LemmyContext, context: &LemmyContext,
undo: Undo, undo: Undo,
announce: Option<Announce>,
expected_domain: &Url, expected_domain: &Url,
) -> Result<(), LemmyError> { ) -> Result<(), LemmyError> {
let remove = Remove::from_any_base(undo.object().to_owned().one().context(location_info!())?)? let remove = Remove::from_any_base(undo.object().to_owned().one().context(location_info!())?)?
.context(location_info!())?; .context(location_info!())?;
verify_activity_domains_valid(&remove, &expected_domain, false)?; verify_activity_domains_valid(&remove, &expected_domain, false)?;
is_addressed_to_public(&remove)?; verify_is_addressed_to_public(&remove)?;
verify_undo_remove_actor_instance(&undo, &remove, &announce, context).await?;
let object = remove let object = remove
.object() .object()
@ -317,7 +365,7 @@ pub(in crate::inbox) async fn receive_undo_like_for_community(
let like = Like::from_any_base(undo.object().to_owned().one().context(location_info!())?)? let like = Like::from_any_base(undo.object().to_owned().one().context(location_info!())?)?
.context(location_info!())?; .context(location_info!())?;
verify_activity_domains_valid(&like, &expected_domain, false)?; verify_activity_domains_valid(&like, &expected_domain, false)?;
is_addressed_to_public(&like)?; verify_is_addressed_to_public(&like)?;
let object_id = like let object_id = like
.object() .object()
@ -333,6 +381,50 @@ pub(in crate::inbox) async fn receive_undo_like_for_community(
} }
} }
/// Add a new mod to the community (can only be done by an existing mod).
pub(in crate::inbox) async fn receive_add_for_community(
context: &LemmyContext,
add_any_base: AnyBase,
announce: Option<Announce>,
request_counter: &mut i32,
) -> Result<(), LemmyError> {
let add = Add::from_any_base(add_any_base.to_owned())?.context(location_info!())?;
let community = extract_community_from_cc(&add, context).await?;
verify_mod_activity(&add, announce, &community, context).await?;
verify_is_addressed_to_public(&add)?;
verify_add_remove_moderator_target(&add, &community)?;
let new_mod = add
.object()
.as_single_xsd_any_uri()
.context(location_info!())?;
let new_mod = get_or_fetch_and_upsert_person(&new_mod, context, request_counter).await?;
// If we had to refetch the community while parsing the activity, then the new mod has already
// been added. Skip it here as it would result in a duplicate key error.
let new_mod_id = new_mod.id;
let moderated_communities = blocking(context.pool(), move |conn| {
CommunityModerator::get_person_moderated_communities(conn, new_mod_id)
})
.await??;
if !moderated_communities.contains(&community.id) {
let form = CommunityModeratorForm {
community_id: community.id,
person_id: new_mod.id,
};
blocking(context.pool(), move |conn| {
CommunityModerator::join(conn, &form)
})
.await??;
}
if community.local {
community.send_announce(add_any_base, context).await?;
}
// TODO: send websocket notification about added mod
Ok(())
}
/// A post or comment downvote being reverted /// A post or comment downvote being reverted
pub(in crate::inbox) async fn receive_undo_dislike_for_community( pub(in crate::inbox) async fn receive_undo_dislike_for_community(
context: &LemmyContext, context: &LemmyContext,
@ -343,7 +435,7 @@ pub(in crate::inbox) async fn receive_undo_dislike_for_community(
let dislike = Dislike::from_any_base(undo.object().to_owned().one().context(location_info!())?)? let dislike = Dislike::from_any_base(undo.object().to_owned().one().context(location_info!())?)?
.context(location_info!())?; .context(location_info!())?;
verify_activity_domains_valid(&dislike, &expected_domain, false)?; verify_activity_domains_valid(&dislike, &expected_domain, false)?;
is_addressed_to_public(&dislike)?; verify_is_addressed_to_public(&dislike)?;
let object_id = dislike let object_id = dislike
.object() .object()
@ -374,3 +466,172 @@ async fn fetch_post_or_comment_by_id(
Err(NotFound.into()) Err(NotFound.into())
} }
/// Searches the activity's cc field for a Community ID, and returns the community.
async fn extract_community_from_cc<T, Kind>(
activity: &T,
context: &LemmyContext,
) -> Result<Community, LemmyError>
where
T: AsObject<Kind>,
{
let cc = activity
.cc()
.map(|c| c.as_many())
.flatten()
.context(location_info!())?;
let community_id = cc
.first()
.map(|c| c.as_xsd_any_uri())
.flatten()
.context(location_info!())?;
let community_id: DbUrl = community_id.to_owned().into();
let community = blocking(&context.pool(), move |conn| {
Community::read_from_apub_id(&conn, &community_id)
})
.await??;
Ok(community)
}
/// Checks that a moderation activity was sent by a user who is listed as mod for the community.
/// This is only used in the case of remote mods, as local mod actions don't go through the
/// community inbox.
///
/// This method should only be used for activities received by the community, not for activities
/// used by community followers.
async fn verify_actor_is_community_mod<T, Kind>(
activity: &T,
community: &Community,
context: &LemmyContext,
) -> Result<(), LemmyError>
where
T: ActorAndObjectRef + BaseExt<Kind>,
{
let actor = activity
.actor()?
.as_single_xsd_any_uri()
.context(location_info!())?
.to_owned();
let actor = blocking(&context.pool(), move |conn| {
Person::read_from_apub_id(&conn, &actor.into())
})
.await??;
// Note: this will also return true for admins in addition to mods, but as we dont know about
// remote admins, it doesnt make any difference.
let community_id = community.id;
let actor_id = actor.id;
let is_mod_or_admin = blocking(context.pool(), move |conn| {
CommunityView::is_mod_or_admin(conn, actor_id, community_id)
})
.await?;
if !is_mod_or_admin {
return Err(anyhow!("Not a mod").into());
}
Ok(())
}
/// This method behaves differently, depending if it is called via community inbox (activity
/// received by community from a remote user), or via user inbox (activity received by user from
/// community). We distinguish the cases by checking if the activity is wrapper in an announce
/// (only true when sent from user to community).
///
/// In the first case, we check that the actor is listed as community mod. In the second case, we
/// only check that the announce comes from the same domain as the activity. We trust the
/// community's instance to have validated the inner activity correctly. We can't do this validation
/// here, because we don't know who the instance admins are. Plus this allows for compatibility with
/// software that uses different rules for mod actions.
pub(crate) async fn verify_mod_activity<T, Kind>(
mod_action: &T,
announce: Option<Announce>,
community: &Community,
context: &LemmyContext,
) -> Result<(), LemmyError>
where
T: ActorAndObjectRef + BaseExt<Kind>,
{
match announce {
None => verify_actor_is_community_mod(mod_action, community, context).await?,
Some(a) => verify_activity_domains_valid(&a, &community.actor_id.to_owned().into(), false)?,
}
Ok(())
}
/// For Add/Remove community moderator activities, check that the target field actually contains
/// /c/community/moderators. Any different values are unsupported.
fn verify_add_remove_moderator_target<T, Kind>(
activity: &T,
community: &Community,
) -> Result<(), LemmyError>
where
T: ActorAndObjectRef + BaseExt<Kind> + OptTargetRef,
{
let target = activity
.target()
.map(|t| t.as_single_xsd_any_uri())
.flatten()
.context(location_info!())?;
if target != &generate_moderators_url(&community.actor_id)?.into_inner() {
return Err(anyhow!("Unkown target url").into());
}
Ok(())
}
/// For activities like Update, Delete or Remove, check that the actor is from the same instance
/// as the original object itself (or is a remote mod).
///
/// Note: This is only needed for mod actions. Normal user actions (edit post, undo vote etc) are
/// already verified with `expected_domain`, so this serves as an additional check.
async fn verify_modification_actor_instance<T, Kind>(
activity: &T,
announce: &Option<Announce>,
context: &LemmyContext,
) -> Result<(), LemmyError>
where
T: ActorAndObjectRef + BaseExt<Kind> + AsObject<Kind>,
{
let actor_id = activity
.actor()?
.to_owned()
.single_xsd_any_uri()
.context(location_info!())?;
let object_id = activity
.object()
.as_one()
.map(|o| o.id())
.flatten()
.context(location_info!())?;
let original_id = match find_object_by_id(context, object_id.to_owned()).await? {
Object::Post(p) => p.ap_id.into_inner(),
Object::Comment(c) => c.ap_id.into_inner(),
Object::Community(c) => c.actor_id(),
Object::Person(p) => p.actor_id(),
Object::PrivateMessage(p) => p.ap_id.into_inner(),
};
if actor_id.domain() != original_id.domain() {
let community = extract_community_from_cc(activity, context).await?;
verify_mod_activity(activity, announce.to_owned(), &community, context).await?;
}
Ok(())
}
pub(crate) async fn verify_undo_remove_actor_instance<T, Kind>(
undo: &Undo,
inner: &T,
announce: &Option<Announce>,
context: &LemmyContext,
) -> Result<(), LemmyError>
where
T: ActorAndObjectRef + BaseExt<Kind> + AsObject<Kind>,
{
if announce.is_none() {
let community = extract_community_from_cc(undo, context).await?;
verify_mod_activity(undo, announce.to_owned(), &community, context).await?;
verify_mod_activity(inner, announce.to_owned(), &community, context).await?;
}
Ok(())
}

View file

@ -36,6 +36,7 @@ pub enum ValidTypes {
Undo, Undo,
Remove, Remove,
Announce, Announce,
Add,
} }
// TODO: this isnt entirely correct, cause some of these receive are not ActorAndObject, // TODO: this isnt entirely correct, cause some of these receive are not ActorAndObject,

View file

@ -17,7 +17,7 @@ use crate::extensions::{
}; };
use activitystreams::{ use activitystreams::{
activity::Follow, activity::Follow,
actor::{ApActor, Group, Person}, actor,
base::AnyBase, base::AnyBase,
object::{ApObject, Note, Page}, object::{ApObject, Note, Page},
}; };
@ -31,7 +31,7 @@ use lemmy_db_schema::{
activity::Activity, activity::Activity,
comment::Comment, comment::Comment,
community::Community, community::Community,
person::Person as DbPerson, person::{Person as DbPerson, Person},
post::Post, post::Post,
private_message::PrivateMessage, private_message::PrivateMessage,
}, },
@ -44,9 +44,9 @@ use std::net::IpAddr;
use url::{ParseError, Url}; use url::{ParseError, Url};
/// Activitystreams type for community /// Activitystreams type for community
type GroupExt = Ext2<ApActor<ApObject<Group>>, GroupExtension, PublicKeyExtension>; type GroupExt = Ext2<actor::ApActor<ApObject<actor::Group>>, GroupExtension, PublicKeyExtension>;
/// Activitystreams type for person /// Activitystreams type for person
type PersonExt = Ext1<ApActor<ApObject<Person>>, PublicKeyExtension>; type PersonExt = Ext1<actor::ApActor<ApObject<actor::Person>>, PublicKeyExtension>;
/// Activitystreams type for post /// Activitystreams type for post
type PageExt = Ext1<ApObject<Page>, PageExtension>; type PageExt = Ext1<ApObject<Page>, PageExtension>;
type NoteExt = ApObject<Note>; type NoteExt = ApObject<Note>;
@ -166,38 +166,6 @@ pub trait ActorType {
fn public_key(&self) -> Option<String>; fn public_key(&self) -> Option<String>;
fn private_key(&self) -> Option<String>; fn private_key(&self) -> Option<String>;
async fn send_follow(
&self,
follow_actor_id: &Url,
context: &LemmyContext,
) -> Result<(), LemmyError>;
async fn send_unfollow(
&self,
follow_actor_id: &Url,
context: &LemmyContext,
) -> Result<(), LemmyError>;
async fn send_accept_follow(
&self,
follow: Follow,
context: &LemmyContext,
) -> Result<(), LemmyError>;
async fn send_delete(&self, context: &LemmyContext) -> Result<(), LemmyError>;
async fn send_undo_delete(&self, context: &LemmyContext) -> Result<(), LemmyError>;
async fn send_remove(&self, context: &LemmyContext) -> Result<(), LemmyError>;
async fn send_undo_remove(&self, context: &LemmyContext) -> Result<(), LemmyError>;
async fn send_announce(
&self,
activity: AnyBase,
context: &LemmyContext,
) -> Result<(), LemmyError>;
/// For a given community, returns the inboxes of all followers.
async fn get_follower_inboxes(&self, pool: &DbPool) -> Result<Vec<Url>, LemmyError>;
fn get_shared_inbox_or_inbox_url(&self) -> Url; fn get_shared_inbox_or_inbox_url(&self) -> Url;
/// Outbox URL is not generally used by Lemmy, so it can be generated on the fly (but only for /// Outbox URL is not generally used by Lemmy, so it can be generated on the fly (but only for
@ -221,6 +189,55 @@ pub trait ActorType {
} }
} }
#[async_trait::async_trait(?Send)]
pub trait CommunityType {
async fn get_follower_inboxes(&self, pool: &DbPool) -> Result<Vec<Url>, LemmyError>;
async fn send_accept_follow(
&self,
follow: Follow,
context: &LemmyContext,
) -> Result<(), LemmyError>;
async fn send_delete(&self, context: &LemmyContext) -> Result<(), LemmyError>;
async fn send_undo_delete(&self, context: &LemmyContext) -> Result<(), LemmyError>;
async fn send_remove(&self, context: &LemmyContext) -> Result<(), LemmyError>;
async fn send_undo_remove(&self, context: &LemmyContext) -> Result<(), LemmyError>;
async fn send_announce(
&self,
activity: AnyBase,
context: &LemmyContext,
) -> Result<(), LemmyError>;
async fn send_add_mod(
&self,
actor: &Person,
added_mod: Person,
context: &LemmyContext,
) -> Result<(), LemmyError>;
async fn send_remove_mod(
&self,
actor: &Person,
removed_mod: Person,
context: &LemmyContext,
) -> Result<(), LemmyError>;
}
#[async_trait::async_trait(?Send)]
pub trait UserType {
async fn send_follow(
&self,
follow_actor_id: &Url,
context: &LemmyContext,
) -> Result<(), LemmyError>;
async fn send_unfollow(
&self,
follow_actor_id: &Url,
context: &LemmyContext,
) -> Result<(), LemmyError>;
}
pub enum EndpointType { pub enum EndpointType {
Community, Community,
Person, Person,
@ -276,6 +293,10 @@ pub fn generate_shared_inbox_url(actor_id: &DbUrl) -> Result<DbUrl, LemmyError>
Ok(Url::parse(&url)?.into()) Ok(Url::parse(&url)?.into())
} }
pub(crate) fn generate_moderators_url(community_id: &DbUrl) -> Result<DbUrl, LemmyError> {
Ok(Url::parse(&format!("{}/moderators", community_id))?.into())
}
/// Store a sent or received activity in the database, for logging purposes. These records are not /// Store a sent or received activity in the database, for logging purposes. These records are not
/// persistent. /// persistent.
pub(crate) async fn insert_activity<T>( pub(crate) async fn insert_activity<T>(
@ -329,6 +350,7 @@ pub(crate) async fn find_post_or_comment_by_id(
Err(NotFound.into()) Err(NotFound.into())
} }
#[derive(Debug)]
pub(crate) enum Object { pub(crate) enum Object {
Comment(Box<Comment>), Comment(Box<Comment>),
Post(Box<Post>), Post(Box<Post>),

View file

@ -102,9 +102,16 @@ impl FromApub for Comment {
context: &LemmyContext, context: &LemmyContext,
expected_domain: Url, expected_domain: Url,
request_counter: &mut i32, request_counter: &mut i32,
mod_action_allowed: bool,
) -> Result<Comment, LemmyError> { ) -> Result<Comment, LemmyError> {
let comment: Comment = let comment: Comment = get_object_from_apub(
get_object_from_apub(note, context, expected_domain, request_counter).await?; note,
context,
expected_domain,
request_counter,
mod_action_allowed,
)
.await?;
let post_id = comment.post_id; let post_id = comment.post_id;
let post = blocking(context.pool(), move |conn| Post::read(conn, post_id)).await??; let post = blocking(context.pool(), move |conn| Post::read(conn, post_id)).await??;
@ -131,6 +138,7 @@ impl FromApubToForm<NoteExt> for CommentForm {
context: &LemmyContext, context: &LemmyContext,
expected_domain: Url, expected_domain: Url,
request_counter: &mut i32, request_counter: &mut i32,
_mod_action_allowed: bool,
) -> Result<CommentForm, LemmyError> { ) -> Result<CommentForm, LemmyError> {
let creator_actor_id = &note let creator_actor_id = &note
.attributed_to() .attributed_to()

View file

@ -1,6 +1,7 @@
use crate::{ use crate::{
extensions::{context::lemmy_context, group_extensions::GroupExtension}, extensions::{context::lemmy_context, group_extensions::GroupExtension},
fetcher::person::get_or_fetch_and_upsert_person, fetcher::{community::fetch_community_mods, person::get_or_fetch_and_upsert_person},
generate_moderators_url,
objects::{ objects::{
check_object_domain, check_object_domain,
create_tombstone, create_tombstone,
@ -23,10 +24,11 @@ use activitystreams::{
use activitystreams_ext::Ext2; use activitystreams_ext::Ext2;
use anyhow::Context; use anyhow::Context;
use lemmy_api_structs::blocking; use lemmy_api_structs::blocking;
use lemmy_db_queries::DbPool; use lemmy_db_queries::{DbPool, Joinable};
use lemmy_db_schema::{ use lemmy_db_schema::{
naive_now, naive_now,
source::community::{Community, CommunityForm}, source::community::{Community, CommunityForm, CommunityModerator, CommunityModeratorForm},
DbUrl,
}; };
use lemmy_db_views_actor::community_moderator_view::CommunityModeratorView; use lemmy_db_views_actor::community_moderator_view::CommunityModeratorView;
use lemmy_utils::{ use lemmy_utils::{
@ -41,28 +43,13 @@ use url::Url;
impl ToApub for Community { impl ToApub for Community {
type ApubType = GroupExt; type ApubType = GroupExt;
async fn to_apub(&self, pool: &DbPool) -> Result<GroupExt, LemmyError> { async fn to_apub(&self, _pool: &DbPool) -> Result<GroupExt, LemmyError> {
// The attributed to, is an ordered vector with the creator actor_ids first,
// then the rest of the moderators
// TODO Technically the instance admins can mod the community, but lets
// ignore that for now
let id = self.id;
let moderators = blocking(pool, move |conn| {
CommunityModeratorView::for_community(&conn, id)
})
.await??;
let moderators: Vec<Url> = moderators
.into_iter()
.map(|m| m.moderator.actor_id.into_inner())
.collect();
let mut group = ApObject::new(Group::new()); let mut group = ApObject::new(Group::new());
group group
.set_many_contexts(lemmy_context()?) .set_many_contexts(lemmy_context()?)
.set_id(self.actor_id.to_owned().into()) .set_id(self.actor_id.to_owned().into())
.set_name(self.title.to_owned()) .set_name(self.title.to_owned())
.set_published(convert_datetime(self.published)) .set_published(convert_datetime(self.published));
.set_many_attributed_tos(moderators);
if let Some(u) = self.updated.to_owned() { if let Some(u) = self.updated.to_owned() {
group.set_updated(convert_datetime(u)); group.set_updated(convert_datetime(u));
@ -95,7 +82,7 @@ impl ToApub for Community {
Ok(Ext2::new( Ok(Ext2::new(
ap_actor, ap_actor,
GroupExtension::new(self.nsfw)?, GroupExtension::new(self.nsfw, generate_moderators_url(&self.actor_id)?.into())?,
self.get_public_key_ext()?, self.get_public_key_ext()?,
)) ))
} }
@ -114,14 +101,64 @@ impl ToApub for Community {
impl FromApub for Community { impl FromApub for Community {
type ApubType = GroupExt; type ApubType = GroupExt;
/// Converts a `Group` to `Community`. /// Converts a `Group` to `Community`, inserts it into the database and updates moderators.
async fn from_apub( async fn from_apub(
group: &GroupExt, group: &GroupExt,
context: &LemmyContext, context: &LemmyContext,
expected_domain: Url, expected_domain: Url,
request_counter: &mut i32, request_counter: &mut i32,
mod_action_allowed: bool,
) -> Result<Community, LemmyError> { ) -> Result<Community, LemmyError> {
get_object_from_apub(group, context, expected_domain, request_counter).await let community: Community = get_object_from_apub(
group,
context,
expected_domain,
request_counter,
mod_action_allowed,
)
.await?;
let new_moderators = fetch_community_mods(context, group, request_counter).await?;
let community_id = community.id;
let current_moderators = blocking(context.pool(), move |conn| {
CommunityModeratorView::for_community(&conn, community_id)
})
.await??;
// Remove old mods from database which arent in the moderators collection anymore
for mod_user in &current_moderators {
if !new_moderators.contains(&&mod_user.moderator.actor_id.clone().into()) {
let community_moderator_form = CommunityModeratorForm {
community_id: mod_user.community.id,
person_id: mod_user.moderator.id,
};
blocking(context.pool(), move |conn| {
CommunityModerator::leave(conn, &community_moderator_form)
})
.await??;
}
}
// Add new mods to database which have been added to moderators collection
for mod_uri in new_moderators {
let mod_user = get_or_fetch_and_upsert_person(&mod_uri, context, request_counter).await?;
let current_mod_uris: Vec<DbUrl> = current_moderators
.clone()
.iter()
.map(|c| c.moderator.actor_id.clone())
.collect();
if !current_mod_uris.contains(&mod_user.actor_id) {
let community_moderator_form = CommunityModeratorForm {
community_id: community.id,
person_id: mod_user.id,
};
blocking(context.pool(), move |conn| {
CommunityModerator::join(conn, &community_moderator_form)
})
.await??;
}
}
Ok(community)
} }
} }
@ -132,16 +169,10 @@ impl FromApubToForm<GroupExt> for CommunityForm {
context: &LemmyContext, context: &LemmyContext,
expected_domain: Url, expected_domain: Url,
request_counter: &mut i32, request_counter: &mut i32,
_mod_action_allowed: bool,
) -> Result<Self, LemmyError> { ) -> Result<Self, LemmyError> {
let creator_and_moderator_uris = group.inner.attributed_to().context(location_info!())?; let moderator_uris = fetch_community_mods(context, group, request_counter).await?;
let creator_uri = creator_and_moderator_uris let creator_uri = moderator_uris.first().context(location_info!())?;
.as_many()
.context(location_info!())?
.iter()
.next()
.context(location_info!())?
.as_xsd_any_uri()
.context(location_info!())?;
let creator = get_or_fetch_and_upsert_person(creator_uri, context, request_counter).await?; let creator = get_or_fetch_and_upsert_person(creator_uri, context, request_counter).await?;
let name = group let name = group

View file

@ -45,12 +45,14 @@ pub(crate) trait FromApub {
/// ///
/// * `apub` The object to read from /// * `apub` The object to read from
/// * `context` LemmyContext which holds DB pool, HTTP client etc /// * `context` LemmyContext which holds DB pool, HTTP client etc
/// * `expected_domain` Domain where the object was received from /// * `expected_domain` Domain where the object was received from. None in case of mod action.
/// * `mod_action_allowed` True if the object can be a mod activity, ignore `expected_domain` in this case
async fn from_apub( async fn from_apub(
apub: &Self::ApubType, apub: &Self::ApubType,
context: &LemmyContext, context: &LemmyContext,
expected_domain: Url, expected_domain: Url,
request_counter: &mut i32, request_counter: &mut i32,
mod_action_allowed: bool,
) -> Result<Self, LemmyError> ) -> Result<Self, LemmyError>
where where
Self: Sized; Self: Sized;
@ -63,6 +65,7 @@ pub(in crate::objects) trait FromApubToForm<ApubType> {
context: &LemmyContext, context: &LemmyContext,
expected_domain: Url, expected_domain: Url,
request_counter: &mut i32, request_counter: &mut i32,
mod_action_allowed: bool,
) -> Result<Self, LemmyError> ) -> Result<Self, LemmyError>
where where
Self: Sized; Self: Sized;
@ -132,19 +135,17 @@ where
{ {
let content = object let content = object
.content() .content()
.map(|s| s.as_single_xsd_string()) .map(|s| s.as_single_xsd_string().map(|s2| s2.to_string()))
.flatten() .flatten();
.map(|s| s.to_string());
if content.is_some() { if content.is_some() {
let source = object.source().context(location_info!())?; let source = object.source().context(location_info!())?;
let source = Object::<()>::from_any_base(source.to_owned())?.context(location_info!())?; let source = Object::<()>::from_any_base(source.to_owned())?.context(location_info!())?;
check_is_markdown(source.media_type())?; check_is_markdown(source.media_type())?;
let source_content = source let source_content = source
.content() .content()
.map(|s| s.as_single_xsd_string()) .map(|s| s.as_single_xsd_string().map(|s2| s2.to_string()))
.flatten() .flatten()
.context(location_info!())? .context(location_info!())?;
.to_string();
return Ok(Some(source_content)); return Ok(Some(source_content));
} }
Ok(None) Ok(None)
@ -177,6 +178,7 @@ pub(in crate::objects) async fn get_object_from_apub<From, Kind, To, ToForm, IdT
context: &LemmyContext, context: &LemmyContext,
expected_domain: Url, expected_domain: Url,
request_counter: &mut i32, request_counter: &mut i32,
is_mod_action: bool,
) -> Result<To, LemmyError> ) -> Result<To, LemmyError>
where where
From: BaseExt<Kind>, From: BaseExt<Kind>,
@ -196,7 +198,14 @@ where
} }
// otherwise parse and insert, assuring that it comes from the right domain // otherwise parse and insert, assuring that it comes from the right domain
else { else {
let to_form = ToForm::from_apub(&from, context, expected_domain, request_counter).await?; let to_form = ToForm::from_apub(
&from,
context,
expected_domain,
request_counter,
is_mod_action,
)
.await?;
let to = blocking(context.pool(), move |conn| To::upsert(conn, &to_form)).await??; let to = blocking(context.pool(), move |conn| To::upsert(conn, &to_form)).await??;
Ok(to) Ok(to)

View file

@ -93,6 +93,7 @@ impl FromApub for DbPerson {
context: &LemmyContext, context: &LemmyContext,
expected_domain: Url, expected_domain: Url,
request_counter: &mut i32, request_counter: &mut i32,
mod_action_allowed: bool,
) -> Result<DbPerson, LemmyError> { ) -> Result<DbPerson, LemmyError> {
let person_id = person.id_unchecked().context(location_info!())?.to_owned(); let person_id = person.id_unchecked().context(location_info!())?.to_owned();
let domain = person_id.domain().context(location_info!())?; let domain = person_id.domain().context(location_info!())?;
@ -103,8 +104,14 @@ impl FromApub for DbPerson {
.await??; .await??;
Ok(person) Ok(person)
} else { } else {
let person_form = let person_form = PersonForm::from_apub(
PersonForm::from_apub(person, context, expected_domain, request_counter).await?; person,
context,
expected_domain,
request_counter,
mod_action_allowed,
)
.await?;
let person = blocking(context.pool(), move |conn| { let person = blocking(context.pool(), move |conn| {
DbPerson::upsert(conn, &person_form) DbPerson::upsert(conn, &person_form)
}) })
@ -121,6 +128,7 @@ impl FromApubToForm<PersonExt> for PersonForm {
_context: &LemmyContext, _context: &LemmyContext,
expected_domain: Url, expected_domain: Url,
_request_counter: &mut i32, _request_counter: &mut i32,
_mod_action_allowed: bool,
) -> Result<Self, LemmyError> { ) -> Result<Self, LemmyError> {
let avatar = match person.icon() { let avatar = match person.icon() {
Some(any_image) => Some( Some(any_image) => Some(

View file

@ -1,4 +1,5 @@
use crate::{ use crate::{
check_is_apub_id_valid,
extensions::{context::lemmy_context, page_extension::PageExtension}, extensions::{context::lemmy_context, page_extension::PageExtension},
fetcher::person::get_or_fetch_and_upsert_person, fetcher::person::get_or_fetch_and_upsert_person,
objects::{ objects::{
@ -117,8 +118,16 @@ impl FromApub for Post {
context: &LemmyContext, context: &LemmyContext,
expected_domain: Url, expected_domain: Url,
request_counter: &mut i32, request_counter: &mut i32,
mod_action_allowed: bool,
) -> Result<Post, LemmyError> { ) -> Result<Post, LemmyError> {
let post: Post = get_object_from_apub(page, context, expected_domain, request_counter).await?; 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) check_object_for_community_or_site_ban(page, post.community_id, context, request_counter)
.await?; .await?;
Ok(post) Ok(post)
@ -132,7 +141,15 @@ impl FromApubToForm<PageExt> for PostForm {
context: &LemmyContext, context: &LemmyContext,
expected_domain: Url, expected_domain: Url,
request_counter: &mut i32, request_counter: &mut i32,
mod_action_allowed: bool,
) -> Result<PostForm, LemmyError> { ) -> Result<PostForm, LemmyError> {
let ap_id = if mod_action_allowed {
let id = page.id_unchecked().context(location_info!())?;
check_is_apub_id_valid(id)?;
id.to_owned().into()
} else {
check_object_domain(page, expected_domain)?
};
let ext = &page.ext_one; let ext = &page.ext_one;
let creator_actor_id = page let creator_actor_id = page
.inner .inner
@ -179,16 +196,20 @@ impl FromApubToForm<PageExt> for PostForm {
let name = page let name = page
.inner .inner
.name() .name()
.map(|s| s.map(|s2| s2.to_owned()))
// The following is for compatibility with lemmy v0.9.9 and older // The following is for compatibility with lemmy v0.9.9 and older
// TODO: remove it after some time (along with the map above) // TODO: remove it after some time (along with the map above)
.or_else(|| page.inner.summary().map(|s| s.to_owned())) .or_else(|| page.inner.summary())
.context(location_info!())? .context(location_info!())?
.as_single_xsd_string() .as_single_xsd_string()
.context(location_info!())? .context(location_info!())?
.to_string(); .to_string();
let body = get_source_markdown_value(page)?; 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)?; check_slurs(&name)?;
let body_slurs_removed = body.map(|b| remove_slurs(&b)); let body_slurs_removed = body.map(|b| remove_slurs(&b));
Ok(PostForm { Ok(PostForm {
@ -216,7 +237,7 @@ impl FromApubToForm<PageExt> for PostForm {
embed_description: iframely_description, embed_description: iframely_description,
embed_html: iframely_html, embed_html: iframely_html,
thumbnail_url: pictrs_thumbnail.map(|u| u.into()), thumbnail_url: pictrs_thumbnail.map(|u| u.into()),
ap_id: Some(check_object_domain(page, expected_domain)?), ap_id: Some(ap_id),
local: false, local: false,
}) })
} }

View file

@ -77,8 +77,16 @@ impl FromApub for PrivateMessage {
context: &LemmyContext, context: &LemmyContext,
expected_domain: Url, expected_domain: Url,
request_counter: &mut i32, request_counter: &mut i32,
mod_action_allowed: bool,
) -> Result<PrivateMessage, LemmyError> { ) -> Result<PrivateMessage, LemmyError> {
get_object_from_apub(note, context, expected_domain, request_counter).await get_object_from_apub(
note,
context,
expected_domain,
request_counter,
mod_action_allowed,
)
.await
} }
} }
@ -89,6 +97,7 @@ impl FromApubToForm<NoteExt> for PrivateMessageForm {
context: &LemmyContext, context: &LemmyContext,
expected_domain: Url, expected_domain: Url,
request_counter: &mut i32, request_counter: &mut i32,
_mod_action_allowed: bool,
) -> Result<PrivateMessageForm, LemmyError> { ) -> Result<PrivateMessageForm, LemmyError> {
let creator_actor_id = note let creator_actor_id = note
.attributed_to() .attributed_to()

View file

@ -5,6 +5,7 @@ use crate::{
get_apub_community_followers, get_apub_community_followers,
get_apub_community_http, get_apub_community_http,
get_apub_community_inbox, get_apub_community_inbox,
get_apub_community_moderators,
get_apub_community_outbox, get_apub_community_outbox,
}, },
get_activity, get_activity,
@ -57,6 +58,10 @@ pub fn config(cfg: &mut web::ServiceConfig) {
"/c/{community_name}/inbox", "/c/{community_name}/inbox",
web::get().to(get_apub_community_inbox), web::get().to(get_apub_community_inbox),
) )
.route(
"/c/{community_name}/moderators",
web::get().to(get_apub_community_moderators),
)
.route("/u/{user_name}", web::get().to(get_apub_person_http)) .route("/u/{user_name}", web::get().to(get_apub_person_http))
.route( .route(
"/u/{user_name}/outbox", "/u/{user_name}/outbox",

View file

@ -2,13 +2,26 @@ use crate::Crud;
use bcrypt::{hash, DEFAULT_COST}; use bcrypt::{hash, DEFAULT_COST};
use diesel::{dsl::*, result::Error, *}; use diesel::{dsl::*, result::Error, *};
use lemmy_db_schema::{ use lemmy_db_schema::{
naive_now,
schema::local_user::dsl::*, schema::local_user::dsl::*,
source::local_user::{LocalUser, LocalUserForm}, source::local_user::{LocalUser, LocalUserForm},
LocalUserId, LocalUserId,
PersonId, PersonId,
}; };
mod safe_type {
use crate::ToSafe;
use lemmy_db_schema::{schema::local_user::columns::*, source::local_user::LocalUser};
type Columns = (id, person_id, admin, matrix_user_id);
impl ToSafe for LocalUser {
type SafeColumns = Columns;
fn safe_columns_tuple() -> Self::SafeColumns {
(id, person_id, admin, matrix_user_id)
}
}
}
mod safe_settings_type { mod safe_settings_type {
use crate::ToSafeSettings; use crate::ToSafeSettings;
use lemmy_db_schema::{schema::local_user::columns::*, source::local_user::LocalUser}; use lemmy_db_schema::{schema::local_user::columns::*, source::local_user::LocalUser};
@ -26,7 +39,6 @@ mod safe_settings_type {
show_avatars, show_avatars,
send_notifications_to_email, send_notifications_to_email,
matrix_user_id, matrix_user_id,
validator_time,
); );
impl ToSafeSettings for LocalUser { impl ToSafeSettings for LocalUser {
@ -47,7 +59,6 @@ mod safe_settings_type {
show_avatars, show_avatars,
send_notifications_to_email, send_notifications_to_email,
matrix_user_id, matrix_user_id,
validator_time,
) )
} }
} }
@ -81,10 +92,7 @@ impl LocalUser_ for LocalUser {
let password_hash = hash(new_password, DEFAULT_COST).expect("Couldn't hash password"); let password_hash = hash(new_password, DEFAULT_COST).expect("Couldn't hash password");
diesel::update(local_user.find(local_user_id)) diesel::update(local_user.find(local_user_id))
.set(( .set((password_encrypted.eq(password_hash),))
password_encrypted.eq(password_hash),
validator_time.eq(naive_now()),
))
.get_result::<Self>(conn) .get_result::<Self>(conn)
} }

View file

@ -155,7 +155,6 @@ table! {
show_avatars -> Bool, show_avatars -> Bool,
send_notifications_to_email -> Bool, send_notifications_to_email -> Bool,
matrix_user_id -> Nullable<Text>, matrix_user_id -> Nullable<Text>,
validator_time -> Timestamp,
} }
} }

View file

@ -17,7 +17,6 @@ pub struct LocalUser {
pub show_avatars: bool, pub show_avatars: bool,
pub send_notifications_to_email: bool, pub send_notifications_to_email: bool,
pub matrix_user_id: Option<String>, pub matrix_user_id: Option<String>,
pub validator_time: chrono::NaiveDateTime,
} }
// TODO redo these, check table defaults // TODO redo these, check table defaults
@ -54,5 +53,4 @@ pub struct LocalUserSettings {
pub show_avatars: bool, pub show_avatars: bool,
pub send_notifications_to_email: bool, pub send_notifications_to_email: bool,
pub matrix_user_id: Option<String>, pub matrix_user_id: Option<String>,
pub validator_time: chrono::NaiveDateTime,
} }

View file

@ -227,7 +227,7 @@ fn get_feed_front(
jwt: String, jwt: String,
) -> Result<ChannelBuilder, LemmyError> { ) -> Result<ChannelBuilder, LemmyError> {
let site_view = SiteView::read(&conn)?; let site_view = SiteView::read(&conn)?;
let local_user_id = LocalUserId(Claims::decode(&jwt)?.claims.sub); let local_user_id = LocalUserId(Claims::decode(&jwt)?.claims.local_user_id);
let person_id = LocalUser::read(&conn, local_user_id)?.person_id; let person_id = LocalUser::read(&conn, local_user_id)?.person_id;
let posts = PostQueryBuilder::create(&conn) let posts = PostQueryBuilder::create(&conn)
@ -254,7 +254,7 @@ fn get_feed_front(
fn get_feed_inbox(conn: &PgConnection, jwt: String) -> Result<ChannelBuilder, LemmyError> { fn get_feed_inbox(conn: &PgConnection, jwt: String) -> Result<ChannelBuilder, LemmyError> {
let site_view = SiteView::read(&conn)?; let site_view = SiteView::read(&conn)?;
let local_user_id = LocalUserId(Claims::decode(&jwt)?.claims.sub); let local_user_id = LocalUserId(Claims::decode(&jwt)?.claims.local_user_id);
let person_id = LocalUser::read(&conn, local_user_id)?.person_id; let person_id = LocalUser::read(&conn, local_user_id)?.person_id;
let sort = SortType::New; let sort = SortType::New;

View file

@ -1,5 +1,4 @@
use crate::settings::structs::Settings; use crate::settings::structs::Settings;
use chrono::Utc;
use jsonwebtoken::{decode, encode, DecodingKey, EncodingKey, Header, TokenData, Validation}; use jsonwebtoken::{decode, encode, DecodingKey, EncodingKey, Header, TokenData, Validation};
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
@ -7,11 +6,8 @@ type Jwt = String;
#[derive(Debug, Serialize, Deserialize)] #[derive(Debug, Serialize, Deserialize)]
pub struct Claims { pub struct Claims {
/// local_user_id, standard claim by RFC 7519. pub local_user_id: i32,
pub sub: i32,
pub iss: String, pub iss: String,
/// Time when this token was issued as UNIX-timestamp in seconds
pub iat: i64,
} }
impl Claims { impl Claims {
@ -27,11 +23,10 @@ impl Claims {
) )
} }
pub fn jwt(local_user_id: i32) -> Result<Jwt, jsonwebtoken::errors::Error> { pub fn jwt(local_user_id: i32, hostname: String) -> Result<Jwt, jsonwebtoken::errors::Error> {
let my_claims = Claims { let my_claims = Claims {
sub: local_user_id, local_user_id,
iss: Settings::get().hostname(), iss: hostname,
iat: Utc::now().timestamp(),
}; };
encode( encode(
&Header::default(), &Header::default(),

View file

@ -13,7 +13,6 @@ use crate::{
}; };
use anyhow::{anyhow, Context}; use anyhow::{anyhow, Context};
use deser_hjson::from_str; use deser_hjson::from_str;
use log::warn;
use merge::Merge; use merge::Merge;
use std::{env, fs, io::Error, net::IpAddr, sync::RwLock}; use std::{env, fs, io::Error, net::IpAddr, sync::RwLock};
@ -25,13 +24,7 @@ static CONFIG_FILE: &str = "config/config.hjson";
lazy_static! { lazy_static! {
static ref SETTINGS: RwLock<Settings> = RwLock::new(match Settings::init() { static ref SETTINGS: RwLock<Settings> = RwLock::new(match Settings::init() {
Ok(c) => c, Ok(c) => c,
Err(e) => { Err(e) => panic!("{}", e),
warn!(
"Couldn't load settings file, using default settings.\n{}",
e
);
Settings::default()
}
}); });
} }

View file

@ -1 +1 @@
pub const VERSION: &str = "0.10.0-rc.7"; pub const VERSION: &str = "0.10.0-rc.5";

View file

@ -17,7 +17,7 @@ services:
- iframely - iframely
lemmy-ui: lemmy-ui:
image: dessalines/lemmy-ui:0.10.0-rc.7 image: dessalines/lemmy-ui:0.10.0-rc.5
ports: ports:
- "1235:1234" - "1235:1234"
restart: always restart: always

View file

@ -29,7 +29,7 @@ services:
- ./volumes/pictrs_alpha:/mnt - ./volumes/pictrs_alpha:/mnt
lemmy-alpha-ui: lemmy-alpha-ui:
image: dessalines/lemmy-ui:0.10.0-rc.7 image: dessalines/lemmy-ui:0.10.0-rc.5
environment: environment:
- LEMMY_INTERNAL_HOST=lemmy-alpha:8541 - LEMMY_INTERNAL_HOST=lemmy-alpha:8541
- LEMMY_EXTERNAL_HOST=localhost:8541 - LEMMY_EXTERNAL_HOST=localhost:8541
@ -58,7 +58,7 @@ services:
- ./volumes/postgres_alpha:/var/lib/postgresql/data - ./volumes/postgres_alpha:/var/lib/postgresql/data
lemmy-beta-ui: lemmy-beta-ui:
image: dessalines/lemmy-ui:0.10.0-rc.7 image: dessalines/lemmy-ui:0.10.0-rc.5
environment: environment:
- LEMMY_INTERNAL_HOST=lemmy-beta:8551 - LEMMY_INTERNAL_HOST=lemmy-beta:8551
- LEMMY_EXTERNAL_HOST=localhost:8551 - LEMMY_EXTERNAL_HOST=localhost:8551
@ -87,7 +87,7 @@ services:
- ./volumes/postgres_beta:/var/lib/postgresql/data - ./volumes/postgres_beta:/var/lib/postgresql/data
lemmy-gamma-ui: lemmy-gamma-ui:
image: dessalines/lemmy-ui:0.10.0-rc.7 image: dessalines/lemmy-ui:0.10.0-rc.5
environment: environment:
- LEMMY_INTERNAL_HOST=lemmy-gamma:8561 - LEMMY_INTERNAL_HOST=lemmy-gamma:8561
- LEMMY_EXTERNAL_HOST=localhost:8561 - LEMMY_EXTERNAL_HOST=localhost:8561
@ -117,7 +117,7 @@ services:
# An instance with only an allowlist for beta # An instance with only an allowlist for beta
lemmy-delta-ui: lemmy-delta-ui:
image: dessalines/lemmy-ui:0.10.0-rc.7 image: dessalines/lemmy-ui:0.10.0-rc.5
environment: environment:
- LEMMY_INTERNAL_HOST=lemmy-delta:8571 - LEMMY_INTERNAL_HOST=lemmy-delta:8571
- LEMMY_EXTERNAL_HOST=localhost:8571 - LEMMY_EXTERNAL_HOST=localhost:8571
@ -147,7 +147,7 @@ services:
# An instance who has a blocklist, with lemmy-alpha blocked # An instance who has a blocklist, with lemmy-alpha blocked
lemmy-epsilon-ui: lemmy-epsilon-ui:
image: dessalines/lemmy-ui:0.10.0-rc.7 image: dessalines/lemmy-ui:0.10.0-rc.5
environment: environment:
- LEMMY_INTERNAL_HOST=lemmy-epsilon:8581 - LEMMY_INTERNAL_HOST=lemmy-epsilon:8581
- LEMMY_EXTERNAL_HOST=localhost:8581 - LEMMY_EXTERNAL_HOST=localhost:8581

View file

@ -1,4 +1,5 @@
{ {
hostname: lemmy-alpha:8541
port: 8541 port: 8541
tls_enabled: false tls_enabled: false
jwt_secret: changeme jwt_secret: changeme

View file

@ -12,7 +12,7 @@ services:
restart: always restart: always
lemmy: lemmy:
image: dessalines/lemmy:0.10.0-rc.7 image: dessalines/lemmy:0.10.0-rc.5
ports: ports:
- "127.0.0.1:8536:8536" - "127.0.0.1:8536:8536"
restart: always restart: always
@ -26,7 +26,7 @@ services:
- iframely - iframely
lemmy-ui: lemmy-ui:
image: dessalines/lemmy-ui:0.10.0-rc.7 image: dessalines/lemmy-ui:0.10.0-rc.5
ports: ports:
- "127.0.0.1:1235:1234" - "127.0.0.1:1235:1234"
restart: always restart: always

View file

@ -1 +0,0 @@
alter table local_user drop column validator_time;

View file

@ -1 +0,0 @@
alter table local_user add column validator_time timestamp not null default now();