mod serial; #[cfg(test)] mod tests; use chrono::Utc; use derive_more::From; use reqwest::StatusCode; use rocket::http::{CookieJar, Cookie}; use serial::*; use serde::Deserialize; use crate::cookies::{token::*, user::*}; #[derive(Default, Deserialize)] pub struct User { #[serde(deserialize_with = "deserialize_id")] pub id: u64, #[serde(rename = "username")] pub name: String, #[serde(deserialize_with = "deserialize_discriminator")] pub discriminator: u16, pub avatar: String, } impl User { pub fn username(&self) -> String { if self.discriminator == 0 { return self.name.clone(); } format!("{}#{:0>4}", self.name, self.discriminator) } pub async fn init(token: &str, cookies: &CookieJar<'_>) -> Result { let (status, text) = { let response = reqwest::Client::new() .get("https://discord.com/api/users/@me") .header("Authorization", format!("Bearer {token}")) .send() .await?; (response.status(), response.text().await) }; if !status.is_success() { return Err(GetUserError::DiscordError { status, message: text.ok(), }); } let user: Self = serde_json::from_str(&text?)?; cookies.add_private(Cookie::new(TOKEN_COOKIE, token.to_owned())); cookies.add_private(Cookie::new(USER_ID_COOKIE, user.id.to_string())); cookies.add_private(Cookie::new(USER_NAME_COOKIE, user.name.clone())); cookies.add_private(Cookie::new( USER_DISCRIMINATOR_COOKIE, user.discriminator.to_string(), )); cookies.add_private(Cookie::new(USER_AVATAR_COOKIE, user.avatar.clone())); Ok(user) } pub fn purge(cookies: &CookieJar<'_>) { cookies.remove_private(Cookie::named(TOKEN_COOKIE)); cookies.remove_private(Cookie::named(USER_ID_COOKIE)); cookies.remove_private(Cookie::named(USER_NAME_COOKIE)); cookies.remove_private(Cookie::named(USER_DISCRIMINATOR_COOKIE)); cookies.remove_private(Cookie::named(USER_AVATAR_COOKIE)); cookies.remove(Cookie::named(TOKEN_EXPIRE_COOKIE)); } fn from_cookies(cookies: &CookieJar<'_>) -> Option { Some(Self { id: parse_cookie_value(cookies, USER_ID_COOKIE)?, name: parse_cookie_value(cookies, USER_NAME_COOKIE)?, discriminator: parse_cookie_value(cookies, USER_DISCRIMINATOR_COOKIE)?, avatar: parse_cookie_value(cookies, USER_AVATAR_COOKIE)?, }) } pub async fn get(cookies: &CookieJar<'_>) -> Result, GetUserError> { let user = match Self::from_cookies(cookies) { Some(user) => user, None => return Ok(None), }; if cookies .get(TOKEN_EXPIRE_COOKIE) .map(|expire| expire.value().parse::()) .and_then(Result::ok) .map_or(true, |timestamp| Utc::now().timestamp() >= timestamp) { cookies.remove_private(Cookie::named(TOKEN_COOKIE)); cookies.remove_private(Cookie::named(USER_ID_COOKIE)); cookies.remove_private(Cookie::named(USER_NAME_COOKIE)); cookies.remove_private(Cookie::named(USER_DISCRIMINATOR_COOKIE)); cookies.remove_private(Cookie::named(USER_AVATAR_COOKIE)); cookies.remove(Cookie::named(TOKEN_EXPIRE_COOKIE)); return Ok(None); } Ok(Some(user)) } } #[derive(From, Debug)] pub enum GetUserError { ReqwestError(reqwest::Error), DeserializeError(serde_json::Error), #[allow(unused)] DiscordError { status: StatusCode, message: Option, }, } fn parse_cookie_value(cookies: &CookieJar<'_>, name: &str) -> Option { cookies.get_private(name)?.value().parse().ok() }