mirror of
https://github.com/versia-pub/activitypub.git
synced 2025-12-06 06:38:20 +01:00
basic AP
This commit is contained in:
parent
1c09eb793d
commit
b90a332b3c
2077
Cargo.lock
generated
2077
Cargo.lock
generated
File diff suppressed because it is too large
Load diff
20
Cargo.toml
20
Cargo.toml
|
|
@ -7,18 +7,22 @@ authors = ["April John <aprl@acab.dev>"]
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
tokio = { version = "1.20.0", features = ["rt", "macros"] }
|
tokio = { version = "1.20.0", features = ["rt", "macros"] }
|
||||||
sea-orm = { version = "0.12.12", features = [
|
|
||||||
"sqlx-postgres",
|
|
||||||
"runtime-tokio-native-tls",
|
|
||||||
"with-json",
|
|
||||||
] }
|
|
||||||
serde = { version = "1.0.130", features = ["derive"] }
|
serde = { version = "1.0.130", features = ["derive"] }
|
||||||
actix-web = "4"
|
actix-web = "4"
|
||||||
env_logger = "0.11.0"
|
env_logger = "0.11.0"
|
||||||
clap = { version = "4.3.14", features = ["derive"] }
|
clap = { version = "4.3.14", features = ["derive"] }
|
||||||
|
activitypub_federation = "0.5.2"
|
||||||
[target.'cfg(unix)'.dependencies]
|
anyhow = "1.0.81"
|
||||||
openssl = { version = "0.10.63", features = ["vendored"] }
|
url = "2.5.0"
|
||||||
|
rand = "0.8.5"
|
||||||
|
tracing = "0.1.40"
|
||||||
|
async-trait = "0.1.79"
|
||||||
|
enum_delegate = "0.2.0"
|
||||||
|
chrono = "0.4.37"
|
||||||
|
activitystreams-kinds = "0.3.0"
|
||||||
|
thiserror = "1.0.58"
|
||||||
|
num_cpus = "1.16.0"
|
||||||
|
actix-web-prom = { version = "0.8.0", features = ["process"] }
|
||||||
|
|
||||||
[build-dependencies]
|
[build-dependencies]
|
||||||
vcpkg = "0.2.15"
|
vcpkg = "0.2.15"
|
||||||
|
|
|
||||||
|
|
@ -33,6 +33,11 @@
|
||||||
inherit (cargoToml.package) name version;
|
inherit (cargoToml.package) name version;
|
||||||
src = ./.;
|
src = ./.;
|
||||||
cargoLock.lockFile = ./Cargo.lock;
|
cargoLock.lockFile = ./Cargo.lock;
|
||||||
|
buildInputs = nonRustDeps;
|
||||||
|
nativeBuildInputs = with pkgs; [
|
||||||
|
rust-toolchain
|
||||||
|
pkg-config
|
||||||
|
];
|
||||||
};
|
};
|
||||||
|
|
||||||
# Rust dev environment
|
# Rust dev environment
|
||||||
|
|
|
||||||
74
src/activities/create_post.rs
Normal file
74
src/activities/create_post.rs
Normal file
|
|
@ -0,0 +1,74 @@
|
||||||
|
use crate::{
|
||||||
|
database::DatabaseHandle,
|
||||||
|
error::Error,
|
||||||
|
objects::{person::DbUser, post::Note},
|
||||||
|
utils::generate_object_id,
|
||||||
|
objects::post::DbPost,
|
||||||
|
};
|
||||||
|
use activitypub_federation::{
|
||||||
|
activity_sending::SendActivityTask,
|
||||||
|
config::Data,
|
||||||
|
fetch::object_id::ObjectId,
|
||||||
|
kinds::activity::CreateType,
|
||||||
|
protocol::{context::WithContext, helpers::deserialize_one_or_many},
|
||||||
|
traits::{ActivityHandler, Object},
|
||||||
|
};
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
use url::Url;
|
||||||
|
|
||||||
|
#[derive(Deserialize, Serialize, Debug)]
|
||||||
|
#[serde(rename_all = "camelCase")]
|
||||||
|
pub struct CreatePost {
|
||||||
|
pub(crate) actor: ObjectId<DbUser>,
|
||||||
|
#[serde(deserialize_with = "deserialize_one_or_many")]
|
||||||
|
pub(crate) to: Vec<Url>,
|
||||||
|
pub(crate) object: Note,
|
||||||
|
#[serde(rename = "type")]
|
||||||
|
pub(crate) kind: CreateType,
|
||||||
|
pub(crate) id: Url,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl CreatePost {
|
||||||
|
pub async fn send(note: Note, inbox: Url, data: &Data<DatabaseHandle>) -> Result<(), Error> {
|
||||||
|
print!("Sending reply to {}", ¬e.attributed_to);
|
||||||
|
let create = CreatePost {
|
||||||
|
actor: note.attributed_to.clone(),
|
||||||
|
to: note.to.clone(),
|
||||||
|
object: note,
|
||||||
|
kind: CreateType::Create,
|
||||||
|
id: generate_object_id(data.domain())?,
|
||||||
|
};
|
||||||
|
let create_with_context = WithContext::new_default(create);
|
||||||
|
let sends =
|
||||||
|
SendActivityTask::prepare(&create_with_context, &data.local_user(), vec![inbox], data)
|
||||||
|
.await?;
|
||||||
|
for send in sends {
|
||||||
|
send.sign_and_send(data).await?;
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[async_trait::async_trait]
|
||||||
|
impl ActivityHandler for CreatePost {
|
||||||
|
type DataType = DatabaseHandle;
|
||||||
|
type Error = crate::error::Error;
|
||||||
|
|
||||||
|
fn id(&self) -> &Url {
|
||||||
|
&self.id
|
||||||
|
}
|
||||||
|
|
||||||
|
fn actor(&self) -> &Url {
|
||||||
|
self.actor.inner()
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn verify(&self, data: &Data<Self::DataType>) -> Result<(), Self::Error> {
|
||||||
|
DbPost::verify(&self.object, &self.id, data).await?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn receive(self, data: &Data<Self::DataType>) -> Result<(), Self::Error> {
|
||||||
|
DbPost::from_json(self.object, data).await?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
1
src/activities/mod.rs
Normal file
1
src/activities/mod.rs
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
pub mod create_post;
|
||||||
27
src/database.rs
Normal file
27
src/database.rs
Normal file
|
|
@ -0,0 +1,27 @@
|
||||||
|
use crate::{objects::person::DbUser, error::Error};
|
||||||
|
use anyhow::anyhow;
|
||||||
|
use std::sync::{Arc, Mutex};
|
||||||
|
|
||||||
|
pub type DatabaseHandle = Arc<Database>;
|
||||||
|
|
||||||
|
/// Our "database" which contains all known users (local and federated)
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub struct Database {
|
||||||
|
pub users: Mutex<Vec<DbUser>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Database {
|
||||||
|
pub fn local_user(&self) -> DbUser {
|
||||||
|
let lock = self.users.lock().unwrap();
|
||||||
|
lock.first().unwrap().clone()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn read_user(&self, name: &str) -> Result<DbUser, Error> {
|
||||||
|
let db_user = self.local_user();
|
||||||
|
if name == db_user.name {
|
||||||
|
Ok(db_user)
|
||||||
|
} else {
|
||||||
|
Err(anyhow!("Invalid user {name}").into())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
26
src/error.rs
Normal file
26
src/error.rs
Normal file
|
|
@ -0,0 +1,26 @@
|
||||||
|
use std::fmt::{Display, Formatter};
|
||||||
|
|
||||||
|
/// Necessary because of this issue: https://github.com/actix/actix-web/issues/1711
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub struct Error(pub(crate) anyhow::Error);
|
||||||
|
|
||||||
|
impl Display for Error {
|
||||||
|
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
|
||||||
|
std::fmt::Display::fmt(&self.0, f)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl actix_web::ResponseError for Error {
|
||||||
|
fn error_response(&self) -> actix_web::HttpResponse {
|
||||||
|
actix_web::HttpResponse::InternalServerError().body(self.to_string())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<T> From<T> for Error
|
||||||
|
where
|
||||||
|
T: Into<anyhow::Error>,
|
||||||
|
{
|
||||||
|
fn from(t: T) -> Self {
|
||||||
|
Error(t.into())
|
||||||
|
}
|
||||||
|
}
|
||||||
97
src/http.rs
Normal file
97
src/http.rs
Normal file
|
|
@ -0,0 +1,97 @@
|
||||||
|
use crate::{
|
||||||
|
error::Error,
|
||||||
|
database::DatabaseHandle,
|
||||||
|
objects::person::{DbUser, PersonAcceptedActivities},
|
||||||
|
};
|
||||||
|
use activitypub_federation::{
|
||||||
|
actix_web::{inbox::receive_activity, signing_actor},
|
||||||
|
config::{Data, FederationConfig, FederationMiddleware},
|
||||||
|
fetch::webfinger::{build_webfinger_response, extract_webfinger_name},
|
||||||
|
protocol::context::WithContext,
|
||||||
|
traits::{Actor, Object},
|
||||||
|
FEDERATION_CONTENT_TYPE,
|
||||||
|
};
|
||||||
|
use actix_web::{web, web::Bytes, App, HttpRequest, HttpResponse, HttpServer};
|
||||||
|
use anyhow::anyhow;
|
||||||
|
use serde::Deserialize;
|
||||||
|
use tracing::info;
|
||||||
|
|
||||||
|
pub fn listen(config: &FederationConfig<DatabaseHandle>) -> Result<(), Error> {
|
||||||
|
let hostname = config.domain();
|
||||||
|
info!("Listening with actix-web on {hostname}");
|
||||||
|
let config = config.clone();
|
||||||
|
let server = HttpServer::new(move || {
|
||||||
|
App::new()
|
||||||
|
.wrap(FederationMiddleware::new(config.clone()))
|
||||||
|
//.route("/", web::get().to(http_get_system_user))
|
||||||
|
.route("/{user}", web::get().to(http_get_user))
|
||||||
|
.route("/{user}/inbox", web::post().to(http_post_user_inbox))
|
||||||
|
.route("/.well-known/webfinger", web::get().to(webfinger))
|
||||||
|
})
|
||||||
|
.bind(hostname)?
|
||||||
|
.run();
|
||||||
|
tokio::spawn(server);
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Handles requests to fetch system user json over HTTP
|
||||||
|
/*pub async fn http_get_system_user(data: Data<DatabaseHandle>) -> Result<HttpResponse, Error> {
|
||||||
|
let json_user = data.system_user.clone().into_json(&data).await?;
|
||||||
|
Ok(HttpResponse::Ok()
|
||||||
|
.content_type(FEDERATION_CONTENT_TYPE)
|
||||||
|
.json(WithContext::new_default(json_user)))
|
||||||
|
}*/
|
||||||
|
|
||||||
|
/// Handles requests to fetch user json over HTTP
|
||||||
|
pub async fn http_get_user(
|
||||||
|
request: HttpRequest,
|
||||||
|
user_name: web::Path<String>,
|
||||||
|
data: Data<DatabaseHandle>,
|
||||||
|
) -> Result<HttpResponse, Error> {
|
||||||
|
let signed_by = signing_actor::<DbUser>(&request, None, &data).await?;
|
||||||
|
// here, checks can be made on the actor or the domain to which
|
||||||
|
// it belongs, to verify whether it is allowed to access this resource
|
||||||
|
info!(
|
||||||
|
"Fetch user request is signed by system account {}",
|
||||||
|
signed_by.id()
|
||||||
|
);
|
||||||
|
|
||||||
|
let db_user = data.local_user();
|
||||||
|
if user_name.into_inner() == db_user.name {
|
||||||
|
let json_user = db_user.into_json(&data).await?;
|
||||||
|
Ok(HttpResponse::Ok()
|
||||||
|
.content_type(FEDERATION_CONTENT_TYPE)
|
||||||
|
.json(WithContext::new_default(json_user)))
|
||||||
|
} else {
|
||||||
|
Err(anyhow!("Invalid user").into())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Handles messages received in user inbox
|
||||||
|
pub async fn http_post_user_inbox(
|
||||||
|
request: HttpRequest,
|
||||||
|
body: Bytes,
|
||||||
|
data: Data<DatabaseHandle>,
|
||||||
|
) -> Result<HttpResponse, Error> {
|
||||||
|
receive_activity::<WithContext<PersonAcceptedActivities>, DbUser, DatabaseHandle>(
|
||||||
|
request, body, &data,
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Deserialize)]
|
||||||
|
pub struct WebfingerQuery {
|
||||||
|
resource: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn webfinger(
|
||||||
|
query: web::Query<WebfingerQuery>,
|
||||||
|
data: Data<DatabaseHandle>,
|
||||||
|
) -> Result<HttpResponse, Error> {
|
||||||
|
let name = extract_webfinger_name(&query.resource, &data)?;
|
||||||
|
let db_user = data.read_user(name)?;
|
||||||
|
Ok(HttpResponse::Ok().json(build_webfinger_response(
|
||||||
|
query.resource.clone(),
|
||||||
|
db_user.ap_id.into_inner(),
|
||||||
|
)))
|
||||||
|
}
|
||||||
98
src/main.rs
98
src/main.rs
|
|
@ -1,12 +1,26 @@
|
||||||
use actix_web::{get, middleware, web, App, Error, HttpResponse, HttpServer};
|
use activitypub_federation::config::{FederationConfig, FederationMiddleware};
|
||||||
use sea_orm::{Database, DatabaseConnection};
|
use actix_web::{get, http::KeepAlive, middleware, web, App, Error, HttpResponse, HttpServer};
|
||||||
|
use actix_web_prom::PrometheusMetricsBuilder;
|
||||||
|
use clap::Parser;
|
||||||
|
use database::Database;
|
||||||
|
use http::{http_get_user, http_post_user_inbox, webfinger};
|
||||||
|
use objects::person::DbUser;
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use std::env;
|
use tokio::signal;
|
||||||
use std::time::Duration;
|
use std::{
|
||||||
|
collections::HashMap, env, net::ToSocketAddrs, sync::{Arc, Mutex}
|
||||||
|
};
|
||||||
|
|
||||||
|
mod database;
|
||||||
|
mod objects;
|
||||||
|
mod activities;
|
||||||
|
mod utils;
|
||||||
|
mod error;
|
||||||
|
mod http;
|
||||||
|
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone)]
|
||||||
struct State {
|
struct State {
|
||||||
db: DatabaseConnection,
|
database: Arc<Database>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Serialize, Deserialize)]
|
#[derive(Debug, Serialize, Deserialize)]
|
||||||
|
|
@ -14,41 +28,83 @@ struct Response {
|
||||||
health: bool,
|
health: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Parser, Debug)]
|
||||||
|
#[clap(author = "April John", version, about)]
|
||||||
|
/// Application configuration
|
||||||
|
struct Args {
|
||||||
|
/// whether to be verbose
|
||||||
|
#[arg(short = 'v')]
|
||||||
|
verbose: bool,
|
||||||
|
|
||||||
|
/// optional parse arg for config file
|
||||||
|
#[arg()]
|
||||||
|
config_file: Option<String>,
|
||||||
|
}
|
||||||
|
|
||||||
#[get("/")]
|
#[get("/")]
|
||||||
async fn index(_: web::Data<State>) -> actix_web::Result<HttpResponse, Error> {
|
async fn index(_: web::Data<State>) -> actix_web::Result<HttpResponse, Error> {
|
||||||
Ok(HttpResponse::Ok().json(Response { health: true }))
|
Ok(HttpResponse::Ok().json(Response { health: true }))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const DOMAIN: &str = "example.com";
|
||||||
|
const LOCAL_USER_NAME: &str = "example";
|
||||||
|
|
||||||
#[actix_web::main]
|
#[actix_web::main]
|
||||||
async fn main() -> actix_web::Result<()> {
|
async fn main() -> actix_web::Result<(), anyhow::Error> {
|
||||||
env_logger::init_from_env(env_logger::Env::new().default_filter_or("info"));
|
env_logger::init_from_env(env_logger::Env::new().default_filter_or("info"));
|
||||||
|
|
||||||
let server_url = env::var("LISTEN").unwrap_or("127.0.0.1:8080".to_string());
|
let server_url = env::var("LISTEN").unwrap_or("127.0.0.1:8080".to_string());
|
||||||
|
|
||||||
let mut opts =
|
let local_user = DbUser::new(env::var("FEDERATED_DOMAIN").unwrap_or(DOMAIN.to_string()).as_str(), env::var("LOCAL_USER_NAME").unwrap_or(LOCAL_USER_NAME.to_string()).as_str()).unwrap();
|
||||||
sea_orm::ConnectOptions::new(env::var("DATABASE_URL").expect("DATABASE_URL ust be set"));
|
|
||||||
opts.max_connections(5)
|
|
||||||
.min_connections(1)
|
|
||||||
.connect_timeout(Duration::from_secs(8))
|
|
||||||
.acquire_timeout(Duration::from_secs(8))
|
|
||||||
.idle_timeout(Duration::from_secs(8))
|
|
||||||
.max_lifetime(Duration::from_secs(8));
|
|
||||||
|
|
||||||
let db: DatabaseConnection = Database::connect(opts)
|
let database = Arc::new(Database {
|
||||||
.await
|
users: Mutex::new(vec![local_user]),
|
||||||
.expect("Failed to connect to database");
|
});
|
||||||
|
|
||||||
let state = State { db };
|
let state = State { database };
|
||||||
|
|
||||||
let _ = HttpServer::new(move || {
|
let data = FederationConfig::builder()
|
||||||
|
.domain(env::var("FEDERATED_DOMAIN").expect("FEDERATED_DOMAIN must be set"))
|
||||||
|
.app_data(state.clone().database)
|
||||||
|
.build().await?;
|
||||||
|
|
||||||
|
|
||||||
|
let mut labels = HashMap::new();
|
||||||
|
labels.insert("domain".to_string(), env::var("FEDERATED_DOMAIN").expect("FEDERATED_DOMAIN must be set").to_string());
|
||||||
|
labels.insert("name".to_string(), env::var("LOCAL_USER_NAME").expect("LOCAL_USER_NAME must be set").to_string());
|
||||||
|
|
||||||
|
let prometheus = PrometheusMetricsBuilder::new("api")
|
||||||
|
.endpoint("/metrics")
|
||||||
|
.const_labels(labels)
|
||||||
|
.build()
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
let http_server = HttpServer::new(move || {
|
||||||
App::new()
|
App::new()
|
||||||
.app_data(web::Data::new(state.clone()))
|
.app_data(web::Data::new(state.clone()))
|
||||||
.wrap(middleware::Logger::default()) // enable logger
|
.wrap(middleware::Logger::default()) // enable logger
|
||||||
|
.wrap(prometheus.clone())
|
||||||
|
.wrap(FederationMiddleware::new(data.clone()))
|
||||||
|
.route("/{user}", web::get().to(http_get_user))
|
||||||
|
.route("/{user}/inbox", web::post().to(http_post_user_inbox))
|
||||||
|
.route("/.well-known/webfinger", web::get().to(webfinger))
|
||||||
.service(index)
|
.service(index)
|
||||||
})
|
})
|
||||||
.bind(&server_url)?
|
.bind(&server_url)?
|
||||||
.run()
|
.workers(num_cpus::get())
|
||||||
.await?;
|
.shutdown_timeout(20)
|
||||||
|
.keep_alive(KeepAlive::Os)
|
||||||
|
.run();
|
||||||
|
|
||||||
|
tokio::spawn(http_server);
|
||||||
|
|
||||||
|
match signal::ctrl_c().await {
|
||||||
|
Ok(()) => {},
|
||||||
|
Err(err) => {
|
||||||
|
eprintln!("Unable to listen for shutdown signal: {}", err);
|
||||||
|
// we also shut down in case of error
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
|
||||||
2
src/objects/mod.rs
Normal file
2
src/objects/mod.rs
Normal file
|
|
@ -0,0 +1,2 @@
|
||||||
|
pub mod person;
|
||||||
|
pub mod post;
|
||||||
140
src/objects/person.rs
Normal file
140
src/objects/person.rs
Normal file
|
|
@ -0,0 +1,140 @@
|
||||||
|
use crate::{activities::create_post::CreatePost, database::DatabaseHandle, error::Error};
|
||||||
|
use activitypub_federation::{
|
||||||
|
config::Data,
|
||||||
|
fetch::object_id::ObjectId,
|
||||||
|
http_signatures::generate_actor_keypair,
|
||||||
|
kinds::actor::PersonType,
|
||||||
|
protocol::{public_key::PublicKey, verification::verify_domains_match},
|
||||||
|
traits::{ActivityHandler, Actor, Object},
|
||||||
|
};
|
||||||
|
use chrono::{DateTime, Utc};
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
use std::fmt::Debug;
|
||||||
|
use url::Url;
|
||||||
|
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub struct DbUser {
|
||||||
|
pub name: String,
|
||||||
|
pub ap_id: ObjectId<DbUser>,
|
||||||
|
pub inbox: Url,
|
||||||
|
// exists for all users (necessary to verify http signatures)
|
||||||
|
pub public_key: String,
|
||||||
|
// exists only for local users
|
||||||
|
pub private_key: Option<String>,
|
||||||
|
last_refreshed_at: DateTime<Utc>,
|
||||||
|
pub followers: Vec<Url>,
|
||||||
|
pub local: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// List of all activities which this actor can receive.
|
||||||
|
#[derive(Deserialize, Serialize, Debug)]
|
||||||
|
#[serde(untagged)]
|
||||||
|
#[enum_delegate::implement(ActivityHandler)]
|
||||||
|
pub enum PersonAcceptedActivities {
|
||||||
|
CreateNote(CreatePost),
|
||||||
|
}
|
||||||
|
|
||||||
|
impl DbUser {
|
||||||
|
pub fn new(hostname: &str, name: &str) -> Result<DbUser, Error> {
|
||||||
|
let ap_id = Url::parse(&format!("https://{}/{}", hostname, &name))?.into();
|
||||||
|
let inbox = Url::parse(&format!("https://{}/{}/inbox", hostname, &name))?;
|
||||||
|
let keypair = generate_actor_keypair()?;
|
||||||
|
Ok(DbUser {
|
||||||
|
name: name.to_string(),
|
||||||
|
ap_id,
|
||||||
|
inbox,
|
||||||
|
public_key: keypair.public_key,
|
||||||
|
private_key: Some(keypair.private_key),
|
||||||
|
last_refreshed_at: Utc::now(),
|
||||||
|
followers: vec![],
|
||||||
|
local: true,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, Deserialize, Serialize)]
|
||||||
|
#[serde(rename_all = "camelCase")]
|
||||||
|
pub struct Person {
|
||||||
|
#[serde(rename = "type")]
|
||||||
|
kind: PersonType,
|
||||||
|
preferred_username: String,
|
||||||
|
id: ObjectId<DbUser>,
|
||||||
|
inbox: Url,
|
||||||
|
public_key: PublicKey,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[async_trait::async_trait]
|
||||||
|
impl Object for DbUser {
|
||||||
|
type DataType = DatabaseHandle;
|
||||||
|
type Kind = Person;
|
||||||
|
type Error = Error;
|
||||||
|
|
||||||
|
fn last_refreshed_at(&self) -> Option<DateTime<Utc>> {
|
||||||
|
Some(self.last_refreshed_at)
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn read_from_id(
|
||||||
|
object_id: Url,
|
||||||
|
data: &Data<Self::DataType>,
|
||||||
|
) -> Result<Option<Self>, Self::Error> {
|
||||||
|
let users = data.users.lock().unwrap();
|
||||||
|
let res = users
|
||||||
|
.clone()
|
||||||
|
.into_iter()
|
||||||
|
.find(|u| u.ap_id.inner() == &object_id);
|
||||||
|
Ok(res)
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn into_json(self, _data: &Data<Self::DataType>) -> Result<Self::Kind, Self::Error> {
|
||||||
|
Ok(Person {
|
||||||
|
preferred_username: self.name.clone(),
|
||||||
|
kind: Default::default(),
|
||||||
|
id: self.ap_id.clone(),
|
||||||
|
inbox: self.inbox.clone(),
|
||||||
|
public_key: self.public_key(),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn verify(
|
||||||
|
json: &Self::Kind,
|
||||||
|
expected_domain: &Url,
|
||||||
|
_data: &Data<Self::DataType>,
|
||||||
|
) -> Result<(), Self::Error> {
|
||||||
|
verify_domains_match(json.id.inner(), expected_domain)?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn from_json(
|
||||||
|
json: Self::Kind,
|
||||||
|
_data: &Data<Self::DataType>,
|
||||||
|
) -> Result<Self, Self::Error> {
|
||||||
|
Ok(DbUser {
|
||||||
|
name: json.preferred_username,
|
||||||
|
ap_id: json.id,
|
||||||
|
inbox: json.inbox,
|
||||||
|
public_key: json.public_key.public_key_pem,
|
||||||
|
private_key: None,
|
||||||
|
last_refreshed_at: Utc::now(),
|
||||||
|
followers: vec![],
|
||||||
|
local: false,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Actor for DbUser {
|
||||||
|
fn id(&self) -> Url {
|
||||||
|
self.ap_id.inner().clone()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn public_key_pem(&self) -> &str {
|
||||||
|
&self.public_key
|
||||||
|
}
|
||||||
|
|
||||||
|
fn private_key_pem(&self) -> Option<String> {
|
||||||
|
self.private_key.clone()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn inbox(&self) -> Url {
|
||||||
|
self.inbox.clone()
|
||||||
|
}
|
||||||
|
}
|
||||||
104
src/objects/post.rs
Normal file
104
src/objects/post.rs
Normal file
|
|
@ -0,0 +1,104 @@
|
||||||
|
use crate::{
|
||||||
|
activities::create_post::CreatePost,
|
||||||
|
database::DatabaseHandle,
|
||||||
|
error::Error,
|
||||||
|
utils::generate_object_id,
|
||||||
|
objects::person::DbUser,
|
||||||
|
};
|
||||||
|
use activitypub_federation::{
|
||||||
|
config::Data,
|
||||||
|
fetch::object_id::ObjectId,
|
||||||
|
kinds::{object::NoteType, public},
|
||||||
|
protocol::{helpers::deserialize_one_or_many, verification::verify_domains_match},
|
||||||
|
traits::{Actor, Object},
|
||||||
|
};
|
||||||
|
use activitystreams_kinds::link::MentionType;
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
use url::Url;
|
||||||
|
|
||||||
|
#[derive(Clone, Debug)]
|
||||||
|
pub struct DbPost {
|
||||||
|
pub text: String,
|
||||||
|
pub ap_id: ObjectId<DbPost>,
|
||||||
|
pub creator: ObjectId<DbUser>,
|
||||||
|
pub local: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Deserialize, Serialize, Debug)]
|
||||||
|
#[serde(rename_all = "camelCase")]
|
||||||
|
pub struct Note {
|
||||||
|
#[serde(rename = "type")]
|
||||||
|
kind: NoteType,
|
||||||
|
id: ObjectId<DbPost>,
|
||||||
|
pub(crate) attributed_to: ObjectId<DbUser>,
|
||||||
|
#[serde(deserialize_with = "deserialize_one_or_many")]
|
||||||
|
pub(crate) to: Vec<Url>,
|
||||||
|
content: String,
|
||||||
|
in_reply_to: Option<ObjectId<DbPost>>,
|
||||||
|
tag: Vec<Mention>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, Deserialize, Serialize)]
|
||||||
|
pub struct Mention {
|
||||||
|
pub href: Url,
|
||||||
|
#[serde(rename = "type")]
|
||||||
|
pub kind: MentionType,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[async_trait::async_trait]
|
||||||
|
impl Object for DbPost {
|
||||||
|
type DataType = DatabaseHandle;
|
||||||
|
type Kind = Note;
|
||||||
|
type Error = Error;
|
||||||
|
|
||||||
|
async fn read_from_id(
|
||||||
|
_object_id: Url,
|
||||||
|
_data: &Data<Self::DataType>,
|
||||||
|
) -> Result<Option<Self>, Self::Error> {
|
||||||
|
Ok(None)
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn into_json(self, _data: &Data<Self::DataType>) -> Result<Self::Kind, Self::Error> {
|
||||||
|
unimplemented!()
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn verify(
|
||||||
|
json: &Self::Kind,
|
||||||
|
expected_domain: &Url,
|
||||||
|
_data: &Data<Self::DataType>,
|
||||||
|
) -> Result<(), Self::Error> {
|
||||||
|
verify_domains_match(json.id.inner(), expected_domain)?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn from_json(json: Self::Kind, data: &Data<Self::DataType>) -> Result<Self, Self::Error> {
|
||||||
|
println!(
|
||||||
|
"Received post with content {} and id {}",
|
||||||
|
&json.content, &json.id
|
||||||
|
);
|
||||||
|
let creator = json.attributed_to.dereference(data).await?;
|
||||||
|
let post = DbPost {
|
||||||
|
text: json.content,
|
||||||
|
ap_id: json.id.clone(),
|
||||||
|
creator: json.attributed_to.clone(),
|
||||||
|
local: false,
|
||||||
|
};
|
||||||
|
|
||||||
|
let mention = Mention {
|
||||||
|
href: creator.ap_id.clone().into_inner(),
|
||||||
|
kind: Default::default(),
|
||||||
|
};
|
||||||
|
let note = Note {
|
||||||
|
kind: Default::default(),
|
||||||
|
id: generate_object_id(data.domain())?.into(),
|
||||||
|
attributed_to: data.local_user().ap_id,
|
||||||
|
to: vec![public()],
|
||||||
|
content: format!("Hello {}", creator.name),
|
||||||
|
in_reply_to: Some(json.id.clone()),
|
||||||
|
tag: vec![mention],
|
||||||
|
};
|
||||||
|
CreatePost::send(note, creator.shared_inbox_or_inbox(), data).await?;
|
||||||
|
|
||||||
|
Ok(post)
|
||||||
|
}
|
||||||
|
}
|
||||||
13
src/utils.rs
Normal file
13
src/utils.rs
Normal file
|
|
@ -0,0 +1,13 @@
|
||||||
|
use rand::{distributions::Alphanumeric, thread_rng, Rng};
|
||||||
|
use url::{ParseError, Url};
|
||||||
|
|
||||||
|
/// Just generate random url as object id. In a real project, you probably want to use
|
||||||
|
/// an url which contains the database id for easy retrieval (or store the random id in db).
|
||||||
|
pub fn generate_object_id(domain: &str) -> Result<Url, ParseError> {
|
||||||
|
let id: String = thread_rng()
|
||||||
|
.sample_iter(&Alphanumeric)
|
||||||
|
.take(7)
|
||||||
|
.map(char::from)
|
||||||
|
.collect();
|
||||||
|
Url::parse(&format!("https://{}/objects/{}", domain, id))
|
||||||
|
}
|
||||||
Loading…
Reference in a new issue