blatherskite

a toy discord-like chat app backend written for a swe class
Log | Files | Refs | README

commit 0f23cb3c17490af6325efd3fa80c1f4462df3271
parent c78231c5a5ca6f2c22c6cd4ce9ff78eaea2b01db
Author: quantumish <freifeld.david@gmail.com>
Date:   Sun, 16 Oct 2022 16:08:16 -0700

Untabify, revert back to old mutex-bound IDs, make tests pass

Diffstat:
Mscuttlebutt/src/db.rs | 665++++++++++++++++++++++++++++++++++++++++---------------------------------------
Mscuttlebutt/src/main.rs | 806+++++++++++++++++++++++++++++++++++++++----------------------------------------
Mscuttlebutt/src/tests.rs | 133+++++++++++++++++++++++++++++++++++--------------------------------------------
3 files changed, 798 insertions(+), 806 deletions(-)

diff --git a/scuttlebutt/src/db.rs b/scuttlebutt/src/db.rs @@ -3,342 +3,353 @@ use crate::responses::*; #[derive(Debug)] pub enum IdType { - User, - Group, - Channel, - Message + User, + Group, + Channel, + Message } -pub trait Database: Sync + Send { - fn valid_id(&self, kind: IdType, id: i64) -> Result<bool>; - - fn create_user(&self, id: i64, name: String, email: String, hash: String) -> Result<()>; - fn update_user(&self, id: i64, name: String, email: String) -> Result<()>; - fn get_user(&self, id: i64) -> Result<User>; - fn get_user_hash(&self, id: i64) -> Result<String>; - fn delete_user(&self, id: i64) -> Result<()>; - - fn create_group(&self, gid: i64, uid: i64, name: String) -> Result<()>; - fn update_group(&self, id: i64, name: String) -> Result<()>; - fn get_group(&self, id: i64) -> Result<Group>; - fn delete_group(&self, id: i64) -> Result<()>; - fn get_group_members(&self, gid: i64) -> Result<Vec<i64>>; - fn remove_group_member(&self, gid: i64, uid: i64) -> Result<()>; - fn add_group_member(&self, gid: i64, uid: i64) -> Result<()>; - fn get_group_channels(&self, gid: i64) -> Result<Vec<i64>>; - fn remove_group_channel(&self, gid: i64, uid: i64) -> Result<()>; - fn add_group_channel(&self, gid: i64, uid: i64) -> Result<()>; - - fn create_channel(&self, cid: i64, gid: i64, uid: i64, name: String) -> Result<()>; - fn get_channel(&self, id: i64) -> Result<Channel>; - fn update_channel(&self, id: i64, name: String) -> Result<()>; - fn delete_channel(&self, id: i64) -> Result<()>; - fn get_channel_members(&self, gid: i64) -> Result<Vec<i64>>; - fn remove_channel_member(&self, cid: i64, id: i64) -> Result<()>; - fn add_channel_member(&self,cid: i64, id: i64) -> Result<()>; - - fn get_user_groups(&self, id: i64) -> Result<Vec<i64>>; - fn delete_user_groups(&self, id: i64) -> Result<()>; - fn add_user_group(&self, uid: i64, gid: i64) -> Result<()>; - fn remove_user_group(&self, uid: i64, gid: i64) -> Result<()>; - - fn get_message(&self, id: i64) -> Result<Message>; - fn get_messages(&self, cid: i64, num: u64) -> Result<Vec<Message>>; +pub trait Database: Sync + Send { + fn valid_id(&self, kind: IdType, id: i64) -> Result<bool>; + + fn create_user(&self, id: i64, name: String, email: String, hash: String) -> Result<()>; + fn update_user(&self, id: i64, name: String, email: String) -> Result<()>; + fn get_user(&self, id: i64) -> Result<User>; + fn get_user_hash(&self, id: i64) -> Result<String>; + fn delete_user(&self, id: i64) -> Result<()>; + + fn create_group(&self, gid: i64, uid: i64, name: String) -> Result<()>; + fn update_group(&self, id: i64, name: String) -> Result<()>; + fn get_group(&self, id: i64) -> Result<Group>; + fn delete_group(&self, id: i64) -> Result<()>; + fn get_group_members(&self, gid: i64) -> Result<Vec<i64>>; + fn remove_group_member(&self, gid: i64, uid: i64) -> Result<()>; + fn add_group_member(&self, gid: i64, uid: i64) -> Result<()>; + fn get_group_channels(&self, gid: i64) -> Result<Vec<i64>>; + fn remove_group_channel(&self, gid: i64, uid: i64) -> Result<()>; + fn add_group_channel(&self, gid: i64, uid: i64) -> Result<()>; + + fn create_channel(&self, cid: i64, gid: i64, uid: i64, name: String) -> Result<()>; + fn get_channel(&self, id: i64) -> Result<Channel>; + fn update_channel(&self, id: i64, name: String) -> Result<()>; + fn delete_channel(&self, id: i64) -> Result<()>; + fn get_channel_members(&self, gid: i64) -> Result<Vec<i64>>; + fn remove_channel_member(&self, cid: i64, id: i64) -> Result<()>; + fn add_channel_member(&self,cid: i64, id: i64) -> Result<()>; + + fn create_user_groups(&self, id: i64) -> Result<()>; + fn get_user_groups(&self, id: i64) -> Result<Vec<i64>>; + fn delete_user_groups(&self, id: i64) -> Result<()>; + fn add_user_group(&self, uid: i64, gid: i64) -> Result<()>; + fn remove_user_group(&self, uid: i64, gid: i64) -> Result<()>; + + fn get_message(&self, id: i64) -> Result<Message>; + fn get_messages(&self, cid: i64, num: u64) -> Result<Vec<Message>>; } pub struct Cassandra { - kspc: String, - sess: Session + kspc: String, + sess: Session } impl Cassandra { - pub fn new(keyspc: &str) -> Self { - let contact_points = "127.0.0.1"; - let mut cluster = Cluster::default(); - cluster.set_contact_points(contact_points).unwrap(); - cluster.set_load_balance_round_robin(); - let session = cluster.connect().unwrap(); - - session.execute(&stmt!(&format!( - "CREATE KEYSPACE IF NOT EXISTS {keyspc} \ - WITH replication = {{'class':'SimpleStrategy', 'replication_factor': 1}}" - ))).wait().unwrap(); - - session.execute(&stmt!(&format!( - "CREATE TABLE IF NOT EXISTS {keyspc}.users \ - (id bigint PRIMARY KEY, name text, email text, hash text);" - ))).wait().unwrap(); - - session.execute(&stmt!(&format!( - "CREATE TABLE IF NOT EXISTS {keyspc}.groups \ - (id bigint PRIMARY KEY, name text, members set<bigint>, channels set<bigint>);" - ))).wait().unwrap(); - - session.execute(&stmt!(&format!( - "CREATE TABLE IF NOT EXISTS {keyspc}.channels \ - (id bigint PRIMARY KEY, name text, group bigint, members set<bigint>);" - ))).wait().unwrap(); - - session.execute(&stmt!(&format!( - "CREATE TABLE IF NOT EXISTS {keyspc}.user_groups \ - (id bigint PRIMARY KEY, groups set<bigint>);" - ))).wait().unwrap(); - - session.execute(&stmt!(&format!( - "CREATE TABLE IF NOT EXISTS {keyspc}.messages \ - (channel bigint, id bigint, author bigint, \ - time timestamp, content text, PRIMARY KEY (channel, id)) \ - WITH CLUSTERING ORDER BY (id DESC);" - ))).wait().unwrap(); - - Self { - kspc: keyspc.to_string(), - sess: session - } - } - - fn delete_row(&self, table: &str, id: i64) -> Result<()> { - self.sess.execute(&stmt!(&format!( - "DELETE FROM {}.{table} WHERE id={id};", self.kspc - ))).wait().unwrap(); - Ok(()) - } - - fn get_set(&self, table: &str, set: &str, id: i64) -> Result<Vec<i64>> { - let res = self.sess.execute(&stmt!(&format!( - "SELECT {set} FROM {}.{table} WHERE id = {id};", self.kspc - ))).wait()?; - let row = res.first_row().unwrap(); - let items: SetIterator = row.get(0)?; - Ok(items.map(|i| i.get_i64().unwrap()).collect()) - } - - fn pop_set(&self, table: &str, set: &str, id: i64, elem: i64) -> Result<()> { - self.sess.execute(&stmt!(&format!( - "UPDATE {}.{table} SET {set} = {set} - {{{elem}}} WHERE ID={id};", self.kspc - ))).wait()?; - Ok(()) - } - - fn push_set(&self, table: &str, set: &str, id: i64, elem: i64) -> Result<()> { - self.sess.execute(&stmt!(&format!( - "UPDATE {}.{table} SET {set} = {set} + {{{elem}}} WHERE ID={id};", self.kspc - ))).wait()?; - Ok(()) - } + pub fn new(keyspc: &str) -> Self { + let contact_points = "127.0.0.1"; + let mut cluster = Cluster::default(); + cluster.set_contact_points(contact_points).unwrap(); + cluster.set_load_balance_round_robin(); + let session = cluster.connect().unwrap(); + + session.execute(&stmt!(&format!( + "CREATE KEYSPACE IF NOT EXISTS {keyspc} \ + WITH replication = {{'class':'SimpleStrategy', 'replication_factor': 1}}" + ))).wait().unwrap(); + + session.execute(&stmt!(&format!( + "CREATE TABLE IF NOT EXISTS {keyspc}.users \ + (id bigint PRIMARY KEY, name text, email text, hash text);" + ))).wait().unwrap(); + + session.execute(&stmt!(&format!( + "CREATE TABLE IF NOT EXISTS {keyspc}.groups \ + (id bigint PRIMARY KEY, name text, members set<bigint>, channels set<bigint>);" + ))).wait().unwrap(); + + session.execute(&stmt!(&format!( + "CREATE TABLE IF NOT EXISTS {keyspc}.channels \ + (id bigint PRIMARY KEY, name text, group bigint, members set<bigint>);" + ))).wait().unwrap(); + + session.execute(&stmt!(&format!( + "CREATE TABLE IF NOT EXISTS {keyspc}.user_groups \ + (id bigint PRIMARY KEY, groups set<bigint>);" + ))).wait().unwrap(); + + session.execute(&stmt!(&format!( + "CREATE TABLE IF NOT EXISTS {keyspc}.messages \ + (channel bigint, id bigint, author bigint, \ + time timestamp, content text, PRIMARY KEY (channel, id)) \ + WITH CLUSTERING ORDER BY (id DESC);" + ))).wait().unwrap(); + + Self { + kspc: keyspc.to_string(), + sess: session + } + } + + fn delete_row(&self, table: &str, id: i64) -> Result<()> { + self.sess.execute(&stmt!(&format!( + "DELETE FROM {}.{table} WHERE id={id};", self.kspc + ))).wait().unwrap(); + Ok(()) + } + + fn get_set(&self, table: &str, set: &str, id: i64) -> Result<Vec<i64>> { + let res = self.sess.execute(&stmt!(&format!( + "SELECT {set} FROM {}.{table} WHERE id = {id};", self.kspc + ))).wait()?; + let row = res.first_row().unwrap(); + let set: Value = row.get_column(0)?; + Ok(match set.is_null() { + true => Vec::new(), + false => set.get_set()?.map(|i| i.get_i64().unwrap()).collect() + }) + } + + fn pop_set(&self, table: &str, set: &str, id: i64, elem: i64) -> Result<()> { + self.sess.execute(&stmt!(&format!( + "UPDATE {}.{table} SET {set} = {set} - {{{elem}}} WHERE ID={id};", self.kspc + ))).wait()?; + Ok(()) + } + + fn push_set(&self, table: &str, set: &str, id: i64, elem: i64) -> Result<()> { + self.sess.execute(&stmt!(&format!( + "UPDATE {}.{table} SET {set} = {set} + {{{elem}}} WHERE ID={id};", self.kspc + ))).wait()?; + Ok(()) + } } impl Database for Cassandra { - fn valid_id(&self, kind: IdType, id: i64) -> Result<bool> { - let table = match kind { - IdType::User => "users", - IdType::Group => "groups", - IdType::Channel => "channels", - IdType::Message => "messages", - }; - let res = self.sess.execute(&stmt!(&format!( - "SELECT * FROM {}.{table} WHERE ID={id};", self.kspc - ))).wait()?; - if let Some(_row) = res.first_row() { - return Ok(true) - } else { return Ok(false) }; - } - - fn create_user(&self, id: i64, name: String, email: String, hash: String) -> Result<()> { - let mut stmt = stmt!(&format!( - "INSERT INTO {}.users (id, name, email, hash) VALUES ({id}, ?, ?, ?);", self.kspc - )); - stmt.bind(0, name.as_str())?; - stmt.bind(1, email.as_str())?; - stmt.bind(2, hash.as_str())?; - self.sess.execute(&stmt).wait()?; - Ok(()) - } - - fn get_user(&self, id: i64) -> Result<User> { - let res = self.sess.execute(&stmt!(&format!( - "SELECT name, email FROM {}.users WHERE ID={id};", self.kspc - ))).wait()?; - let row = res.first_row().unwrap(); - Ok(User { - id, - username: row.get(0)?, - email: row.get(1)? - }) - } - - fn get_user_hash(&self, id: i64) -> Result<String> { - let res = self.sess.execute(&stmt!(&format!( - "SELECT hash FROM {}.users WHERE ID={id};", self.kspc - ))).wait()?; - let row = res.first_row().unwrap(); - Ok(row.get(0)?) - } - - fn update_user(&self, id: i64, name: String, email: String) -> Result<()> { - let mut stmt = stmt!(&format!( - "UPDATE {}.users SET name=?, email=? WHERE ID={id};", self.kspc - )); - stmt.bind(0, name.as_str())?; - stmt.bind(1, email.as_str())?; - self.sess.execute(&stmt).wait()?; - Ok(()) - } - - fn delete_user(&self, id: i64) -> Result<()> { - self.delete_row("users", id) - } - - fn get_group(&self, id: i64) -> Result<Group> { - let res = self.sess.execute(&stmt!(&format!( - "SELECT name, members, channels FROM {}.groups WHERE ID={id};", self.kspc - ))).wait()?; - let row = res.first_row().unwrap(); - let members: SetIterator = row.get(1)?; - let channels: SetIterator = row.get(2)?; - Ok(Group { - id, - name: row.get(0)?, - members: members.map(|i| i.get_i64().unwrap()).collect(), - channels: channels.map(|i| i.get_i64().unwrap()).collect(), - }) - } - - fn create_group(&self, gid: i64, uid: i64, name: String) -> Result<()> { - let mut stmt = stmt!(&format!( - "INSERT INTO {}.groups (id, name, channels, members) VALUES ({gid}, ?, {{}}, {{{uid}}});", self.kspc - )); - stmt.bind(0, name.as_str())?; - self.sess.execute(&stmt).wait()?; - Ok(()) - } - - fn delete_group(&self, id: i64) -> Result<()> { - self.delete_row("groups", id) - } - - fn update_group(&self, id: i64, name: String) -> Result<()> { - let mut stmt = stmt!(&format!( - "UPDATE {}.groups SET name = ? WHERE id = {id};", self.kspc - )); - stmt.bind(0, name.as_str())?; - self.sess.execute(&stmt).wait()?; - Ok(()) - } - - fn get_group_members(&self, gid: i64) -> Result<Vec<i64>> { - self.get_set("groups", "members", gid) - } - - fn get_group_channels(&self, gid: i64) -> Result<Vec<i64>> { - self.get_set("groups", "channels", gid) - } - - fn add_group_member(&self, gid: i64, uid: i64) -> Result<()> { - self.push_set("groups", "members", gid, uid) - } - - fn remove_group_member(&self, gid: i64, uid: i64) -> Result<()> { - self.pop_set("groups", "members", gid, uid) - } - - fn add_group_channel(&self, gid: i64, uid: i64) -> Result<()> { - self.push_set("groups", "channels", gid, uid) - } - - fn remove_group_channel(&self, gid: i64, uid: i64) -> Result<()> { - self.pop_set("groups", "channels", gid, uid) - } - - fn get_channel(&self, id: i64) -> Result<Channel> { - let res = self.sess.execute(&stmt!(&format!( - "SELECT group, name, members FROM {}.channels WHERE ID={id};", self.kspc - ))).wait()?; - let row = res.first_row().unwrap(); - let members: SetIterator = row.get(2)?; - Ok(Channel { - id, - group: row.get(0)?, - name: row.get(1)?, - members: members.map(|i| i.get_i64().unwrap()).collect(), - }) - } - - fn create_channel(&self, cid: i64, gid: i64, uid: i64, name: String) -> Result<()> { - let mut stmt = stmt!(&format!( - "INSERT INTO {}.channels (id, group, name, members) VALUES ({cid}, {gid}, ?, {{{uid}}});", self.kspc - )); - stmt.bind(0, name.as_str())?; - self.sess.execute(&stmt).wait()?; - Ok(()) - } - - fn delete_channel(&self, id: i64) -> Result<()> { - self.delete_row("channels", id) - } - - fn update_channel(&self, id: i64, name: String) -> Result<()> { - let mut stmt = stmt!(&format!( - "UPDATE {}.channels SET name = ? WHERE id = {id};", self.kspc - )); - stmt.bind(0, name.as_str())?; - self.sess.execute(&stmt).wait()?; - Ok(()) - } - - fn get_channel_members(&self, cid: i64) -> Result<Vec<i64>> { - self.get_set("channel", "members", cid) - } - - fn add_channel_member(&self, gid: i64, uid: i64) -> Result<()> { - self.push_set("channels", "members", gid, uid) - } - - fn remove_channel_member(&self, gid: i64, uid: i64) -> Result<()> { - self.pop_set("channels", "members", gid, uid) - } - - fn get_user_groups(&self, id: i64) -> Result<Vec<i64>> { - self.get_set("user_groups", "groups", id) - } - - fn add_user_group(&self, uid: i64, gid: i64) -> Result<()> { - self.push_set("user_groups", "groups", uid, gid) - } - - fn remove_user_group(&self, uid: i64, gid: i64) -> Result<()> { - self.pop_set("user_groups", "groups", uid, gid) - } - - fn delete_user_groups(&self, id: i64) -> Result<()> { - self.delete_row("user_groups", id) - } - - // TODO the unwraps here are not great - fn get_messages(&self, cid: i64, num: u64) -> Result<Vec<Message>> { - let res = self.sess.execute(&stmt!(&format!( - "SELECT * FROM {}.messages WHERE channel={cid} LIMIT {num};", self.kspc - ))).wait()?; - Ok(res.iter().map(|row| { - Message { - id: row.get(0).unwrap(), - author: row.get(2).unwrap(), - channel: row.get(1).unwrap(), - content: row.get(4).unwrap() - } - }).collect::<Vec<Message>>()) - } - - fn get_message(&self, id: i64) -> Result<Message> { - let res = self.sess.execute(&stmt!(&format!( - "SELECT channel, author, content, time FROM {}.users WHERE ID={id};", self.kspc - ))).wait()?; - let row = res.first_row().unwrap(); - Ok(Message { - id, - channel: row.get(0)?, - author: row.get(1)?, - content: row.get(2)?, - }) - } + fn valid_id(&self, kind: IdType, id: i64) -> Result<bool> { + let table = match kind { + IdType::User => "users", + IdType::Group => "groups", + IdType::Channel => "channels", + IdType::Message => "messages", + }; + let res = self.sess.execute(&stmt!(&format!( + "SELECT * FROM {}.{table} WHERE ID={id};", self.kspc + ))).wait()?; + if let Some(_row) = res.first_row() { + return Ok(true) + } else { return Ok(false) }; + } + + fn create_user(&self, id: i64, name: String, email: String, hash: String) -> Result<()> { + let mut stmt = stmt!(&format!( + "INSERT INTO {}.users (id, name, email, hash) VALUES ({id}, ?, ?, ?);", self.kspc + )); + stmt.bind(0, name.as_str())?; + stmt.bind(1, email.as_str())?; + stmt.bind(2, hash.as_str())?; + self.sess.execute(&stmt).wait()?; + Ok(()) + } + + fn get_user(&self, id: i64) -> Result<User> { + let res = self.sess.execute(&stmt!(&format!( + "SELECT name, email FROM {}.users WHERE ID={id};", self.kspc + ))).wait()?; + let row = res.first_row().unwrap(); + Ok(User { + id, + username: row.get(0)?, + email: row.get(1)? + }) + } + + fn get_user_hash(&self, id: i64) -> Result<String> { + let res = self.sess.execute(&stmt!(&format!( + "SELECT hash FROM {}.users WHERE ID={id};", self.kspc + ))).wait()?; + let row = res.first_row().unwrap(); + Ok(row.get(0)?) + } + + fn update_user(&self, id: i64, name: String, email: String) -> Result<()> { + let mut stmt = stmt!(&format!( + "UPDATE {}.users SET name=?, email=? WHERE ID={id};", self.kspc + )); + stmt.bind(0, name.as_str())?; + stmt.bind(1, email.as_str())?; + self.sess.execute(&stmt).wait()?; + Ok(()) + } + + fn delete_user(&self, id: i64) -> Result<()> { + self.delete_row("users", id) + } + + fn get_group(&self, id: i64) -> Result<Group> { + let res = self.sess.execute(&stmt!(&format!( + "SELECT name, members, channels FROM {}.groups WHERE ID={id};", self.kspc + ))).wait()?; + let row = res.first_row().unwrap(); + let members: SetIterator = row.get(1)?; + let channels: SetIterator = row.get(2)?; + Ok(Group { + id, + name: row.get(0)?, + members: members.map(|i| i.get_i64().unwrap()).collect(), + channels: channels.map(|i| i.get_i64().unwrap()).collect(), + }) + } + + fn create_group(&self, gid: i64, uid: i64, name: String) -> Result<()> { + let mut stmt = stmt!(&format!( + "INSERT INTO {}.groups (id, name, channels, members) VALUES ({gid}, ?, {{}}, {{{uid}}});", self.kspc + )); + stmt.bind(0, name.as_str())?; + self.sess.execute(&stmt).wait()?; + Ok(()) + } + + fn delete_group(&self, id: i64) -> Result<()> { + self.delete_row("groups", id) + } + + fn update_group(&self, id: i64, name: String) -> Result<()> { + let mut stmt = stmt!(&format!( + "UPDATE {}.groups SET name = ? WHERE id = {id};", self.kspc + )); + stmt.bind(0, name.as_str())?; + self.sess.execute(&stmt).wait()?; + Ok(()) + } + + fn get_group_members(&self, gid: i64) -> Result<Vec<i64>> { + self.get_set("groups", "members", gid) + } + + fn get_group_channels(&self, gid: i64) -> Result<Vec<i64>> { + self.get_set("groups", "channels", gid) + } + + fn add_group_member(&self, gid: i64, uid: i64) -> Result<()> { + self.push_set("groups", "members", gid, uid) + } + + fn remove_group_member(&self, gid: i64, uid: i64) -> Result<()> { + self.pop_set("groups", "members", gid, uid) + } + + fn add_group_channel(&self, gid: i64, cid: i64) -> Result<()> { + self.push_set("groups", "channels", gid, cid) + } + + fn remove_group_channel(&self, gid: i64, cid: i64) -> Result<()> { + self.pop_set("groups", "channels", gid, cid) + } + + fn get_channel(&self, id: i64) -> Result<Channel> { + let res = self.sess.execute(&stmt!(&format!( + "SELECT group, name, members FROM {}.channels WHERE ID={id};", self.kspc + ))).wait()?; + let row = res.first_row().unwrap(); + let members: SetIterator = row.get(2)?; + Ok(Channel { + id, + group: row.get(0)?, + name: row.get(1)?, + members: members.map(|i| i.get_i64().unwrap()).collect(), + }) + } + + fn create_channel(&self, cid: i64, gid: i64, uid: i64, name: String) -> Result<()> { + let mut stmt = stmt!(&format!( + "INSERT INTO {}.channels (id, group, name, members) VALUES ({cid}, {gid}, ?, {{{uid}}});", self.kspc + )); + stmt.bind(0, name.as_str())?; + self.sess.execute(&stmt).wait()?; + Ok(()) + } + + fn delete_channel(&self, id: i64) -> Result<()> { + self.delete_row("channels", id) + } + + fn update_channel(&self, id: i64, name: String) -> Result<()> { + let mut stmt = stmt!(&format!( + "UPDATE {}.channels SET name = ? WHERE id = {id};", self.kspc + )); + stmt.bind(0, name.as_str())?; + self.sess.execute(&stmt).wait()?; + Ok(()) + } + + fn get_channel_members(&self, cid: i64) -> Result<Vec<i64>> { + self.get_set("channel", "members", cid) + } + + fn add_channel_member(&self, gid: i64, uid: i64) -> Result<()> { + self.push_set("channels", "members", gid, uid) + } + + fn remove_channel_member(&self, gid: i64, uid: i64) -> Result<()> { + self.pop_set("channels", "members", gid, uid) + } + + fn create_user_groups(&self, id: i64) -> Result<()> { + self.sess.execute(&stmt!(&format!( + "INSERT INTO {}.user_groups (id, groups) VALUES ({id}, {{}});", self.kspc + ))).wait()?; + Ok(()) + } + + fn get_user_groups(&self, id: i64) -> Result<Vec<i64>> { + self.get_set("user_groups", "groups", id) + } + + fn add_user_group(&self, uid: i64, gid: i64) -> Result<()> { + self.push_set("user_groups", "groups", uid, gid) + } + + fn remove_user_group(&self, uid: i64, gid: i64) -> Result<()> { + self.pop_set("user_groups", "groups", uid, gid) + } + + fn delete_user_groups(&self, id: i64) -> Result<()> { + self.delete_row("user_groups", id) + } + + // TODO the unwraps here are not great + fn get_messages(&self, cid: i64, num: u64) -> Result<Vec<Message>> { + let res = self.sess.execute(&stmt!(&format!( + "SELECT * FROM {}.messages WHERE channel={cid} LIMIT {num};", self.kspc + ))).wait()?; + Ok(res.iter().map(|row| { + Message { + id: row.get(0).unwrap(), + author: row.get(2).unwrap(), + channel: row.get(1).unwrap(), + content: row.get(4).unwrap() + } + }).collect::<Vec<Message>>()) + } + + fn get_message(&self, id: i64) -> Result<Message> { + let res = self.sess.execute(&stmt!(&format!( + "SELECT channel, author, content, time FROM {}.users WHERE ID={id};", self.kspc + ))).wait()?; + let row = res.first_row().unwrap(); + Ok(Message { + id, + channel: row.get(0)?, + author: row.get(1)?, + content: row.get(2)?, + }) + } } diff --git a/scuttlebutt/src/main.rs b/scuttlebutt/src/main.rs @@ -2,15 +2,16 @@ use chrono::{DateTime, Duration, Local, Utc}; use hmac::Hmac; use jwt::{SignWithKey, VerifyWithKey}; use poem::{ - listener::TcpListener, web::Data, EndpointExt, Request, Result, - Route, Server, + listener::TcpListener, web::Data, EndpointExt, Request, Result, + Route, Server, }; use poem_openapi::{ - auth::ApiKey, - param::Query, - payload::{Json, PlainText}, - *, + auth::ApiKey, + param::Query, + payload::{Json, PlainText}, + *, }; +use std::sync::Mutex; use rand::{distributions::Alphanumeric, Rng}; use rustflake::Snowflake; use serde::{Deserialize, Serialize}; @@ -26,428 +27,423 @@ type ServerKey = Hmac<Sha256>; #[derive(Serialize, Deserialize)] struct Claims { - id: i64, - exp: DateTime<Local>, + id: i64, + exp: DateTime<Local>, } /// ApiKey authorization #[derive(SecurityScheme)] #[oai( - type = "api_key", - key_name = "Authorization", - in = "header", - checker = "api_checker" + type = "api_key", + key_name = "Authorization", + in = "header", + checker = "api_checker" )] struct Authorization(Claims); -struct IdGenerator(Snowflake); - -impl Clone for IdGenerator { - fn clone(&self) -> Self { - println!("Cloning from thread {:?}", std::thread::current().id()); - let now = Utc::now().timestamp_nanos(); - // TODO generalize hardcoded numbers - IdGenerator(Snowflake::new(now, 1, 0)) - } -} - -async fn api_checker(req: &Request, api_key: ApiKey) -> Option<Claims> { - let claims: Claims = serde_json::from_str( - &String::from_utf8(base64::decode(api_key.key.split(".").nth(1).unwrap()).unwrap()) - .unwrap(), - ) - .unwrap(); - if claims.exp < Local::now() { - return None; - } - let server_key = req.data::<ServerKey>().unwrap(); - VerifyWithKey::<Claims>::verify_with_key(api_key.key.as_str(), server_key).ok() +async fn api_checker(req: &Request, api_key: ApiKey) -> Option<Claims> { + let encoded_claims_str = match api_key.key.split(".").nth(1) { + None => return None, + Some(s) => s, + }; + let claims_str = match base64::decode(encoded_claims_str) { + Err(_) => return None, + Ok(s) => s, + }; + let claims: Claims = match serde_json::from_str(&String::from_utf8(claims_str).unwrap()) { + Err(_) => return None, + Ok(c) => c + }; + if claims.exp < Local::now() { + return None; + } + let server_key = req.data::<ServerKey>().unwrap(); + VerifyWithKey::<Claims>::verify_with_key(api_key.key.as_str(), server_key).ok() } struct Api { - db: Box<dyn Database>, + db: Box<dyn Database>, } pub fn gen_id() -> i64 { - // Very cursed thread-unique number generation - // NOTE: substitute for std::thread::current().id() when it's stabilized - thread_local! { static V: u8 = 0; } - let id: u64 = V.with(|v| v as *const u8 as u64); - - let now = Utc::now().timestamp_nanos(); - // TODO generalize this hardcoded 1 - Snowflake::new(now, 1, id as i64).generate() + static STATE: Mutex<Option<Snowflake>> = Mutex::new(None); + + STATE + .lock() + .unwrap() + .get_or_insert_with(|| Snowflake::default()) + .generate() } #[OpenApi] #[allow(unused_variables)] impl Api { - fn new(db: Box<dyn Database>) -> Api { - Api { db } - } - - fn __remove_group_member(&self, gid: i64, uid: i64) { - self.db.remove_group_member(gid, uid).unwrap(); - let channels = self.db.get_group_channels(gid).unwrap(); - for channel in channels { - self.db.remove_channel_member(channel, uid).unwrap(); - } - self.db.remove_user_group(uid, gid).unwrap(); - } - - #[oai(path = "/login", method = "post")] - async fn login(&self, key: Data<&ServerKey>, id: Query<i64>, hash: PlainText<String>) -> LoginResponse { - use LoginResponse::*; - if hash.0.len() != 64 { - return BadRequest; - } else if !self.db.valid_id(IdType::User, id.0).unwrap() { - return NotFound; - } - let db_hash = self.db.get_user_hash(id.0).unwrap(); - if hex::decode(db_hash).unwrap() != hex::decode(hash.0).unwrap() { - Unauthorized - } else { - let token = Claims { - id: id.0, - exp: Local::now() + Duration::days(1), - } - .sign_with_key(key.0); - Success(PlainText(token.unwrap())) - } - } - - #[oai(path = "/user", method = "get")] - /// Gets the user with the given ID - /// - /// # Example - /// - /// Call `/user?id=1234` to get the user with id 1234 - async fn get_user(&self, id: Query<i64>) -> UserResponse { - use UserResponse::*; - if !self.db.valid_id(IdType::User, id.0).unwrap() { return NotFound; } - match self.db.get_user(id.0) { - Ok(user) => Success(Json(user)), - Err(e) => InternalError(PlainText(e.to_string())) - } - } - - #[oai(path = "/user", method = "post")] - /// Creates a new user - async fn make_user(&self, idgen: Data<&IdGenerator>, name: Query<String>, email: Query<String>, hash: Query<String>) -> CreateUserResponse { - use CreateUserResponse::*; - if hash.0.len() != 64 { - return BadRequest(PlainText("Invalid hash provided.".to_string())); - } - let id = (*idgen.0).0.generate(); - self.db.create_user(id, name.0.clone(), email.0.clone(), hash.0).unwrap(); - Success(Json(User { - id, - username: name.0, - email: email.0, - })) - } - - #[oai(path = "/user", method = "put")] - /// Updates your current name and email - async fn update_user(&self, auth: Authorization, name: Query<String>, email: Query<String>) -> GenericResponse { - use GenericResponse::*; - self.db.update_user(auth.0.id, name.0, email.0).unwrap(); - Success - } - - #[oai(path = "/user", method = "delete")] - /// Deletes your user - async fn delete_user(&self, auth: Authorization) -> DeleteResponse { - use DeleteResponse::*; - self.db.delete_user(auth.0.id).unwrap(); - for group in self.db.get_user_groups(auth.0.id).unwrap() { - self.__remove_group_member(group, auth.0.id); - } - self.db.delete_user_groups(auth.0.id).unwrap(); - Success - } - - #[oai(path = "/user/groups", method = "get")] - /// Gets all groups accessible to you - async fn get_groups(&self, auth: Authorization) -> GroupsResponse { - use GroupsResponse::*; - let groups = self.db.get_user_groups(auth.0.id).unwrap(); - let group_vec = groups.iter().map(|i| { - self.db.get_group(*i).unwrap() - }).collect(); - Success(Json(group_vec)) - } - - #[oai(path = "/user/groups", method = "delete")] - /// Leaves a group accessible to you - async fn leave_group(&self, auth: Authorization, gid: Query<i64>) -> GenericResponse { - use GenericResponse::*; - if !self.db.valid_id(IdType::Group, gid.0).unwrap() { - return NotFound(PlainText("Group not found".to_string())); - } - self.__remove_group_member(gid.0, auth.0.id); - Success - } - - #[oai(path = "/group", method = "get")] - /// Gets the group with the given ID - async fn get_group(&self, auth: Authorization, id: Query<i64>) -> GroupResponse { - use GroupResponse::*; - if !self.db.valid_id(IdType::Group, id.0).unwrap() { return NotFound; } - Success(Json(self.db.get_group(id.0).unwrap())) - } - - #[oai(path = "/group", method = "post")] - /// Creates a new group - async fn make_group(&self, auth: Authorization, name: Query<String>) -> CreateGroupResponse { - use CreateGroupResponse::*; - let gid = gen_id(); - let cid = gen_id(); - if name.0 == "" { - return BadRequest(PlainText("Empty string not allowed for name".to_string())) - } - self.db.create_group(gid, auth.0.id, name.0.clone()).unwrap(); - self.db.create_channel(cid, gid, auth.0.id, String::from("main")).unwrap(); - self.db.add_group_channel(gid, cid).unwrap(); - self.db.add_user_group(auth.0.id, gid).unwrap(); - Success(Json(Group { - id: gid, - name: name.0, - members: vec![auth.0.id], - channels: vec![cid], - })) - } - - #[oai(path = "/group", method = "put")] - /// Updates the name of an existing group - async fn update_group(&self, auth: Authorization, id: Query<i64>, name: Query<String>) -> GenericResponse { - use GenericResponse::*; - if name.0 == "" { - return BadRequest(PlainText("Empty string not allowed for name".to_string())) - } else if !self.db.valid_id(IdType::Group, id.0).unwrap() { - return NotFound(PlainText("Didn't find group or experienced database error.".to_string())); - } - self.db.update_group(id.0, name.0).unwrap(); - Success - } - - #[oai(path = "/group", method = "delete")] - /// Deletes a group - async fn delete_group(&self, auth: Authorization, id: Query<i64>) -> DeleteResponse { - use DeleteResponse::*; - if !self.db.valid_id(IdType::Group, id.0).unwrap() { - return NotFound(PlainText("Group not found".to_string())); - } - let group = self.db.get_group(id.0).unwrap(); - for member in group.members { - self.db.remove_user_group(member, id.0).unwrap(); - } - for channel in group.channels { - self.db.delete_channel(channel).unwrap(); - } - self.db.delete_group(id.0).unwrap(); - Success - } - - #[oai(path = "/group/members", method = "get")] - /// Gets the members of the specified group - async fn get_group_members(&self, auth: Authorization, id: Query<i64>) -> MembersResponse { - use MembersResponse::*; - if !self.db.valid_id(IdType::Group, id.0).unwrap() { - return NotFound; - } - let members = self.db.get_group_members(id.0).unwrap(); - Success(Json(members.iter().map(|m| { - self.db.get_user(*m).unwrap() - }).collect::<Vec<User>>())) - } - - #[oai(path = "/group/members", method = "put")] - /// Adds a member to an existing group - async fn add_group_member(&self, auth: Authorization, gid: Query<i64>, uid: Query<i64>) -> GenericResponse { - use GenericResponse::*; - if !self.db.valid_id(IdType::Group, gid.0).unwrap() { - return NotFound(PlainText("Group not found".to_string())); - } - self.db.add_group_member(gid.0, uid.0).unwrap(); - let channels = self.db.get_group_channels(gid.0).unwrap(); - self.db.add_channel_member(channels[0], uid.0).unwrap(); - self.db.add_user_group(uid.0, gid.0).unwrap(); - Success - } - - #[oai(path = "/group/members", method = "delete")] - /// Removes a member from an existing group - async fn remove_group_member(&self, auth: Authorization, gid: Query<i64>, uid: Query<i64>) -> DeleteResponse { - use DeleteResponse::*; - if !self.db.valid_id(IdType::Group, gid.0).unwrap() { - return NotFound(PlainText("Group not found".to_string())) - } else if !self.db.valid_id(IdType::User, uid.0).unwrap() { - return NotFound(PlainText("User not found".to_string())) - } - self.__remove_group_member(gid.0, uid.0); - Success - } - - #[oai(path = "/group/channels", method = "get")] - /// Gets all channels in a group that are accessible to you - async fn get_channels(&self, auth: Authorization, gid: Query<i64>) -> ChannelsResponse { - use ChannelsResponse::*; - if !self.db.valid_id(IdType::Group, gid.0).unwrap() { - return NotFound; - } - let channels = self.db.get_group_channels(gid.0).unwrap(); - Success(Json(channels.iter().map(|c| { - self.db.get_channel(*c).unwrap() - }).collect::<Vec<Channel>>())) - } - - #[oai(path = "/group/channels", method = "post")] - /// CREATES a channel in a group - async fn make_channel(&self, auth: Authorization, gid: Query<i64>, name: Query<String>) -> CreateChannelResponse { - use CreateChannelResponse::*; - if name.0 == "" { - return BadRequest(PlainText("Empty string not allowed for name".to_string())) - } else if !self.db.valid_id(IdType::Group, gid.0).unwrap() { - return NotFound(PlainText("Group not found".to_string())); - } - let cid = gen_id(); - self.db.create_channel(cid, gid.0, auth.0.id, name.0.clone()).unwrap(); - self.db.add_group_channel(cid, gid.0).unwrap(); - Success(Json(Channel { - id: cid, - name: name.0, - group: gid.0, - members: vec![auth.0.id] - })) - } - - #[oai(path = "/channel", method = "put")] - /// Updates the name of a channel - async fn update_channel(&self, auth: Authorization, id: Query<i64>, name: Query<String>) -> GenericResponse { - use GenericResponse::*; - if !self.db.valid_id(IdType::Channel, id.0).unwrap() { - return NotFound(PlainText("Channel not found".to_string())); - } - self.db.update_channel(id.0, name.0).unwrap(); - Success - } - - #[oai(path = "/channel", method = "get")] - /// Gets a channel - async fn get_channel(&self, auth: Authorization, id: Query<i64>) -> ChannelResponse { - use ChannelResponse::*; - if !self.db.valid_id(IdType::Channel, id.0).unwrap() { - return NotFound; - } - Success(Json(self.db.get_channel(id.0).unwrap())) - } - - #[oai(path = "/channel", method = "delete")] - /// Deletes a channel - async fn delete_channel(&self, auth: Authorization, id: Query<i64>) -> DeleteResponse { - use DeleteResponse::*; - if !self.db.valid_id(IdType::Channel, id.0).unwrap() { - return NotFound(PlainText("Channel not found".to_string())); - } - let channel = self.db.get_channel(id.0).unwrap(); - self.db.remove_group_channel(channel.group, id.0).unwrap(); - self.db.delete_channel(id.0).unwrap(); - Success - } - - #[oai(path = "/channel/members", method = "get")] - /// Gets the members that can access a channel - async fn get_channel_members(&self, auth: Authorization, id: Query<i64>) -> MembersResponse { - use MembersResponse::*; - let members = self.db.get_channel_members(id.0).unwrap(); - Success(Json(members.iter().map(|m| { - self.db.get_user(*m).unwrap() - }).collect::<Vec<User>>())) - } - - #[oai(path = "/channel/members", method = "put")] - /// Adds a member to a channel - async fn add_channel_member(&self, auth: Authorization, id: Query<i64>, uid: Query<i64>) -> GenericResponse { - use GenericResponse::*; - if !self.db.valid_id(IdType::Channel, id.0).unwrap() { - return NotFound(PlainText("Channel not found".to_string())) - } else if !self.db.valid_id(IdType::User, uid.0).unwrap() { - return NotFound(PlainText("User not found".to_string())) - } - self.db.add_channel_member(id.0, uid.0).unwrap(); - Success - } - - #[oai(path = "/channel/members", method = "delete")] - /// Removes a member from a channel - async fn remove_channel_member(&self, auth: Authorization, cid: Query<i64>, uid: Query<i64>) -> DeleteResponse { - use DeleteResponse::*; - if !self.db.valid_id(IdType::Channel, cid.0).unwrap() { - return NotFound(PlainText("Channel not found".to_string())) - } else if !self.db.valid_id(IdType::User, uid.0).unwrap() { - return NotFound(PlainText("User not found".to_string())) - } - self.db.remove_channel_member(cid.0, uid.0).unwrap(); - Success - } - - #[oai(path = "/channel/term", method = "get")] - /// Returns batch of messages in channel containing "term" in the last 100 messages - async fn search_channel(&self, auth: Authorization, cid: Query<i64>, term: Query<String>, off: Query<u64>) -> MessagesResponse { - use MessagesResponse::*; - if !self.db.valid_id(IdType::Channel, cid.0).unwrap() { - return NotFound(PlainText("Channel not found".to_string())) - } - let mut messages = self.db.get_messages(cid.0, 100).unwrap(); - messages.retain(|msg| msg.content.contains(&term.0)); - Success(Json(messages)) - } - - #[oai(path = "/channel/messages", method = "get")] - /// Returns batch of messages in channel. Do not use for small batches. - /// - /// For small batches, use `chatterbox`, the websocket service for messaging, instead. - async fn get_channel_messages(&self, auth: Authorization, cid: Query<i64>, num_msgs: Query<u64>) -> MessagesResponse { - use MessagesResponse::*; - if !self.db.valid_id(IdType::Channel, cid.0).unwrap() { - return NotFound(PlainText("Channel not found".to_string())) - } - Success(Json(self.db.get_messages(cid.0, num_msgs.0).unwrap())) - } + fn new(db: Box<dyn Database>) -> Api { + Api { db } + } + + fn __remove_group_member(&self, gid: i64, uid: i64) { + self.db.remove_group_member(gid, uid).unwrap(); + let channels = self.db.get_group_channels(gid).unwrap(); + for channel in channels { + self.db.remove_channel_member(channel, uid).unwrap(); + } + self.db.remove_user_group(uid, gid).unwrap(); + } + + #[oai(path = "/login", method = "post")] + async fn login(&self, key: Data<&ServerKey>, id: Query<i64>, hash: PlainText<String>) -> LoginResponse { + use LoginResponse::*; + if hash.0.len() != 64 { + return BadRequest; + } else if !self.db.valid_id(IdType::User, id.0).unwrap() { + return NotFound; + } + let db_hash = self.db.get_user_hash(id.0).unwrap(); + if hex::decode(db_hash).unwrap() != hex::decode(hash.0).unwrap() { + Unauthorized + } else { + let token = Claims { + id: id.0, + exp: Local::now() + Duration::days(1), + } + .sign_with_key(key.0); + Success(PlainText(token.unwrap())) + } + } + + #[oai(path = "/user", method = "get")] + /// Gets the user with the given ID + /// + /// # Example + /// + /// Call `/user?id=1234` to get the user with id 1234 + async fn get_user(&self, id: Query<i64>) -> UserResponse { + use UserResponse::*; + if !self.db.valid_id(IdType::User, id.0).unwrap() { return NotFound; } + match self.db.get_user(id.0) { + Ok(user) => Success(Json(user)), + Err(e) => InternalError(PlainText(e.to_string())) + } + } + + #[oai(path = "/user", method = "post")] + /// Creates a new user + async fn make_user(&self, name: Query<String>, email: Query<String>, hash: Query<String>) -> CreateUserResponse { + use CreateUserResponse::*; + if hash.0.len() != 64 { + return BadRequest(PlainText("Invalid hash provided.".to_string())); + } + let id = gen_id(); + self.db.create_user(id, name.0.clone(), email.0.clone(), hash.0).unwrap(); + self.db.create_user_groups(id).unwrap(); + Success(Json(User { + id, + username: name.0, + email: email.0, + })) + } + + #[oai(path = "/user", method = "put")] + /// Updates your current name and email + async fn update_user(&self, auth: Authorization, name: Query<String>, email: Query<String>) -> GenericResponse { + use GenericResponse::*; + self.db.update_user(auth.0.id, name.0, email.0).unwrap(); + Success + } + + #[oai(path = "/user", method = "delete")] + /// Deletes your user + async fn delete_user(&self, auth: Authorization) -> DeleteResponse { + use DeleteResponse::*; + self.db.delete_user(auth.0.id).unwrap(); + for group in self.db.get_user_groups(auth.0.id).unwrap() { + self.__remove_group_member(group, auth.0.id); + } + self.db.delete_user_groups(auth.0.id).unwrap(); + Success + } + + #[oai(path = "/user/groups", method = "get")] + /// Gets all groups accessible to you + async fn get_groups(&self, auth: Authorization) -> GroupsResponse { + use GroupsResponse::*; + let groups = self.db.get_user_groups(auth.0.id).unwrap(); + let group_vec = groups.iter().map(|i| { + self.db.get_group(*i).unwrap() + }).collect(); + Success(Json(group_vec)) + } + + #[oai(path = "/user/groups", method = "delete")] + /// Leaves a group accessible to you + async fn leave_group(&self, auth: Authorization, gid: Query<i64>) -> GenericResponse { + use GenericResponse::*; + if !self.db.valid_id(IdType::Group, gid.0).unwrap() { + return NotFound(PlainText("Group not found".to_string())); + } + self.__remove_group_member(gid.0, auth.0.id); + Success + } + + #[oai(path = "/group", method = "get")] + /// Gets the group with the given ID + async fn get_group(&self, auth: Authorization, id: Query<i64>) -> GroupResponse { + use GroupResponse::*; + if !self.db.valid_id(IdType::Group, id.0).unwrap() { return NotFound; } + Success(Json(self.db.get_group(id.0).unwrap())) + } + + #[oai(path = "/group", method = "post")] + /// Creates a new group + async fn make_group(&self, auth: Authorization, name: Query<String>) -> CreateGroupResponse { + use CreateGroupResponse::*; + let gid = gen_id(); + if name.0 == "" { + return BadRequest(PlainText("Empty string not allowed for name".to_string())) + } + self.db.create_group(gid, auth.0.id, name.0.clone()).unwrap(); + self.db.add_user_group(auth.0.id, gid).unwrap(); + let cid = gen_id(); + self.db.create_channel(cid, gid, auth.0.id, String::from("main")).unwrap(); + self.db.add_group_channel(gid, cid).unwrap(); + Success(Json(Group { + id: gid, + name: name.0, + members: vec![auth.0.id], + channels: vec![cid], + })) + } + + #[oai(path = "/group", method = "put")] + /// Updates the name of an existing group + async fn update_group(&self, auth: Authorization, id: Query<i64>, name: Query<String>) -> GenericResponse { + use GenericResponse::*; + if name.0 == "" { + return BadRequest(PlainText("Empty string not allowed for name".to_string())) + } else if !self.db.valid_id(IdType::Group, id.0).unwrap() { + return NotFound(PlainText("Didn't find group or experienced database error.".to_string())); + } + self.db.update_group(id.0, name.0).unwrap(); + Success + } + + #[oai(path = "/group", method = "delete")] + /// Deletes a group + async fn delete_group(&self, auth: Authorization, id: Query<i64>) -> DeleteResponse { + use DeleteResponse::*; + if !self.db.valid_id(IdType::Group, id.0).unwrap() { + return NotFound(PlainText("Group not found".to_string())); + } + let group = self.db.get_group(id.0).unwrap(); + for member in group.members { + self.db.remove_user_group(member, id.0).unwrap(); + } + for channel in group.channels { + self.db.delete_channel(channel).unwrap(); + } + self.db.delete_group(id.0).unwrap(); + Success + } + + #[oai(path = "/group/members", method = "get")] + /// Gets the members of the specified group + async fn get_group_members(&self, auth: Authorization, id: Query<i64>) -> MembersResponse { + use MembersResponse::*; + if !self.db.valid_id(IdType::Group, id.0).unwrap() { + return NotFound; + } + let members = self.db.get_group_members(id.0).unwrap(); + Success(Json(members.iter().map(|m| { + self.db.get_user(*m).unwrap() + }).collect::<Vec<User>>())) + } + + #[oai(path = "/group/members", method = "put")] + /// Adds a member to an existing group + async fn add_group_member(&self, auth: Authorization, gid: Query<i64>, uid: Query<i64>) -> GenericResponse { + use GenericResponse::*; + if !self.db.valid_id(IdType::Group, gid.0).unwrap() { + return NotFound(PlainText("Group not found".to_string())); + } + self.db.add_group_member(gid.0, uid.0).unwrap(); + let channels = self.db.get_group_channels(gid.0).unwrap(); + self.db.add_channel_member(channels[0], uid.0).unwrap(); + self.db.add_user_group(uid.0, gid.0).unwrap(); + Success + } + + #[oai(path = "/group/members", method = "delete")] + /// Removes a member from an existing group + async fn remove_group_member(&self, auth: Authorization, gid: Query<i64>, uid: Query<i64>) -> DeleteResponse { + use DeleteResponse::*; + if !self.db.valid_id(IdType::Group, gid.0).unwrap() { + return NotFound(PlainText("Group not found".to_string())) + } else if !self.db.valid_id(IdType::User, uid.0).unwrap() { + return NotFound(PlainText("User not found".to_string())) + } + self.__remove_group_member(gid.0, uid.0); + Success + } + + #[oai(path = "/group/channels", method = "get")] + /// Gets all channels in a group that are accessible to you + async fn get_channels(&self, auth: Authorization, gid: Query<i64>) -> ChannelsResponse { + use ChannelsResponse::*; + if !self.db.valid_id(IdType::Group, gid.0).unwrap() { + return NotFound; + } + let channels = self.db.get_group_channels(gid.0).unwrap(); + Success(Json(channels.iter().map(|c| { + self.db.get_channel(*c).unwrap() + }).collect::<Vec<Channel>>())) + } + + #[oai(path = "/group/channels", method = "post")] + /// CREATES a channel in a group + async fn make_channel(&self, auth: Authorization, gid: Query<i64>, name: Query<String>) -> CreateChannelResponse { + use CreateChannelResponse::*; + if name.0 == "" { + return BadRequest(PlainText("Empty string not allowed for name".to_string())) + } else if !self.db.valid_id(IdType::Group, gid.0).unwrap() { + return NotFound(PlainText("Group not found".to_string())); + } + let cid = gen_id(); + self.db.create_channel(cid, gid.0, auth.0.id, name.0.clone()).unwrap(); + self.db.add_group_channel(gid.0, cid).unwrap(); + Success(Json(Channel { + id: cid, + name: name.0, + group: gid.0, + members: vec![auth.0.id] + })) + } + + #[oai(path = "/channel", method = "put")] + /// Updates the name of a channel + async fn update_channel(&self, auth: Authorization, id: Query<i64>, name: Query<String>) -> GenericResponse { + use GenericResponse::*; + if !self.db.valid_id(IdType::Channel, id.0).unwrap() { + return NotFound(PlainText("Channel not found".to_string())); + } + self.db.update_channel(id.0, name.0).unwrap(); + Success + } + + #[oai(path = "/channel", method = "get")] + /// Gets a channel + async fn get_channel(&self, auth: Authorization, id: Query<i64>) -> ChannelResponse { + use ChannelResponse::*; + if !self.db.valid_id(IdType::Channel, id.0).unwrap() { + return NotFound; + } + Success(Json(self.db.get_channel(id.0).unwrap())) + } + + #[oai(path = "/channel", method = "delete")] + /// Deletes a channel + async fn delete_channel(&self, auth: Authorization, id: Query<i64>) -> DeleteResponse { + use DeleteResponse::*; + if !self.db.valid_id(IdType::Channel, id.0).unwrap() { + return NotFound(PlainText("Channel not found".to_string())); + } + let channel = self.db.get_channel(id.0).unwrap(); + self.db.remove_group_channel(channel.group, id.0).unwrap(); + self.db.delete_channel(id.0).unwrap(); + Success + } + + #[oai(path = "/channel/members", method = "get")] + /// Gets the members that can access a channel + async fn get_channel_members(&self, auth: Authorization, id: Query<i64>) -> MembersResponse { + use MembersResponse::*; + let members = self.db.get_channel_members(id.0).unwrap(); + Success(Json(members.iter().map(|m| { + self.db.get_user(*m).unwrap() + }).collect::<Vec<User>>())) + } + + #[oai(path = "/channel/members", method = "put")] + /// Adds a member to a channel + async fn add_channel_member(&self, auth: Authorization, id: Query<i64>, uid: Query<i64>) -> GenericResponse { + use GenericResponse::*; + if !self.db.valid_id(IdType::Channel, id.0).unwrap() { + return NotFound(PlainText("Channel not found".to_string())) + } else if !self.db.valid_id(IdType::User, uid.0).unwrap() { + return NotFound(PlainText("User not found".to_string())) + } + self.db.add_channel_member(id.0, uid.0).unwrap(); + Success + } + + #[oai(path = "/channel/members", method = "delete")] + /// Removes a member from a channel + async fn remove_channel_member(&self, auth: Authorization, cid: Query<i64>, uid: Query<i64>) -> DeleteResponse { + use DeleteResponse::*; + if !self.db.valid_id(IdType::Channel, cid.0).unwrap() { + return NotFound(PlainText("Channel not found".to_string())) + } else if !self.db.valid_id(IdType::User, uid.0).unwrap() { + return NotFound(PlainText("User not found".to_string())) + } + self.db.remove_channel_member(cid.0, uid.0).unwrap(); + Success + } + + #[oai(path = "/channel/term", method = "get")] + /// Returns batch of messages in channel containing "term" in the last 100 messages + async fn search_channel(&self, auth: Authorization, cid: Query<i64>, term: Query<String>, off: Query<u64>) -> MessagesResponse { + use MessagesResponse::*; + if !self.db.valid_id(IdType::Channel, cid.0).unwrap() { + return NotFound(PlainText("Channel not found".to_string())) + } + let mut messages = self.db.get_messages(cid.0, 100).unwrap(); + messages.retain(|msg| msg.content.contains(&term.0)); + Success(Json(messages)) + } + + #[oai(path = "/channel/messages", method = "get")] + /// Returns batch of messages in channel. Do not use for small batches. + /// + /// For small batches, use `chatterbox`, the websocket service for messaging, instead. + async fn get_channel_messages(&self, auth: Authorization, cid: Query<i64>, num_msgs: Query<u64>) -> MessagesResponse { + use MessagesResponse::*; + if !self.db.valid_id(IdType::Channel, cid.0).unwrap() { + return NotFound(PlainText("Channel not found".to_string())) + } + Success(Json(self.db.get_messages(cid.0, num_msgs.0).unwrap())) + } } #[tokio::main] async fn main() -> Result<(), std::io::Error> { - use hmac::Mac; - if std::env::var_os("RUST_LOG").is_none() { - std::env::set_var("RUST_LOG", "poem=debug"); - } - tracing_subscriber::fmt::init(); - - let db = Box::new(Cassandra::new("bsk")); - let api_service = OpenApiService::new(Api::new(db), "Scuttlebutt", "1.0") - .description( - "Scuttlebutt is the REST API for managing everything but sending/receiving messages \ - - which means creating/updating/deleting all of your users/groups/channels.", - ) - .server("http://localhost:3000/api"); - - let ui = api_service.swagger_ui(); - - let key: String = rand::thread_rng() - .sample_iter(&Alphanumeric) - .take(7) - .map(char::from) - .collect(); - - let app = Route::new() - .nest("/api", api_service) - .nest("/", ui) - .data(IdGenerator(Snowflake::default())) - .data(ServerKey::new_from_slice(&key.as_bytes()).unwrap()); - - Server::new(TcpListener::bind("127.0.0.1:3000")).run(app).await + use hmac::Mac; + if std::env::var_os("RUST_LOG").is_none() { + std::env::set_var("RUST_LOG", "poem=debug"); + } + tracing_subscriber::fmt::init(); + + let db = Box::new(Cassandra::new("bsk")); + let api_service = OpenApiService::new(Api::new(db), "Scuttlebutt", "1.0") + .description( + "Scuttlebutt is the REST API for managing everything but sending/receiving messages \ + - which means creating/updating/deleting all of your users/groups/channels.", + ) + .server("http://localhost:3000/api"); + + let ui = api_service.swagger_ui(); + + let key: String = rand::thread_rng() + .sample_iter(&Alphanumeric) + .take(7) + .map(char::from) + .collect(); + + let app = Route::new() + .nest("/api", api_service) + .nest("/", ui) + .data(ServerKey::new_from_slice(&key.as_bytes()).unwrap()); + + Server::new(TcpListener::bind("127.0.0.1:3000")).run(app).await } #[cfg(test)] diff --git a/scuttlebutt/src/tests.rs b/scuttlebutt/src/tests.rs @@ -14,7 +14,7 @@ use sha2::Digest; type FakeClient = TestClient<AddDataEndpoint<Route, ServerKey>>; fn contents_eq<T: PartialEq>(a: Vec<T>, b: Vec<T>) -> bool { - b.iter().all(|item| a.contains(item)) + b.iter().all(|item| a.contains(item)) } fn setup() -> FakeClient { @@ -23,7 +23,7 @@ fn setup() -> FakeClient { .take(7) .map(char::from) .collect(); - let db = Box::new(Cassandra::new("test")); + let db = Box::new(Cassandra::new("test")); let api_service = OpenApiService::new(Api::new(db), "Scuttlebutt", "1.0").server("http://localhost:3000/api"); let app = Route::new() .nest("/api", api_service) @@ -39,19 +39,14 @@ fn hash_pass(pass: &str) -> String { async fn make_user(cli: &FakeClient, name: &str, email: &str, pass: &str) -> User { let hash = hash_pass(pass); - let resp = cli - .post(format!("/api/user?name={}&email={}&hash={}", name, email, hash)) - .send() - .await; + let resp = cli.post(format!("/api/user?name={}&email={}&hash={}", name, email, hash)).send().await; resp.assert_status_is_ok(); resp.json().await.value().deserialize::<User>() } async fn login(cli: &FakeClient, id: i64, pass: &str) -> String { let hash = hash_pass(pass); - let mut resp = cli.post(format!("/api/login?id={}", id)) - .content_type("text/plain") - .body(hash).send().await; + let mut resp = cli.post(format!("/api/login?id={}", id)).content_type("text/plain").body(hash).send().await; resp.assert_status_is_ok(); resp.0.take_body().into_string().await.unwrap() } @@ -60,12 +55,11 @@ async fn setup_user_auth() -> (FakeClient, User) { let cli = setup(); let user = make_user(&cli, "test", "test@example.com", "12345").await; let auth = login(&cli, user.id, "12345").await; - let cli = cli.default_header("Authorization", &auth); - let cli = cli.default_content_type("text/plain"); + let cli = cli.default_header("Authorization", &auth); + let cli = cli.default_content_type("text/plain"); (cli, user) } - async fn make_group(cli: &FakeClient, name: &str) -> Group { let resp = cli.post(format!("/api/group?name={}", name)).send().await; resp.assert_status_is_ok(); @@ -95,7 +89,7 @@ async fn find_group(cli: &FakeClient, id: i64) -> Group { async fn post_user() { let cli = setup(); let user = make_user(&cli, "test", "test@example.com", "12345").await; - + assert_eq!(user.email, "test@example.com"); assert_eq!(user.username, "test"); @@ -103,15 +97,13 @@ async fn post_user() { // let mut id_gen = Snowflake::default(); // assert_ge!(id_gen.generate(), resp.id); - let resp = cli.get(format!("/api/user?id={}", user.id)).send().await; - resp.assert_status_is_ok(); - - let same_user = resp.json().await.value().deserialize::<User>(); - assert_eq!(user, same_user); + let resp = cli.get(format!("/api/user?id={}", user.id)).send().await; + resp.assert_status_is_ok(); + + let same_user = resp.json().await.value().deserialize::<User>(); + assert_eq!(user, same_user); } - - #[tokio::test] async fn post_login() { let cli = setup(); @@ -119,25 +111,25 @@ async fn post_login() { let hash = hash_pass("12345"); let resp = cli.post(format!("/api/login?id={}", user.id)) - .content_type("text/plain").body("abc").send().await; + .content_type("text/plain").body("abc").send().await; resp.assert_status(StatusCode::BAD_REQUEST); let resp = cli.post("/api/login?id=12") - .content_type("text/plain").body(hash.clone()).send().await; + .content_type("text/plain").body(hash.clone()).send().await; resp.assert_status(StatusCode::NOT_FOUND); let resp = cli.post(format!("/api/login?id={}", user.id)) - .header::<&str, &str>("Authorization", "") - .content_type("text/plain").body(hash_pass("123")).send().await; + .header::<&str, &str>("Authorization", "") + .content_type("text/plain").body(hash_pass("123")).send().await; resp.assert_status(StatusCode::UNAUTHORIZED); let mut resp = cli.post(format!("/api/login?id={}", user.id)) - .content_type("text/plain").body(hash.clone()).send().await; + .content_type("text/plain").body(hash.clone()).send().await; resp.assert_status_is_ok(); let raw_str = resp.0.take_body().into_string().await.unwrap(); let claims: Claims = serde_json::from_str(&String::from_utf8(base64::decode( - raw_str.split(".").nth(1).unwrap() - ).unwrap()).unwrap()).unwrap(); + raw_str.split(".").nth(1).unwrap() + ).unwrap()).unwrap()).unwrap(); assert_eq!(claims.id, user.id); assert_ge!(claims.exp, Local::now()) } @@ -155,16 +147,16 @@ async fn get_user() { #[tokio::test] async fn put_user() { let (cli, user) = setup_user_auth().await; - + let resp = cli.put("/api/user?name=fred&email=whoo@whee.com") - .header::<&str, &str>("Authorization", "").send().await; + .header::<&str, &str>("Authorization", "").send().await; resp.assert_status(StatusCode::UNAUTHORIZED); - + let resp = cli.put("/api/user?name=fred&email=whoo@whee.com").send().await; resp.assert_status_is_ok(); - - let resp = cli.get(format!("/api/user?id={}", user.id)).send().await; - resp.assert_status_is_ok(); + + let resp = cli.get(format!("/api/user?id={}", user.id)).send().await; + resp.assert_status_is_ok(); let ret_user = resp.json().await.value().deserialize::<User>(); assert_eq!(ret_user.email, "whoo@whee.com"); assert_eq!(ret_user.username, "fred"); @@ -177,27 +169,22 @@ async fn del_user() { let (cli, user) = setup_user_auth().await; let resp = cli.delete(format!("/api/user?id={}", user.id)) - .header::<&str, &str>("Authorization", "").send().await; + .header::<&str, &str>("Authorization", "").send().await; resp.assert_status(StatusCode::UNAUTHORIZED); let resp = cli.delete(format!("/api/user?id={}", user.id)).send().await; - + resp.assert_status_is_ok(); let resp = cli.get(format!("/api/user?id={}", user.id)).send().await; resp.assert_status(StatusCode::NOT_FOUND); } #[tokio::test] async fn post_group() { - let (cli, user) = setup_user_auth().await; - let resp = cli.post("/api/group?name=") - .send().await; - resp.assert_status(StatusCode::BAD_REQUEST); - - let resp = cli - .post("/api/group?name=test") - - .send() - .await; + let (cli, user) = setup_user_auth().await; + let resp = cli.post("/api/group?name=").send().await; + resp.assert_status(StatusCode::BAD_REQUEST); + + let resp = cli.post("/api/group?name=test").send().await; resp.assert_status_is_ok(); let group = resp.json().await.value().deserialize::<Group>(); assert_eq!(group.name, "test"); @@ -215,7 +202,7 @@ async fn put_group() { let group = make_group(&cli, "test").await; let resp = cli.put(format!("/api/group?id={}&name=test2", group.id)) - .header::<&str, &str>("Authorization", "").send().await; + .header::<&str, &str>("Authorization", "").send().await; resp.assert_status(StatusCode::UNAUTHORIZED); let resp = cli.put(format!("/api/group?id={}&name=", group.id)).send().await; @@ -234,7 +221,7 @@ async fn del_group() { let group = make_group(&cli, "test").await; let resp = cli.delete(format!("/api/group?id={}", group.id)) - .header::<&str, &str>("Authorization", "").send().await; + .header::<&str, &str>("Authorization", "").send().await; resp.assert_status(StatusCode::UNAUTHORIZED); let resp = cli.delete("/api/group?id=12").send().await; @@ -243,48 +230,45 @@ async fn del_group() { let resp = cli.delete(format!("/api/group?id={}", group.id)).send().await; resp.assert_status_is_ok(); - let resp = cli.get(format!("/api/group?id={}", group.id)).send().await; - resp.assert_status(StatusCode::NOT_FOUND); - let resp = cli.get(format!("/api/channel?id={}", group.channels[0])).send().await; + let resp = cli.get(format!("/api/group?id={}", group.id)).send().await; resp.assert_status(StatusCode::NOT_FOUND); - - let resp = cli.get("/api/user/groups").send().await; + let resp = cli.get(format!("/api/channel?id={}", group.channels[0])).send().await; resp.assert_status(StatusCode::NOT_FOUND); + + let resp = cli.get("/api/user/groups").send().await; + resp.assert_status_is_ok(); + let groups = resp.json().await.value().deserialize::<Vec<Group>>(); + assert!(!groups.contains(&group)); } #[tokio::test] +/// TODO non exhaustive async fn put_group_members() { let (cli, user) = setup_user_auth().await; let group = make_group(&cli, "test").await; let user2 = make_user(&cli, "testeroo", "test2@example.com", "123456").await; - let _user3 = make_user(&cli, "testeroo", "test2@example.com", "123456").await; - let resp = cli - .put(format!("/api/group/members?gid={}&uid={}", group.id, user2.id)) - .header::<&str, &str>("Authorization", "") - .send() - .await; + let resp = cli.put(format!("/api/group/members?gid={}&uid={}", group.id, user2.id)) + .header::<&str, &str>("Authorization", "").send().await; resp.assert_status(StatusCode::UNAUTHORIZED); - let resp = cli.post(format!("/api/group/channels?gid={}&name=", group.id)).send().await; + let resp = cli.put(format!("/api/group/members?gid={}&uid=", group.id)).send().await; resp.assert_status(StatusCode::BAD_REQUEST); - let resp = cli.post("/api/group/channels?gid=12&name=test").send().await; + let resp = cli.put(format!("/api/group/members?gid=12&uid={}", user.id)).send().await; resp.assert_status(StatusCode::NOT_FOUND); - let resp = cli.post(format!("/api/group/channels?gid={}&name=test", group.id)).send().await; + let resp = cli.put(format!("/api/group/members?gid={}&uid={}", group.id, user2.id)).send().await; resp.assert_status_is_ok(); - let channel = resp.json().await.value().deserialize::<Channel>(); - assert_eq!(channel.name, "test"); - assert_eq!(channel.members, vec![user.id]); - assert!(find_group(&cli, group.id).await.channels.contains(&channel.id)); + + assert!(find_group(&cli, group.id).await.members.contains(&user2.id)); } #[test] /// Test if gen_id() gives unique IDs on successive calls /// and if it can be called from multiple threads without error fn test_id_gen() { let a = gen_id(); - std::thread::sleep(std::time::Duration::from_secs(1)); + std::thread::sleep(std::time::Duration::from_secs(1)); let b = gen_id(); assert_ge!(b, a); let threads: Vec<_> = (0..100).map(|i| std::thread::spawn(move || gen_id())).collect(); @@ -315,6 +299,7 @@ async fn get_channels() { let resp = cli.get(format!("/api/group/channels?gid={}", group.id)).send().await; resp.assert_status_is_ok(); let channels = resp.json().await.value().deserialize::<Vec<Channel>>(); + assert!(contents_eq( channels, vec![find_channel(&cli, group.channels[0]).await, chan1, chan2, chan3] @@ -323,18 +308,18 @@ async fn get_channels() { #[tokio::test] async fn get_groups() { - let (cli, _user) = setup_user_auth().await; - let group = make_group(&cli, "test1").await; - let group2 = make_group(&cli, "test2").await; - let group3 = make_group(&cli, "test3").await; - let resp = cli.get("/api/user/groups").send().await; + let (cli, _user) = setup_user_auth().await; + let group = make_group(&cli, "test1").await; + let group2 = make_group(&cli, "test2").await; + let group3 = make_group(&cli, "test3").await; + let resp = cli.get("/api/user/groups").send().await; resp.assert_status_is_ok(); let groups = resp.json().await.value().deserialize::<Vec<Group>>(); - assert!(contents_eq(groups, vec![group, group2, group3])); + assert!(contents_eq(groups, vec![group, group2, group3])); } // #[tokio::test] // async fn get_group_members() { -// let (cli, user) = setup_user_auth().await; -// let group = make_group(&cli, auth.clone(), "test").await; +// let (cli, user) = setup_user_auth().await; +// let group = make_group(&cli, auth.clone(), "test").await; // }