mirror of
https://github.com/LemmyNet/lemmy.git
synced 2025-02-27 16:16:36 +00:00
Implement instance actor
This commit is contained in:
parent
f1aef63149
commit
95ac570a24
36 changed files with 629 additions and 86 deletions
|
@ -540,6 +540,7 @@ impl Perform for TransferCommunity {
|
|||
community_view,
|
||||
moderators,
|
||||
online: 0,
|
||||
site: None,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
|
@ -91,7 +91,7 @@ impl Perform for Login {
|
|||
return Err(LemmyError::from_message("password_incorrect"));
|
||||
}
|
||||
|
||||
let site = blocking(context.pool(), Site::read_simple).await??;
|
||||
let site = blocking(context.pool(), Site::read_local_site).await??;
|
||||
if site.require_email_verification && !local_user_view.local_user.email_verified {
|
||||
return Err(LemmyError::from_message("email_not_verified"));
|
||||
}
|
||||
|
@ -200,7 +200,7 @@ impl Perform for SaveUserSettings {
|
|||
|
||||
// When the site requires email, make sure email is not Some(None). IE, an overwrite to a None value
|
||||
if let Some(email) = &email {
|
||||
let site_fut = blocking(context.pool(), Site::read_simple);
|
||||
let site_fut = blocking(context.pool(), Site::read_local_site);
|
||||
if email.is_none() && site_fut.await??.require_email_verification {
|
||||
return Err(LemmyError::from_message("email_required"));
|
||||
}
|
||||
|
|
|
@ -583,7 +583,7 @@ impl Perform for ListRegistrationApplications {
|
|||
is_admin(&local_user_view)?;
|
||||
|
||||
let unread_only = data.unread_only;
|
||||
let verified_email_only = blocking(context.pool(), Site::read_simple)
|
||||
let verified_email_only = blocking(context.pool(), Site::read_local_site)
|
||||
.await??
|
||||
.require_email_verification;
|
||||
|
||||
|
@ -689,7 +689,7 @@ impl Perform for GetUnreadRegistrationApplicationCount {
|
|||
// Only let admins do this
|
||||
is_admin(&local_user_view)?;
|
||||
|
||||
let verified_email_only = blocking(context.pool(), Site::read_simple)
|
||||
let verified_email_only = blocking(context.pool(), Site::read_local_site)
|
||||
.await??
|
||||
.require_email_verification;
|
||||
|
||||
|
|
|
@ -1,4 +1,7 @@
|
|||
use lemmy_db_schema::newtypes::{CommunityId, PersonId};
|
||||
use lemmy_db_schema::{
|
||||
newtypes::{CommunityId, PersonId},
|
||||
source::site::Site,
|
||||
};
|
||||
use lemmy_db_views_actor::{
|
||||
community_moderator_view::CommunityModeratorView,
|
||||
community_view::CommunityView,
|
||||
|
@ -20,6 +23,12 @@ pub struct GetCommunityResponse {
|
|||
pub community_view: CommunityView,
|
||||
pub moderators: Vec<CommunityModeratorView>,
|
||||
pub online: usize,
|
||||
/// Metadata of the instance where the community is located. Only fields name, sidebar,
|
||||
/// description, icon, banner, actor_id, last_refreshed_at get federated, everything else uses
|
||||
/// default values. May be null if the community is hosted on an older Lemmy version, or on
|
||||
/// another software.
|
||||
/// TODO: this should probably be SiteView
|
||||
pub site: Option<Site>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
|
|
|
@ -267,7 +267,7 @@ pub async fn check_person_block(
|
|||
#[tracing::instrument(skip_all)]
|
||||
pub async fn check_downvotes_enabled(score: i16, pool: &DbPool) -> Result<(), LemmyError> {
|
||||
if score == -1 {
|
||||
let site = blocking(pool, Site::read_simple).await??;
|
||||
let site = blocking(pool, Site::read_local_site).await??;
|
||||
if !site.enable_downvotes {
|
||||
return Err(LemmyError::from_message("downvotes_disabled"));
|
||||
}
|
||||
|
@ -281,7 +281,7 @@ pub async fn check_private_instance(
|
|||
pool: &DbPool,
|
||||
) -> Result<(), LemmyError> {
|
||||
if local_user_view.is_none() {
|
||||
let site = blocking(pool, Site::read_simple).await?;
|
||||
let site = blocking(pool, Site::read_local_site).await?;
|
||||
|
||||
// The site might not be set up yet
|
||||
if let Ok(site) = site {
|
||||
|
@ -511,7 +511,7 @@ pub async fn check_private_instance_and_federation_enabled(
|
|||
pool: &DbPool,
|
||||
settings: &Settings,
|
||||
) -> Result<(), LemmyError> {
|
||||
let site_opt = blocking(pool, Site::read_simple).await?;
|
||||
let site_opt = blocking(pool, Site::read_local_site).await?;
|
||||
|
||||
if let Ok(site) = site_opt {
|
||||
if site.private_instance && settings.federation.enabled {
|
||||
|
|
|
@ -53,7 +53,7 @@ impl PerformCrud for CreateCommunity {
|
|||
let local_user_view =
|
||||
get_local_user_view_from_jwt(&data.auth, context.pool(), context.secret()).await?;
|
||||
|
||||
let site = blocking(context.pool(), move |conn| Site::read(conn, 0)).await??;
|
||||
let site = blocking(context.pool(), Site::read_local_site).await??;
|
||||
if site.community_creation_admin_only && is_admin(&local_user_view).is_err() {
|
||||
return Err(LemmyError::from_message(
|
||||
"only_admins_can_create_communities",
|
||||
|
|
|
@ -7,9 +7,10 @@ use lemmy_api_common::{
|
|||
get_local_user_view_from_jwt_opt,
|
||||
resolve_actor_identifier,
|
||||
};
|
||||
use lemmy_apub::objects::instance::instance_actor_id_from_url;
|
||||
use lemmy_db_schema::{
|
||||
from_opt_str_to_opt_enum,
|
||||
source::community::Community,
|
||||
source::{community::Community, site::Site},
|
||||
traits::DeleteableOrRemoveable,
|
||||
ListingType,
|
||||
SortType,
|
||||
|
@ -78,10 +79,21 @@ impl PerformCrud for GetCommunity {
|
|||
.await
|
||||
.unwrap_or(1);
|
||||
|
||||
let site_id = instance_actor_id_from_url(community_view.community.actor_id.clone().into());
|
||||
let site = blocking(context.pool(), move |conn| {
|
||||
Site::read_from_apub_id(conn, site_id)
|
||||
})
|
||||
.await
|
||||
.map(|s| s.ok())
|
||||
.ok()
|
||||
.flatten()
|
||||
.flatten();
|
||||
|
||||
let res = GetCommunityResponse {
|
||||
community_view,
|
||||
moderators,
|
||||
online,
|
||||
site,
|
||||
};
|
||||
|
||||
// Return the jwt
|
||||
|
|
|
@ -7,19 +7,25 @@ use lemmy_api_common::{
|
|||
site::*,
|
||||
site_description_length_check,
|
||||
};
|
||||
use lemmy_apub::generate_inbox_url;
|
||||
use lemmy_db_schema::{
|
||||
diesel_option_overwrite,
|
||||
diesel_option_overwrite_to_url,
|
||||
naive_now,
|
||||
newtypes::DbUrl,
|
||||
source::site::{Site, SiteForm},
|
||||
traits::Crud,
|
||||
};
|
||||
use lemmy_db_views::site_view::SiteView;
|
||||
use lemmy_utils::{
|
||||
apub::generate_actor_keypair,
|
||||
settings::structs::Settings,
|
||||
utils::{check_slurs, check_slurs_opt},
|
||||
ConnectionId,
|
||||
LemmyError,
|
||||
};
|
||||
use lemmy_websocket::LemmyContext;
|
||||
use url::Url;
|
||||
|
||||
#[async_trait::async_trait(?Send)]
|
||||
impl PerformCrud for CreateSite {
|
||||
|
@ -33,7 +39,7 @@ impl PerformCrud for CreateSite {
|
|||
) -> Result<SiteResponse, LemmyError> {
|
||||
let data: &CreateSite = self;
|
||||
|
||||
let read_site = Site::read_simple;
|
||||
let read_site = Site::read_local_site;
|
||||
if blocking(context.pool(), read_site).await?.is_ok() {
|
||||
return Err(LemmyError::from_message("site_already_exists"));
|
||||
};
|
||||
|
@ -56,6 +62,8 @@ impl PerformCrud for CreateSite {
|
|||
site_description_length_check(desc)?;
|
||||
}
|
||||
|
||||
let actor_id: DbUrl = Url::parse(&Settings::get().get_protocol_and_hostname())?.into();
|
||||
let keypair = generate_actor_keypair()?;
|
||||
let site_form = SiteForm {
|
||||
name: data.name.to_owned(),
|
||||
sidebar,
|
||||
|
@ -66,6 +74,10 @@ impl PerformCrud for CreateSite {
|
|||
open_registration: data.open_registration,
|
||||
enable_nsfw: data.enable_nsfw,
|
||||
community_creation_admin_only: data.community_creation_admin_only,
|
||||
last_refreshed_at: Some(naive_now()),
|
||||
inbox_url: Some(generate_inbox_url(&actor_id)?),
|
||||
private_key: Some(Some(keypair.private_key)),
|
||||
public_key: Some(keypair.public_key),
|
||||
..SiteForm::default()
|
||||
};
|
||||
|
||||
|
|
|
@ -20,6 +20,7 @@ use lemmy_db_schema::{
|
|||
use lemmy_db_views::site_view::SiteView;
|
||||
use lemmy_utils::{utils::check_slurs_opt, ConnectionId, LemmyError};
|
||||
use lemmy_websocket::{messages::SendAllMessage, LemmyContext, UserOperationCrud};
|
||||
use std::default::Default;
|
||||
|
||||
#[async_trait::async_trait(?Send)]
|
||||
impl PerformCrud for EditSite {
|
||||
|
@ -41,7 +42,7 @@ impl PerformCrud for EditSite {
|
|||
// Make sure user is an admin
|
||||
is_admin(&local_user_view)?;
|
||||
|
||||
let found_site = blocking(context.pool(), Site::read_simple).await??;
|
||||
let found_site = blocking(context.pool(), Site::read_local_site).await??;
|
||||
|
||||
let sidebar = diesel_option_overwrite(&data.sidebar);
|
||||
let description = diesel_option_overwrite(&data.description);
|
||||
|
@ -68,6 +69,7 @@ impl PerformCrud for EditSite {
|
|||
require_application: data.require_application,
|
||||
application_question,
|
||||
private_instance: data.private_instance,
|
||||
..SiteForm::default()
|
||||
};
|
||||
|
||||
let update_site = blocking(context.pool(), move |conn| {
|
||||
|
|
|
@ -58,7 +58,7 @@ impl PerformCrud for Register {
|
|||
let (mut email_verification, mut require_application) = (false, false);
|
||||
|
||||
// Make sure site has open registration
|
||||
if let Ok(site) = blocking(context.pool(), Site::read_simple).await? {
|
||||
if let Ok(site) = blocking(context.pool(), Site::read_local_site).await? {
|
||||
if !site.open_registration {
|
||||
return Err(LemmyError::from_message("registration_closed"));
|
||||
}
|
||||
|
|
39
crates/apub/assets/lemmy/objects/instance.json
Normal file
39
crates/apub/assets/lemmy/objects/instance.json
Normal file
|
@ -0,0 +1,39 @@
|
|||
{
|
||||
"@context": [
|
||||
"https://www.w3.org/ns/activitystreams",
|
||||
{
|
||||
"stickied": "as:stickied",
|
||||
"pt": "https://join-lemmy.org#",
|
||||
"sc": "http://schema.org#",
|
||||
"matrixUserId": {
|
||||
"type": "sc:Text",
|
||||
"id": "as:alsoKnownAs"
|
||||
},
|
||||
"sensitive": "as:sensitive",
|
||||
"comments_enabled": {
|
||||
"type": "sc:Boolean",
|
||||
"id": "pt:commentsEnabled"
|
||||
},
|
||||
"moderators": "as:moderators"
|
||||
},
|
||||
"https://w3id.org/security/v1"
|
||||
],
|
||||
"type": "Service",
|
||||
"id": "https://enterprise.lemmy.ml/",
|
||||
"name": "Enterprise",
|
||||
"summary": "A test instance",
|
||||
"content": "<p>Enterprise sidebar</p>\\n",
|
||||
"mediaType": "text/html",
|
||||
"source": {
|
||||
"content": "Enterprise sidebar",
|
||||
"mediaType": "text/markdown"
|
||||
},
|
||||
"inbox": "https://enterprise.lemmy.ml/inbox",
|
||||
"outbox": "https://enterprise.lemmy.ml/outbox",
|
||||
"publicKey": {
|
||||
"id": "https://enterprise.lemmy.ml/#main-key",
|
||||
"owner": "https://enterprise.lemmy.ml/",
|
||||
"publicKeyPem": "-----BEGIN PUBLIC KEY-----\nMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAupcK0xTw5yQb/fnztAmb\n9LfPbhJJP1+1GwUaOXGYiDJD6uYJhl9CLmgztLl3RyV9ltOYoN8/NLNDfOMmgOjd\nrsNWEjDI9IcVPmiZnhU7hsi6KgQvJzzv8O5/xYjAGhDfrGmtdpL+lyG0B5fQod8J\n/V5VWvTQ0B0qFrLSBBuhOrp8/fTtDskdtElDPtnNfH2jn6FgtLOijidWwf9ekFo4\n0I1JeuEw6LuD/CzKVJTPoztzabUV1DQF/DnFJm+8y7SCJa9jEO56Uf9eVfa1jF6f\ndH6ZvNJMiafstVuLMAw7C/eNJy3ufXgtZ4403oOKA0aRSYf1cc9pHSZ9gDE/mevH\nLwIDAQAB\n-----END PUBLIC KEY-----\n"
|
||||
},
|
||||
"published": "2022-01-19T21:52:11.110741+00:00"
|
||||
}
|
|
@ -140,6 +140,7 @@ mod tests {
|
|||
use super::*;
|
||||
use crate::objects::{
|
||||
community::tests::parse_lemmy_community,
|
||||
instance::tests::parse_lemmy_instance,
|
||||
person::tests::parse_lemmy_person,
|
||||
tests::{file_to_json_object, init_context},
|
||||
};
|
||||
|
@ -148,6 +149,7 @@ mod tests {
|
|||
source::{
|
||||
community::Community,
|
||||
person::{Person, PersonForm},
|
||||
site::Site,
|
||||
},
|
||||
traits::Crud,
|
||||
};
|
||||
|
@ -159,6 +161,7 @@ mod tests {
|
|||
let client = reqwest::Client::new().into();
|
||||
let manager = create_activity_queue(client);
|
||||
let context = init_context(manager.queue_handle().clone());
|
||||
let site = parse_lemmy_instance(&context).await;
|
||||
let community = parse_lemmy_community(&context).await;
|
||||
let community_id = community.id;
|
||||
|
||||
|
@ -209,5 +212,6 @@ mod tests {
|
|||
community_context.0.id,
|
||||
)
|
||||
.unwrap();
|
||||
Site::delete(&*community_context.1.pool().get().unwrap(), site.id).unwrap();
|
||||
}
|
||||
}
|
||||
|
|
99
crates/apub/src/fetcher/deletable_apub_object.rs
Normal file
99
crates/apub/src/fetcher/deletable_apub_object.rs
Normal file
|
@ -0,0 +1,99 @@
|
|||
use crate::fetcher::post_or_comment::PostOrComment;
|
||||
use lemmy_api_common::blocking;
|
||||
use lemmy_db_queries::source::{
|
||||
comment::Comment_,
|
||||
community::Community_,
|
||||
person::Person_,
|
||||
post::Post_,
|
||||
};
|
||||
use lemmy_db_schema::source::{
|
||||
comment::Comment,
|
||||
community::Community,
|
||||
person::Person,
|
||||
post::Post,
|
||||
site::Site,
|
||||
};
|
||||
use lemmy_utils::LemmyError;
|
||||
use lemmy_websocket::LemmyContext;
|
||||
|
||||
// TODO: merge this trait with ApubObject (means that db_schema needs to depend on apub_lib)
|
||||
#[async_trait::async_trait(?Send)]
|
||||
pub trait DeletableApubObject {
|
||||
// TODO: pass in tombstone with summary field, to decide between remove/delete
|
||||
async fn delete(self, context: &LemmyContext) -> Result<(), LemmyError>;
|
||||
}
|
||||
|
||||
#[async_trait::async_trait(?Send)]
|
||||
impl DeletableApubObject for Community {
|
||||
async fn delete(self, context: &LemmyContext) -> Result<(), LemmyError> {
|
||||
let id = self.id;
|
||||
blocking(context.pool(), move |conn| {
|
||||
Community::update_deleted(conn, id, true)
|
||||
})
|
||||
.await??;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait::async_trait(?Send)]
|
||||
impl DeletableApubObject for Person {
|
||||
async fn delete(self, context: &LemmyContext) -> Result<(), LemmyError> {
|
||||
let id = self.id;
|
||||
blocking(context.pool(), move |conn| Person::delete_account(conn, id)).await??;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait::async_trait(?Send)]
|
||||
impl DeletableApubObject for Post {
|
||||
async fn delete(self, context: &LemmyContext) -> Result<(), LemmyError> {
|
||||
let id = self.id;
|
||||
blocking(context.pool(), move |conn| {
|
||||
Post::update_deleted(conn, id, true)
|
||||
})
|
||||
.await??;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait::async_trait(?Send)]
|
||||
impl DeletableApubObject for Comment {
|
||||
async fn delete(self, context: &LemmyContext) -> Result<(), LemmyError> {
|
||||
let id = self.id;
|
||||
blocking(context.pool(), move |conn| {
|
||||
Comment::update_deleted(conn, id, true)
|
||||
})
|
||||
.await??;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait::async_trait(?Send)]
|
||||
impl DeletableApubObject for PostOrComment {
|
||||
async fn delete(self, context: &LemmyContext) -> Result<(), LemmyError> {
|
||||
match self {
|
||||
PostOrComment::Comment(c) => {
|
||||
blocking(context.pool(), move |conn| {
|
||||
Comment::update_deleted(conn, c.id, true)
|
||||
})
|
||||
.await??;
|
||||
}
|
||||
PostOrComment::Post(p) => {
|
||||
blocking(context.pool(), move |conn| {
|
||||
Post::update_deleted(conn, p.id, true)
|
||||
})
|
||||
.await??;
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait::async_trait(?Send)]
|
||||
impl DeletableApubObject for Site {
|
||||
async fn delete(self, _context: &LemmyContext) -> Result<(), LemmyError> {
|
||||
// not implemented, ignore
|
||||
Ok(())
|
||||
}
|
||||
}
|
|
@ -36,6 +36,7 @@ mod community;
|
|||
mod person;
|
||||
mod post;
|
||||
pub mod routes;
|
||||
pub mod site;
|
||||
|
||||
#[tracing::instrument(skip_all)]
|
||||
pub async fn shared_inbox(
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
use crate::{
|
||||
activity_lists::PersonInboxActivities,
|
||||
context::WithContext,
|
||||
generate_outbox_url,
|
||||
http::{
|
||||
create_apub_response,
|
||||
create_apub_tombstone_response,
|
||||
|
@ -9,7 +10,7 @@ use crate::{
|
|||
ActivityCommonFields,
|
||||
},
|
||||
objects::person::ApubPerson,
|
||||
protocol::collections::person_outbox::PersonOutbox,
|
||||
protocol::collections::empty_outbox::EmptyOutbox,
|
||||
};
|
||||
use actix_web::{web, web::Payload, HttpRequest, HttpResponse};
|
||||
use lemmy_api_common::blocking;
|
||||
|
@ -80,6 +81,7 @@ pub(crate) async fn get_apub_person_outbox(
|
|||
Person::read_from_name(conn, &info.user_name)
|
||||
})
|
||||
.await??;
|
||||
let outbox = PersonOutbox::new(person).await?;
|
||||
let outbox_id = generate_outbox_url(&person.actor_id)?.into();
|
||||
let outbox = EmptyOutbox::new(outbox_id).await?;
|
||||
Ok(create_apub_response(&outbox))
|
||||
}
|
||||
|
|
|
@ -11,6 +11,7 @@ use crate::http::{
|
|||
person::{get_apub_person_http, get_apub_person_outbox, person_inbox},
|
||||
post::get_apub_post,
|
||||
shared_inbox,
|
||||
site::get_apub_site_http,
|
||||
};
|
||||
use actix_web::{
|
||||
guard::{Guard, GuardContext},
|
||||
|
@ -26,6 +27,8 @@ pub fn config(cfg: &mut web::ServiceConfig, settings: &Settings) {
|
|||
println!("federation enabled, host is {}", settings.hostname);
|
||||
|
||||
cfg
|
||||
.route("/", web::get().to(get_apub_site_http))
|
||||
.route("/site_outbox", web::get().to(get_apub_site_http))
|
||||
.route(
|
||||
"/c/{community_name}",
|
||||
web::get().to(get_apub_community_http),
|
||||
|
|
33
crates/apub/src/http/site.rs
Normal file
33
crates/apub/src/http/site.rs
Normal file
|
@ -0,0 +1,33 @@
|
|||
use crate::{
|
||||
http::create_apub_response,
|
||||
objects::instance::ApubSite,
|
||||
protocol::collections::empty_outbox::EmptyOutbox,
|
||||
};
|
||||
use actix_web::{web, HttpResponse};
|
||||
use lemmy_api_common::blocking;
|
||||
use lemmy_apub_lib::traits::ApubObject;
|
||||
use lemmy_db_schema::source::site::Site;
|
||||
use lemmy_utils::{settings::structs::Settings, LemmyError};
|
||||
use lemmy_websocket::LemmyContext;
|
||||
use url::Url;
|
||||
|
||||
pub(crate) async fn get_apub_site_http(
|
||||
context: web::Data<LemmyContext>,
|
||||
) -> Result<HttpResponse, LemmyError> {
|
||||
let site: ApubSite = blocking(context.pool(), Site::read_local_site)
|
||||
.await??
|
||||
.into();
|
||||
|
||||
let apub = site.into_apub(&context).await?;
|
||||
Ok(create_apub_response(&apub))
|
||||
}
|
||||
|
||||
#[tracing::instrument(skip_all)]
|
||||
pub(crate) async fn get_apub_site_outbox() -> Result<HttpResponse, LemmyError> {
|
||||
let outbox_id = format!(
|
||||
"{}/site_outbox",
|
||||
Settings::get().get_protocol_and_hostname()
|
||||
);
|
||||
let outbox = EmptyOutbox::new(Url::parse(&outbox_id)?).await?;
|
||||
Ok(create_apub_response(&outbox))
|
||||
}
|
|
@ -18,7 +18,7 @@ use lemmy_api_common::blocking;
|
|||
use lemmy_apub_lib::{
|
||||
object_id::ObjectId,
|
||||
traits::ApubObject,
|
||||
values::{MediaTypeHtml, MediaTypeMarkdown},
|
||||
values::MediaTypeHtml,
|
||||
verify::verify_domains_match,
|
||||
};
|
||||
use lemmy_db_schema::{
|
||||
|
@ -120,10 +120,7 @@ impl ApubObject for ApubComment {
|
|||
cc: maa.ccs,
|
||||
content: markdown_to_html(&self.content),
|
||||
media_type: Some(MediaTypeHtml::Html),
|
||||
source: SourceCompat::Lemmy(Source {
|
||||
content: self.content.clone(),
|
||||
media_type: MediaTypeMarkdown::Markdown,
|
||||
}),
|
||||
source: SourceCompat::Lemmy(Source::new(self.content.clone())),
|
||||
in_reply_to,
|
||||
published: Some(convert_datetime(self.published)),
|
||||
updated: self.updated.map(convert_datetime),
|
||||
|
@ -213,19 +210,22 @@ pub(crate) mod tests {
|
|||
use super::*;
|
||||
use crate::objects::{
|
||||
community::{tests::parse_lemmy_community, ApubCommunity},
|
||||
instance::{tests::parse_lemmy_instance, ApubSite},
|
||||
person::{tests::parse_lemmy_person, ApubPerson},
|
||||
post::ApubPost,
|
||||
tests::{file_to_json_object, init_context},
|
||||
};
|
||||
use assert_json_diff::assert_json_include;
|
||||
use lemmy_apub_lib::activity_queue::create_activity_queue;
|
||||
use lemmy_db_schema::source::site::Site;
|
||||
use serial_test::serial;
|
||||
|
||||
async fn prepare_comment_test(
|
||||
url: &Url,
|
||||
context: &LemmyContext,
|
||||
) -> (ApubPerson, ApubCommunity, ApubPost) {
|
||||
) -> (ApubPerson, ApubCommunity, ApubPost, ApubSite) {
|
||||
let person = parse_lemmy_person(context).await;
|
||||
let site = parse_lemmy_instance(context).await;
|
||||
let community = parse_lemmy_community(context).await;
|
||||
let post_json = file_to_json_object("assets/lemmy/objects/page.json").unwrap();
|
||||
ApubPost::verify(&post_json, url, context, &mut 0)
|
||||
|
@ -234,13 +234,14 @@ pub(crate) mod tests {
|
|||
let post = ApubPost::from_apub(post_json, context, &mut 0)
|
||||
.await
|
||||
.unwrap();
|
||||
(person, community, post)
|
||||
(person, community, post, site)
|
||||
}
|
||||
|
||||
fn cleanup(data: (ApubPerson, ApubCommunity, ApubPost), context: &LemmyContext) {
|
||||
fn cleanup(data: (ApubPerson, ApubCommunity, ApubPost, ApubSite), context: &LemmyContext) {
|
||||
Post::delete(&*context.pool().get().unwrap(), data.2.id).unwrap();
|
||||
Community::delete(&*context.pool().get().unwrap(), data.1.id).unwrap();
|
||||
Person::delete(&*context.pool().get().unwrap(), data.0.id).unwrap();
|
||||
Site::delete(&*context.pool().get().unwrap(), data.3.id).unwrap();
|
||||
}
|
||||
|
||||
#[actix_rt::test]
|
||||
|
|
|
@ -3,6 +3,7 @@ use crate::{
|
|||
collections::{community_moderators::ApubCommunityModerators, CommunityContext},
|
||||
generate_moderators_url,
|
||||
generate_outbox_url,
|
||||
objects::instance::{instance_actor_id_from_url, ApubSite},
|
||||
protocol::{
|
||||
objects::{group::Group, tombstone::Tombstone, Endpoints},
|
||||
ImageObject,
|
||||
|
@ -16,7 +17,6 @@ use lemmy_api_common::blocking;
|
|||
use lemmy_apub_lib::{
|
||||
object_id::ObjectId,
|
||||
traits::{ActorType, ApubObject},
|
||||
values::MediaTypeMarkdown,
|
||||
};
|
||||
use lemmy_db_schema::{source::community::Community, traits::ApubActor};
|
||||
use lemmy_db_views_actor::community_follower_view::CommunityFollowerView;
|
||||
|
@ -26,7 +26,7 @@ use lemmy_utils::{
|
|||
};
|
||||
use lemmy_websocket::LemmyContext;
|
||||
use std::ops::Deref;
|
||||
use tracing::debug;
|
||||
use tracing::{debug, info};
|
||||
use url::Url;
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
|
@ -80,22 +80,15 @@ impl ApubObject for ApubCommunity {
|
|||
|
||||
#[tracing::instrument(skip_all)]
|
||||
async fn into_apub(self, _context: &LemmyContext) -> Result<Group, LemmyError> {
|
||||
let source = self.description.clone().map(|bio| Source {
|
||||
content: bio,
|
||||
media_type: MediaTypeMarkdown::Markdown,
|
||||
});
|
||||
let icon = self.icon.clone().map(ImageObject::new);
|
||||
let image = self.banner.clone().map(ImageObject::new);
|
||||
|
||||
let group = Group {
|
||||
kind: GroupType::Group,
|
||||
id: ObjectId::new(self.actor_id()),
|
||||
preferred_username: self.name.clone(),
|
||||
name: self.title.clone(),
|
||||
summary: self.description.as_ref().map(|b| markdown_to_html(b)),
|
||||
source,
|
||||
icon,
|
||||
image,
|
||||
source: self.description.clone().map(Source::new),
|
||||
icon: self.icon.clone().map(ImageObject::new),
|
||||
image: self.banner.clone().map(ImageObject::new),
|
||||
sensitive: Some(self.nsfw),
|
||||
moderators: Some(ObjectId::<ApubCommunityModerators>::new(
|
||||
generate_moderators_url(&self.actor_id)?,
|
||||
|
@ -160,6 +153,15 @@ impl ApubObject for ApubCommunity {
|
|||
.ok();
|
||||
}
|
||||
|
||||
// try to fetch the instance actor (to make things like instance rules available)
|
||||
let instance_id = instance_actor_id_from_url(community.actor_id.clone().into());
|
||||
let site = ObjectId::<ApubSite>::new(instance_id.clone())
|
||||
.dereference(context, context.client(), request_counter)
|
||||
.await;
|
||||
if let Err(e) = site {
|
||||
info!("Failed to dereference site for {}: {}", instance_id, e);
|
||||
}
|
||||
|
||||
Ok(community)
|
||||
}
|
||||
}
|
||||
|
@ -219,9 +221,12 @@ impl ApubCommunity {
|
|||
#[cfg(test)]
|
||||
pub(crate) mod tests {
|
||||
use super::*;
|
||||
use crate::objects::tests::{file_to_json_object, init_context};
|
||||
use crate::objects::{
|
||||
instance::tests::parse_lemmy_instance,
|
||||
tests::{file_to_json_object, init_context},
|
||||
};
|
||||
use lemmy_apub_lib::activity_queue::create_activity_queue;
|
||||
use lemmy_db_schema::traits::Crud;
|
||||
use lemmy_db_schema::{source::site::Site, traits::Crud};
|
||||
use serial_test::serial;
|
||||
|
||||
pub(crate) async fn parse_lemmy_community(context: &LemmyContext) -> ApubCommunity {
|
||||
|
@ -239,7 +244,7 @@ pub(crate) mod tests {
|
|||
let community = ApubCommunity::from_apub(json, context, &mut request_counter)
|
||||
.await
|
||||
.unwrap();
|
||||
// this makes two requests to the (intentionally) broken outbox/moderators collections
|
||||
// this makes one requests to the (intentionally broken) outbox collection
|
||||
assert_eq!(request_counter, 1);
|
||||
community
|
||||
}
|
||||
|
@ -250,6 +255,7 @@ pub(crate) mod tests {
|
|||
let client = reqwest::Client::new().into();
|
||||
let manager = create_activity_queue(client);
|
||||
let context = init_context(manager.queue_handle().clone());
|
||||
let site = parse_lemmy_instance(&context).await;
|
||||
let community = parse_lemmy_community(&context).await;
|
||||
|
||||
assert_eq!(community.title, "Ten Forward");
|
||||
|
@ -257,5 +263,6 @@ pub(crate) mod tests {
|
|||
assert_eq!(community.description.as_ref().unwrap().len(), 132);
|
||||
|
||||
Community::delete(&*context.pool().get().unwrap(), community.id).unwrap();
|
||||
Site::delete(&*context.pool().get().unwrap(), site.id).unwrap();
|
||||
}
|
||||
}
|
||||
|
|
205
crates/apub/src/objects/instance.rs
Normal file
205
crates/apub/src/objects/instance.rs
Normal file
|
@ -0,0 +1,205 @@
|
|||
use crate::{
|
||||
check_is_apub_id_valid,
|
||||
generate_outbox_url,
|
||||
objects::get_summary_from_string_or_source,
|
||||
protocol::{objects::instance::Instance, ImageObject, Source, Unparsed},
|
||||
};
|
||||
use activitystreams_kinds::actor::ServiceType;
|
||||
use chrono::NaiveDateTime;
|
||||
use lemmy_api_common::blocking;
|
||||
use lemmy_apub_lib::{
|
||||
object_id::ObjectId,
|
||||
traits::{ActorType, ApubObject},
|
||||
values::MediaTypeHtml,
|
||||
verify::verify_domains_match,
|
||||
};
|
||||
use lemmy_db_schema::{
|
||||
naive_now,
|
||||
source::site::{Site, SiteForm},
|
||||
};
|
||||
use lemmy_utils::{
|
||||
utils::{check_slurs, check_slurs_opt, convert_datetime, markdown_to_html},
|
||||
LemmyError,
|
||||
};
|
||||
use lemmy_websocket::LemmyContext;
|
||||
use std::ops::Deref;
|
||||
use url::Url;
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct ApubSite(Site);
|
||||
|
||||
impl Deref for ApubSite {
|
||||
type Target = Site;
|
||||
fn deref(&self) -> &Self::Target {
|
||||
&self.0
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Site> for ApubSite {
|
||||
fn from(s: Site) -> Self {
|
||||
ApubSite { 0: s }
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait::async_trait(?Send)]
|
||||
impl ApubObject for ApubSite {
|
||||
type DataType = LemmyContext;
|
||||
type ApubType = Instance;
|
||||
type TombstoneType = ();
|
||||
|
||||
fn last_refreshed_at(&self) -> Option<NaiveDateTime> {
|
||||
Some(self.last_refreshed_at)
|
||||
}
|
||||
|
||||
#[tracing::instrument(skip_all)]
|
||||
async fn read_from_apub_id(
|
||||
object_id: Url,
|
||||
data: &Self::DataType,
|
||||
) -> Result<Option<Self>, LemmyError> {
|
||||
Ok(
|
||||
blocking(data.pool(), move |conn| {
|
||||
Site::read_from_apub_id(conn, object_id)
|
||||
})
|
||||
.await??
|
||||
.map(Into::into),
|
||||
)
|
||||
}
|
||||
|
||||
async fn delete(self, _data: &Self::DataType) -> Result<(), LemmyError> {
|
||||
unimplemented!()
|
||||
}
|
||||
|
||||
#[tracing::instrument(skip_all)]
|
||||
async fn into_apub(self, _data: &Self::DataType) -> Result<Self::ApubType, LemmyError> {
|
||||
let instance = Instance {
|
||||
kind: ServiceType::Service,
|
||||
id: ObjectId::new(self.actor_id()),
|
||||
name: self.name.clone(),
|
||||
content: self.sidebar.as_ref().map(|d| markdown_to_html(d)),
|
||||
source: self.sidebar.clone().map(Source::new),
|
||||
summary: self.description.clone(),
|
||||
media_type: self.sidebar.as_ref().map(|_| MediaTypeHtml::Html),
|
||||
icon: self.icon.clone().map(ImageObject::new),
|
||||
image: self.banner.clone().map(ImageObject::new),
|
||||
inbox: self.inbox_url.clone().into(),
|
||||
outbox: generate_outbox_url(&self.actor_id)?.into(),
|
||||
public_key: self.get_public_key()?,
|
||||
published: convert_datetime(self.published),
|
||||
updated: self.updated.map(convert_datetime),
|
||||
unparsed: Unparsed::default(),
|
||||
};
|
||||
Ok(instance)
|
||||
}
|
||||
|
||||
fn to_tombstone(&self) -> Result<Self::TombstoneType, LemmyError> {
|
||||
unimplemented!()
|
||||
}
|
||||
|
||||
#[tracing::instrument(skip_all)]
|
||||
async fn verify(
|
||||
apub: &Self::ApubType,
|
||||
expected_domain: &Url,
|
||||
data: &Self::DataType,
|
||||
_request_counter: &mut i32,
|
||||
) -> Result<(), LemmyError> {
|
||||
check_is_apub_id_valid(apub.id.inner(), true, &data.settings())?;
|
||||
verify_domains_match(expected_domain, apub.id.inner())?;
|
||||
|
||||
let slur_regex = &data.settings().slur_regex();
|
||||
check_slurs(&apub.name, slur_regex)?;
|
||||
check_slurs_opt(&apub.summary, slur_regex)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tracing::instrument(skip_all)]
|
||||
async fn from_apub(
|
||||
apub: Self::ApubType,
|
||||
data: &Self::DataType,
|
||||
_request_counter: &mut i32,
|
||||
) -> Result<Self, LemmyError> {
|
||||
let site_form = SiteForm {
|
||||
name: apub.name.clone(),
|
||||
sidebar: Some(get_summary_from_string_or_source(
|
||||
&apub.content,
|
||||
&apub.source,
|
||||
)),
|
||||
updated: apub.updated.map(|u| u.clone().naive_local()),
|
||||
icon: Some(apub.icon.clone().map(|i| i.url.into())),
|
||||
banner: Some(apub.image.clone().map(|i| i.url.into())),
|
||||
description: Some(apub.summary.clone()),
|
||||
actor_id: Some(apub.id.clone().into()),
|
||||
last_refreshed_at: Some(naive_now()),
|
||||
inbox_url: Some(apub.inbox.clone().into()),
|
||||
public_key: Some(apub.public_key.public_key_pem.clone()),
|
||||
..SiteForm::default()
|
||||
};
|
||||
let site = blocking(data.pool(), move |conn| Site::upsert(conn, &site_form)).await??;
|
||||
Ok(site.into())
|
||||
}
|
||||
}
|
||||
|
||||
impl ActorType for ApubSite {
|
||||
fn actor_id(&self) -> Url {
|
||||
self.actor_id.to_owned().into()
|
||||
}
|
||||
fn public_key(&self) -> String {
|
||||
self.public_key.to_owned()
|
||||
}
|
||||
fn private_key(&self) -> Option<String> {
|
||||
self.private_key.to_owned()
|
||||
}
|
||||
|
||||
fn inbox_url(&self) -> Url {
|
||||
self.inbox_url.clone().into()
|
||||
}
|
||||
|
||||
fn shared_inbox_url(&self) -> Option<Url> {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
/// Instance actor is at the root path, so we simply need to clear the path and other unnecessary
|
||||
/// parts of the url.
|
||||
pub fn instance_actor_id_from_url(mut url: Url) -> Url {
|
||||
url.set_fragment(None);
|
||||
url.set_path("");
|
||||
url.set_query(None);
|
||||
url
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
pub(crate) mod tests {
|
||||
use super::*;
|
||||
use crate::objects::tests::{file_to_json_object, init_context};
|
||||
use lemmy_apub_lib::activity_queue::create_activity_queue;
|
||||
use lemmy_db_schema::traits::Crud;
|
||||
use serial_test::serial;
|
||||
|
||||
pub(crate) async fn parse_lemmy_instance(context: &LemmyContext) -> ApubSite {
|
||||
let json: Instance = file_to_json_object("assets/lemmy/objects/instance.json").unwrap();
|
||||
let id = Url::parse("https://enterprise.lemmy.ml/").unwrap();
|
||||
let mut request_counter = 0;
|
||||
ApubSite::verify(&json, &id, context, &mut request_counter)
|
||||
.await
|
||||
.unwrap();
|
||||
let site = ApubSite::from_apub(json, context, &mut request_counter)
|
||||
.await
|
||||
.unwrap();
|
||||
assert_eq!(request_counter, 0);
|
||||
site
|
||||
}
|
||||
|
||||
#[actix_rt::test]
|
||||
#[serial]
|
||||
async fn test_parse_lemmy_instance() {
|
||||
let client = reqwest::Client::new().into();
|
||||
let manager = create_activity_queue(client);
|
||||
let context = init_context(manager.queue_handle().clone());
|
||||
let site = parse_lemmy_instance(&context).await;
|
||||
|
||||
assert_eq!(site.name, "Enterprise");
|
||||
assert_eq!(site.description.as_ref().unwrap().len(), 15);
|
||||
|
||||
Site::delete(&*context.pool().get().unwrap(), site.id).unwrap();
|
||||
}
|
||||
}
|
|
@ -3,6 +3,7 @@ use html2md::parse_html;
|
|||
|
||||
pub mod comment;
|
||||
pub mod community;
|
||||
pub mod instance;
|
||||
pub mod person;
|
||||
pub mod post;
|
||||
pub mod private_message;
|
||||
|
|
|
@ -16,7 +16,6 @@ use lemmy_api_common::blocking;
|
|||
use lemmy_apub_lib::{
|
||||
object_id::ObjectId,
|
||||
traits::{ActorType, ApubObject},
|
||||
values::MediaTypeMarkdown,
|
||||
verify::verify_domains_match,
|
||||
};
|
||||
use lemmy_db_schema::{
|
||||
|
@ -88,12 +87,6 @@ impl ApubObject for ApubPerson {
|
|||
} else {
|
||||
UserTypes::Person
|
||||
};
|
||||
let source = self.bio.clone().map(|bio| Source {
|
||||
content: bio,
|
||||
media_type: MediaTypeMarkdown::Markdown,
|
||||
});
|
||||
let icon = self.avatar.clone().map(ImageObject::new);
|
||||
let image = self.banner.clone().map(ImageObject::new);
|
||||
|
||||
let person = Person {
|
||||
kind,
|
||||
|
@ -101,9 +94,9 @@ impl ApubObject for ApubPerson {
|
|||
preferred_username: self.name.clone(),
|
||||
name: self.display_name.clone(),
|
||||
summary: self.bio.as_ref().map(|b| markdown_to_html(b)),
|
||||
source,
|
||||
icon,
|
||||
image,
|
||||
source: self.bio.clone().map(Source::new),
|
||||
icon: self.avatar.clone().map(ImageObject::new),
|
||||
image: self.banner.clone().map(ImageObject::new),
|
||||
matrix_user_id: self.matrix_user_id.clone(),
|
||||
published: Some(convert_datetime(self.published)),
|
||||
outbox: generate_outbox_url(&self.actor_id)?.into(),
|
||||
|
|
|
@ -209,11 +209,13 @@ mod tests {
|
|||
use super::*;
|
||||
use crate::objects::{
|
||||
community::tests::parse_lemmy_community,
|
||||
instance::tests::parse_lemmy_instance,
|
||||
person::tests::parse_lemmy_person,
|
||||
post::ApubPost,
|
||||
tests::{file_to_json_object, init_context},
|
||||
};
|
||||
use lemmy_apub_lib::activity_queue::create_activity_queue;
|
||||
use lemmy_db_schema::source::site::Site;
|
||||
use serial_test::serial;
|
||||
|
||||
#[actix_rt::test]
|
||||
|
@ -222,6 +224,7 @@ mod tests {
|
|||
let client = reqwest::Client::new().into();
|
||||
let manager = create_activity_queue(client);
|
||||
let context = init_context(manager.queue_handle().clone());
|
||||
let site = parse_lemmy_instance(&context).await;
|
||||
let community = parse_lemmy_community(&context).await;
|
||||
let person = parse_lemmy_person(&context).await;
|
||||
|
||||
|
@ -246,5 +249,6 @@ mod tests {
|
|||
Post::delete(&*context.pool().get().unwrap(), post.id).unwrap();
|
||||
Person::delete(&*context.pool().get().unwrap(), person.id).unwrap();
|
||||
Community::delete(&*context.pool().get().unwrap(), community.id).unwrap();
|
||||
Site::delete(&*context.pool().get().unwrap(), site.id).unwrap();
|
||||
}
|
||||
}
|
||||
|
|
|
@ -8,7 +8,7 @@ use lemmy_api_common::blocking;
|
|||
use lemmy_apub_lib::{
|
||||
object_id::ObjectId,
|
||||
traits::ApubObject,
|
||||
values::{MediaTypeHtml, MediaTypeMarkdown},
|
||||
values::MediaTypeHtml,
|
||||
verify::verify_domains_match,
|
||||
};
|
||||
use lemmy_db_schema::{
|
||||
|
@ -87,10 +87,7 @@ impl ApubObject for ApubPrivateMessage {
|
|||
to: [ObjectId::new(recipient.actor_id)],
|
||||
content: markdown_to_html(&self.content),
|
||||
media_type: Some(MediaTypeHtml::Html),
|
||||
source: Some(Source {
|
||||
content: self.content.clone(),
|
||||
media_type: MediaTypeMarkdown::Markdown,
|
||||
}),
|
||||
source: Some(Source::new(self.content.clone())),
|
||||
published: Some(convert_datetime(self.published)),
|
||||
updated: self.updated.map(convert_datetime),
|
||||
unparsed: Default::default(),
|
||||
|
|
|
@ -1,24 +1,23 @@
|
|||
use crate::generate_outbox_url;
|
||||
use activitystreams_kinds::collection::OrderedCollectionType;
|
||||
use lemmy_db_schema::source::person::Person;
|
||||
use lemmy_utils::LemmyError;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use url::Url;
|
||||
|
||||
/// Empty placeholder outbox used for Person, Instance, which dont implement a proper outbox yet.
|
||||
#[derive(Clone, Debug, Deserialize, Serialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub(crate) struct PersonOutbox {
|
||||
pub(crate) struct EmptyOutbox {
|
||||
r#type: OrderedCollectionType,
|
||||
id: Url,
|
||||
ordered_items: Vec<()>,
|
||||
total_items: i32,
|
||||
}
|
||||
|
||||
impl PersonOutbox {
|
||||
pub(crate) async fn new(user: Person) -> Result<PersonOutbox, LemmyError> {
|
||||
Ok(PersonOutbox {
|
||||
impl EmptyOutbox {
|
||||
pub(crate) async fn new(outbox_id: Url) -> Result<EmptyOutbox, LemmyError> {
|
||||
Ok(EmptyOutbox {
|
||||
r#type: OrderedCollectionType::OrderedCollection,
|
||||
id: generate_outbox_url(&user.actor_id)?.into(),
|
||||
id: outbox_id,
|
||||
ordered_items: vec![],
|
||||
total_items: 0,
|
||||
})
|
|
@ -1,16 +1,16 @@
|
|||
pub(crate) mod empty_outbox;
|
||||
pub(crate) mod group_followers;
|
||||
pub(crate) mod group_moderators;
|
||||
pub(crate) mod group_outbox;
|
||||
pub(crate) mod person_outbox;
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use crate::protocol::{
|
||||
collections::{
|
||||
empty_outbox::EmptyOutbox,
|
||||
group_followers::GroupFollowers,
|
||||
group_moderators::GroupModerators,
|
||||
group_outbox::GroupOutbox,
|
||||
person_outbox::PersonOutbox,
|
||||
},
|
||||
tests::test_parse_lemmy_item,
|
||||
};
|
||||
|
@ -24,6 +24,6 @@ mod tests {
|
|||
assert_eq!(outbox.ordered_items.len() as i32, outbox.total_items);
|
||||
test_parse_lemmy_item::<GroupModerators>("assets/lemmy/collections/group_moderators.json")
|
||||
.unwrap();
|
||||
test_parse_lemmy_item::<PersonOutbox>("assets/lemmy/collections/person_outbox.json").unwrap();
|
||||
test_parse_lemmy_item::<EmptyOutbox>("assets/lemmy/collections/person_outbox.json").unwrap();
|
||||
}
|
||||
}
|
||||
|
|
|
@ -17,6 +17,15 @@ pub struct Source {
|
|||
pub(crate) media_type: MediaTypeMarkdown,
|
||||
}
|
||||
|
||||
impl Source {
|
||||
pub(crate) fn new(content: String) -> Self {
|
||||
Source {
|
||||
content,
|
||||
media_type: MediaTypeMarkdown::Markdown,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Deserialize, Serialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct ImageObject {
|
||||
|
|
39
crates/apub/src/protocol/objects/instance.rs
Normal file
39
crates/apub/src/protocol/objects/instance.rs
Normal file
|
@ -0,0 +1,39 @@
|
|||
use crate::{
|
||||
objects::instance::ApubSite,
|
||||
protocol::{ImageObject, Source, Unparsed},
|
||||
};
|
||||
use activitystreams_kinds::actor::ServiceType;
|
||||
use chrono::{DateTime, FixedOffset};
|
||||
use lemmy_apub_lib::{object_id::ObjectId, signatures::PublicKey, values::MediaTypeHtml};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use serde_with::skip_serializing_none;
|
||||
use url::Url;
|
||||
|
||||
#[skip_serializing_none]
|
||||
#[derive(Clone, Debug, Deserialize, Serialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct Instance {
|
||||
#[serde(rename = "type")]
|
||||
pub(crate) kind: ServiceType,
|
||||
pub(crate) id: ObjectId<ApubSite>,
|
||||
// site name
|
||||
pub(crate) name: String,
|
||||
// sidebar
|
||||
pub(crate) content: Option<String>,
|
||||
pub(crate) source: Option<Source>,
|
||||
// short instance description
|
||||
pub(crate) summary: Option<String>,
|
||||
pub(crate) media_type: Option<MediaTypeHtml>,
|
||||
/// instance icon
|
||||
pub(crate) icon: Option<ImageObject>,
|
||||
/// instance banner
|
||||
pub(crate) image: Option<ImageObject>,
|
||||
pub(crate) inbox: Url,
|
||||
/// mandatory field in activitypub, currently empty in lemmy
|
||||
pub(crate) outbox: Url,
|
||||
pub(crate) public_key: PublicKey,
|
||||
pub(crate) published: DateTime<FixedOffset>,
|
||||
pub(crate) updated: Option<DateTime<FixedOffset>>,
|
||||
#[serde(flatten)]
|
||||
pub(crate) unparsed: Unparsed,
|
||||
}
|
|
@ -3,6 +3,7 @@ use url::Url;
|
|||
|
||||
pub(crate) mod chat_message;
|
||||
pub(crate) mod group;
|
||||
pub(crate) mod instance;
|
||||
pub(crate) mod note;
|
||||
pub(crate) mod page;
|
||||
pub(crate) mod person;
|
||||
|
@ -23,6 +24,7 @@ mod tests {
|
|||
objects::{
|
||||
chat_message::ChatMessage,
|
||||
group::Group,
|
||||
instance::Instance,
|
||||
note::Note,
|
||||
page::Page,
|
||||
person::Person,
|
||||
|
@ -33,9 +35,10 @@ mod tests {
|
|||
};
|
||||
|
||||
#[actix_rt::test]
|
||||
async fn test_parse_object_lemmy() {
|
||||
test_parse_lemmy_item::<Person>("assets/lemmy/objects/person.json").unwrap();
|
||||
async fn test_parse_objects_lemmy() {
|
||||
test_parse_lemmy_item::<Instance>("assets/lemmy/objects/instance.json").unwrap();
|
||||
test_parse_lemmy_item::<Group>("assets/lemmy/objects/group.json").unwrap();
|
||||
test_parse_lemmy_item::<Person>("assets/lemmy/objects/person.json").unwrap();
|
||||
test_parse_lemmy_item::<Page>("assets/lemmy/objects/page.json").unwrap();
|
||||
test_parse_lemmy_item::<Note>("assets/lemmy/objects/note.json").unwrap();
|
||||
test_parse_lemmy_item::<ChatMessage>("assets/lemmy/objects/chat_message.json").unwrap();
|
||||
|
@ -43,7 +46,7 @@ mod tests {
|
|||
}
|
||||
|
||||
#[actix_rt::test]
|
||||
async fn test_parse_object_pleroma() {
|
||||
async fn test_parse_objects_pleroma() {
|
||||
file_to_json_object::<WithContext<Person>>("assets/pleroma/objects/person.json").unwrap();
|
||||
file_to_json_object::<WithContext<Note>>("assets/pleroma/objects/note.json").unwrap();
|
||||
file_to_json_object::<WithContext<ChatMessage>>("assets/pleroma/objects/chat_message.json")
|
||||
|
@ -51,19 +54,19 @@ mod tests {
|
|||
}
|
||||
|
||||
#[actix_rt::test]
|
||||
async fn test_parse_object_smithereen() {
|
||||
async fn test_parse_objects_smithereen() {
|
||||
file_to_json_object::<WithContext<Person>>("assets/smithereen/objects/person.json").unwrap();
|
||||
file_to_json_object::<Note>("assets/smithereen/objects/note.json").unwrap();
|
||||
}
|
||||
|
||||
#[actix_rt::test]
|
||||
async fn test_parse_object_mastodon() {
|
||||
async fn test_parse_objects_mastodon() {
|
||||
file_to_json_object::<WithContext<Person>>("assets/mastodon/objects/person.json").unwrap();
|
||||
file_to_json_object::<WithContext<Note>>("assets/mastodon/objects/note.json").unwrap();
|
||||
}
|
||||
|
||||
#[actix_rt::test]
|
||||
async fn test_parse_object_lotide() {
|
||||
async fn test_parse_objects_lotide() {
|
||||
file_to_json_object::<WithContext<Group>>("assets/lotide/objects/group.json").unwrap();
|
||||
file_to_json_object::<WithContext<Person>>("assets/lotide/objects/person.json").unwrap();
|
||||
file_to_json_object::<WithContext<Note>>("assets/lotide/objects/note.json").unwrap();
|
||||
|
|
|
@ -55,19 +55,7 @@ mod tests {
|
|||
|
||||
let site_form = SiteForm {
|
||||
name: "test_site".into(),
|
||||
sidebar: None,
|
||||
description: None,
|
||||
icon: None,
|
||||
banner: None,
|
||||
enable_downvotes: None,
|
||||
open_registration: None,
|
||||
enable_nsfw: None,
|
||||
updated: None,
|
||||
community_creation_admin_only: Some(false),
|
||||
require_email_verification: None,
|
||||
require_application: None,
|
||||
application_question: None,
|
||||
private_instance: None,
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
Site::create(&conn, &site_form).unwrap();
|
||||
|
@ -136,7 +124,8 @@ mod tests {
|
|||
let after_delete_creator = SiteAggregates::read(&conn);
|
||||
assert!(after_delete_creator.is_ok());
|
||||
|
||||
Site::delete(&conn, 1).unwrap();
|
||||
let site_id = after_delete_creator.unwrap().id;
|
||||
Site::delete(&conn, site_id).unwrap();
|
||||
let after_delete_site = SiteAggregates::read(&conn);
|
||||
assert!(after_delete_site.is_err());
|
||||
}
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
use crate::{source::site::*, traits::Crud};
|
||||
use crate::{source::site::*, traits::Crud, DbUrl};
|
||||
use diesel::{dsl::*, result::Error, *};
|
||||
use url::Url;
|
||||
|
||||
impl Crud for Site {
|
||||
type Form = SiteForm;
|
||||
|
@ -27,8 +28,30 @@ impl Crud for Site {
|
|||
}
|
||||
|
||||
impl Site {
|
||||
pub fn read_simple(conn: &PgConnection) -> Result<Self, Error> {
|
||||
pub fn read_local_site(conn: &PgConnection) -> Result<Self, Error> {
|
||||
use crate::schema::site::dsl::*;
|
||||
site.first::<Self>(conn)
|
||||
site.order_by(id).first::<Self>(conn)
|
||||
}
|
||||
|
||||
pub fn upsert(conn: &PgConnection, site_form: &SiteForm) -> Result<Site, Error> {
|
||||
use crate::schema::site::dsl::*;
|
||||
insert_into(site)
|
||||
.values(site_form)
|
||||
.on_conflict(actor_id)
|
||||
.do_update()
|
||||
.set(site_form)
|
||||
.get_result::<Self>(conn)
|
||||
}
|
||||
|
||||
pub fn read_from_apub_id(conn: &PgConnection, object_id: Url) -> Result<Option<Self>, Error> {
|
||||
use crate::schema::site::dsl::*;
|
||||
let object_id: DbUrl = object_id.into();
|
||||
Ok(
|
||||
site
|
||||
.filter(actor_id.eq(object_id))
|
||||
.first::<Site>(conn)
|
||||
.ok()
|
||||
.map(Into::into),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -454,6 +454,11 @@ table! {
|
|||
require_application -> Bool,
|
||||
application_question -> Nullable<Text>,
|
||||
private_instance -> Bool,
|
||||
actor_id -> Text,
|
||||
last_refreshed_at -> Timestamp,
|
||||
inbox_url -> Text,
|
||||
private_key -> Nullable<Text>,
|
||||
public_key -> Text,
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -20,6 +20,11 @@ pub struct Site {
|
|||
pub require_application: bool,
|
||||
pub application_question: Option<String>,
|
||||
pub private_instance: bool,
|
||||
pub actor_id: DbUrl,
|
||||
pub last_refreshed_at: chrono::NaiveDateTime,
|
||||
pub inbox_url: DbUrl,
|
||||
pub private_key: Option<String>,
|
||||
pub public_key: String,
|
||||
}
|
||||
|
||||
#[derive(Insertable, AsChangeset, Default)]
|
||||
|
@ -40,4 +45,9 @@ pub struct SiteForm {
|
|||
pub require_application: Option<bool>,
|
||||
pub application_question: Option<Option<String>>,
|
||||
pub private_instance: Option<bool>,
|
||||
pub actor_id: Option<DbUrl>,
|
||||
pub last_refreshed_at: Option<chrono::NaiveDateTime>,
|
||||
pub inbox_url: Option<DbUrl>,
|
||||
pub private_key: Option<Option<String>>,
|
||||
pub public_key: Option<String>,
|
||||
}
|
||||
|
|
6
migrations/2022-01-28-104106_instance-actor/down.sql
Normal file
6
migrations/2022-01-28-104106_instance-actor/down.sql
Normal file
|
@ -0,0 +1,6 @@
|
|||
alter table site
|
||||
drop column actor_id,
|
||||
drop column last_refreshed_at,
|
||||
drop column inbox_url,
|
||||
drop column private_key,
|
||||
drop column public_key;
|
6
migrations/2022-01-28-104106_instance-actor/up.sql
Normal file
6
migrations/2022-01-28-104106_instance-actor/up.sql
Normal file
|
@ -0,0 +1,6 @@
|
|||
alter table site
|
||||
add column actor_id varchar(255) not null unique default generate_unique_changeme(),
|
||||
add column last_refreshed_at Timestamp not null default now(),
|
||||
add column inbox_url varchar(255) not null default generate_unique_changeme(),
|
||||
add column private_key text,
|
||||
add column public_key text not null default generate_unique_changeme();
|
|
@ -18,11 +18,13 @@ use lemmy_db_schema::{
|
|||
person::{Person, PersonForm},
|
||||
post::Post,
|
||||
private_message::PrivateMessage,
|
||||
site::{Site, SiteForm},
|
||||
},
|
||||
traits::Crud,
|
||||
};
|
||||
use lemmy_utils::{apub::generate_actor_keypair, LemmyError};
|
||||
use tracing::info;
|
||||
use url::Url;
|
||||
|
||||
pub fn run_advanced_migrations(
|
||||
conn: &PgConnection,
|
||||
|
@ -35,6 +37,7 @@ pub fn run_advanced_migrations(
|
|||
private_message_updates_2020_05_05(conn, protocol_and_hostname)?;
|
||||
post_thumbnail_url_updates_2020_07_27(conn, protocol_and_hostname)?;
|
||||
apub_columns_2021_02_02(conn)?;
|
||||
instance_actor_2022_01_28(conn, protocol_and_hostname)?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
@ -284,3 +287,29 @@ fn apub_columns_2021_02_02(conn: &PgConnection) -> Result<(), LemmyError> {
|
|||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Site object turns into an actor, so that things like instance description can be federated. This
|
||||
/// means we need to add actor columns to the site table, and initialize them with correct values.
|
||||
/// Before this point, there is only a single value in the site table which refers to the local
|
||||
/// Lemmy instance, so thats all we need to update.
|
||||
fn instance_actor_2022_01_28(
|
||||
conn: &PgConnection,
|
||||
protocol_and_hostname: &str,
|
||||
) -> Result<(), LemmyError> {
|
||||
info!("Running instance_actor_2021_09_29");
|
||||
if let Ok(site) = Site::read_local_site(conn) {
|
||||
let key_pair = generate_actor_keypair()?;
|
||||
let actor_id = Url::parse(protocol_and_hostname)?;
|
||||
let site_form = SiteForm {
|
||||
name: site.name,
|
||||
actor_id: Some(actor_id.clone().into()),
|
||||
last_refreshed_at: Some(naive_now()),
|
||||
inbox_url: Some(generate_inbox_url(&actor_id.into())?),
|
||||
private_key: Some(Some(key_pair.private_key)),
|
||||
public_key: Some(key_pair.public_key),
|
||||
..Default::default()
|
||||
};
|
||||
Site::update(conn, site.id, &site_form)?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue