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": {
|
"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": {
|
"nixpkgs": {
|
||||||
"locked": {
|
"locked": {
|
||||||
"lastModified": 1693377291,
|
"lastModified": 1742889210,
|
||||||
"narHash": "sha256-vYGY9bnqEeIncNarDZYhm6KdLKgXMS+HA2mTRaWEc80=",
|
"narHash": "sha256-hw63HnwnqU3ZQfsMclLhMvOezpM7RSB0dMAtD5/sOiw=",
|
||||||
"owner": "NixOS",
|
"owner": "NixOS",
|
||||||
"repo": "nixpkgs",
|
"repo": "nixpkgs",
|
||||||
"rev": "e7f38be3775bab9659575f192ece011c033655f0",
|
"rev": "698214a32beb4f4c8e3942372c694f40848b360d",
|
||||||
"type": "github"
|
"type": "github"
|
||||||
},
|
},
|
||||||
"original": {
|
"original": {
|
||||||
|
@ -36,11 +18,11 @@
|
||||||
},
|
},
|
||||||
"nixpkgs_2": {
|
"nixpkgs_2": {
|
||||||
"locked": {
|
"locked": {
|
||||||
"lastModified": 1681358109,
|
"lastModified": 1736320768,
|
||||||
"narHash": "sha256-eKyxW4OohHQx9Urxi7TQlFBTDWII+F+x2hklDOQPB50=",
|
"narHash": "sha256-nIYdTAiKIGnFNugbomgBJR+Xv5F1ZQU+HfaBqJKroC0=",
|
||||||
"owner": "NixOS",
|
"owner": "NixOS",
|
||||||
"repo": "nixpkgs",
|
"repo": "nixpkgs",
|
||||||
"rev": "96ba1c52e54e74c3197f4d43026b3f3d92e83ff9",
|
"rev": "4bc9c909d9ac828a039f288cf872d16d38185db8",
|
||||||
"type": "github"
|
"type": "github"
|
||||||
},
|
},
|
||||||
"original": {
|
"original": {
|
||||||
|
@ -58,15 +40,14 @@
|
||||||
},
|
},
|
||||||
"rust-overlay": {
|
"rust-overlay": {
|
||||||
"inputs": {
|
"inputs": {
|
||||||
"flake-utils": "flake-utils",
|
|
||||||
"nixpkgs": "nixpkgs_2"
|
"nixpkgs": "nixpkgs_2"
|
||||||
},
|
},
|
||||||
"locked": {
|
"locked": {
|
||||||
"lastModified": 1693447852,
|
"lastModified": 1743042789,
|
||||||
"narHash": "sha256-K9npbs4S6+r51vpiElJi+0vwbAeftCAcOGbot/PCBnQ=",
|
"narHash": "sha256-yPlxN0r3pQjUIwyX/qeWSTdpHjWy/AfmM0PK1bYkO18=",
|
||||||
"owner": "oxalica",
|
"owner": "oxalica",
|
||||||
"repo": "rust-overlay",
|
"repo": "rust-overlay",
|
||||||
"rev": "40e851593ef4f9f8cd0b69c8cae7b722b9953a23",
|
"rev": "b4d2dee9d16e7725b71969f28862ded3a94a7934",
|
||||||
"type": "github"
|
"type": "github"
|
||||||
},
|
},
|
||||||
"original": {
|
"original": {
|
||||||
|
@ -74,21 +55,6 @@
|
||||||
"repo": "rust-overlay",
|
"repo": "rust-overlay",
|
||||||
"type": "github"
|
"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",
|
"root": "root",
|
||||||
|
|
|
@ -6,24 +6,24 @@ edition = "2021"
|
||||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
accept-language = "2.0.0"
|
accept-language = "3.1"
|
||||||
chrono = { version = "0.4.28", features = ["serde"] }
|
chrono = { version = "0.4", features = ["serde"] }
|
||||||
clap = "4.4.2"
|
clap = "4.5"
|
||||||
comrak = "0.18.0"
|
comrak = "0.37"
|
||||||
derive_more = "0.99.17"
|
derive_more = { version = "2.0", features = ["full"] }
|
||||||
dotenv = "0.15.0"
|
dotenv = "0.15"
|
||||||
gettext = "0.4.0"
|
gettext = "0.4"
|
||||||
gh-emoji = "1.0.7"
|
gh-emoji = "1.0"
|
||||||
poise = "0.5.5"
|
lazy_static = "1.5.0"
|
||||||
r2d2 = "0.8.10"
|
poise = "0.5" # TODO: update to 0.6
|
||||||
r2d2_sqlite = "0.22.0"
|
r2d2 = "0.8"
|
||||||
regex = "1.9.4"
|
r2d2_sqlite = "0.27"
|
||||||
reqwest = "0.11.20"
|
regex = "1.11"
|
||||||
rocket = { version = "=0.5.0-rc.3", features = ["secrets", "json"] }
|
reqwest = "0.12"
|
||||||
rocket_contrib = { version = "0.4.11", features = ["templates"] }
|
rocket = { version = "0.5", features = ["secrets", "json"] }
|
||||||
rocket_dyn_templates = { version = "0.1.0-rc.3", features = ["tera"] }
|
rocket_dyn_templates = { version = "0.2", features = ["tera"] }
|
||||||
rusqlite = { version = "0.29.0", features = ["chrono"] }
|
rusqlite = { version = "0.34", features = ["chrono"] }
|
||||||
serde = "1.0.188"
|
serde = "1.0"
|
||||||
serde_json = "1.0.105"
|
serde_json = "1.0"
|
||||||
serde_yaml = "0.9.25"
|
serde_yaml = "0.9.34" # deprecated
|
||||||
tokio = { version = "1.32.0", features = ["macros", "rt-multi-thread"] }
|
tokio = { version = "1.44", features = ["macros", "rt-multi-thread"] }
|
||||||
|
|
|
@ -11,6 +11,7 @@ use std::{
|
||||||
use gettext::Catalog;
|
use gettext::Catalog;
|
||||||
|
|
||||||
#[derive(From, Debug)]
|
#[derive(From, Debug)]
|
||||||
|
#[allow(dead_code)]
|
||||||
pub enum LoadCatalogsError {
|
pub enum LoadCatalogsError {
|
||||||
Io(std::io::Error),
|
Io(std::io::Error),
|
||||||
Parse(gettext::Error),
|
Parse(gettext::Error),
|
||||||
|
|
|
@ -4,11 +4,12 @@ extern crate rocket;
|
||||||
use clap::{Parser, Subcommand};
|
use clap::{Parser, Subcommand};
|
||||||
use poise::serenity_prelude::Http;
|
use poise::serenity_prelude::Http;
|
||||||
use rocket::{
|
use rocket::{
|
||||||
fs::{relative, FileServer},
|
fs::{relative, FileServer}, Ignite, Rocket
|
||||||
Ignite, Rocket,
|
|
||||||
};
|
};
|
||||||
use rocket_dyn_templates::{tera, Template};
|
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;
|
mod models;
|
||||||
use models::{Database, Settings};
|
use models::{Database, Settings};
|
||||||
|
@ -61,6 +62,15 @@ async fn main() {
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
None => {
|
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");
|
rocket().await.expect("Failed to launch rocket");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -75,9 +85,12 @@ fn http() -> Http {
|
||||||
Http::new(&token)
|
Http::new(&token)
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn rocket() -> Result<Rocket<Ignite>, rocket::Error> {
|
lazy_static! {
|
||||||
let http = http();
|
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((
|
let config = rocket::Config::figment().merge(("port", 1313)).merge((
|
||||||
"secret_key",
|
"secret_key",
|
||||||
env::var("SECRET")
|
env::var("SECRET")
|
||||||
|
@ -85,9 +98,8 @@ async fn rocket() -> Result<Rocket<Ignite>, rocket::Error> {
|
||||||
));
|
));
|
||||||
|
|
||||||
rocket::custom(config)
|
rocket::custom(config)
|
||||||
.manage(Settings::new(&http).await.unwrap())
|
.manage(Settings::new(&HTTP).await.unwrap())
|
||||||
.manage(http)
|
// .manage(http)
|
||||||
.manage(load_database())
|
|
||||||
.mount(
|
.mount(
|
||||||
"/",
|
"/",
|
||||||
routes![
|
routes![
|
||||||
|
|
|
@ -2,7 +2,7 @@ use std::collections::HashSet;
|
||||||
use std::{collections::HashMap, fs::File, io::Read, path::Path};
|
use std::{collections::HashMap, fs::File, io::Read, path::Path};
|
||||||
|
|
||||||
use derive_more::From;
|
use derive_more::From;
|
||||||
use poise::serenity_prelude::{Http, SerenityError};
|
use poise::serenity_prelude::{Http, SerenityError, StatusCode};
|
||||||
use r2d2::{Pool, PooledConnection};
|
use r2d2::{Pool, PooledConnection};
|
||||||
use r2d2_sqlite::rusqlite::{self, params};
|
use r2d2_sqlite::rusqlite::{self, params};
|
||||||
use r2d2_sqlite::SqliteConnectionManager;
|
use r2d2_sqlite::SqliteConnectionManager;
|
||||||
|
@ -20,13 +20,15 @@ pub struct Database {
|
||||||
const DATABASE_FILENAME: &str = "database.db";
|
const DATABASE_FILENAME: &str = "database.db";
|
||||||
|
|
||||||
#[derive(From, Debug)]
|
#[derive(From, Debug)]
|
||||||
|
#[allow(dead_code)]
|
||||||
pub enum DatabaseError {
|
pub enum DatabaseError {
|
||||||
Rusqlite(rusqlite::Error),
|
Rusqlite(rusqlite::Error),
|
||||||
Pool(r2d2::Error),
|
Pool(r2d2::Error),
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(From, Debug)]
|
#[derive(From, Debug)]
|
||||||
pub enum LoadLegacyError {
|
#[allow(dead_code)]
|
||||||
|
pub enum GenericError {
|
||||||
Database(DatabaseError),
|
Database(DatabaseError),
|
||||||
Serenity(SerenityError),
|
Serenity(SerenityError),
|
||||||
}
|
}
|
||||||
|
@ -82,7 +84,7 @@ impl Database {
|
||||||
.is_ok())
|
.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();
|
let latest_challenge = get_challenge_number();
|
||||||
// HashMap of archived users that are no longer sharing a server with 字ちゃん
|
// HashMap of archived users that are no longer sharing a server with 字ちゃん
|
||||||
// Their historical usernames and discriminators will be used
|
// Their historical usernames and discriminators will be used
|
||||||
|
@ -124,12 +126,7 @@ impl Database {
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
Ok(user) => {
|
Ok(user) => self.insert_user(&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)?;
|
|
||||||
}
|
|
||||||
Err(error) if error.to_string().eq("Unknown User") => {
|
Err(error) if error.to_string().eq("Unknown User") => {
|
||||||
// This will also be called in the case of an invalid user ID
|
// This will also be called in the case of an invalid user ID
|
||||||
println!("Failed to fetch user {id}, adding to archive");
|
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()
|
.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)]
|
#[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)
|
// Periodically refresh all changable user data (name, discriminator, avatar)
|
||||||
// Ideally this should run periodically.
|
// 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)]
|
#[allow(dead_code, unused_variables)]
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
mod user;
|
mod user;
|
||||||
pub use user::{SessionUser, User, Username};
|
pub use user::{SessionUser, User};
|
||||||
|
|
||||||
mod challenge;
|
mod challenge;
|
||||||
pub use challenge::Challenge;
|
pub use challenge::Challenge;
|
||||||
|
|
|
@ -26,6 +26,7 @@ impl Settings {
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(From, Debug)]
|
#[derive(From, Debug)]
|
||||||
|
#[allow(dead_code)]
|
||||||
pub enum GetSettingsError {
|
pub enum GetSettingsError {
|
||||||
Io(std::io::Error),
|
Io(std::io::Error),
|
||||||
Deserialize(serde_yaml::Error),
|
Deserialize(serde_yaml::Error),
|
||||||
|
|
|
@ -21,6 +21,7 @@ where
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(From, Debug)]
|
#[derive(From, Debug)]
|
||||||
|
#[allow(dead_code)]
|
||||||
pub enum LegacySubmissionParseError {
|
pub enum LegacySubmissionParseError {
|
||||||
BadAuthorId(std::num::ParseIntError),
|
BadAuthorId(std::num::ParseIntError),
|
||||||
}
|
}
|
||||||
|
@ -45,7 +46,7 @@ impl LegacySubmission {
|
||||||
.next()?
|
.next()?
|
||||||
// Get last number
|
// Get last number
|
||||||
.split('-')
|
.split('-')
|
||||||
.last()?
|
.next_back()?
|
||||||
.parse()
|
.parse()
|
||||||
.ok()
|
.ok()
|
||||||
// Check if discriminator or timestamp, then convert
|
// Check if discriminator or timestamp, then convert
|
||||||
|
|
|
@ -19,7 +19,7 @@ pub trait Username {
|
||||||
fn username(&self) -> String;
|
fn username(&self) -> String;
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Default, Deserialize, Debug)]
|
#[derive(Default, Deserialize, Debug, PartialEq, Eq)]
|
||||||
pub struct User {
|
pub struct User {
|
||||||
#[serde(deserialize_with = "deserialize_id")]
|
#[serde(deserialize_with = "deserialize_id")]
|
||||||
pub id: u64,
|
pub id: u64,
|
||||||
|
@ -92,11 +92,16 @@ impl User {
|
||||||
}
|
}
|
||||||
),
|
),
|
||||||
// Archived user or user with no avatar, calculate default avatar
|
// 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!(
|
None => format!(
|
||||||
"https://cdn.discordapp.com/embed/avatars/{}.png",
|
"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<'_>) {
|
pub fn purge(cookies: &CookieJar<'_>) {
|
||||||
cookies.remove_private(Cookie::named(TOKEN_COOKIE));
|
cookies.remove_private(Cookie::from(TOKEN_COOKIE));
|
||||||
cookies.remove_private(Cookie::named(USER_ID_COOKIE));
|
cookies.remove_private(Cookie::from(USER_ID_COOKIE));
|
||||||
cookies.remove_private(Cookie::named(USER_NAME_COOKIE));
|
cookies.remove_private(Cookie::from(USER_NAME_COOKIE));
|
||||||
cookies.remove_private(Cookie::named(USER_DISCRIMINATOR_COOKIE));
|
cookies.remove_private(Cookie::from(USER_DISCRIMINATOR_COOKIE));
|
||||||
cookies.remove_private(Cookie::named(USER_AVATAR_COOKIE));
|
cookies.remove_private(Cookie::from(USER_AVATAR_COOKIE));
|
||||||
cookies.remove(Cookie::named(TOKEN_EXPIRE_COOKIE));
|
cookies.remove(Cookie::from(TOKEN_EXPIRE_COOKIE));
|
||||||
}
|
}
|
||||||
|
|
||||||
fn from_cookies(cookies: &CookieJar<'_>) -> Option<Self> {
|
fn from_cookies(cookies: &CookieJar<'_>) -> Option<Self> {
|
||||||
|
@ -169,14 +174,14 @@ impl SessionUser {
|
||||||
.get(TOKEN_EXPIRE_COOKIE)
|
.get(TOKEN_EXPIRE_COOKIE)
|
||||||
.map(|expire| expire.value().parse::<i64>())
|
.map(|expire| expire.value().parse::<i64>())
|
||||||
.and_then(Result::ok)
|
.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::from(TOKEN_COOKIE));
|
||||||
cookies.remove_private(Cookie::named(USER_ID_COOKIE));
|
cookies.remove_private(Cookie::from(USER_ID_COOKIE));
|
||||||
cookies.remove_private(Cookie::named(USER_NAME_COOKIE));
|
cookies.remove_private(Cookie::from(USER_NAME_COOKIE));
|
||||||
cookies.remove_private(Cookie::named(USER_DISCRIMINATOR_COOKIE));
|
cookies.remove_private(Cookie::from(USER_DISCRIMINATOR_COOKIE));
|
||||||
cookies.remove_private(Cookie::named(USER_AVATAR_COOKIE));
|
cookies.remove_private(Cookie::from(USER_AVATAR_COOKIE));
|
||||||
cookies.remove(Cookie::named(TOKEN_EXPIRE_COOKIE));
|
cookies.remove(Cookie::from(TOKEN_EXPIRE_COOKIE));
|
||||||
return Ok(None);
|
return Ok(None);
|
||||||
}
|
}
|
||||||
Ok(Some(user))
|
Ok(Some(user))
|
||||||
|
@ -184,6 +189,7 @@ impl SessionUser {
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(From, Debug)]
|
#[derive(From, Debug)]
|
||||||
|
#[allow(dead_code)]
|
||||||
pub enum GetUserError {
|
pub enum GetUserError {
|
||||||
Reqwest(reqwest::Error),
|
Reqwest(reqwest::Error),
|
||||||
Deserialize(serde_json::Error),
|
Deserialize(serde_json::Error),
|
||||||
|
|
|
@ -6,8 +6,8 @@ use rocket_dyn_templates::{context, Template};
|
||||||
use crate::{
|
use crate::{
|
||||||
cookies::LANG_COOKIE,
|
cookies::LANG_COOKIE,
|
||||||
i18n::DEFAULT as DEFAULT_LANG,
|
i18n::DEFAULT as DEFAULT_LANG,
|
||||||
models::{Challenge, Database, SessionUser, Settings},
|
models::{Challenge, SessionUser, Settings},
|
||||||
utils::AcceptLanguage,
|
utils::AcceptLanguage, DATABASE,
|
||||||
};
|
};
|
||||||
|
|
||||||
#[get("/<challenge>")]
|
#[get("/<challenge>")]
|
||||||
|
@ -15,10 +15,9 @@ pub async fn get_challenge(
|
||||||
challenge: u32,
|
challenge: u32,
|
||||||
cookies: &CookieJar<'_>,
|
cookies: &CookieJar<'_>,
|
||||||
settings: &State<Settings>,
|
settings: &State<Settings>,
|
||||||
database: &State<Database>,
|
|
||||||
accept_language: AcceptLanguage,
|
accept_language: AcceptLanguage,
|
||||||
) -> Template {
|
) -> Template {
|
||||||
let (submissions, users) = database.get_challenge_user_data(challenge).unwrap();
|
let (submissions, users) = DATABASE.get_challenge_user_data(challenge).unwrap();
|
||||||
Template::render(
|
Template::render(
|
||||||
"challenge",
|
"challenge",
|
||||||
context! {
|
context! {
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
use poise::serenity_prelude::Http;
|
|
||||||
use rocket::response::stream::{Event, EventStream};
|
use rocket::response::stream::{Event, EventStream};
|
||||||
use rocket::{http::CookieJar, State};
|
use rocket::{http::CookieJar, State};
|
||||||
|
|
||||||
|
use crate::HTTP;
|
||||||
use crate::{cookies::user::USER_ID_COOKIE, models::Settings};
|
use crate::{cookies::user::USER_ID_COOKIE, models::Settings};
|
||||||
|
|
||||||
// TODO: Incrementally send guilds
|
// TODO: Incrementally send guilds
|
||||||
|
@ -9,7 +9,6 @@ use crate::{cookies::user::USER_ID_COOKIE, models::Settings};
|
||||||
pub async fn get_guilds<'a>(
|
pub async fn get_guilds<'a>(
|
||||||
cookies: &'a CookieJar<'_>,
|
cookies: &'a CookieJar<'_>,
|
||||||
settings: &'a State<Settings>,
|
settings: &'a State<Settings>,
|
||||||
http: &'a State<Http>,
|
|
||||||
) -> EventStream![Event + 'a] {
|
) -> EventStream![Event + 'a] {
|
||||||
// EventStream![] is shorthand for EventStream[Event],
|
// EventStream![] is shorthand for EventStream[Event],
|
||||||
// but we need to pass in a lifetime parameter.
|
// but we need to pass in a lifetime parameter.
|
||||||
|
@ -28,7 +27,7 @@ pub async fn get_guilds<'a>(
|
||||||
if guild.hidden {
|
if guild.hidden {
|
||||||
continue;
|
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::{
|
use crate::{
|
||||||
cookies::LANG_COOKIE,
|
cookies::LANG_COOKIE,
|
||||||
i18n::DEFAULT as DEFAULT_LANG,
|
i18n::DEFAULT as DEFAULT_LANG,
|
||||||
models::{Database, DatabaseError, SessionUser, Settings},
|
models::{DatabaseError, SessionUser, Settings},
|
||||||
utils::AcceptLanguage,
|
utils::AcceptLanguage, DATABASE,
|
||||||
};
|
};
|
||||||
|
|
||||||
#[get("/users/<user>")]
|
#[get("/users/<user>")]
|
||||||
|
@ -18,10 +18,9 @@ pub async fn get_user(
|
||||||
user: String,
|
user: String,
|
||||||
cookies: &CookieJar<'_>,
|
cookies: &CookieJar<'_>,
|
||||||
settings: &State<Settings>,
|
settings: &State<Settings>,
|
||||||
database: &State<Database>,
|
|
||||||
accept_language: AcceptLanguage,
|
accept_language: AcceptLanguage,
|
||||||
) -> Result<Template, Status> {
|
) -> 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,
|
Ok(profile_user) => profile_user,
|
||||||
Err(DatabaseError::Rusqlite(rusqlite::Error::QueryReturnedNoRows)) => {
|
Err(DatabaseError::Rusqlite(rusqlite::Error::QueryReturnedNoRows)) => {
|
||||||
return Err(Status::NotFound)
|
return Err(Status::NotFound)
|
||||||
|
@ -35,7 +34,7 @@ pub async fn get_user(
|
||||||
"user",
|
"user",
|
||||||
context! {
|
context! {
|
||||||
profile_user,
|
profile_user,
|
||||||
submissions: database.get_submissions_by_user_name(&user).unwrap(),
|
submissions: DATABASE.get_submissions_by_user_name(&user).unwrap(),
|
||||||
settings: settings.deref(),
|
settings: settings.deref(),
|
||||||
lang: cookies
|
lang: cookies
|
||||||
.get(LANG_COOKIE)
|
.get(LANG_COOKIE)
|
||||||
|
|
|
@ -29,7 +29,7 @@ pub async fn post_login(login: Form<Login<'_>>, cookies: &CookieJar<'_>) -> Redi
|
||||||
.timestamp()
|
.timestamp()
|
||||||
.to_string(),
|
.to_string(),
|
||||||
));
|
));
|
||||||
cookies.remove(Cookie::named(WELCOMED_COOKIE));
|
cookies.remove(Cookie::from(WELCOMED_COOKIE));
|
||||||
}
|
}
|
||||||
Redirect::to("/")
|
Redirect::to("/")
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,20 +1,19 @@
|
||||||
use std::ops::Deref;
|
use std::ops::Deref;
|
||||||
|
|
||||||
use poise::serenity_prelude::Http;
|
use rocket::http::CookieJar;
|
||||||
use rocket::{http::CookieJar, State};
|
|
||||||
|
|
||||||
use crate::models::SessionUser;
|
use crate::{models::SessionUser, HTTP};
|
||||||
|
|
||||||
#[get("/testing")]
|
#[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
|
// Get logged in user's join date in 字ちゃん server
|
||||||
format!(
|
format!(
|
||||||
"{:?}",
|
"{:?}",
|
||||||
http.get_guild(814700630958276649)
|
HTTP.get_guild(814700630958276649)
|
||||||
.await
|
.await
|
||||||
.expect("Failed to get testing guild")
|
.expect("Failed to get testing guild")
|
||||||
.member(
|
.member(
|
||||||
http.deref(),
|
HTTP.deref(),
|
||||||
SessionUser::get(cookies)
|
SessionUser::get(cookies)
|
||||||
.await
|
.await
|
||||||
.expect("Failed to get logged in user data")
|
.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 \{
|
// so { needs to be escaped as \{
|
||||||
// Curly brace literals \{ need to have their backslash escaped as \\{
|
// Curly brace literals \{ need to have their backslash escaped as \\{
|
||||||
// TODO: Modify so <span lang="ja"> only wraps continuous sections of furigana
|
// 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();
|
let re = Regex::new(r"\[([^\]]*)\]\{([^\\}]*)}").unwrap();
|
||||||
format!(
|
format!(
|
||||||
"<span lang=\"ja\">{}</span>",
|
"<span lang=\"ja\">{}</span>",
|
||||||
|
|
|
@ -353,6 +353,7 @@ const KYUJITAI: &[(char, char)] = &[
|
||||||
|
|
||||||
pub trait Kyujitai {
|
pub trait Kyujitai {
|
||||||
fn to_kyujitai(&self) -> String;
|
fn to_kyujitai(&self) -> String;
|
||||||
|
#[allow(dead_code)]
|
||||||
fn to_shinjitai(&self) -> String;
|
fn to_shinjitai(&self) -> String;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
@use "sass:color"
|
@use "sass:color"
|
||||||
|
|
||||||
$bg: #FFFDF3
|
$bg: #fefdfa
|
||||||
$bg0: #{color.adjust($bg, $lightness: -5%, $hue: -7deg, $saturation: -50%)}
|
$bg0: #fefefa
|
||||||
$fg: #011627
|
$fg: #1a1110
|
|
@ -1,6 +1,38 @@
|
||||||
#challenge
|
@use "_theme" as *
|
||||||
padding: 1em
|
|
||||||
|
|
||||||
|
$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
|
a
|
||||||
&.noun
|
&.noun
|
||||||
color: #a6e3a1
|
color: #a6e3a1
|
||||||
|
@ -18,4 +50,22 @@ a
|
||||||
color: #f9e2af
|
color: #f9e2af
|
||||||
|
|
||||||
&.phrase
|
&.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
|
height: 100vh
|
||||||
box-sizing: border-box
|
box-sizing: border-box
|
||||||
padding: 2em
|
padding: 2em
|
||||||
background: $bg0
|
background: #2b2028
|
||||||
color: var(--fg)
|
color: var(--bg)
|
||||||
font-size: 1.25em
|
font-size: 1.25em
|
||||||
|
|
||||||
|
b, strong
|
||||||
|
color: white
|
||||||
|
|
||||||
input[type=number]
|
input[type=number]
|
||||||
background: var(--bg)
|
background: var(--bg)
|
||||||
color: var(--fg)
|
color: var(--fg)
|
||||||
|
@ -27,6 +30,7 @@ input[type=number]
|
||||||
appearance: textfield
|
appearance: textfield
|
||||||
|
|
||||||
a
|
a
|
||||||
|
color: #9fe2bf
|
||||||
text-decoration: none
|
text-decoration: none
|
||||||
|
|
||||||
&:hover
|
&:hover
|
||||||
|
@ -38,5 +42,30 @@ a
|
||||||
h1
|
h1
|
||||||
text-align: center
|
text-align: center
|
||||||
|
|
||||||
&:first-child
|
:first-child
|
||||||
margin-top: 0
|
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}
|
--fg: #{$bg}
|
||||||
background: var(--bg)
|
background: var(--bg)
|
||||||
color: var(--fg)
|
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
|
display: flex
|
||||||
|
align-items: center
|
||||||
|
z-index: 2
|
||||||
|
|
||||||
.dropdown
|
.dropdown
|
||||||
position: relative
|
position: relative
|
||||||
|
@ -51,5 +53,8 @@ nav
|
||||||
cursor: pointer
|
cursor: pointer
|
||||||
|
|
||||||
.icon
|
.icon
|
||||||
height: 1.5em
|
height: 1em
|
||||||
vertical-align: middle
|
vertical-align: middle
|
||||||
|
|
||||||
|
nav .icon
|
||||||
|
height: 1.5em
|
|
@ -10,7 +10,6 @@
|
||||||
background: var(--bg)
|
background: var(--bg)
|
||||||
box-shadow: 0 0 16px rgba(0, 0, 0, 0.125)
|
box-shadow: 0 0 16px rgba(0, 0, 0, 0.125)
|
||||||
border: none
|
border: none
|
||||||
border-radius: 4px
|
|
||||||
|
|
||||||
#content
|
#content
|
||||||
width: 100%
|
width: 100%
|
||||||
|
@ -19,7 +18,7 @@
|
||||||
grid-template-areas: "nav nav" "challenge submissions"
|
grid-template-areas: "nav nav" "challenge submissions"
|
||||||
grid-template-columns: 30em auto
|
grid-template-columns: 30em auto
|
||||||
grid-template-rows: min-content auto
|
grid-template-rows: min-content auto
|
||||||
overflow: hidden
|
border: 4px solid $fg
|
||||||
|
|
||||||
#nav
|
#nav
|
||||||
grid-area: nav
|
grid-area: nav
|
||||||
|
@ -31,10 +30,10 @@
|
||||||
grid-area: submissions
|
grid-area: submissions
|
||||||
|
|
||||||
#challenge, #submissions
|
#challenge, #submissions
|
||||||
overflow: scroll
|
overflow-x: hidden;
|
||||||
|
overflow-y: scroll
|
||||||
display: flex
|
display: flex
|
||||||
flex-direction: column
|
flex-direction: column
|
||||||
gap: 1em
|
|
||||||
|
|
||||||
footer
|
footer
|
||||||
margin-top: auto
|
margin-top: auto
|
||||||
|
|
|
@ -1,6 +1,8 @@
|
||||||
|
@use "_theme" as *
|
||||||
|
|
||||||
#submissions
|
#submissions
|
||||||
|
background: #3f313a
|
||||||
grid-area: submissions
|
grid-area: submissions
|
||||||
background: rgba(0, 0, 0, 0.125)
|
|
||||||
padding: 1em
|
padding: 1em
|
||||||
|
|
||||||
& > div
|
& > div
|
||||||
|
@ -13,7 +15,9 @@
|
||||||
gap: $gap
|
gap: $gap
|
||||||
|
|
||||||
& > figure
|
& > 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
|
padding: $gap
|
||||||
width: 100%
|
width: 100%
|
||||||
box-sizing: border-box
|
box-sizing: border-box
|
||||||
|
@ -23,6 +27,7 @@
|
||||||
text-align: center
|
text-align: center
|
||||||
|
|
||||||
& > img
|
& > img
|
||||||
|
border-radius: 0.5em
|
||||||
width: 100%
|
width: 100%
|
||||||
cursor: pointer
|
cursor: pointer
|
||||||
|
|
||||||
|
|
|
@ -16,7 +16,7 @@
|
||||||
<div id="content">
|
<div id="content">
|
||||||
<nav id="nav">{% block nav %}{% include "nav" %}{% endblock %}</nav>
|
<nav id="nav">{% block nav %}{% include "nav" %}{% endblock %}</nav>
|
||||||
<div id="challenge">
|
<div id="challenge">
|
||||||
<div>{% block content %}{% endblock %}</div>
|
{% block content %}{% endblock %}
|
||||||
<footer>
|
<footer>
|
||||||
{% block content_copyright %}
|
{% 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>.
|
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 %}
|
{% if profile_user %}
|
||||||
<a href="/{{ submission.challenge }}">#{{ submission.challenge }}</a>
|
<a href="/{{ submission.challenge }}">#{{ submission.challenge }}</a>
|
||||||
{% else %}
|
{% 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 %}
|
{% endif %}
|
||||||
</figcaption>
|
</figcaption>
|
||||||
</figure>
|
</figure>
|
||||||
|
|
|
@ -1,7 +1,11 @@
|
||||||
{% extends "base" %}
|
{% extends "base" %}
|
||||||
|
|
||||||
{% block content %}
|
{% 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 %}
|
{% if content.japanese %}
|
||||||
<div lang="ja">
|
<div lang="ja">
|
||||||
<script>
|
<script>
|
||||||
|
@ -15,7 +19,19 @@
|
||||||
kyujitai = !kyujitai;
|
kyujitai = !kyujitai;
|
||||||
}
|
}
|
||||||
</script>
|
</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 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 line in content.japanese %}
|
||||||
{% for subline in line %}
|
{% for subline in line %}
|
||||||
<p>
|
<p>
|
||||||
|
@ -35,6 +51,9 @@
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</div>
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
<div class="wave wave-bottom"></div>
|
||||||
|
<div id="challenge-description">
|
||||||
{{ content.text | safe }}
|
{{ content.text | safe }}
|
||||||
{% if content.translation %}
|
{% if content.translation %}
|
||||||
<p>
|
<p>
|
||||||
|
@ -52,4 +71,5 @@
|
||||||
{%- if content.suggester -%}
|
{%- if content.suggester -%}
|
||||||
<p><em>This challenge was suggested by <strong>{{ content.suggester }}</strong> using the <code>-h suggest</code> command.</code></em></p>
|
<p><em>This challenge was suggested by <strong>{{ content.suggester }}</strong> using the <code>-h suggest</code> command.</code></em></p>
|
||||||
{%- endif -%}
|
{%- endif -%}
|
||||||
|
</div>
|
||||||
{% endblock %}
|
{% endblock %}
|
|
@ -2,7 +2,8 @@
|
||||||
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
<div style="text-align: center">
|
<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>
|
<h1><a href="https://discord.com/users/{{ profile_user.id }}">{{ profile_user.username }}</a>'s submissions</h1>
|
||||||
{{ submissions | length }} images
|
{{ submissions | length }} images
|
||||||
</div>
|
</div>
|
||||||
|
|
Loading…
Add table
Reference in a new issue