]> Untitled Git - lemmy.git/commitdiff
Merge branch 'main' into federated-moderation
authorFelix Ableitner <me@nutomic.com>
Fri, 19 Mar 2021 16:11:34 +0000 (17:11 +0100)
committerFelix Ableitner <me@nutomic.com>
Fri, 19 Mar 2021 16:11:34 +0000 (17:11 +0100)
30 files changed:
1  2 
Cargo.lock
api_tests/src/post.spec.ts
crates/api/src/community.rs
crates/apub/src/activities/receive/comment.rs
crates/apub/src/activities/receive/post.rs
crates/apub/src/activities/receive/private_message.rs
crates/apub/src/activities/send/community.rs
crates/apub/src/activities/send/person.rs
crates/apub/src/activity_queue.rs
crates/apub/src/fetcher/community.rs
crates/apub/src/fetcher/person.rs
crates/apub/src/fetcher/search.rs
crates/apub/src/http/comment.rs
crates/apub/src/http/mod.rs
crates/apub/src/http/person.rs
crates/apub/src/http/post.rs
crates/apub/src/inbox/community_inbox.rs
crates/apub/src/inbox/mod.rs
crates/apub/src/inbox/person_inbox.rs
crates/apub/src/inbox/receive_for_community.rs
crates/apub/src/inbox/shared_inbox.rs
crates/apub/src/lib.rs
crates/apub/src/objects/comment.rs
crates/apub/src/objects/community.rs
crates/apub/src/objects/mod.rs
crates/apub/src/objects/person.rs
crates/apub/src/objects/post.rs
crates/apub/src/objects/private_message.rs
crates/apub/src/routes.rs
docker/federation/docker-compose.yml

diff --cc Cargo.lock
Simple merge
index 0d1788c62be5ca2c848b02f60665fdd9a345d242,a79ee198b4be7abacca68075dd67c4ea0c8ebaf2..db5f3edee6ddea956425eb422f2e25da7a4079e9
@@@ -20,10 -20,9 +20,10 @@@ import 
    getPost,
    unfollowRemotes,
    searchForUser,
-   banUserFromSite,
+   banPersonFromSite,
    searchPostLocal,
-   banUserFromCommunity,
 +  followCommunity,
+   banPersonFromCommunity,
  } from './shared';
  import { PostView, CommunityView } from 'lemmy-js-client';
  
index 0bb64f373f2470e67b2a613c9f756e25d06a508e,f6ecbf1255df3678fb63477eb9d93d348d431ae6..f7e8e23cee2a3f17e2277564fb406698ab6e5486
@@@ -36,7 -34,8 +36,8 @@@ use lemmy_db_queries::
  };
  use lemmy_db_schema::{
    naive_now,
-   source::{comment::Comment, community::*, moderator::*, post::Post, site::*, user::User_},
 -  source::{comment::Comment, community::*, moderator::*, post::Post, site::*},
++  source::{comment::Comment, community::*, moderator::*, person::Person, post::Post, site::*},
+   PersonId,
  };
  use lemmy_db_views::comment_view::CommentQueryBuilder;
  use lemmy_db_views_actor::{
@@@ -699,18 -705,18 +707,18 @@@ impl Perform for AddModToCommunity 
      websocket_id: Option<ConnectionId>,
    ) -> Result<AddModToCommunityResponse, LemmyError> {
      let data: &AddModToCommunity = &self;
-     let user = get_user_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;
  
      // Verify that only mods or admins can add mod
-     is_mod_or_admin(context.pool(), user.id, community_id).await?;
+     is_mod_or_admin(context.pool(), local_user_view.person.id, community_id).await?;
  
-       user_id: data.user_id,
 +    // Update in local database
 +    let community_moderator_form = CommunityModeratorForm {
 +      community_id: data.community_id,
++      person_id: data.person_id,
 +    };
      if data.added {
        let join = move |conn: &'_ _| CommunityModerator::join(conn, &community_moderator_form);
        if blocking(context.pool(), join).await?.is_err() {
      })
      .await??;
  
-     let updated_mod_id = data.user_id;
 +    // Send to federated instances
-       User_::read(conn, updated_mod_id)
++    let updated_mod_id = data.person_id;
 +    let updated_mod = blocking(context.pool(), move |conn| {
-       community.send_add_mod(&user, updated_mod, context).await?;
++      Person::read(conn, updated_mod_id)
 +    })
 +    .await??;
 +    let community = blocking(context.pool(), move |conn| {
 +      Community::read(conn, community_id)
 +    })
 +    .await??;
 +    if data.added {
-         .send_remove_mod(&user, updated_mod, context)
++      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 moderators = blocking(context.pool(), move |conn| {
        CommunityModeratorView::for_community(conn, community_id)
index 07f25eec746543924c70300b580cb429809c3b1f,9ab14dabfdb9c9af511acb54a8d3ddd20e47793b..95b51d64808fccf85eb206a75770424d56b7e865
@@@ -1,6 -1,6 +1,6 @@@
- use crate::{activities::receive::get_actor_as_user, objects::FromApub, ActorType, NoteExt};
+ use crate::{activities::receive::get_actor_as_person, objects::FromApub, ActorType, NoteExt};
  use activitystreams::{
 -  activity::{ActorAndObjectRefExt, Create, Dislike, Like, Remove, Update},
 +  activity::{ActorAndObjectRefExt, Create, Dislike, Like, Update},
    base::ExtendsExt,
  };
  use anyhow::Context;
@@@ -23,7 -23,7 +23,8 @@@ pub(crate) async fn receive_create_comm
    let note = NoteExt::from_any_base(create.object().to_owned().one().context(location_info!())?)?
      .context(location_info!())?;
  
-   let comment = Comment::from_apub(&note, context, user.actor_id(), request_counter, false).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 post_id = comment.post_id;
    let post = blocking(context.pool(), move |conn| Post::read(conn, post_id)).await??;
@@@ -64,9 -71,9 +72,10 @@@ pub(crate) async fn receive_update_comm
  ) -> Result<(), LemmyError> {
    let note = NoteExt::from_any_base(update.object().to_owned().one().context(location_info!())?)?
      .context(location_info!())?;
-   let user = get_actor_as_user(&update, context, request_counter).await?;
+   let person = get_actor_as_person(&update, context, request_counter).await?;
  
-   let comment = Comment::from_apub(&note, context, user.actor_id(), request_counter, false).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 post_id = comment.post_id;
index 631f9933b9f82670fe9a8adb868e6059e0960f44,528b1276aa74edd690d1d38e4e0e53cc4f01ee2a..2473c2c952061abcdb57ead96a2abb6e2929dd14
@@@ -1,12 -1,6 +1,12 @@@
 -use crate::{activities::receive::get_actor_as_person, objects::FromApub, ActorType, PageExt};
 +use crate::{
-   activities::receive::get_actor_as_user,
++  activities::receive::get_actor_as_person,
 +  inbox::receive_for_community::verify_mod_activity,
 +  objects::FromApub,
 +  ActorType,
 +  PageExt,
 +};
  use activitystreams::{
 -  activity::{Create, Dislike, Like, Remove, Update},
 +  activity::{Announce, Create, Dislike, Like, Update},
    prelude::*,
  };
  use anyhow::Context;
@@@ -32,7 -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!())?)?
      .context(location_info!())?;
  
-   let post = Post::from_apub(&page, context, user.actor_id(), request_counter, false).await?;
 -  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
    let post_id = post.id;
@@@ -62,40 -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!())?)?
      .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,
-     user.actor_id(),
++    person.actor_id(),
 +    request_counter,
 +    mod_action_allowed,
 +  )
 +  .await?;
  
    let post_id = post.id;
    // Refetch the view
index 84d2fcedd0ecae4ce1e0d20349a9f332b41e4a2e,af77a47357a0bfa660aeca2e86f7f02d8c18f5c5..80f0a42c8606f431d2ee4b470f2f999933b6dabf
@@@ -1,12 -1,11 +1,13 @@@
  use crate::{
    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,
    extensions::context::lemmy_context,
-   fetcher::user::get_or_fetch_and_upsert_user,
+   fetcher::person::get_or_fetch_and_upsert_person,
 +  generate_moderators_url,
+   insert_activity,
    ActorType,
 +  CommunityType,
  };
  use activitystreams::{
    activity::{
@@@ -29,9 -26,9 +30,9 @@@ use anyhow::Context
  use itertools::Itertools;
  use lemmy_api_structs::blocking;
  use lemmy_db_queries::DbPool;
- use lemmy_db_schema::source::{community::Community, user::User_};
 -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_utils::{location_info, LemmyError};
+ use lemmy_utils::{location_info, settings::structs::Settings, LemmyError};
  use lemmy_websocket::LemmyContext;
  use url::Url;
  
@@@ -57,11 -54,24 +58,11 @@@ impl ActorType for Community 
        .unwrap_or_else(|| self.inbox_url.to_owned())
        .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 user.
+   /// As a local community, accept the follow request from a remote person.
    async fn send_accept_follow(
      &self,
      follow: Follow,
  
      Ok(inboxes)
    }
-     actor: &User_,
-     added_mod: User_,
 +
 +  async fn send_add_mod(
 +    &self,
-     actor: &User_,
-     removed_mod: User_,
++    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(())
 +  }
  }
index a0778c086261b291c3d9bae3ebce5649de4cdb23,40cc142281acecf6851af31df60d0f502aeda851..9560c2fbcef957a96be00340971f146a34681c2f
@@@ -15,10 -14,10 +15,10 @@@ use activitystreams::
    object::ObjectExt,
  };
  use lemmy_api_structs::blocking;
 -use lemmy_db_queries::{ApubObject, DbPool, Followable};
 +use lemmy_db_queries::{ApubObject, Followable};
  use lemmy_db_schema::source::{
    community::{Community, CommunityFollower, CommunityFollowerForm},
-   user::User_,
+   person::Person,
  };
  use lemmy_utils::LemmyError;
  use lemmy_websocket::LemmyContext;
@@@ -48,11 -47,8 +48,11 @@@ impl ActorType for Person 
        .unwrap_or_else(|| self.inbox_url.to_owned())
        .into()
    }
 +}
  
- impl UserType for User_ {
-   /// As a given local user, send out a follow request to a remote community.
 +#[async_trait::async_trait(?Send)]
++impl UserType for Person {
+   /// As a given local person, send out a follow request to a remote community.
    async fn send_follow(
      &self,
      follow_actor_id: &Url,
Simple merge
index 4ae98be6d765529eb33df79b0232be6989ea4b4f,12821bbe85ef3c6659c5becdf98f3bf12040ff44..2fd5ed255e34ce114c097b5fe73fcf8fa6fcc198
@@@ -1,6 -1,11 +1,6 @@@
  use crate::{
 -  fetcher::{
 -    fetch::fetch_remote_object,
 -    get_or_fetch_and_upsert_person,
 -    is_deleted,
 -    should_refetch_actor,
 -  },
 +  fetcher::{fetch::fetch_remote_object, is_deleted, should_refetch_actor},
-   inbox::user_inbox::receive_announce,
+   inbox::person_inbox::receive_announce,
    objects::FromApub,
    GroupExt,
  };
index 7f998ac759d8c01d96b8f76842274fbd626c669c,792e63707384fd45ed4a1c6ed630442ceaa81b75..3788163b84a66528ef01565ac2c69f5ac353b059
@@@ -46,18 -46,12 +46,18 @@@ pub(crate) async fn get_or_fetch_and_up
          return Ok(u);
        }
  
-       let user = User_::from_apub(
 -      let person =
 -        Person::from_apub(&person?, context, apub_id.to_owned(), recursion_counter).await?;
++      let person = Person::from_apub(
 +        &person?,
 +        context,
 +        apub_id.to_owned(),
 +        recursion_counter,
 +        false,
 +      )
 +      .await?;
  
-       let user_id = user.id;
+       let person_id = person.id;
        blocking(context.pool(), move |conn| {
-         User_::mark_as_updated(conn, user_id)
+         Person::mark_as_updated(conn, person_id)
        })
        .await??;
  
        let person =
          fetch_remote_object::<PersonExt>(context.client(), apub_id, recursion_counter).await?;
  
-       let user = User_::from_apub(
 -      let person =
 -        Person::from_apub(&person, context, apub_id.to_owned(), recursion_counter).await?;
++      let person = Person::from_apub(
 +        &person,
 +        context,
 +        apub_id.to_owned(),
 +        recursion_counter,
 +        false,
 +      )
 +      .await?;
  
-       Ok(user)
+       Ok(person)
      }
      Err(e) => Err(e.into()),
    }
Simple merge
Simple merge
Simple merge
index dcb73e3b0d5ef6bdf2a87b90329cbd630c3d4639,89d678cf59b4de8b28e45453ae73a5ba223b6770..d523d6412b299fa201931f68513f68b937fa0227
@@@ -22,9 -22,9 +22,9 @@@ pub struct PersonQuery 
    user_name: String,
  }
  
- /// Return the ActivityPub json representation of a local user over HTTP.
- pub(crate) async fn get_apub_user_http(
-   info: web::Path<UserQuery>,
+ /// 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>,
    context: web::Data<LemmyContext>,
  ) -> Result<HttpResponse<Body>, LemmyError> {
    let user_name = info.into_inner().user_name;
    }
  }
  
- pub(crate) async fn get_apub_user_outbox(
-   info: web::Path<UserQuery>,
 -pub async fn get_apub_person_outbox(
++pub(crate) async fn get_apub_person_outbox(
+   info: web::Path<PersonQuery>,
    context: web::Data<LemmyContext>,
  ) -> Result<HttpResponse<Body>, LemmyError> {
-   let user = blocking(context.pool(), move |conn| {
-     User_::read_from_name(&conn, &info.user_name)
+   let person = blocking(context.pool(), move |conn| {
+     Person::find_by_name(&conn, &info.user_name)
    })
    .await??;
-   // TODO: populate the user outbox
+   // TODO: populate the person outbox
    let mut collection = OrderedCollection::new();
    collection
      .set_many_items(Vec::<Url>::new())
    Ok(create_apub_response(&collection))
  }
  
- pub(crate) async fn get_apub_user_inbox(
-   info: web::Path<UserQuery>,
 -pub async fn get_apub_person_inbox(
++pub(crate) async fn get_apub_person_inbox(
+   info: web::Path<PersonQuery>,
    context: web::Data<LemmyContext>,
  ) -> Result<HttpResponse<Body>, LemmyError> {
-   let user = blocking(context.pool(), move |conn| {
-     User_::read_from_name(&conn, &info.user_name)
+   let person = blocking(context.pool(), move |conn| {
+     Person::find_by_name(&conn, &info.user_name)
    })
    .await??;
  
    let mut collection = OrderedCollection::new();
    collection
-     .set_id(user.inbox_url.into())
 -    .set_id(format!("{}/inbox", person.actor_id.into_inner()).parse()?)
++    .set_id(person.inbox_url.into())
      .set_many_contexts(lemmy_context()?);
    Ok(create_apub_response(&collection))
  }
Simple merge
index ea884183c433a033ae0af1fe6b08670d1b5cbf9f,5a3b9d3d5868404fab249668a3840b0f7cb76259..4fb1732d33376d5c8dfb6844935037e49087afab
@@@ -26,9 -26,9 +26,9 @@@ use std::fmt::Debug
  use url::Url;
  
  pub mod community_inbox;
 -mod receive_for_community;
+ pub mod person_inbox;
 +pub(crate) mod receive_for_community;
  pub mod shared_inbox;
- pub mod user_inbox;
  
  pub(crate) fn get_activity_id<T, Kind>(activity: &T, creator_uri: &Url) -> Result<Url, LemmyError>
  where
index 7ddb8336846b6e509cdf3f46aecfa2a0a0199c3e,14d76f0c890e17a0ac2d708a50d2ce89649a9ed3..1778ea00fcff6f9991f64427e60d150ed8ff2ac7
@@@ -25,9 -25,9 +25,9 @@@ use crate::
      inbox_verify_http_signature,
      is_activity_already_known,
      is_addressed_to_community_followers,
-     is_addressed_to_local_user,
+     is_addressed_to_local_person,
 -    is_addressed_to_public,
      receive_for_community::{
 +      receive_add_for_community,
        receive_create_for_community,
        receive_delete_for_community,
        receive_dislike_for_community,
@@@ -153,20 -152,20 +153,20 @@@ pub(crate) async fn person_receive_mess
        )
        .await?;
      }
-     UserValidTypes::Announce => {
+     PersonValidTypes::Announce => {
        receive_announce(&context, any_base, actor, request_counter).await?
      }
-     UserValidTypes::Create => {
+     PersonValidTypes::Create => {
        receive_create(&context, any_base, actor_url, request_counter).await?
      }
-     UserValidTypes::Update => {
+     PersonValidTypes::Update => {
        receive_update(&context, any_base, actor_url, request_counter).await?
      }
-     UserValidTypes::Delete => {
+     PersonValidTypes::Delete => {
        receive_delete(context, any_base, &actor_url, request_counter).await?
      }
-     UserValidTypes::Undo => receive_undo(context, any_base, &actor_url, request_counter).await?,
-     UserValidTypes::Remove => receive_remove(context, any_base, &actor_url).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
index cd8f32bbca587012d93499b83cc72372ae264c4d,3c5c2303419eb902dbbd11c926dc6baa1dea20cd..b099434a8606dcbc1f3039c732621a62a84f460c
@@@ -31,50 -31,21 +31,50 @@@ use crate::
      receive_unhandled_activity,
      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},
-     user::get_or_fetch_and_upsert_user,
++    person::get_or_fetch_and_upsert_person,
 +  },
 +  find_object_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,
  };
  use activitystreams::{
 -  activity::{Create, Delete, Dislike, Like, Remove, Undo, Update},
 +  activity::{
 +    ActorAndObjectRef,
 +    Add,
 +    Announce,
 +    Create,
 +    Delete,
 +    Dislike,
 +    Like,
 +    OptTargetRef,
 +    Remove,
 +    Undo,
 +    Update,
 +  },
    base::AnyBase,
 +  object::AsObject,
    prelude::*,
  };
 -use anyhow::Context;
 +use anyhow::{anyhow, Context};
  use diesel::result::Error::NotFound;
  use lemmy_api_structs::blocking;
 -use lemmy_db_queries::Crud;
 -use lemmy_db_schema::source::site::Site;
 +use lemmy_db_queries::{source::community::CommunityModerator_, ApubObject, Crud, Joinable};
 +use lemmy_db_schema::{
 +  source::{
 +    community::{Community, CommunityModerator, CommunityModeratorForm},
++    person::Person,
 +    site::Site,
-     user::User_,
 +  },
 +  DbUrl,
 +};
 +use lemmy_db_views_actor::community_view::CommunityView;
  use lemmy_utils::{location_info, LemmyError};
  use lemmy_websocket::LemmyContext;
  use strum_macros::EnumString;
@@@ -220,48 -187,38 +220,48 @@@ pub(in crate::inbox) async fn receive_d
  /// A post or comment being removed by a mod/admin
  pub(in crate::inbox) async fn receive_remove_for_community(
    context: &LemmyContext,
 -  activity: AnyBase,
 -  expected_domain: &Url,
 +  remove_any_base: AnyBase,
 +  announce: Option<Announce>,
 +  request_counter: &mut i32,
  ) -> Result<(), LemmyError> {
 -  let remove = Remove::from_any_base(activity)?.context(location_info!())?;
 -  verify_activity_domains_valid(&remove, &expected_domain, false)?;
 -  is_addressed_to_public(&remove)?;
 -
 -  let cc = remove
 -    .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 object = remove
 -    .object()
 -    .to_owned()
 -    .single_xsd_any_uri()
 -    .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 {
 -    Ok(PostOrComment::Post(p)) => receive_remove_post(context, remove, *p).await,
 -    Ok(PostOrComment::Comment(c)) => receive_remove_comment(context, remove, *c).await,
 -    // if we dont have the object, no need to do anything
 -    Err(_) => Ok(()),
 +  let remove = Remove::from_any_base(remove_any_base.to_owned())?.context(location_info!())?;
 +  let community = extract_community_from_cc(&remove, context).await?;
 +
 +  verify_mod_activity(&remove, announce, &community, context).await?;
 +  verify_is_addressed_to_public(&remove)?;
 +
 +  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_user(&remove_mod, context, request_counter).await?;
++    let remove_mod = get_or_fetch_and_upsert_person(&remove_mod, context, request_counter).await?;
 +    let form = CommunityModeratorForm {
 +      community_id: community.id,
-       user_id: remove_mod.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
 +      .object()
 +      .to_owned()
 +      .single_xsd_any_uri()
 +      .context(location_info!())?;
 +
 +    match find_post_or_comment_by_id(context, object).await {
 +      Ok(PostOrComment::Post(p)) => receive_remove_post(context, *p).await,
 +      Ok(PostOrComment::Comment(c)) => receive_remove_comment(context, *c).await,
 +      // if we dont have the object, no need to do anything
 +      Err(_) => Ok(()),
 +    }
    }
  }
  
@@@ -381,50 -333,6 +381,50 @@@ pub(in crate::inbox) async fn receive_u
    }
  }
  
-   let new_mod = get_or_fetch_and_upsert_user(&new_mod, context, request_counter).await?;
 +/// 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!())?;
-     CommunityModerator::get_user_moderated_communities(conn, new_mod_id)
++  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| {
-       user_id: new_mod.id,
++    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
  pub(in crate::inbox) async fn receive_undo_dislike_for_community(
    context: &LemmyContext,
@@@ -466,172 -374,3 +466,172 @@@ async fn fetch_post_or_comment_by_id
  
    Err(NotFound.into())
  }
-     User_::read_from_apub_id(&conn, &actor.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| {
-     Object::User(u) => u.actor_id(),
++    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(())
 +}
Simple merge
index ec5c6d943d423504319aadd26224c0d014651565,38b039ca921e6f8a00a2b12a8c3fa8a3e83adc41..74d4cbef306bbda7833dd7de78c2c01d064f9b5d
@@@ -17,7 -17,7 +17,7 @@@ use crate::extensions::
  };
  use activitystreams::{
    activity::Follow,
--  actor::{ApActor, Group, Person},
++  actor,
    base::AnyBase,
    object::{ApObject, Note, Page},
  };
@@@ -31,9 -31,9 +31,9 @@@ use lemmy_db_schema::
      activity::Activity,
      comment::Comment,
      community::Community,
 -    person::Person as DbPerson,
++    person::{Person as DbPerson, Person},
      post::Post,
      private_message::PrivateMessage,
-     user::User_,
    },
    DbUrl,
  };
@@@ -44,9 -44,9 +44,9 @@@ use std::net::IpAddr
  use url::{ParseError, Url};
  
  /// Activitystreams type for community
--type GroupExt = Ext2<ApActor<ApObject<Group>>, GroupExtension, PublicKeyExtension>;
- /// Activitystreams type for user
- type PersonExt = Ext1<ApActor<ApObject<Person>>, PublicKeyExtension>;
++type GroupExt = Ext2<actor::ApActor<ApObject<actor::Group>>, GroupExtension, PublicKeyExtension>;
+ /// Activitystreams type for person
 -type PersonExt = Ext1<ApActor<ApObject<Person>>, PublicKeyExtension>;
++type PersonExt = Ext1<actor::ApActor<ApObject<actor::Person>>, PublicKeyExtension>;
  /// Activitystreams type for post
  type PageExt = Ext1<ApObject<Page>, PageExtension>;
  type NoteExt = ApObject<Note>;
@@@ -175,58 -221,9 +189,58 @@@ pub trait ActorType 
    }
  }
  
-     actor: &User_,
-     added_mod: User_,
 +#[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: &User_,
-     removed_mod: User_,
++    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 {
    Community,
-   User,
+   Person,
    Post,
    Comment,
    PrivateMessage,
@@@ -336,13 -329,12 +350,13 @@@ pub(crate) async fn find_post_or_commen
    Err(NotFound.into())
  }
  
 +#[derive(Debug)]
  pub(crate) enum Object {
-   Comment(Comment),
-   Post(Post),
-   Community(Community),
-   User(User_),
-   PrivateMessage(PrivateMessage),
+   Comment(Box<Comment>),
+   Post(Box<Post>),
+   Community(Box<Community>),
+   Person(Box<DbPerson>),
+   PrivateMessage(Box<PrivateMessage>),
  }
  
  pub(crate) async fn find_object_by_id(
Simple merge
index 73cca5c711b9fae7df08d9a4c489bf94dc160e21,936938130fc3ad5341a8db3f26a417ce720d038d..ae941a8050f4c3ba0df24b04237ada0f1b9e9c29
@@@ -1,7 -1,6 +1,7 @@@
  use crate::{
    extensions::{context::lemmy_context, group_extensions::GroupExtension},
-   fetcher::{community::fetch_community_mods, user::get_or_fetch_and_upsert_user},
 -  fetcher::person::get_or_fetch_and_upsert_person,
++  fetcher::{community::fetch_community_mods, person::get_or_fetch_and_upsert_person},
 +  generate_moderators_url,
    objects::{
      check_object_domain,
      create_tombstone,
@@@ -107,58 -120,8 +107,58 @@@ impl FromApub for Community 
      context: &LemmyContext,
      expected_domain: Url,
      request_counter: &mut i32,
 +    mod_action_allowed: bool,
    ) -> 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,
-           user_id: mod_user.moderator.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_user(&mod_uri, context, request_counter).await?;
++      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,
-           user_id: mod_user.id,
++          person_id: mod_user.id,
 +        };
 +        blocking(context.pool(), move |conn| {
 +          CommunityModerator::join(conn, &community_moderator_form)
 +        })
 +        .await??;
 +      }
 +    }
 +
 +    Ok(community)
    }
  }
  
@@@ -169,12 -132,18 +169,12 @@@ impl FromApubToForm<GroupExt> for Commu
      context: &LemmyContext,
      expected_domain: Url,
      request_counter: &mut i32,
 +    _mod_action_allowed: bool,
    ) -> Result<Self, LemmyError> {
 -    let creator_and_moderator_uris = group.inner.attributed_to().context(location_info!())?;
 -    let creator_uri = creator_and_moderator_uris
 -      .as_many()
 -      .context(location_info!())?
 -      .iter()
 -      .next()
 -      .context(location_info!())?
 -      .as_xsd_any_uri()
 -      .context(location_info!())?;
 +    let moderator_uris = fetch_community_mods(context, group, request_counter).await?;
 +    let creator_uri = moderator_uris.first().context(location_info!())?;
  
-     let creator = get_or_fetch_and_upsert_user(creator_uri, context, request_counter).await?;
+     let creator = get_or_fetch_and_upsert_person(creator_uri, context, request_counter).await?;
      let name = group
        .inner
        .preferred_username()
Simple merge
index b6043d31dd8fa90d0cd8dd6167ab93cad41efa04,de45aedd3ffffa7eef9a69fcc9cebbe490a3c774..87227dd148c81a766b657314e21fddbe0a8e9f51
@@@ -93,27 -93,23 +93,30 @@@ impl FromApub for DbPerson 
      context: &LemmyContext,
      expected_domain: Url,
      request_counter: &mut i32,
-   ) -> Result<User_, LemmyError> {
-     let user_id = person.id_unchecked().context(location_info!())?.to_owned();
-     let domain = user_id.domain().context(location_info!())?;
 +    mod_action_allowed: bool,
+   ) -> Result<DbPerson, LemmyError> {
+     let person_id = person.id_unchecked().context(location_info!())?.to_owned();
+     let domain = person_id.domain().context(location_info!())?;
      if domain == Settings::get().hostname() {
-       let user = blocking(context.pool(), move |conn| {
-         User_::read_from_apub_id(conn, &user_id.into())
+       let person = blocking(context.pool(), move |conn| {
+         DbPerson::read_from_apub_id(conn, &person_id.into())
        })
        .await??;
-       Ok(user)
+       Ok(person)
      } else {
-       let user_form = UserForm::from_apub(
 -      let person_form =
 -        PersonForm::from_apub(person, context, expected_domain, request_counter).await?;
++      let person_form = PersonForm::from_apub(
 +        person,
 +        context,
 +        expected_domain,
 +        request_counter,
 +        mod_action_allowed,
 +      )
 +      .await?;
-       let user = blocking(context.pool(), move |conn| User_::upsert(conn, &user_form)).await??;
-       Ok(user)
+       let person = blocking(context.pool(), move |conn| {
+         DbPerson::upsert(conn, &person_form)
+       })
+       .await??;
+       Ok(person)
      }
    }
  }
index 14b43c6cb584bdc6f1ba75332bfaa008cba3c11a,776946cbdceec6afae927ad85c8a6990f17cd037..1e1326385c0a37fe53d948dbd6a9bc2b5cdb18d6
@@@ -1,7 -1,6 +1,7 @@@
  use crate::{
 +  check_is_apub_id_valid,
    extensions::{context::lemmy_context, page_extension::PageExtension},
-   fetcher::user::get_or_fetch_and_upsert_user,
+   fetcher::person::get_or_fetch_and_upsert_person,
    objects::{
      check_object_domain,
      check_object_for_community_or_site_ban,
index 2ec8c26d0918928491e5f357ccfbd3f3793368ba,37fdd66f08d2cb827ecfcd44f1c7c0ed98af411d..fecf333fab3006b21dd9c87234b4d4df7a89d856
@@@ -54,13 -57,12 +58,16 @@@ pub fn config(cfg: &mut web::ServiceCon
              "/c/{community_name}/inbox",
              web::get().to(get_apub_community_inbox),
            )
-           .route("/u/{user_name}", web::get().to(get_apub_user_http))
-           .route("/u/{user_name}/outbox", web::get().to(get_apub_user_outbox))
-           .route("/u/{user_name}/inbox", web::get().to(get_apub_user_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}/outbox",
+             web::get().to(get_apub_person_outbox),
+           )
+           .route("/u/{user_name}/inbox", web::get().to(get_apub_person_inbox))
            .route("/post/{post_id}", web::get().to(get_apub_post))
            .route("/comment/{comment_id}", web::get().to(get_apub_comment))
            .route("/activities/{type_}/{id}", web::get().to(get_activity)),
index 142c4fa444e432845a23ab303e5d402ea4ac5cac,a29e3cf9d9ec0d963dac2b4e5beb2cafdbe76e52..77382899c8dd3eb6e3d018a0d7effa77196d8817
@@@ -29,7 -29,7 +29,11 @@@ services
        - ./volumes/pictrs_alpha:/mnt
  
    lemmy-alpha-ui:
++<<<<<<< HEAD
 +    image: lemmy-ui:test
++=======
+     image: dessalines/lemmy-ui:0.10.0-rc.5
++>>>>>>> main
      environment:
        - LEMMY_INTERNAL_HOST=lemmy-alpha:8541
        - LEMMY_EXTERNAL_HOST=localhost:8541
        - ./volumes/postgres_alpha:/var/lib/postgresql/data
  
    lemmy-beta-ui:
++<<<<<<< HEAD
 +    image: lemmy-ui:test
++=======
+     image: dessalines/lemmy-ui:0.10.0-rc.5
++>>>>>>> main
      environment:
        - LEMMY_INTERNAL_HOST=lemmy-beta:8551
        - LEMMY_EXTERNAL_HOST=localhost:8551
        - ./volumes/postgres_beta:/var/lib/postgresql/data
  
    lemmy-gamma-ui:
++<<<<<<< HEAD
 +    image: lemmy-ui:test
++=======
+     image: dessalines/lemmy-ui:0.10.0-rc.5
++>>>>>>> main
      environment:
        - LEMMY_INTERNAL_HOST=lemmy-gamma:8561
        - LEMMY_EXTERNAL_HOST=localhost:8561