use openssl::pkey::{PKey, Private};
use serde::{Deserialize, Serialize};
use http::Uri;
use url::Url;
use rusqlite::Error::QueryReturnedNoRows;
use uuid::Uuid;
use crate::local::notes::get_notes;
use crate::activitypub::{AtContext, Context};
use crate::activitypub::objects::activities::create::CreateActivity;
use crate::activitypub::objects::collections::{OrderedCollection, OrderedCollectionPage, OutboxPage, PageOrLink};
use crate::database::Database;
use crate::{get_server_internal_actor, query_row, query_row_custom};
use crate::activitypub::objects::actor::{Actor, ActorType, Icon, PublicKey};
use crate::activitypub::signature::generate_pkey_string;
use crate::server::ap_request::UnverifiedApRequest;
use crate::error::{bad_request, MainsailError};
use crate::local::feeds::{Meta, Post, TimelineEntry};
use crate::local::{InternalResult, notes};
use crate::server::response;
use crate::server::response::ServerResult;
#[derive(Clone, Debug, Serialize)]
pub struct LocalActorStub {
pub id: String,
pub domain: String,
#[serde(skip)] pub pkey: PKey<Private>,
}
impl LocalActorStub {
pub fn get(db: &Database, id: &str) -> Option<Self> {
let domain = db.query_row(
"SELECT value FROM globals WHERE key = 'domain'",
[],
|row| row.get(0),
).ok()?;
let private_key_pem: String = db.query_row(
"SELECT private_key_pem FROM actors WHERE id = ?1",
[&id],
|row| row.get(0),
).ok()?;
let pkey = PKey::private_key_from_pem(private_key_pem.as_bytes()).ok()?;
let current_profile = LocalActorStub { id: id.to_owned(), pkey, domain };
Some(current_profile)
}
}
pub fn new_local_actor(db: &Database, preferred_username: &str, display_name: &str, nickname: &str) -> InternalResult<LocalActorStub> {
let domain: String = db.query_row("SELECT value FROM globals WHERE key = 'domain'", (), |row| row.get(0))?;
let pkey = generate_pkey_string()?;
let uuid = Uuid::new_v4().to_string();
let id = id(&domain, &uuid);
db.execute(
"INSERT INTO actors (id, uuid, preferred_username, display_name, nickname, private_key_pem)
VALUES (?1, ?2, ?3, ?4, ?5, ?6)",
(&id, uuid, preferred_username, display_name, nickname, &pkey),
)?;
LocalActorStub::get(db, &id)
.ok_or(MainsailError::InternalServerError(format!("Failed to get actor {id} after creating it")))
}
pub fn new_local_actor_with_uuid(db: &Database, uuid: &str, preferred_username: &str, display_name: &str, nickname: &str) -> InternalResult<String> {
let domain: String = db.query_row("SELECT value FROM globals WHERE key = 'domain'", (), |row| row.get(0))?;
let pkey = generate_pkey_string()?;
let id = id(&domain, uuid);
db.execute(
"INSERT INTO actors (id, uuid, preferred_username, display_name, nickname, private_key_pem)
VALUES (?1, ?2, ?3, ?4, ?5, ?6)",
(&id, uuid, preferred_username, display_name, nickname, &pkey),
)?;
Ok(id)
}
fn id(domain: &str, uuid: &str) -> String {
format!("https://{domain}/_mainsail/actors/{uuid}")
}
fn url(domain: &str, preferred_username: &str) -> String {
format!("https://{domain}/@{preferred_username}")
}
pub fn inbox_url(domain: &str) -> String {
format!("https://{domain}/inbox")
}
fn outbox_url(domain: &str, preferred_username: &str) -> String {
format!("https://{domain}/@{preferred_username}/outbox")
}
fn following_url(domain: &str, preferred_username: &str) -> String {
format!("https://{domain}/@{preferred_username}/following")
}
fn followers_url(domain: &str, preferred_username: &str) -> String {
format!("https://{domain}/@{preferred_username}/followers")
}
pub fn get_outbox(db: &Database, actor_id: &str) -> InternalResult<OrderedCollection<String>> {
let actor = get_actor_by_id(db, actor_id)?;
let profile = query_row_custom!(
db,
Profile { total_items: i64 },
"SELECT count(*) as total_items FROM notes WHERE actor_id = ?",
[actor_id]
)?;
let total_items = profile.total_items as u64;
let first_page = format!("{}?page=1", actor.outbox);
let last_page = format!("{}?page=1", actor.outbox);
let outbox = OrderedCollection::new(actor.outbox, first_page, last_page, total_items);
Ok(outbox)
}
pub fn get_outbox_page(db: &Database, actor_id: &str, _page_num: usize) -> InternalResult<OutboxPage> {
let actor = get_actor_by_id(db, actor_id)?;
let notes = get_notes(db, actor_id)?;
let page_url = format!("{}?page=1", actor.outbox);
let items: Vec<CreateActivity> = notes.into_iter()
.map(|internal_note| { internal_note.into_note() })
.map(|note| { note.into_create() })
.collect();
let page = OrderedCollectionPage {
context: AtContext::Context(Context::ActivityStreams),
next: None,
prev: Some(Box::new(PageOrLink::Link(page_url.to_owned()))),
ordered_items: items,
};
Ok(page)
}
pub fn get_actor_id_from_uuid(db: &Database, uuid: &str) -> InternalResult<Option<String>> {
let actor = query_row!(
db,
Profile { id: String },
"FROM actors WHERE uuid = ?1",
[uuid]
);
if let Err(QueryReturnedNoRows) = actor {
return Ok(None);
}
Ok(Some(actor?.id))
}
pub fn get_profile_id_from_url(db: &Database, url: &str) -> InternalResult<Option<String>> {
let uri: Uri = url.parse().map_err(|_| bad_request("Invalid URI provided"))?;
let profile_id = uri.path().split('/').next_back();
let profile = query_row!(
db,
Profile { uuid: String },
"FROM actors WHERE uuid = ?1",
[profile_id]
);
if let Err(QueryReturnedNoRows) = profile {
return Ok(None);
}
Ok(Some(profile?.uuid))
}
pub async fn get_followers(req: UnverifiedApRequest, actor: Actor) -> ServerResult {
let req = req.authenticate().await?;
let mut query = req.db.prepare("SELECT sender_id FROM followers WHERE actor_id = ?1")?;
let followers = query.query_map([actor.id], |row| row.get(0))?;
let followers: Vec<String> = followers.collect::<Result<_, _>>()?;
let url = followers_url(&req.domain, &actor.preferred_username);
let collection = OrderedCollection::new_from_list(url, followers);
let body = serde_json::json!(collection).to_string();
response::send(body)
}
pub async fn get_following(req: UnverifiedApRequest, actor: Actor) -> ServerResult {
let req = req.authenticate().await?;
let mut query = req.db.prepare("SELECT followed_actor_id FROM following WHERE actor_id = ?1")?;
let following = query.query_map([actor.id], |row| row.get(0))?;
let following: Vec<String> = following.collect::<Result<_, _>>()?;
let url = following_url(&req.domain, &actor.preferred_username);
let collection = OrderedCollection::new_from_list(url, following);
let body = serde_json::json!(collection).to_string();
response::send(body)
}
pub fn get_server_id(domain: &str) -> String {
format!("https://{domain}/_mainsail/server-actor")
}
pub fn get_server_actor(db: &Database) -> InternalResult<Actor> {
let server = get_server_internal_actor(db)?;
let domain = &server.domain;
let pkey = server.pkey.private_key_to_pem_pkcs8()?;
let private_key_pem = String::from_utf8(pkey).map_err(|e| MainsailError::GenericError(e.into()))?;
let id = get_server_id(domain);
let public_key = PublicKey::new(&id, &private_key_pem);
let context = vec![Context::ActivityStreams, Context::SecurityV1];
let context = AtContext::Collection(context);
let actor = Actor {
context: Some(context),
id: id.to_owned(),
url: id.to_owned(),
name: None,
actor_type: ActorType::Person,
summary: Some("This is a server actor for a Mainsail-based server".to_owned()),
preferred_username: "MainsailInternal".to_owned(),
icon: None,
inbox: id.clone(),
outbox: id.clone(),
followers: None,
following: None,
public_key,
};
Ok(actor)
}
pub fn get_actor_notes(db: &Database, actor_uuid: &str) -> InternalResult<Vec<TimelineEntry>> {
let actor = get_actor_by_uuid(db, actor_uuid)?;
let notes = notes::get_notes(db, &actor.id)?;
let items = notes.into_iter().map(|note| {
let meta = Meta { internal_id: note.uuid };
let item = Post::new(note.note, actor.to_owned(), meta);
TimelineEntry::Post(item.into())
});
Ok(items.collect())
}
enum ActorColumnParameter {
Id,
Uuid,
PreferredUsername,
}
impl ActorColumnParameter {
pub fn as_str(&self) -> &str {
match self {
ActorColumnParameter::Id => "id",
ActorColumnParameter::Uuid => "uuid",
ActorColumnParameter::PreferredUsername => "preferred_username"
}
}
}
pub fn get_actor_by_id(db: &Database, id: &str) -> InternalResult<Actor> {
get_actor_internal(db, ActorColumnParameter::Id, id)
}
pub fn get_actor_by_uuid(db: &Database, uuid: &str) -> InternalResult<Actor> {
get_actor_internal(db,ActorColumnParameter::Uuid, uuid)
}
pub fn get_actor_by_preferred_username(db: &Database, preferred_username: &str) -> Option<Actor> {
log::debug!("Getting actor by preferred_username: {preferred_username}");
get_actor_internal(db, ActorColumnParameter::PreferredUsername, preferred_username).ok()
}
fn get_actor_internal(db: &Database, param: ActorColumnParameter, value: &str) -> InternalResult<Actor> {
#[derive(Serialize, Deserialize)]
struct DbActor {
id: String,
uuid: String,
preferred_username: String,
display_name: String,
icon_url: Option<String>,
nickname: String,
following_count: i64,
follower_count: i64,
private_key_pem: String,
}
let parameter = param.as_str();
let actor = db.query_row(
&format!("
SELECT
id,
uuid,
preferred_username,
display_name,
icon_url,
nickname,
(SELECT count(*) FROM following WHERE actor_id = id) as following_count,
(SELECT count(*) FROM followers WHERE actor_id = id) as follower_count,
private_key_pem
FROM actors
WHERE {parameter} = ?2"),
[¶meter, &value],
|row| {
let profile = DbActor {
id: row.get("id")?,
uuid: row.get("uuid")?,
preferred_username: row.get("preferred_username")?,
display_name: row.get("display_name")?,
icon_url: row.get("icon_url")?,
nickname: row.get("nickname")?,
following_count: row.get("following_count")?,
follower_count: row.get("follower_count")?,
private_key_pem: row.get("private_key_pem")?,
};
Ok(profile)
});
let actor = match actor {
Ok(x) => x,
Err(QueryReturnedNoRows) => return Err(MainsailError::NotFound),
Err(e) => return Err(e.into())
};
let id = actor.id.parse::<Url>().map_err(|_| {
MainsailError::InvalidActorUri(format!("Failed to parse {} as URL", actor.id))
})?;
let domain = id.domain().ok_or_else(|| {
MainsailError::InvalidActorUri(format!("No domain found for {id}"))
})?;
let inbox = inbox_url(domain);
let outbox = outbox_url(domain, &actor.preferred_username);
let following = following_url(domain, &actor.preferred_username);
let followers = followers_url(domain, &actor.preferred_username);
let public_key = PublicKey::new(&actor.id, &actor.private_key_pem);
let icon = actor.icon_url.map(Icon::new_from_url);
let context = vec![Context::ActivityStreams, Context::SecurityV1];
let context = AtContext::Collection(context);
let actor = Actor {
context: Some(context),
id: actor.id,
url: url(domain, &actor.preferred_username),
name: Some(actor.display_name),
actor_type: ActorType::Person,
summary: None, preferred_username: actor.preferred_username,
icon,
inbox,
outbox,
followers: Some(followers),
following: Some(following),
public_key,
};
Ok(actor)
}