blatherskite

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

commit edb3ef68cc3349d12752da9f751337dddcb7359d
parent 7bad55c86326b8e8b4e9f1d05cdd64d8d13cf6dd
Author: quantumish <freifeld.david@gmail.com>
Date:   Mon,  3 Oct 2022 09:16:06 -0700

Fail to simplify tests, add many more methods

Diffstat:
M.pre-commit-config.yaml | 1-
Mchatterbox/src/main.rs | 4+---
Mscuttlebutt/Cargo.toml | 1+
Mscuttlebutt/src/main.rs | 1061+++++++++++++++++++++++++++++++++++++++++++++++++------------------------------
Mscuttlebutt/src/responses.rs | 39+++++++++++++++++++++++++++++----------
5 files changed, 692 insertions(+), 414 deletions(-)

diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml @@ -2,5 +2,4 @@ repos: - repo: https://github.com/doublify/pre-commit-rust rev: v1.0 hooks: - - id: fmt - id: cargo-check diff --git a/chatterbox/src/main.rs b/chatterbox/src/main.rs @@ -53,7 +53,5 @@ async fn main() -> Result<(), std::io::Error> { get(ws.data(tokio::sync::broadcast::channel::<String>(32).0)), ); - Server::new(TcpListener::bind("127.0.0.1:3001")) - .run(app) - .await + Server::new(TcpListener::bind("127.0.0.1:3001")).run(app).await } diff --git a/scuttlebutt/Cargo.toml b/scuttlebutt/Cargo.toml @@ -6,6 +6,7 @@ edition = "2021" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies] +anyhow = "1.0.65" base64 = "0.13.0" cassandra-cpp = "1.1.0" chrono = { version = "0.4.22", features = ["serde"] } diff --git a/scuttlebutt/src/main.rs b/scuttlebutt/src/main.rs @@ -3,14 +3,14 @@ use chrono::{DateTime, Duration, Local}; use hmac::Hmac; use jwt::{SignWithKey, VerifyWithKey}; use poem::{ - http::StatusCode, listener::TcpListener, web::Data, Endpoint, EndpointExt, Request, Result, - Route, Server, + http::StatusCode, listener::TcpListener, web::Data, Endpoint, EndpointExt, Request, Result, + Route, Server, }; use poem_openapi::{ - auth::ApiKey, - param::Query, - payload::{Json, PlainText}, - *, + auth::ApiKey, + param::Query, + payload::{Json, PlainText}, + *, }; use rand::{distributions::Alphanumeric, Rng}; use rustflake::Snowflake; @@ -23,428 +23,689 @@ pub use responses::*; type ServerKey = Hmac<Sha256>; +const UNUSUAL_ROW_ERROR: &'static str = "Found duplicate ID (or negative rows??)! Giving up!"; + #[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 = "ScuttleKey", - in = "header", - checker = "api_checker" + type = "api_key", + key_name = "ScuttleKey", + in = "header", + checker = "api_checker" )] struct Authorization(Claims); 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() + 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() } struct Api { - sess: Session, - kspc: String, + sess: Session, + kspc: String, } pub fn gen_id() -> i64 { - static STATE: Mutex<Option<Snowflake>> = Mutex::new(None); + static STATE: Mutex<Option<Snowflake>> = Mutex::new(None); - STATE - .lock() - .unwrap() - .get_or_insert_with(|| Snowflake::default()) - .generate() + STATE + .lock() + .unwrap() + .get_or_insert_with(|| Snowflake::default()) + .generate() } #[OpenApi] #[allow(unused_variables)] impl Api { - fn new(keyspc: &str) -> Api { - 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 list<bigint>, channels list<bigint>);"))).wait().unwrap(); - session.execute(&stmt!(&format!("CREATE TABLE IF NOT EXISTS {keyspc}.channels (id bigint PRIMARY KEY, name text, group bigint, members list<bigint>);"))).wait().unwrap(); - session.execute(&stmt!(&format!("CREATE TABLE IF NOT EXISTS {keyspc}.user_groups (id bigint PRIMARY KEY, groups list<bigint>);"))).wait().unwrap(); - session.execute(&stmt!(&format!("CREATE TABLE IF NOT EXISTS {keyspc}.messages (group bigint, channel bigint, author bigint, time timestamp, content text, PRIMARY KEY ((group, channel)));"))).wait().unwrap(); - - Api { - sess: session, - kspc: String::from(keyspc), - } - } - - #[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; - } - let hash_stmt = &stmt!(&format!( - "SELECT id, name, email, hash FROM {}.users WHERE id={};", - self.kspc, id.0 - )); - let res = self.sess.execute(hash_stmt).wait().unwrap(); - if res.row_count() == 1 { - let row = res.first_row().unwrap(); - let db_hash: String = row.get(3).unwrap(); - if hex::decode(db_hash).unwrap() != hex::decode(hash.0).unwrap() { - Unauthorized - } else { - let row = res.first_row().unwrap(); - let token = Claims { - id: id.0, - exp: Local::now() + Duration::days(1), - } - .sign_with_key(key.0); - Success(PlainText(token.unwrap())) - } - } else if res.row_count() == 0 { - NotFound - } else { - InternalError(PlainText( - "Found multiple (or negative?) number of rows.".to_string(), - )) - } - } - - #[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::*; - let insert_stmt = &stmt!(&format!( - "SELECT id, name, email FROM {}.users WHERE id={};", - self.kspc, id.0 - )); - match self.sess.execute(insert_stmt).wait() { - Ok(res) => { - if res.row_count() == 1 { - let row = res.first_row().unwrap(); - Success(Json(User { - id: id.0, - username: row.get(1).unwrap(), - email: row.get(2).unwrap(), - })) - } else if res.row_count() == 0 { - NotFound - } else { - InternalError(PlainText( - "Found duplicate ID (or negative rows??)! Giving up!".to_string(), - )) - } - } - 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(); - let insert_stmt = &stmt!(&format!( - "INSERT INTO {}.users (id, name, email, hash) VALUES ({},'{}','{}','{}');", - self.kspc, id, name.0, email.0, hash.0 - )); - if let Err(e) = self.sess.execute(insert_stmt).wait() { - InternalError(PlainText(e.to_string())) - } else { - 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>, - ) -> CreateUserResponse { - use CreateUserResponse::*; - let id = auth.0.id; - let update_stmt = &stmt!(&format!( - "UPDATE {}.users SET name = '{}', email = '{}' WHERE id = {};", - self.kspc, name.0, email.0, id - )); - if let Err(e) = self.sess.execute(update_stmt).wait() { - InternalError(PlainText(e.to_string())) - } else { - Success(Json(User { - id, - username: name.0, - email: email.0, - })) - } - } - - #[oai(path = "/user", method = "delete")] - /// Deletes your user - async fn delete_user(&self, auth: Authorization) -> DeleteResponse { - use DeleteResponse::*; - let id = auth.0.id; - let delete_stmt = &stmt!(&format!("DELETE FROM {}.users WHERE id={};", self.kspc, id)); - if let Err(e) = self.sess.execute(delete_stmt).wait() { - InternalError(PlainText(e.to_string())) - } else { - Success - } - } - - #[oai(path = "/user/groups", method = "get")] - /// Gets all groups accessible to you - async fn get_groups(&self, auth: Authorization) -> GroupsResponse { - todo!() - } - - #[oai(path = "/user/groups", method = "delete")] - /// Leaves a group accessible to you - async fn leave_group(&self, auth: Authorization, gid: Query<i64>) -> GenericResponse { - todo!() - } - - #[oai(path = "/group", method = "get")] - /// Gets the group with the given ID - async fn get_group(&self, auth: Authorization, id: Query<i64>) -> GroupResponse { - todo!() - } - - #[oai(path = "/group", method = "post")] - /// Creates a new group - async fn make_group(&self, auth: Authorization, name: Query<String>) -> CreateGroupResponse { - use CreateGroupResponse::*; - // InternalError(PlainText("test".to_string())) - let gid = gen_id(); - let cid = gen_id(); - // if name.0 == "" { - // return BadRequest(PlainText("Empty string not allowed for name".to_string())) - // } - // let channel_stmt = &stmt!(&format!( - // "INSERT INTO {}.channels (id, group, name, members) VALUES ({}, {}, '{}', [{}]);", - // self.kspc, cid, gid, "main", auth.0.id - // )); - // if let Err(e) = self.sess.execute(channel_stmt).wait() { - // return InternalError(PlainText(e.to_string())) - // } - let group_stmt = &stmt!(&format!( - "INSERT INTO {}.groups (id, name, channels, members) VALUES ({}, '{}', [{}], [{}]);", - self.kspc, gid, name.0, cid, auth.0.id - )); - 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>, - ) -> CreateGroupResponse { - todo!() - } - - #[oai(path = "/group", method = "delete")] - /// Deletes a group - async fn delete_group(&self, auth: Authorization, id: Query<i64>) -> DeleteResponse { - todo!() - } - - #[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 { - todo!() - } - - #[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 { - todo!() - } - - #[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 { - todo!() - } - - #[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 { - todo!() - } - - #[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 { - todo!() - } - - #[oai(path = "/channel", method = "put")] - /// Updates the name of a channel - async fn update_channel( - &self, - auth: Authorization, - id: Query<i64>, - name: Query<String>, - ) -> CreateChannelResponse { - todo!() - } - - #[oai(path = "/channel", method = "delete")] - /// Deletes a channel - async fn delete_channel(&self, auth: Authorization, id: Query<i64>) -> DeleteResponse { - todo!() - } - - #[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 { - todo!() - } - - #[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 { - todo!() - } - - #[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 { - todo!() - } - - #[oai(path = "/channel/message", method = "get")] - /// Returns batch of messages in channel containing "term" starting at offset - async fn search_channel( - &self, - auth: Authorization, - cid: Query<i64>, - term: Query<String>, - off: Query<u64>, - ) -> MessagesResponse { - todo!() - } - - #[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 { - todo!() - } + fn new(keyspc: &str) -> Api { + 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 (group bigint, channel bigint, author bigint, time timestamp, content text, PRIMARY KEY ((group, channel)));"))).wait().unwrap(); + + Api { + sess: session, + kspc: String::from(keyspc), + } + } + + fn validate_id(&self, table: &str, gid: i64) -> anyhow::Result<()> { + let res = self.sess.execute(&stmt!(&format!( + "SELECT id FROM {}.groups WHERE id = {};", self.kspc, gid + ))).wait().unwrap(); + match res.row_count() { + 1 => Ok(()), + _ => Err(anyhow::anyhow!("not found")), + } + + } + + async fn __remove_channel_member(&self, cid: i64, uid: i64) { + self.sess.execute(&stmt!(&format!( + "UPDATE {}.channels SET members = members - {{{}}} WHERE id={};", self.kspc, uid, cid + ))).wait().unwrap(); + } + + async fn __remove_group_member(&self, gid: i64, uid: i64) { + self.sess.execute(&stmt!(&format!( + "UPDATE {}.groups SET members = members - {{{}}} WHERE id={};", self.kspc, uid, gid + ))).wait().unwrap(); + let res = self.sess.execute(&stmt!(&format!( + "SELECT channels FROM {}.groups WHERE id={};", self.kspc, gid + ))).wait().unwrap(); + let row = res.first_row().unwrap(); + let channels: SetIterator = row.get(0).unwrap(); + for channel in channels { + self.__remove_channel_member(channel.get_i64().unwrap(), uid); + } + self.sess.execute(&stmt!(&format!( + "UPDATE {}.user_groups SET groups = groups - {{{}}} WHERE id={};", self.kspc, gid, uid + ))).wait().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; + } + let hash_stmt = &stmt!(&format!( + "SELECT id, name, email, hash FROM {}.users WHERE id={};", + self.kspc, id.0 + )); + let res = self.sess.execute(hash_stmt).wait().unwrap(); + if res.row_count() == 1 { + let row = res.first_row().unwrap(); + let db_hash: String = row.get(3).unwrap(); + if hex::decode(db_hash).unwrap() != hex::decode(hash.0).unwrap() { + Unauthorized + } else { + let row = res.first_row().unwrap(); + let token = Claims { + id: id.0, + exp: Local::now() + Duration::days(1), + } + .sign_with_key(key.0); + Success(PlainText(token.unwrap())) + } + } else if res.row_count() == 0 { + NotFound + } else { + InternalError(PlainText( + "Found multiple (or negative?) number of rows.".to_string(), + )) + } + } + + #[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::*; + let insert_stmt = &stmt!(&format!( + "SELECT id, name, email FROM {}.users WHERE id={};", + self.kspc, id.0 + )); + match self.sess.execute(insert_stmt).wait() { + Ok(res) => { + if res.row_count() == 1 { + let row = res.first_row().unwrap(); + Success(Json(User { + id: id.0, + username: row.get(1).unwrap(), + email: row.get(2).unwrap(), + })) + } else if res.row_count() == 0 { + NotFound + } else { + InternalError(PlainText(UNUSUAL_ROW_ERROR.to_string())) + } + } + 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(); + let insert_stmt = &stmt!(&format!( + "INSERT INTO {}.users (id, name, email, hash) VALUES ({},'{}','{}','{}');", + self.kspc, id, name.0, email.0, hash.0 + )); + if let Err(e) = self.sess.execute(insert_stmt).wait() { + InternalError(PlainText(e.to_string())) + } else { + 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>, + ) -> CreateUserResponse { + use CreateUserResponse::*; + let id = auth.0.id; + let update_stmt = &stmt!(&format!( + "UPDATE {}.users SET name = '{}', email = '{}' WHERE id = {};", + self.kspc, name.0, email.0, id + )); + if let Err(e) = self.sess.execute(update_stmt).wait() { + InternalError(PlainText(e.to_string())) + } else { + Success(Json(User { + id, + username: name.0, + email: email.0, + })) + } + } + + #[oai(path = "/user", method = "delete")] + /// Deletes your user + async fn delete_user(&self, auth: Authorization) -> DeleteResponse { + use DeleteResponse::*; + let id = auth.0.id; + self.sess.execute(&stmt!(&format!( + "DELETE FROM {}.users WHERE id={};", self.kspc, id + ))).wait().unwrap(); + + let res = self.sess.execute(&stmt!(&format!( + "SELECT id, name, members, channels FROM {}.user_groups WHERE id={};", + self.kspc, auth.0.id + ))).wait().unwrap(); + let groups: SetIterator = match res.row_count() { + 0 => return Success, + _ => res.first_row().unwrap().get(0).unwrap(), + }; + for group in groups { + self.__remove_group_member(group.get_i64().unwrap(), id); + } + self.sess.execute(&stmt!(&format!( + "DELETE FROM {}.user_groups WHERE id={};", self.kspc, id + ))).wait().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 res = self.sess.execute(&stmt!(&format!( + "SELECT id, name, members, channels FROM {}.user_groups WHERE id={};", + self.kspc, auth.0.id + ))).wait().unwrap(); + + let groups: SetIterator = match res.row_count() { + 0 => return Success(Json(Vec::new())), + _ => res.first_row().unwrap().get(0).unwrap(), + }; + + let group_vec = groups.map(|i| { + let res = self.sess.execute(&stmt!(&format!( + "SELECT id, name, members, channels FROM {}.groups WHERE id={};", self.kspc, i + ))).wait().unwrap(); + let row = res.first_row().unwrap(); + let (members, channels): (SetIterator, SetIterator) = (row.get(2).unwrap(), row.get(3).unwrap()); + Group { + id: row.get(0).unwrap(), + name: row.get(1).unwrap(), + members: members.map(|i| i.get_i64().unwrap()).collect(), + channels: channels.map(|i| i.get_i64().unwrap()).collect(), + } + }).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 let Err(e) = self.validate_id("groups", gid.0) { + return NotFound(PlainText("Didn't find group or experienced database error.".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::*; + let res = self.sess.execute(&stmt!(&format!( + "SELECT name, members, channels FROM {}.groups WHERE id={};", self.kspc, id.0 + ))).wait().unwrap(); + let (name, members, channels): (String, SetIterator, SetIterator) = match res.row_count() { + 1 => { + let row = res.first_row().unwrap(); + (row.get(0).unwrap(), row.get(1).unwrap(), row.get(2).unwrap()) + }, + 0 => return NotFound, + _ => return InternalError(PlainText(UNUSUAL_ROW_ERROR.to_string())) + }; + + Success(Json(Group{ + id: id.0, + name, + members: members.map(|i| i.get_i64().unwrap()).collect(), + channels: channels.map(|i| i.get_i64().unwrap()).collect(), + })) + } + + #[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.sess.execute(&stmt!(&format!( + "INSERT INTO {}.channels (id, group, name, members) VALUES ({}, {}, '{}', {{{}}});", + self.kspc, cid, gid, "main", auth.0.id + ))).wait().unwrap(); + self.sess.execute(&stmt!(&format!( + "INSERT INTO {}.groups (id, name, channels, members) VALUES ({}, '{}', {{{}}}, {{{}}});", + self.kspc, gid, name.0, cid, auth.0.id + ))).wait().unwrap(); + self.sess.execute(&stmt!(&format!( + "UPDATE {}.user_groups SET groups = groups + {{{}}} WHERE id = {};", + self.kspc, gid, auth.0.id + ))).wait().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 let Err(e) = self.validate_id("groups", id.0) { + return NotFound(PlainText("Didn't find group or experienced database error.".to_string())); + } + self.sess.execute(&stmt!(&format!( + "UPDATE {}.groups SET name = '{}' WHERE id = {};", + self.kspc, name.0, id.0 + ))).wait().unwrap(); + Success + } + + #[oai(path = "/group", method = "delete")] + /// Deletes a group + async fn delete_group(&self, auth: Authorization, id: Query<i64>) -> DeleteResponse { + use DeleteResponse::*; + let res = self.sess.execute(&stmt!(&format!( + "SELECT id, members, channels FROM {}.groups WHERE id={};", self.kspc, id.0 + ))).wait().unwrap(); + + let (members, channels): (SetIterator, SetIterator) = match res.row_count() { + 1 => { + let row = res.first_row().unwrap(); + (row.get(1).unwrap(), row.get(2).unwrap()) + }, + 0 => return NotFound(PlainText("Group not found".to_string())), + _ => return InternalError(PlainText(UNUSUAL_ROW_ERROR.to_string())) + }; + + for member in members { + self.sess.execute(&stmt!(&format!( + "UPDATE {}.user_groups SET groups = groups - {{{}}} WHERE id = {};", + self.kspc, id.0, member + ))).wait().unwrap(); + } + + for channel in channels { + self.sess.execute(&stmt!(&format!( + "DELETE FROM {}.channels WHERE id={};", self.kspc, channel + ))).wait().unwrap(); + } + + self.sess.execute(&stmt!(&format!( + "DELETE FROM {}.groups WHERE id={};", self.kspc, id.0 + ))).wait().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 let Err(_) = self.validate_id("groups", id.0) { + return NotFound; + } + let res = self.sess.execute(&stmt!(&format!( + "SELECT members FROM {}.groups WHERE id={};", self.kspc, id.0 + ))).wait().unwrap(); + let row = res.first_row().unwrap(); + let members: SetIterator = row.get(0).unwrap(); + let members_objs = members.map(|_| { + let res = self.sess.execute(&stmt!(&format!( + "SELECT id, name, email FROM {}.users WHERE id={};", + self.kspc, id.0 + ))).wait().unwrap(); + let row = res.first_row().unwrap(); + User { + id: id.0, + username: row.get(1).unwrap(), + email: row.get(2).unwrap(), + } + }).collect::<Vec<User>>(); + Success(Json(members_objs)) + } + + #[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 let Err(e) = self.validate_id("groups", gid.0) { + return NotFound(PlainText("Didn't find group or experienced database error.".to_string())); + } + self.sess.execute(&stmt!(&format!( + "UPDATE {}.groups SET members = members + {{{}}} WHERE id = {};", + self.kspc, uid.0, gid.0 + ))).wait().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 let Err(_) = self.validate_id("groups", gid.0) { + return NotFound(PlainText("Group not found".to_string())) + } else if let Err(_) = self.validate_id("users", uid.0) { + 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 let Err(_) = self.validate_id("groups", gid.0) { + return NotFound; + } + let res = self.sess.execute(&stmt!(&format!( + "SELECT channels FROM {}.groups WHERE id={};", self.kspc, gid.0 + ))).wait().unwrap(); + let row = res.first_row().unwrap(); + let channels: SetIterator = row.get(0).unwrap(); + let mut channel_objs = Vec::new(); + for chan in channels { + let res = self.sess.execute(&stmt!(&format!( + "SELECT id, name, members FROM {}.channels WHERE id={};", self.kspc, chan + ))).wait().unwrap(); + let row = res.first_row().unwrap(); + let members: SetIterator = row.get(2).unwrap(); + let members = members.map(|m| m.get_i64().unwrap()).collect::<Vec<i64>>(); + if !members.contains(&auth.0.id) { + continue; + } + channel_objs.push(Channel { + id: row.get(0).unwrap(), + name: row.get(1).unwrap(), + group: gid.0, + members + }); + } + Success(Json(channel_objs)) + } + + #[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 let Err(e) = self.validate_id("groups", gid.0) { + return NotFound(PlainText("Group not found.".to_string())); + } + let cid = gen_id(); + self.sess.execute(&stmt!(&format!( + "INSERT INTO {}.channels (id, group, name, members) VALUES ({}, {}, '{}', {{{}}});", + self.kspc, cid, gid.0, name.0, auth.0.id + ))).wait().unwrap(); + self.sess.execute(&stmt!(&format!( + "UPDATE {}.groups SET channels = channels + {{{}}} WHERE id = {};", + self.kspc, cid, gid.0 + ))).wait().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::*; + let res = self.sess.execute(&stmt!(&format!( + "SELECT id FROM {}.channels WHERE id = {};", self.kspc, id.0 + ))).wait().unwrap(); + match res.row_count() { + 0 => return NotFound(PlainText("Channel not found".to_string())), + 1 => (), + _ => return InternalError(PlainText(UNUSUAL_ROW_ERROR.to_string())) + } + self.sess.execute(&stmt!(&format!( + "UPDATE {}.channels SET name = '{}', WHERE id = {};", + self.kspc, name.0, id.0 + ))).wait().unwrap(); + Success + } + + #[oai(path = "/channel", method = "get")] + /// Gets a channel + async fn get_channel(&self, auth: Authorization, id: Query<i64>) -> ChannelResponse { + use ChannelResponse::*; + let res = self.sess.execute(&stmt!(&format!( + "SELECT name, group, members FROM {}.channels WHERE id={};", self.kspc, id.0 + ))).wait().unwrap(); + let (name, group, members): (String, i64, SetIterator) = match res.row_count() { + 1 => { + let row = res.first_row().unwrap(); + (row.get(0).unwrap(), row.get(1).unwrap(), row.get(2).unwrap()) + }, + 0 => return NotFound, + _ => return InternalError(PlainText(UNUSUAL_ROW_ERROR.to_string())) + }; + + Success(Json(Channel { + id: id.0, + name, + group, + members: members.map(|i| i.get_i64().unwrap()).collect(), + })) + } + + #[oai(path = "/channel", method = "delete")] + /// Deletes a channel + async fn delete_channel(&self, auth: Authorization, id: Query<i64>) -> DeleteResponse { + todo!() + } + + #[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 { + todo!() + } + + #[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 { + todo!() + } + + #[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 let Err(_) = self.validate_id("channels", cid.0) { + return NotFound(PlainText("Channel not found".to_string())) + } else if let Err(_) = self.validate_id("users", uid.0) { + return NotFound(PlainText("User not found".to_string())) + } + self.__remove_channel_member(cid.0, uid.0); + Success + } + + #[oai(path = "/channel/message", method = "get")] + /// Returns batch of messages in channel containing "term" starting at offset + async fn search_channel( + &self, + auth: Authorization, + cid: Query<i64>, + term: Query<String>, + off: Query<u64>, + ) -> MessagesResponse { + todo!() + } + + #[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 { + todo!() + } } #[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 api_service = OpenApiService::new(Api::new("bsk"), "Scuttlebutt", "1.0") - .description( - "Scuttlebutt is the REST API for managing everything but sending/receiving messages \ + 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 api_service = OpenApiService::new(Api::new("bsk"), "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 spec = api_service.spec(); - - 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()); - // let cli = poem::test::TestClient::new(app); - // let resp = cli.post("/api/login?id=234").body("abc").send().await; - // resp.assert_status_is_ok(); - // Ok(()) - Server::new(TcpListener::bind("127.0.0.1:3000")) - .run( - app, // .at("/spec", poem::endpoint::make_sync(move |_| spec.clone())) - ) - .await + ) + .server("http://localhost:3000/api"); + + let ui = api_service.swagger_ui(); + let spec = api_service.spec(); + + 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()); + // let cli = poem::test::TestClient::new(app); + // let resp = cli.post("/api/login?id=234").body("abc").send().await; + // resp.assert_status_is_ok(); + // Ok(()) + Server::new(TcpListener::bind("127.0.0.1:3000")) + .run( + app, // .at("/spec", poem::endpoint::make_sync(move |_| spec.clone())) + ) + .await } #[cfg(test)] diff --git a/scuttlebutt/src/responses.rs b/scuttlebutt/src/responses.rs @@ -25,6 +25,7 @@ pub struct Group { pub struct Channel { pub id: i64, pub name: String, + pub group: i64, pub members: Vec<i64>, } @@ -103,10 +104,13 @@ pub enum DeleteResponse { pub enum GroupResponse { /// Returns the group requested #[oai(status = 200)] - Group(Json<Group>), + Success(Json<Group>), /// Invalid ID. #[oai(status = 404)] NotFound, + /// Internal server error when attempting to access database + #[oai(status = 500)] + InternalError(PlainText<String>), } #[derive(ApiResponse)] @@ -128,17 +132,20 @@ pub enum CreateGroupResponse { pub enum ChannelResponse { /// Returns the channel requested #[oai(status = 200)] - Channel(Json<Channel>), - /// Invalid ID. Content specifies which of the IDs passed is invalid. + Success(Json<Channel>), + /// Invalid ID. #[oai(status = 404)] - NotFound(PlainText<String>), + NotFound, + /// Internal server error: likely due to a database operation failing + #[oai(status = 500)] + InternalError(PlainText<String>), } #[derive(ApiResponse)] pub enum CreateChannelResponse { /// Returns the channel requested #[oai(status = 200)] - Channel(Json<Channel>), + Success(Json<Channel>), /// Invalid parameter, such as: /// - empty string for name /// - bad string @@ -147,23 +154,32 @@ pub enum CreateChannelResponse { /// Invalid ID. Content specifies which of the IDs passed is invalid. #[oai(status = 404)] NotFound(PlainText<String>), + /// Internal server error: likely due to a database operation failing + #[oai(status = 500)] + InternalError(PlainText<String>), } #[derive(ApiResponse)] pub enum GenericResponse { - /// Action succeeded + /// Action succeeded. #[oai(status = 200)] Success, + /// Recieved a bad argument. + #[oai(status = 400)] + BadRequest(PlainText<String>), /// Invalid ID. Content specifies which of the IDs passed is invalid. #[oai(status = 404)] NotFound(PlainText<String>), + /// Internal server error: likely due to a database operation failing + #[oai(status = 500)] + InternalError(PlainText<String>), } #[derive(ApiResponse)] pub enum MessagesResponse { /// Returns the messages requested #[oai(status = 200)] - Messages(Json<Vec<Message>>), + Success(Json<Vec<Message>>), /// Invalid ID, or no messages found. Content specifies which error occured. #[oai(status = 404)] NotFound(PlainText<String>), @@ -176,7 +192,7 @@ pub enum MessagesResponse { pub enum MembersResponse { /// Returns the members of current channel/group #[oai(status = 200)] - Messages(Json<Vec<User>>), + Success(Json<Vec<User>>), /// Invalid ID #[oai(status = 404)] NotFound, @@ -186,7 +202,7 @@ pub enum MembersResponse { pub enum GroupsResponse { /// Returns the groups the user is a memmber of #[oai(status = 200)] - Messages(Json<Vec<Group>>), + Success(Json<Vec<Group>>), /// Invalid user ID #[oai(status = 404)] NotFound, @@ -196,8 +212,11 @@ pub enum GroupsResponse { pub enum ChannelsResponse { /// Returns the channels in a group #[oai(status = 200)] - Messages(Json<Vec<Channel>>), + Success(Json<Vec<Channel>>), /// Invalid group ID #[oai(status = 404)] NotFound, + /// Internal server error: likely due to a database operation failing + #[oai(status = 500)] + InternalError(PlainText<String>), }