From b374c6ed660a4d48763da8875ee925dcb3e59e4d Mon Sep 17 00:00:00 2001 From: ElnuDev Date: Wed, 26 Mar 2025 10:39:47 -0700 Subject: [PATCH] Update for Nix module --- Cargo.lock | 124 +++++++++++++++++++++++++++++++++++++- Cargo.toml | 9 ++- flake.nix | 66 ++++++++++++++++++++ src/commands/challenge.rs | 9 +-- src/commands/meta.rs | 8 +-- src/commands/owner.rs | 6 +- src/main.rs | 54 ++++++++++++++--- src/utils.rs | 25 +++----- 8 files changed, 262 insertions(+), 39 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index f0c388d..2eacda7 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -41,6 +41,56 @@ dependencies = [ "libc", ] +[[package]] +name = "anstream" +version = "0.6.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8acc5369981196006228e28809f761875c0327210a891e941f4c683b3a99529b" +dependencies = [ + "anstyle", + "anstyle-parse", + "anstyle-query", + "anstyle-wincon", + "colorchoice", + "is_terminal_polyfill", + "utf8parse", +] + +[[package]] +name = "anstyle" +version = "1.0.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "55cc3b69f167a1ef2e161439aa98aed94e6028e5f9a59be9a6ffb47aef1651f9" + +[[package]] +name = "anstyle-parse" +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b2d16507662817a6a20a9ea92df6652ee4f94f914589377d69f3b21bc5798a9" +dependencies = [ + "utf8parse", +] + +[[package]] +name = "anstyle-query" +version = "1.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "79947af37f4177cfead1110013d678905c37501914fba0efea834c3fe9a8d60c" +dependencies = [ + "windows-sys 0.59.0", +] + +[[package]] +name = "anstyle-wincon" +version = "3.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ca3534e77181a9cc07539ad51f2141fe32f6c3ffd4df76db8ad92346b003ae4e" +dependencies = [ + "anstyle", + "once_cell", + "windows-sys 0.59.0", +] + [[package]] name = "async-trait" version = "0.1.88" @@ -180,6 +230,52 @@ dependencies = [ "windows-link", ] +[[package]] +name = "clap" +version = "4.5.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6088f3ae8c3608d19260cd7445411865a485688711b78b5be70d78cd96136f83" +dependencies = [ + "clap_builder", + "clap_derive", +] + +[[package]] +name = "clap_builder" +version = "4.5.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "22a7ef7f676155edfb82daa97f99441f3ebf4a58d5e32f295a56259f1b6facc8" +dependencies = [ + "anstream", + "anstyle", + "clap_lex", + "strsim 0.11.1", +] + +[[package]] +name = "clap_derive" +version = "4.5.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09176aae279615badda0765c0c0b3f6ed53f4709118af73cf4655d85d1530cd7" +dependencies = [ + "heck", + "proc-macro2", + "quote", + "syn 2.0.100", +] + +[[package]] +name = "clap_lex" +version = "0.7.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f46ad14479a25103f283c0f10005961cf086d8dc42205bb44c46ac563475dca6" + +[[package]] +name = "colorchoice" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5b63caa9aa9397e2d9480a9b13673856c78d8ac123288526c37d7839f2a86990" + [[package]] name = "core-foundation" version = "0.9.4" @@ -244,7 +340,7 @@ dependencies = [ "ident_case", "proc-macro2", "quote", - "strsim", + "strsim 0.10.0", "syn 1.0.109", ] @@ -570,6 +666,12 @@ version = "0.15.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bf151400ff0baff5465007dd2f3e717f3fe502074ca563069ce3a6629d07b289" +[[package]] +name = "heck" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" + [[package]] name = "http" version = "0.2.12" @@ -933,6 +1035,12 @@ version = "2.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "469fb0b9cefa57e3ef31275ee7cacb78f2fdca44e4765491884a2b119d4eb130" +[[package]] +name = "is_terminal_polyfill" +version = "1.70.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7943c866cc5cd64cbc25b2e01621d07fa8eb2a1a23160ee81ce38704e97b8ecf" + [[package]] name = "itoa" version = "1.0.15" @@ -943,8 +1051,10 @@ checksum = "4a5f13b858c8d314ee3e8f639011f7ccefe71f97f96e50151fb991f267928e2c" name = "ji-chan" version = "0.1.0" dependencies = [ + "clap", "dotenv", "fs_extra", + "lazy_static", "poise", "rand 0.9.0", "reqwest 0.12.15", @@ -1831,6 +1941,12 @@ version = "0.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "73473c0e59e6d5812c5dfe2a064a6444949f089e20eec9a2e5506596494e4623" +[[package]] +name = "strsim" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" + [[package]] name = "subtle" version = "2.6.1" @@ -2277,6 +2393,12 @@ version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be" +[[package]] +name = "utf8parse" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" + [[package]] name = "valuable" version = "0.1.1" diff --git a/Cargo.toml b/Cargo.toml index eb621fe..b0dbd70 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,7 +1,12 @@ [package] name = "ji-chan" version = "0.1.0" -edition = "2021" +authors = ["ElnuDev "] +edition = "2024" +description = "Ji-chan (字ちゃん), the Discord bot and website manager for Tegaki Tuesday, the weekly Japanese handwriting challenge. https://tegakituesday.com/" +homepage = "https://tegakituesday.com/" +repository = "https://git.elnu.com/tegakituesday/ji-chan" +license = "GPL-3.0" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html @@ -16,6 +21,8 @@ slug = "0.1" unicode_hfwidth = "0.2" fs_extra = "1.3" poise = "0.5.7" +clap = { version = "4.5.32", features = ["derive"] } +lazy_static = "1.5.0" [dependencies.tokio] version = "1.32" diff --git a/flake.nix b/flake.nix index 7415250..3f2dd28 100644 --- a/flake.nix +++ b/flake.nix @@ -52,5 +52,71 @@ Some utility commands: }; }; }; + nixosModules.default = { config, ... }: let + lib = nixpkgs.lib; + in { + options.services.ji-chan = { + enable = lib.mkEnableOption (lib.mdDoc "ji-chan service"); + package = lib.mkOption { + type = lib.types.package; + default = self.packages.${system}.ji-chan; + defaultText = "pkgs.ji-chan"; + description = lib.mdDoc '' + The ji-chan package that should be used. + ''; + }; + domain = lib.mkOption { + type = lib.types.str; + default = "tegakituesday.com"; + description = lib.mdDoc '' + Website domain + ''; + }; + token = lib.mkOption { + type = lib.types.str; + description = lib.mdDoc '' + Discord token + ''; + }; + prefix = lib.mkOption { + type = lib.types.str; + description = lib.mdDoc '' + Traditional text command prefix, including space, if any + ''; + }; + hugo = lib.mkOption { + type = lib.types.str; + description = lib.mdDoc '' + Path to Hugo project where site rebuilds + ''; + }; + guildData = lib.mkOption { + type = lib.types.str; + description = lib.mdDoc '' + Path to guild data JSON file + ''; + }; + }; + config.systemd.services.ji-chan = let + cfg = config.services.ji-chan; + pkg = self.packages.${system}.ji-chan; + in lib.mkIf cfg.enable { + description = pkg.meta.description; + after = [ "network.target" ]; + wantedBy = [ "network.target" ]; + serviceConfig = { + ExecStart = '' + ${cfg.package}/bin/ji-chan \ + --domain ${cfg.domain} \ + --token ${cfg.token} \ + --prefix ${cfg.prefix} \ + --hugo ${cfg.hugo} \ + --guildData ${cfg.guildData} + ''; + Restart = "always"; + DynamicUser = true; + }; + }; + }; }; } diff --git a/src/commands/challenge.rs b/src/commands/challenge.rs index 17d4607..c665067 100644 --- a/src/commands/challenge.rs +++ b/src/commands/challenge.rs @@ -1,4 +1,5 @@ use crate::utils::*; +use crate::ARGS; use serde_json::Map; use crate::serenity; @@ -21,7 +22,7 @@ use std::path::Path; pub async fn challenge(ctx: Context<'_>) -> Result<(), Error> { ctx.say(format!( "Tegaki Tuesday #{n}: ", - domain = get_domain(), + domain = ARGS.domain, n = get_challenge_number() )) .await?; @@ -199,7 +200,7 @@ pub async fn submit(ctx: Context<'_>, submission: serenity::Attachment) -> Resul let thank_you = &format!( "Thank you for submitting! You can view your submission at ", challenge_number, - domain = get_domain() + domain = ARGS.domain ); let mut repost_here = true; match ctx { @@ -262,7 +263,7 @@ pub async fn submit(ctx: Context<'_>, submission: serenity::Attachment) -> Resul channel .send_message(&ctx.serenity_context().http, |m| { m.embed(|e| { - let domain = get_domain(); + let domain = &ARGS.domain; let n = get_challenge_number(); let mut description = format!( "New submission to [Tegaki Tuesday #{n}](https://{domain}/{n})!" @@ -344,7 +345,7 @@ pub async fn images(ctx: Context<'_>) -> Result<(), Error> { to_fullwidth(&(i + 1).to_string()), challenge_number, image, - domain = get_domain() + domain = ARGS.domain )); } ctx.say(message).await?; diff --git a/src/commands/meta.rs b/src/commands/meta.rs index 3f24b53..b1ad415 100644 --- a/src/commands/meta.rs +++ b/src/commands/meta.rs @@ -1,10 +1,8 @@ -use crate::utils::get_domain; use crate::Context; use crate::Error; +use crate::ARGS; use poise::command; -use std::env; - // TODO: Implement proper help text for command-specific help, // see https://github.com/kangalioo/poise/blob/90ac24a8ef621ec6dc3fc452762dc9cfa144f693/examples/framework_usage/main.rs#L18-L38 #[command( @@ -33,8 +31,8 @@ __**Kanji 漢字**__ :game_die: `{p}kyoiku ` Random Kyōiku kanji :game_die: `{p}jlpt ` Random JLPT kanji :game_die: `{p}hyogai ` Random Hyōgai kanji", - p = env::var("PREFIX").unwrap(), - domain = get_domain(), + p = ARGS.prefix, + domain = ARGS.domain, ); ctx.say(message).await?; Ok(()) diff --git a/src/commands/owner.rs b/src/commands/owner.rs index 7abfd61..ac31678 100644 --- a/src/commands/owner.rs +++ b/src/commands/owner.rs @@ -2,10 +2,10 @@ use crate::Context; use crate::Error; +use crate::ARGS; use poise::command; use serde_json::json; -use std::env; use crate::utils::*; @@ -100,7 +100,7 @@ pub async fn announcechallenge(ctx: Context<'_>) -> Result<(), Error> { let message = format!("Welcome to the **{n}{th}** weekly **Tegaki Tuesday** (手書きの火曜日) handwriting challenge! :pen_fountain: The prompt is available in both Japanese and English on the website at . You can make submissions in both languages, but please submit in your target language first. Submissions can be submitted by uploading the image to this channel along with the `{p}submit` command. By submitting, you agree to having your work posted to the website under the Attribution-ShareAlike 4.0 Unported (CC BY-SA 4.0) license, attributed to your Discord account. ().", - domain = get_domain(), + domain = ARGS.domain, n = challenge_number, th = match challenge_number % 10 { 1 => "ˢᵗ", @@ -108,7 +108,7 @@ You can make submissions in both languages, but please submit in your target lan 3 => "ʳᵈ", _ => "ᵗʰ" }, - p = env::var("PREFIX").unwrap() + p = ARGS.prefix ); send(ctx, &message, true, false).await?; ctx.say("Announced!").await?; diff --git a/src/main.rs b/src/main.rs index 9b93652..e417ebe 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,7 +1,8 @@ mod commands; mod utils; -use std::env; +use std::env::{self, VarError}; +use lazy_static::lazy_static; type Error = Box; type Context<'a> = poise::Context<'a, Data, Error>; @@ -12,6 +13,7 @@ pub struct Data {} use commands::{challenge::*, kanji::*, meta::*, owner::*}; use poise::serenity_prelude as serenity; use poise::serenity_prelude::model::gateway::GatewayIntents; +use clap::Parser; #[poise::command(prefix_command)] async fn register(ctx: Context<'_>) -> Result<(), Error> { @@ -19,12 +21,50 @@ async fn register(ctx: Context<'_>) -> Result<(), Error> { Ok(()) } +#[derive(Parser)] +#[clap(author, version, about)] +struct Arguments { + #[clap(long, help = "Website domain", default_value = "tegakituesday.com")] + pub domain: String, + #[clap(long = "token", help = "Discord token")] + pub discord_token: String, + #[clap(long, help = "Traditional text command prefix, including space, if any", default_value = "-h")] + pub prefix: String, + #[clap(long, help = "Path to Hugo project where site rebuilds")] + pub hugo: String, + #[clap(long = "guilds", help = "Guild data JSON file", default_value = "guilds.json")] + pub guild_data: String, +} + +impl Arguments { + fn from_dotenv() -> Result { + Ok(Self { + domain: env::var("DOMAIN")?, + discord_token: env::var("DISCORD_TOKEN")?, + prefix: env::var("PREFIX")?, + hugo: env::var("HUGO")?.into(), + guild_data: env::var("GUILD_DATA")?.into(), + }) + } +} + +lazy_static! { + static ref ARGS: Arguments = { + match env::args().count() { + 1 => { + // This will load the environment variables located at `./.env`, relative to + // the CWD. See `./.env.example` for an example on how to structure this. + dotenv::dotenv().expect("Failed to load .env file"); + + Arguments::from_dotenv().unwrap() + }, + _ => Arguments::parse(), + } + }; +} + #[tokio::main] async fn main() { - // This will load the environment variables located at `./.env`, relative to - // the CWD. See `./.env.example` for an example on how to structure this. - dotenv::dotenv().expect("Failed to load .env file"); - // Initialize the logger to use environment variables. // // In this case, a good default is setting the environment variable @@ -59,12 +99,12 @@ async fn main() { so(), ], prefix_options: poise::PrefixFrameworkOptions { - prefix: Some(env::var("PREFIX").expect("Expected a prefix in the environment")), + prefix: Some(ARGS.prefix.clone()), ..Default::default() }, ..Default::default() }) - .token(std::env::var("DISCORD_TOKEN").expect("Expected a token in the environment")) + .token(&ARGS.discord_token) .intents( GatewayIntents::GUILD_MESSAGES | GatewayIntents::DIRECT_MESSAGES diff --git a/src/utils.rs b/src/utils.rs index 819f4aa..023fc52 100644 --- a/src/utils.rs +++ b/src/utils.rs @@ -1,6 +1,7 @@ use crate::serenity; use crate::Context; use crate::Error; +use crate::ARGS; use rand::seq::IteratorRandom; use serde_json::Map; use serde_json::Value; @@ -52,7 +53,7 @@ mod tests { } pub fn get_challenge_number() -> i32 { - let challenge_dir = format!("{}/content/challenges", env::var("HUGO").unwrap()); + let challenge_dir = format!("{}/content/challenges", ARGS.hugo); let paths = fs::read_dir(challenge_dir).unwrap(); let mut max = 0; for path in paths { @@ -72,20 +73,12 @@ pub fn get_challenge_number() -> i32 { max } -pub fn get_domain() -> String { - env::var("DOMAIN").unwrap() -} - -pub fn get_hugo_path() -> String { - env::var("HUGO").unwrap() -} - pub fn get_submission_images_dir() -> String { - format!("{}/assets/{}", get_hugo_path(), get_challenge_number()) + format!("{}/assets/{}", ARGS.hugo, get_challenge_number()) } pub fn get_submission_data_path(challenge: i32) -> String { - format!("{}/data/challenges/{}.json", get_hugo_path(), challenge) + format!("{}/data/challenges/{}.json", ARGS.hugo, challenge) } pub fn get_current_submission_data_path() -> String { @@ -153,17 +146,13 @@ pub fn to_fullwidth(string: &str) -> String { pub fn rebuild_site() { Command::new("./build.sh") - .current_dir(get_hugo_path()) + .current_dir(&ARGS.hugo) .status() .expect("Failed to rebuild site"); } -pub fn get_guild_data_path() -> String { - env::var("GUILD_DATA").unwrap() -} - pub fn get_guild_data() -> Map { - let guild_data_path = get_guild_data_path(); + let guild_data_path = &ARGS.guild_data; let guild_data_json = match File::open(&guild_data_path) { Ok(mut file) => { let mut json = String::new(); @@ -187,7 +176,7 @@ pub fn set_guild_data(guild_data: Map) { let mut guild_data_file = OpenOptions::new() .write(true) .truncate(true) - .open(get_guild_data_path()) + .open(&ARGS.guild_data) .unwrap(); guild_data_file .write_all(