Private
Public Access
1
0

kpcli: adds 'db' subcommand for interacting with the database

This commit is contained in:
2025-01-08 13:32:55 -08:00
parent 89f8d21ebb
commit 793faab721
10 changed files with 197 additions and 29 deletions

3
Cargo.lock generated
View File

@@ -702,6 +702,7 @@ dependencies = [
"chrono", "chrono",
"diesel", "diesel",
"diesel_migrations", "diesel_migrations",
"kordophone",
"serde", "serde",
"time", "time",
"uuid", "uuid",
@@ -715,8 +716,10 @@ dependencies = [
"clap", "clap",
"dotenv", "dotenv",
"kordophone", "kordophone",
"kordophone-db",
"log", "log",
"pretty", "pretty",
"time",
"tokio", "tokio",
] ]

View File

@@ -8,6 +8,7 @@ anyhow = "1.0.94"
chrono = "0.4.38" chrono = "0.4.38"
diesel = { version = "2.2.6", features = ["chrono", "sqlite", "time"] } diesel = { version = "2.2.6", features = ["chrono", "sqlite", "time"] }
diesel_migrations = { version = "2.2.0", features = ["sqlite"] } diesel_migrations = { version = "2.2.0", features = ["sqlite"] }
kordophone = { path = "../kordophone" }
serde = { version = "1.0.215", features = ["derive"] } serde = { version = "1.0.215", features = ["derive"] }
time = "0.3.37" time = "0.3.37"
uuid = { version = "1.11.0", features = ["v4"] } uuid = { version = "1.11.0", features = ["v4"] }

View File

@@ -1,6 +1,7 @@
use anyhow::Result; use anyhow::Result;
use diesel::prelude::*; use diesel::{prelude::*, sqlite::Sqlite};
use diesel::query_dsl::BelongingToDsl; use diesel::query_dsl::BelongingToDsl;
use std::path::{Path, PathBuf};
use crate::{ use crate::{
models::{ models::{
@@ -20,7 +21,11 @@ pub struct ChatDatabase {
impl ChatDatabase { impl ChatDatabase {
pub fn new_in_memory() -> Result<Self> { pub fn new_in_memory() -> Result<Self> {
let mut db = SqliteConnection::establish(":memory:")?; Self::new(":memory:")
}
pub fn new(db_path: &str) -> Result<Self> {
let mut db = SqliteConnection::establish(db_path)?;
db.run_pending_migrations(MIGRATIONS) db.run_pending_migrations(MIGRATIONS)
.map_err(|e| anyhow::anyhow!("Error running migrations: {}", e))?; .map_err(|e| anyhow::anyhow!("Error running migrations: {}", e))?;

View File

@@ -4,3 +4,5 @@ pub mod schema;
#[cfg(test)] #[cfg(test)]
mod tests; mod tests;
pub use chat_database::ChatDatabase;

View File

@@ -1,4 +1,4 @@
use chrono::NaiveDateTime; use chrono::{DateTime, NaiveDateTime};
use uuid::Uuid; use uuid::Uuid;
use crate::models::participant::Participant; use crate::models::participant::Participant;
@@ -29,6 +29,29 @@ impl Conversation {
} }
} }
impl From<kordophone::model::Conversation> for Conversation {
fn from(value: kordophone::model::Conversation) -> Self {
Self {
guid: value.guid,
unread_count: u16::try_from(value.unread_count).unwrap(),
display_name: value.display_name,
last_message_preview: value.last_message_preview,
date: DateTime::from_timestamp(
value.date.unix_timestamp(),
value.date.unix_timestamp_nanos()
.try_into()
.unwrap_or(0),
)
.unwrap()
.naive_local(),
participants: value.participant_display_names
.into_iter()
.map(|p| p.into())
.collect(),
}
}
}
#[derive(Default)] #[derive(Default)]
pub struct ConversationBuilder { pub struct ConversationBuilder {
guid: Option<String>, guid: Option<String>,

View File

@@ -10,6 +10,8 @@ anyhow = "1.0.93"
clap = { version = "4.5.20", features = ["derive"] } clap = { version = "4.5.20", features = ["derive"] }
dotenv = "0.15.0" dotenv = "0.15.0"
kordophone = { path = "../kordophone" } kordophone = { path = "../kordophone" }
kordophone-db = { path = "../kordophone-db" }
log = "0.4.22" log = "0.4.22"
pretty = { version = "0.12.3", features = ["termcolor"] } pretty = { version = "0.12.3", features = ["termcolor"] }
time = "0.3.37"
tokio = "1.41.1" tokio = "1.41.1"

View File

@@ -3,34 +3,11 @@ use kordophone::api::http_client::HTTPAPIClient;
use kordophone::api::http_client::Credentials; use kordophone::api::http_client::Credentials;
use dotenv; use dotenv;
use anyhow::Result;
use clap::Subcommand; use clap::Subcommand;
use crate::printers::ConversationPrinter; use crate::printers::ConversationPrinter;
#[derive(Subcommand)] pub fn make_api_client_from_env() -> HTTPAPIClient {
pub enum Commands {
/// Prints all known conversations on the server.
Conversations,
/// Prints the server Kordophone version.
Version,
}
impl Commands {
pub async fn run(cmd: Commands) -> Result<(), Box<dyn std::error::Error>> {
let mut client = ClientCli::new();
match cmd {
Commands::Version => client.print_version().await,
Commands::Conversations => client.print_conversations().await,
}
}
}
struct ClientCli {
api: HTTPAPIClient,
}
impl ClientCli {
pub fn new() -> Self {
dotenv::dotenv().ok(); dotenv::dotenv().ok();
// read from env // read from env
@@ -45,20 +22,48 @@ impl ClientCli {
.expect("KORDOPHONE_PASSWORD must be set"), .expect("KORDOPHONE_PASSWORD must be set"),
}; };
let api = HTTPAPIClient::new(base_url.parse().unwrap(), credentials.into()); HTTPAPIClient::new(base_url.parse().unwrap(), credentials.into())
}
#[derive(Subcommand)]
pub enum Commands {
/// Prints all known conversations on the server.
Conversations,
/// Prints the server Kordophone version.
Version,
}
impl Commands {
pub async fn run(cmd: Commands) -> Result<()> {
let mut client = ClientCli::new();
match cmd {
Commands::Version => client.print_version().await,
Commands::Conversations => client.print_conversations().await,
}
}
}
struct ClientCli {
api: HTTPAPIClient,
}
impl ClientCli {
pub fn new() -> Self {
let api = make_api_client_from_env();
Self { api: api } Self { api: api }
} }
pub async fn print_version(&mut self) -> Result<(), Box<dyn std::error::Error>> { pub async fn print_version(&mut self) -> Result<()> {
let version = self.api.get_version().await?; let version = self.api.get_version().await?;
println!("Version: {}", version); println!("Version: {}", version);
Ok(()) Ok(())
} }
pub async fn print_conversations(&mut self) -> Result<(), Box<dyn std::error::Error>> { pub async fn print_conversations(&mut self) -> Result<()> {
let conversations = self.api.get_conversations().await?; let conversations = self.api.get_conversations().await?;
for conversation in conversations { for conversation in conversations {
println!("{}", ConversationPrinter::new(&conversation)); println!("{}", ConversationPrinter::new(&conversation.into()));
} }
Ok(()) Ok(())

83
kpcli/src/db/mod.rs Normal file
View File

@@ -0,0 +1,83 @@
use anyhow::Result;
use clap::Subcommand;
use kordophone::APIInterface;
use std::{env, path::{Path, PathBuf}};
use kordophone_db::ChatDatabase;
use crate::{client, printers::ConversationPrinter};
#[derive(Subcommand)]
pub enum Commands {
/// For dealing with the table of cached conversations.
Conversations {
#[clap(subcommand)]
command: ConversationCommands
},
}
#[derive(Subcommand)]
pub enum ConversationCommands {
/// Lists all conversations currently in the database.
List,
/// Syncs with an API client.
Sync,
}
impl Commands {
pub async fn run(cmd: Commands) -> Result<()> {
let mut db = DbClient::new()?;
match cmd {
Commands::Conversations { command: cmd } => match cmd {
ConversationCommands::List => db.print_conversations(),
ConversationCommands::Sync => db.sync_with_client().await,
},
}
}
}
struct DbClient {
db: ChatDatabase
}
impl DbClient {
fn database_path() -> PathBuf {
let temp_dir = env::temp_dir();
temp_dir.join("kpcli_chat.db")
}
pub fn new() -> Result<Self> {
let path = Self::database_path();
let path_str: &str = path.as_path().to_str().unwrap();
println!("kpcli: Using temporary db at {}", path_str);
let db = ChatDatabase::new(path_str)?;
Ok( Self { db })
}
pub fn print_conversations(&mut self) -> Result<()> {
let all_conversations = self.db.all_conversations()?;
println!("{} Conversations: ", all_conversations.len());
for conversation in all_conversations {
println!("{}", ConversationPrinter::new(&conversation.into()));
}
Ok(())
}
pub async fn sync_with_client(&mut self) -> Result<()> {
let mut client = client::make_api_client_from_env();
let fetched_conversations = client.get_conversations().await?;
let db_conversations: Vec<kordophone_db::models::Conversation> = fetched_conversations.into_iter()
.map(|c| kordophone_db::models::Conversation::from(c))
.collect();
for conversation in db_conversations {
self.db.insert_conversation(conversation)?;
}
Ok(())
}
}

View File

@@ -1,5 +1,8 @@
mod printers;
mod client; mod client;
mod db;
mod printers;
use anyhow::Result;
use clap::{Parser, Subcommand}; use clap::{Parser, Subcommand};
/// A command line interface for the Kordophone library and daemon /// A command line interface for the Kordophone library and daemon
@@ -17,11 +20,18 @@ enum Commands {
#[command(subcommand)] #[command(subcommand)]
command: client::Commands, command: client::Commands,
}, },
/// Commands for the cache database
Db {
#[command(subcommand)]
command: db::Commands,
}
} }
async fn run_command(command: Commands) -> Result<(), Box<dyn std::error::Error>> { async fn run_command(command: Commands) -> Result<()> {
match command { match command {
Commands::Client { command } => client::Commands::run(command).await, Commands::Client { command } => client::Commands::run(command).await,
Commands::Db { command } => db::Commands::run(command).await,
} }
} }

View File

@@ -1,14 +1,48 @@
use std::fmt::Display; use std::fmt::Display;
use time::OffsetDateTime;
use pretty::RcDoc; use pretty::RcDoc;
use kordophone::model::Conversation;
pub struct PrintableConversation {
pub guid: String,
pub date: OffsetDateTime,
pub unread_count: i32,
pub last_message_preview: Option<String>,
pub participants: Vec<String>,
pub display_name: Option<String>,
}
impl From<kordophone::model::Conversation> for PrintableConversation {
fn from(value: kordophone::model::Conversation) -> Self {
Self {
guid: value.guid,
date: value.date,
unread_count: value.unread_count,
last_message_preview: value.last_message_preview,
participants: value.participant_display_names,
display_name: value.display_name,
}
}
}
impl From<kordophone_db::models::Conversation> for PrintableConversation {
fn from(value: kordophone_db::models::Conversation) -> Self {
Self {
guid: value.guid,
date: OffsetDateTime::from_unix_timestamp(value.date.and_utc().timestamp()).unwrap(),
unread_count: value.unread_count.into(),
last_message_preview: value.last_message_preview,
participants: value.participants.into_iter().map(|p| p.display_name).collect(),
display_name: value.display_name,
}
}
}
pub struct ConversationPrinter<'a> { pub struct ConversationPrinter<'a> {
doc: RcDoc<'a, Conversation> doc: RcDoc<'a, PrintableConversation>
} }
impl<'a> ConversationPrinter<'a> { impl<'a> ConversationPrinter<'a> {
pub fn new(conversation: &'a Conversation) -> Self { pub fn new(conversation: &'a PrintableConversation) -> Self {
let preview = conversation.last_message_preview let preview = conversation.last_message_preview
.as_deref() .as_deref()
.unwrap_or("<null>") .unwrap_or("<null>")
@@ -27,7 +61,7 @@ impl<'a> ConversationPrinter<'a> {
.append("[") .append("[")
.append(RcDoc::line() .append(RcDoc::line()
.append( .append(
conversation.participant_display_names conversation.participants
.iter() .iter()
.map(|name| .map(|name|
RcDoc::text(name) RcDoc::text(name)