kpcli: adds 'db' subcommand for interacting with the database
This commit is contained in:
3
Cargo.lock
generated
3
Cargo.lock
generated
@@ -702,6 +702,7 @@ dependencies = [
|
||||
"chrono",
|
||||
"diesel",
|
||||
"diesel_migrations",
|
||||
"kordophone",
|
||||
"serde",
|
||||
"time",
|
||||
"uuid",
|
||||
@@ -715,8 +716,10 @@ dependencies = [
|
||||
"clap",
|
||||
"dotenv",
|
||||
"kordophone",
|
||||
"kordophone-db",
|
||||
"log",
|
||||
"pretty",
|
||||
"time",
|
||||
"tokio",
|
||||
]
|
||||
|
||||
|
||||
@@ -8,6 +8,7 @@ anyhow = "1.0.94"
|
||||
chrono = "0.4.38"
|
||||
diesel = { version = "2.2.6", features = ["chrono", "sqlite", "time"] }
|
||||
diesel_migrations = { version = "2.2.0", features = ["sqlite"] }
|
||||
kordophone = { path = "../kordophone" }
|
||||
serde = { version = "1.0.215", features = ["derive"] }
|
||||
time = "0.3.37"
|
||||
uuid = { version = "1.11.0", features = ["v4"] }
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
use anyhow::Result;
|
||||
use diesel::prelude::*;
|
||||
use diesel::{prelude::*, sqlite::Sqlite};
|
||||
use diesel::query_dsl::BelongingToDsl;
|
||||
use std::path::{Path, PathBuf};
|
||||
|
||||
use crate::{
|
||||
models::{
|
||||
@@ -20,7 +21,11 @@ pub struct ChatDatabase {
|
||||
|
||||
impl ChatDatabase {
|
||||
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)
|
||||
.map_err(|e| anyhow::anyhow!("Error running migrations: {}", e))?;
|
||||
|
||||
|
||||
@@ -4,3 +4,5 @@ pub mod schema;
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests;
|
||||
|
||||
pub use chat_database::ChatDatabase;
|
||||
@@ -1,4 +1,4 @@
|
||||
use chrono::NaiveDateTime;
|
||||
use chrono::{DateTime, NaiveDateTime};
|
||||
use uuid::Uuid;
|
||||
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)]
|
||||
pub struct ConversationBuilder {
|
||||
guid: Option<String>,
|
||||
|
||||
@@ -10,6 +10,8 @@ anyhow = "1.0.93"
|
||||
clap = { version = "4.5.20", features = ["derive"] }
|
||||
dotenv = "0.15.0"
|
||||
kordophone = { path = "../kordophone" }
|
||||
kordophone-db = { path = "../kordophone-db" }
|
||||
log = "0.4.22"
|
||||
pretty = { version = "0.12.3", features = ["termcolor"] }
|
||||
time = "0.3.37"
|
||||
tokio = "1.41.1"
|
||||
|
||||
@@ -3,9 +3,28 @@ use kordophone::api::http_client::HTTPAPIClient;
|
||||
use kordophone::api::http_client::Credentials;
|
||||
|
||||
use dotenv;
|
||||
use anyhow::Result;
|
||||
use clap::Subcommand;
|
||||
use crate::printers::ConversationPrinter;
|
||||
|
||||
pub fn make_api_client_from_env() -> HTTPAPIClient {
|
||||
dotenv::dotenv().ok();
|
||||
|
||||
// read from env
|
||||
let base_url = std::env::var("KORDOPHONE_API_URL")
|
||||
.expect("KORDOPHONE_API_URL must be set");
|
||||
|
||||
let credentials = Credentials {
|
||||
username: std::env::var("KORDOPHONE_USERNAME")
|
||||
.expect("KORDOPHONE_USERNAME must be set"),
|
||||
|
||||
password: std::env::var("KORDOPHONE_PASSWORD")
|
||||
.expect("KORDOPHONE_PASSWORD must be set"),
|
||||
};
|
||||
|
||||
HTTPAPIClient::new(base_url.parse().unwrap(), credentials.into())
|
||||
}
|
||||
|
||||
#[derive(Subcommand)]
|
||||
pub enum Commands {
|
||||
/// Prints all known conversations on the server.
|
||||
@@ -16,7 +35,7 @@ pub enum Commands {
|
||||
}
|
||||
|
||||
impl Commands {
|
||||
pub async fn run(cmd: Commands) -> Result<(), Box<dyn std::error::Error>> {
|
||||
pub async fn run(cmd: Commands) -> Result<()> {
|
||||
let mut client = ClientCli::new();
|
||||
match cmd {
|
||||
Commands::Version => client.print_version().await,
|
||||
@@ -31,34 +50,20 @@ struct ClientCli {
|
||||
|
||||
impl ClientCli {
|
||||
pub fn new() -> Self {
|
||||
dotenv::dotenv().ok();
|
||||
|
||||
// read from env
|
||||
let base_url = std::env::var("KORDOPHONE_API_URL")
|
||||
.expect("KORDOPHONE_API_URL must be set");
|
||||
|
||||
let credentials = Credentials {
|
||||
username: std::env::var("KORDOPHONE_USERNAME")
|
||||
.expect("KORDOPHONE_USERNAME must be set"),
|
||||
|
||||
password: std::env::var("KORDOPHONE_PASSWORD")
|
||||
.expect("KORDOPHONE_PASSWORD must be set"),
|
||||
};
|
||||
|
||||
let api = HTTPAPIClient::new(base_url.parse().unwrap(), credentials.into());
|
||||
let api = make_api_client_from_env();
|
||||
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?;
|
||||
println!("Version: {}", version);
|
||||
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?;
|
||||
for conversation in conversations {
|
||||
println!("{}", ConversationPrinter::new(&conversation));
|
||||
println!("{}", ConversationPrinter::new(&conversation.into()));
|
||||
}
|
||||
|
||||
Ok(())
|
||||
|
||||
83
kpcli/src/db/mod.rs
Normal file
83
kpcli/src/db/mod.rs
Normal 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(())
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,8 @@
|
||||
mod printers;
|
||||
mod client;
|
||||
mod db;
|
||||
mod printers;
|
||||
|
||||
use anyhow::Result;
|
||||
use clap::{Parser, Subcommand};
|
||||
|
||||
/// A command line interface for the Kordophone library and daemon
|
||||
@@ -17,11 +20,18 @@ enum Commands {
|
||||
#[command(subcommand)]
|
||||
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 {
|
||||
Commands::Client { command } => client::Commands::run(command).await,
|
||||
Commands::Db { command } => db::Commands::run(command).await,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,14 +1,48 @@
|
||||
use std::fmt::Display;
|
||||
|
||||
use time::OffsetDateTime;
|
||||
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> {
|
||||
doc: RcDoc<'a, Conversation>
|
||||
doc: RcDoc<'a, PrintableConversation>
|
||||
}
|
||||
|
||||
impl<'a> ConversationPrinter<'a> {
|
||||
pub fn new(conversation: &'a Conversation) -> Self {
|
||||
pub fn new(conversation: &'a PrintableConversation) -> Self {
|
||||
let preview = conversation.last_message_preview
|
||||
.as_deref()
|
||||
.unwrap_or("<null>")
|
||||
@@ -27,7 +61,7 @@ impl<'a> ConversationPrinter<'a> {
|
||||
.append("[")
|
||||
.append(RcDoc::line()
|
||||
.append(
|
||||
conversation.participant_display_names
|
||||
conversation.participants
|
||||
.iter()
|
||||
.map(|name|
|
||||
RcDoc::text(name)
|
||||
|
||||
Reference in New Issue
Block a user