Refactoring
This commit is contained in:
parent
244e66cfa3
commit
94bd2da27b
18 changed files with 415 additions and 339 deletions
10
src/cookies.rs
Normal file
10
src/cookies.rs
Normal file
|
@ -0,0 +1,10 @@
|
||||||
|
pub mod token {
|
||||||
|
pub const TOKEN_COOKIE: &str = "token";
|
||||||
|
pub const TOKEN_EXPIRE_COOKIE: &str = "token_expire";
|
||||||
|
}
|
||||||
|
pub mod user {
|
||||||
|
pub const USER_ID_COOKIE: &str = "user_id";
|
||||||
|
pub const USER_NAME_COOKIE: &str = "user_name";
|
||||||
|
pub const USER_DISCRIMINATOR_COOKIE: &str = "user_discriminator";
|
||||||
|
pub const USER_AVATAR_COOKIE: &str = "user_avatar";
|
||||||
|
}
|
346
src/main.rs
346
src/main.rs
|
@ -1,327 +1,22 @@
|
||||||
#[macro_use]
|
#[macro_use]
|
||||||
extern crate rocket;
|
extern crate rocket;
|
||||||
|
|
||||||
use chrono::{Duration, Utc};
|
use poise::serenity_prelude::Http;
|
||||||
use poise::serenity_prelude::{GatewayIntents, Client, Http};
|
|
||||||
use core::panic;
|
|
||||||
use std::ops::Deref;
|
|
||||||
use derive_more::From;
|
|
||||||
use reqwest::StatusCode;
|
|
||||||
use rocket::form::Form;
|
|
||||||
use rocket::fs::{relative, FileServer};
|
use rocket::fs::{relative, FileServer};
|
||||||
use rocket::http::{Cookie, CookieJar};
|
use rocket_dyn_templates::Template;
|
||||||
use rocket::request::FromRequest;
|
|
||||||
use rocket::response::{content::RawHtml, Redirect};
|
|
||||||
use rocket::{request, Request, State};
|
|
||||||
use rocket_dyn_templates::{context, Template};
|
|
||||||
use sass_rocket_fairing::SassFairing;
|
use sass_rocket_fairing::SassFairing;
|
||||||
use serde::ser::SerializeStruct;
|
|
||||||
use serde::{Deserialize, Serialize, Serializer};
|
|
||||||
use std::collections::HashMap;
|
|
||||||
use std::convert::Infallible;
|
|
||||||
use std::env;
|
use std::env;
|
||||||
use std::fs;
|
|
||||||
|
|
||||||
mod challenge;
|
mod models;
|
||||||
use challenge::Challenge;
|
|
||||||
|
|
||||||
mod kyujitai;
|
mod utils;
|
||||||
|
|
||||||
#[get("/<challenge>")]
|
mod cookies;
|
||||||
async fn get_challenge(challenge: u32, cookies: &CookieJar<'_>) -> Template {
|
|
||||||
println!(
|
|
||||||
"{:?}",
|
|
||||||
cookies
|
|
||||||
.get_private("user_name")
|
|
||||||
.map(|cookie| cookie.value().to_owned())
|
|
||||||
);
|
|
||||||
Template::render(
|
|
||||||
"index",
|
|
||||||
context! {
|
|
||||||
challenge,
|
|
||||||
user: User::get(cookies).await.unwrap(),
|
|
||||||
content: {
|
|
||||||
use comrak::{parse_document, Arena, ComrakOptions};
|
|
||||||
let options = {
|
|
||||||
let mut options = ComrakOptions::default();
|
|
||||||
options.extension.front_matter_delimiter = Some("---".to_owned());
|
|
||||||
options
|
|
||||||
};
|
|
||||||
let arena = Arena::new();
|
|
||||||
let root = parse_document(
|
|
||||||
&arena,
|
|
||||||
&fs::read_to_string(format!("content/challenges/{challenge}.md")).expect("Couldn't find challenge file"),
|
|
||||||
&options,
|
|
||||||
);
|
|
||||||
if let Some(node) = root.children().next() {
|
|
||||||
if let comrak::nodes::NodeValue::FrontMatter(frontmatter) = &node.data.borrow().value {
|
|
||||||
let frontmatter = {
|
|
||||||
// Trim starting and ending fences
|
|
||||||
let lines: Vec<&str> = frontmatter.trim().lines().collect();
|
|
||||||
lines[1..lines.len() - 1].join("\n")
|
|
||||||
};
|
|
||||||
let challenge: Challenge = serde_yaml::from_str(&frontmatter).unwrap();
|
|
||||||
challenge
|
|
||||||
} else {
|
|
||||||
panic!("No frontmatter!")
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
panic!("Empty document!")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
#[get("/login")]
|
mod routes;
|
||||||
fn login() -> Redirect {
|
use routes::*;
|
||||||
Redirect::to(format!(
|
|
||||||
// Switch from response_type=code to response_type=token from URL generator
|
|
||||||
"https://discord.com/api/oauth2/authorize?client_id={client_id}&redirect_uri={redirect_uri}&response_type=token&scope=identify%20guilds.join%20guilds",
|
|
||||||
client_id = env::var("CLIENT_ID").unwrap(),
|
|
||||||
redirect_uri = format!("{}success", env::var("DOMAIN").unwrap()),
|
|
||||||
))
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(FromForm)]
|
mod prelude;
|
||||||
struct Login<'r> {
|
|
||||||
token_type: &'r str,
|
|
||||||
access_token: &'r str,
|
|
||||||
expires_in: u64,
|
|
||||||
scope: &'r str,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[post("/login", data = "<login>")]
|
|
||||||
async fn post_login(login: Form<Login<'_>>, cookies: &CookieJar<'_>) -> Redirect {
|
|
||||||
if (login.token_type != "Bearer" || login.scope != "guilds.join+identify+guilds")
|
|
||||||
&& User::init(login.access_token, cookies).await.is_ok()
|
|
||||||
{
|
|
||||||
cookies.add(Cookie::new(
|
|
||||||
TOKEN_EXPIRE_COOKIE,
|
|
||||||
(Utc::now() + Duration::seconds(login.expires_in as i64))
|
|
||||||
.timestamp()
|
|
||||||
.to_string(),
|
|
||||||
));
|
|
||||||
}
|
|
||||||
Redirect::to("/")
|
|
||||||
}
|
|
||||||
|
|
||||||
#[get("/success")]
|
|
||||||
fn success() -> RawHtml<&'static str> {
|
|
||||||
RawHtml(
|
|
||||||
"<form action=\"/login\" method=\"post\"></form>
|
|
||||||
<script>
|
|
||||||
try {
|
|
||||||
const params = new URLSearchParams(location.hash.slice(1));
|
|
||||||
const form = document.querySelector(\"form\");
|
|
||||||
[\"token_type\", \"access_token\", \"expires_in\", \"scope\"].forEach(field => {
|
|
||||||
const input = document.createElement(\"input\");
|
|
||||||
input.type = \"hidden\";
|
|
||||||
input.name = field;
|
|
||||||
input.value = params.get(field);
|
|
||||||
form.appendChild(input);
|
|
||||||
});
|
|
||||||
form.submit();
|
|
||||||
} catch {
|
|
||||||
location.href = \"/\";
|
|
||||||
}
|
|
||||||
</script>",
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
struct Referer(Option<String>);
|
|
||||||
|
|
||||||
#[rocket::async_trait]
|
|
||||||
impl<'r> FromRequest<'r> for Referer {
|
|
||||||
type Error = Infallible;
|
|
||||||
|
|
||||||
async fn from_request(req: &'r Request<'_>) -> request::Outcome<Self, Self::Error> {
|
|
||||||
let referer = req.headers().get_one("Referer");
|
|
||||||
request::Outcome::Success(Referer(referer.map(|referer| referer.to_owned())))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const TOKEN_COOKIE: &str = "token";
|
|
||||||
const TOKEN_EXPIRE_COOKIE: &str = "token_expire";
|
|
||||||
|
|
||||||
#[get("/logout")]
|
|
||||||
fn logout(cookies: &CookieJar<'_>, referer: Referer) -> Redirect {
|
|
||||||
let token = match cookies.get_private(TOKEN_COOKIE) {
|
|
||||||
Some(cookie) => cookie.value().to_owned(),
|
|
||||||
None => return Redirect::to("/"),
|
|
||||||
};
|
|
||||||
rocket::tokio::spawn(async {
|
|
||||||
let client = reqwest::Client::new();
|
|
||||||
let params = {
|
|
||||||
let mut params = HashMap::new();
|
|
||||||
params.insert("client_id", env::var("CLIENT_ID").unwrap());
|
|
||||||
params.insert("client_secret", env::var("CLIENT_SECRET").unwrap());
|
|
||||||
params.insert("token", token);
|
|
||||||
params
|
|
||||||
};
|
|
||||||
match client
|
|
||||||
.post("https://discord.com/api/oauth2/token/revoke")
|
|
||||||
.header("Content-Type", "application/x-www-form-urlencoded")
|
|
||||||
.form(¶ms)
|
|
||||||
.send()
|
|
||||||
.await
|
|
||||||
{
|
|
||||||
Ok(_) => println!("Successfully revoked token"),
|
|
||||||
Err(error) => println!("Failed to revoke token: {:?}", error),
|
|
||||||
};
|
|
||||||
});
|
|
||||||
User::purge(cookies);
|
|
||||||
let redirect_url = referer.0.unwrap_or("/".to_owned());
|
|
||||||
Redirect::to(redirect_url)
|
|
||||||
}
|
|
||||||
|
|
||||||
#[get("/testing")]
|
|
||||||
async fn testing(cookies: &CookieJar<'_>, http: &State<Http>) -> String {
|
|
||||||
// Get logged in user's join date in 字ちゃん server
|
|
||||||
format!("{:?}", http.get_guild(814700630958276649).await
|
|
||||||
.expect("Failed to get testing guild")
|
|
||||||
.member(http.deref(), User::get(cookies).await
|
|
||||||
.expect("Failed to get logged in user data")
|
|
||||||
.expect("No logged in user")
|
|
||||||
.id
|
|
||||||
).await
|
|
||||||
.expect("Failed to fetch user in server")
|
|
||||||
.joined_at)
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Default, Deserialize)]
|
|
||||||
struct User {
|
|
||||||
#[serde(deserialize_with = "deserialize_id")]
|
|
||||||
id: u64,
|
|
||||||
#[serde(rename = "username")]
|
|
||||||
name: String,
|
|
||||||
#[serde(deserialize_with = "deserialize_discriminator")]
|
|
||||||
discriminator: u16,
|
|
||||||
avatar: String,
|
|
||||||
}
|
|
||||||
|
|
||||||
fn deserialize_id<'de, D>(deserializer: D) -> Result<u64, D::Error>
|
|
||||||
where
|
|
||||||
D: serde::Deserializer<'de>,
|
|
||||||
{
|
|
||||||
let id_str: &str = serde::Deserialize::deserialize(deserializer)?;
|
|
||||||
id_str.parse().map_err(serde::de::Error::custom)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn deserialize_discriminator<'de, D>(deserializer: D) -> Result<u16, D::Error>
|
|
||||||
where
|
|
||||||
D: serde::Deserializer<'de>,
|
|
||||||
{
|
|
||||||
let id_str: &str = serde::Deserialize::deserialize(deserializer)?;
|
|
||||||
id_str.parse().map_err(serde::de::Error::custom)
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Serialize for User {
|
|
||||||
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
|
|
||||||
where
|
|
||||||
S: Serializer,
|
|
||||||
{
|
|
||||||
let mut state = serializer.serialize_struct("User", 5)?;
|
|
||||||
state.serialize_field("id", &self.id)?;
|
|
||||||
state.serialize_field("name", &self.name)?;
|
|
||||||
state.serialize_field("discriminator", &self.discriminator)?;
|
|
||||||
state.serialize_field("avatar", &self.avatar)?;
|
|
||||||
state.serialize_field("username", &self.username())?;
|
|
||||||
state.end()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(From, Debug)]
|
|
||||||
|
|
||||||
enum GetUserError {
|
|
||||||
ReqwestError(reqwest::Error),
|
|
||||||
DeserializeError(serde_json::Error),
|
|
||||||
#[allow(unused)]
|
|
||||||
DiscordError {
|
|
||||||
status: StatusCode,
|
|
||||||
message: Option<String>,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
fn parse_cookie_value<T: std::str::FromStr>(cookies: &CookieJar<'_>, name: &str) -> Option<T> {
|
|
||||||
cookies.get_private(name)?.value().parse().ok()
|
|
||||||
}
|
|
||||||
|
|
||||||
impl User {
|
|
||||||
fn username(&self) -> String {
|
|
||||||
if self.discriminator == 0 {
|
|
||||||
return self.name.clone();
|
|
||||||
}
|
|
||||||
format!("{}#{:0>4}", self.name, self.discriminator)
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn init(token: &str, cookies: &CookieJar<'_>) -> Result<Self, GetUserError> {
|
|
||||||
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", user.id.to_string()));
|
|
||||||
cookies.add_private(Cookie::new("user_name", user.name.clone()));
|
|
||||||
cookies.add_private(Cookie::new(
|
|
||||||
"user_discriminator",
|
|
||||||
user.discriminator.to_string(),
|
|
||||||
));
|
|
||||||
cookies.add_private(Cookie::new("user_avatar", user.avatar.clone()));
|
|
||||||
Ok(user)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn purge(cookies: &CookieJar<'_>) {
|
|
||||||
cookies.remove_private(Cookie::named(TOKEN_COOKIE));
|
|
||||||
cookies.remove_private(Cookie::named("user_id"));
|
|
||||||
cookies.remove_private(Cookie::named("user_name"));
|
|
||||||
cookies.remove_private(Cookie::named("user_discriminator"));
|
|
||||||
cookies.remove_private(Cookie::named("user_avatar"));
|
|
||||||
cookies.remove(Cookie::named(TOKEN_EXPIRE_COOKIE));
|
|
||||||
}
|
|
||||||
|
|
||||||
fn from_cookies(cookies: &CookieJar<'_>) -> Option<Self> {
|
|
||||||
Some(Self {
|
|
||||||
id: parse_cookie_value(cookies, "user_id")?,
|
|
||||||
name: parse_cookie_value(cookies, "user_name")?,
|
|
||||||
discriminator: parse_cookie_value(cookies, "user_discriminator")?,
|
|
||||||
avatar: parse_cookie_value(cookies, "user_avatar")?,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn get(cookies: &CookieJar<'_>) -> Result<Option<Self>, 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::<i64>())
|
|
||||||
.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"));
|
|
||||||
cookies.remove_private(Cookie::named("user_name"));
|
|
||||||
cookies.remove_private(Cookie::named("user_discriminator"));
|
|
||||||
cookies.remove_private(Cookie::named("user_avatar"));
|
|
||||||
cookies.remove(Cookie::named(TOKEN_EXPIRE_COOKIE));
|
|
||||||
return Ok(None);
|
|
||||||
}
|
|
||||||
Ok(Some(user))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[launch]
|
#[launch]
|
||||||
async fn rocket() -> _ {
|
async fn rocket() -> _ {
|
||||||
|
@ -343,28 +38,3 @@ async fn rocket() -> _ {
|
||||||
.attach(Template::fairing())
|
.attach(Template::fairing())
|
||||||
.attach(SassFairing::default())
|
.attach(SassFairing::default())
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
|
||||||
mod tests {
|
|
||||||
use crate::User;
|
|
||||||
|
|
||||||
fn test_user(name: &str, discriminator: u16) -> User {
|
|
||||||
User {
|
|
||||||
name: name.to_owned(),
|
|
||||||
discriminator,
|
|
||||||
..Default::default()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_legacy_username() {
|
|
||||||
let user = test_user("test", 123);
|
|
||||||
assert_eq!(user.username(), "test#0123");
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_new_username() {
|
|
||||||
let user = test_user("test", 0);
|
|
||||||
assert_eq!(user.username(), "test");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
|
@ -4,7 +4,7 @@ use std::str::FromStr;
|
||||||
use serde::{Deserialize, Deserializer, Serialize};
|
use serde::{Deserialize, Deserializer, Serialize};
|
||||||
use serde_yaml::Value;
|
use serde_yaml::Value;
|
||||||
|
|
||||||
use crate::kyujitai::Kyujitai;
|
use crate::prelude::*;
|
||||||
|
|
||||||
#[derive(Serialize, Deserialize)]
|
#[derive(Serialize, Deserialize)]
|
||||||
pub struct Challenge {
|
pub struct Challenge {
|
||||||
|
@ -18,6 +18,40 @@ pub struct Challenge {
|
||||||
pub date: chrono::NaiveDate,
|
pub date: chrono::NaiveDate,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl Challenge {
|
||||||
|
pub fn get(number: u32) -> Self {
|
||||||
|
use comrak::{parse_document, Arena, ComrakOptions};
|
||||||
|
use std::fs;
|
||||||
|
|
||||||
|
let options = {
|
||||||
|
let mut options = ComrakOptions::default();
|
||||||
|
options.extension.front_matter_delimiter = Some("---".to_owned());
|
||||||
|
options
|
||||||
|
};
|
||||||
|
let arena = Arena::new();
|
||||||
|
let root = parse_document(
|
||||||
|
&arena,
|
||||||
|
&fs::read_to_string(format!("content/challenges/{number}.md")).expect("Couldn't find challenge file"),
|
||||||
|
&options,
|
||||||
|
);
|
||||||
|
if let Some(node) = root.children().next() {
|
||||||
|
if let comrak::nodes::NodeValue::FrontMatter(frontmatter) = &node.data.borrow().value {
|
||||||
|
let frontmatter = {
|
||||||
|
// Trim starting and ending fences
|
||||||
|
let lines: Vec<&str> = frontmatter.trim().lines().collect();
|
||||||
|
lines[1..lines.len() - 1].join("\n")
|
||||||
|
};
|
||||||
|
let challenge: Challenge = serde_yaml::from_str(&frontmatter).unwrap();
|
||||||
|
challenge
|
||||||
|
} else {
|
||||||
|
panic!("No frontmatter!")
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
panic!("Empty document!")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Serialize, Deserialize)]
|
#[derive(Serialize, Deserialize)]
|
||||||
pub struct Song {
|
pub struct Song {
|
||||||
pub japanese: Option<String>,
|
pub japanese: Option<String>,
|
5
src/models/mod.rs
Normal file
5
src/models/mod.rs
Normal file
|
@ -0,0 +1,5 @@
|
||||||
|
mod user;
|
||||||
|
pub use user::User;
|
||||||
|
|
||||||
|
mod challenge;
|
||||||
|
pub use challenge::Challenge;
|
116
src/models/user/mod.rs
Normal file
116
src/models/user/mod.rs
Normal file
|
@ -0,0 +1,116 @@
|
||||||
|
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<Self, GetUserError> {
|
||||||
|
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<Self> {
|
||||||
|
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<Option<Self>, 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::<i64>())
|
||||||
|
.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<String>,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
fn parse_cookie_value<T: std::str::FromStr>(cookies: &CookieJar<'_>, name: &str) -> Option<T> {
|
||||||
|
cookies.get_private(name)?.value().parse().ok()
|
||||||
|
}
|
34
src/models/user/serial.rs
Normal file
34
src/models/user/serial.rs
Normal file
|
@ -0,0 +1,34 @@
|
||||||
|
use serde::{Serialize, Serializer, ser::SerializeStruct};
|
||||||
|
|
||||||
|
use super::User;
|
||||||
|
|
||||||
|
pub fn deserialize_id<'de, D>(deserializer: D) -> Result<u64, D::Error>
|
||||||
|
where
|
||||||
|
D: serde::Deserializer<'de>,
|
||||||
|
{
|
||||||
|
let id_str: &str = serde::Deserialize::deserialize(deserializer)?;
|
||||||
|
id_str.parse().map_err(serde::de::Error::custom)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn deserialize_discriminator<'de, D>(deserializer: D) -> Result<u16, D::Error>
|
||||||
|
where
|
||||||
|
D: serde::Deserializer<'de>,
|
||||||
|
{
|
||||||
|
let id_str: &str = serde::Deserialize::deserialize(deserializer)?;
|
||||||
|
id_str.parse().map_err(serde::de::Error::custom)
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Serialize for User {
|
||||||
|
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
|
||||||
|
where
|
||||||
|
S: Serializer,
|
||||||
|
{
|
||||||
|
let mut state = serializer.serialize_struct("User", 5)?;
|
||||||
|
state.serialize_field("id", &self.id)?;
|
||||||
|
state.serialize_field("name", &self.name)?;
|
||||||
|
state.serialize_field("discriminator", &self.discriminator)?;
|
||||||
|
state.serialize_field("avatar", &self.avatar)?;
|
||||||
|
state.serialize_field("username", &self.username())?;
|
||||||
|
state.end()
|
||||||
|
}
|
||||||
|
}
|
21
src/models/user/tests.rs
Normal file
21
src/models/user/tests.rs
Normal file
|
@ -0,0 +1,21 @@
|
||||||
|
use super::User;
|
||||||
|
|
||||||
|
fn test_user(name: &str, discriminator: u16) -> User {
|
||||||
|
User {
|
||||||
|
name: name.to_owned(),
|
||||||
|
discriminator,
|
||||||
|
..Default::default()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_legacy_username() {
|
||||||
|
let user = test_user("test", 123);
|
||||||
|
assert_eq!(user.username(), "test#0123");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_new_username() {
|
||||||
|
let user = test_user("test", 0);
|
||||||
|
assert_eq!(user.username(), "test");
|
||||||
|
}
|
1
src/prelude.rs
Normal file
1
src/prelude.rs
Normal file
|
@ -0,0 +1 @@
|
||||||
|
pub use crate::utils::Kyujitai;
|
22
src/routes/get_challenge.rs
Normal file
22
src/routes/get_challenge.rs
Normal file
|
@ -0,0 +1,22 @@
|
||||||
|
use rocket::http::CookieJar;
|
||||||
|
use rocket_dyn_templates::{Template, context};
|
||||||
|
|
||||||
|
use crate::models::{User, Challenge};
|
||||||
|
|
||||||
|
#[get("/<challenge>")]
|
||||||
|
pub async fn get_challenge(challenge: u32, cookies: &CookieJar<'_>) -> Template {
|
||||||
|
println!(
|
||||||
|
"{:?}",
|
||||||
|
cookies
|
||||||
|
.get_private("user_name")
|
||||||
|
.map(|cookie| cookie.value().to_owned())
|
||||||
|
);
|
||||||
|
Template::render(
|
||||||
|
"index",
|
||||||
|
context! {
|
||||||
|
challenge,
|
||||||
|
user: User::get(cookies).await.unwrap(),
|
||||||
|
content: Challenge::get(challenge),
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
13
src/routes/login.rs
Normal file
13
src/routes/login.rs
Normal file
|
@ -0,0 +1,13 @@
|
||||||
|
use std::env;
|
||||||
|
|
||||||
|
use rocket::response::Redirect;
|
||||||
|
|
||||||
|
#[get("/login")]
|
||||||
|
pub fn login() -> Redirect {
|
||||||
|
Redirect::to(format!(
|
||||||
|
// Switch from response_type=code to response_type=token from URL generator
|
||||||
|
"https://discord.com/api/oauth2/authorize?client_id={client_id}&redirect_uri={redirect_uri}&response_type=token&scope=identify%20guilds.join%20guilds",
|
||||||
|
client_id = env::var("CLIENT_ID").unwrap(),
|
||||||
|
redirect_uri = format!("{}success", env::var("DOMAIN").unwrap()),
|
||||||
|
))
|
||||||
|
}
|
36
src/routes/logout.rs
Normal file
36
src/routes/logout.rs
Normal file
|
@ -0,0 +1,36 @@
|
||||||
|
use std::{collections::HashMap, env};
|
||||||
|
|
||||||
|
use rocket::{http::CookieJar, response::Redirect};
|
||||||
|
|
||||||
|
use crate::{utils::Referer, cookies::token::TOKEN_COOKIE, models::User};
|
||||||
|
|
||||||
|
#[get("/logout")]
|
||||||
|
pub fn logout(cookies: &CookieJar<'_>, referer: Referer) -> Redirect {
|
||||||
|
let token = match cookies.get_private(TOKEN_COOKIE) {
|
||||||
|
Some(cookie) => cookie.value().to_owned(),
|
||||||
|
None => return Redirect::to("/"),
|
||||||
|
};
|
||||||
|
rocket::tokio::spawn(async {
|
||||||
|
let client = reqwest::Client::new();
|
||||||
|
let params = {
|
||||||
|
let mut params = HashMap::new();
|
||||||
|
params.insert("client_id", env::var("CLIENT_ID").unwrap());
|
||||||
|
params.insert("client_secret", env::var("CLIENT_SECRET").unwrap());
|
||||||
|
params.insert("token", token);
|
||||||
|
params
|
||||||
|
};
|
||||||
|
match client
|
||||||
|
.post("https://discord.com/api/oauth2/token/revoke")
|
||||||
|
.header("Content-Type", "application/x-www-form-urlencoded")
|
||||||
|
.form(¶ms)
|
||||||
|
.send()
|
||||||
|
.await
|
||||||
|
{
|
||||||
|
Ok(_) => println!("Successfully revoked token"),
|
||||||
|
Err(error) => println!("Failed to revoke token: {:?}", error),
|
||||||
|
};
|
||||||
|
});
|
||||||
|
User::purge(cookies);
|
||||||
|
let redirect_url = referer.0.unwrap_or("/".to_owned());
|
||||||
|
Redirect::to(redirect_url)
|
||||||
|
}
|
23
src/routes/mod.rs
Normal file
23
src/routes/mod.rs
Normal file
|
@ -0,0 +1,23 @@
|
||||||
|
#[path = "get_challenge.rs"]
|
||||||
|
mod _get_challenge;
|
||||||
|
pub use _get_challenge::get_challenge;
|
||||||
|
|
||||||
|
#[path = "login.rs"]
|
||||||
|
mod _login;
|
||||||
|
pub use _login::login;
|
||||||
|
|
||||||
|
#[path = "logout.rs"]
|
||||||
|
mod _logout;
|
||||||
|
pub use _logout::logout;
|
||||||
|
|
||||||
|
#[path = "post_login.rs"]
|
||||||
|
mod _post_login;
|
||||||
|
pub use _post_login::post_login;
|
||||||
|
|
||||||
|
#[path = "success.rs"]
|
||||||
|
mod _success;
|
||||||
|
pub use _success::success;
|
||||||
|
|
||||||
|
#[path = "testing.rs"]
|
||||||
|
mod _testing;
|
||||||
|
pub use _testing::testing;
|
27
src/routes/post_login.rs
Normal file
27
src/routes/post_login.rs
Normal file
|
@ -0,0 +1,27 @@
|
||||||
|
use chrono::{Utc, Duration};
|
||||||
|
use rocket::{form::Form, http::{CookieJar, Cookie}, response::Redirect};
|
||||||
|
|
||||||
|
use crate::{cookies::token::TOKEN_EXPIRE_COOKIE, models::User};
|
||||||
|
|
||||||
|
#[derive(FromForm)]
|
||||||
|
pub struct Login<'r> {
|
||||||
|
token_type: &'r str,
|
||||||
|
access_token: &'r str,
|
||||||
|
expires_in: u64,
|
||||||
|
scope: &'r str,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[post("/login", data = "<login>")]
|
||||||
|
pub async fn post_login(login: Form<Login<'_>>, cookies: &CookieJar<'_>) -> Redirect {
|
||||||
|
if (login.token_type != "Bearer" || login.scope != "guilds.join+identify+guilds")
|
||||||
|
&& User::init(login.access_token, cookies).await.is_ok()
|
||||||
|
{
|
||||||
|
cookies.add(Cookie::new(
|
||||||
|
TOKEN_EXPIRE_COOKIE,
|
||||||
|
(Utc::now() + Duration::seconds(login.expires_in as i64))
|
||||||
|
.timestamp()
|
||||||
|
.to_string(),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
Redirect::to("/")
|
||||||
|
}
|
24
src/routes/success.rs
Normal file
24
src/routes/success.rs
Normal file
|
@ -0,0 +1,24 @@
|
||||||
|
use rocket::response::content::RawHtml;
|
||||||
|
|
||||||
|
#[get("/success")]
|
||||||
|
pub fn success() -> RawHtml<&'static str> {
|
||||||
|
RawHtml(
|
||||||
|
"<form action=\"/login\" method=\"post\"></form>
|
||||||
|
<script>
|
||||||
|
try {
|
||||||
|
const params = new URLSearchParams(location.hash.slice(1));
|
||||||
|
const form = document.querySelector(\"form\");
|
||||||
|
[\"token_type\", \"access_token\", \"expires_in\", \"scope\"].forEach(field => {
|
||||||
|
const input = document.createElement(\"input\");
|
||||||
|
input.type = \"hidden\";
|
||||||
|
input.name = field;
|
||||||
|
input.value = params.get(field);
|
||||||
|
form.appendChild(input);
|
||||||
|
});
|
||||||
|
form.submit();
|
||||||
|
} catch {
|
||||||
|
location.href = \"/\";
|
||||||
|
}
|
||||||
|
</script>",
|
||||||
|
)
|
||||||
|
}
|
20
src/routes/testing.rs
Normal file
20
src/routes/testing.rs
Normal file
|
@ -0,0 +1,20 @@
|
||||||
|
use std::ops::Deref;
|
||||||
|
|
||||||
|
use poise::serenity_prelude::Http;
|
||||||
|
use rocket::{http::CookieJar, State};
|
||||||
|
|
||||||
|
use crate::models::User;
|
||||||
|
|
||||||
|
#[get("/testing")]
|
||||||
|
pub async fn testing(cookies: &CookieJar<'_>, http: &State<Http>) -> String {
|
||||||
|
// Get logged in user's join date in 字ちゃん server
|
||||||
|
format!("{:?}", http.get_guild(814700630958276649).await
|
||||||
|
.expect("Failed to get testing guild")
|
||||||
|
.member(http.deref(), User::get(cookies).await
|
||||||
|
.expect("Failed to get logged in user data")
|
||||||
|
.expect("No logged in user")
|
||||||
|
.id
|
||||||
|
).await
|
||||||
|
.expect("Failed to fetch user in server")
|
||||||
|
.joined_at)
|
||||||
|
}
|
5
src/utils/mod.rs
Normal file
5
src/utils/mod.rs
Normal file
|
@ -0,0 +1,5 @@
|
||||||
|
mod kyujitai;
|
||||||
|
pub use kyujitai::Kyujitai;
|
||||||
|
|
||||||
|
mod referer;
|
||||||
|
pub use referer::Referer;
|
15
src/utils/referer.rs
Normal file
15
src/utils/referer.rs
Normal file
|
@ -0,0 +1,15 @@
|
||||||
|
use std::convert::Infallible;
|
||||||
|
|
||||||
|
use rocket::{request::{FromRequest, self}, Request};
|
||||||
|
|
||||||
|
pub struct Referer(pub Option<String>);
|
||||||
|
|
||||||
|
#[rocket::async_trait]
|
||||||
|
impl<'r> FromRequest<'r> for Referer {
|
||||||
|
type Error = Infallible;
|
||||||
|
|
||||||
|
async fn from_request(req: &'r Request<'_>) -> request::Outcome<Self, Self::Error> {
|
||||||
|
let referer = req.headers().get_one("Referer");
|
||||||
|
request::Outcome::Success(Referer(referer.map(|referer| referer.to_owned())))
|
||||||
|
}
|
||||||
|
}
|
Loading…
Add table
Reference in a new issue