lemmy/server/src/apub/mod.rs
nutomic 77a2a5eb01 Update activitystreams library to latest version (#71)
Merge branch 'main' into more-upgrade-apub-3

Update activitystreams library to latest version

Remove remaining usages of old activitystreams library

Migrate community inbox and user inbox

Migrate private message

Migrate post

Migrate community activities

Migrate extensions to new activitystreams library

Co-authored-by: dessalines <dessalines@noreply.yerbamate.dev>
Co-authored-by: Felix Ableitner <me@nutomic.com>
Reviewed-on: https://yerbamate.dev/LemmyNet/lemmy/pulls/71
2020-07-17 21:11:07 +00:00

362 lines
8.4 KiB
Rust

pub mod activities;
pub mod comment;
pub mod community;
pub mod community_inbox;
pub mod extensions;
pub mod fetcher;
pub mod post;
pub mod private_message;
pub mod shared_inbox;
pub mod user;
pub mod user_inbox;
use crate::{
apub::extensions::{
group_extensions::GroupExtension,
page_extension::PageExtension,
signatures::{PublicKey, PublicKeyExtension},
},
blocking,
request::{retry, RecvError},
routes::webfinger::WebFingerResponse,
DbPool, LemmyError,
};
use activitystreams_ext::{Ext1, Ext2};
use activitystreams_new::{
activity::Follow,
actor::{ApActor, Group, Person},
object::{Page, Tombstone},
prelude::*,
};
use actix_web::{body::Body, client::Client, HttpResponse};
use chrono::NaiveDateTime;
use failure::_core::fmt::Debug;
use lemmy_db::{activity::do_insert_activity, user::User_};
use lemmy_utils::{convert_datetime, get_apub_protocol_string, settings::Settings, MentionData};
use log::debug;
use serde::Serialize;
use url::{ParseError, Url};
type GroupExt = Ext2<ApActor<Group>, GroupExtension, PublicKeyExtension>;
type PersonExt = Ext1<ApActor<Person>, PublicKeyExtension>;
type PageExt = Ext1<Page, PageExtension>;
pub static APUB_JSON_CONTENT_TYPE: &str = "application/activity+json";
/// Convert the data to json and turn it into an HTTP Response with the correct ActivityPub
/// headers.
fn create_apub_response<T>(data: &T) -> HttpResponse<Body>
where
T: Serialize,
{
HttpResponse::Ok()
.content_type(APUB_JSON_CONTENT_TYPE)
.json(data)
}
fn create_apub_tombstone_response<T>(data: &T) -> HttpResponse<Body>
where
T: Serialize,
{
HttpResponse::Gone()
.content_type(APUB_JSON_CONTENT_TYPE)
.json(data)
}
// Checks if the ID has a valid format, correct scheme, and is in the allowed instance list.
fn is_apub_id_valid(apub_id: &Url) -> bool {
debug!("Checking {}", apub_id);
if apub_id.scheme() != get_apub_protocol_string() {
debug!("invalid scheme: {:?}", apub_id.scheme());
return false;
}
let allowed_instances: Vec<String> = Settings::get()
.federation
.allowed_instances
.split(',')
.map(|d| d.to_string())
.collect();
match apub_id.domain() {
Some(d) => {
let contains = allowed_instances.contains(&d.to_owned());
if !contains {
debug!("{} not in {:?}", d, allowed_instances);
}
contains
}
None => {
debug!("missing domain");
false
}
}
}
#[async_trait::async_trait(?Send)]
pub trait ToApub {
type Response;
async fn to_apub(&self, pool: &DbPool) -> Result<Self::Response, LemmyError>;
fn to_tombstone(&self) -> Result<Tombstone, LemmyError>;
}
/// Updated is actually the deletion time
fn create_tombstone(
deleted: bool,
object_id: &str,
updated: Option<NaiveDateTime>,
former_type: String,
) -> Result<Tombstone, LemmyError> {
if deleted {
if let Some(updated) = updated {
let mut tombstone = Tombstone::new();
tombstone.set_id(object_id.parse()?);
tombstone.set_former_type(former_type);
tombstone.set_deleted(convert_datetime(updated));
Ok(tombstone)
} else {
Err(format_err!("Cant convert to tombstone because updated time was None.").into())
}
} else {
Err(format_err!("Cant convert object to tombstone if it wasnt deleted").into())
}
}
#[async_trait::async_trait(?Send)]
pub trait FromApub {
type ApubType;
async fn from_apub(
apub: &Self::ApubType,
client: &Client,
pool: &DbPool,
actor_id: &Url,
) -> Result<Self, LemmyError>
where
Self: Sized;
}
#[async_trait::async_trait(?Send)]
pub trait ApubObjectType {
async fn send_create(
&self,
creator: &User_,
client: &Client,
pool: &DbPool,
) -> Result<(), LemmyError>;
async fn send_update(
&self,
creator: &User_,
client: &Client,
pool: &DbPool,
) -> Result<(), LemmyError>;
async fn send_delete(
&self,
creator: &User_,
client: &Client,
pool: &DbPool,
) -> Result<(), LemmyError>;
async fn send_undo_delete(
&self,
creator: &User_,
client: &Client,
pool: &DbPool,
) -> Result<(), LemmyError>;
async fn send_remove(
&self,
mod_: &User_,
client: &Client,
pool: &DbPool,
) -> Result<(), LemmyError>;
async fn send_undo_remove(
&self,
mod_: &User_,
client: &Client,
pool: &DbPool,
) -> Result<(), LemmyError>;
}
#[async_trait::async_trait(?Send)]
pub trait ApubLikeableType {
async fn send_like(
&self,
creator: &User_,
client: &Client,
pool: &DbPool,
) -> Result<(), LemmyError>;
async fn send_dislike(
&self,
creator: &User_,
client: &Client,
pool: &DbPool,
) -> Result<(), LemmyError>;
async fn send_undo_like(
&self,
creator: &User_,
client: &Client,
pool: &DbPool,
) -> Result<(), LemmyError>;
}
pub fn get_shared_inbox(actor_id: &Url) -> String {
format!(
"{}://{}{}/inbox",
&actor_id.scheme(),
&actor_id.host_str().unwrap(),
if let Some(port) = actor_id.port() {
format!(":{}", port)
} else {
"".to_string()
},
)
}
#[async_trait::async_trait(?Send)]
pub trait ActorType {
fn actor_id_str(&self) -> String;
fn public_key(&self) -> String;
fn private_key(&self) -> String;
// These two have default impls, since currently a community can't follow anything,
// and a user can't be followed (yet)
#[allow(unused_variables)]
async fn send_follow(
&self,
follow_actor_id: &str,
client: &Client,
pool: &DbPool,
) -> Result<(), LemmyError>;
async fn send_unfollow(
&self,
follow_actor_id: &str,
client: &Client,
pool: &DbPool,
) -> Result<(), LemmyError>;
#[allow(unused_variables)]
async fn send_accept_follow(
&self,
follow: Follow,
client: &Client,
pool: &DbPool,
) -> Result<(), LemmyError>;
async fn send_delete(
&self,
creator: &User_,
client: &Client,
pool: &DbPool,
) -> Result<(), LemmyError>;
async fn send_undo_delete(
&self,
creator: &User_,
client: &Client,
pool: &DbPool,
) -> Result<(), LemmyError>;
async fn send_remove(
&self,
mod_: &User_,
client: &Client,
pool: &DbPool,
) -> Result<(), LemmyError>;
async fn send_undo_remove(
&self,
mod_: &User_,
client: &Client,
pool: &DbPool,
) -> Result<(), LemmyError>;
/// For a given community, returns the inboxes of all followers.
async fn get_follower_inboxes(&self, pool: &DbPool) -> Result<Vec<String>, LemmyError>;
fn actor_id(&self) -> Result<Url, ParseError> {
Url::parse(&self.actor_id_str())
}
// TODO move these to the db rows
fn get_inbox_url(&self) -> String {
format!("{}/inbox", &self.actor_id_str())
}
// TODO: make this return `Result<Url, ParseError>
fn get_shared_inbox_url(&self) -> String {
get_shared_inbox(&self.actor_id().unwrap())
}
fn get_outbox_url(&self) -> String {
format!("{}/outbox", &self.actor_id_str())
}
fn get_followers_url(&self) -> String {
format!("{}/followers", &self.actor_id_str())
}
fn get_following_url(&self) -> String {
format!("{}/following", &self.actor_id_str())
}
fn get_liked_url(&self) -> String {
format!("{}/liked", &self.actor_id_str())
}
fn get_public_key_ext(&self) -> PublicKeyExtension {
PublicKey {
id: format!("{}#main-key", self.actor_id_str()),
owner: self.actor_id_str(),
public_key_pem: self.public_key(),
}
.to_ext()
}
}
pub async fn fetch_webfinger_url(
mention: &MentionData,
client: &Client,
) -> Result<Url, LemmyError> {
let fetch_url = format!(
"{}://{}/.well-known/webfinger?resource=acct:{}@{}",
get_apub_protocol_string(),
mention.domain,
mention.name,
mention.domain
);
debug!("Fetching webfinger url: {}", &fetch_url);
let mut response = retry(|| client.get(&fetch_url).send()).await?;
let res: WebFingerResponse = response
.json()
.await
.map_err(|e| RecvError(e.to_string()))?;
let link = res
.links
.iter()
.find(|l| l.type_.eq(&Some("application/activity+json".to_string())))
.ok_or_else(|| format_err!("No application/activity+json link found."))?;
link
.href
.to_owned()
.map(|u| Url::parse(&u))
.transpose()?
.ok_or_else(|| format_err!("No href found.").into())
}
pub async fn insert_activity<T>(
user_id: i32,
data: T,
local: bool,
pool: &DbPool,
) -> Result<(), LemmyError>
where
T: Serialize + Debug + Send + 'static,
{
blocking(pool, move |conn| {
do_insert_activity(conn, user_id, &data, local)
})
.await??;
Ok(())
}