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",
|
"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",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|||||||
@@ -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"] }
|
||||||
|
|||||||
@@ -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))?;
|
||||||
|
|
||||||
|
|||||||
@@ -4,3 +4,5 @@ pub mod schema;
|
|||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests;
|
mod tests;
|
||||||
|
|
||||||
|
pub use chat_database::ChatDatabase;
|
||||||
@@ -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>,
|
||||||
|
|||||||
@@ -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"
|
||||||
|
|||||||
@@ -3,9 +3,28 @@ 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;
|
||||||
|
|
||||||
|
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)]
|
#[derive(Subcommand)]
|
||||||
pub enum Commands {
|
pub enum Commands {
|
||||||
/// Prints all known conversations on the server.
|
/// Prints all known conversations on the server.
|
||||||
@@ -16,7 +35,7 @@ pub enum Commands {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl 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();
|
let mut client = ClientCli::new();
|
||||||
match cmd {
|
match cmd {
|
||||||
Commands::Version => client.print_version().await,
|
Commands::Version => client.print_version().await,
|
||||||
@@ -31,34 +50,20 @@ struct ClientCli {
|
|||||||
|
|
||||||
impl ClientCli {
|
impl ClientCli {
|
||||||
pub fn new() -> Self {
|
pub fn new() -> Self {
|
||||||
dotenv::dotenv().ok();
|
let api = make_api_client_from_env();
|
||||||
|
|
||||||
// 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());
|
|
||||||
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
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 client;
|
||||||
|
mod db;
|
||||||
mod printers;
|
mod printers;
|
||||||
mod client;
|
|
||||||
|
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,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
Reference in New Issue
Block a user