use activitypub_federation::{ config::Data, fetch::object_id::ObjectId, http_signatures::generate_actor_keypair, kinds::actor::PersonType, protocol::public_key::PublicKey, traits::{Actor, Object}, }; use async_trait::async_trait; use serde::{Deserialize, Serialize}; use stack_up::{Environment, Services}; use tracing::trace; use url::Url; use uuid::Uuid; use crate::{error::AppError, state::AppHandle}; #[derive(PartialEq, Clone, Debug)] pub(crate) struct User { pub id: String, pub username: String, pub ap_id: ObjectId, pub private_key: Option, pub public_key: String, pub inbox: Url, pub outbox: Option, } pub struct DbUser { pub id: String, pub username: String, pub ap_id: String, pub private_key: Option, pub public_key: String, pub inbox: String, pub outbox: Option, pub local: bool, } impl TryFrom for User { type Error = AppError; fn try_from(value: DbUser) -> Result { Ok(Self { id: value.id, username: value.username, ap_id: Url::parse(&value.ap_id)?.into(), private_key: value.private_key, public_key: value.public_key, inbox: Url::parse(&value.inbox)?, outbox: match value.outbox { Some(ref url) => Some(Url::parse(url)?), None => None, }, }) } } impl User { pub async fn new( username: &str, hostname: &str, services: &Services, environment: Environment, ) -> Result { trace!(username = ?username, "checking for system user"); let user = sqlx::query_as!( DbUser, "select * from account where username = $1 and local = $2", username, true ) .fetch_optional(&services.postgres) .await?; if let Some(user) = user { trace!(username = ?username, "system user exists"); return Self::try_from(user); } else { trace!(username = ?username, "system user does not exist. creating"); } trace!("creating keypair for new user"); let keys = generate_actor_keypair()?; let stub = &format!( "{}://{hostname}/users/{username}", match environment { Environment::Development => "http", Environment::Production => "https", } ); let id = Uuid::now_v7(); trace!(id = ?id, "creating a new user"); let user = sqlx::query_as!( DbUser, "insert into account (id, username, ap_id, private_key, public_key, inbox, outbox, local) values ($1, $2, $3, $4, $5, $6, $7, $8) returning *", id, username, stub, keys.private_key, keys.public_key, &format!("{stub}/inbox"), &format!("{stub}/outbox"), true ).fetch_one(&services.postgres).await?; Self::try_from(user) } } #[derive(Clone, Debug, Deserialize, Serialize)] #[serde(rename_all = "camelCase")] pub struct Person { #[serde(rename = "type")] kind: PersonType, preferred_username: String, id: ObjectId, inbox: Url, public_key: PublicKey, #[serde(skip_serializing_if = "Option::is_none")] outbox: Option, } #[async_trait] impl Object for User { #[doc = " App data type passed to handlers. Must be identical to"] #[doc = " [crate::config::FederationConfigBuilder::app_data] type."] type DataType = AppHandle; #[doc = " The type of protocol struct which gets sent over network to federate this database struct."] type Kind = Person; #[doc = " Error type returned by handler methods"] type Error = AppError; #[doc = " `id` field of the object"] fn id(&self) -> &Url { self.ap_id.inner() } #[doc = " Try to read the object with given `id` from local database."] #[doc = " Should return `Ok(None)` if not found."] async fn read_from_id( object_id: Url, data: &Data, ) -> Result, Self::Error> { todo!() } #[doc = " Convert database type to Activitypub type."] #[doc = " Called when a local object gets fetched by another instance over HTTP, or when an object"] #[doc = " gets sent in an activity."] async fn into_json(self, data: &Data) -> Result { Ok(Person { preferred_username: self.username.clone(), kind: Default::default(), id: self.ap_id.clone(), inbox: self.inbox.clone(), public_key: self.public_key(), outbox: self.outbox.clone(), }) } #[doc = " Verifies that the received object is valid."] #[doc = " You should check here that the domain of id matches `expected_domain`. Additionally you"] #[doc = " should perform any application specific checks."] #[doc = " It is necessary to use a separate method for this, because it might be used for activities"] #[doc = " like `Delete/Note`, which shouldn\'t perform any database write for the inner `Note`."] async fn verify( json: &Self::Kind, expected_domain: &Url, data: &Data, ) -> Result<(), Self::Error> { todo!() } #[doc = " Convert object from ActivityPub type to database type."] #[doc = " Called when an object is received from HTTP fetch or as part of an activity. This method"] #[doc = " should write the received object to database. Note that there is no distinction between"] #[doc = " create and update, so an `upsert` operation should be used."] async fn from_json(json: Self::Kind, data: &Data) -> Result { todo!() } } impl Actor for User { fn public_key_pem(&self) -> &str { &self.public_key } fn private_key_pem(&self) -> Option { self.private_key.clone() } fn inbox(&self) -> Url { self.inbox.clone() } }