use rusqlite::{Connection, OptionalExtension, params};
use serde::Serialize;
use std::collections::{HashMap, HashSet};
use super::{Author, Change, MAIN, author_name, current, key, parse_author};
use crate::check::Refusal;
use crate::store::{self, Result, fingerprint};
#[derive(Clone, Debug, PartialEq, Eq, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct Fact {
pub text: String,
pub author: Author,
pub author_bot: Option<String>,
pub message: Option<String>,
pub at: i64,
}
pub fn facts(content: &str) -> Vec<&str> {
lines(content).filter(|line| !line.starts_with('#')).map(store::unmarked).filter(|fact| !fact.is_empty()).collect()
}
fn lines(content: &str) -> impl Iterator<Item = &str> {
let body = match super::frontmatter(content) {
Some(rest) => {
let mut lines = rest.split_inclusive('\n');
let mut skipped = 0;
for line in lines.by_ref() {
skipped += line.len();
if line.trim() == "---" {
break;
}
}
&rest[skipped.min(rest.len())..]
}
None => content,
};
body.lines().map(str::trim).filter(|line| !line.is_empty())
}
pub fn fact_id(text: &str) -> i64 {
i64::from_str_radix(&fingerprint(text)[..13], 16).unwrap_or_default()
}
pub fn facts_of(conn: &Connection, bot: Option<&str>) -> Result<Vec<Fact>> {
let content = current(conn, bot, MAIN)?.unwrap_or_default();
let updated = super::get(conn, bot, MAIN)?.map_or(0, |f| f.updated_at);
let mut stmt =
conn.prepare("SELECT fingerprint, author, author_bot, message, at FROM memory_facts WHERE bot = ?1")?;
let origins: HashMap<String, (Author, Option<String>, Option<String>, i64)> = stmt
.query_map([key(bot)], |row| {
Ok((
row.get::<_, String>(0)?,
(parse_author(&row.get::<_, String>(1)?), row.get(2)?, row.get(3)?, row.get(4)?),
))
})?
.collect::<rusqlite::Result<_>>()?;
Ok(facts(&content)
.into_iter()
.map(|text| {
let (author, author_bot, message, at) =
origins.get(&fingerprint(text)).cloned().unwrap_or((Author::Computer, None, None, updated));
Fact { text: text.to_owned(), author, author_bot, message, at }
})
.collect())
}
pub fn set_origin(conn: &Connection, bot: Option<&str>, fact: &Fact) -> Result<()> {
conn.execute(
"INSERT INTO memory_facts (bot, fingerprint, author, author_bot, message, at) VALUES (?1, ?2, ?3, ?4, ?5, ?6)
ON CONFLICT (bot, fingerprint) DO UPDATE SET author = ?3, author_bot = ?4, message = ?5, at = ?6",
params![key(bot), fingerprint(&fact.text), author_name(fact.author), fact.author_bot, fact.message, fact.at],
)?;
Ok(())
}
pub(super) fn track(conn: &Connection, change: &Change<'_>, now: i64) -> Result<()> {
let before = current(conn, change.bot, MAIN)?.unwrap_or_default();
let before: HashSet<String> = facts(&before).into_iter().map(fingerprint).collect();
let after: HashSet<String> = change.content.map(facts).unwrap_or_default().into_iter().map(fingerprint).collect();
for gone in before.difference(&after) {
conn.execute("DELETE FROM memory_facts WHERE bot = ?1 AND fingerprint = ?2", params![key(change.bot), gone])?;
}
for new in after.difference(&before) {
conn.execute(
"INSERT OR IGNORE INTO memory_facts (bot, fingerprint, author, author_bot, message, at)
VALUES (?1, ?2, ?3, ?4, ?5, ?6)",
params![key(change.bot), new, author_name(change.author), change.author_bot, change.message, now],
)?;
}
Ok(())
}
pub fn admit(
conn: &Connection,
bot: Option<&str>,
before: Option<&str>,
after: Option<&str>,
asked: bool,
) -> Result<()> {
if asked {
return Ok(());
}
if !gained(after, before).is_empty() {
return Err(Refusal::NotAsked.into());
}
let bare = |line: &str| fingerprint(line.trim_start_matches('#'));
let had: HashSet<String> = before.into_iter().flat_map(every_line).map(bare).collect();
for line in after.into_iter().flat_map(every_line).map(|line| line.trim_start_matches('#')) {
if !had.contains(&fingerprint(line)) && store::forgotten(conn, bot, line)? {
return Err(Refusal::Forgotten.into());
}
}
Ok(())
}
pub(super) fn gained<'a>(from: Option<&str>, to: Option<&'a str>) -> Vec<&'a str> {
let mut had: HashMap<&str, usize> = HashMap::new();
for line in from.into_iter().flat_map(every_line) {
*had.entry(line).or_default() += 1;
}
to.into_iter()
.flat_map(every_line)
.filter(|line| match had.get_mut(line) {
Some(n) if *n > 0 => {
*n -= 1;
false
}
_ => true,
})
.collect()
}
fn every_line(content: &str) -> impl Iterator<Item = &str> {
content.lines().map(str::trim).filter(|line| !line.is_empty())
}
pub fn settle(
conn: &Connection,
bot: Option<&str>,
name: &str,
before: Option<&str>,
after: Option<&str>,
asked: bool,
) -> Result<()> {
if !asked {
return Ok(());
}
for fact in added(before, after) {
store::unbury(conn, bot, fact)?;
}
let kept: HashSet<String> = after.map(facts).unwrap_or_default().into_iter().map(fingerprint).collect();
let elsewhere = elsewhere(conn, bot, name)?;
let gone: Vec<&str> = before
.map(facts)
.unwrap_or_default()
.into_iter()
.filter(|fact| !kept.contains(&fingerprint(fact)) && !elsewhere.contains(&fingerprint(fact)))
.collect();
if gone.is_empty() {
return Ok(());
}
let mut messages = Vec::new();
if name == MAIN {
let mut stmt = conn.prepare("SELECT message FROM memory_facts WHERE bot = ?1 AND fingerprint = ?2")?;
for fact in &gone {
let message = stmt.query_row(params![key(bot), fingerprint(fact)], |row| row.get::<_, Option<String>>(0));
messages.extend(message.optional()?.flatten());
}
}
store::bury(conn, bot, gone, messages.iter().map(String::as_str))
}
fn added<'a>(before: Option<&str>, after: Option<&'a str>) -> Vec<&'a str> {
let had: HashSet<String> = before.map(facts).unwrap_or_default().into_iter().map(fingerprint).collect();
after.map(facts).unwrap_or_default().into_iter().filter(|fact| !had.contains(&fingerprint(fact))).collect()
}
fn elsewhere(conn: &Connection, bot: Option<&str>, name: &str) -> Result<HashSet<String>> {
let mut stmt = conn.prepare(
"SELECT v.content FROM memory_files f JOIN memory_file_versions v ON v.id = f.version
WHERE f.bot = ?1 AND f.name != ?2 AND f.hash != ''",
)?;
let contents = stmt.query_map(params![key(bot), name], |row| row.get::<_, Option<String>>(0))?;
let mut found = HashSet::new();
for content in contents {
found.extend(facts(&content?.unwrap_or_default()).into_iter().map(fingerprint));
}
Ok(found)
}