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:
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;
// }