Refactoring
This commit is contained in:
parent
244e66cfa3
commit
94bd2da27b
18 changed files with 415 additions and 339 deletions
187
src/models/challenge.rs
Normal file
187
src/models/challenge.rs
Normal file
|
@ -0,0 +1,187 @@
|
|||
use core::panic;
|
||||
use std::str::FromStr;
|
||||
|
||||
use serde::{Deserialize, Deserializer, Serialize};
|
||||
use serde_yaml::Value;
|
||||
|
||||
use crate::prelude::*;
|
||||
|
||||
#[derive(Serialize, Deserialize)]
|
||||
pub struct Challenge {
|
||||
pub japanese: Vec<Vec<Vec<ChallengeWord>>>,
|
||||
pub english: Option<Vec<String>>,
|
||||
pub song: Option<Song>,
|
||||
pub youtube: Option<String>,
|
||||
pub spotify: Option<String>,
|
||||
pub translation: Option<Translation>,
|
||||
pub suggester: Option<String>,
|
||||
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)]
|
||||
pub struct Song {
|
||||
pub japanese: Option<String>,
|
||||
pub english: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
pub struct ChallengeWord {
|
||||
pub dictionary: Option<String>,
|
||||
pub pos: Option<PartOfSpeech>,
|
||||
pub text: Option<Vec<Furigana>>,
|
||||
}
|
||||
|
||||
impl<'de> Deserialize<'de> for ChallengeWord {
|
||||
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
|
||||
where
|
||||
D: Deserializer<'de>,
|
||||
{
|
||||
let value = Deserialize::deserialize(deserializer)?;
|
||||
let map = if let Value::Mapping(map) = value {
|
||||
map
|
||||
} else {
|
||||
return Err(serde::de::Error::invalid_type(
|
||||
serde::de::Unexpected::Other("not a string or map"),
|
||||
&"a string or map",
|
||||
));
|
||||
};
|
||||
let text = map.get("text").map(|value| match value {
|
||||
Value::String(string) => vec![Furigana::new(string.clone(), None)],
|
||||
Value::Sequence(sequence) => sequence
|
||||
.iter()
|
||||
.map(|value| match value {
|
||||
Value::String(kanji) => Furigana::new(kanji.to_owned(), None),
|
||||
Value::Mapping(mapping) => {
|
||||
let (kanji, furigana) = mapping.iter().next().unwrap();
|
||||
Furigana::new(
|
||||
kanji.as_str().unwrap().to_owned(),
|
||||
Some(furigana.as_str().unwrap().to_owned()),
|
||||
)
|
||||
}
|
||||
_ => panic!(),
|
||||
})
|
||||
.collect(),
|
||||
_ => panic!(),
|
||||
});
|
||||
let dictionary = match map.get("dictionary") {
|
||||
Some(value) => match value {
|
||||
Value::Null => None,
|
||||
Value::String(dictionary) => Some(if !dictionary.starts_with("http") {
|
||||
format!("https://jisho.org/word/{dictionary}")
|
||||
} else {
|
||||
dictionary.to_owned()
|
||||
}),
|
||||
_ => panic!("dictionary must be string!"),
|
||||
},
|
||||
None => text.as_ref().map(|furigana| {
|
||||
furigana
|
||||
.iter()
|
||||
.map(|segment| segment.kanji.clone())
|
||||
.collect()
|
||||
}),
|
||||
};
|
||||
let pos: Option<PartOfSpeech> = map
|
||||
.get("pos")
|
||||
.map(|value| value.as_str().unwrap().parse().unwrap());
|
||||
Ok(ChallengeWord {
|
||||
dictionary,
|
||||
pos,
|
||||
text,
|
||||
})
|
||||
}
|
||||
}
|
||||
#[derive(Serialize, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub enum PartOfSpeech {
|
||||
Noun,
|
||||
Adjective,
|
||||
Verb,
|
||||
Adverb,
|
||||
Particle,
|
||||
Phrase,
|
||||
}
|
||||
|
||||
#[derive(Debug, PartialEq, Eq)]
|
||||
pub struct ParsePartOfSpeechError;
|
||||
|
||||
impl FromStr for PartOfSpeech {
|
||||
type Err = ParsePartOfSpeechError;
|
||||
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
||||
use PartOfSpeech::*;
|
||||
Ok(match s {
|
||||
"noun" => Noun,
|
||||
"adjective" => Adjective,
|
||||
"verb" => Verb,
|
||||
"adverb" => Adverb,
|
||||
"particle" => Particle,
|
||||
"phrase" => Phrase,
|
||||
_ => return Err(ParsePartOfSpeechError),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize)]
|
||||
pub struct Furigana {
|
||||
pub kanji: String,
|
||||
pub kyujitai: Option<String>,
|
||||
pub furigana: Option<String>,
|
||||
}
|
||||
|
||||
impl Furigana {
|
||||
fn new(kanji: String, furigana: Option<String>) -> Self {
|
||||
Self {
|
||||
kyujitai: match kanji.to_kyujitai() {
|
||||
kyujitai if kyujitai.eq(&kanji) => None,
|
||||
kyujitai => Some(kyujitai),
|
||||
},
|
||||
kanji,
|
||||
furigana,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize)]
|
||||
pub struct Translation {
|
||||
pub author: Option<String>,
|
||||
pub site: TranslationSite,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize)]
|
||||
pub struct TranslationSite {
|
||||
pub name: String,
|
||||
pub link: 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");
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue