pub mod followers; use std::fmt::Display; use activitypub_federation::{ activity_queue::queue_activity, activity_sending::SendActivityTask, config::Data, fetch::object_id::ObjectId, http_signatures::generate_actor_keypair, kinds::actor::{ApplicationType, GroupType, OrganizationType, PersonType, ServiceType}, protocol::{context::WithContext, public_key::PublicKey}, traits::{Activity, Actor, Object}, }; use async_trait::async_trait; use serde::{Deserialize, Serialize}; use sqlx::types::time::OffsetDateTime; 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 struct User { pub id: String, pub username: String, pub ap_id: ObjectId, pub private_key: Option, pub description: Option, pub avatar_url: Option, pub public_key: String, pub inbox: Url, pub outbox: Option, pub user_type: UserType, } pub struct DbUser { pub id: String, pub description: Option, pub username: String, pub ap_id: String, pub private_key: Option, pub public_key: String, pub inbox: String, pub outbox: Option, pub avatar_url: Option, pub local: bool, pub updated_at: OffsetDateTime, pub created_at: OffsetDateTime, pub user_type: UserType, } #[derive(Deserialize, Serialize, Debug, Clone, PartialEq)] #[serde(rename_all = "PascalCase")] #[serde(untagged)] pub enum UserType { Person(PersonType), Application(ApplicationType), Group(GroupType), Organization(OrganizationType), Service(ServiceType), } impl Display for UserType { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { write!( f, "{}", match self { UserType::Person(person) => person.to_string(), UserType::Application(application_type) => application_type.to_string(), UserType::Group(group_type) => group_type.to_string(), UserType::Organization(organization_type) => organization_type.to_string(), UserType::Service(service_type) => service_type.to_string(), } .to_uppercase() ) } } impl From for UserType { fn from(value: String) -> Self { match value.to_lowercase().as_str() { "person" => Self::Person(PersonType::Person), "application" => Self::Application(ApplicationType::Application), "group" => Self::Group(GroupType::Group), "organization" => Self::Organization(OrganizationType::Organization), "service" => Self::Service(ServiceType::Service), _ => unreachable!("{}", value), } } } 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, }, description: value.description, avatar_url: value.avatar_url, user_type: value.user_type, }) } } 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(); let kind = UserType::Service(ServiceType::Service); 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, user_type) values ($1, $2, $3, $4, $5, $6, $7, $8, $9) returning *", id, username, stub, keys.private_key, keys.public_key, &format!("{stub}/inbox"), &format!("{stub}/outbox"), true, kind.to_string(), ).fetch_one(&services.postgres).await?; Self::try_from(user) } pub(crate) async fn send( &self, activity: A, recipients: Vec, use_queue: bool, data: &Data, ) -> Result<(), AppError> where A: Activity + Serialize + std::fmt::Debug + Send + Sync, ::Error: From + From, { let activity = WithContext::new_default(activity); // Send through queue in some cases and bypass it in others to test both code paths if use_queue { queue_activity(&activity, self, recipients, data).await?; } else { let sends = SendActivityTask::prepare(&activity, self, recipients, data).await?; for send in sends { send.sign_and_send(data).await?; } } Ok(()) } } #[derive(Clone, Debug, Deserialize, Serialize)] #[serde(rename_all = "camelCase")] pub struct Person { #[serde(rename = "type")] kind: UserType, preferred_username: String, id: ObjectId, inbox: Url, public_key: PublicKey, #[serde(skip_serializing_if = "Option::is_none")] outbox: Option, followers: Url, following: Url, #[serde(skip_serializing_if = "Option::is_none")] summary: Option, #[serde(skip_serializing_if = "Option::is_none")] image: 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> { let id = object_id.as_str(); let result = sqlx::query_as!(DbUser, "select * from account where ap_id = $1", id) .fetch_optional(&data.services.postgres) .await?; let user = match result { Some(user) => Some(User::try_from(user)?), None => None, }; Ok(user) } #[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: self.user_type.clone(), id: self.ap_id.clone(), inbox: self.inbox.clone(), public_key: self.public_key(), outbox: self.outbox.clone(), followers: Url::parse(&format!("{}/followers", self.ap_id))?, following: Url::parse(&format!("{}/following", self.ap_id))?, summary: self.description, image: match self.avatar_url { Some(ref v) => Some(Url::parse(v)?), None => None, }, }) } #[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 { Ok(Self { id: todo!(), username: todo!(), ap_id: todo!(), private_key: todo!(), description: todo!(), avatar_url: todo!(), public_key: todo!(), inbox: todo!(), outbox: todo!(), user_type: 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() } }