blatherskite

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

main.rs (29234B)


      1 #![allow(warnings, unused)]
      2 
      3 use chrono::{DateTime, Duration, Local, Utc};
      4 use hmac::Hmac;
      5 use jwt::{SignWithKey, VerifyWithKey};
      6 use poem::{
      7 	listener::TcpListener, web::Data, EndpointExt, Request, Result,
      8 	Route, Server, http::StatusCode,
      9 };
     10 use poem_openapi::{
     11 	auth::ApiKey,
     12 	param::Query,
     13 	payload::{Json, PlainText},
     14 	*,
     15 };
     16 use std::sync::Mutex;
     17 use rand::{distributions::Alphanumeric, Rng};
     18 use rustflake::Snowflake;
     19 use serde::{Deserialize, Serialize};
     20 use sha2::Sha256;
     21 
     22 use diesel::pg::PgConnection;
     23 use diesel::prelude::*;
     24 use dotenvy::dotenv;
     25 use std::env;
     26 
     27 pub mod models;
     28 use models::*;
     29 pub mod schema;
     30 use schema::*;
     31 pub mod error;
     32 use error::*;
     33 
     34 type ServerKey = Hmac<Sha256>;
     35 
     36 /// Struct representing the ID of the authorized users and the expiration date of the token
     37 /// The serialized form of this struct forms the content portion of the JWT returned by /login
     38 #[derive(Serialize, Deserialize)]
     39 struct Claims {
     40 	id: i64,
     41 	exp: DateTime<Local>,
     42 }
     43 
     44 /// API key authorization scheme
     45 #[derive(SecurityScheme)]
     46 #[oai(
     47 	type = "api_key",
     48 	key_name = "Authorization", // header to look for API key in
     49 	in = "header",
     50 	checker = "api_checker" // hook to run when checking authorization
     51 )]
     52 struct Authorization(Claims);
     53 
     54 fn append_array(conn: &mut PgConnection, table: &str, field: &str, id: i64, val: i64) -> Result<usize, WithBacktrace<diesel::result::Error>> {
     55 	diesel::sql_query(format!(
     56 		"UPDATE {table} SET {field} = array_append({field},$1) WHERE id = $2;"
     57 	))
     58 		.bind::<diesel::sql_types::BigInt, _>(val)
     59 		.bind::<diesel::sql_types::BigInt, _>(id)
     60 		.execute(conn).with_backtrace()
     61 }
     62 
     63 
     64 fn remove_array(conn: &mut PgConnection, table: &str, field: &str, id: i64, val: i64) -> Result<usize, WithBacktrace<diesel::result::Error>> {
     65 	diesel::sql_query(format!(
     66 		"UPDATE {table} SET {field} = array_remove({field},$1) WHERE id = $2;"
     67 	))
     68 		.bind::<diesel::sql_types::BigInt, _>(val)
     69 		.bind::<diesel::sql_types::BigInt, _>(id)
     70 		.execute(conn).with_backtrace()
     71 }
     72 
     73 
     74 /// Check if a user has supplied a valid authorization token.
     75 ///
     76 /// Returns None if the token was invalid or if it fails to parse the given token
     77 /// (which will then be handled by Poem to throw a 401), otherwise returns the
     78 /// Claims struct.
     79 async fn api_checker(req: &Request, api_key: ApiKey) -> Option<Claims> {
     80 	let encoded_claims_str = match api_key.key.split(".").nth(1) {
     81 		None => return None,
     82 		Some(s) => s,
     83 	};
     84 	let claims_str = match base64::decode(encoded_claims_str) {
     85 		Err(_) => return None,
     86 		Ok(s) => s,
     87 	};
     88 	let claims: Claims = match serde_json::from_str(&String::from_utf8(claims_str).unwrap()) {
     89 		Err(_) => return None,
     90 		Ok(c) => c
     91 	};
     92 	if claims.exp < Local::now() {
     93 		return None;
     94 	}
     95 	let server_key = req.data::<ServerKey>().unwrap(); // get server secret
     96 	VerifyWithKey::<Claims>::verify_with_key(api_key.key.as_str(), server_key).ok()
     97 }
     98 
     99 /// Wrapper struct for the API functions
    100 struct Api {}
    101 
    102 /// Generates a unique i64 for ID generation
    103 // FIXME: Very bad performance - acts as a chokehold for parallelism since
    104 // every request that sends a message / makes a channel / etc. has to contest
    105 // a global mutex.
    106 pub fn gen_id() -> i64 {
    107 	static STATE: Mutex<Option<Snowflake>> = Mutex::new(None);
    108 
    109 	STATE
    110 		.lock()
    111 		.unwrap()
    112 		.get_or_insert_with(|| Snowflake::default())
    113 		.generate()
    114 }
    115 
    116 pub fn check_name(name: String) -> String{
    117 	let name_chars = name.chars();
    118 	let fixed_name_chars = name_chars.filter(|i| !i.is_whitespace());
    119 	let mut fixed_name: String = "".to_string();
    120 	for i in fixed_name_chars{
    121 		fixed_name.push(i);
    122 	};
    123 	// assert!(fixed_name != "".to_string(), "name is empty or contains only illegal characters");
    124 	return fixed_name;
    125 }
    126 
    127 pub fn open_db_conn() -> PgConnection {
    128 	let database_url = env::var("DATABASE_URL").expect("DATABASE_URL must be set");
    129 	PgConnection::establish(&database_url)
    130 		.unwrap_or_else(|_| panic!("Error connecting to {}", database_url))
    131 }
    132 
    133 #[OpenApi]
    134 #[allow(unused_variables)]
    135 impl Api {
    136 	fn __remove_group_member(&self, conn: &mut PgConnection, gid: i64, uid: i64) -> Result<usize, WithBacktrace<diesel::result::Error>> {
    137 		remove_array(conn, "groups", "members", gid, uid)?;
    138 
    139 		let channels = groups::table.select(groups::dsl::channels)
    140 			.filter(groups::dsl::id.eq(gid)).first::<Vec<i64>>(conn)
    141 			.with_backtrace()?;
    142 
    143 		for channel in channels {
    144 			remove_array(conn, "channels", "members", channel, uid)?;
    145 		}
    146 		remove_array(conn, "user_groups", "groups", uid, gid)
    147 	}
    148 
    149 	#[oai(path = "/login", method = "post")]
    150 	/// Log in as a user. Returns an authentication token given id and hash.
    151 	///
    152 	/// Expects hash of user's password to be given in the request body.
    153 	/// Checks validity of hash, then signs JWT with a server secret key.
    154 	// good
    155 	async fn login(&self, key: Data<&ServerKey>, id: Query<i64>, hash: PlainText<String>) -> Result<PlainText<String>> {
    156 		if hash.0.len() != 64 {
    157 			return Err(UserFacingError::new(StatusCode::BAD_REQUEST, "Hash is not of length 24!").into())
    158 		}
    159 		let conn = &mut open_db_conn();
    160 		let user: User = users::table.find(id.0).first(conn)
    161 			.poemify("retrieving specified user")?;
    162 		if hex::decode(&user.hash).unwrap() != hex::decode(&hash.0).unwrap() {
    163 			return Err(UserFacingError::new(StatusCode::UNAUTHORIZED, "Incorrect hash provided.").into())
    164 		} else {
    165 			let token = Claims {
    166 				id: id.0,
    167 				exp: Local::now() + Duration::days(1),
    168 			}
    169 			.sign_with_key(key.0);
    170 			Ok(PlainText(token.unwrap()))
    171 		}
    172 	}
    173 
    174 	#[oai(path = "/user", method = "get")]
    175 	/// Get the user with the given ID
    176 	///
    177 	/// Does not require any authorization.
    178 	// good
    179 	async fn get_user(&self, id: Query<i64>) -> Result<Json<User>> {
    180 		let conn = &mut open_db_conn();
    181 		let user: User = users::table.find(id.0).first(conn)
    182 			.poemify("retrieving specified user")?;
    183 		Ok(Json(user))
    184 	}
    185 
    186 	#[oai(path = "/user", method = "post")]
    187 	/// Create a new user.
    188 	///
    189 	/// Expects hash of user's password to be given in the request body.
    190 	/// Does not require any authorization.
    191 	// good
    192 	async fn make_user(&self, name: Query<String>, email: Query<String>, hash: PlainText<String>) -> Result<Json<User>> {
    193 		let conn = &mut open_db_conn();
    194 
    195 		if hash.0.len() != 64 {
    196 			return Err(UserFacingError::new(StatusCode::BAD_REQUEST, "Hash is not of length 24!").into())
    197 		}
    198 
    199 		let user = User {
    200 			id: gen_id(),
    201 			name: check_name(name.0),
    202 			email: email.0,
    203 			hash: hash.0,
    204 		};
    205 
    206 		diesel::insert_into(users::table).values(&user)
    207 			.execute(conn).poemify("adding user to database")?;
    208 		// diesel::update(users::table).set(users::dsl::hash.eq(hash.0))
    209 		//	.execute(conn).poemify("setting user hash");
    210 		diesel::insert_into(user_groups::table)
    211 			.values((user_groups::dsl::id.eq(user.id), user_groups::dsl::groups.eq(Vec::<i64>::new())))
    212 			.execute(conn).poemify("initializing associated database entries")?;
    213 		diesel::insert_into(user_dms::table)
    214 			.values((user_dms::dsl::id.eq(user.id), user_dms::dsl::dms.eq(Vec::<i64>::new())))
    215 			.execute(conn).poemify("initializing associated database entries")?;
    216 
    217 		Ok(Json(user))
    218 	}
    219 
    220 	#[oai(path = "/user", method = "put")]
    221 	/// Update your name and email.
    222 	// good
    223 	async fn update_user(&self, auth: Authorization, name: Query<String>, email: Query<String>) -> Result<()> {
    224 		let conn = &mut open_db_conn();
    225 		let checked_name = check_name(name.0.clone());
    226 		diesel::update(users::table.find(auth.0.id))
    227 			.set((users::dsl::name.eq(checked_name), users::dsl::email.eq(email.0)))
    228 			.execute(conn).poemify("updating database")?;
    229 		Ok(())
    230 	}
    231 
    232 	#[oai(path = "/user", method = "delete")]
    233 	/// Delete your user.
    234 	///
    235 	/// Has the side effects of removing your user from every group, channel, or DM
    236 	/// it is a member of.
    237 	// good
    238 	async fn delete_user(&self, auth: Authorization) -> Result<()> {
    239 		let conn = &mut open_db_conn();
    240 		diesel::delete(users::table.find(auth.0.id)).execute(conn).poemify("deleting your user")?;
    241 		let your_groups: Vec<i64> = user_groups::table
    242 			.select(user_groups::dsl::groups).find(auth.0.id)
    243 			.first(conn).poemify("getting your groups")?;
    244 		let your_dms: Vec<i64> = user_dms::table
    245 			.select(user_dms::dsl::dms).find(auth.0.id)
    246 			.first(conn).poemify("getting your DMs")?;
    247 		for group in your_groups {
    248 			self.__remove_group_member(conn, group, auth.0.id);
    249 		}
    250 		for dm in your_dms {
    251 			self.__remove_group_member(conn, dm, auth.0.id);
    252 		}
    253 		diesel::delete(user_dms::table.find(auth.0.id)).execute(conn).poemify("deleting your groups list")?;
    254 		diesel::delete(user_groups::table.find(auth.0.id)).execute(conn).poemify("deleting your DM list")?;
    255 		Ok(())
    256 	}
    257 
    258 	#[oai(path = "/user/groups", method = "get")]
    259 	/// Get all groups accessible to you.
    260 	// good
    261 	async fn get_groups(&self, auth: Authorization) -> Result<Json<Vec<Group>>> {
    262 		let conn = &mut open_db_conn();
    263 		let res: UserGroup = user_groups::table.find(auth.0.id)
    264 			.first(conn).poemify("retrieving your groups")?;
    265 		Ok(Json(res.groups.iter().map(|i| {
    266 			groups::table.find(*i).first(conn).unwrap()
    267 		}).collect()))
    268 	}
    269 
    270 	#[oai(path = "/user/dms", method = "get")]
    271 	/// Get all DMs accessible to you.
    272 	// good
    273 	async fn get_dms(&self, auth: Authorization) -> Result<Json<Vec<Group>>> {
    274 		let conn = &mut open_db_conn();
    275 		let res: UserDm = user_dms::table.find(auth.0.id)
    276 			.first(conn).poemify("retrieving your dms")?;
    277 		Ok(Json(res.dms.iter().map(|i| {
    278 			groups::table.find(*i).first(conn).unwrap()
    279 		}).collect()))
    280 	}
    281 
    282 	#[oai(path = "/user/groups", method = "delete")]
    283 	/// Leave a group accessible to you
    284 	// good
    285 	async fn leave_group(&self, auth: Authorization, gid: Query<i64>) -> Result<()> {
    286 		let conn = &mut open_db_conn();
    287 		self.__remove_group_member(conn, gid.0, auth.0.id).poemify("removing you from group")?;
    288 		Ok(())
    289 	}
    290 
    291 
    292 	#[oai(path = "/group", method = "get")]
    293 	/// Gets the group with the given ID
    294 	// good
    295 	async fn get_group(&self, auth: Authorization, id: Query<i64>) -> Result<Json<Group>> {
    296 		let conn = &mut open_db_conn();
    297 		let group: Group = groups::table.find(id.0).first(conn)
    298 			.poemify("retrieving specified group")?;
    299 		if !group.members.contains(&auth.0.id) {
    300 			return Err(UserFacingError::terse(StatusCode::FORBIDDEN).into())
    301 		}
    302 		Ok(Json(group))
    303 	}
    304 
    305 
    306 	#[oai(path = "/group", method = "post")]
    307 	/// Create a new group.
    308 	///
    309 	/// The group created...
    310 	/// - will have a default public "main" channel
    311 	/// - will have your user as the owner
    312 	/// - will have your user as an admin
    313 	// good
    314 	async fn make_group(&self, auth: Authorization, name: Query<String>) -> Result<Json<Group>> {
    315 		let conn = &mut open_db_conn();
    316 		let gid = gen_id();
    317 		let channel = Channel {
    318 			id: gen_id(),
    319 			src_group: gid,
    320 			name: String::from("main"),
    321 			members: vec![auth.0.id],
    322 			private: false
    323 		};
    324 		diesel::insert_into(channels::table).values(&channel)
    325 			.execute(conn).poemify("adding 'main' channel to database")?;
    326 		let group = Group {
    327 			id: gid,
    328 			name: check_name(name.0),
    329 			members: vec![auth.0.id],
    330 			admin: vec![auth.0.id],
    331 			owner: auth.0.id,
    332 			is_dm: false,
    333 			channels: vec![channel.id]
    334 		};
    335 		diesel::insert_into(groups::table).values(&group)
    336 			.execute(conn).poemify("adding group to database")?;
    337 		append_array(conn, "user_groups", "groups", auth.0.id, gid)
    338 			.poemify("adding group to your groups")?;
    339 		Ok(Json(group))
    340 	}
    341 
    342 	#[oai(path = "/dm", method = "post")]
    343 	/// Create a new DM with a user `uid`.
    344 	///
    345 	/// The group created...
    346 	/// - will have the `is_dm` attribute set to true.
    347 	/// - will have only one channel "main" with you and `uid`
    348 	/// - will have no owner or admins
    349 	// good
    350 	async fn make_dm(&self, auth: Authorization, uid: Query<i64>) -> Result<Json<Group>> {
    351 		let conn = &mut open_db_conn();
    352 		let user: User = users::table.find(uid.0).first(conn)
    353 			.poemify("retreiving specified user")?;
    354 		let gid = gen_id();
    355 		let channel = Channel {
    356 			id: gen_id(),
    357 			src_group: gid,
    358 			name: String::from("main"),
    359 			members: vec![auth.0.id, uid.0],
    360 			private: false
    361 		};
    362 		diesel::insert_into(channels::table).values(&channel)
    363 			.execute(conn).poemify("adding 'main' channel to database")?;
    364 		let group = Group {
    365 			id: gid,
    366 			name: String::from(""),
    367 			members: vec![auth.0.id, uid.0],
    368 			admin: vec![],
    369 			owner: auth.0.id,
    370 			is_dm: false,
    371 			channels: vec![channel.id]
    372 		};
    373 		diesel::insert_into(groups::table).values(&group)
    374 			.execute(conn).poemify("adding group to database")?;
    375 		append_array(conn, "user_dms", "groups", auth.0.id, gid)
    376 			.poemify("adding DM to your DMs")?;
    377 		append_array(conn, "user_dms", "groups", uid.0, gid)
    378 			.poemify("adding DM to their DMs")?;
    379 		Ok(Json(group))
    380 	}
    381 
    382 	#[oai(path = "/group", method = "put")]
    383 	/// Update the name of an existing group.
    384 	///
    385 	/// Only authorized for the owner of a group.
    386 	// good
    387 	async fn update_group(&self, auth: Authorization, id: Query<i64>, name: Query<String>) -> Result<()> {
    388 		let conn = &mut open_db_conn();
    389 		let mut group: Group = groups::table.find(id.0).first(conn)
    390 			.poemify("retreiving specified group")?;
    391 		if group.owner != auth.0.id {
    392 			return Err(UserFacingError::terse(StatusCode::FORBIDDEN).into());
    393 		}
    394 		group.name = check_name(name.0);
    395 		diesel::update(groups::table.find(id.0)).set(&group)
    396 			.execute(conn).poemify("updating group in database");
    397 		Ok(())
    398 	}
    399 
    400 	#[oai(path = "/group", method = "delete")]
    401 	/// Delete a group.
    402 	///
    403 	/// Only authorized for the owner of a group.
    404 	// good
    405 	async fn delete_group(&self, auth: Authorization, id: Query<i64>) -> Result<()> {
    406 		let conn = &mut open_db_conn();
    407 		let group: Group = groups::table.find(id.0).first(conn)
    408 			.poemify("retreiving specified group")?;
    409 		if group.owner != auth.0.id {
    410 			return Err(UserFacingError::terse(StatusCode::FORBIDDEN).into());
    411 		}
    412 		for member in group.members {
    413 			remove_array(conn, "user_groups", "groups", member, id.0)
    414 				.poemify("removing member from group")?;
    415 		}
    416 		for channel in group.channels {
    417 			diesel::delete(channels::table.find(channel)).execute(conn)
    418 				.poemify("deleting channel from database")?;
    419 		}
    420 		diesel::delete(groups::table.find(id.0)).execute(conn)
    421 			.poemify("deleting group from database")?;
    422 		Ok(())
    423 	}
    424 
    425 	#[oai(path = "/group/members", method = "get")]
    426 	/// Get the members of the specified group.
    427 	///
    428 	/// No specific order for the list is guaranteed.
    429 	// good
    430 	async fn get_group_members(&self, auth: Authorization, id: Query<i64>) -> Result<Json<Vec<User>>> {
    431 		let conn = &mut open_db_conn();
    432 		let group: Group = groups::table.find(id.0).first(conn)
    433 			.poemify("retreiving specified group")?;
    434 		Ok(Json(group.members.iter().map(|m| {
    435 			users::table.find(*m).first(conn).unwrap()
    436 		}).collect()))
    437 	}
    438 
    439 	#[oai(path = "/group/members", method = "put")]
    440 	/// Add a member to an existing group
    441 	///
    442 	/// Only authorized for group admins.
    443 	/// Has the side effect of adding that member to all public channels.
    444 	// good
    445 	async fn add_group_member(&self, auth: Authorization, gid: Query<i64>, uid: Query<i64>) -> Result<()> {
    446 		let conn = &mut open_db_conn();
    447 		let mut group: Group = groups::table.find(gid.0).first(conn)
    448 			.poemify("retreiving specified group")?;
    449 		if !group.admin.contains(&auth.0.id) {
    450 			return Err(UserFacingError::terse(StatusCode::FORBIDDEN).into());
    451 		}
    452 		group.members.push(uid.0);
    453 		for chan_id in group.channels {
    454 			let mut channel: Channel = channels::table.find(chan_id).first(conn)
    455 				.poemify(&format!("retreiving channel (id {}) of group", chan_id))?;
    456 			if channel.private { continue; }
    457 			diesel::update(channels::table.find(chan_id)).set(&channel)
    458 				.execute(conn).poemify("adding member to channel");
    459 		}
    460 		if !group.is_dm {
    461 			append_array(conn, "user_groups", "groups", auth.0.id, gid.0)
    462 				.poemify("adding group to your groups")?;
    463 		} else {
    464 			append_array(conn, "user_dms", "dms", auth.0.id, gid.0)
    465 				.poemify("adding DM to your DMs")?;
    466 		}
    467 		Ok(())
    468 	}
    469 
    470 	#[oai(path = "/group/members", method = "delete")]
    471 	/// Remove a member from an existing group
    472 	///
    473 	/// Only authorized for group admin.
    474 	/// Attempting to remove the owner from their group will always be unauthorized.
    475 	///
    476 	/// Has the side effect of removing the member from all channels.
    477 	// good
    478 	async fn remove_group_member(&self, auth: Authorization, gid: Query<i64>, uid: Query<i64>) -> Result<()> {
    479 		let conn = &mut open_db_conn();
    480 		let admin: Vec<i64> = groups::table.select(groups::dsl::admin)
    481 			.find(gid.0).first(conn).poemify("getting group admin")?;
    482 		if !admin.contains(&auth.0.id) {
    483 			return Err(UserFacingError::terse(StatusCode::FORBIDDEN).into());
    484 		}
    485 		self.__remove_group_member(conn, gid.0, uid.0).poemify("removing group member")?;
    486 		Ok(())
    487 	}
    488 
    489 	#[oai(path = "/group/admin", method = "get")]
    490 	/// Get the admins of the specified group.
    491 	///
    492 	/// No specific order for the list is guaranteed.
    493 	// good
    494 	async fn get_group_admin(&self, auth: Authorization, id: Query<i64>) -> Result<Json<Vec<User>>> {
    495 		let conn = &mut open_db_conn();
    496 		Ok(Json(
    497 			groups::table
    498 				.select(groups::dsl::admin)
    499 				.filter(groups::dsl::id.eq(id.0))
    500 				.first::<Vec<i64>>(conn)
    501 				.poemify("retreiving admin of group")?
    502 				.iter().map(|a| {
    503 					users::table.find(*a).first(conn).unwrap()
    504 				}).collect()
    505 		))
    506 	}
    507 
    508 	#[oai(path = "/group/admin", method = "put")]
    509 	/// Add an admin to an existing group
    510 	///
    511 	/// Only authorized for the owner of a group.
    512 	// good
    513 	async fn add_group_admin(&self, auth: Authorization, gid: Query<i64>, uid: Query<i64>) -> Result<()> {
    514 		let conn = &mut open_db_conn();
    515 		let mut group: Group = groups::table.find(gid.0).first(conn)
    516 			.poemify("retreiving specified group")?;
    517 		let _user: User = users::table.find(uid.0).first(conn)
    518 			.poemify("retreiving specified user")?; // prevent adding invalid UID
    519 		if auth.0.id != group.owner {
    520 			return Err(UserFacingError::terse(StatusCode::FORBIDDEN).into());
    521 		}
    522 		group.admin.push(uid.0);
    523 		diesel::update(groups::table.find(gid.0)).set(&group)
    524 			.execute(conn).poemify("updating group in database");
    525 		Ok(())
    526 	}
    527 
    528 	#[oai(path = "/group/admin", method = "delete")]
    529 	/// Remove an admin from an existing group
    530 	///
    531 	/// Only authorized for the owner of a group.
    532 	// good
    533 	async fn remove_group_admin(&self, auth: Authorization, gid: Query<i64>, uid: Query<i64>) -> Result<()> {
    534 		let conn = &mut open_db_conn();
    535 		let mut group: Group = groups::table.find(gid.0).first(conn)
    536 			.poemify("retreiving specified group")?;
    537 		let _user: User = users::table.find(uid.0).first(conn)
    538 			.poemify("retreiving specified user")?; // prevent adding invalid UID
    539 		if auth.0.id != group.owner {
    540 			return Err(UserFacingError::terse(StatusCode::FORBIDDEN).into());
    541 		}
    542 		group.admin.retain(|x| *x != uid.0);
    543 		diesel::update(groups::table.find(gid.0)).set(&group)
    544 			.execute(conn).poemify("updating group in database");
    545 		Ok(())
    546 	}
    547 
    548 	#[oai(path = "/group/channels", method = "get")]
    549 	/// Gets all channels in a group that are accessible to you
    550 	// good
    551 	async fn get_channels(&self, auth: Authorization, gid: Query<i64>) -> Result<Json<Vec<Channel>>> {
    552 		let conn = &mut open_db_conn();
    553 		Ok(Json(
    554 			groups::table
    555 				.select(groups::dsl::channels)
    556 				.filter(groups::dsl::id.eq(gid.0))
    557 				.first::<Vec<i64>>(conn)
    558 				.poemify("retreiving group channels")?
    559 				.iter()
    560 				.map(|c| channels::table.find(*c).first(conn).unwrap())
    561 				.filter(|c: &Channel| c.members.contains(&auth.0.id))
    562 				.collect()
    563 		))
    564 	}
    565 
    566 	#[oai(path = "/group/channels", method = "post")]
    567 	// good
    568 	async fn make_channel(&self, auth: Authorization, gid: Query<i64>, name: Query<String>) -> Result<Json<Channel>> {
    569 		let conn = &mut open_db_conn();
    570 		let out: Group = groups::table.find(gid.0).first(conn)
    571 			.with_backtrace().poemify("retrieving specified group")?;
    572 
    573 		if !out.admin.contains(&auth.0.id) {
    574 			return Err(UserFacingError::new(StatusCode::FORBIDDEN,
    575 				"You do not have permission to perform the requested action."
    576 			).into())
    577 		}
    578 
    579 		let chan = Channel {
    580 			id: gen_id(),
    581 			src_group: gid.0,
    582 			name: check_name(name.0.clone()),
    583 			members: vec![auth.0.id],
    584 			private: false
    585 		};
    586 
    587 		diesel::insert_into(channels::table).values(&chan)
    588 			.execute(conn).poemify("adding channel to database")?;
    589 
    590 		append_array(conn, "groups", "channels", gid.0, chan.id)
    591 			.poemify("adding channel to group")?;
    592 
    593 		Ok(Json(chan))
    594 	}
    595 
    596 	#[oai(path = "/channel", method = "put")]
    597 	/// Update the name of a channel.
    598 	///
    599 	/// Only authorized for group admins.
    600 	// good
    601 	async fn update_channel(&self, auth: Authorization, id: Query<i64>, name: Query<String>) -> Result<()> {
    602 		let conn = &mut open_db_conn();
    603 		diesel::update(channels::table.find(auth.0.id))
    604 			.set(channels::dsl::name.eq(check_name(name.0)))
    605 			.execute(conn).poemify("updating database");
    606 		Ok(())
    607 	}
    608 
    609 	#[oai(path = "/channel/private", method = "put")]
    610 	/// Make a channel private.
    611 	///
    612 	/// Only authorized for group admins.
    613 	// good
    614 	async fn make_channel_private(&self, auth: Authorization, id: Query<i64>, val: Query<bool>) -> Result<()> {
    615 		let conn = &mut open_db_conn();
    616 		let group: i64 = channels::table.select(channels::dsl::src_group).find(id.0)
    617 			.first(conn).poemify("getting channel group")?;
    618 		let admin: Vec<i64> = groups::table.select(groups::dsl::admin)
    619 			.find(group).first(conn).poemify("getting group admin")?;
    620 		if !admin.contains(&auth.0.id) {
    621 			return Err(UserFacingError::terse(StatusCode::FORBIDDEN).into())
    622 		}
    623 		diesel::update(channels::table.find(id.0))
    624 			.set(channels::dsl::private.eq(val.0))
    625 			.execute(conn).poemify("setting channel privacy in database");
    626 		Ok(())
    627 	}
    628 
    629 	#[oai(path = "/channel", method = "get")]
    630 	/// Get a channel.
    631 	// good
    632 	async fn get_channel(&self, auth: Authorization, id: Query<i64>) -> Result<Json<Channel>> {
    633 		let conn = &mut open_db_conn();
    634 		Ok(Json(channels::table.find(id.0).first(conn).poemify("retrieving specified channel")?))
    635 	}
    636 
    637 	#[oai(path = "/channel", method = "delete")]
    638 	/// Delete a channel.
    639 	///
    640 	/// Only authorized for group admins.
    641 	// good
    642 	async fn delete_channel(&self, auth: Authorization, id: Query<i64>) -> Result<()> {
    643 		let conn = &mut open_db_conn();
    644 		let group: i64 = channels::table.select(channels::dsl::src_group).find(id.0)
    645 			.first(conn).poemify("getting channel group")?;
    646 		let admin: Vec<i64> = groups::table.select(groups::dsl::admin)
    647 			.find(group).first(conn).poemify("getting group admin")?;
    648 		if !admin.contains(&auth.0.id) {
    649 			return Err(UserFacingError::terse(StatusCode::FORBIDDEN).into())
    650 		}
    651 		diesel::delete(channels::table.find(id.0)).execute(conn)
    652 			.poemify("deleting channel from database")?;
    653 		remove_array(conn, "groups", "channels", group, id.0)
    654 			.poemify("removing channel from group")?;
    655 		Ok(())
    656 	}
    657 
    658 	#[oai(path = "/channel/members", method = "get")]
    659 	/// Get the members that can access a channel.
    660 	///
    661 	/// No specific order for the list is guaranteed.
    662 	// good
    663 	async fn get_channel_members(&self, auth: Authorization, id: Query<i64>) -> Result<Json<Vec<User>>> {
    664 		let conn = &mut open_db_conn();
    665 		Ok(Json(
    666 			channels::table
    667 				.select(channels::dsl::members)
    668 				.filter(channels::dsl::id.eq(id.0))
    669 				.first::<Vec<i64>>(conn)
    670 				.poemify("retreiving channel members")?
    671 				.iter().map(|u| {
    672 					users::table.find(*u).first(conn).unwrap()
    673 				}).collect()
    674 		))
    675 	}
    676 
    677 	#[oai(path = "/channel/members", method = "put")]
    678 	/// Add a member to a channel
    679 	///
    680 	/// Only authorized for group admins.
    681 	// good
    682 	async fn add_channel_member(&self, auth: Authorization, cid: Query<i64>, uid: Query<i64>) -> Result<()> {
    683 		let conn = &mut open_db_conn();
    684 		let mut chan: Channel = channels::table.find(cid.0).first(conn)
    685 			.poemify("retreiving specified channel")?;
    686 		let group: Group = groups::table.find(chan.src_group).first(conn)
    687 			.poemify("retreiving channel's group")?;
    688 		let _user: User = users::table.find(uid.0).first(conn)
    689 			.poemify("retreiving specified user")?; // prevent adding invalid UID
    690 		if !group.admin.contains(&auth.0.id) {
    691 			return Err(UserFacingError::terse(StatusCode::FORBIDDEN).into());
    692 		}
    693 		chan.members.push(uid.0);
    694 		diesel::update(channels::table.find(cid.0)).set(&chan)
    695 			.execute(conn).poemify("updating channel in database");
    696 		Ok(())
    697 	}
    698 
    699 	#[oai(path = "/channel/members", method = "delete")]
    700 	/// Remove a member from a channel.
    701 	///
    702 	/// Only authorized for group admins.
    703 	// good
    704 	async fn remove_channel_member(&self, auth: Authorization, cid: Query<i64>, uid: Query<i64>) -> Result<()> {
    705 		let conn = &mut open_db_conn();
    706 		let mut chan: Channel = channels::table.find(cid.0).first(conn)
    707 			.poemify("retreiving specified channel")?;
    708 		let group: Group = groups::table.find(chan.src_group).first(conn)
    709 			.poemify("retreiving channel's group")?;
    710 		let _user: User = users::table.find(uid.0).first(conn)
    711 			.poemify("retreiving specified user")?; // prevent adding invalid UID
    712 		if !group.admin.contains(&auth.0.id) {
    713 			return Err(UserFacingError::terse(StatusCode::FORBIDDEN).into());
    714 		}
    715 		chan.members.retain(|x| *x != uid.0);
    716 		diesel::update(channels::table.find(cid.0)).set(&chan)
    717 			.execute(conn).poemify("updating channel in database");
    718 		Ok(())
    719 	}
    720 
    721 	#[oai(path = "/channel/term", method = "get")]
    722 	/// Get a batch of messages in channel containing `term` in the last 100 messages
    723 	///
    724 	/// Will not search for `term` in any messages older than the last 100.
    725 	// good
    726 	async fn search_channel(&self, auth: Authorization, cid: Query<i64>, term: Query<String>) -> Result<Json<Vec<Message>>> {
    727 		let conn = &mut open_db_conn();
    728 		let mut messages = messages::table.limit(100).load::<Message>(conn)
    729 			.poemify("retrieving past 100 messages")?;
    730 		messages.retain(|msg| {
    731 			if let Some(content) = &msg.content {
    732 				return content.contains(&term.0)
    733 			} else { return false }
    734 		});
    735 		Ok(Json(messages))
    736 	}
    737 
    738 	#[oai(path = "/channel/messages", method = "get")]
    739 	/// Returns batch of messages in channel. Do not use for small batches.
    740 	///
    741 	/// For small batches, use `chatterbox`, the websocket service for messaging, instead.
    742 	// good
    743 	async fn get_channel_messages(&self, auth: Authorization, cid: Query<i64>, num_msgs: Query<i64>) -> Result<Json<Vec<Message>>> {
    744 		let conn = &mut open_db_conn();
    745 		Ok(Json(
    746 			messages::table.limit(num_msgs.0).load::<Message>(conn)
    747 				.poemify("retrieving past messages")?
    748 		))
    749 	}
    750 
    751 	#[oai(path = "/message/thread", method = "put")]
    752 	/// Make a thread for a given message.
    753 	///
    754 	/// Thread will be private with you as its sole member
    755 	async fn make_thread(&self, auth: Authorization, id: Query<i64>, name: Query<String>) -> Result<Json<Channel>> {
    756 		let conn = &mut open_db_conn();
    757 		let mut msg: Message = messages::table.find(id.0).first(conn)
    758 			.poemify("retreiving specified message")?;
    759 		let group: i64 = channels::table.select(channels::dsl::src_group).find(msg.channel)
    760 			.first(conn).poemify("getting channel group")?;
    761 		let thread = Channel {
    762 			id: gen_id(),
    763 			name: check_name(name.0),
    764 			src_group: group,
    765 			members: vec![auth.0.id],
    766 			private: true,
    767 		};
    768 		msg.thread = Some(thread.id);
    769 		diesel::insert_into(channels::table).values(&thread)
    770 			.execute(conn).poemify("adding thread to database")?;
    771 		diesel::update(messages::table.find(id.0)).set(&msg)
    772 			.execute(conn).poemify("updating message in database")?;
    773 		Ok(Json(thread))
    774 	}
    775 
    776 	#[oai(path = "/message", method = "delete")]
    777 	/// Delete a message
    778 	///
    779 	/// Only authorized for the message author or a group admin.
    780 	async fn delete_message(&self, auth: Authorization, id: Query<i64>) -> Result<()> {
    781 		let conn = &mut open_db_conn();
    782 		let msg: Message = messages::table.find(id.0).first(conn)
    783 			.poemify("retreiving specified message")?;
    784 		let group: i64 = channels::table.select(channels::dsl::src_group).find(msg.channel)
    785 			.first(conn).poemify("getting channel group")?;
    786 		let admin: Vec<i64> = groups::table.select(groups::dsl::admin)
    787 			.find(group).first(conn).poemify("getting group admin")?;
    788 		if msg.author != auth.0.id && !admin.contains(&auth.0.id) {
    789 			return Err(UserFacingError::terse(StatusCode::FORBIDDEN).into());;
    790 		}
    791 		diesel::delete(messages::table.find(id.0)).execute(conn)
    792 			.poemify("deleting message")?;
    793 		Ok(())
    794 	}
    795 
    796 
    797 }
    798 
    799 #[tokio::main]
    800 async fn main() -> Result<(), std::io::Error> {
    801 	use hmac::Mac;
    802 
    803 	dotenv().ok();
    804 
    805 	if std::env::var_os("RUST_LOG").is_none() {
    806 		std::env::set_var("RUST_LOG", "poem=debug");
    807 	}
    808 	tracing_subscriber::fmt::init();
    809 	
    810 	let api_service = OpenApiService::new(Api {}, "Scuttlebutt", "1.0")
    811 		.description(
    812 			"Scuttlebutt is the REST API for managing everything but sending/receiving messages \
    813 					  - which means creating/updating/deleting all of your users/groups/channels.",
    814 		)
    815 		.server("http://localhost:3000/api");
    816 
    817 	// API documentation
    818 	// let ui = api_service.swagger_ui();
    819 
    820 	// Generate server-side secret key used for signing the JWTs
    821 	let key: String = rand::thread_rng()
    822 		.sample_iter(&Alphanumeric)
    823 		.take(7)
    824 		.map(char::from)
    825 		.collect();
    826 
    827 	let app = Route::new()
    828 		.nest("/api", api_service)
    829 		// .nest("/", ui)
    830 		.data(ServerKey::new_from_slice(&key.as_bytes()).unwrap())
    831 		.catch_error(|_: poem::error::NotFoundError| async move {
    832 			poem::Response::builder()
    833 				.status(StatusCode::NOT_FOUND)
    834 				.body("<h1>404 Not Found</h1>Path not found.")
    835 		})
    836 		.catch_error(|err: poem_openapi::error::ParseParamError| async move {
    837 			poem::Response::builder()
    838 				.status(StatusCode::BAD_REQUEST)
    839 				.body(format!("<h1>400 Bad Request</h1><pre>{}.</pre>", err))
    840 		});
    841 
    842 	Server::new(TcpListener::bind("127.0.0.1:3000")).run(app).await
    843 }
    844 
    845 #[cfg(test)]
    846 mod tests;