lemmy/server/src/apub/mod.rs

270 lines
7.3 KiB
Rust
Raw Normal View History

2020-04-09 19:04:31 +00:00
pub mod activities;
pub mod comment;
pub mod community;
pub mod community_inbox;
2020-05-05 14:30:13 +00:00
pub mod extensions;
pub mod fetcher;
pub mod post;
2020-05-06 02:06:24 +00:00
pub mod private_message;
pub mod shared_inbox;
pub mod user;
pub mod user_inbox;
2020-04-24 14:04:36 +00:00
2020-05-16 14:04:08 +00:00
use crate::{
apub::extensions::{
group_extensions::GroupExtension,
page_extension::PageExtension,
signatures::{PublicKey, PublicKeyExtension},
},
convert_datetime,
db::user::User_,
routes::webfinger::WebFingerResponse,
MentionData,
2020-05-16 14:04:08 +00:00
Settings,
};
2020-04-24 14:04:36 +00:00
use activitystreams::{
2020-05-16 14:04:08 +00:00
activity::Follow,
actor::{properties::ApActorProperties, Group, Person},
ext::Ext,
object::{Page, Tombstone},
2020-04-24 14:04:36 +00:00
};
2020-05-16 14:04:08 +00:00
use actix_web::{body::Body, HttpResponse, Result};
use chrono::NaiveDateTime;
2020-04-24 14:04:36 +00:00
use diesel::PgConnection;
2020-04-24 19:55:54 +00:00
use failure::Error;
use isahc::prelude::*;
use log::debug;
2020-05-16 14:04:08 +00:00
use serde::Serialize;
2020-04-24 19:55:54 +00:00
use url::Url;
2020-04-24 14:04:36 +00:00
2020-05-05 14:30:13 +00:00
type GroupExt = Ext<Ext<Ext<Group, GroupExtension>, ApActorProperties>, PublicKeyExtension>;
2020-04-10 13:50:40 +00:00
type PersonExt = Ext<Ext<Person, ApActorProperties>, PublicKeyExtension>;
2020-05-05 00:04:48 +00:00
type PageExt = Ext<Page, PageExtension>;
pub static APUB_JSON_CONTENT_TYPE: &str = "application/activity+json";
pub enum EndpointType {
Community,
User,
Post,
Comment,
2020-05-06 02:06:24 +00:00
PrivateMessage,
}
2020-04-17 15:33:55 +00:00
/// 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>
2020-03-19 01:16:17 +00:00
where
2020-04-17 15:33:55 +00:00
T: Serialize,
2020-03-19 01:16:17 +00:00
{
2020-03-16 17:30:25 +00:00
HttpResponse::Ok()
.content_type(APUB_JSON_CONTENT_TYPE)
2020-04-17 15:33:55 +00:00
.json(data)
}
2020-04-29 19:10:50 +00:00
fn create_apub_tombstone_response<T>(data: &T) -> HttpResponse<Body>
where
T: Serialize,
{
HttpResponse::Gone()
.content_type(APUB_JSON_CONTENT_TYPE)
.json(data)
}
2020-05-14 11:23:56 +00:00
/// Generates the ActivityPub ID for a given object type and ID.
pub fn make_apub_endpoint(endpoint_type: EndpointType, name: &str) -> Url {
let point = match endpoint_type {
2020-04-07 15:34:44 +00:00
EndpointType::Community => "c",
EndpointType::User => "u",
EndpointType::Post => "post",
EndpointType::Comment => "comment",
2020-05-06 02:06:24 +00:00
EndpointType::PrivateMessage => "private_message",
};
Url::parse(&format!(
"{}://{}/{}/{}",
2020-02-29 17:38:47 +00:00
get_apub_protocol_string(),
Settings::get().hostname,
point,
name
))
.unwrap()
}
2020-02-29 17:38:47 +00:00
pub fn get_apub_protocol_string() -> &'static str {
2020-03-18 21:09:00 +00:00
if Settings::get().federation.tls_enabled {
"https"
} else {
"http"
}
2020-02-29 17:38:47 +00:00
}
2020-04-17 17:34:18 +00:00
// Checks if the ID has a valid format, correct scheme, and is in the whitelist.
fn is_apub_id_valid(apub_id: &Url) -> bool {
if apub_id.scheme() != get_apub_protocol_string() {
2020-04-17 17:34:18 +00:00
return false;
}
let whitelist: Vec<String> = Settings::get()
.federation
.instance_whitelist
.split(',')
.map(|d| d.to_string())
.collect();
match apub_id.domain() {
2020-04-17 17:34:18 +00:00
Some(d) => whitelist.contains(&d.to_owned()),
None => false,
}
}
2020-04-24 19:55:54 +00:00
pub trait ToApub {
type Response;
fn to_apub(&self, conn: &PgConnection) -> Result<Self::Response, Error>;
fn to_tombstone(&self) -> Result<Tombstone, Error>;
2020-04-28 17:46:25 +00:00
}
2020-05-03 14:22:25 +00:00
/// Updated is actually the deletion time
fn create_tombstone(
deleted: bool,
object_id: &str,
updated: Option<NaiveDateTime>,
2020-04-29 19:10:50 +00:00
former_type: String,
) -> Result<Tombstone, Error> {
if deleted {
if let Some(updated) = updated {
let mut tombstone = Tombstone::default();
tombstone.object_props.set_id(object_id)?;
tombstone
.tombstone_props
.set_former_type_xsd_string(former_type)?
.set_deleted(convert_datetime(updated))?;
Ok(tombstone)
} else {
Err(format_err!(
"Cant convert to tombstone because updated time was None."
))
2020-04-28 17:46:25 +00:00
}
} else {
Err(format_err!(
"Cant convert object to tombstone if it wasnt deleted"
))
2020-04-28 17:46:25 +00:00
}
}
pub trait FromApub {
type ApubType;
fn from_apub(apub: &Self::ApubType, conn: &PgConnection) -> Result<Self, Error>
2020-04-24 19:55:54 +00:00
where
Self: Sized;
}
pub trait ApubObjectType {
fn send_create(&self, creator: &User_, conn: &PgConnection) -> Result<(), Error>;
fn send_update(&self, creator: &User_, conn: &PgConnection) -> Result<(), Error>;
fn send_delete(&self, creator: &User_, conn: &PgConnection) -> Result<(), Error>;
fn send_undo_delete(&self, creator: &User_, conn: &PgConnection) -> Result<(), Error>;
2020-05-03 14:00:59 +00:00
fn send_remove(&self, mod_: &User_, conn: &PgConnection) -> Result<(), Error>;
fn send_undo_remove(&self, mod_: &User_, conn: &PgConnection) -> Result<(), Error>;
}
2020-04-28 02:46:09 +00:00
pub trait ApubLikeableType {
fn send_like(&self, creator: &User_, conn: &PgConnection) -> Result<(), Error>;
fn send_dislike(&self, creator: &User_, conn: &PgConnection) -> Result<(), Error>;
fn send_undo_like(&self, creator: &User_, conn: &PgConnection) -> Result<(), Error>;
2020-04-28 02:46:09 +00:00
}
pub fn get_shared_inbox(actor_id: &str) -> String {
let url = Url::parse(actor_id).unwrap();
format!(
"{}://{}{}/inbox",
&url.scheme(),
&url.host_str().unwrap(),
if let Some(port) = url.port() {
format!(":{}", port)
} else {
"".to_string()
},
)
}
2020-04-24 19:55:54 +00:00
pub trait ActorType {
fn actor_id(&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)]
2020-05-04 02:41:45 +00:00
fn send_follow(&self, follow_actor_id: &str, conn: &PgConnection) -> Result<(), Error>;
fn send_unfollow(&self, follow_actor_id: &str, conn: &PgConnection) -> Result<(), Error>;
#[allow(unused_variables)]
2020-05-04 02:41:45 +00:00
fn send_accept_follow(&self, follow: &Follow, conn: &PgConnection) -> Result<(), Error>;
fn send_delete(&self, creator: &User_, conn: &PgConnection) -> Result<(), Error>;
fn send_undo_delete(&self, creator: &User_, conn: &PgConnection) -> Result<(), Error>;
2020-04-28 17:46:25 +00:00
2020-05-03 14:00:59 +00:00
fn send_remove(&self, mod_: &User_, conn: &PgConnection) -> Result<(), Error>;
fn send_undo_remove(&self, mod_: &User_, conn: &PgConnection) -> Result<(), Error>;
/// For a given community, returns the inboxes of all followers.
2020-05-04 02:41:45 +00:00
fn get_follower_inboxes(&self, conn: &PgConnection) -> Result<Vec<String>, Error>;
// TODO move these to the db rows
2020-04-24 19:55:54 +00:00
fn get_inbox_url(&self) -> String {
format!("{}/inbox", &self.actor_id())
}
fn get_shared_inbox_url(&self) -> String {
get_shared_inbox(&self.actor_id())
}
2020-04-24 19:55:54 +00:00
fn get_outbox_url(&self) -> String {
format!("{}/outbox", &self.actor_id())
}
fn get_followers_url(&self) -> String {
format!("{}/followers", &self.actor_id())
}
2020-04-24 19:55:54 +00:00
fn get_following_url(&self) -> String {
format!("{}/following", &self.actor_id())
}
2020-04-24 19:55:54 +00:00
fn get_liked_url(&self) -> String {
format!("{}/liked", &self.actor_id())
}
fn get_public_key_ext(&self) -> PublicKeyExtension {
PublicKey {
id: format!("{}#main-key", self.actor_id()),
owner: self.actor_id(),
public_key_pem: self.public_key(),
}
.to_ext()
}
2020-04-24 19:55:54 +00:00
}
pub fn fetch_webfinger_url(mention: &MentionData) -> Result<String, Error> {
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 text = isahc::get(&fetch_url)?.text()?;
let res: WebFingerResponse = serde_json::from_str(&text)?;
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()
.ok_or_else(|| format_err!("No href found."))
}