Compare commits
11 commits
35d9135e07
...
7109b3435d
Author | SHA1 | Date | |
---|---|---|---|
7109b3435d | |||
6ff3db09c5 | |||
0040dcf6df | |||
b2a9ec9d45 | |||
d03095dbc3 | |||
c202e4e250 | |||
5e848ffe76 | |||
6f0a4d1316 | |||
24512469e9 | |||
2689f9ac0d | |||
add2885c26 |
26 changed files with 2101 additions and 1603 deletions
3274
Cargo.lock
generated
3274
Cargo.lock
generated
File diff suppressed because it is too large
Load diff
52
flake.lock
generated
52
flake.lock
generated
|
@ -1,30 +1,12 @@
|
|||
{
|
||||
"nodes": {
|
||||
"flake-utils": {
|
||||
"inputs": {
|
||||
"systems": "systems"
|
||||
},
|
||||
"locked": {
|
||||
"lastModified": 1681202837,
|
||||
"narHash": "sha256-H+Rh19JDwRtpVPAWp64F+rlEtxUWBAQW28eAi3SRSzg=",
|
||||
"owner": "numtide",
|
||||
"repo": "flake-utils",
|
||||
"rev": "cfacdce06f30d2b68473a46042957675eebb3401",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "numtide",
|
||||
"repo": "flake-utils",
|
||||
"type": "github"
|
||||
}
|
||||
},
|
||||
"nixpkgs": {
|
||||
"locked": {
|
||||
"lastModified": 1693377291,
|
||||
"narHash": "sha256-vYGY9bnqEeIncNarDZYhm6KdLKgXMS+HA2mTRaWEc80=",
|
||||
"lastModified": 1742889210,
|
||||
"narHash": "sha256-hw63HnwnqU3ZQfsMclLhMvOezpM7RSB0dMAtD5/sOiw=",
|
||||
"owner": "NixOS",
|
||||
"repo": "nixpkgs",
|
||||
"rev": "e7f38be3775bab9659575f192ece011c033655f0",
|
||||
"rev": "698214a32beb4f4c8e3942372c694f40848b360d",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
|
@ -36,11 +18,11 @@
|
|||
},
|
||||
"nixpkgs_2": {
|
||||
"locked": {
|
||||
"lastModified": 1681358109,
|
||||
"narHash": "sha256-eKyxW4OohHQx9Urxi7TQlFBTDWII+F+x2hklDOQPB50=",
|
||||
"lastModified": 1736320768,
|
||||
"narHash": "sha256-nIYdTAiKIGnFNugbomgBJR+Xv5F1ZQU+HfaBqJKroC0=",
|
||||
"owner": "NixOS",
|
||||
"repo": "nixpkgs",
|
||||
"rev": "96ba1c52e54e74c3197f4d43026b3f3d92e83ff9",
|
||||
"rev": "4bc9c909d9ac828a039f288cf872d16d38185db8",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
|
@ -58,15 +40,14 @@
|
|||
},
|
||||
"rust-overlay": {
|
||||
"inputs": {
|
||||
"flake-utils": "flake-utils",
|
||||
"nixpkgs": "nixpkgs_2"
|
||||
},
|
||||
"locked": {
|
||||
"lastModified": 1693447852,
|
||||
"narHash": "sha256-K9npbs4S6+r51vpiElJi+0vwbAeftCAcOGbot/PCBnQ=",
|
||||
"lastModified": 1743042789,
|
||||
"narHash": "sha256-yPlxN0r3pQjUIwyX/qeWSTdpHjWy/AfmM0PK1bYkO18=",
|
||||
"owner": "oxalica",
|
||||
"repo": "rust-overlay",
|
||||
"rev": "40e851593ef4f9f8cd0b69c8cae7b722b9953a23",
|
||||
"rev": "b4d2dee9d16e7725b71969f28862ded3a94a7934",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
|
@ -74,21 +55,6 @@
|
|||
"repo": "rust-overlay",
|
||||
"type": "github"
|
||||
}
|
||||
},
|
||||
"systems": {
|
||||
"locked": {
|
||||
"lastModified": 1681028828,
|
||||
"narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=",
|
||||
"owner": "nix-systems",
|
||||
"repo": "default",
|
||||
"rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "nix-systems",
|
||||
"repo": "default",
|
||||
"type": "github"
|
||||
}
|
||||
}
|
||||
},
|
||||
"root": "root",
|
||||
|
|
|
@ -6,24 +6,24 @@ edition = "2021"
|
|||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||
|
||||
[dependencies]
|
||||
accept-language = "2.0.0"
|
||||
chrono = { version = "0.4.28", features = ["serde"] }
|
||||
clap = "4.4.2"
|
||||
comrak = "0.18.0"
|
||||
derive_more = "0.99.17"
|
||||
dotenv = "0.15.0"
|
||||
gettext = "0.4.0"
|
||||
gh-emoji = "1.0.7"
|
||||
poise = "0.5.5"
|
||||
r2d2 = "0.8.10"
|
||||
r2d2_sqlite = "0.22.0"
|
||||
regex = "1.9.4"
|
||||
reqwest = "0.11.20"
|
||||
rocket = { version = "=0.5.0-rc.3", features = ["secrets", "json"] }
|
||||
rocket_contrib = { version = "0.4.11", features = ["templates"] }
|
||||
rocket_dyn_templates = { version = "0.1.0-rc.3", features = ["tera"] }
|
||||
rusqlite = { version = "0.29.0", features = ["chrono"] }
|
||||
serde = "1.0.188"
|
||||
serde_json = "1.0.105"
|
||||
serde_yaml = "0.9.25"
|
||||
tokio = { version = "1.32.0", features = ["macros", "rt-multi-thread"] }
|
||||
accept-language = "3.1"
|
||||
chrono = { version = "0.4", features = ["serde"] }
|
||||
clap = "4.5"
|
||||
comrak = "0.37"
|
||||
derive_more = { version = "2.0", features = ["full"] }
|
||||
dotenv = "0.15"
|
||||
gettext = "0.4"
|
||||
gh-emoji = "1.0"
|
||||
lazy_static = "1.5.0"
|
||||
poise = "0.5" # TODO: update to 0.6
|
||||
r2d2 = "0.8"
|
||||
r2d2_sqlite = "0.27"
|
||||
regex = "1.11"
|
||||
reqwest = "0.12"
|
||||
rocket = { version = "0.5", features = ["secrets", "json"] }
|
||||
rocket_dyn_templates = { version = "0.2", features = ["tera"] }
|
||||
rusqlite = { version = "0.34", features = ["chrono"] }
|
||||
serde = "1.0"
|
||||
serde_json = "1.0"
|
||||
serde_yaml = "0.9.34" # deprecated
|
||||
tokio = { version = "1.44", features = ["macros", "rt-multi-thread"] }
|
||||
|
|
|
@ -11,6 +11,7 @@ use std::{
|
|||
use gettext::Catalog;
|
||||
|
||||
#[derive(From, Debug)]
|
||||
#[allow(dead_code)]
|
||||
pub enum LoadCatalogsError {
|
||||
Io(std::io::Error),
|
||||
Parse(gettext::Error),
|
||||
|
|
|
@ -4,11 +4,12 @@ extern crate rocket;
|
|||
use clap::{Parser, Subcommand};
|
||||
use poise::serenity_prelude::Http;
|
||||
use rocket::{
|
||||
fs::{relative, FileServer},
|
||||
Ignite, Rocket,
|
||||
fs::{relative, FileServer}, Ignite, Rocket
|
||||
};
|
||||
use rocket_dyn_templates::{tera, Template};
|
||||
use std::{collections::HashMap, env};
|
||||
use tokio::time::sleep;
|
||||
use std::{collections::HashMap, env, time::Duration};
|
||||
use lazy_static::lazy_static;
|
||||
|
||||
mod models;
|
||||
use models::{Database, Settings};
|
||||
|
@ -61,6 +62,15 @@ async fn main() {
|
|||
}
|
||||
},
|
||||
None => {
|
||||
tokio::task::spawn(async {
|
||||
const INTERVAL: Duration = Duration::from_millis(60 * 60 * 1000); // every hour
|
||||
loop {
|
||||
sleep(INTERVAL).await;
|
||||
if let Err(err) = DATABASE.refresh_users(&HTTP).await {
|
||||
error!("{:?}", err);
|
||||
}
|
||||
}
|
||||
});
|
||||
rocket().await.expect("Failed to launch rocket");
|
||||
}
|
||||
}
|
||||
|
@ -75,9 +85,12 @@ fn http() -> Http {
|
|||
Http::new(&token)
|
||||
}
|
||||
|
||||
async fn rocket() -> Result<Rocket<Ignite>, rocket::Error> {
|
||||
let http = http();
|
||||
lazy_static! {
|
||||
static ref DATABASE: Database = load_database();
|
||||
static ref HTTP: Http = http();
|
||||
}
|
||||
|
||||
async fn rocket() -> Result<Rocket<Ignite>, rocket::Error> {
|
||||
let config = rocket::Config::figment().merge(("port", 1313)).merge((
|
||||
"secret_key",
|
||||
env::var("SECRET")
|
||||
|
@ -85,9 +98,8 @@ async fn rocket() -> Result<Rocket<Ignite>, rocket::Error> {
|
|||
));
|
||||
|
||||
rocket::custom(config)
|
||||
.manage(Settings::new(&http).await.unwrap())
|
||||
.manage(http)
|
||||
.manage(load_database())
|
||||
.manage(Settings::new(&HTTP).await.unwrap())
|
||||
// .manage(http)
|
||||
.mount(
|
||||
"/",
|
||||
routes![
|
||||
|
|
|
@ -2,7 +2,7 @@ use std::collections::HashSet;
|
|||
use std::{collections::HashMap, fs::File, io::Read, path::Path};
|
||||
|
||||
use derive_more::From;
|
||||
use poise::serenity_prelude::{Http, SerenityError};
|
||||
use poise::serenity_prelude::{Http, SerenityError, StatusCode};
|
||||
use r2d2::{Pool, PooledConnection};
|
||||
use r2d2_sqlite::rusqlite::{self, params};
|
||||
use r2d2_sqlite::SqliteConnectionManager;
|
||||
|
@ -20,13 +20,15 @@ pub struct Database {
|
|||
const DATABASE_FILENAME: &str = "database.db";
|
||||
|
||||
#[derive(From, Debug)]
|
||||
#[allow(dead_code)]
|
||||
pub enum DatabaseError {
|
||||
Rusqlite(rusqlite::Error),
|
||||
Pool(r2d2::Error),
|
||||
}
|
||||
|
||||
#[derive(From, Debug)]
|
||||
pub enum LoadLegacyError {
|
||||
#[allow(dead_code)]
|
||||
pub enum GenericError {
|
||||
Database(DatabaseError),
|
||||
Serenity(SerenityError),
|
||||
}
|
||||
|
@ -82,7 +84,7 @@ impl Database {
|
|||
.is_ok())
|
||||
}
|
||||
|
||||
pub async fn load_legacy(&self, http: &Http) -> std::result::Result<(), LoadLegacyError> {
|
||||
pub async fn load_legacy(&self, http: &Http) -> std::result::Result<(), GenericError> {
|
||||
let latest_challenge = get_challenge_number();
|
||||
// HashMap of archived users that are no longer sharing a server with 字ちゃん
|
||||
// Their historical usernames and discriminators will be used
|
||||
|
@ -124,12 +126,7 @@ impl Database {
|
|||
},
|
||||
);
|
||||
}
|
||||
Ok(user) => {
|
||||
conn.execute(
|
||||
"INSERT INTO User(id, name, discriminator, avatar, deleted) VALUES (?1, ?2, ?3, ?4, ?5)",
|
||||
params![user.id, user.name, user.discriminator, user.avatar, user.deleted]
|
||||
).map_err(DatabaseError::Rusqlite)?;
|
||||
}
|
||||
Ok(user) => self.insert_user(&user)?,
|
||||
Err(error) if error.to_string().eq("Unknown User") => {
|
||||
// This will also be called in the case of an invalid user ID
|
||||
println!("Failed to fetch user {id}, adding to archive");
|
||||
|
@ -143,7 +140,7 @@ impl Database {
|
|||
},
|
||||
);
|
||||
}
|
||||
Err(error) => return Err(LoadLegacyError::Serenity(error)),
|
||||
Err(error) => return Err(GenericError::Serenity(error)),
|
||||
},
|
||||
};
|
||||
}
|
||||
|
@ -265,11 +262,53 @@ impl Database {
|
|||
.collect()
|
||||
}
|
||||
|
||||
fn get_all_users(&self) -> Result<Vec<User>> {
|
||||
let conn = self.conn()?;
|
||||
let mut stmt = conn.prepare("SELECT id, name, discriminator, avatar, deleted FROM User")?;
|
||||
let result: Vec<Result<User>> = stmt.query_map([], |row| Ok(User {
|
||||
id: row.get(0)?,
|
||||
name: row.get(1)?,
|
||||
discriminator: row.get(2)?,
|
||||
avatar: row.get(3)?,
|
||||
deleted: row.get(4)?,
|
||||
}))?.map(|result| match result {
|
||||
Ok(user) => Ok(user),
|
||||
Err(err) => Err(DatabaseError::Rusqlite(err)),
|
||||
}).collect();
|
||||
result.into_iter().collect()
|
||||
}
|
||||
|
||||
pub fn insert_user(&self, user: &User) -> Result<()> {
|
||||
self.conn()?.execute(
|
||||
"INSERT OR REPLACE INTO User(id, name, discriminator, avatar, deleted) VALUES (?1, ?2, ?3, ?4, ?5)",
|
||||
params![user.id, user.name, user.discriminator, user.avatar, user.deleted]
|
||||
)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
pub fn refresh_users(&self) -> rusqlite::Result<()> {
|
||||
pub async fn refresh_users(&self, http: &Http) -> std::result::Result<(), GenericError> {
|
||||
// Periodically refresh all changable user data (name, discriminator, avatar)
|
||||
// Ideally this should run periodically.
|
||||
todo!()
|
||||
let all_users = self.get_all_users()?;
|
||||
for (i, user) in all_users.iter().enumerate() {
|
||||
print!("User {i}/{}, ", all_users.len());
|
||||
let updated = match User::fetch(http, user.id).await {
|
||||
Ok(user) => user,
|
||||
Err(SerenityError::Http(error)) if error.status_code().eq(&Some(StatusCode::NOT_FOUND)) => {
|
||||
println!("not found");
|
||||
continue;
|
||||
},
|
||||
Err(error) => panic!("{error}"),
|
||||
};
|
||||
if user.eq(&updated) {
|
||||
println!("no change");
|
||||
continue;
|
||||
}
|
||||
println!("updated");
|
||||
self.insert_user(&updated)?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[allow(dead_code, unused_variables)]
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
mod user;
|
||||
pub use user::{SessionUser, User, Username};
|
||||
pub use user::{SessionUser, User};
|
||||
|
||||
mod challenge;
|
||||
pub use challenge::Challenge;
|
||||
|
|
|
@ -26,6 +26,7 @@ impl Settings {
|
|||
}
|
||||
|
||||
#[derive(From, Debug)]
|
||||
#[allow(dead_code)]
|
||||
pub enum GetSettingsError {
|
||||
Io(std::io::Error),
|
||||
Deserialize(serde_yaml::Error),
|
||||
|
|
|
@ -21,6 +21,7 @@ where
|
|||
}
|
||||
|
||||
#[derive(From, Debug)]
|
||||
#[allow(dead_code)]
|
||||
pub enum LegacySubmissionParseError {
|
||||
BadAuthorId(std::num::ParseIntError),
|
||||
}
|
||||
|
@ -45,7 +46,7 @@ impl LegacySubmission {
|
|||
.next()?
|
||||
// Get last number
|
||||
.split('-')
|
||||
.last()?
|
||||
.next_back()?
|
||||
.parse()
|
||||
.ok()
|
||||
// Check if discriminator or timestamp, then convert
|
||||
|
|
|
@ -19,7 +19,7 @@ pub trait Username {
|
|||
fn username(&self) -> String;
|
||||
}
|
||||
|
||||
#[derive(Default, Deserialize, Debug)]
|
||||
#[derive(Default, Deserialize, Debug, PartialEq, Eq)]
|
||||
pub struct User {
|
||||
#[serde(deserialize_with = "deserialize_id")]
|
||||
pub id: u64,
|
||||
|
@ -92,11 +92,16 @@ impl User {
|
|||
}
|
||||
),
|
||||
// Archived user or user with no avatar, calculate default avatar
|
||||
// https://www.reddit.com/r/discordapp/comments/au6v4e/comment/eh61dm6/
|
||||
// https://docs.rs/serenity/0.11.5/serenity/model/user/struct.User.html#method.default_avatar_url
|
||||
None => format!(
|
||||
"https://cdn.discordapp.com/embed/avatars/{}.png",
|
||||
self.discriminator % 5
|
||||
// https://docs.rs/serenity/0.12.4/src/serenity/model/user.rs.html#805
|
||||
if self.discriminator == 0 {
|
||||
// New avatar system
|
||||
((self.id >> 22) % 6) as u16
|
||||
} else {
|
||||
// Old avatar system
|
||||
self.discriminator % 5
|
||||
}
|
||||
),
|
||||
}
|
||||
}
|
||||
|
@ -142,12 +147,12 @@ impl SessionUser {
|
|||
}
|
||||
|
||||
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));
|
||||
cookies.remove_private(Cookie::from(TOKEN_COOKIE));
|
||||
cookies.remove_private(Cookie::from(USER_ID_COOKIE));
|
||||
cookies.remove_private(Cookie::from(USER_NAME_COOKIE));
|
||||
cookies.remove_private(Cookie::from(USER_DISCRIMINATOR_COOKIE));
|
||||
cookies.remove_private(Cookie::from(USER_AVATAR_COOKIE));
|
||||
cookies.remove(Cookie::from(TOKEN_EXPIRE_COOKIE));
|
||||
}
|
||||
|
||||
fn from_cookies(cookies: &CookieJar<'_>) -> Option<Self> {
|
||||
|
@ -169,14 +174,14 @@ impl SessionUser {
|
|||
.get(TOKEN_EXPIRE_COOKIE)
|
||||
.map(|expire| expire.value().parse::<i64>())
|
||||
.and_then(Result::ok)
|
||||
.map_or(true, |timestamp| Utc::now().timestamp() >= timestamp)
|
||||
.is_none_or(|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));
|
||||
cookies.remove_private(Cookie::from(TOKEN_COOKIE));
|
||||
cookies.remove_private(Cookie::from(USER_ID_COOKIE));
|
||||
cookies.remove_private(Cookie::from(USER_NAME_COOKIE));
|
||||
cookies.remove_private(Cookie::from(USER_DISCRIMINATOR_COOKIE));
|
||||
cookies.remove_private(Cookie::from(USER_AVATAR_COOKIE));
|
||||
cookies.remove(Cookie::from(TOKEN_EXPIRE_COOKIE));
|
||||
return Ok(None);
|
||||
}
|
||||
Ok(Some(user))
|
||||
|
@ -184,6 +189,7 @@ impl SessionUser {
|
|||
}
|
||||
|
||||
#[derive(From, Debug)]
|
||||
#[allow(dead_code)]
|
||||
pub enum GetUserError {
|
||||
Reqwest(reqwest::Error),
|
||||
Deserialize(serde_json::Error),
|
||||
|
|
|
@ -6,8 +6,8 @@ use rocket_dyn_templates::{context, Template};
|
|||
use crate::{
|
||||
cookies::LANG_COOKIE,
|
||||
i18n::DEFAULT as DEFAULT_LANG,
|
||||
models::{Challenge, Database, SessionUser, Settings},
|
||||
utils::AcceptLanguage,
|
||||
models::{Challenge, SessionUser, Settings},
|
||||
utils::AcceptLanguage, DATABASE,
|
||||
};
|
||||
|
||||
#[get("/<challenge>")]
|
||||
|
@ -15,10 +15,9 @@ pub async fn get_challenge(
|
|||
challenge: u32,
|
||||
cookies: &CookieJar<'_>,
|
||||
settings: &State<Settings>,
|
||||
database: &State<Database>,
|
||||
accept_language: AcceptLanguage,
|
||||
) -> Template {
|
||||
let (submissions, users) = database.get_challenge_user_data(challenge).unwrap();
|
||||
let (submissions, users) = DATABASE.get_challenge_user_data(challenge).unwrap();
|
||||
Template::render(
|
||||
"challenge",
|
||||
context! {
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
use poise::serenity_prelude::Http;
|
||||
use rocket::response::stream::{Event, EventStream};
|
||||
use rocket::{http::CookieJar, State};
|
||||
|
||||
use crate::HTTP;
|
||||
use crate::{cookies::user::USER_ID_COOKIE, models::Settings};
|
||||
|
||||
// TODO: Incrementally send guilds
|
||||
|
@ -9,7 +9,6 @@ use crate::{cookies::user::USER_ID_COOKIE, models::Settings};
|
|||
pub async fn get_guilds<'a>(
|
||||
cookies: &'a CookieJar<'_>,
|
||||
settings: &'a State<Settings>,
|
||||
http: &'a State<Http>,
|
||||
) -> EventStream![Event + 'a] {
|
||||
// EventStream![] is shorthand for EventStream[Event],
|
||||
// but we need to pass in a lifetime parameter.
|
||||
|
@ -28,7 +27,7 @@ pub async fn get_guilds<'a>(
|
|||
if guild.hidden {
|
||||
continue;
|
||||
}
|
||||
yield Event::data(format!("{},{}", guild.id, http.get_member(guild.id, user_id).await.is_ok()));
|
||||
yield Event::data(format!("{},{}", guild.id, HTTP.get_member(guild.id, user_id).await.is_ok()));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -9,8 +9,8 @@ use rocket_dyn_templates::{context, Template};
|
|||
use crate::{
|
||||
cookies::LANG_COOKIE,
|
||||
i18n::DEFAULT as DEFAULT_LANG,
|
||||
models::{Database, DatabaseError, SessionUser, Settings},
|
||||
utils::AcceptLanguage,
|
||||
models::{DatabaseError, SessionUser, Settings},
|
||||
utils::AcceptLanguage, DATABASE,
|
||||
};
|
||||
|
||||
#[get("/users/<user>")]
|
||||
|
@ -18,10 +18,9 @@ pub async fn get_user(
|
|||
user: String,
|
||||
cookies: &CookieJar<'_>,
|
||||
settings: &State<Settings>,
|
||||
database: &State<Database>,
|
||||
accept_language: AcceptLanguage,
|
||||
) -> Result<Template, Status> {
|
||||
let profile_user = match database.get_user_by_name(&user) {
|
||||
let profile_user = match DATABASE.get_user_by_name(&user) {
|
||||
Ok(profile_user) => profile_user,
|
||||
Err(DatabaseError::Rusqlite(rusqlite::Error::QueryReturnedNoRows)) => {
|
||||
return Err(Status::NotFound)
|
||||
|
@ -35,7 +34,7 @@ pub async fn get_user(
|
|||
"user",
|
||||
context! {
|
||||
profile_user,
|
||||
submissions: database.get_submissions_by_user_name(&user).unwrap(),
|
||||
submissions: DATABASE.get_submissions_by_user_name(&user).unwrap(),
|
||||
settings: settings.deref(),
|
||||
lang: cookies
|
||||
.get(LANG_COOKIE)
|
||||
|
|
|
@ -29,7 +29,7 @@ pub async fn post_login(login: Form<Login<'_>>, cookies: &CookieJar<'_>) -> Redi
|
|||
.timestamp()
|
||||
.to_string(),
|
||||
));
|
||||
cookies.remove(Cookie::named(WELCOMED_COOKIE));
|
||||
cookies.remove(Cookie::from(WELCOMED_COOKIE));
|
||||
}
|
||||
Redirect::to("/")
|
||||
}
|
||||
|
|
|
@ -1,20 +1,19 @@
|
|||
use std::ops::Deref;
|
||||
|
||||
use poise::serenity_prelude::Http;
|
||||
use rocket::{http::CookieJar, State};
|
||||
use rocket::http::CookieJar;
|
||||
|
||||
use crate::models::SessionUser;
|
||||
use crate::{models::SessionUser, HTTP};
|
||||
|
||||
#[get("/testing")]
|
||||
pub async fn testing(cookies: &CookieJar<'_>, http: &State<Http>) -> String {
|
||||
pub async fn testing(cookies: &CookieJar<'_>) -> String {
|
||||
// Get logged in user's join date in 字ちゃん server
|
||||
format!(
|
||||
"{:?}",
|
||||
http.get_guild(814700630958276649)
|
||||
HTTP.get_guild(814700630958276649)
|
||||
.await
|
||||
.expect("Failed to get testing guild")
|
||||
.member(
|
||||
http.deref(),
|
||||
HTTP.deref(),
|
||||
SessionUser::get(cookies)
|
||||
.await
|
||||
.expect("Failed to get logged in user data")
|
||||
|
|
|
@ -11,6 +11,10 @@ pub fn furigana_to_html(text: &str) -> String {
|
|||
// so { needs to be escaped as \{
|
||||
// Curly brace literals \{ need to have their backslash escaped as \\{
|
||||
// TODO: Modify so <span lang="ja"> only wraps continuous sections of furigana
|
||||
if text.trim().is_empty() {
|
||||
// Don't return empty <span lang="ja"> if text is empty
|
||||
return String::new();
|
||||
}
|
||||
let re = Regex::new(r"\[([^\]]*)\]\{([^\\}]*)}").unwrap();
|
||||
format!(
|
||||
"<span lang=\"ja\">{}</span>",
|
||||
|
|
|
@ -353,6 +353,7 @@ const KYUJITAI: &[(char, char)] = &[
|
|||
|
||||
pub trait Kyujitai {
|
||||
fn to_kyujitai(&self) -> String;
|
||||
#[allow(dead_code)]
|
||||
fn to_shinjitai(&self) -> String;
|
||||
}
|
||||
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
@use "sass:color"
|
||||
|
||||
$bg: #FFFDF3
|
||||
$bg0: #{color.adjust($bg, $lightness: -5%, $hue: -7deg, $saturation: -50%)}
|
||||
$fg: #011627
|
||||
$bg: #fefdfa
|
||||
$bg0: #fefefa
|
||||
$fg: #1a1110
|
|
@ -1,6 +1,38 @@
|
|||
#challenge
|
||||
padding: 1em
|
||||
@use "_theme" as *
|
||||
|
||||
$wave: 10px
|
||||
$wave_scale: 2
|
||||
|
||||
#challenge
|
||||
box-shadow: 0px 0px 10px 5px rgba(0, 0, 0, 0.125)
|
||||
z-index: 1
|
||||
background: #3f313a
|
||||
|
||||
iframe
|
||||
background: black
|
||||
width: 100%
|
||||
aspect-ratio: 16/9
|
||||
margin: 0
|
||||
|
||||
#challenge-description, footer
|
||||
padding: 1em
|
||||
|
||||
#challenge-content
|
||||
padding: 1em
|
||||
padding-top: calc(1em - $wave)
|
||||
padding-bottom: calc(1em - $wave)
|
||||
font-size: 1.25em
|
||||
$shadow: #1a1110
|
||||
$shadow-size: 1px
|
||||
text-shadow: calc(-1 * $shadow-size) calc(-1 * $shadow-size) 0 $shadow, calc(-1 * $shadow-size) 0px 0 $shadow, calc(-1 * $shadow-size) $shadow-size 0 $shadow, 0px $shadow-size 0 $shadow, $shadow-size $shadow-size 0 $shadow, $shadow-size 0px 0 $shadow, $shadow-size calc(-1 * $shadow-size) 0 $shadow, 0px calc(-1 * $shadow-size) 0 $shadow;
|
||||
background: #23191e
|
||||
|
||||
p
|
||||
margin: 0
|
||||
|
||||
a:hover
|
||||
text-decoration: none
|
||||
border-bottom: 3px solid
|
||||
a
|
||||
&.noun
|
||||
color: #a6e3a1
|
||||
|
@ -18,4 +50,22 @@ a
|
|||
color: #f9e2af
|
||||
|
||||
&.phrase
|
||||
color: #fab387
|
||||
color: #fab387
|
||||
|
||||
.wave
|
||||
--size: #{$wave}
|
||||
--R: calc(var(--size)*1.28)
|
||||
|
||||
mask: radial-gradient(var(--R) at 50% calc(1.8*var(--size)),#000 99%,#0000 101%) calc(50% - 2*var(--size)) 0/calc(4*var(--size)) 100%, radial-gradient(var(--R) at 50% calc(-.8*var(--size)),#0000 99%,#000 101%) 50% var(--size)/calc(4*var(--size)) 100% repeat-x
|
||||
background: #23191e
|
||||
$height: calc($wave * 2)
|
||||
height: $height
|
||||
flex-shrink: 0
|
||||
|
||||
&.wave-bottom
|
||||
transform: scaleY(-1) scaleX($wave_scale)
|
||||
|
||||
&.wave-top
|
||||
transform: scaleX($wave_scale) translateX($wave * $wave_scale)
|
||||
margin-top: -$height
|
||||
pointer-events: none
|
|
@ -12,10 +12,13 @@ body
|
|||
height: 100vh
|
||||
box-sizing: border-box
|
||||
padding: 2em
|
||||
background: $bg0
|
||||
color: var(--fg)
|
||||
background: #2b2028
|
||||
color: var(--bg)
|
||||
font-size: 1.25em
|
||||
|
||||
b, strong
|
||||
color: white
|
||||
|
||||
input[type=number]
|
||||
background: var(--bg)
|
||||
color: var(--fg)
|
||||
|
@ -27,6 +30,7 @@ input[type=number]
|
|||
appearance: textfield
|
||||
|
||||
a
|
||||
color: #9fe2bf
|
||||
text-decoration: none
|
||||
|
||||
&:hover
|
||||
|
@ -38,5 +42,30 @@ a
|
|||
h1
|
||||
text-align: center
|
||||
|
||||
&:first-child
|
||||
margin-top: 0
|
||||
:first-child
|
||||
margin-top: 0
|
||||
|
||||
margin-bottom: 0
|
||||
|
||||
.avatar
|
||||
border-radius: 100%
|
||||
display: inline
|
||||
height: 2em
|
||||
vertical-align: middle
|
||||
background: #5e595d
|
||||
|
||||
button
|
||||
border: none
|
||||
color: inherit
|
||||
font: inherit
|
||||
background: #3f313a
|
||||
border: 3px solid #1a1110
|
||||
border-radius: 0.25em
|
||||
box-shadow: 0px 2px #1a1110
|
||||
cursor: pointer
|
||||
|
||||
&:active
|
||||
box-shadow: none
|
||||
transform: translateY(2px)
|
||||
background: transparent
|
||||
color: #3f313a
|
|
@ -5,8 +5,10 @@ nav
|
|||
--fg: #{$bg}
|
||||
background: var(--bg)
|
||||
color: var(--fg)
|
||||
box-shadow: 0 0 8px 2px rgba(0, 0, 0, 0.125)
|
||||
box-shadow: 0 0 8px 2px rgba(0, 0, 0, 0.5)
|
||||
display: flex
|
||||
align-items: center
|
||||
z-index: 2
|
||||
|
||||
.dropdown
|
||||
position: relative
|
||||
|
@ -51,5 +53,8 @@ nav
|
|||
cursor: pointer
|
||||
|
||||
.icon
|
||||
height: 1.5em
|
||||
vertical-align: middle
|
||||
height: 1em
|
||||
vertical-align: middle
|
||||
|
||||
nav .icon
|
||||
height: 1.5em
|
|
@ -10,7 +10,6 @@
|
|||
background: var(--bg)
|
||||
box-shadow: 0 0 16px rgba(0, 0, 0, 0.125)
|
||||
border: none
|
||||
border-radius: 4px
|
||||
|
||||
#content
|
||||
width: 100%
|
||||
|
@ -19,7 +18,7 @@
|
|||
grid-template-areas: "nav nav" "challenge submissions"
|
||||
grid-template-columns: 30em auto
|
||||
grid-template-rows: min-content auto
|
||||
overflow: hidden
|
||||
border: 4px solid $fg
|
||||
|
||||
#nav
|
||||
grid-area: nav
|
||||
|
@ -31,10 +30,10 @@
|
|||
grid-area: submissions
|
||||
|
||||
#challenge, #submissions
|
||||
overflow: scroll
|
||||
overflow-x: hidden;
|
||||
overflow-y: scroll
|
||||
display: flex
|
||||
flex-direction: column
|
||||
gap: 1em
|
||||
|
||||
footer
|
||||
margin-top: auto
|
||||
|
|
|
@ -1,6 +1,8 @@
|
|||
@use "_theme" as *
|
||||
|
||||
#submissions
|
||||
background: #3f313a
|
||||
grid-area: submissions
|
||||
background: rgba(0, 0, 0, 0.125)
|
||||
padding: 1em
|
||||
|
||||
& > div
|
||||
|
@ -13,7 +15,9 @@
|
|||
gap: $gap
|
||||
|
||||
& > figure
|
||||
background: rgba(0, 0, 0, 0.125)
|
||||
background: #5c4f57
|
||||
box-shadow: 0 0 8px 2px rgba(0, 0, 0, 0.125)
|
||||
border-radius: 0.5em
|
||||
padding: $gap
|
||||
width: 100%
|
||||
box-sizing: border-box
|
||||
|
@ -23,6 +27,7 @@
|
|||
text-align: center
|
||||
|
||||
& > img
|
||||
border-radius: 0.5em
|
||||
width: 100%
|
||||
cursor: pointer
|
||||
|
||||
|
|
|
@ -16,7 +16,7 @@
|
|||
<div id="content">
|
||||
<nav id="nav">{% block nav %}{% include "nav" %}{% endblock %}</nav>
|
||||
<div id="challenge">
|
||||
<div>{% block content %}{% endblock %}</div>
|
||||
{% block content %}{% endblock %}
|
||||
<footer>
|
||||
{% block content_copyright %}
|
||||
Copyright {% include "copyright-years" %} Tegaki Tuesday. All rights reserved. 字ちゃん mascot art by <a href="https://twitter.com/bellumela" target="_blank">@bellumela</a>.
|
||||
|
@ -55,7 +55,11 @@
|
|||
{% if profile_user %}
|
||||
<a href="/{{ submission.challenge }}">#{{ submission.challenge }}</a>
|
||||
{% else %}
|
||||
{% if not author.deleted %}<a href="/users/{{ author.name }}" target="_blank">{% endif %}{{ author.username }}{% if author.deleted %} (deleted account){% else %}</a>{% endif %}
|
||||
{% if not author.deleted %}
|
||||
<a href="/users/{{ author.name }}" target="_blank">
|
||||
{% endif %}
|
||||
<img src="{{ author.avatar }}" alt="{{ author.username }}'s avatar" class="avatar"> {{ author.username }}
|
||||
{% if author.deleted %} (deleted account){% else %}</a>{% endif %}
|
||||
{% endif %}
|
||||
</figcaption>
|
||||
</figure>
|
||||
|
|
|
@ -1,7 +1,11 @@
|
|||
{% extends "base" %}
|
||||
|
||||
{% block content %}
|
||||
<h1>Welcome to Tegaki Tuesday #{{ challenge }}!</h1>
|
||||
{% if content.youtube %}
|
||||
<iframe width="560" height="315" src="https://www.youtube.com/embed/{{ content.youtube }}" title="YouTube video player" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share" referrerpolicy="strict-origin-when-cross-origin" allowfullscreen></iframe>
|
||||
{% endif %}
|
||||
<div class="wave wave-top"></div>
|
||||
<div id="challenge-content">
|
||||
{% if content.japanese %}
|
||||
<div lang="ja">
|
||||
<script>
|
||||
|
@ -15,7 +19,19 @@
|
|||
kyujitai = !kyujitai;
|
||||
}
|
||||
</script>
|
||||
<a href="https://en.wikipedia.org/wiki/Ky%C5%ABjitai" target="_blank">
|
||||
<svg class="icon" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor" class="size-6">
|
||||
<path fill-rule="evenodd" d="M2.25 12c0-5.385 4.365-9.75 9.75-9.75s9.75 4.365 9.75 9.75-4.365 9.75-9.75 9.75S2.25 17.385 2.25 12Zm11.378-3.917c-.89-.777-2.366-.777-3.255 0a.75.75 0 0 1-.988-1.129c1.454-1.272 3.776-1.272 5.23 0 1.513 1.324 1.513 3.518 0 4.842a3.75 3.75 0 0 1-.837.552c-.676.328-1.028.774-1.028 1.152v.75a.75.75 0 0 1-1.5 0v-.75c0-1.279 1.06-2.107 1.875-2.502.182-.088.351-.199.503-.331.83-.727.83-1.857 0-2.584ZM12 18a.75.75 0 1 0 0-1.5.75.75 0 0 0 0 1.5Z" clip-rule="evenodd" />
|
||||
</svg>
|
||||
</a>
|
||||
<button onclick="kyujitaiToggle(); this.innerHTML = kyujitai ? '新字体' : '旧字体'">旧字体</button>
|
||||
<button>
|
||||
<svg class="icon" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="size-6">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="M19.5 12c0-1.232-.046-2.453-.138-3.662a4.006 4.006 0 0 0-3.7-3.7 48.678 48.678 0 0 0-7.324 0 4.006 4.006 0 0 0-3.7 3.7c-.017.22-.032.441-.046.662M19.5 12l3-3m-3 3-3-3m-12 3c0 1.232.046 2.453.138 3.662a4.006 4.006 0 0 0 3.7 3.7 48.656 48.656 0 0 0 7.324 0 4.006 4.006 0 0 0 3.7-3.7c.017-.22.032-.441.046-.662M4.5 12l3 3m-3-3-3 3" />
|
||||
</svg>
|
||||
</button>
|
||||
<br>
|
||||
<br>
|
||||
{% for line in content.japanese %}
|
||||
{% for subline in line %}
|
||||
<p>
|
||||
|
@ -35,6 +51,9 @@
|
|||
{% endfor %}
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
<div class="wave wave-bottom"></div>
|
||||
<div id="challenge-description">
|
||||
{{ content.text | safe }}
|
||||
{% if content.translation %}
|
||||
<p>
|
||||
|
@ -52,4 +71,5 @@
|
|||
{%- if content.suggester -%}
|
||||
<p><em>This challenge was suggested by <strong>{{ content.suggester }}</strong> using the <code>-h suggest</code> command.</code></em></p>
|
||||
{%- endif -%}
|
||||
</div>
|
||||
{% endblock %}
|
|
@ -2,7 +2,8 @@
|
|||
|
||||
{% block content %}
|
||||
<div style="text-align: center">
|
||||
<img src="{{ profile_user.avatar }}" alt="{{ profile_user.username }}" style="border-radius: 100%">
|
||||
<br>
|
||||
<img src="{{ profile_user.avatar }}" alt="{{ profile_user.username }}" class="avatar" style="font-size: 6em">
|
||||
<h1><a href="https://discord.com/users/{{ profile_user.id }}">{{ profile_user.username }}</a>'s submissions</h1>
|
||||
{{ submissions | length }} images
|
||||
</div>
|
||||
|
|
Loading…
Add table
Reference in a new issue