use super::types::ActorExt; use super::DOMAIN; use activitystreams::object::Image; use activitystreams::prelude::*; use anyhow::anyhow; use log::debug; use serde::Deserialize; use sqlx::SqlitePool; #[derive(Debug, Deserialize)] pub struct Actor { pub id: String, pub actor_object: String, pub is_follower: bool, pub display_name: Option, pub inbox: String, pub shared_inbox: Option, pub icon_url: Option, pub public_key_pem: Option, } #[derive(Debug, Deserialize)] pub struct Note { pub id: String, pub content: String, pub in_reply_to: String, pub conversation: String, pub published: String, // RFC 3339 encoded UTC date time pub actor_id: String, pub actor_display_name: Option, pub actor_icon_url: Option, pub digested: bool, } pub async fn fetch_cached_actor(id: &str, pool: &SqlitePool) -> Option { sqlx::query_as!(Actor, "select * from actors where id = ?", id) .fetch_optional(pool) .await .ok() .flatten() } pub async fn add_cached_actor(actor: &ActorExt, pool: &SqlitePool) -> anyhow::Result<()> { let id = actor .id_unchecked() .map(|iri| iri.as_str()) .ok_or_else(|| anyhow!("missing actor id"))?; debug!("adding cached actor {:?}", id); let actor_json = serde_json::to_string(&actor)?; let mut display_name = actor .name() .and_then(|names| names.as_single_xsd_string().map(|s| s.to_owned())); if display_name.as_ref().map_or(true, |s| s.is_empty()) { display_name = actor.preferred_username().map(|s| s.to_owned()); } let shared_inbox = actor .endpoints()? .and_then(|endpoints| endpoints.shared_inbox) .map(|s| s.as_str()); let inbox = actor.inbox()?.as_str(); // jesus christ this is ridiculously elaborate let icon = actor .icon() .and_then(|icon| icon.as_one()) .and_then(|icon| Image::from_any_base(icon.clone()).ok().flatten()) .and_then(|icon| { icon.url() .and_then(|urls| urls.as_single_id().map(|s| s.to_string())) }); let pub_key_pem = &actor.ext_one.public_key.public_key_pem; if fetch_cached_actor(id, pool).await.is_some() { sqlx::query!("update actors set actor_object = ?2, display_name = ?3, inbox = ?4, shared_inbox = ?5, icon_url = ?6, public_key_pem = ?7 where id = ?1", id, actor_json, display_name, inbox, shared_inbox, icon, pub_key_pem) .execute(pool) .await?; } else { sqlx::query!("insert into actors (id, actor_object, display_name, inbox, icon_url, public_key_pem) values (?1, ?2, ?3, ?4, ?5, ?6)", id, actor_json, display_name, inbox, icon, pub_key_pem) .execute(pool) .await?; } Ok(()) } pub async fn get_followers(pool: &SqlitePool) -> anyhow::Result> { Ok( sqlx::query_as!(Actor, "select * from actors where is_follower = 1") .fetch_all(pool) .await?, ) } pub async fn get_article_for_path(path: &str, pool: &SqlitePool) -> Option { let trailing_slash = if path.ends_with("/") { "" } else { "/" }; let permalink = format!("https://{}{}{}", *DOMAIN, path, trailing_slash); sqlx::query!( "select article_object from articles where id = ?", permalink ) .fetch_optional(pool) .await .ok() .flatten() .map(|row| row.article_object) } pub async fn get_article_for_conversation(conv: &str, pool: &SqlitePool) -> Option { sqlx::query!( "select article_object from articles where conversation = ?", conv ) .fetch_optional(pool) .await .ok() .flatten() .map(|row| row.article_object) } pub async fn get_unfederated_articles(pool: &SqlitePool) -> anyhow::Result> { Ok( sqlx::query!("select id, article_object from articles where has_federated = 0") .fetch_all(pool) .await? .into_iter() .map(|row| (row.id, row.article_object)) .collect(), ) } pub async fn set_has_federated( article_id: &str, has_federated: bool, pool: &SqlitePool, ) -> anyhow::Result<()> { sqlx::query!( "update articles set has_federated = ?1 where id = ?2", has_federated, article_id ) .execute(pool) .await?; Ok(()) } pub async fn get_conversation_for_article_id( id: &str, pool: &SqlitePool, ) -> anyhow::Result { sqlx::query!("select conversation from articles where id = ?1", id) .fetch_optional(pool) .await? .map(|row| row.conversation) .ok_or(anyhow!("missing conversation for article")) } pub async fn store_note( id: &str, content: &str, in_reply_to: &str, conversation: &str, published: &str, actor_id: &str, pool: &SqlitePool, ) -> anyhow::Result<()> { sqlx::query!("insert into notes (id, content, in_reply_to, conversation, published, actor_id) values (?1, ?2, ?3, ?4, ?5, ?6)", id, content, in_reply_to, conversation, published, actor_id) .execute(pool) .await?; Ok(()) } pub async fn set_follow_state( actor_id: &str, is_follower: bool, pool: &SqlitePool, ) -> anyhow::Result<()> { sqlx::query!( "update actors set is_follower = ?1 where id = ?2", is_follower, actor_id ) .execute(pool) .await?; Ok(()) } pub async fn delete_note(note_id: &str, actor_id: &str, pool: &SqlitePool) -> anyhow::Result<()> { sqlx::query!( "delete from notes where id = ?1 and actor_id = ?2", note_id, actor_id ) .execute(pool) .await?; Ok(()) } pub async fn get_notes(conversation: &str, pool: &SqlitePool) -> Result, sqlx::Error> { sqlx::query_as!(Note, "select notes.*, actors.display_name as actor_display_name, actors.icon_url as actor_icon_url from notes inner join actors on notes.actor_id = actors.id where conversation = ?1", conversation) .fetch_all(pool) .await } pub async fn get_undigested_notes(pool: &SqlitePool) -> Result, sqlx::Error> { Ok( sqlx::query!("select id, conversation from notes where digested = 0") .fetch_all(pool) .await? .into_iter() .map(|row| (row.id, row.conversation)) .collect(), ) } pub async fn set_note_digested(pool: &SqlitePool, id: &str) -> sqlx::Result<()> { sqlx::query!("update notes set digested = 1 where id = ?1", id) .execute(pool) .await?; Ok(()) }