#![deny(missing_docs)]
#![forbid(unsafe_code)]
use std::{path::Path, sync::Arc};
use anyhow::Context as _;
use chrono::Utc;
use kcode_kweb_db::{Config, KwebDb, NodeData, NodeId, Owner, Provenance};
use kcode_kweb_manager::{CreateProvenance, KwebManager, NodeContents, NodeWrite};
use kcode_server_object_envelopes::StoredProvenance;
use rusqlite::{Connection, OptionalExtension, params};
use tokio::sync::Mutex;
use uuid::Uuid;
#[derive(Clone, Copy, Debug)]
pub struct SystemRoots {
pub user: NodeId,
pub kennedy: NodeId,
}
pub fn open(
kweb_root: &Path,
config: Config,
identity_database: &Path,
) -> anyhow::Result<(KwebManager, SystemRoots)> {
let mut identity = Connection::open(identity_database).with_context(|| {
format!(
"opening identity database {} for system roots",
identity_database.display()
)
})?;
identity.execute_batch(
"PRAGMA foreign_keys=ON; PRAGMA journal_mode=WAL; PRAGMA busy_timeout=15000;
CREATE TABLE IF NOT EXISTS kmap_system_roots (
role TEXT PRIMARY KEY CHECK(role IN ('user','kennedy')),
root_node_id TEXT NOT NULL UNIQUE CHECK(length(root_node_id)=8),
created_at TEXT NOT NULL
);",
)?;
let database = KwebDb::open(kweb_root, config).map_err(anyhow::Error::new)?;
let roots = match (
system_root(&identity, "user")?,
system_root(&identity, "kennedy")?,
) {
(Some(user), Some(kennedy)) => SystemRoots {
user: user
.parse::<NodeId>()
.with_context(|| format!("invalid stored user root node ID {user:?}"))?,
kennedy: kennedy
.parse::<NodeId>()
.with_context(|| format!("invalid stored Kennedy root node ID {kennedy:?}"))?,
},
(None, None) => create_system_roots(&database, &mut identity)?,
_ => anyhow::bail!("the system-root directory contains only one of its two required roles"),
};
database.get_node(roots.user).map_err(anyhow::Error::new)?;
database
.get_node(roots.kennedy)
.map_err(anyhow::Error::new)?;
let kmap = KwebManager::open(database, identity_database)
.map_err(anyhow::Error::new)
.context("opening Kmap application service")?;
Ok((kmap, roots))
}
fn create_system_roots(
database: &KwebDb,
identity: &mut Connection,
) -> anyhow::Result<SystemRoots> {
let mut transaction = database
.start_transaction(Provenance {
author: "system-bootstrap".into(),
source: "system-bootstrap".into(),
source_created_at: Utc::now(),
data: "Initial Kweb system-root bootstrap.".into(),
})
.map_err(anyhow::Error::new)?;
let user = transaction
.create_node(root_data(
"Initial User Root",
"The root of the primary user's Kmap knowledge.",
"This root anchors durable knowledge associated with the primary Kennedy user.",
))
.map_err(anyhow::Error::new)?;
let kennedy = transaction
.create_node(root_data(
"Kennedy's Root",
"The root of Kennedy's own Kmap knowledge.",
"This is Kennedy's root node. It anchors Kennedy's own durable knowledge and learned lessons in the Kmap.",
))
.map_err(anyhow::Error::new)?;
transaction.finalize().map_err(anyhow::Error::new)?;
let sql = identity.transaction()?;
let now = Utc::now().to_rfc3339();
sql.execute(
"INSERT INTO kmap_system_roots(role,root_node_id,created_at) VALUES('user',?1,?2)",
params![user.to_string(), now],
)?;
sql.execute(
"INSERT INTO kmap_system_roots(role,root_node_id,created_at) VALUES('kennedy',?1,?2)",
params![kennedy.to_string(), now],
)?;
sql.commit()?;
Ok(SystemRoots { user, kennedy })
}
fn system_root(identity: &Connection, role: &str) -> anyhow::Result<Option<String>> {
Ok(identity
.query_row(
"SELECT root_node_id FROM kmap_system_roots WHERE role=?1",
[role],
|row| row.get(0),
)
.optional()?)
}
fn root_data(short_name: &str, short_description: &str, long_description: &str) -> NodeData {
NodeData {
short_name: short_name.into(),
short_description: short_description.into(),
long_description: long_description.into(),
owner: Owner::SelfNode,
fixed_connections: Vec::new(),
recent_connections: Vec::new(),
objects: Vec::new(),
}
}
#[derive(Clone)]
pub struct DirectoryRoots {
kmap: KwebManager,
directory: Arc<kcode_telegram_identity::Directory>,
web_user_handle: String,
system_user_root: NodeId,
writer: Arc<Mutex<()>>,
}
impl DirectoryRoots {
pub fn new(
kmap: KwebManager,
directory: Arc<kcode_telegram_identity::Directory>,
web_user_handle: impl Into<String>,
system_user_root: NodeId,
writer: Arc<Mutex<()>>,
) -> Self {
Self {
kmap,
directory,
web_user_handle: web_user_handle.into(),
system_user_root,
writer,
}
}
pub async fn reconcile_pending(&self) -> anyhow::Result<()> {
for user in self
.directory
.provisioning_users()
.map_err(directory_error)?
{
let handle = user.handle;
if handles_match(&handle, &self.web_user_handle) {
self.directory
.complete_handle_root(&handle, self.system_user_root)
.map_err(directory_error)?;
} else {
let _guard = self.writer.lock().await;
let root = self.create_root(None)?;
self.directory
.complete_handle_root(&handle, root)
.map_err(directory_error)?;
}
}
for group in self
.directory
.provisioning_groups()
.map_err(directory_error)?
{
let _guard = self.writer.lock().await;
let root = self.create_root(Some("Group Root"))?;
self.directory
.complete_group_root(&group.group_id, root)
.map_err(directory_error)?;
}
Ok(())
}
pub async fn ensure_user(
&self,
telegram_user_id: i64,
) -> anyhow::Result<kcode_telegram_identity::User> {
let user = self
.directory
.user(telegram_user_id)
.map_err(directory_error)?;
if user.root_ready {
return Ok(user);
}
let _guard = self.writer.lock().await;
let user = self
.directory
.user(telegram_user_id)
.map_err(directory_error)?;
if user.root_ready {
return Ok(user);
}
let root = self.create_root(None)?;
self.directory
.complete_user_root(telegram_user_id, root)
.map_err(directory_error)
}
pub async fn ensure_group(
&self,
group_id: &str,
) -> anyhow::Result<kcode_telegram_identity::Group> {
let group = self.directory.group(group_id).map_err(directory_error)?;
if group.root_ready {
return Ok(group);
}
let _guard = self.writer.lock().await;
let group = self.directory.group(group_id).map_err(directory_error)?;
if group.root_ready {
return Ok(group);
}
let root = self.create_root(Some("Group Root"))?;
self.directory
.complete_group_root(group_id, root)
.map_err(directory_error)
}
fn create_root(&self, short_name: Option<&str>) -> anyhow::Result<NodeId> {
let (short_name, short_description, long_description) = root_metadata(short_name);
let source_created_at = Utc::now();
let provenance_id = self
.kmap
.create_provenance(CreateProvenance {
idempotency_id: idempotency_id(),
value: StoredProvenance {
data: "Automatically provisioned Kmap root node.".into(),
source: "system-bootstrap".into(),
source_created_at,
artifacts: Vec::new(),
},
storage_provenance: Provenance {
author: "kennedy-provenance".into(),
source: "system-bootstrap".into(),
source_created_at,
data: "Stored provenance for a Kennedy Kmap mutation.".into(),
},
})
.map_err(kmap_error)?;
self.kmap
.create_node(NodeWrite {
idempotency_id: idempotency_id(),
provenance_id,
author: "system-bootstrap".into(),
contents: NodeContents {
short_name: short_name.into(),
short_description: short_description.into(),
long_description: long_description.into(),
owner: Owner::SelfNode,
fixed_connections: Vec::new(),
recent_connections: Vec::new(),
},
})
.map(|node| node.id)
.map_err(kmap_error)
}
}
fn handles_match(left: &str, right: &str) -> bool {
left.trim_start_matches('@')
.eq_ignore_ascii_case(right.trim_start_matches('@'))
}
fn root_metadata(short_name: Option<&str>) -> (&str, &'static str, &'static str) {
match short_name {
Some("Group Root") => (
"Group Root",
"The root of this Telegram group's shared Kmap knowledge.",
"This root anchors durable knowledge shared in this Telegram group.",
),
Some(short_name) => (
short_name,
"An automatically provisioned Kmap root.",
"This root anchors durable Kmap knowledge.",
),
None => (
"User Root",
"The root of this Telegram user's Kmap knowledge.",
"This root anchors durable knowledge associated with this Telegram user.",
),
}
}
fn idempotency_id() -> String {
Uuid::new_v4().simple().to_string()
}
fn kmap_error(error: kcode_kweb_manager::Error) -> anyhow::Error {
anyhow::anyhow!(error.to_string())
}
fn directory_error(error: kcode_telegram_identity::Error) -> anyhow::Error {
anyhow::anyhow!(error.message().to_owned())
}
#[cfg(test)]
mod tests {
use super::*;
use kcode_kweb_db::{NoopGossip, WriterId};
fn config() -> Config {
let signing_key = rand::random::<[u8; 32]>();
Config {
signing_key,
writers_by_priority: vec![WriterId::from_signing_key(&signing_key)],
gossip: Arc::new(NoopGossip),
}
}
#[test]
fn initializes_canonical_system_roots_and_hands_database_to_kmap() {
let directory = std::env::temp_dir().join(format!("kennedy-roots-{}", Uuid::new_v4()));
std::fs::create_dir_all(&directory).unwrap();
let (kmap, roots) = open(
&directory.join("kweb"),
config(),
&directory.join("users.sqlite3"),
)
.unwrap();
assert_eq!(roots.user.to_string().len(), 8);
assert_eq!(roots.kennedy.to_string().len(), 8);
kmap.get_node(roots.user).unwrap();
kmap.get_node(roots.kennedy).unwrap();
drop(kmap);
std::fs::remove_dir_all(directory).unwrap();
}
#[test]
fn configured_web_handle_matching_is_case_and_at_sign_insensitive() {
assert!(handles_match("@Taek42", "taek42"));
assert!(!handles_match("@other", "@taek42"));
}
#[test]
fn provisioned_roots_keep_complete_application_descriptions() {
let (_, user_short, user_long) = root_metadata(None);
let (_, group_short, group_long) = root_metadata(Some("Group Root"));
assert!(user_short.contains("Telegram user"));
assert!(user_long.contains("associated with this Telegram user"));
assert!(group_short.contains("Telegram group"));
assert!(group_long.contains("shared in this Telegram group"));
}
}