fix(error): migrate session submodules + btmsg/bttask backends to AppError
- session/*.rs (sessions, layout, settings, ssh, agents, metrics, anchors) now return Result<T, AppError> internally, not just at command boundary - btmsg.rs and bttask.rs backends migrated to AppError::Database - 116 cargo tests passing
This commit is contained in:
parent
eb04e7e5b5
commit
f19b69f018
11 changed files with 264 additions and 255 deletions
|
|
@ -4,6 +4,7 @@
|
|||
|
||||
use rusqlite::{params, Connection, OpenFlags};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use crate::error::AppError;
|
||||
use std::path::PathBuf;
|
||||
use std::sync::OnceLock;
|
||||
|
||||
|
|
@ -24,19 +25,19 @@ fn db_path() -> PathBuf {
|
|||
})
|
||||
}
|
||||
|
||||
fn open_db() -> Result<Connection, String> {
|
||||
fn open_db() -> Result<Connection, AppError> {
|
||||
let path = db_path();
|
||||
if !path.exists() {
|
||||
return Err("btmsg database not found. Run 'btmsg register' first.".into());
|
||||
return Err(AppError::database("btmsg database not found. Run 'btmsg register' first."));
|
||||
}
|
||||
let conn = Connection::open_with_flags(&path, OpenFlags::SQLITE_OPEN_READ_WRITE)
|
||||
.map_err(|e| format!("Failed to open btmsg.db: {e}"))?;
|
||||
.map_err(|e| AppError::database(format!("Failed to open btmsg.db: {e}")))?;
|
||||
conn.query_row("PRAGMA journal_mode=WAL", [], |_| Ok(()))
|
||||
.map_err(|e| format!("Failed to set WAL mode: {e}"))?;
|
||||
.map_err(|e| AppError::database(format!("Failed to set WAL mode: {e}")))?;
|
||||
conn.query_row("PRAGMA busy_timeout = 5000", [], |_| Ok(()))
|
||||
.map_err(|e| format!("Failed to set busy_timeout: {e}"))?;
|
||||
.map_err(|e| AppError::database(format!("Failed to set busy_timeout: {e}")))?;
|
||||
conn.execute_batch("PRAGMA foreign_keys = ON")
|
||||
.map_err(|e| format!("Failed to enable foreign keys: {e}"))?;
|
||||
.map_err(|e| AppError::database(format!("Failed to enable foreign keys: {e}")))?;
|
||||
|
||||
// Migration: add seen_messages table if not present
|
||||
conn.execute_batch(
|
||||
|
|
@ -122,12 +123,12 @@ pub struct BtmsgChannelMessage {
|
|||
pub sender_role: String,
|
||||
}
|
||||
|
||||
pub fn get_agents(group_id: &str) -> Result<Vec<BtmsgAgent>, String> {
|
||||
pub fn get_agents(group_id: &str) -> Result<Vec<BtmsgAgent>, AppError> {
|
||||
let db = open_db()?;
|
||||
let mut stmt = db.prepare(
|
||||
"SELECT a.*, (SELECT COUNT(*) FROM messages m WHERE m.to_agent = a.id AND m.read = 0) as unread_count \
|
||||
FROM agents a WHERE a.group_id = ? ORDER BY a.tier, a.role, a.name"
|
||||
).map_err(|e| format!("Query error: {e}"))?;
|
||||
).map_err(|e| AppError::database(format!("Query error: {e}")))?;
|
||||
|
||||
let agents = stmt.query_map(params![group_id], |row| {
|
||||
Ok(BtmsgAgent {
|
||||
|
|
@ -140,28 +141,28 @@ pub fn get_agents(group_id: &str) -> Result<Vec<BtmsgAgent>, String> {
|
|||
status: row.get::<_, Option<String>>("status")?.unwrap_or_else(|| "stopped".into()),
|
||||
unread_count: row.get("unread_count")?,
|
||||
})
|
||||
}).map_err(|e| format!("Query error: {e}"))?;
|
||||
}).map_err(|e| AppError::database(format!("Query error: {e}")))?;
|
||||
|
||||
agents.collect::<Result<Vec<_>, _>>().map_err(|e| format!("Row error: {e}"))
|
||||
agents.collect::<Result<Vec<_>, _>>().map_err(|e| AppError::database(format!("Row error: {e}")))
|
||||
}
|
||||
|
||||
pub fn unread_count(agent_id: &str) -> Result<i32, String> {
|
||||
pub fn unread_count(agent_id: &str) -> Result<i32, AppError> {
|
||||
let db = open_db()?;
|
||||
db.query_row(
|
||||
"SELECT COUNT(*) FROM messages WHERE to_agent = ? AND read = 0",
|
||||
params![agent_id],
|
||||
|row| row.get(0),
|
||||
).map_err(|e| format!("Query error: {e}"))
|
||||
).map_err(|e| AppError::database(format!("Query error: {e}")))
|
||||
}
|
||||
|
||||
pub fn unread_messages(agent_id: &str) -> Result<Vec<BtmsgMessage>, String> {
|
||||
pub fn unread_messages(agent_id: &str) -> Result<Vec<BtmsgMessage>, AppError> {
|
||||
let db = open_db()?;
|
||||
let mut stmt = db.prepare(
|
||||
"SELECT m.id, m.from_agent, m.to_agent, m.content, m.read, m.reply_to, m.created_at, \
|
||||
a.name AS sender_name, a.role AS sender_role \
|
||||
FROM messages m JOIN agents a ON m.from_agent = a.id \
|
||||
WHERE m.to_agent = ? AND m.read = 0 ORDER BY m.created_at ASC"
|
||||
).map_err(|e| format!("Query error: {e}"))?;
|
||||
).map_err(|e| AppError::database(format!("Query error: {e}")))?;
|
||||
|
||||
let msgs = stmt.query_map(params![agent_id], |row| {
|
||||
Ok(BtmsgMessage {
|
||||
|
|
@ -175,15 +176,15 @@ pub fn unread_messages(agent_id: &str) -> Result<Vec<BtmsgMessage>, String> {
|
|||
sender_name: row.get("sender_name")?,
|
||||
sender_role: row.get("sender_role")?,
|
||||
})
|
||||
}).map_err(|e| format!("Query error: {e}"))?;
|
||||
}).map_err(|e| AppError::database(format!("Query error: {e}")))?;
|
||||
|
||||
msgs.collect::<Result<Vec<_>, _>>().map_err(|e| format!("Row error: {e}"))
|
||||
msgs.collect::<Result<Vec<_>, _>>().map_err(|e| AppError::database(format!("Row error: {e}")))
|
||||
}
|
||||
|
||||
/// Get messages that have not been seen by this session.
|
||||
/// Unlike unread_messages (which uses the global `read` flag),
|
||||
/// this tracks per-session acknowledgment via the seen_messages table.
|
||||
pub fn unseen_messages(agent_id: &str, session_id: &str) -> Result<Vec<BtmsgMessage>, String> {
|
||||
pub fn unseen_messages(agent_id: &str, session_id: &str) -> Result<Vec<BtmsgMessage>, AppError> {
|
||||
let db = open_db()?;
|
||||
let mut stmt = db.prepare(
|
||||
"SELECT m.id, m.from_agent, m.to_agent, m.content, m.read, m.reply_to, m.created_at, \
|
||||
|
|
@ -193,7 +194,7 @@ pub fn unseen_messages(agent_id: &str, session_id: &str) -> Result<Vec<BtmsgMess
|
|||
WHERE m.to_agent = ?1 \
|
||||
AND m.id NOT IN (SELECT message_id FROM seen_messages WHERE session_id = ?2) \
|
||||
ORDER BY m.created_at ASC"
|
||||
).map_err(|e| format!("Prepare unseen query: {e}"))?;
|
||||
).map_err(|e| AppError::database(format!("Prepare unseen query: {e}")))?;
|
||||
|
||||
let rows = stmt.query_map(params![agent_id, session_id], |row| {
|
||||
Ok(BtmsgMessage {
|
||||
|
|
@ -207,36 +208,36 @@ pub fn unseen_messages(agent_id: &str, session_id: &str) -> Result<Vec<BtmsgMess
|
|||
sender_name: row.get("sender_name")?,
|
||||
sender_role: row.get("sender_role")?,
|
||||
})
|
||||
}).map_err(|e| format!("Query unseen: {e}"))?;
|
||||
}).map_err(|e| AppError::database(format!("Query unseen: {e}")))?;
|
||||
|
||||
rows.collect::<Result<Vec<_>, _>>().map_err(|e| format!("Row error: {e}"))
|
||||
rows.collect::<Result<Vec<_>, _>>().map_err(|e| AppError::database(format!("Row error: {e}")))
|
||||
}
|
||||
|
||||
/// Mark specific message IDs as seen by this session.
|
||||
pub fn mark_messages_seen(session_id: &str, message_ids: &[String]) -> Result<(), String> {
|
||||
pub fn mark_messages_seen(session_id: &str, message_ids: &[String]) -> Result<(), AppError> {
|
||||
if message_ids.is_empty() {
|
||||
return Ok(());
|
||||
}
|
||||
let db = open_db()?;
|
||||
let mut stmt = db.prepare(
|
||||
"INSERT OR IGNORE INTO seen_messages (session_id, message_id) VALUES (?1, ?2)"
|
||||
).map_err(|e| format!("Prepare mark_seen: {e}"))?;
|
||||
).map_err(|e| AppError::database(format!("Prepare mark_seen: {e}")))?;
|
||||
|
||||
for id in message_ids {
|
||||
stmt.execute(params![session_id, id])
|
||||
.map_err(|e| format!("Insert seen: {e}"))?;
|
||||
.map_err(|e| AppError::database(format!("Insert seen: {e}")))?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Prune seen_messages entries older than the given threshold.
|
||||
/// Uses emergency aggressive pruning (3 days) when row count exceeds the threshold.
|
||||
pub fn prune_seen_messages(max_age_secs: i64, emergency_threshold: i64) -> Result<u64, String> {
|
||||
pub fn prune_seen_messages(max_age_secs: i64, emergency_threshold: i64) -> Result<u64, AppError> {
|
||||
let db = open_db()?;
|
||||
|
||||
let count: i64 = db.query_row(
|
||||
"SELECT COUNT(*) FROM seen_messages", [], |row| row.get(0)
|
||||
).map_err(|e| format!("Count seen: {e}"))?;
|
||||
).map_err(|e| AppError::database(format!("Count seen: {e}")))?;
|
||||
|
||||
let threshold_secs = if count > emergency_threshold {
|
||||
// Emergency: prune more aggressively (3 days instead of configured max)
|
||||
|
|
@ -248,12 +249,12 @@ pub fn prune_seen_messages(max_age_secs: i64, emergency_threshold: i64) -> Resul
|
|||
let deleted = db.execute(
|
||||
"DELETE FROM seen_messages WHERE seen_at < unixepoch() - ?1",
|
||||
params![threshold_secs],
|
||||
).map_err(|e| format!("Prune seen: {e}"))?;
|
||||
).map_err(|e| AppError::database(format!("Prune seen: {e}")))?;
|
||||
|
||||
Ok(deleted as u64)
|
||||
}
|
||||
|
||||
pub fn history(agent_id: &str, other_id: &str, limit: i32) -> Result<Vec<BtmsgMessage>, String> {
|
||||
pub fn history(agent_id: &str, other_id: &str, limit: i32) -> Result<Vec<BtmsgMessage>, AppError> {
|
||||
let db = open_db()?;
|
||||
let mut stmt = db.prepare(
|
||||
"SELECT m.id, m.from_agent, m.to_agent, m.content, m.read, m.reply_to, m.created_at, \
|
||||
|
|
@ -261,7 +262,7 @@ pub fn history(agent_id: &str, other_id: &str, limit: i32) -> Result<Vec<BtmsgMe
|
|||
FROM messages m JOIN agents a ON m.from_agent = a.id \
|
||||
WHERE (m.from_agent = ?1 AND m.to_agent = ?2) OR (m.from_agent = ?2 AND m.to_agent = ?1) \
|
||||
ORDER BY m.created_at ASC LIMIT ?3"
|
||||
).map_err(|e| format!("Query error: {e}"))?;
|
||||
).map_err(|e| AppError::database(format!("Query error: {e}")))?;
|
||||
|
||||
let msgs = stmt.query_map(params![agent_id, other_id, limit], |row| {
|
||||
Ok(BtmsgMessage {
|
||||
|
|
@ -275,15 +276,15 @@ pub fn history(agent_id: &str, other_id: &str, limit: i32) -> Result<Vec<BtmsgMe
|
|||
sender_name: row.get("sender_name")?,
|
||||
sender_role: row.get("sender_role")?,
|
||||
})
|
||||
}).map_err(|e| format!("Query error: {e}"))?;
|
||||
}).map_err(|e| AppError::database(format!("Query error: {e}")))?;
|
||||
|
||||
msgs.collect::<Result<Vec<_>, _>>().map_err(|e| format!("Row error: {e}"))
|
||||
msgs.collect::<Result<Vec<_>, _>>().map_err(|e| AppError::database(format!("Row error: {e}")))
|
||||
}
|
||||
|
||||
/// Default heartbeat staleness threshold: 5 minutes
|
||||
const STALE_HEARTBEAT_SECS: i64 = 300;
|
||||
|
||||
pub fn send_message(from_agent: &str, to_agent: &str, content: &str) -> Result<String, String> {
|
||||
pub fn send_message(from_agent: &str, to_agent: &str, content: &str) -> Result<String, AppError> {
|
||||
let db = open_db()?;
|
||||
|
||||
// Get sender's group and tier
|
||||
|
|
@ -291,7 +292,7 @@ pub fn send_message(from_agent: &str, to_agent: &str, content: &str) -> Result<S
|
|||
"SELECT group_id, tier FROM agents WHERE id = ?",
|
||||
params![from_agent],
|
||||
|row| Ok((row.get(0)?, row.get(1)?)),
|
||||
).map_err(|e| format!("Sender not found: {e}"))?;
|
||||
).map_err(|e| AppError::database(format!("Sender not found: {e}")))?;
|
||||
|
||||
// Admin (tier 0) bypasses contact restrictions
|
||||
if sender_tier > 0 {
|
||||
|
|
@ -299,10 +300,10 @@ pub fn send_message(from_agent: &str, to_agent: &str, content: &str) -> Result<S
|
|||
"SELECT COUNT(*) > 0 FROM contacts WHERE agent_id = ? AND contact_id = ?",
|
||||
params![from_agent, to_agent],
|
||||
|row| row.get(0),
|
||||
).map_err(|e| format!("Contact check error: {e}"))?;
|
||||
).map_err(|e| AppError::database(format!("Contact check error: {e}")))?;
|
||||
|
||||
if !allowed {
|
||||
return Err(format!("Not allowed to message '{to_agent}'"));
|
||||
return Err(AppError::database(format!("Not allowed to message '{to_agent}'")));
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -332,7 +333,7 @@ pub fn send_message(from_agent: &str, to_agent: &str, content: &str) -> Result<S
|
|||
"INSERT INTO dead_letter_queue (from_agent, to_agent, content, error) VALUES (?1, ?2, ?3, ?4)",
|
||||
params![from_agent, to_agent, content, error_msg],
|
||||
)
|
||||
.map_err(|e| format!("Dead letter insert error: {e}"))?;
|
||||
.map_err(|e| AppError::database(format!("Dead letter insert error: {e}")))?;
|
||||
|
||||
// Also log audit event
|
||||
let _ = db.execute(
|
||||
|
|
@ -340,7 +341,7 @@ pub fn send_message(from_agent: &str, to_agent: &str, content: &str) -> Result<S
|
|||
params![from_agent, format!("Message to '{}' routed to dead letter queue: {}", to_agent, error_msg)],
|
||||
);
|
||||
|
||||
return Err(error_msg);
|
||||
return Err(AppError::database(error_msg));
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -349,56 +350,56 @@ pub fn send_message(from_agent: &str, to_agent: &str, content: &str) -> Result<S
|
|||
"INSERT INTO messages (id, from_agent, to_agent, content, group_id, sender_group_id) \
|
||||
VALUES (?1, ?2, ?3, ?4, ?5, (SELECT group_id FROM agents WHERE id = ?2))",
|
||||
params![msg_id, from_agent, to_agent, content, group_id],
|
||||
).map_err(|e| format!("Insert error: {e}"))?;
|
||||
).map_err(|e| AppError::database(format!("Insert error: {e}")))?;
|
||||
|
||||
Ok(msg_id)
|
||||
}
|
||||
|
||||
pub fn set_status(agent_id: &str, status: &str) -> Result<(), String> {
|
||||
pub fn set_status(agent_id: &str, status: &str) -> Result<(), AppError> {
|
||||
let db = open_db()?;
|
||||
db.execute(
|
||||
"UPDATE agents SET status = ?, last_active_at = datetime('now') WHERE id = ?",
|
||||
params![status, agent_id],
|
||||
).map_err(|e| format!("Update error: {e}"))?;
|
||||
).map_err(|e| AppError::database(format!("Update error: {e}")))?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn ensure_admin(group_id: &str) -> Result<(), String> {
|
||||
pub fn ensure_admin(group_id: &str) -> Result<(), AppError> {
|
||||
let db = open_db()?;
|
||||
|
||||
let exists: bool = db.query_row(
|
||||
"SELECT COUNT(*) > 0 FROM agents WHERE id = 'admin'",
|
||||
[],
|
||||
|row| row.get(0),
|
||||
).map_err(|e| format!("Query error: {e}"))?;
|
||||
).map_err(|e| AppError::database(format!("Query error: {e}")))?;
|
||||
|
||||
if !exists {
|
||||
db.execute(
|
||||
"INSERT INTO agents (id, name, role, group_id, tier, status) \
|
||||
VALUES ('admin', 'Operator', 'admin', ?, 0, 'active')",
|
||||
params![group_id],
|
||||
).map_err(|e| format!("Insert error: {e}"))?;
|
||||
).map_err(|e| AppError::database(format!("Insert error: {e}")))?;
|
||||
}
|
||||
|
||||
// Ensure admin has bidirectional contacts with ALL agents in the group
|
||||
let mut stmt = db.prepare(
|
||||
"SELECT id FROM agents WHERE group_id = ? AND id != 'admin'"
|
||||
).map_err(|e| format!("Query error: {e}"))?;
|
||||
).map_err(|e| AppError::database(format!("Query error: {e}")))?;
|
||||
let agent_ids: Vec<String> = stmt.query_map(params![group_id], |row| row.get(0))
|
||||
.map_err(|e| format!("Query error: {e}"))?
|
||||
.map_err(|e| AppError::database(format!("Query error: {e}")))?
|
||||
.collect::<Result<Vec<_>, _>>()
|
||||
.map_err(|e| format!("Row error: {e}"))?;
|
||||
.map_err(|e| AppError::database(format!("Row error: {e}")))?;
|
||||
drop(stmt);
|
||||
|
||||
for aid in &agent_ids {
|
||||
db.execute(
|
||||
"INSERT OR IGNORE INTO contacts (agent_id, contact_id) VALUES ('admin', ?)",
|
||||
params![aid],
|
||||
).map_err(|e| format!("Insert error: {e}"))?;
|
||||
).map_err(|e| AppError::database(format!("Insert error: {e}")))?;
|
||||
db.execute(
|
||||
"INSERT OR IGNORE INTO contacts (agent_id, contact_id) VALUES (?, 'admin')",
|
||||
params![aid],
|
||||
).map_err(|e| format!("Insert error: {e}"))?;
|
||||
).map_err(|e| AppError::database(format!("Insert error: {e}")))?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
|
|
@ -410,7 +411,7 @@ pub fn ensure_admin(group_id: &str) -> Result<(), String> {
|
|||
/// - Manager gets contacts with ALL agents (Tier 1 + Tier 2)
|
||||
/// - Other Tier 1 agents get contacts with Manager
|
||||
/// Also ensures admin agent and review channels exist.
|
||||
pub fn register_agents_from_groups(groups: &crate::groups::GroupsFile) -> Result<(), String> {
|
||||
pub fn register_agents_from_groups(groups: &crate::groups::GroupsFile) -> Result<(), AppError> {
|
||||
for group in &groups.groups {
|
||||
register_group_agents(group)?;
|
||||
}
|
||||
|
|
@ -418,7 +419,7 @@ pub fn register_agents_from_groups(groups: &crate::groups::GroupsFile) -> Result
|
|||
}
|
||||
|
||||
/// Register all agents for a single group.
|
||||
fn register_group_agents(group: &crate::groups::GroupConfig) -> Result<(), String> {
|
||||
fn register_group_agents(group: &crate::groups::GroupConfig) -> Result<(), AppError> {
|
||||
let db = open_db_or_create()?;
|
||||
let group_id = &group.id;
|
||||
|
||||
|
|
@ -478,11 +479,11 @@ fn register_group_agents(group: &crate::groups::GroupConfig) -> Result<(), Strin
|
|||
db.execute(
|
||||
"INSERT OR IGNORE INTO contacts (agent_id, contact_id) VALUES (?1, ?2)",
|
||||
params![tier1_ids[i], tier1_ids[j]],
|
||||
).map_err(|e| format!("Contact insert error: {e}"))?;
|
||||
).map_err(|e| AppError::database(format!("Contact insert error: {e}")))?;
|
||||
db.execute(
|
||||
"INSERT OR IGNORE INTO contacts (agent_id, contact_id) VALUES (?1, ?2)",
|
||||
params![tier1_ids[j], tier1_ids[i]],
|
||||
).map_err(|e| format!("Contact insert error: {e}"))?;
|
||||
).map_err(|e| AppError::database(format!("Contact insert error: {e}")))?;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -492,11 +493,11 @@ fn register_group_agents(group: &crate::groups::GroupConfig) -> Result<(), Strin
|
|||
db.execute(
|
||||
"INSERT OR IGNORE INTO contacts (agent_id, contact_id) VALUES (?1, ?2)",
|
||||
params![mgr_id, t2_id],
|
||||
).map_err(|e| format!("Contact insert error: {e}"))?;
|
||||
).map_err(|e| AppError::database(format!("Contact insert error: {e}")))?;
|
||||
db.execute(
|
||||
"INSERT OR IGNORE INTO contacts (agent_id, contact_id) VALUES (?1, ?2)",
|
||||
params![t2_id, mgr_id],
|
||||
).map_err(|e| format!("Contact insert error: {e}"))?;
|
||||
).map_err(|e| AppError::database(format!("Contact insert error: {e}")))?;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -522,7 +523,7 @@ fn upsert_agent(
|
|||
model: Option<&str>,
|
||||
cwd: Option<&str>,
|
||||
system_prompt: Option<&str>,
|
||||
) -> Result<(), String> {
|
||||
) -> Result<(), AppError> {
|
||||
db.execute(
|
||||
"INSERT INTO agents (id, name, role, group_id, tier, model, cwd, system_prompt)
|
||||
VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8)
|
||||
|
|
@ -536,7 +537,7 @@ fn upsert_agent(
|
|||
system_prompt = excluded.system_prompt",
|
||||
params![id, name, role, group_id, tier, model, cwd, system_prompt],
|
||||
)
|
||||
.map_err(|e| format!("Upsert agent error: {e}"))?;
|
||||
.map_err(|e| AppError::database(format!("Upsert agent error: {e}")))?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
|
|
@ -561,23 +562,23 @@ fn ensure_review_channels_for_group(db: &Connection, group_id: &str) {
|
|||
}
|
||||
|
||||
/// Open btmsg database, creating it with schema if it doesn't exist.
|
||||
fn open_db_or_create() -> Result<Connection, String> {
|
||||
fn open_db_or_create() -> Result<Connection, AppError> {
|
||||
let path = db_path();
|
||||
if let Some(parent) = path.parent() {
|
||||
std::fs::create_dir_all(parent)
|
||||
.map_err(|e| format!("Failed to create data dir: {e}"))?;
|
||||
.map_err(|e| AppError::database(format!("Failed to create data dir: {e}")))?;
|
||||
}
|
||||
|
||||
let conn = Connection::open_with_flags(
|
||||
&path,
|
||||
OpenFlags::SQLITE_OPEN_READ_WRITE | OpenFlags::SQLITE_OPEN_CREATE,
|
||||
)
|
||||
.map_err(|e| format!("Failed to open/create btmsg.db: {e}"))?;
|
||||
.map_err(|e| AppError::database(format!("Failed to open/create btmsg.db: {e}")))?;
|
||||
|
||||
conn.query_row("PRAGMA journal_mode=WAL", [], |_| Ok(()))
|
||||
.map_err(|e| format!("Failed to set WAL mode: {e}"))?;
|
||||
.map_err(|e| AppError::database(format!("Failed to set WAL mode: {e}")))?;
|
||||
conn.query_row("PRAGMA busy_timeout = 5000", [], |_| Ok(()))
|
||||
.map_err(|e| format!("Failed to set busy_timeout: {e}"))?;
|
||||
.map_err(|e| AppError::database(format!("Failed to set busy_timeout: {e}")))?;
|
||||
|
||||
// Create tables if they don't exist (same schema as Python btmsg CLI)
|
||||
conn.execute_batch(
|
||||
|
|
@ -723,11 +724,11 @@ fn open_db_or_create() -> Result<Connection, String> {
|
|||
FOREIGN KEY (message_id) REFERENCES messages(id) ON DELETE CASCADE
|
||||
);
|
||||
CREATE INDEX IF NOT EXISTS idx_seen_messages_session ON seen_messages(session_id);"
|
||||
).map_err(|e| format!("Schema creation error: {e}"))?;
|
||||
).map_err(|e| AppError::database(format!("Schema creation error: {e}")))?;
|
||||
|
||||
// Enable foreign keys for ON DELETE CASCADE support
|
||||
conn.execute_batch("PRAGMA foreign_keys = ON")
|
||||
.map_err(|e| format!("Failed to enable foreign keys: {e}"))?;
|
||||
.map_err(|e| AppError::database(format!("Failed to enable foreign keys: {e}")))?;
|
||||
|
||||
Ok(conn)
|
||||
}
|
||||
|
|
@ -744,23 +745,23 @@ pub struct AgentHeartbeat {
|
|||
pub timestamp: i64,
|
||||
}
|
||||
|
||||
pub fn record_heartbeat(agent_id: &str) -> Result<(), String> {
|
||||
pub fn record_heartbeat(agent_id: &str) -> Result<(), AppError> {
|
||||
let db = open_db()?;
|
||||
let now = std::time::SystemTime::now()
|
||||
.duration_since(std::time::UNIX_EPOCH)
|
||||
.map_err(|e| format!("Time error: {e}"))?
|
||||
.map_err(|e| AppError::database(format!("Time error: {e}")))?
|
||||
.as_secs() as i64;
|
||||
db.execute(
|
||||
"INSERT INTO heartbeats (agent_id, timestamp) VALUES (?1, ?2) \
|
||||
ON CONFLICT(agent_id) DO UPDATE SET timestamp = excluded.timestamp",
|
||||
params![agent_id, now],
|
||||
)
|
||||
.map_err(|e| format!("Heartbeat upsert error: {e}"))?;
|
||||
.map_err(|e| AppError::database(format!("Heartbeat upsert error: {e}")))?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[allow(dead_code)] // Called via Tauri IPC command btmsg_get_agent_heartbeats
|
||||
pub fn get_agent_heartbeats(group_id: &str) -> Result<Vec<AgentHeartbeat>, String> {
|
||||
pub fn get_agent_heartbeats(group_id: &str) -> Result<Vec<AgentHeartbeat>, AppError> {
|
||||
let db = open_db()?;
|
||||
let mut stmt = db
|
||||
.prepare(
|
||||
|
|
@ -768,7 +769,7 @@ pub fn get_agent_heartbeats(group_id: &str) -> Result<Vec<AgentHeartbeat>, Strin
|
|||
FROM heartbeats h JOIN agents a ON h.agent_id = a.id \
|
||||
WHERE a.group_id = ? ORDER BY h.timestamp DESC",
|
||||
)
|
||||
.map_err(|e| format!("Query error: {e}"))?;
|
||||
.map_err(|e| AppError::database(format!("Query error: {e}")))?;
|
||||
|
||||
let rows = stmt
|
||||
.query_map(params![group_id], |row| {
|
||||
|
|
@ -779,17 +780,17 @@ pub fn get_agent_heartbeats(group_id: &str) -> Result<Vec<AgentHeartbeat>, Strin
|
|||
timestamp: row.get("timestamp")?,
|
||||
})
|
||||
})
|
||||
.map_err(|e| format!("Query error: {e}"))?;
|
||||
.map_err(|e| AppError::database(format!("Query error: {e}")))?;
|
||||
|
||||
rows.collect::<Result<Vec<_>, _>>()
|
||||
.map_err(|e| format!("Row error: {e}"))
|
||||
.map_err(|e| AppError::database(format!("Row error: {e}")))
|
||||
}
|
||||
|
||||
pub fn get_stale_agents(group_id: &str, threshold_secs: i64) -> Result<Vec<String>, String> {
|
||||
pub fn get_stale_agents(group_id: &str, threshold_secs: i64) -> Result<Vec<String>, AppError> {
|
||||
let db = open_db()?;
|
||||
let now = std::time::SystemTime::now()
|
||||
.duration_since(std::time::UNIX_EPOCH)
|
||||
.map_err(|e| format!("Time error: {e}"))?
|
||||
.map_err(|e| AppError::database(format!("Time error: {e}")))?
|
||||
.as_secs() as i64;
|
||||
let cutoff = now - threshold_secs;
|
||||
|
||||
|
|
@ -799,14 +800,14 @@ pub fn get_stale_agents(group_id: &str, threshold_secs: i64) -> Result<Vec<Strin
|
|||
"SELECT a.id FROM agents a LEFT JOIN heartbeats h ON a.id = h.agent_id \
|
||||
WHERE a.group_id = ? AND a.tier > 0 AND (h.timestamp IS NULL OR h.timestamp < ?)",
|
||||
)
|
||||
.map_err(|e| format!("Query error: {e}"))?;
|
||||
.map_err(|e| AppError::database(format!("Query error: {e}")))?;
|
||||
|
||||
let ids = stmt
|
||||
.query_map(params![group_id, cutoff], |row| row.get::<_, String>(0))
|
||||
.map_err(|e| format!("Query error: {e}"))?;
|
||||
.map_err(|e| AppError::database(format!("Query error: {e}")))?;
|
||||
|
||||
ids.collect::<Result<Vec<_>, _>>()
|
||||
.map_err(|e| format!("Row error: {e}"))
|
||||
.map_err(|e| AppError::database(format!("Row error: {e}")))
|
||||
}
|
||||
|
||||
// ---- Dead letter queue ----
|
||||
|
|
@ -822,7 +823,7 @@ pub struct DeadLetter {
|
|||
pub created_at: String,
|
||||
}
|
||||
|
||||
pub fn get_dead_letters(group_id: &str, limit: i32) -> Result<Vec<DeadLetter>, String> {
|
||||
pub fn get_dead_letters(group_id: &str, limit: i32) -> Result<Vec<DeadLetter>, AppError> {
|
||||
let db = open_db()?;
|
||||
let mut stmt = db
|
||||
.prepare(
|
||||
|
|
@ -832,7 +833,7 @@ pub fn get_dead_letters(group_id: &str, limit: i32) -> Result<Vec<DeadLetter>, S
|
|||
WHERE a.group_id = ? \
|
||||
ORDER BY d.created_at DESC LIMIT ?",
|
||||
)
|
||||
.map_err(|e| format!("Query error: {e}"))?;
|
||||
.map_err(|e| AppError::database(format!("Query error: {e}")))?;
|
||||
|
||||
let rows = stmt
|
||||
.query_map(params![group_id, limit], |row| {
|
||||
|
|
@ -845,10 +846,10 @@ pub fn get_dead_letters(group_id: &str, limit: i32) -> Result<Vec<DeadLetter>, S
|
|||
created_at: row.get("created_at")?,
|
||||
})
|
||||
})
|
||||
.map_err(|e| format!("Query error: {e}"))?;
|
||||
.map_err(|e| AppError::database(format!("Query error: {e}")))?;
|
||||
|
||||
rows.collect::<Result<Vec<_>, _>>()
|
||||
.map_err(|e| format!("Row error: {e}"))
|
||||
.map_err(|e| AppError::database(format!("Row error: {e}")))
|
||||
}
|
||||
|
||||
#[allow(dead_code)] // Called via Tauri IPC command btmsg_queue_dead_letter
|
||||
|
|
@ -857,28 +858,28 @@ pub fn queue_dead_letter(
|
|||
to_agent: &str,
|
||||
content: &str,
|
||||
error: &str,
|
||||
) -> Result<(), String> {
|
||||
) -> Result<(), AppError> {
|
||||
let db = open_db()?;
|
||||
db.execute(
|
||||
"INSERT INTO dead_letter_queue (from_agent, to_agent, content, error) VALUES (?1, ?2, ?3, ?4)",
|
||||
params![from_agent, to_agent, content, error],
|
||||
)
|
||||
.map_err(|e| format!("Dead letter insert error: {e}"))?;
|
||||
.map_err(|e| AppError::database(format!("Dead letter insert error: {e}")))?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn clear_dead_letters(group_id: &str) -> Result<(), String> {
|
||||
pub fn clear_dead_letters(group_id: &str) -> Result<(), AppError> {
|
||||
let db = open_db()?;
|
||||
db.execute(
|
||||
"DELETE FROM dead_letter_queue WHERE to_agent IN (SELECT id FROM agents WHERE group_id = ?)",
|
||||
params![group_id],
|
||||
)
|
||||
.map_err(|e| format!("Delete error: {e}"))?;
|
||||
.map_err(|e| AppError::database(format!("Delete error: {e}")))?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Clear all communications for a group: messages, channel messages, seen tracking, dead letters
|
||||
pub fn clear_all_communications(group_id: &str) -> Result<(), String> {
|
||||
pub fn clear_all_communications(group_id: &str) -> Result<(), AppError> {
|
||||
let db = open_db()?;
|
||||
let agent_ids_clause = "(SELECT id FROM agents WHERE group_id = ?1)";
|
||||
|
||||
|
|
@ -886,22 +887,22 @@ pub fn clear_all_communications(group_id: &str) -> Result<(), String> {
|
|||
db.execute(
|
||||
&format!("DELETE FROM seen_messages WHERE message_id IN (SELECT id FROM messages WHERE group_id = ?1 OR from_agent IN {agent_ids_clause} OR to_agent IN {agent_ids_clause})"),
|
||||
params![group_id],
|
||||
).map_err(|e| format!("Clear seen_messages error: {e}"))?;
|
||||
).map_err(|e| AppError::database(format!("Clear seen_messages error: {e}")))?;
|
||||
|
||||
db.execute(
|
||||
&format!("DELETE FROM messages WHERE group_id = ?1 OR from_agent IN {agent_ids_clause} OR to_agent IN {agent_ids_clause}"),
|
||||
params![group_id],
|
||||
).map_err(|e| format!("Clear messages error: {e}"))?;
|
||||
).map_err(|e| AppError::database(format!("Clear messages error: {e}")))?;
|
||||
|
||||
db.execute(
|
||||
&format!("DELETE FROM channel_messages WHERE channel_id IN (SELECT id FROM channels WHERE group_id = ?1) OR from_agent IN {agent_ids_clause}"),
|
||||
params![group_id],
|
||||
).map_err(|e| format!("Clear channel_messages error: {e}"))?;
|
||||
).map_err(|e| AppError::database(format!("Clear channel_messages error: {e}")))?;
|
||||
|
||||
db.execute(
|
||||
&format!("DELETE FROM dead_letter_queue WHERE from_agent IN {agent_ids_clause} OR to_agent IN {agent_ids_clause}"),
|
||||
params![group_id],
|
||||
).map_err(|e| format!("Clear dead_letter_queue error: {e}"))?;
|
||||
).map_err(|e| AppError::database(format!("Clear dead_letter_queue error: {e}")))?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
|
@ -918,17 +919,17 @@ pub struct AuditEntry {
|
|||
pub created_at: String,
|
||||
}
|
||||
|
||||
pub fn log_audit_event(agent_id: &str, event_type: &str, detail: &str) -> Result<(), String> {
|
||||
pub fn log_audit_event(agent_id: &str, event_type: &str, detail: &str) -> Result<(), AppError> {
|
||||
let db = open_db_or_create()?;
|
||||
db.execute(
|
||||
"INSERT INTO audit_log (agent_id, event_type, detail) VALUES (?1, ?2, ?3)",
|
||||
params![agent_id, event_type, detail],
|
||||
)
|
||||
.map_err(|e| format!("Audit log insert error: {e}"))?;
|
||||
.map_err(|e| AppError::database(format!("Audit log insert error: {e}")))?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn get_audit_log(group_id: &str, limit: i32, offset: i32) -> Result<Vec<AuditEntry>, String> {
|
||||
pub fn get_audit_log(group_id: &str, limit: i32, offset: i32) -> Result<Vec<AuditEntry>, AppError> {
|
||||
let db = open_db()?;
|
||||
let mut stmt = db
|
||||
.prepare(
|
||||
|
|
@ -938,7 +939,7 @@ pub fn get_audit_log(group_id: &str, limit: i32, offset: i32) -> Result<Vec<Audi
|
|||
WHERE a.group_id = ? \
|
||||
ORDER BY al.created_at DESC, al.id DESC LIMIT ? OFFSET ?",
|
||||
)
|
||||
.map_err(|e| format!("Query error: {e}"))?;
|
||||
.map_err(|e| AppError::database(format!("Query error: {e}")))?;
|
||||
|
||||
let rows = stmt
|
||||
.query_map(params![group_id, limit, offset], |row| {
|
||||
|
|
@ -950,23 +951,23 @@ pub fn get_audit_log(group_id: &str, limit: i32, offset: i32) -> Result<Vec<Audi
|
|||
created_at: row.get("created_at")?,
|
||||
})
|
||||
})
|
||||
.map_err(|e| format!("Query error: {e}"))?;
|
||||
.map_err(|e| AppError::database(format!("Query error: {e}")))?;
|
||||
|
||||
rows.collect::<Result<Vec<_>, _>>()
|
||||
.map_err(|e| format!("Row error: {e}"))
|
||||
.map_err(|e| AppError::database(format!("Row error: {e}")))
|
||||
}
|
||||
|
||||
pub fn get_audit_log_for_agent(
|
||||
agent_id: &str,
|
||||
limit: i32,
|
||||
) -> Result<Vec<AuditEntry>, String> {
|
||||
) -> Result<Vec<AuditEntry>, AppError> {
|
||||
let db = open_db()?;
|
||||
let mut stmt = db
|
||||
.prepare(
|
||||
"SELECT id, agent_id, event_type, detail, created_at \
|
||||
FROM audit_log WHERE agent_id = ? ORDER BY created_at DESC, id DESC LIMIT ?",
|
||||
)
|
||||
.map_err(|e| format!("Query error: {e}"))?;
|
||||
.map_err(|e| AppError::database(format!("Query error: {e}")))?;
|
||||
|
||||
let rows = stmt
|
||||
.query_map(params![agent_id, limit], |row| {
|
||||
|
|
@ -978,13 +979,13 @@ pub fn get_audit_log_for_agent(
|
|||
created_at: row.get("created_at")?,
|
||||
})
|
||||
})
|
||||
.map_err(|e| format!("Query error: {e}"))?;
|
||||
.map_err(|e| AppError::database(format!("Query error: {e}")))?;
|
||||
|
||||
rows.collect::<Result<Vec<_>, _>>()
|
||||
.map_err(|e| format!("Row error: {e}"))
|
||||
.map_err(|e| AppError::database(format!("Row error: {e}")))
|
||||
}
|
||||
|
||||
pub fn all_feed(group_id: &str, limit: i32) -> Result<Vec<BtmsgFeedMessage>, String> {
|
||||
pub fn all_feed(group_id: &str, limit: i32) -> Result<Vec<BtmsgFeedMessage>, AppError> {
|
||||
let db = open_db()?;
|
||||
let mut stmt = db.prepare(
|
||||
"SELECT m.id, m.from_agent, m.to_agent, m.content, m.created_at, m.reply_to, \
|
||||
|
|
@ -995,7 +996,7 @@ pub fn all_feed(group_id: &str, limit: i32) -> Result<Vec<BtmsgFeedMessage>, Str
|
|||
JOIN agents a2 ON m.to_agent = a2.id \
|
||||
WHERE m.group_id = ? \
|
||||
ORDER BY m.created_at DESC LIMIT ?"
|
||||
).map_err(|e| format!("Query error: {e}"))?;
|
||||
).map_err(|e| AppError::database(format!("Query error: {e}")))?;
|
||||
|
||||
let msgs = stmt.query_map(params![group_id, limit], |row| {
|
||||
Ok(BtmsgFeedMessage {
|
||||
|
|
@ -1010,28 +1011,28 @@ pub fn all_feed(group_id: &str, limit: i32) -> Result<Vec<BtmsgFeedMessage>, Str
|
|||
recipient_name: row.get("recipient_name")?,
|
||||
recipient_role: row.get("recipient_role")?,
|
||||
})
|
||||
}).map_err(|e| format!("Query error: {e}"))?;
|
||||
}).map_err(|e| AppError::database(format!("Query error: {e}")))?;
|
||||
|
||||
msgs.collect::<Result<Vec<_>, _>>().map_err(|e| format!("Row error: {e}"))
|
||||
msgs.collect::<Result<Vec<_>, _>>().map_err(|e| AppError::database(format!("Row error: {e}")))
|
||||
}
|
||||
|
||||
pub fn mark_read_conversation(reader_id: &str, sender_id: &str) -> Result<(), String> {
|
||||
pub fn mark_read_conversation(reader_id: &str, sender_id: &str) -> Result<(), AppError> {
|
||||
let db = open_db()?;
|
||||
db.execute(
|
||||
"UPDATE messages SET read = 1 WHERE to_agent = ? AND from_agent = ? AND read = 0",
|
||||
params![reader_id, sender_id],
|
||||
).map_err(|e| format!("Update error: {e}"))?;
|
||||
).map_err(|e| AppError::database(format!("Update error: {e}")))?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn get_channels(group_id: &str) -> Result<Vec<BtmsgChannel>, String> {
|
||||
pub fn get_channels(group_id: &str) -> Result<Vec<BtmsgChannel>, AppError> {
|
||||
let db = open_db()?;
|
||||
let mut stmt = db.prepare(
|
||||
"SELECT c.id, c.name, c.group_id, c.created_by, \
|
||||
(SELECT COUNT(*) FROM channel_members cm WHERE cm.channel_id = c.id) AS member_count, \
|
||||
c.created_at \
|
||||
FROM channels c WHERE c.group_id = ? ORDER BY c.name"
|
||||
).map_err(|e| format!("Query error: {e}"))?;
|
||||
).map_err(|e| AppError::database(format!("Query error: {e}")))?;
|
||||
|
||||
let channels = stmt.query_map(params![group_id], |row| {
|
||||
Ok(BtmsgChannel {
|
||||
|
|
@ -1042,19 +1043,19 @@ pub fn get_channels(group_id: &str) -> Result<Vec<BtmsgChannel>, String> {
|
|||
member_count: row.get("member_count")?,
|
||||
created_at: row.get("created_at")?,
|
||||
})
|
||||
}).map_err(|e| format!("Query error: {e}"))?;
|
||||
}).map_err(|e| AppError::database(format!("Query error: {e}")))?;
|
||||
|
||||
channels.collect::<Result<Vec<_>, _>>().map_err(|e| format!("Row error: {e}"))
|
||||
channels.collect::<Result<Vec<_>, _>>().map_err(|e| AppError::database(format!("Row error: {e}")))
|
||||
}
|
||||
|
||||
pub fn get_channel_messages(channel_id: &str, limit: i32) -> Result<Vec<BtmsgChannelMessage>, String> {
|
||||
pub fn get_channel_messages(channel_id: &str, limit: i32) -> Result<Vec<BtmsgChannelMessage>, AppError> {
|
||||
let db = open_db()?;
|
||||
let mut stmt = db.prepare(
|
||||
"SELECT cm.id, cm.channel_id, cm.from_agent, cm.content, cm.created_at, \
|
||||
a.name AS sender_name, a.role AS sender_role \
|
||||
FROM channel_messages cm JOIN agents a ON cm.from_agent = a.id \
|
||||
WHERE cm.channel_id = ? ORDER BY cm.created_at ASC LIMIT ?"
|
||||
).map_err(|e| format!("Query error: {e}"))?;
|
||||
).map_err(|e| AppError::database(format!("Query error: {e}")))?;
|
||||
|
||||
let msgs = stmt.query_map(params![channel_id, limit], |row| {
|
||||
Ok(BtmsgChannelMessage {
|
||||
|
|
@ -1066,12 +1067,12 @@ pub fn get_channel_messages(channel_id: &str, limit: i32) -> Result<Vec<BtmsgCha
|
|||
sender_name: row.get("sender_name")?,
|
||||
sender_role: row.get("sender_role")?,
|
||||
})
|
||||
}).map_err(|e| format!("Query error: {e}"))?;
|
||||
}).map_err(|e| AppError::database(format!("Query error: {e}")))?;
|
||||
|
||||
msgs.collect::<Result<Vec<_>, _>>().map_err(|e| format!("Row error: {e}"))
|
||||
msgs.collect::<Result<Vec<_>, _>>().map_err(|e| AppError::database(format!("Row error: {e}")))
|
||||
}
|
||||
|
||||
pub fn send_channel_message(channel_id: &str, from_agent: &str, content: &str) -> Result<String, String> {
|
||||
pub fn send_channel_message(channel_id: &str, from_agent: &str, content: &str) -> Result<String, AppError> {
|
||||
let db = open_db()?;
|
||||
|
||||
// Verify channel exists
|
||||
|
|
@ -1079,24 +1080,24 @@ pub fn send_channel_message(channel_id: &str, from_agent: &str, content: &str) -
|
|||
"SELECT id FROM channels WHERE id = ?",
|
||||
params![channel_id],
|
||||
|row| row.get(0),
|
||||
).map_err(|e| format!("Channel not found: {e}"))?;
|
||||
).map_err(|e| AppError::database(format!("Channel not found: {e}")))?;
|
||||
|
||||
// Check membership (admin bypasses)
|
||||
let sender_tier: i32 = db.query_row(
|
||||
"SELECT tier FROM agents WHERE id = ?",
|
||||
params![from_agent],
|
||||
|row| row.get(0),
|
||||
).map_err(|e| format!("Sender not found: {e}"))?;
|
||||
).map_err(|e| AppError::database(format!("Sender not found: {e}")))?;
|
||||
|
||||
if sender_tier > 0 {
|
||||
let is_member: bool = db.query_row(
|
||||
"SELECT COUNT(*) > 0 FROM channel_members WHERE channel_id = ? AND agent_id = ?",
|
||||
params![channel_id, from_agent],
|
||||
|row| row.get(0),
|
||||
).map_err(|e| format!("Membership check error: {e}"))?;
|
||||
).map_err(|e| AppError::database(format!("Membership check error: {e}")))?;
|
||||
|
||||
if !is_member {
|
||||
return Err("Not a member of this channel".into());
|
||||
return Err(AppError::database("Not a member of this channel"));
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -1104,35 +1105,35 @@ pub fn send_channel_message(channel_id: &str, from_agent: &str, content: &str) -
|
|||
db.execute(
|
||||
"INSERT INTO channel_messages (id, channel_id, from_agent, content) VALUES (?1, ?2, ?3, ?4)",
|
||||
params![msg_id, channel_id, from_agent, content],
|
||||
).map_err(|e| format!("Insert error: {e}"))?;
|
||||
).map_err(|e| AppError::database(format!("Insert error: {e}")))?;
|
||||
|
||||
Ok(msg_id)
|
||||
}
|
||||
|
||||
pub fn create_channel(name: &str, group_id: &str, created_by: &str) -> Result<String, String> {
|
||||
pub fn create_channel(name: &str, group_id: &str, created_by: &str) -> Result<String, AppError> {
|
||||
let db = open_db()?;
|
||||
let channel_id = uuid::Uuid::new_v4().to_string()[..8].to_string();
|
||||
|
||||
db.execute(
|
||||
"INSERT INTO channels (id, name, group_id, created_by) VALUES (?1, ?2, ?3, ?4)",
|
||||
params![channel_id, name, group_id, created_by],
|
||||
).map_err(|e| format!("Insert error: {e}"))?;
|
||||
).map_err(|e| AppError::database(format!("Insert error: {e}")))?;
|
||||
|
||||
// Auto-add creator as member
|
||||
db.execute(
|
||||
"INSERT INTO channel_members (channel_id, agent_id) VALUES (?1, ?2)",
|
||||
params![channel_id, created_by],
|
||||
).map_err(|e| format!("Insert error: {e}"))?;
|
||||
).map_err(|e| AppError::database(format!("Insert error: {e}")))?;
|
||||
|
||||
Ok(channel_id)
|
||||
}
|
||||
|
||||
pub fn add_channel_member(channel_id: &str, agent_id: &str) -> Result<(), String> {
|
||||
pub fn add_channel_member(channel_id: &str, agent_id: &str) -> Result<(), AppError> {
|
||||
let db = open_db()?;
|
||||
db.execute(
|
||||
"INSERT OR IGNORE INTO channel_members (channel_id, agent_id) VALUES (?1, ?2)",
|
||||
params![channel_id, agent_id],
|
||||
).map_err(|e| format!("Insert error: {e}"))?;
|
||||
).map_err(|e| AppError::database(format!("Insert error: {e}")))?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue