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;