2024-04-18 04:03:52 +02:00
|
|
|
use activitypub_federation::{
|
2024-05-04 19:07:34 +02:00
|
|
|
config::{Data, FederationConfig, FederationMiddleware},
|
|
|
|
|
fetch::{object_id::ObjectId, webfinger::webfinger_resolve_actor},
|
|
|
|
|
http_signatures::generate_actor_keypair,
|
|
|
|
|
traits::Actor,
|
2024-04-18 04:03:52 +02:00
|
|
|
};
|
2024-05-04 17:54:11 +02:00
|
|
|
use activitystreams_kinds::public;
|
2024-05-04 19:07:34 +02:00
|
|
|
use actix_web::{
|
|
|
|
|
get, http::KeepAlive, middleware, post, web, App, Error, HttpResponse, HttpServer,
|
|
|
|
|
};
|
2024-04-09 19:48:18 +02:00
|
|
|
use actix_web_prom::PrometheusMetricsBuilder;
|
2024-05-04 17:54:11 +02:00
|
|
|
use async_once::AsyncOnce;
|
2024-04-18 04:03:52 +02:00
|
|
|
use chrono::{DateTime, Utc};
|
2024-04-09 19:48:18 +02:00
|
|
|
use clap::Parser;
|
|
|
|
|
use database::Database;
|
2024-05-04 17:54:11 +02:00
|
|
|
use entities::post;
|
2024-04-09 19:48:18 +02:00
|
|
|
use http::{http_get_user, http_post_user_inbox, webfinger};
|
2024-06-27 05:13:38 +02:00
|
|
|
use lysand::http::{create_activity, fetch_post};
|
2024-04-09 19:48:18 +02:00
|
|
|
use objects::person::DbUser;
|
2024-04-18 04:03:52 +02:00
|
|
|
use sea_orm::{ActiveModelTrait, DatabaseConnection, Set};
|
2024-01-26 21:01:43 +01:00
|
|
|
use serde::{Deserialize, Serialize};
|
2024-04-09 19:48:18 +02:00
|
|
|
use std::{
|
2024-04-09 19:55:07 +02:00
|
|
|
collections::HashMap,
|
|
|
|
|
env,
|
|
|
|
|
net::ToSocketAddrs,
|
2024-05-04 17:54:11 +02:00
|
|
|
sync::{Arc, Mutex, OnceLock},
|
2024-04-09 19:48:18 +02:00
|
|
|
};
|
2024-04-09 19:55:07 +02:00
|
|
|
use tokio::signal;
|
2024-05-03 23:42:42 +02:00
|
|
|
use tracing::{info, instrument::WithSubscriber};
|
2024-04-18 04:03:52 +02:00
|
|
|
use url::Url;
|
2024-06-27 09:48:47 +02:00
|
|
|
use utils::generate_object_id;
|
2024-04-09 19:48:18 +02:00
|
|
|
|
2024-05-04 19:07:34 +02:00
|
|
|
use crate::{
|
|
|
|
|
activities::create_post::CreatePost,
|
|
|
|
|
database::{Config, State},
|
|
|
|
|
objects::post::{Mention, Note},
|
|
|
|
|
};
|
2024-05-05 18:18:39 +02:00
|
|
|
use crate::{activities::follow::Follow, entities::user};
|
2024-06-17 19:52:51 +02:00
|
|
|
use dotenv::dotenv;
|
2024-06-18 03:43:59 +02:00
|
|
|
use lazy_static::lazy_static;
|
2024-05-04 17:54:11 +02:00
|
|
|
|
2024-04-09 19:48:18 +02:00
|
|
|
mod activities;
|
2024-04-09 19:55:07 +02:00
|
|
|
mod database;
|
2024-04-18 04:03:52 +02:00
|
|
|
mod entities;
|
2024-04-09 19:48:18 +02:00
|
|
|
mod error;
|
|
|
|
|
mod http;
|
2024-05-09 23:10:36 +02:00
|
|
|
mod lysand;
|
2024-04-09 19:55:07 +02:00
|
|
|
mod objects;
|
|
|
|
|
mod utils;
|
2024-01-26 21:01:43 +01:00
|
|
|
|
|
|
|
|
#[derive(Debug, Serialize, Deserialize)]
|
|
|
|
|
struct Response {
|
|
|
|
|
health: bool,
|
|
|
|
|
}
|
|
|
|
|
|
2024-04-09 19:48:18 +02:00
|
|
|
#[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>,
|
|
|
|
|
}
|
|
|
|
|
|
2024-01-26 21:01:43 +01:00
|
|
|
#[get("/")]
|
|
|
|
|
async fn index(_: web::Data<State>) -> actix_web::Result<HttpResponse, Error> {
|
|
|
|
|
Ok(HttpResponse::Ok().json(Response { health: true }))
|
|
|
|
|
}
|
|
|
|
|
|
2024-05-04 17:59:26 +02:00
|
|
|
#[get("/test/postmanually/{user}/{post}")]
|
2024-05-04 17:54:11 +02:00
|
|
|
async fn post_manually(
|
|
|
|
|
path: web::Path<(String, String)>,
|
|
|
|
|
state: web::Data<State>,
|
|
|
|
|
) -> actix_web::Result<HttpResponse, error::Error> {
|
|
|
|
|
let local_user = state.local_user().await?;
|
|
|
|
|
let data = FEDERATION_CONFIG.get().unwrap();
|
2024-05-19 07:17:13 +02:00
|
|
|
let target =
|
2024-05-04 19:07:34 +02:00
|
|
|
webfinger_resolve_actor::<State, user::Model>(path.0.as_str(), &data.to_request_data())
|
|
|
|
|
.await?;
|
2024-05-04 17:54:11 +02:00
|
|
|
|
|
|
|
|
let mention = Mention {
|
2024-05-19 07:17:13 +02:00
|
|
|
href: Url::parse(&target.id)?,
|
2024-05-04 17:54:11 +02:00
|
|
|
kind: Default::default(),
|
|
|
|
|
};
|
2024-06-15 02:06:01 +02:00
|
|
|
// TODO change
|
2024-06-27 05:31:58 +02:00
|
|
|
let uuid = uuid::Uuid::now_v7().to_string();
|
|
|
|
|
let id: ObjectId<post::Model> = generate_object_id(data.domain(), &uuid)?.into();
|
2024-05-04 17:54:11 +02:00
|
|
|
let note = Note {
|
|
|
|
|
kind: Default::default(),
|
2024-06-27 05:13:38 +02:00
|
|
|
id: id.clone(),
|
2024-05-04 17:54:11 +02:00
|
|
|
sensitive: false,
|
2024-05-19 07:17:13 +02:00
|
|
|
attributed_to: Url::parse(&local_user.id).unwrap().into(),
|
2024-06-27 17:14:37 +02:00
|
|
|
to: vec![public(), mention.href.clone()],
|
2024-05-19 07:17:13 +02:00
|
|
|
content: format!("{} {}", path.1, target.name),
|
2024-05-04 17:54:11 +02:00
|
|
|
tag: vec![mention],
|
|
|
|
|
in_reply_to: None,
|
2024-06-18 03:43:59 +02:00
|
|
|
cc: vec![].into(),
|
2024-05-04 17:54:11 +02:00
|
|
|
};
|
|
|
|
|
|
2024-06-27 05:13:38 +02:00
|
|
|
let post = entities::post::ActiveModel {
|
2024-06-27 05:31:58 +02:00
|
|
|
id: Set(uuid),
|
2024-06-27 05:13:38 +02:00
|
|
|
creator: Set(local_user.id.clone()),
|
|
|
|
|
content: Set(note.content.clone()),
|
|
|
|
|
sensitive: Set(false),
|
|
|
|
|
created_at: Set(Utc::now()),
|
|
|
|
|
local: Set(true),
|
|
|
|
|
updated_at: Set(Some(Utc::now())),
|
|
|
|
|
content_type: Set("Note".to_string()),
|
|
|
|
|
visibility: Set("public".to_string()),
|
|
|
|
|
url: Set(id.to_string()),
|
|
|
|
|
ap_json: Set(Some(serde_json::to_string(¬e).unwrap())),
|
|
|
|
|
..Default::default()
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
let post = post.insert(DB.get().unwrap()).await?;
|
|
|
|
|
|
2024-05-04 19:07:34 +02:00
|
|
|
CreatePost::send(
|
|
|
|
|
note,
|
2024-06-27 05:13:38 +02:00
|
|
|
post,
|
2024-05-19 07:17:13 +02:00
|
|
|
target.shared_inbox_or_inbox(),
|
2024-05-04 19:07:34 +02:00
|
|
|
&data.to_request_data(),
|
|
|
|
|
)
|
|
|
|
|
.await?;
|
2024-05-04 17:54:11 +02:00
|
|
|
|
|
|
|
|
Ok(HttpResponse::Ok().json(Response { health: true }))
|
|
|
|
|
}
|
|
|
|
|
|
2024-05-05 18:18:39 +02:00
|
|
|
#[get("/test/follow/{user}")]
|
|
|
|
|
async fn follow_manually(
|
|
|
|
|
path: web::Path<String>,
|
|
|
|
|
state: web::Data<State>,
|
|
|
|
|
) -> actix_web::Result<HttpResponse, error::Error> {
|
|
|
|
|
let local_user = state.local_user().await?;
|
|
|
|
|
let data = FEDERATION_CONFIG.get().unwrap();
|
|
|
|
|
let followee =
|
|
|
|
|
webfinger_resolve_actor::<State, user::Model>(path.as_str(), &data.to_request_data())
|
|
|
|
|
.await?;
|
|
|
|
|
|
|
|
|
|
let followee_object: ObjectId<user::Model> = Url::parse(&followee.id)?.into();
|
|
|
|
|
let localuser_object: ObjectId<user::Model> = Url::parse(&local_user.id)?.into();
|
|
|
|
|
|
|
|
|
|
Follow::send(
|
|
|
|
|
localuser_object,
|
|
|
|
|
followee_object,
|
|
|
|
|
followee.shared_inbox_or_inbox(),
|
|
|
|
|
&data.to_request_data(),
|
|
|
|
|
)
|
|
|
|
|
.await?;
|
|
|
|
|
|
|
|
|
|
Ok(HttpResponse::Ok().json(Response { health: true }))
|
|
|
|
|
}
|
|
|
|
|
|
2024-05-19 07:17:13 +02:00
|
|
|
const DOMAIN_DEF: &str = "social.lysand.org";
|
2024-06-18 03:34:37 +02:00
|
|
|
const LOCAL_USER_NAME: &str = "apservice";
|
2024-04-09 19:48:18 +02:00
|
|
|
|
2024-05-04 19:07:34 +02:00
|
|
|
lazy_static! {
|
2024-05-19 07:17:13 +02:00
|
|
|
static ref SERVER_URL: String = env::var("LISTEN").unwrap_or("0.0.0.0:8080".to_string());
|
2024-05-04 17:54:11 +02:00
|
|
|
static ref DATABASE_URL: String = env::var("DATABASE_URL").expect("DATABASE_URL must be set");
|
2024-05-04 19:07:34 +02:00
|
|
|
static ref USERNAME: String =
|
|
|
|
|
env::var("LOCAL_USER_NAME").unwrap_or(LOCAL_USER_NAME.to_string());
|
2024-05-19 07:17:13 +02:00
|
|
|
static ref API_DOMAIN: String = env::var("API_DOMAIN").expect("not set API_DOMAIN");
|
|
|
|
|
static ref LYSAND_DOMAIN: String = env::var("LYSAND_DOMAIN").expect("not set LYSAND_DOMAIN");
|
|
|
|
|
static ref FEDERATED_DOMAIN: String =
|
|
|
|
|
env::var("FEDERATED_DOMAIN").unwrap_or(API_DOMAIN.to_string());
|
2024-05-04 17:54:11 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
static DB: OnceLock<DatabaseConnection> = OnceLock::new();
|
|
|
|
|
static FEDERATION_CONFIG: OnceLock<FederationConfig<State>> = OnceLock::new();
|
|
|
|
|
|
2024-01-26 21:01:43 +01:00
|
|
|
#[actix_web::main]
|
2024-04-09 19:48:18 +02:00
|
|
|
async fn main() -> actix_web::Result<(), anyhow::Error> {
|
2024-06-17 19:52:51 +02:00
|
|
|
dotenv().ok();
|
2024-01-26 21:01:43 +01:00
|
|
|
env_logger::init_from_env(env_logger::Env::new().default_filter_or("info"));
|
|
|
|
|
|
2024-05-04 19:07:34 +02:00
|
|
|
let ap_id = Url::parse(&format!(
|
|
|
|
|
"https://{}/{}",
|
2024-05-17 11:30:10 +02:00
|
|
|
API_DOMAIN.to_string(),
|
2024-05-04 19:07:34 +02:00
|
|
|
&USERNAME.to_string()
|
|
|
|
|
))?;
|
|
|
|
|
let inbox = Url::parse(&format!(
|
|
|
|
|
"https://{}/{}/inbox",
|
2024-05-17 11:30:10 +02:00
|
|
|
API_DOMAIN.to_string(),
|
2024-05-04 19:07:34 +02:00
|
|
|
&USERNAME.to_string()
|
|
|
|
|
))?;
|
2024-04-18 04:03:52 +02:00
|
|
|
let keypair = generate_actor_keypair()?;
|
|
|
|
|
|
|
|
|
|
let user = entities::user::ActiveModel {
|
2024-04-18 19:05:22 +02:00
|
|
|
id: Set(ap_id.clone().into()),
|
2024-05-04 17:54:11 +02:00
|
|
|
username: Set(USERNAME.to_string()),
|
2024-04-18 19:01:14 +02:00
|
|
|
name: Set("Test account <3".to_string()),
|
2024-04-18 04:03:52 +02:00
|
|
|
inbox: Set(inbox.to_string()),
|
|
|
|
|
public_key: Set(keypair.public_key.clone()),
|
|
|
|
|
private_key: Set(Some(keypair.private_key.clone())),
|
2024-04-18 19:14:06 +02:00
|
|
|
last_refreshed_at: Set(Utc::now()),
|
2024-04-18 19:09:10 +02:00
|
|
|
follower_count: Set(0),
|
|
|
|
|
following_count: Set(0),
|
2024-04-18 19:05:22 +02:00
|
|
|
url: Set(ap_id.to_string()),
|
2024-04-18 04:03:52 +02:00
|
|
|
local: Set(true),
|
2024-04-18 19:14:06 +02:00
|
|
|
created_at: Set(Utc::now()),
|
2024-04-18 04:03:52 +02:00
|
|
|
..Default::default()
|
|
|
|
|
};
|
2024-01-26 21:01:43 +01:00
|
|
|
|
2024-05-04 17:54:11 +02:00
|
|
|
let db = sea_orm::Database::connect(DATABASE_URL.to_string()).await?;
|
2024-04-18 03:41:52 +02:00
|
|
|
|
2024-04-18 18:30:15 +02:00
|
|
|
info!("Connected to database: {:?}", db);
|
|
|
|
|
|
2024-05-04 19:07:34 +02:00
|
|
|
DB.set(db)
|
|
|
|
|
.expect("We were not able to save the DB conn into memory");
|
2024-05-04 17:54:11 +02:00
|
|
|
|
|
|
|
|
let db = DB.get().unwrap();
|
|
|
|
|
|
|
|
|
|
let user = user.insert(db).await;
|
2024-04-18 04:03:52 +02:00
|
|
|
|
2024-04-18 18:26:41 +02:00
|
|
|
if let Err(err) = user {
|
|
|
|
|
eprintln!("Error inserting user: {:?}", err);
|
|
|
|
|
} else {
|
|
|
|
|
info!("User inserted: {:?}", user.unwrap());
|
|
|
|
|
}
|
|
|
|
|
|
2024-04-16 19:53:24 +02:00
|
|
|
let state: State = State {
|
2024-05-04 17:54:11 +02:00
|
|
|
database_connection: Arc::new(db.clone()),
|
2024-04-16 19:53:24 +02:00
|
|
|
};
|
2024-01-26 21:01:43 +01:00
|
|
|
|
2024-04-09 19:48:18 +02:00
|
|
|
let data = FederationConfig::builder()
|
2024-05-19 07:17:13 +02:00
|
|
|
.domain(FEDERATED_DOMAIN.to_string())
|
2024-04-18 03:41:52 +02:00
|
|
|
.app_data(state.clone())
|
2024-04-18 04:16:33 +02:00
|
|
|
.http_signature_compat(true)
|
2024-05-03 23:42:42 +02:00
|
|
|
.signed_fetch_actor(&state.local_user().await.unwrap())
|
2024-04-09 19:55:07 +02:00
|
|
|
.build()
|
|
|
|
|
.await?;
|
2024-04-09 19:48:18 +02:00
|
|
|
|
2024-05-04 18:02:02 +02:00
|
|
|
let _ = FEDERATION_CONFIG.set(data.clone());
|
|
|
|
|
|
2024-04-09 19:48:18 +02:00
|
|
|
let mut labels = HashMap::new();
|
2024-05-19 07:17:13 +02:00
|
|
|
labels.insert("domain".to_string(), FEDERATED_DOMAIN.to_string());
|
|
|
|
|
labels.insert("name".to_string(), USERNAME.to_string());
|
|
|
|
|
labels.insert("api_domain".to_string(), API_DOMAIN.to_string());
|
2024-04-09 19:48:18 +02:00
|
|
|
|
|
|
|
|
let prometheus = PrometheusMetricsBuilder::new("api")
|
|
|
|
|
.endpoint("/metrics")
|
|
|
|
|
.const_labels(labels)
|
|
|
|
|
.build()
|
|
|
|
|
.unwrap();
|
|
|
|
|
|
|
|
|
|
let http_server = HttpServer::new(move || {
|
2024-01-26 21:01:43 +01:00
|
|
|
App::new()
|
|
|
|
|
.app_data(web::Data::new(state.clone()))
|
|
|
|
|
.wrap(middleware::Logger::default()) // enable logger
|
2024-04-09 19:48:18 +02:00
|
|
|
.wrap(prometheus.clone())
|
|
|
|
|
.wrap(FederationMiddleware::new(data.clone()))
|
2024-05-04 17:54:11 +02:00
|
|
|
.service(post_manually)
|
2024-05-05 18:18:39 +02:00
|
|
|
.service(follow_manually)
|
2024-04-09 19:48:18 +02:00
|
|
|
.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))
|
2024-01-26 21:01:43 +01:00
|
|
|
.service(index)
|
2024-06-27 05:13:38 +02:00
|
|
|
.service(fetch_post)
|
|
|
|
|
.service(create_activity)
|
2024-01-26 21:01:43 +01:00
|
|
|
})
|
2024-05-04 17:54:11 +02:00
|
|
|
.bind(SERVER_URL.to_string())?
|
2024-04-09 19:48:18 +02:00
|
|
|
.workers(num_cpus::get())
|
|
|
|
|
.shutdown_timeout(20)
|
|
|
|
|
.keep_alive(KeepAlive::Os)
|
|
|
|
|
.run();
|
|
|
|
|
|
|
|
|
|
tokio::spawn(http_server);
|
|
|
|
|
|
|
|
|
|
match signal::ctrl_c().await {
|
2024-04-09 19:55:07 +02:00
|
|
|
Ok(()) => {}
|
2024-04-09 19:48:18 +02:00
|
|
|
Err(err) => {
|
|
|
|
|
eprintln!("Unable to listen for shutdown signal: {}", err);
|
|
|
|
|
// we also shut down in case of error
|
2024-04-09 19:55:07 +02:00
|
|
|
}
|
2024-04-09 19:48:18 +02:00
|
|
|
}
|
2024-01-26 21:01:43 +01:00
|
|
|
|
2024-04-16 19:53:24 +02:00
|
|
|
info!("Main thread shutdown..");
|
2024-04-15 01:07:15 +02:00
|
|
|
|
2024-01-26 21:01:43 +01:00
|
|
|
Ok(())
|
|
|
|
|
}
|