diff --git a/Cargo.lock b/Cargo.lock index f624075..1ef6135 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -266,6 +266,21 @@ dependencies = [ "serde", ] +[[package]] +name = "buckets-core" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6df107757f765b92fc260dd4b7c2df6c4e4646f79a4f4020c5ca5f249f52dcb" +dependencies = [ + "oiseau", + "pathbufd", + "serde", + "serde_json", + "tetratto-core", + "tetratto-shared", + "toml", +] + [[package]] name = "bumpalo" version = "3.19.0" @@ -534,12 +549,6 @@ dependencies = [ "dtoa", ] -[[package]] -name = "either" -version = "1.15.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "48c757948c5ede0e46177b7add2e67155f70e33c07fea8284df6576da70b3719" - [[package]] name = "emojis" version = "0.7.2" @@ -1148,7 +1157,6 @@ checksum = "fe4cd85333e22411419a0bcae1297d25e58c9443848b11dc6a86fefe8c78a661" dependencies = [ "equivalent", "hashbrown", - "serde", ] [[package]] @@ -1178,15 +1186,6 @@ dependencies = [ "serde", ] -[[package]] -name = "itertools" -version = "0.13.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "413ee7dfc52ee1a4949ceeb7dbc8a33f2d6c088194d9f922fb8318faf1f01186" -dependencies = [ - "either", -] - [[package]] name = "itoa" version = "1.0.15" @@ -1268,31 +1267,6 @@ version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c41e0c4fef86961ac6d6f8a82609f55f31b05e4fce149ac5710e439df7619ba4" -[[package]] -name = "malachite" -version = "1.0.0" -dependencies = [ - "axum", - "axum-extra", - "dotenv", - "glob", - "nanoneo", - "oiseau", - "pathbufd", - "regex", - "serde", - "serde_json", - "serde_valid", - "tera", - "tetratto-core", - "tetratto-shared", - "tokio", - "toml 0.9.5", - "tower-http", - "tracing", - "tracing-subscriber", -] - [[package]] name = "maplit" version = "1.0.2" @@ -1599,7 +1573,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1db05f56d34358a8b1066f67cbb203ee3e7ed2ba674a6263a1d5ec6db2204323" dependencies = [ "memchr", - "thiserror 2.0.12", + "thiserror", "ucd-trie", ] @@ -1796,27 +1770,6 @@ version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "925383efa346730478fb4838dbe9137d2a47675ad789c546d150a6e1dd4ab31c" -[[package]] -name = "proc-macro-error-attr2" -version = "2.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "96de42df36bb9bba5542fe9f1a054b8cc87e172759a1868aa05c1f3acc89dfc5" -dependencies = [ - "proc-macro2", - "quote", -] - -[[package]] -name = "proc-macro-error2" -version = "2.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "11ec05c52be0a07b08061f7dd003e7d7092e0472bc731b4af7bb1ef876109802" -dependencies = [ - "proc-macro-error-attr2", - "proc-macro2", - "quote", -] - [[package]] name = "proc-macro2" version = "1.0.95" @@ -2226,15 +2179,6 @@ dependencies = [ "serde", ] -[[package]] -name = "serde_spanned" -version = "0.6.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bf41e0cfaf7226dca15e8197172c295a782857fcb97fad1808a166870dee75a3" -dependencies = [ - "serde", -] - [[package]] name = "serde_spanned" version = "1.0.0" @@ -2256,52 +2200,6 @@ dependencies = [ "serde", ] -[[package]] -name = "serde_valid" -version = "1.0.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3b615bed66931a7a9809b273937adc8a402d038b1e509d027fcaf62f084d33d1" -dependencies = [ - "indexmap", - "itertools", - "num-traits", - "once_cell", - "paste", - "regex", - "serde", - "serde_json", - "serde_valid_derive", - "serde_valid_literal", - "thiserror 1.0.69", - "toml 0.8.23", - "unicode-segmentation", -] - -[[package]] -name = "serde_valid_derive" -version = "1.0.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5fa1a5a21ea5aab06d2e6a6b59837d450fb2be9695be97735a711edfbe79ea07" -dependencies = [ - "itertools", - "paste", - "proc-macro-error2", - "proc-macro2", - "quote", - "strsim", - "syn", -] - -[[package]] -name = "serde_valid_literal" -version = "1.0.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dd07331596ea967dccf9a35bde71ecd757490e09827b938a5c6226c648e3a25e" -dependencies = [ - "paste", - "regex", -] - [[package]] name = "sha1" version = "0.10.6" @@ -2450,12 +2348,6 @@ dependencies = [ "unicode-properties", ] -[[package]] -name = "strsim" -version = "0.11.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" - [[package]] name = "subtle" version = "2.6.1" @@ -2514,6 +2406,31 @@ dependencies = [ "libc", ] +[[package]] +name = "tawny" +version = "1.0.0" +dependencies = [ + "axum", + "axum-extra", + "buckets-core", + "dotenv", + "glob", + "nanoneo", + "oiseau", + "pathbufd", + "regex", + "serde", + "serde_json", + "tera", + "tetratto-core", + "tetratto-shared", + "tokio", + "toml", + "tower-http", + "tracing", + "tracing-subscriber", +] + [[package]] name = "tempfile" version = "3.20.0" @@ -2562,9 +2479,9 @@ dependencies = [ [[package]] name = "tetratto-core" -version = "15.0.1" +version = "15.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c7aeb9dcc5631ec6188bb9438dc97015c6662b6f59e650e5afa865775f170c9c" +checksum = "605c03fac71468f57f9c47d9246300640f3f65ec9f19fb86799e10f632d3ea68" dependencies = [ "async-recursion", "base16ct", @@ -2582,7 +2499,7 @@ dependencies = [ "tetratto-l10n", "tetratto-shared", "tokio", - "toml 0.9.5", + "toml", "totp-rs", ] @@ -2594,7 +2511,7 @@ checksum = "d96f5e41633c757e3519efb47c9b85d00d14322c1961360e126d0ecc0ea79b86" dependencies = [ "pathbufd", "serde", - "toml 0.9.5", + "toml", ] [[package]] @@ -2615,33 +2532,13 @@ dependencies = [ "uuid", ] -[[package]] -name = "thiserror" -version = "1.0.69" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52" -dependencies = [ - "thiserror-impl 1.0.69", -] - [[package]] name = "thiserror" version = "2.0.12" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "567b8a2dae586314f7be2a752ec7474332959c6460e02bde30d702a66d488708" dependencies = [ - "thiserror-impl 2.0.12", -] - -[[package]] -name = "thiserror-impl" -version = "1.0.69" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1" -dependencies = [ - "proc-macro2", - "quote", - "syn", + "thiserror-impl", ] [[package]] @@ -2821,18 +2718,6 @@ dependencies = [ "tokio", ] -[[package]] -name = "toml" -version = "0.8.23" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dc1beb996b9d83529a9e75c17a1686767d148d70663143c7854d8b4a09ced362" -dependencies = [ - "serde", - "serde_spanned 0.6.9", - "toml_datetime 0.6.11", - "toml_edit", -] - [[package]] name = "toml" version = "0.9.5" @@ -2841,22 +2726,13 @@ checksum = "75129e1dc5000bfbaa9fee9d1b21f974f9fbad9daec557a521ee6e080825f6e8" dependencies = [ "indexmap", "serde", - "serde_spanned 1.0.0", - "toml_datetime 0.7.0", + "serde_spanned", + "toml_datetime", "toml_parser", "toml_writer", "winnow", ] -[[package]] -name = "toml_datetime" -version = "0.6.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "22cddaf88f4fbc13c51aebbf5f8eceb5c7c5a9da2ac40a13519eb5b0a0e8f11c" -dependencies = [ - "serde", -] - [[package]] name = "toml_datetime" version = "0.7.0" @@ -2866,20 +2742,6 @@ dependencies = [ "serde", ] -[[package]] -name = "toml_edit" -version = "0.22.27" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "41fe8c660ae4257887cf66394862d21dbca4a6ddd26f04a3560410406a2f819a" -dependencies = [ - "indexmap", - "serde", - "serde_spanned 0.6.9", - "toml_datetime 0.6.11", - "toml_write", - "winnow", -] - [[package]] name = "toml_parser" version = "1.0.2" @@ -2889,12 +2751,6 @@ dependencies = [ "winnow", ] -[[package]] -name = "toml_write" -version = "0.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5d99f8c9a7727884afe522e9bd5edbfc91a3312b36a77b5fb8926e4c31a41801" - [[package]] name = "toml_writer" version = "1.0.2" @@ -3055,7 +2911,7 @@ dependencies = [ "log", "rand 0.9.1", "sha1", - "thiserror 2.0.12", + "thiserror", "utf-8", ] @@ -3154,12 +3010,6 @@ version = "0.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e70f2a8b45122e719eb623c01822704c4e0907e7e426a05927e1a1cfff5b75d0" -[[package]] -name = "unicode-segmentation" -version = "1.12.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f6ccf251212114b54433ec949fd6a7841275f9ada20dddd2f29e9ceea4501493" - [[package]] name = "unicode-width" version = "0.2.1" @@ -3628,9 +3478,6 @@ name = "winnow" version = "0.7.12" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f3edebf492c8125044983378ecb5766203ad3b4c2f7a922bd7dd207f6d443e95" -dependencies = [ - "memchr", -] [[package]] name = "wit-bindgen-rt" diff --git a/Cargo.toml b/Cargo.toml index 1f204ef..cedb403 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,11 +1,11 @@ [package] -name = "malachite" +name = "tawny" version = "1.0.0" edition = "2024" authors = ["trisuaso"] -repository = "https://trisua.com/t/malachite" +repository = "https://trisua.com/t/tawny" license = "AGPL-3.0-or-later" -homepage = "https://trisua.com" +homepage = "https://tawny.cc" [dependencies] tetratto-core = "15.0.1" @@ -29,6 +29,6 @@ dotenv = "0.15.0" glob = "0.3.2" serde_json = "1.0.142" toml = "0.9.4" -serde_valid = { version = "1.0.5", features = ["toml"] } regex = "1.11.1" oiseau = { version = "0.1.2", default-features = false, features = ["postgres", "redis",] } +buckets-core = "1.0.4" diff --git a/README.md b/README.md index 59180a5..8f9ea16 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,5 @@ -# 🪨 malachite +# 🦉 tawny -simple template for building backends with a structure similar to how the [tetratto](https://trisua.com/t/tetratto) repository is organized +messaging service :) + + diff --git a/app/templates_src/root.lisp b/app/templates_src/root.lisp index 830ca5e..1a53805 100644 --- a/app/templates_src/root.lisp +++ b/app/templates_src/root.lisp @@ -6,7 +6,7 @@ (meta ("name" "viewport") ("content" "width=device-width, initial-scale=1.0")) (meta ("http-equiv" "X-UA-Compatible") ("content" "ie=edge")) - (link ("rel" "stylesheet") ("href" "https://repodelivery.trisua.com/tetratto/crates/app/src/public/css/utility.css")) + (link ("rel" "stylesheet") ("href" "https://repodelivery.tetratto.com/tetratto/crates/app/src/public/css/utility.css")) (link ("rel" "stylesheet") ("href" "/public/style.css?v={{ build_code }}")) (style (text ":root { --color-primary: {{ theme_color }}; }")) diff --git a/src/config.rs b/src/config.rs index 3866212..9d98e68 100644 --- a/src/config.rs +++ b/src/config.rs @@ -2,6 +2,12 @@ use oiseau::config::{Configuration, DatabaseConfig}; use pathbufd::PathBufD; use serde::{Deserialize, Serialize}; +#[derive(Clone, Debug, Serialize, Deserialize)] +pub struct ServiceHostsConfig { + pub tetratto: String, + pub buckets: String, +} + #[derive(Clone, Debug, Serialize, Deserialize)] pub struct Config { /// The name of the site. Shown in the UI. @@ -13,17 +19,24 @@ pub struct Config { /// Real IP header (for reverse proxy). #[serde(default = "default_real_ip_header")] pub real_ip_header: String, + /// The host URL of additional services the app needs. + #[serde(default = "default_service_hosts")] + pub service_hosts: ServiceHostsConfig, + /// The location of the uploads directory. Should match `directory` in your + /// buckets config. + #[serde(default = "default_uploads_dir")] + pub uploads_dir: String, /// Database configuration. #[serde(default = "default_database")] pub database: DatabaseConfig, } fn default_name() -> String { - "App".to_string() + "Tawny".to_string() } fn default_theme_color() -> String { - "#6ee7b7".to_string() + "#cd5700".to_string() } fn default_real_ip_header() -> String { @@ -34,6 +47,17 @@ fn default_database() -> DatabaseConfig { DatabaseConfig::default() } +fn default_service_hosts() -> ServiceHostsConfig { + ServiceHostsConfig { + tetratto: String::new(), + buckets: String::new(), + } +} + +fn default_uploads_dir() -> String { + "./uploads".to_string() +} + impl Configuration for Config { fn db_config(&self) -> DatabaseConfig { self.database.to_owned() @@ -46,6 +70,8 @@ impl Default for Config { name: default_name(), theme_color: default_theme_color(), real_ip_header: default_real_ip_header(), + service_hosts: default_service_hosts(), + uploads_dir: default_uploads_dir(), database: default_database(), } } diff --git a/src/database/chats.rs b/src/database/chats.rs new file mode 100644 index 0000000..920089d --- /dev/null +++ b/src/database/chats.rs @@ -0,0 +1,101 @@ +use super::DataManager; +use crate::model::{Chat, ChatStyle}; +use oiseau::{PostgresRow, cache::Cache, execute, get, params}; +use tetratto_core::{ + auto_method, + model::{Error, Result}, +}; + +impl DataManager { + /// Get a [`Chat`] from an SQL row. + pub(crate) fn get_chat_from_row(x: &PostgresRow) -> Chat { + Chat { + id: get!(x->0(i64)) as usize, + created: get!(x->1(i64)) as usize, + style: serde_json::from_str(&get!(x->2(String))).unwrap(), + members: serde_json::from_str(&get!(x->3(String))).unwrap(), + last_message_created: get!(x->4(i64)) as usize, + last_message_read_by: serde_json::from_str(&get!(x->5(String))).unwrap(), + } + } + + auto_method!(get_chat_by_id(usize as i64)@get_chat_from_row -> "SELECT * FROM t_chats WHERE id = $1" --name="chat" --returns=Chat --cache-key-tmpl="twny.chat:{}"); + + /// Create a new chat in the database. + /// + /// # Arguments + /// * `data` - a mock [`Chat`] object to insert + pub async fn create_chat(&self, data: Chat) -> Result { + // check values + if let ChatStyle::Group(ref info) = data.style { + if info.name.trim().len() < 2 { + return Err(Error::DataTooShort("name".to_string())); + } else if info.name.len() > 128 { + return Err(Error::DataTooLong("name".to_string())); + } + } + + // ... + let conn = match self.0.connect().await { + Ok(c) => c, + Err(e) => return Err(Error::DatabaseConnection(e.to_string())), + }; + + let res = execute!( + &conn, + "INSERT INTO t_chats VALUES ($1, $2, $3, $4, $5, $6)", + params![ + &(data.id as i64), + &(data.created as i64), + &serde_json::to_string(&data.style).unwrap(), + &serde_json::to_string(&data.members).unwrap(), + &(data.last_message_created as i64), + &serde_json::to_string(&data.last_message_read_by).unwrap() + ] + ); + + if let Err(e) = res { + return Err(Error::DatabaseError(e.to_string())); + } + + Ok(data) + } + + /// Delete an existing chat. + pub async fn delete_chat(&self, id: usize) -> Result<()> { + let conn = match self.0.connect().await { + Ok(c) => c, + Err(e) => return Err(Error::DatabaseConnection(e.to_string())), + }; + + // delete chat + let res = execute!( + &conn, + "DELETE FROM t_chats WHERE id = $1", + params![&(id as i64)] + ); + + if let Err(e) = res { + return Err(Error::DatabaseError(e.to_string())); + } + + // delete messages + let res = execute!( + &conn, + "DELETE FROM t_messages WHERE chat = $1", + params![&(id as i64)] + ); + + if let Err(e) = res { + return Err(Error::DatabaseError(e.to_string())); + } + + // ... + Ok(()) + } + + auto_method!(update_chat_style(ChatStyle) -> "UPDATE t_chats SET style = $1 WHERE id = $2" --serde --cache-key-tmpl="twny.chat:{}"); + auto_method!(update_chat_members(Vec) -> "UPDATE t_chats SET members = $1 WHERE id = $2" --serde --cache-key-tmpl="twny.chat:{}"); + auto_method!(update_chat_last_message_read_by(Vec) -> "UPDATE t_chats SET last_message_read_by = $1 WHERE id = $2" --serde --cache-key-tmpl="twny.chat:{}"); + auto_method!(update_chat_last_message_created(i64) -> "UPDATE t_chats SET last_message_created = $1 WHERE id = $2" --cache-key-tmpl="twny.chat:{}"); +} diff --git a/src/database/messages.rs b/src/database/messages.rs new file mode 100644 index 0000000..dcaefd4 --- /dev/null +++ b/src/database/messages.rs @@ -0,0 +1,99 @@ +use super::DataManager; +use crate::model::Message; +use oiseau::{PostgresRow, cache::Cache, execute, get, params}; +use tetratto_core::{ + auto_method, + model::{Error, Result, auth::User}, +}; + +impl DataManager { + /// Get a [`Message`] from an SQL row. + pub(crate) fn get_message_from_row(x: &PostgresRow) -> Message { + Message { + id: get!(x->0(i64)) as usize, + created: get!(x->1(i64)) as usize, + owner: get!(x->2(i64)) as usize, + chat: get!(x->3(i64)) as usize, + content: get!(x->4(String)), + uploads: serde_json::from_str(&get!(x->5(String))).unwrap(), + } + } + + auto_method!(get_message_by_id(usize as i64)@get_message_from_row -> "SELECT * FROM t_messages WHERE id = $1" --name="message" --returns=Message --cache-key-tmpl="twny.message:{}"); + + /// Create a new message in the database. + /// + /// # Arguments + /// * `data` - a mock [`Message`] object to insert + pub async fn create_message(&self, data: Message) -> Result { + // check values + if data.content.trim().len() < 2 { + return Err(Error::DataTooShort("content".to_string())); + } else if data.content.len() > 2048 { + return Err(Error::DataTooLong("content".to_string())); + } + + // ... + let conn = match self.0.connect().await { + Ok(c) => c, + Err(e) => return Err(Error::DatabaseConnection(e.to_string())), + }; + + let res = execute!( + &conn, + "INSERT INTO t_messages VALUES ($1, $2, $3, $4, $5, $6)", + params![ + &(data.id as i64), + &(data.created as i64), + &(data.owner as i64), + &(data.chat as i64), + &data.content, + &serde_json::to_string(&data.uploads).unwrap(), + ] + ); + + if let Err(e) = res { + return Err(Error::DatabaseError(e.to_string())); + } + + Ok(data) + } + + /// Delete an existing message. + pub async fn delete_message(&self, id: usize, user: &User) -> Result<()> { + let message = self.get_message_by_id(id).await?; + + if message.owner != user.id { + return Err(Error::NotAllowed); + } + + // ... + let conn = match self.0.connect().await { + Ok(c) => c, + Err(e) => return Err(Error::DatabaseConnection(e.to_string())), + }; + + // delete message + let res = execute!( + &conn, + "DELETE FROM t_messages WHERE id = $1", + params![&(id as i64)] + ); + + if let Err(e) = res { + return Err(Error::DatabaseError(e.to_string())); + } + + // delete uploads + for upload in message.uploads { + if let Err(e) = self.1.delete_upload(upload).await { + return Err(Error::MiscError(e.to_string())); + } + } + + // ... + Ok(()) + } + + auto_method!(update_message_content(&str) -> "UPDATE t_messages SET content = $1 WHERE id = $2" --serde --cache-key-tmpl="twny.message:{}"); +} diff --git a/src/database/mod.rs b/src/database/mod.rs index 71ffaad..0cf9f70 100644 --- a/src/database/mod.rs +++ b/src/database/mod.rs @@ -1,18 +1,28 @@ +mod chats; +mod messages; mod sql; use crate::config::Config; +use buckets_core::{Config as BucketsConfig, DataManager as BucketsManager}; use oiseau::{execute, postgres::DataManager as OiseauManager, postgres::Result as PgResult}; +use std::collections::HashMap; use tetratto_core::model::{Error, Result}; -pub const NAME_REGEX: &str = r"[^\w_\-\.,!]+"; - #[derive(Clone)] -pub struct DataManager(pub OiseauManager); +pub struct DataManager(pub OiseauManager, pub BucketsManager); impl DataManager { /// Create a new [`DataManager`]. pub async fn new(config: Config) -> PgResult { - Ok(Self(OiseauManager::new(config).await?)) + let buckets_manager = BucketsManager::new(BucketsConfig { + directory: config.uploads_dir.clone(), + bucket_defaults: HashMap::new(), + database: config.database.clone(), + }) + .await + .expect("failed to create buckets manager"); + + Ok(Self(OiseauManager::new(config).await?, buckets_manager)) } /// Initialize tables. @@ -22,7 +32,8 @@ impl DataManager { Err(e) => return Err(Error::DatabaseConnection(e.to_string())), }; - // execute!(&conn, sql::CREATE_TABLE_ENTRIES).unwrap(); + execute!(&conn, sql::CREATE_TABLE_CHATS).unwrap(); + execute!(&conn, sql::CREATE_TABLE_MESSAGES).unwrap(); Ok(()) } diff --git a/src/database/sql/create_chats.sql b/src/database/sql/create_chats.sql new file mode 100644 index 0000000..2150fb7 --- /dev/null +++ b/src/database/sql/create_chats.sql @@ -0,0 +1,8 @@ +CREATE TABLE IF NOT EXISTS t_chats ( + id BIGINT NOT NULL, + created BIGINT NOT NULL, + style TEXT NOT NULL, + members TEXT NOT NULL, + last_message_created BIGINT NOT NULL, + last_message_read_by TEXT NOT NULL +); diff --git a/src/database/sql/create_messages.sql b/src/database/sql/create_messages.sql new file mode 100644 index 0000000..4ab2a73 --- /dev/null +++ b/src/database/sql/create_messages.sql @@ -0,0 +1,8 @@ +CREATE TABLE IF NOT EXISTS t_messages ( + id BIGINT NOT NULL, + created BIGINT NOT NULL, + owner BIGINT NOT NULL, + chats BIGINT NOT NULL, + content TEXT NOT NULL, + uploads TEXT NOT NULL +); diff --git a/src/database/sql/mod.rs b/src/database/sql/mod.rs index a02070e..0f6e946 100644 --- a/src/database/sql/mod.rs +++ b/src/database/sql/mod.rs @@ -1 +1,2 @@ -// pub const CREATE_TABLE_ENTRIES: &str = include_str!("./create_entries.sql"); +pub const CREATE_TABLE_CHATS: &str = include_str!("./create_chats.sql"); +pub const CREATE_TABLE_MESSAGES: &str = include_str!("./create_messages.sql"); diff --git a/src/main.rs b/src/main.rs index 04cdfe8..7157ce6 100644 --- a/src/main.rs +++ b/src/main.rs @@ -125,7 +125,7 @@ async fn main() { .await .unwrap(); - info!("🪨 malachite."); + info!("🦉 tawny."); info!("listening on http://0.0.0.0:{}", port); axum::serve( listener, diff --git a/src/model.rs b/src/model.rs index 83af5d7..de6da6f 100644 --- a/src/model.rs +++ b/src/model.rs @@ -1 +1,73 @@ -//! Base types matching SQL table structures. +use serde::{Deserialize, Serialize}; +use tetratto_shared::{snow::Snowflake, unix_epoch_timestamp}; + +#[derive(Serialize, Deserialize)] +pub struct GroupChatInfo { + pub name: String, +} + +#[derive(Serialize, Deserialize)] +pub enum ChatStyle { + /// Direct messages between two users. + Direct, + /// Messages between a group of users (up to 10). + Group(GroupChatInfo), +} + +#[derive(Serialize, Deserialize)] +pub struct Chat { + pub id: usize, + pub created: usize, + pub style: ChatStyle, + /// When the last member of the chat leaves, the chat will be deleted. + pub members: Vec, + pub last_message_created: usize, + /// The IDs of each user in the chat who read the last message. + /// + /// Will always have the ID of the user who sent the last message as index 0. + /// + /// The UI should show two checkmarks once this vector has a length of at least 2. + /// + /// Read receipts are stored by chat instead of message since it's easier to + /// keep up with if we store by chat instead. This will also declutter + /// the UI and prevent every message showing a read receipt. + pub last_message_read_by: Vec, +} + +impl Chat { + /// Create a new [`Chat`]. + pub fn new(style: ChatStyle, members: Vec) -> Self { + Self { + id: Snowflake::new().to_string().parse::().unwrap(), + created: unix_epoch_timestamp(), + style, + members, + last_message_created: 0, + last_message_read_by: Vec::new(), + } + } +} + +#[derive(Serialize, Deserialize)] +pub struct Message { + pub id: usize, + pub created: usize, + pub owner: usize, + pub chat: usize, + pub content: String, + pub uploads: Vec, +} + +impl Message { + /// Create a new [`Message`]. + pub fn new(owner: usize, chat: usize, content: String, uploads: Vec) -> Self { + Self { + id: Snowflake::new().to_string().parse::().unwrap(), + created: unix_epoch_timestamp(), + owner, + chat, + content, + uploads, + } + } +}