mascarpone/src/switchboard.rs

148 lines
4.2 KiB
Rust
Raw Normal View History

use maud::{Markup, PreEscaped};
use regex::Regex;
use sqlx::QueryBuilder;
use sqlx::sqlite::SqlitePool;
use std::collections::HashSet;
use crate::AppError;
use crate::db::DbId;
pub struct Switchboard {
trie: radix_trie::Trie<String, String>,
}
struct Mentionable {
text: String,
uri: String,
}
#[derive(Debug, PartialEq, Eq, Hash)]
pub struct Mention {
pub entity_id: DbId,
pub entity_type: DbId,
pub url: String,
pub input_text: String,
pub byte_range_start: DbId,
pub byte_range_end: DbId,
}
// must match the constants in trigger definitions in
// migrations/each_user/0010_more-mentions.sql (or future migrations)
#[derive(Copy, Clone)]
pub enum MentionHostType {
JournalEntry,
ContactTextBody,
ContactLivesWith,
}
#[derive(Copy, Clone)]
pub struct MentionHost<'a> {
pub entity_id: DbId,
pub entity_type: DbId,
pub input: &'a String,
}
impl MentionHost<'_> {
pub fn format<'a>(
self: &Self,
mentions: impl IntoIterator<Item = &'a Mention>,
) -> Result<Markup, AppError> {
let mut out = self.input.clone();
for mention in mentions.into_iter() {
out.replace_range(
(mention.byte_range_start as usize)..(mention.byte_range_end as usize),
&format!("[{}]({})", mention.input_text, mention.url),
);
}
Ok(PreEscaped(markdown::to_html(&out)))
}
pub async fn format_pool(self: &Self, pool: &SqlitePool) -> Result<Markup, AppError> {
let mentions = sqlx::query_as!(
Mention,
"select * from mentions
where entity_id = $1 and entity_type = $2
order by byte_range_start desc",
self.entity_id,
self.entity_type
)
.fetch_all(pool)
.await?;
self.format(&mentions)
}
}
impl Switchboard {
pub async fn new(pool: &SqlitePool) -> Result<Self, AppError> {
let mut trie = radix_trie::Trie::new();
let mentionables = sqlx::query_as!(
Mentionable,
"select name as text, '/contact/'||contact_id as uri from (
select contact_id, name, count(name) as ct from names group by name
) where ct = 1
union
select distinct name as text, '/group/'||slug as uri from groups",
)
.fetch_all(pool)
.await?;
for mentionable in mentionables {
trie.insert(mentionable.text, mentionable.uri);
}
Ok(Switchboard { trie })
}
pub fn remove(self: &mut Self, text: &String) {
self.trie.remove(text);
}
pub fn add_mentionable(self: &mut Self, text: String, uri: String) {
self.trie.insert(text, uri);
}
pub fn extract_mentions<'a>(&self, host: impl Into<MentionHost<'a>>) -> HashSet<Mention> {
let host: MentionHost = host.into();
let name_re = Regex::new(r"\[\[(.+?)\]\]").unwrap();
name_re
.captures_iter(host.input)
.map(|caps| {
let range = caps.get_match().range();
self.trie.get(&caps[1]).map(|url| Mention {
entity_id: host.entity_id,
entity_type: host.entity_type,
url: url.to_string(),
input_text: caps[1].to_string(),
byte_range_start: DbId::try_from(range.start).unwrap(),
byte_range_end: DbId::try_from(range.end).unwrap(),
})
})
.filter(|o| o.is_some())
.map(|o| o.unwrap())
.collect()
}
}
pub async fn insert_mentions<'a>(
mentions: impl IntoIterator<Item = &'a Mention>,
pool: &SqlitePool,
) -> Result<(), AppError> {
let mut qb = QueryBuilder::<sqlx::Sqlite>::new(
"insert into mentions (
entity_id, entity_type, url, input_text,
byte_range_start, byte_range_end) ",
);
qb.push_values(mentions, |mut b, mention| {
b.push_bind(mention.entity_id)
.push_bind(mention.entity_type)
.push_bind(&mention.url)
.push_bind(&mention.input_text)
.push_bind(mention.byte_range_start)
.push_bind(mention.byte_range_end);
});
qb.build().execute(pool).await?;
Ok(())
}