From 0c87e5f56ac44664d8b9605fa59a8f533d3287eb Mon Sep 17 00:00:00 2001 From: Byte Dice Date: Sun, 6 Apr 2025 15:43:35 +0200 Subject: [PATCH 01/39] added support for language files --- .gitignore | 2 + Cargo.toml | 1 + TODO.md | 3 +- ...d_data_preset.json => dc_data_preset.json} | 0 data/lang/en.json | 62 ++ ...t_data_preset.json => re_data_preset.json} | 0 src/bk_week_cmds.rs | 752 ------------------ src/cmds.rs | 47 +- src/data.rs | 45 +- src/events.rs | 11 +- src/gen.rs | 96 +++ src/macros.rs | 36 + src/main.rs | 172 +--- src/messages.rs | 28 +- src/python.rs | 15 +- src/python/data.py | 6 +- src/python/main.py | 2 +- src/re_cmds/add.rs | 68 ++ src/re_cmds/admin_bind.rs | 28 + src/re_cmds/approve.rs | 58 ++ src/re_cmds/generic_fns.rs | 34 + src/re_cmds/get.rs | 50 ++ src/re_cmds/remove.rs | 40 + src/re_cmds/top.rs | 77 ++ src/re_cmds/update.rs | 305 +++++++ src/re_cmds/vote.rs | 67 ++ src/schedule.rs | 35 + src/websocket.rs | 20 +- 28 files changed, 1087 insertions(+), 973 deletions(-) rename data/{discord_data_preset.json => dc_data_preset.json} (100%) create mode 100644 data/lang/en.json rename data/{reddit_data_preset.json => re_data_preset.json} (100%) delete mode 100644 src/bk_week_cmds.rs create mode 100644 src/gen.rs create mode 100644 src/re_cmds/add.rs create mode 100644 src/re_cmds/admin_bind.rs create mode 100644 src/re_cmds/approve.rs create mode 100644 src/re_cmds/generic_fns.rs create mode 100644 src/re_cmds/get.rs create mode 100644 src/re_cmds/remove.rs create mode 100644 src/re_cmds/top.rs create mode 100644 src/re_cmds/update.rs create mode 100644 src/re_cmds/vote.rs create mode 100644 src/schedule.rs diff --git a/.gitignore b/.gitignore index 448c8eb..d226673 100644 --- a/.gitignore +++ b/.gitignore @@ -9,4 +9,6 @@ __pycache__/ # program-created data data/reddit_data.json data/discord_data.json +data/re_data.json +data/dc_data.json data/cfg.json \ No newline at end of file diff --git a/Cargo.toml b/Cargo.toml index 53528d7..f35a927 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -7,6 +7,7 @@ edition = "2021" [dependencies] clap = { version = "4.5.28", features = ["derive"] } +formatx = "0.2.3" futures = "0.3.31" poise = "0.6.1" pyo3 = "0.23.4" diff --git a/TODO.md b/TODO.md index 65d324e..a152e3f 100644 --- a/TODO.md +++ b/TODO.md @@ -1,6 +1,7 @@ ### High priority: - [ ] Reddit bot that scrapes images with tag "Original Art" and posts them in Discord server + - [ ] Rename commands from "bk_week_{name}" to "re_{name}" @@ -10,7 +11,7 @@ - [ ] Allow disabling bk_week - - [ ] Language files? + - [ ] Language files - [ ] Allow updating the data autonomously and via manual commands. diff --git a/data/discord_data_preset.json b/data/dc_data_preset.json similarity index 100% rename from data/discord_data_preset.json rename to data/dc_data_preset.json diff --git a/data/lang/en.json b/data/lang/en.json new file mode 100644 index 0000000..59749e6 --- /dev/null +++ b/data/lang/en.json @@ -0,0 +1,62 @@ +{ + "8-ball_answer": "Q: {0}\nA: {1}", + "add_to_data": "Added your server to my data! Thanks for letting me steal it! (/s)", + "bound_dc_channel": "Successfully bound channel ID `{0}` as the bk_week channel!", + "cfg_data_404": "cfg.json not found,", + "cfg_data_wipe": "[WIPE] (cfg.json)", + "cmd_404": "No command with the name \"{0}\" found!\nHint: Try `/help` without any arguments or `/help `", + "corrupted_data": "Oopsies `(。>\\\\<)`. It looks like my data i-is \\**sob*\\*... c-corrupted!", + "couldnt_shorturl": "Couldn't convert to shortURL: Invalid URL", + "creating_data_file": "{0} creating new from preset...", + "data_post_embed": "Spoilers and vote length anonymizer for fair review!\n ## Post Data:\n **Post upvotes:** ||`{0:>6}`||\n **Moderator votes:** ||`{1:>6}`||\n **Media type:** `{2}`\n **URL:** ||<{3}>||\n\n ## Listing Data:\n **Added by:** `{{ human: {4}, bot: {5} }}`\n **Approved by:** `{{ human: {6}, bot: [not implemented] }}`", + "data_post_removed_embed": "## Removed by `{0}`\n**Reason:** {1}\nURL: ||<{2}>||\n\nJSON: ||`{3}`||", + "data_save_complete": "Saving data... Done!\nShutting down...", + "data_save_progress": "Saving data...", + "dc_404": "Your server is not in the data!\n Hint: Run the command `/add_server` inside of a Discord server.", + "dc_bot_started": "Bot started as user \"{0}\" with id {1}", + "dc_bot_starting": "Starting bot...", + "dc_data_404": "discord_data.json not found,", + "dc_data_wipe": "[WIPE] (discord_data.json)", + "default_embed_desc": "Default description", + "err_read_py_file": "Failed to read Python file!\nPath: {0}", + "help_text_removed": "Help text not found. Someone deleted it. :(", + "invalid_confirm_shutdown": "Failed to shut down: Invalid confirmation.", + "invalid_os": "Man what kinda OS do you have? Neither unix or windows, what the hell!? I can't process this anymore, you're too weird!", + "lang_data_404": "file for language \"{}\" not found!", + "lang_load_success": "Successfully loaded the english language file!", + "mandatory_response": "Mandatory response message, please ignore.", + "python_err": "Unknown internal Python Error: {0}", + "python_socket_binary_response": "[Binary] from Python: {0}", + "python_socket_err": "Unknown internal Python error occurred: Websocket response error.", + "python_socket_null": "Response from Python is None!", + "python_socket_response": "Received from Python: [RESPONSE] {0}", + "python_socket_unknown_response": "Received from Python: [UNKNOWN / OTHER]", + "re_404": "Post URL \"<{0}>\" not found: Post doesn't exist in the data!\n Hint: Run the command `/re_addPost [URL]` in a Discord channel or `u/ByteDiceAssistant bk_week_add` in a Reddit post.", + "re_add_post_success": "Added post with URL \"<{0}>\"!", + "re_already_voted": "Couldn't cast a vote: You have already voted on this post!", + "re_also_approved": "Also approved it!", + "re_approved_post_success": "Successfully approved the post!", + "re_data_404": "reddit_data.json not found,", + "re_data_corrupted": "Post URL \"<{0}>\" not found: Post data is corrupted!\n Full details: Could not find key \"bk_weekly_art_posts\" in data file \"reddit_data.json\"", + "re_data_wipe": "[WIPE] (reddit_data.json)", + "re_disapproved_post_success": "Successfully disapproved the post!", + "re_err_trace": "Unknown error!\nError trace: {0}.", + "re_havent_voted_remove_vote": "Couldn't remove your vote: You haven't voted on this post yet!", + "re_permdeny_bk_mod": "Permission denied: You are not a moderator of {0}", + "re_remove_post_success": "Successfully removed post!", + "re_unremove_post_success": "Successfully un-removed post with URL \"<{0}>\"!", + "re_update_post_success": "Updated post with URL \"<{0}>\"!", + "re_vote_err": "Failed to vote/un-vote: Unknown internal error", + "re_vote_remove_success": "Successfully removed vote!", + "re_vote_success_mod": "Successfully voted (as moderator vote)!", + "re_vote_success": "Successfully voted!", + "reload_config_python_fail": "Failed to reload configs: Failed-type response from Python.", + "reload_config_success": "Successfully reloaded the configs!\nNew configs:\n```\n{0}\n```", + "shorturl": "ShortURL: <{0}>", + "socket_received_python": "Received from Python: {0}", + "socket_rust_connection_test": "[Connection test] Hello from Rust!", + "started_socket": "WebSocket server running on ws://{0}", + "starting_python_program": "Running Python program...", + "starting_schedules": "Starting schedules...", + "starting_socket": "Starting local websocket..." +} \ No newline at end of file diff --git a/data/reddit_data_preset.json b/data/re_data_preset.json similarity index 100% rename from data/reddit_data_preset.json rename to data/re_data_preset.json diff --git a/src/bk_week_cmds.rs b/src/bk_week_cmds.rs deleted file mode 100644 index 06c21d5..0000000 --- a/src/bk_week_cmds.rs +++ /dev/null @@ -1,752 +0,0 @@ -use crate::websocket::send_cmd_json; -use crate::{cmds, rs_println, websocket, Context, Error, BK_WEEK}; -use crate::messages::*; -use crate::data::{self, dc_bind_bk, get_mutex_data}; - -use std::collections::HashMap; -use std::time::{SystemTime, UNIX_EPOCH}; - -use poise::serenity_prelude::{ChannelId, EditMessage, GetMessages, Http, Message, MessageId, UserId}; -use poise::ReplyHandle; -use serde_json::{json, Map, Value}; - - -#[derive(poise::ChoiceParameter, PartialEq)] -enum TopCategory { - Upvotes, - ModVotes, - Oldest, - Newest -} - - -fn is_bk_mod(mod_list: Vec, uid: u64) -> bool { - return mod_list.contains(&uid); -} - - -async fn not_bk_mod_msg(ctx: Context<'_>) { - send_msg(ctx, "Permission denied: You are not a moderator of r/boykisser or r/boykisser2".to_string(), true, true).await; -} - - - - -#[poise::command( - slash_command, - prefix_command, - category = "bk_week", - required_bot_permissions = "SEND_MESSAGES | VIEW_CHANNEL | EMBED_LINKS" -)] -/// Fetches the data of a single post, just for you. The data has to be within the database to work. -pub async fn bk_week_get( - ctx: Context<'_>, - #[description = "The post URL."] url: String -) -> Result<(), Error> -{ - data::update_re_data(ctx.data()).await; - - let reddit_data = get_mutex_data(&ctx.data().reddit_data).await?; - - if let Some(post) = get_post_from_data(ctx, &reddit_data, &url).await? { - send_embed_for_post(ctx, post, &url).await?; - } - - return Ok(()); -} - - -async fn get_post_from_data(ctx: Context<'_>, reddit_data: &Value, url: &str) -> Result, Error> { - if let Some(bk_week) = reddit_data.get(BK_WEEK) { - if let Some(post) = bk_week.get(url) { - if post.get("removed").is_some() { - send_post_removed_message(ctx, url, post).await; - return Ok(None); - } - return Ok(Some(post.clone())); - } - else { - send_post_not_found_message(ctx, url).await; - } - } - else { - send_data_corrupted_message(ctx, url).await; - rs_println!("{}", serde_json::to_string_pretty(reddit_data)?); - } - return Ok(None); -} - - -async fn send_embed_for_post(ctx: Context<'_>, post: Value, url: &str) -> Result<(), Error> { - send_embed(ctx, embed_post(&post, url, true), true).await; - Ok(()) -} - - -async fn send_post_not_found_message(ctx: Context<'_>, url: &str) { - send_msg( - ctx, - format!( - r#"Post URL \"<{}>\" not found: Post doesn't exist in the data! - Hint: Run the command `/bk_week_add [URL]` in a Discord channel or `u/ByteDiceAssistant bk_week_add` in a Reddit post."#, - url - ).trim().to_string(), - true, - true - ).await; -} - - -async fn send_post_removed_message(ctx: Context<'_>, url: &str, post: &Value) { - send_embed( - ctx, - embed_post_removed(post, url, true), - true - ).await; -} - - -async fn send_data_corrupted_message(ctx: Context<'_>, url: &str) { - send_msg( - ctx, - format!( - r#"Post URL \"<{}>\" not found: Post data is corrupted! - Full details: Could not find key \"bk_weekly_art_posts\" in data file \"reddit_data.json\""#, - url, - ).trim().to_string(), - true, - true - ).await; -} - - - - -#[poise::command( - slash_command, - prefix_command, - category = "bk_week", - required_bot_permissions = "SEND_MESSAGES | VIEW_CHANNEL" -)] -/// Fetches a post from Reddit and adds it to the database. -pub async fn bk_week_add( - ctx: Context<'_>, - #[description = "The post URL."] url: String, - #[description = "Wether to approve it after adding it"] approve: Option -) -> Result<(), Error> -{ - if !is_bk_mod(ctx.data().bk_mods.clone(), ctx.author().id.get()) { - not_bk_mod_msg(ctx).await; - return Ok(()); - } - - let shorturl_u = cmds::to_shorturl(&url); - let shorturl = &shorturl_u.unwrap_or(url.clone()); - - data::update_re_data(ctx.data()).await; - let reddit_data = get_mutex_data(&ctx.data().reddit_data).await?; - - if let Some(bk_week) = reddit_data.get(BK_WEEK) { - let a = approve.unwrap_or(false); - let r = websocket::send_cmd_json("add_post_url", Some(json!([&shorturl, a, true]))).await.unwrap(); - - if !r["value"].as_bool().unwrap() { - send_msg( - ctx, - r#"Unknown error! - Error trace: `bk_week_cmds.rs -> bk_week_add() -> Unknown error`. - Common reasons: The URL provided was likely invalid or 403: forbidden (e.g a private subreddit)."#.to_string(), - true, - true - ).await; - return Ok(()); - } - - if let Some(post) = bk_week.get(shorturl) { - if post.get("removed").is_some() { - send_unremove_msg(ctx, shorturl).await; - } - else { - send_updated_msg(ctx, shorturl).await; - } - } - else { - send_msg(ctx, format!("Added post with URL \"<{}>\"!", &shorturl), true, true).await; - } - - if a { - send_msg(ctx, "Also approved it!".to_string(), true, true).await; - } - } - - return Ok(()); -} - - -async fn send_unremove_msg(ctx: Context<'_>, url: &str) { - send_msg(ctx, format!("Un-removed post with URL \"<{}>\"!", url), true, true).await; -} - - -async fn send_updated_msg(ctx: Context<'_>, url: &str) { - send_msg(ctx, format!("Updated post with URL \"<{}>\"!", url), true, true).await; -} - - - - -#[poise::command( - slash_command, - prefix_command, - category = "bk_week", - required_bot_permissions = "SEND_MESSAGES | VIEW_CHANNEL" -)] -/// Removes a post from the database. It will show who last removed it. -pub async fn bk_week_remove( - ctx: Context<'_>, - #[description = "The post URL."] url: String, - #[description = "The reason of the removal."] reason: Option -) -> Result<(), Error> -{ - if !is_bk_mod(ctx.data().bk_mods.clone(), ctx.author().id.get()) { - not_bk_mod_msg(ctx).await; - return Ok(()); - } - - let auth = &ctx.author().name; - let r = send_cmd_json("remove_post_url", Some(json!([&url, &auth, &reason]))).await.unwrap(); - - if r["value"].as_bool().unwrap() { - send_msg( - ctx, - "Successfully flagged the post as removed!".to_string(), - true, - true - ).await; - } - else { - send_post_not_found_message(ctx, &url).await; - } - - return Ok(()); -} - - - - -#[poise::command( - slash_command, - prefix_command, - category = "bk_week", - required_bot_permissions = "SEND_MESSAGES | VIEW_CHANNEL" -)] -/// Approves a post in the database. Approving posts tells the bot that it's original. -pub async fn bk_week_approve( - ctx: Context<'_>, - #[description = "The post URL."] url: String, - #[description = "Wether to approve or disapprove the post"] disapprove: Option -) -> Result<(), Error> -{ - if !is_bk_mod(ctx.data().bk_mods.clone(), ctx.author().id.get()) { - not_bk_mod_msg(ctx).await; - return Ok(()); - } - - data::update_re_data(ctx.data()).await; - let reddit_data = get_mutex_data(&ctx.data().reddit_data).await?; - - approve_cmd(ctx, &url, &reddit_data, !disapprove.unwrap_or(false)).await; - - return Ok(()); -} - - -async fn approve_cmd(ctx: Context<'_>, url: &str, reddit_data: &Value, approve: bool) { - if let Some(post) = reddit_data.get(BK_WEEK).unwrap().get(url) { - if post.get("removed").is_some() { - send_post_removed_message(ctx, url, post).await; - return; - } - - let r = websocket::send_cmd_json("set_approve_post", Some(json!([approve, &url]))).await.unwrap(); - if r.get("value").is_some() { - if approve { - send_msg(ctx, "Successfully flagged the post as approved (by a human)!".to_string(), true, true).await; - } - else { - send_msg(ctx, "Successfully removed the \"approved (by a human)\" flag from the post!".to_string(), true, true).await; - } - } - else { - send_msg(ctx, "Unknown error!\nError trace: `bk_week_cmds.rs -> bk_week_approve() -> unwrap websocket result error`.".to_string(), true, true).await; - } - } - else { - send_post_not_found_message(ctx, url).await; - } -} - - - - -#[poise::command( - slash_command, - prefix_command, - category = "admin", - default_member_permissions = "ADMINISTRATOR", - guild_only, - required_bot_permissions = "SEND_MESSAGES | VIEW_CHANNEL" -)] -/// Sets the channel where the bot will dump all log info. It's recommended to only run this once. -pub async fn bk_admin_bind( - ctx: Context<'_> -) -> Result<(), Error> -{ - let c_id = ctx.channel_id().into(); - let r = dc_bind_bk(ctx.data(), ctx.guild_id().unwrap().into(), c_id).await; - - if r.is_ok() { - send_msg(ctx, format!("Successfully bound channel ID `{}` as the bk_week channel!", c_id), true, true).await; - } - else { - send_server_not_in_data_msg(ctx).await; - } - - return Ok(()); -} - - -async fn send_server_not_in_data_msg(ctx: Context<'_>) { - send_msg(ctx, "Your server is not in the data!\nHint: Run the command `/add_server` inside of a Discord server.".to_string(), true, true).await; -} - - - - -#[poise::command( - slash_command, - prefix_command, - category = "bk_week", - guild_only, - guild_cooldown = 120, - required_bot_permissions = "SEND_MESSAGES | VIEW_CHANNEL | READ_MESSAGE_HISTORY | EMBED_LINKS" -)] -/// Updates all logs -pub async fn bk_week_update( - ctx: Context<'_>, - #[description = "Only adds new posts, leaves everything else unchanged."] - only_add: Option, - #[description = "The max age of a post (in days). Any post older than this will be removed. (0 is infinite.)"] - #[min = 0] - #[max = 65535] - max_age: Option -) -> Result<(), Error> -{ - let http = ctx.http(); - - let mut p_text = "`/bk_week_update`:".to_string(); - - let progress = send_msg(ctx, p_text.clone(), true, true).await.unwrap(); - p_text = update_progress(ctx, progress.clone(), p_text, "\nFetching new posts & updating data file...".to_string()).await; - - let max_age_u = max_age.unwrap_or(8); - let max_age_secs = max_age_u as u64 * (60 * 60 * 24); - - send_cmd_json("add_new_posts", Some(json!([max_age_secs]))).await; - data::update_re_data(ctx.data()).await; - let r_data = get_mutex_data(&ctx.data().reddit_data).await?; - - let c_id_u = get_c_id(ctx).await; - - if c_id_u.is_none() { - send_msg(ctx, "Could not find bk_week_channel in data!\nHint: Run (or tell an admin to run) `/bk_admin_bind` in a (preferably read-only) channel.".to_string(), true, true).await; - return Ok(()); - } - - let c_id = c_id_u.unwrap(); - - // Reading messages - p_text = update_progress(ctx, progress.clone(), p_text.clone(), format!("✅\nReading messages in <#{}>...", c_id)).await; - let msgs = read_msgs(http, ctx.framework().bot_id, c_id).await; - - // Parsing messages to JSON - p_text = update_progress(ctx, progress.clone(), p_text.clone(), "✅\nParsing messages to JSON...".to_string()).await; - let msgs_json = msgs_to_json(msgs, &r_data, max_age_secs).await; - - // Adding new posts - p_text = update_progress(ctx, progress.clone(), p_text.clone(), "✅\nAdding new posts...".to_string()).await; - let weekly_art = r_data[BK_WEEK].as_object().unwrap(); - add_posts(http, c_id, weekly_art, &msgs_json, max_age_secs).await; - - // Stop if only_add - if only_add.unwrap_or(false) { - send_msg(ctx, "`/bk_week_update`\n## Done!".to_string(), true, true).await; - update_progress(ctx, progress.clone(), p_text, "✅\n## Done!".to_string()).await; - return Ok(()); - } - - // Editing updated posts - p_text = update_progress(ctx, progress.clone(), p_text.clone(), "✅\nEditing updated posts...".to_string()).await; - edit_posts(http, c_id, weekly_art, &msgs_json).await; - - // Removing removed posts - p_text = update_progress(ctx, progress.clone(), p_text.clone(), "✅\nRemoving removed posts...".to_string()).await; - remove_posts(http, c_id, weekly_art, &msgs_json).await; - - // Removing old posts - if max_age_u > 0 { - p_text = update_progress(ctx, progress.clone(), p_text.clone(), format!("✅\nRemoving old posts (threshold: {}d)...", max_age_u)).await; - remove_old(http, c_id, &msgs_json).await; - send_cmd_json("remove_old_posts", Some(json!([max_age_secs]))).await; - } - - // Removing duplicate posts - p_text = update_progress(ctx, progress.clone(), p_text.clone(), "✅\nRemoving duplicate posts...".to_string()).await; - remove_dupes(http, c_id, &msgs_json).await; - - // Done - update_progress(ctx, progress.clone(), p_text, "✅\n## Done!".to_string()).await; - send_msg(ctx, "`/bk_week_update`\n## Done!".to_string(), true, true).await; - - return Ok(()); -} - - -async fn update_progress(ctx: Context<'_>, p: ReplyHandle<'_>, t: String, added_t: String) -> String { - let p_text = format!("{} {}", t, added_t); - - edit_reply(ctx, p, p_text.clone()).await; - return p_text; -} - - -async fn get_c_id(ctx: Context<'_>) -> Option { - if !data::dc_contains_server(ctx.data(), ctx.guild_id().unwrap().into()).await { - send_server_not_in_data_msg(ctx).await; - return None; - } - - let d = get_mutex_data(&ctx.data().discord_data).await.unwrap(); - let c_id_u = - d["servers"] - [ctx.guild_id().unwrap().to_string()] - ["bk_week_channel"].as_u64().unwrap(); - - let c_id = ChannelId::new(c_id_u); - - return Some(c_id); -} - - -async fn read_msgs(http: &Http, bot_id: UserId, c_id: ChannelId) -> Vec { - let b = GetMessages::new().limit(100); - let mut msgs = c_id.messages(http, b).await.unwrap(); - msgs.retain(|item| item.author.id == bot_id); - - let mut last_msg: Option = msgs.last().cloned(); - - while last_msg.is_some() { - let new_b = GetMessages::new().limit(100).before(last_msg.clone().unwrap()); - let new_msgs = c_id.messages(http, new_b).await.unwrap(); - - last_msg = new_msgs.last().cloned(); - - if new_msgs.is_empty() { - break; - } - - let filtered_msgs: Vec = new_msgs - .into_iter() - .filter(|item| item.author.id == bot_id) - .collect(); - - msgs.extend(filtered_msgs); - } - - return msgs; -} - - -async fn msgs_to_json(msgs: Vec, reddit_data: &Value, max_age: u64) -> Value { - let mut msgs_json: Value = json!({"no_change": {}, "updated": {}, "removed": {}, "duplicates": {}, "old": {}}); - let now = SystemTime::now() - .duration_since(UNIX_EPOCH) - .expect("Time went backwards") - .as_secs(); - - for msg in msgs { - if msg.embeds.is_empty() { continue; } - if msg.embeds[0].url.is_none() { continue; } - - let url = msg.embeds[0].url.clone().unwrap(); - - // duplicates - if ["no_change", "updated", "removed", "old"] - .iter() - .any(|key| msgs_json[key].as_object().unwrap().contains_key(&url)) - { - let dupes_mut = msgs_json["duplicates"].as_object_mut().unwrap(); - if !dupes_mut.contains_key(&url) { - dupes_mut.insert(url.clone(), json!(msg.id.get())); - } - continue; - } - - let msg_desc = &msg.embeds[0].description.clone().unwrap(); - let msg_lines = msg_desc.split("\n"); - let msg_last_len = msg_lines.clone().last().unwrap().len(); - - if msg_last_len < 13 { continue; } - - let msg_json_str = &msg_lines.clone().last().unwrap()[9..msg_last_len - 3]; - - let msg_json = serde_json::from_str(msg_json_str); - if msg_json.is_err() { continue; } - - let mut u_json: Value = msg_json.unwrap(); - let re_url = &reddit_data[BK_WEEK][&url]; - - let post_date = re_url["post_data"]["date_unix"].as_u64().unwrap_or(0); - - // old - if now - post_date > max_age { - if let Some(obj) = msgs_json["old"].as_object_mut() { - obj.insert(url.clone(), json!(msg.id.get())); - continue; - } - } - - // removed - if re_url.get("removed").is_some() { - if u_json.get("removed").is_some() { - // no change - if let Some(obj) = msgs_json["no_change"].as_object_mut() { - obj.insert(url.clone(), json!(msg.id.get())); - continue; - } - } - - // removed - if let Some(obj) = msgs_json["removed"].as_object_mut() { - obj.insert(url.clone(), json!(msg.id.get())); - continue; - } - } - - // updated - if u_json["added"] != re_url["added"] - || u_json["approved"] != re_url["approved"] - || u_json["post_data"]["upvotes"] != re_url["post_data"]["upvotes"] - || u_json["votes"]["mod_voters"] != re_url["votes"]["mod_voters"] - { - u_json.as_object_mut().unwrap().insert("msg_id".to_string(), Value::String(msg.id.clone().to_string())); - - if let Some(obj) = msgs_json["updated"].as_object_mut() { - obj.insert(url.clone(), json!(msg.id.get())); - continue; - } - } - - // no change - if let Some(obj) = msgs_json["no_change"].as_object_mut() { - obj.insert(url.clone(), json!(msg.id.get())); - } - } - - return msgs_json; -} - - -async fn add_posts(http: &Http, c_id: ChannelId, r_data: &Map, msgs_json: &Value, max_age: u64) { - let now = SystemTime::now() - .duration_since(UNIX_EPOCH) - .expect("Time went backwards") - .as_secs(); - - for url in r_data.keys() { - if ["no_change", "updated", "removed", "old", "duplicates"] - .iter() - .any(|key| msgs_json[key].as_object().unwrap().contains_key(url)) - { continue; } - - let post_date = r_data[url]["post_data"]["date_unix"].as_u64().unwrap(); - if now - post_date > max_age { continue; } - - if r_data[url].get("removed").is_some() { - http_send_embed(http, c_id, embed_post_removed(&r_data[url], url, false)).await; - continue; - } - - http_send_embed(http, c_id, embed_post(&r_data[url], url, false)).await; - } -} - - -async fn edit_posts(http: &Http, c_id: ChannelId, r_data: &Map, msgs_json: &Value) { - for (url, msg_id) in msgs_json["updated"].as_object().unwrap() { - let mut msg = http.get_message(c_id, MessageId::new(msg_id.as_u64().unwrap())).await.unwrap(); - let r = EditMessage::new() - .embeds(vec![embed_from_options(embed_post(&r_data[url], url, false))]); - - let _ = msg.edit(http, r).await; - } -} - - -async fn remove_posts(http: &Http, c_id: ChannelId, r_data: &Map, msgs_json: &Value) { - for (url, msg_id) in msgs_json["removed"].as_object().unwrap() { - let mut msg = http.get_message(c_id, MessageId::new(msg_id.as_u64().unwrap())).await.unwrap(); - let r = EditMessage::new() - .embeds(vec![embed_from_options(embed_post_removed(&r_data[url], url, false))]); - - let _ = msg.edit(http, r).await; - } -} - - -async fn remove_old(http: &Http, c_id: ChannelId, msgs_json: &Value) { - for (_url, msg_id) in msgs_json["old"].as_object().unwrap() { - let msg = http.get_message(c_id, MessageId::new(msg_id.as_u64().unwrap())).await.unwrap(); - let _ = msg.delete(http).await; - } -} - - -async fn remove_dupes(http: &Http, c_id: ChannelId, msgs_json: &Value) { - for (_url, msgs) in msgs_json["duplicates"].as_object().unwrap() { - for msg_id in msgs.as_array().unwrap() { - let msg = http.get_message(c_id, MessageId::new(msg_id.as_u64().unwrap())).await.unwrap(); - let _ = msg.delete(http).await; - } - } -} - - -#[poise::command( - slash_command, - prefix_command, - category = "bk_week", - required_bot_permissions = "SEND_MESSAGES | VIEW_CHANNEL" -)] -/// Adds/removes a vote from a post. These votes are not tied to Reddit upvotes. -pub async fn bk_week_vote( - ctx: Context<'_>, - #[description = "The post URL."] url: String, - #[description = "Wether to undo your vote or not"] un_vote: Option -) -> Result<(), Error> -{ - data::update_re_data(ctx.data()).await; - let uid = ctx.author().id.get(); - let re_data = get_mutex_data(&ctx.data().reddit_data).await?; - let post_data = re_data[BK_WEEK].clone(); - let unw_vote = un_vote.unwrap_or(false); - - if post_data.get(&url).is_none() { - send_post_not_found_message(ctx, &url).await; - return Ok(()); - } - if post_data[&url].get("removed").is_some() { - send_post_removed_message(ctx, &url, &post_data[&url]).await; - return Ok(()); - } - - let url_data = &post_data[&url]; - - let is_mod = is_bk_mod(ctx.data().bk_mods.clone(), ctx.author().id.get()); - let voters_dc = url_data["votes"]["voters_dc"].as_array().unwrap(); - let mod_voters = url_data["votes"]["mod_voters"].as_array().unwrap(); - let voters = if is_mod { mod_voters } else { voters_dc }; - - if voters.contains(&json!(uid)) && !unw_vote { - send_msg(ctx, "Couldn't cast a vote: You have already voted on this post!".to_string(), true, true).await; - return Ok(()); - } - else if !voters.contains(&json!(uid)) && unw_vote { - send_msg(ctx, "Couldn't remove your vote: You haven't voted on this post yet!".to_string(), true, true).await; - return Ok(()); - } - - let r = send_cmd_json("set_vote_post", Some(json!([url, uid, is_mod, true, unw_vote]))).await.unwrap(); - let unw_r = r["value"].as_bool().unwrap(); - - if unw_r && !unw_vote && is_mod { - send_msg(ctx, "Successfully voted (as moderator vote)!".to_string(), true, true).await; - } - else if unw_r && !unw_vote && !is_mod { - send_msg(ctx, "Successfully voted!".to_string(), true, true).await; - } - else if unw_r && unw_vote { - send_msg(ctx, "Successfully removed vote!".to_string(), true, true).await; - } - else { - send_msg(ctx, "Failed to vote/un-vote: Unknown internal error".to_string(), true, true).await; - } - - return Ok(()); -} - - - -#[poise::command( - slash_command, - prefix_command, - category = "bk_week", - required_bot_permissions = "SEND_MESSAGES | VIEW_CHANNEL | EMBED_LINKS" -)] -/// Gets the top N (up to 10) posts within a certain category, such as upvotes. (Sorted descending.) -pub async fn bk_week_top( - ctx: Context<'_>, - #[description = "The sorting criteria, such as upvotes."] - category: TopCategory, - #[description = "The amount of posts to show (max 10)."] - #[min = 1] - #[max = 10] - amount: Option -) -> Result<(), Error> -{ - let mut all: HashMap<&str, i32> = HashMap::new(); - let posts = &get_mutex_data(&ctx.data().reddit_data).await?[BK_WEEK]; - let posts_u = posts.as_object().unwrap(); - - for (url, dat) in posts_u { - if dat.get("removed").is_some() { continue; } - - let val: i32 = match category { - TopCategory::Upvotes => dat["post_data"]["upvotes"].as_i64().unwrap() as i32, - TopCategory::ModVotes => dat["votes"]["mod_voters"].as_array().unwrap().len() as i32, - TopCategory::Oldest - | TopCategory::Newest => dat["post_data"]["date_unix"].as_i64().unwrap() as i32, - }; - - all.insert(url, val); - } - - let amount_u = amount.unwrap_or(3); - let amount_clamped = amount_u.clamp(1, 10); - - let top = - if category != TopCategory::Oldest - { largest_n (&all, amount_clamped as usize) } - else { smallest_n(&all, amount_clamped as usize) }; - - for post in top { - let url = post.0; - let _ = send_embed_for_post(ctx, posts_u[url].clone(), url).await; - } - - return Ok(()); -} - - -fn largest_n<'a>(map: &'a HashMap<&'a str, i32>, n: usize) -> Vec<(&'a str, i32)> { - let mut vec: Vec<_> = map.iter().collect(); - vec.sort_unstable_by(|a, b| b.1.cmp(a.1)); - vec.into_iter().take(n).map(|(&k, &v)| (k, v)).collect() -} - - -fn smallest_n<'a>(map: &'a HashMap<&'a str, i32>, n: usize) -> Vec<(&'a str, i32)> { - let mut vec: Vec<_> = map.iter().collect(); - vec.sort_unstable_by(|a, b| a.1.cmp(b.1)); - vec.into_iter().take(n).map(|(&k, &v)| (k, v)).collect() -} \ No newline at end of file diff --git a/src/cmds.rs b/src/cmds.rs index bf9ec13..18c9b18 100644 --- a/src/cmds.rs +++ b/src/cmds.rs @@ -3,14 +3,14 @@ use std::process; use std::error::Error as StdErr; use crate::data::{dc_add_server, get_mutex_data, read_cfg_data}; +use crate::re_cmds::generic_fns::to_shorturl; use crate::websocket::send_cmd_json; -use crate::{data, Context, Data, Error}; +use crate::{data, lang, Context, Data, Error}; use crate::messages::{edit_reply, send_embed, send_msg, Author, EmbedOptions, MANDATORY_MSG}; use poise::serenity_prelude::{OnlineStatus, Timestamp}; use poise::Command; use rand::{seq::IteratorRandom, Rng}; -use regex::Regex; use serde_json::json; use tokio::fs; @@ -63,19 +63,19 @@ pub async fn stop( || confirmation.unwrap_or_default().to_lowercase() == "i want to stop the bot now"; if should_stop { - let msg = send_msg(ctx, "Saving data...".to_string(), true, true).await.unwrap(); + let msg = send_msg(ctx, lang!("data_save_progress"), true, true).await.unwrap(); data::write_dc_data(ctx.data()).await; data::write_re_data().await; send_cmd_json("stop_praw", None).await; - edit_reply(ctx, msg, "Saving data... Done!\nShutting down...".to_string()).await; + edit_reply(ctx, msg, lang!("data_save_complete")).await; ctx.serenity_context().set_presence(None, OnlineStatus::Invisible); ctx.framework().shard_manager.shutdown_all().await; process::exit(0); } else { - send_msg(ctx, "Failed to shut down: Invalid confirmation.".to_string(), true, true).await; + send_msg(ctx, lang!("invalid_confirm_shutdown"), true, true).await; } return Ok(()); @@ -171,7 +171,7 @@ pub async fn eight_ball( send_msg( ctx, - format!("Q: {}\nA: {}", question, rand_item.unwrap()), + lang!("8-ball_answer", question, rand_item.unwrap()), true, true ).await; @@ -194,29 +194,16 @@ pub async fn re_shorturl( let shorturl = to_shorturl(&url); if shorturl.is_ok() { - send_msg(ctx, format!("ShortURL: <{}>", shorturl.unwrap()), true, true).await; + send_msg(ctx, lang!("shorturl", shorturl.unwrap()), true, true).await; } else { - send_msg(ctx, "Couldn't convert to shortURL: Invalid URL".to_string(), true, true).await; + send_msg(ctx, lang!("couldnt_shorturl"), true, true).await; } return Ok(()); } -pub fn to_shorturl(url: &str) -> Result { - let re = Regex::new(r"comments/([a-zA-Z0-9]+)").unwrap(); - - if let Some(caps) = re.captures(url) { - let post_id = &caps[1]; - let short_url = format!("https://redd.it/{}", post_id); - return Ok(short_url); - } - - return Err("Invalid URL"); -} - - #[poise::command( slash_command, prefix_command, @@ -233,10 +220,10 @@ pub async fn add_server( let r = dc_add_server(ctx.data(), ctx.guild_id().unwrap().into()).await; if r.is_ok() { - send_msg(ctx, "Added your server to my data! Thanks for letting me steal it! (/s)".to_string(), true, true).await; + send_msg(ctx, lang!("add_to_data"), true, true).await; } else { - send_msg(ctx, "Oopsies `(。>\\\\<)`. It looks like my data i-is \\**sob*\\*... c-corrupted!".to_string(), true, true).await; + send_msg(ctx, lang!("corrupted_data"), true, true).await; } return Ok(()); @@ -263,14 +250,14 @@ pub async fn reload_cfg( if r.is_some() && r.unwrap()["value"].as_bool().unwrap() { send_msg( ctx, - format!("Successfully reloaded the configs!\nNew configs:\n```\n{}\n```", serde_json::to_string_pretty(&d)?), + lang!("reload_config_success", serde_json::to_string_pretty(&d).unwrap()), true, true ).await; return Ok(()); } - send_msg(ctx, "Failed to reload configs: Failed-type response from Python.".to_string(), true, true).await; + send_msg(ctx, lang!("reload_config_python_fail"), true, true).await; return Ok(()); } @@ -342,7 +329,7 @@ async fn send_single_help(ctx: Context<'_>, mut cmd_name: String) { if cmd.is_none() { send_msg( ctx, - format!("No command with the name \"{}\" found!\nHint: Try `/help` without any arguments or `/help `", cmd_name), + lang!("cmd_404", cmd_name), true, true ).await; @@ -368,7 +355,7 @@ async fn send_category_help(ctx: Context<'_>, category: HelpOptions) { async fn send_bk_week_help_re(ctx: Context<'_>) { let t: String = fs::read_to_string("./bk_week_help_re.md").await - .unwrap_or("Help text not found. Someone deleted it. :(".to_string()); + .unwrap_or(lang!("help_text_removed")); send_msg(ctx, t, true, true).await; } @@ -435,10 +422,10 @@ async fn send_bk_week_help(ctx: Context<'_>) { let cmds = &ctx.framework().options().commands; let bk_week_cmds: Vec<_> = cmds .iter() - .filter(|cmd| cmd.category == Some("bk_week".to_string())) + .filter(|cmd| cmd.category == Some("re".to_string())) .collect(); - let t = format_cmds(vec![("bk_week", bk_week_cmds)]); + let t = format_cmds(vec![("re", bk_week_cmds)]); send_msg(ctx, t, true, true).await; } @@ -449,7 +436,7 @@ async fn send_generic_help(ctx: Context<'_>) { .iter() .filter( |cmd| - cmd.category != Some("bk_week".to_string()) + cmd.category != Some("re".to_string()) || cmd.category != Some("owner".to_string()) || cmd.category != Some("admin".to_string()) ) diff --git a/src/data.rs b/src/data.rs index 1e42086..f3eca25 100644 --- a/src/data.rs +++ b/src/data.rs @@ -4,23 +4,24 @@ use std::path::Path; use serde_json::{self, Value, json}; use tokio::sync::Mutex; -use crate::{Data, BK_WEEK, rs_println, Error}; +use crate::{lang, rs_println, Data, Error, BK_WEEK, LANG}; use crate::websocket::send_cmd_json; -static DATA_PATH_DC: &str = "./data/discord_data.json"; -static PRESET_PATH_DC: &str = "./data/discord_data_preset.json"; -static DATA_PATH_RE: &str = "./data/reddit_data.json"; -static PRESET_PATH_RE: &str = "./data/reddit_data_preset.json"; +static DATA_PATH_DC: &str = "./data/dc_data.json"; +static PRESET_PATH_DC: &str = "./data/dc_data_preset.json"; +static DATA_PATH_RE: &str = "./data/re_data.json"; +static PRESET_PATH_RE: &str = "./data/re_data_preset.json"; static DATA_PATH_CFG: &str = "./data/cfg.json"; static PRESET_PATH_CFG: &str = "./data/cfg_default.json"; +static DATA_PATH_LANG: &str = "./data/lang/"; pub async fn read_dc_data(data: &Data, wipe: bool) { if !Path::new(DATA_PATH_DC).exists() || wipe { rs_println!( - "{} creating new from preset...", - if !wipe { "discord_data.json not found," } else { "[WIPE] (discord_data.json)" } + "{}", + lang!("creating_data_file", if !wipe { lang!("dc_data_404") } else { lang!("dc_data_wipe") }) ); generate_dc_data(); } @@ -70,8 +71,8 @@ pub async fn write_dc_data(data: &Data) { pub async fn read_re_data(data: &Data, wipe: bool) { if !Path::new(DATA_PATH_RE).exists() || wipe { rs_println!( - "{} creating new from preset...", - if !wipe { "reddit_data.json not found," } else { "[WIPE] (reddit_data.json)" } + "{}", + lang!("creating_data_file", if !wipe { lang!("re_data_404") } else { lang!("re_data_wipe") }) ); generate_re_data(); } @@ -113,8 +114,8 @@ pub async fn write_re_data() { pub async fn read_cfg_data(data: &Data, wipe: bool) { if !Path::new(DATA_PATH_CFG).exists() || wipe { rs_println!( - "{} creating new from preset...", - if !wipe { "cfg.json not found," } else { "[WIPE] (cfg.json)" } + "{}", + lang!("creating_data_file", if !wipe { lang!("cfg_data_404") } else { lang!("cfg_data_wipe") }) ); generate_cfg_data(); } @@ -148,7 +149,7 @@ pub async fn dc_add_server(data: &Data, server_id: u64) -> Result<(), ()> { let servers = dc_data["servers"].as_object_mut().unwrap(); if !servers.contains_key(&server_id.to_string()) { - servers.insert(server_id.to_string(), json!({ "bk_week_channel": 0, "bk_mod_role": "bk mod", "bk_mods": [] })); + servers.insert(server_id.to_string(), json!({ "bk_week_channel": 0 })); } return Ok(()); @@ -194,4 +195,24 @@ pub async fn get_mutex_data(data: &Mutex>) -> Result Some(data) => Ok(data.clone()), None => Err("Cannot get mutex data: The data is corrupted!".into()), }; +} + + +pub fn load_lang_data(lang: String) { + let full_path = format!("{}{}.json", DATA_PATH_LANG, lang); + + if !Path::new(&full_path).exists() { + rs_println!( + "{}", + lang!("creating_data_file", lang!("cfg_data_404", lang)) + ); + generate_cfg_data(); + } + + let str_data = fs::read_to_string(full_path).unwrap(); + let json_data = serde_json::from_str(&str_data).unwrap(); + + unsafe { + LANG = json_data; + }; } \ No newline at end of file diff --git a/src/events.rs b/src/events.rs index adcfe64..098d575 100644 --- a/src/events.rs +++ b/src/events.rs @@ -1,4 +1,4 @@ -use crate::{Data, Error, rs_println}; +use crate::{lang, rs_println, Data, Error}; use poise::serenity_prelude::{self as serenity, ActivityData}; @@ -14,9 +14,12 @@ pub fn event_handler<'a>( Box::pin(async move { if let serenity::FullEvent::Ready { data_about_bot } = event { rs_println!( - "Bot started as user \"{}\" with id {}", - data_about_bot.user.name, - data_about_bot.user.id + "{}", + lang!( + "dc_bot_started", + data_about_bot.user.name.clone(), + data_about_bot.user.id + ) ); let file_text = std::fs::read_to_string("./data/status.txt").unwrap(); diff --git a/src/gen.rs b/src/gen.rs new file mode 100644 index 0000000..c93b32c --- /dev/null +++ b/src/gen.rs @@ -0,0 +1,96 @@ +use std::collections::HashSet; + +use poise::serenity_prelude::UserId; +use poise::serenity_prelude as serenity; +use poise::serenity_prelude::Client; + +use crate::{cmds, data, events, re_cmds, rs_println, Args, Data}; + + +pub async fn gen_data(args: Args, owners: Vec) -> Data { + let ball_classic_str = std::fs::read_to_string("./data/8-ball_classic.txt").unwrap(); + let ball_quirk_str = std::fs::read_to_string("./data/8-ball_quirky.txt").unwrap(); + + let ball_classic: Vec = ball_classic_str.lines().map(String::from).collect(); + let ball_quirk: Vec = ball_quirk_str .lines().map(String::from).collect(); + + let mods_env = std::env::var("ASSISTANT_BK_MODS").unwrap_or("0".to_string()); + let mods_vec_str: Vec = mods_env.split(",").map(String::from).collect(); + let mods_vec_u64: Vec = mods_vec_str + .iter() + .map(|s| s.parse::().expect("Failed to parse ASSISTANT_BK_MODS. Invalid syntax.")) + .collect(); + + let data = Data { + owners, + ball_prompts: [ball_classic, ball_quirk], + bk_mods: mods_vec_u64, + reddit_data: None.into(), + discord_data: None.into(), + cfg: None.into(), + args: args.clone() + }; + + data::read_dc_data (&data, args.clone().wipe).await; + data::read_re_data (&data, args.clone().wipe).await; + data::read_cfg_data(&data, args.clone().wipe).await; + + return data; +} + + +pub async fn gen_bot(data: Data, args: Args) -> Client { + let token = + if !args.test { std::env::var("ASSISTANT_TOKEN").expect("Missing ASSISTANT_TOKEN env var!") } + else { std::env::var("ASSISTANT_TOKEN_TEST").expect("Missing ASSISTANT_TOKEN_TEST env var!") }; + + let intents = serenity::GatewayIntents::all(); + + let peek_len = 27; + let token_peek = &token[..peek_len]; + let token_end_len = token[peek_len..].len(); + rs_println!("Token: {}{}", token_peek, "*".repeat(token_end_len)); + + let own: HashSet = data.owners.clone().into_iter().map(UserId::from).collect(); + + let framework = poise::Framework::builder() + .options(poise::FrameworkOptions { + owners: own, + commands: vec![ + cmds::help(), + cmds::ping(), + cmds::embed(), + cmds::send(), + cmds::stop(), + cmds::eight_ball(), + cmds::re_shorturl(), + cmds::add_server(), + // reddit + re_cmds::add::cmd(), + re_cmds::approve::cmd(), + re_cmds::get::cmd(), + re_cmds::remove::cmd(), + re_cmds::top::cmd(), + re_cmds::update::cmd(), + re_cmds::vote::cmd(), + // reddit admin + re_cmds::admin_bind::cmd(), + // cfg + cmds::reload_cfg() + ], + event_handler: events::event_handler, + ..Default::default() + }) + .setup(|ctx, _ready, framework| { + Box::pin(async move { + poise::builtins::register_globally(ctx, &framework.options().commands).await?; + return Ok(data); + }) + }) + .build(); + + return serenity::ClientBuilder::new(token, intents) + .framework(framework) + .await + .unwrap(); +} \ No newline at end of file diff --git a/src/macros.rs b/src/macros.rs index 1e6e040..6873725 100644 --- a/src/macros.rs +++ b/src/macros.rs @@ -34,4 +34,40 @@ macro_rules! errln { ); std::process::exit(1); }; +} + + +#[macro_export] +macro_rules! lang { + ($key:expr) => { + { + use crate::{LANG, errln}; + let value = unsafe { + LANG + .as_ref() + .expect("LANG must be initialized before use") + .get($key) + }; + + if value.is_none() { errln!("Key not found in LANG JSON: \"{}\"", $key); } + + value.unwrap().as_str().expect("LANG JSON value is not a string!").to_string() + } + }; + ($key:expr, $($arg:expr),*) => {{ + use crate::{LANG, errln}; + use formatx::formatx; + + let value = unsafe { + LANG + .as_ref() + .expect("LANG must be initialized before use") + .get($key) + }; + + if value.is_none() { errln!("Key not found in LANG JSON: \"{}\"", $key); } + + let format_str = value.unwrap().as_str().expect("LANG JSON value is not a string!"); + formatx!(format_str, $($arg),*).unwrap() + }}; } \ No newline at end of file diff --git a/src/main.rs b/src/main.rs index 68db106..bf17a00 100644 --- a/src/main.rs +++ b/src/main.rs @@ -2,7 +2,17 @@ #![allow(clippy::needless_return)] mod cmds; -mod bk_week_cmds; +mod re_cmds { + pub mod add; + pub mod admin_bind; + pub mod approve; + pub mod generic_fns; + pub mod get; + pub mod remove; + pub mod top; + pub mod update; + pub mod vote; +} mod events; mod messages; mod python; @@ -10,27 +20,27 @@ mod macros; #[allow(unknown_lints)] mod websocket; mod data; +mod schedule; +mod gen; -use std::collections::HashSet; -use std::future::Future; -use std::pin::Pin; use std::process; use std::thread; use std::time::Duration; use std::vec; use clap::Parser; -use poise::serenity_prelude::UserId; -use poise::serenity_prelude as serenity; -use poise::serenity_prelude::Client; +use r#gen::gen_bot; +use r#gen::gen_data; +use schedule::run_schedules; use serde::Serialize; use serde_json::Value; use tokio::runtime::Runtime; use tokio::sync::Mutex; -use tokio::task::JoinHandle; -use tokio::time; use websocket::send_cmd_json; +use crate::schedule::Schedule; + +// TODO: convert to lang! #[derive(Parser, Serialize, Clone)] struct Args { @@ -49,13 +59,14 @@ struct Args { #[arg(long, help = "Removes the annoying ping prints.")] noping: bool, #[arg(long, help = "Makes the program not use the schedules.")] - nosched: bool + nosched: bool, + #[arg(long, default_value = "en", help = "Which language file to use (Do not include file extention)")] + lang: String } type Error = Box; type Context<'a> = poise::Context<'a, Data, Error>; -type Schedule = (Duration, fn() -> Pin + Send>>); struct Data { @@ -65,18 +76,24 @@ struct Data { discord_data: Mutex>, cfg: Mutex>, bk_mods: Vec, - args: Args + args: Args, } static BK_WEEK: &str = "bk_weekly_art_posts"; +pub static mut LANG: Option = None; + #[tokio::main] async fn main() { let args = ::parse(); let args_str = serde_json::to_string(&args).expect("Error serializing args to JSON"); + rs_println!("Fetching language file..."); + data::load_lang_data(args.clone().lang); + rs_println!("{}", lang!("lang_load_success")); + let own_env = std::env::var("ASSISTANT_OWNERS").unwrap_or("0".to_string()); let own_vec_str: Vec = own_env.split(",").map(String::from).collect(); let own_vec_u64: Vec = own_vec_str @@ -92,7 +109,7 @@ async fn main() { if args.py && !args.rs { println!("----- PYTHON ONLY MODE -----"); rs_println!("ARGS: {}", args_str); - let _ = python::start(args); + let _ = python::start(args).await; process::exit(0); } else if args.rs && ! args.py { @@ -101,25 +118,25 @@ async fn main() { start(args, own_vec_u64.clone()).await; process::exit(0); } - else if args.py && args.rs { - errln!("Invalid arguments: Arguments cannot include both --rs and --py."); - } rs_println!("ARGS: {}", args_str); - let rt = Runtime::new().unwrap(); + let rt_rs = Runtime::new().unwrap(); + let rt_py = Runtime::new().unwrap(); let python_args = args.clone(); let rust_args = args.clone(); let rust = thread::spawn(move || { - rt.block_on(async { + rt_rs.block_on(async { websocket::start(rust_args.clone(), own_vec_u64.clone()).await; start(rust_args, own_vec_u64).await; }); }); - let python = thread::spawn(|| { - let _ = python::start(python_args); + let python = thread::spawn(move || { + rt_py.block_on(async { + let _ = python::start(python_args).await; + }); }); if !args.nosched { @@ -139,124 +156,11 @@ async fn start(args: Args, owners: Vec) { let data = gen_data(args.clone(), owners).await; let mut bot = gen_bot(data, args).await; - rs_println!("Starting bot..."); + rs_println!("{}", lang!("dc_bot_starting")); bot.start().await.unwrap(); } -async fn gen_data(args: Args, owners: Vec) -> Data { - let ball_classic_str = std::fs::read_to_string("./data/8-ball_classic.txt").unwrap(); - let ball_quirk_str = std::fs::read_to_string("./data/8-ball_quirky.txt").unwrap(); - - let ball_classic: Vec = ball_classic_str.lines().map(String::from).collect(); - let ball_quirk: Vec = ball_quirk_str .lines().map(String::from).collect(); - - let mods_env = std::env::var("ASSISTANT_BK_MODS").unwrap_or("0".to_string()); - let mods_vec_str: Vec = mods_env.split(",").map(String::from).collect(); - let mods_vec_u64: Vec = mods_vec_str - .iter() - .map(|s| s.parse::().expect("Failed to parse ASSISTANT_BK_MODS. Invalid syntax.")) - .collect(); - - let data = Data { - owners, - ball_prompts: [ball_classic, ball_quirk], - bk_mods: mods_vec_u64, - reddit_data: None.into(), - discord_data: None.into(), - cfg: None.into(), - args: args.clone() - }; - - data::read_dc_data (&data, args.clone().wipe).await; - data::read_re_data (&data, args.clone().wipe).await; - data::read_cfg_data(&data, args.clone().wipe).await; - - return data; -} - - -async fn gen_bot(data: Data, args: Args) -> Client { - let token = - if !args.test { std::env::var("ASSISTANT_TOKEN").expect("Missing ASSISTANT_TOKEN env var!") } - else { std::env::var("ASSISTANT_TOKEN_TEST").expect("Missing ASSISTANT_TOKEN_TEST env var!") }; - - let intents = serenity::GatewayIntents::all(); - - let peek_len = 27; - let token_peek = &token[..peek_len]; - let token_end_len = token[peek_len..].len(); - rs_println!("Token: {}{}", token_peek, "*".repeat(token_end_len)); - - let own: HashSet = data.owners.clone().into_iter().map(UserId::from).collect(); - - let framework = poise::Framework::builder() - .options(poise::FrameworkOptions { - owners: own, - commands: vec![ - cmds::help(), - cmds::ping(), - cmds::embed(), - cmds::send(), - cmds::stop(), - cmds::eight_ball(), - cmds::re_shorturl(), - cmds::add_server(), - // bk_week - bk_week_cmds::bk_week_get(), - bk_week_cmds::bk_week_add(), - bk_week_cmds::bk_week_remove(), - bk_week_cmds::bk_week_approve(), - bk_week_cmds::bk_week_update(), - bk_week_cmds::bk_week_vote(), - bk_week_cmds::bk_week_top(), - // bk_admin - bk_week_cmds::bk_admin_bind(), - // cfg - cmds::reload_cfg() - ], - event_handler: events::event_handler, - ..Default::default() - }) - .setup(|ctx, _ready, framework| { - Box::pin(async move { - poise::builtins::register_globally(ctx, &framework.options().commands).await?; - return Ok(data); - }) - }) - .build(); - - return serenity::ClientBuilder::new(token, intents) - .framework(framework) - .await - .unwrap(); -} - - -async fn run_schedule Pin + Send>>>(d: Duration, f: F) { - let mut ticker = time::interval(d); - loop { - ticker.tick().await; - f().await; - } -} - - -async fn run_schedules(schedules: Vec) { - let mut handles: Vec> = vec![]; - - rs_println!("Starting schedules..."); - for (d, f) in schedules { - let handle = tokio::spawn(run_schedule(d, f)); - handles.push(handle); - } - - for handle in handles { - let _ = handle.await; - } -} - - async fn read_reddit_inbox() { unsafe { if !websocket::HAS_CONNECTED { return; } } send_cmd_json("respond_mentions", None).await; diff --git a/src/messages.rs b/src/messages.rs index 4db60e8..5b5ccc9 100644 --- a/src/messages.rs +++ b/src/messages.rs @@ -1,6 +1,6 @@ use std::env; -use crate::{Args, Context}; +use crate::{lang, Args, Context}; use poise::serenity_prelude::json::Value; use poise::{serenity_prelude::CreateMessage, CreateReply, ReplyHandle}; @@ -29,7 +29,7 @@ pub struct EmbedOptions { impl Default for EmbedOptions { fn default() -> Self { return EmbedOptions { - desc: "default description".to_string(), + desc: lang!("default_embed_desc"), title: None, col: None, url: None, @@ -194,24 +194,14 @@ pub async fn send_dm(msg: String, args: Args, owners: Vec) { } -pub fn embed_post(post_data: &Value, url: &str, ephemeral: bool) -> EmbedOptions { +pub fn make_post_embed(post_data: &Value, url: &str, ephemeral: bool) -> EmbedOptions { let media_type = &post_data["post_data"]["media_type"]; - let desc_str = format!( - r#"Sorted by what I think will be most important - Spoilers and vote length anonymizer for fair review! - ## Post Data: - **Media type:** `{}` - **Post upvotes:** ||`{:>6}`|| - **Moderator votes:** ||`{:>6}`|| - **URL:** ||<{}>|| - - ## Listing Data: - **Added by:** `{{ human: {}, bot: {} }}` - **Approved by:** `{{ human: {}, bot: [not implemented] }}`"#, - if !media_type.is_null() { media_type.as_str().unwrap() } else { "None" }, + let desc_str = lang!( + "data_post_embed", post_data["post_data"]["upvotes"].as_i64().unwrap(), post_data["votes"]["mod_voters"].as_array().unwrap().len(), + if !media_type.is_null() { media_type.as_str().unwrap() } else { "None" }, url, if post_data["added"] ["by_human"].as_bool().unwrap() { "✅" } else { "❌" }, @@ -248,11 +238,11 @@ pub fn embed_post(post_data: &Value, url: &str, ephemeral: bool) -> EmbedOptions } -pub fn embed_post_removed(post_data: &Value, url: &str, ephemeral: bool) -> EmbedOptions { +pub fn make_removed_embed(post_data: &Value, url: &str, ephemeral: bool) -> EmbedOptions { return EmbedOptions { title: Some("REMOVED!".to_string()), - desc: format!( - "## Removed by `{}`\n**Reason:** {}\nURL: ||<{}>||\n\nJSON: ||`{}`||", + desc: lang!( + "data_post_removed_embed", post_data["removed_by"].as_str().unwrap(), if !post_data["remove_reason"].is_null() { post_data["remove_reason"].as_str().unwrap() } else { "None" }, diff --git a/src/python.rs b/src/python.rs index 4dc055f..d6146b2 100644 --- a/src/python.rs +++ b/src/python.rs @@ -1,5 +1,5 @@ use crate::messages::send_dm; -use crate::{errln, rs_println, Args}; +use crate::{errln, lang, rs_println, Args}; use std::fs; use std::ffi::CString; @@ -10,12 +10,12 @@ use pyo3::types::PyList; pub async fn start(args: Args) -> PyResult<()> { - rs_println!("Running Python program..."); + rs_println!("{}", lang!("starting_python_program")); let args_str = serde_json::to_string(&args).expect("Error serializing args to JSON"); let slash = if cfg!(windows) { "\\" } else if cfg!(unix) { "/" } else { "" }; - if slash.is_empty() { errln!("Man what kinda OS do you have? Neither unix or windows, what the hell!? I can't process this anymore, you're too weird!"); } + if slash.is_empty() { errln!("{}", lang!("invalid_os")); } let path = format!("{0}{1}src{1}python", env!("CARGO_MANIFEST_DIR"), slash); @@ -43,7 +43,7 @@ pub async fn start(args: Args) -> PyResult<()> { .map(|s| s.parse::().expect("Failed to parse ASSISTANT_OWNERS. Invalid syntax.")) .collect(); - send_dm(format!("Unknown internal Python Error: {:?}", from_python), args, own_vec_u64).await; + send_dm(lang!("python_err", format!("{:?}", from_python)), args, own_vec_u64).await; errln!("pyO3: {:?}", from_python); } return Ok(()); @@ -51,7 +51,8 @@ pub async fn start(args: Args) -> PyResult<()> { fn get_code(path: &str) -> String { - return fs::read_to_string(path) - .unwrap_or_else(|_| errln!("Failed to read Python file.\nPath: {}", path)) - .to_string(); + let file = fs::read_to_string(path); + if file.is_err() { errln!("Failed to read Python file.\nPath: {}", path); } + + return file.unwrap().to_string(); } diff --git a/src/python/data.py b/src/python/data.py index 5ce0367..de6dfde 100644 --- a/src/python/data.py +++ b/src/python/data.py @@ -66,7 +66,7 @@ class PostData: def read_data(bot: botPy.Bot) -> bool: - r_path = os.path.join(DATA_PATH, "reddit_data.json") + r_path = os.path.join(DATA_PATH, "re_data.json") if os.path.isfile(r_path): bot.data_f = open(r_path, "r+") @@ -75,8 +75,8 @@ def read_data(bot: botPy.Bot) -> bool: if not bot.args["py"]: return False - py_print("reddit_data.json not found, creating new from preset...") - with open(os.path.join(DATA_PATH, "reddit_data_preset.json", "r")) as f: + py_print("re_data.json not found, creating new from preset...") + with open(os.path.join(DATA_PATH, "re_data_preset.json", "r")) as f: data_preset_json = json.load(f) data_preset_json[botPy.BK_WEEKLY].pop("EXAMPLE VALUE", None) diff --git a/src/python/main.py b/src/python/main.py index 9285bf8..72f32b9 100644 --- a/src/python/main.py +++ b/src/python/main.py @@ -40,7 +40,7 @@ async def main(): rd = data.read_data(bot) if data_retries == 5 and not rd: - raise Exception("Couldn't read reddit_data.json: File doesn't exist") + raise Exception("Couldn't read re_data.json: File doesn't exist") py_print("Successfully read data!") diff --git a/src/re_cmds/add.rs b/src/re_cmds/add.rs new file mode 100644 index 0000000..689fad3 --- /dev/null +++ b/src/re_cmds/add.rs @@ -0,0 +1,68 @@ +use serde_json::json; + +use crate::data::get_mutex_data; +use crate::messages::send_msg; +use crate::{data, websocket, Context, Error, BK_WEEK}; +use crate::re_cmds::generic_fns::{is_bk_mod, to_shorturl}; +use crate::lang; + +#[poise::command( + slash_command, + prefix_command, + rename = "re_addpost", + category = "re", + required_bot_permissions = "SEND_MESSAGES | VIEW_CHANNEL" +)] +/// Fetches a post from Reddit and adds it to the database. +pub async fn cmd( + ctx: Context<'_>, + #[description = "The post URL."] url: String, + #[description = "Wether to approve it after adding it"] approve: Option +) -> Result<(), Error> +{ + if !is_bk_mod(ctx.data().bk_mods.clone(), ctx.author().id.get()) { + send_msg(ctx, lang!("re_permdeny_bk_mod"), false, false).await; + return Ok(()); + } + + let shorturl_u = to_shorturl(&url); + let shorturl = &shorturl_u.unwrap_or(url.clone()); + + data::update_re_data(ctx.data()).await; + let reddit_data = get_mutex_data(&ctx.data().reddit_data).await?; + + if let Some(bk_week) = reddit_data.get(BK_WEEK) { + let a = approve.unwrap_or(false); + let r = websocket::send_cmd_json("add_post_url", Some(json!([&shorturl, a, true]))).await.unwrap(); + + if !r["value"].as_bool().unwrap() { + send_msg( + ctx, + r#"Unknown error! + Error trace: `bk_week_cmds.rs -> bk_week_add() -> Unknown error`. + Common reasons: The URL provided was likely invalid or 403: forbidden (e.g a private subreddit)."#.to_string(), + true, + true + ).await; + return Ok(()); + } + + if let Some(post) = bk_week.get(shorturl) { + if post.get("removed").is_some() { + send_msg(ctx, lang!("re_unremove_post_success", url), true, true).await; + } + else { + send_msg(ctx, lang!("re_update_post_success", url), true, true).await; + } + } + else { + send_msg(ctx, lang!("re_add_post_success", &shorturl), true, true).await; + } + + if a { + send_msg(ctx, lang!("re_also_approved"), true, true).await; + } + } + + return Ok(()); +} \ No newline at end of file diff --git a/src/re_cmds/admin_bind.rs b/src/re_cmds/admin_bind.rs new file mode 100644 index 0000000..9336004 --- /dev/null +++ b/src/re_cmds/admin_bind.rs @@ -0,0 +1,28 @@ +use crate::{data::dc_bind_bk, lang, messages::send_msg, Context, Error}; + +#[poise::command( + slash_command, + prefix_command, + rename = "admin_re_bindchannel", + category = "admin", + default_member_permissions = "ADMINISTRATOR", + guild_only, + required_bot_permissions = "SEND_MESSAGES | VIEW_CHANNEL" +)] +/// Sets the channel where the bot will dump all Reddit data upon using /re_updateDiscord. +pub async fn cmd( + ctx: Context<'_> +) -> Result<(), Error> +{ + let c_id = ctx.channel_id().into(); + let r = dc_bind_bk(ctx.data(), ctx.guild_id().unwrap().into(), c_id).await; + + if r.is_ok() { + send_msg(ctx, lang!("bound_dc_channel", c_id), true, true).await; + } + else { + send_msg(ctx, lang!("dc_404"), false, false).await; + } + + return Ok(()); +} \ No newline at end of file diff --git a/src/re_cmds/approve.rs b/src/re_cmds/approve.rs new file mode 100644 index 0000000..ed3eeaf --- /dev/null +++ b/src/re_cmds/approve.rs @@ -0,0 +1,58 @@ +use serde_json::{json, Value}; + +use crate::{data::{self, get_mutex_data}, lang, messages::send_msg, re_cmds::generic_fns::is_bk_mod, websocket, Context, Error, BK_WEEK}; + +use super::generic_fns::send_embed_for_removed; + +#[poise::command( + slash_command, + prefix_command, + rename = "re_approvepost", + category = "re", + required_bot_permissions = "SEND_MESSAGES | VIEW_CHANNEL" +)] +/// Approves a post in the database. +pub async fn cmd( + ctx: Context<'_>, + #[description = "The post URL."] url: String, + #[description = "Wether to approve or disapprove the post"] disapprove: Option +) -> Result<(), Error> +{ + if !is_bk_mod(ctx.data().bk_mods.clone(), ctx.author().id.get()) { + send_msg(ctx, lang!("re_permdeny_bk_mod"), false, false).await; + return Ok(()); + } + + data::update_re_data(ctx.data()).await; + let reddit_data = get_mutex_data(&ctx.data().reddit_data).await?; + + approve_cmd(ctx, &url, &reddit_data, !disapprove.unwrap_or(false)).await; + + return Ok(()); +} + + +async fn approve_cmd(ctx: Context<'_>, url: &str, reddit_data: &Value, approve: bool) { + if let Some(post) = reddit_data.get(BK_WEEK).unwrap().get(url) { + if post.get("removed").is_some() { + send_embed_for_removed(ctx, url, post).await; + return; + } + + let r = websocket::send_cmd_json("set_approve_post", Some(json!([approve, &url]))).await.unwrap(); + if r.get("value").is_some() { + if approve { + send_msg(ctx, lang!("re_approve_post_success"), true, true).await; + } + else { + send_msg(ctx, lang!("re_disapprove_post_success"), true, true).await; + } + } + else { + send_msg(ctx, lang!("re_err_trace", "`re_cmds -> approve.rs -> cmd() -> unwrap websocket result error`"), true, true).await; + } + } + else { + send_msg(ctx, lang!("re_404"), false, false).await; + } +} \ No newline at end of file diff --git a/src/re_cmds/generic_fns.rs b/src/re_cmds/generic_fns.rs new file mode 100644 index 0000000..e5a6b2d --- /dev/null +++ b/src/re_cmds/generic_fns.rs @@ -0,0 +1,34 @@ +use regex::Regex; +use serde_json::Value; + +use crate::{messages::{make_post_embed, make_removed_embed, send_embed}, Context, Error}; + +pub fn is_bk_mod(mod_list: Vec, uid: u64) -> bool { + return mod_list.contains(&uid); +} + +pub fn to_shorturl(url: &str) -> Result { + let re = Regex::new(r"comments/([a-zA-Z0-9]+)").unwrap(); + + if let Some(caps) = re.captures(url) { + let post_id = &caps[1]; + let short_url = format!("https://redd.it/{}", post_id); + return Ok(short_url); + } + + return Err("Invalid URL"); +} + + +pub async fn send_embed_for_post(ctx: Context<'_>, post: Value, url: &str) -> Result<(), Error> { + send_embed(ctx, make_post_embed(&post, url, true), true).await; + Ok(()) +} + +pub async fn send_embed_for_removed(ctx: Context<'_>, url: &str, post: &Value) { + send_embed( + ctx, + make_removed_embed(post, url, true), + true + ).await; +} \ No newline at end of file diff --git a/src/re_cmds/get.rs b/src/re_cmds/get.rs new file mode 100644 index 0000000..54f34ce --- /dev/null +++ b/src/re_cmds/get.rs @@ -0,0 +1,50 @@ +use serde_json::Value; + +use crate::{data::{self, get_mutex_data}, lang, messages::send_msg, re_cmds::generic_fns::send_embed_for_post, rs_println, Context, Error, BK_WEEK}; + +use super::generic_fns::send_embed_for_removed; + +#[poise::command( + slash_command, + prefix_command, + rename = "re_getpost", + category = "re", + required_bot_permissions = "SEND_MESSAGES | VIEW_CHANNEL | EMBED_LINKS" +)] +/// Fetches and shows a single post, just for you. The post has to be within the database. +pub async fn cmd( + ctx: Context<'_>, + #[description = "The post URL."] url: String +) -> Result<(), Error> +{ + data::update_re_data(ctx.data()).await; + + let reddit_data = get_mutex_data(&ctx.data().reddit_data).await?; + + if let Some(post) = get_post_from_data(ctx, &reddit_data, &url).await? { + send_embed_for_post(ctx, post, &url).await?; + } + + return Ok(()); +} + + +async fn get_post_from_data(ctx: Context<'_>, reddit_data: &Value, url: &str) -> Result, Error> { + if let Some(bk_week) = reddit_data.get(BK_WEEK) { + if let Some(post) = bk_week.get(url) { + if post.get("removed").is_some() { + send_embed_for_removed(ctx, url, post).await; + return Ok(None); + } + return Ok(Some(post.clone())); + } + else { + send_msg(ctx, lang!("re_404", url), true, true).await; + } + } + else { + send_msg(ctx, lang!("re_data_corrupted", url), true, true).await; + rs_println!("{}", serde_json::to_string_pretty(reddit_data)?); + } + return Ok(None); +} \ No newline at end of file diff --git a/src/re_cmds/remove.rs b/src/re_cmds/remove.rs new file mode 100644 index 0000000..f19dfb0 --- /dev/null +++ b/src/re_cmds/remove.rs @@ -0,0 +1,40 @@ +use serde_json::json; + +use crate::{lang, messages::send_msg, re_cmds::generic_fns::is_bk_mod, websocket::send_cmd_json, Context, Error}; + +#[poise::command( + slash_command, + prefix_command, + rename = "re_removepost", + category = "re", + required_bot_permissions = "SEND_MESSAGES | VIEW_CHANNEL" +)] +/// Removes a post from the database. +pub async fn cmd( + ctx: Context<'_>, + #[description = "The post URL."] url: String, + #[description = "The reason of the removal."] reason: Option +) -> Result<(), Error> +{ + if !is_bk_mod(ctx.data().bk_mods.clone(), ctx.author().id.get()) { + send_msg(ctx, lang!("re_permdeny_bk_mod"), false, false).await; + return Ok(()); + } + + let auth = &ctx.author().name; + let r = send_cmd_json("remove_post_url", Some(json!([&url, &auth, &reason]))).await.unwrap(); + + if r["value"].as_bool().unwrap() { + send_msg( + ctx, + lang!("re_remove_post_success"), + true, + true + ).await; + } + else { + send_msg(ctx, lang!("re_404"), false, false).await; + } + + return Ok(()); +} \ No newline at end of file diff --git a/src/re_cmds/top.rs b/src/re_cmds/top.rs new file mode 100644 index 0000000..e723d11 --- /dev/null +++ b/src/re_cmds/top.rs @@ -0,0 +1,77 @@ +use std::collections::HashMap; + +use crate::{data::get_mutex_data, re_cmds::generic_fns::send_embed_for_post, Context, Error, BK_WEEK}; + +#[derive(poise::ChoiceParameter, PartialEq)] +enum TopCategory { + Upvotes, + ModVotes, + Oldest, + Newest +} + + +#[poise::command( + slash_command, + prefix_command, + rename = "re_topposts", + category = "re", + required_bot_permissions = "SEND_MESSAGES | VIEW_CHANNEL | EMBED_LINKS" +)] +/// Shows the top N (up to 10, default is 3) posts within a certain category, such as upvotes. +pub async fn cmd( + ctx: Context<'_>, + #[description = "The sorting criteria, such as upvotes."] + category: TopCategory, + #[description = "The amount of posts to show (max 10, default is 3)."] + #[min = 1] + #[max = 10] + amount: Option +) -> Result<(), Error> +{ + let mut all: HashMap<&str, i32> = HashMap::new(); + let posts = &get_mutex_data(&ctx.data().reddit_data).await?[BK_WEEK]; + let posts_u = posts.as_object().unwrap(); + + for (url, dat) in posts_u { + if dat.get("removed").is_some() { continue; } + + let val: i32 = match category { + TopCategory::Upvotes => dat["post_data"]["upvotes"].as_i64().unwrap() as i32, + TopCategory::ModVotes => dat["votes"]["mod_voters"].as_array().unwrap().len() as i32, + TopCategory::Oldest + | TopCategory::Newest => dat["post_data"]["date_unix"].as_i64().unwrap() as i32, + }; + + all.insert(url, val); + } + + let amount_u = amount.unwrap_or(3); + let amount_clamped = amount_u.clamp(1, 10); + + let top = + if category != TopCategory::Oldest + { largest_n (&all, amount_clamped as usize) } + else { smallest_n(&all, amount_clamped as usize) }; + + for post in top { + let url = post.0; + let _ = send_embed_for_post(ctx, posts_u[url].clone(), url).await; + } + + return Ok(()); +} + + +fn largest_n<'a>(map: &'a HashMap<&'a str, i32>, n: usize) -> Vec<(&'a str, i32)> { + let mut vec: Vec<_> = map.iter().collect(); + vec.sort_unstable_by(|a, b| b.1.cmp(a.1)); + vec.into_iter().take(n).map(|(&k, &v)| (k, v)).collect() +} + + +fn smallest_n<'a>(map: &'a HashMap<&'a str, i32>, n: usize) -> Vec<(&'a str, i32)> { + let mut vec: Vec<_> = map.iter().collect(); + vec.sort_unstable_by(|a, b| a.1.cmp(b.1)); + vec.into_iter().take(n).map(|(&k, &v)| (k, v)).collect() +} \ No newline at end of file diff --git a/src/re_cmds/update.rs b/src/re_cmds/update.rs new file mode 100644 index 0000000..da8af2a --- /dev/null +++ b/src/re_cmds/update.rs @@ -0,0 +1,305 @@ +use std::time::{SystemTime, UNIX_EPOCH}; + +use poise::{serenity_prelude::{ChannelId, EditMessage, GetMessages, Http, Message, MessageId, UserId}, ReplyHandle}; +use serde_json::{json, Map, Value}; + +use crate::{data::{self, get_mutex_data}, lang, messages::{edit_reply, embed_from_options, http_send_embed, make_post_embed, make_removed_embed, send_msg}, websocket::send_cmd_json, Context, Error, BK_WEEK}; + +#[poise::command( + slash_command, + prefix_command, + rename = "re_updatediscord", + category = "re", + guild_only, + guild_cooldown = 120, + required_bot_permissions = "SEND_MESSAGES | VIEW_CHANNEL | READ_MESSAGE_HISTORY | EMBED_LINKS" +)] +/// Updates the binded Discord channel with the bot's current Reddit data. +pub async fn cmd( + ctx: Context<'_>, + #[description = "Only adds new posts, leaves everything else unchanged."] + only_add: Option, + #[description = "The max age of a post (in days). Any post older than this will be removed. (0 is infinite.)"] + #[min = 0] + #[max = 65535] + max_age: Option +) -> Result<(), Error> +{ + let http = ctx.http(); + + let mut p_text = "`/bk_week_update`:".to_string(); + + let progress = send_msg(ctx, p_text.clone(), true, true).await.unwrap(); + p_text = update_progress(ctx, progress.clone(), p_text, "\nFetching new posts & updating data file...".to_string()).await; + + let max_age_u = max_age.unwrap_or(8); + let max_age_secs = max_age_u as u64 * (60 * 60 * 24); + + send_cmd_json("add_new_posts", Some(json!([max_age_secs]))).await; + data::update_re_data(ctx.data()).await; + let r_data = get_mutex_data(&ctx.data().reddit_data).await?; + + let c_id_u = get_c_id(ctx).await; + + if c_id_u.is_none() { + send_msg(ctx, "Could not find bk_week_channel in data!\nHint: Run (or tell an admin to run) `/bk_admin_bind` in a (preferably read-only) channel.".to_string(), true, true).await; + return Ok(()); + } + + let c_id = c_id_u.unwrap(); + + // Reading messages + p_text = update_progress(ctx, progress.clone(), p_text.clone(), format!("✅\nReading messages in <#{}>...", c_id)).await; + let msgs = read_msgs(http, ctx.framework().bot_id, c_id).await; + + // Parsing messages to JSON + p_text = update_progress(ctx, progress.clone(), p_text.clone(), "✅\nParsing messages to JSON...".to_string()).await; + let msgs_json = msgs_to_json(msgs, &r_data, max_age_secs).await; + + // Adding new posts + p_text = update_progress(ctx, progress.clone(), p_text.clone(), "✅\nAdding new posts...".to_string()).await; + let weekly_art = r_data[BK_WEEK].as_object().unwrap(); + add_posts(http, c_id, weekly_art, &msgs_json, max_age_secs).await; + + // Stop if only_add + if only_add.unwrap_or(false) { + send_msg(ctx, "`/bk_week_update`\n## Done!".to_string(), true, true).await; + update_progress(ctx, progress.clone(), p_text, "✅\n## Done!".to_string()).await; + return Ok(()); + } + + // Editing updated posts + p_text = update_progress(ctx, progress.clone(), p_text.clone(), "✅\nEditing updated posts...".to_string()).await; + edit_posts(http, c_id, weekly_art, &msgs_json).await; + + // Removing removed posts + p_text = update_progress(ctx, progress.clone(), p_text.clone(), "✅\nRemoving removed posts...".to_string()).await; + remove_posts(http, c_id, weekly_art, &msgs_json).await; + + // Removing old posts + if max_age_u > 0 { + p_text = update_progress(ctx, progress.clone(), p_text.clone(), format!("✅\nRemoving old posts (threshold: {}d)...", max_age_u)).await; + remove_old(http, c_id, &msgs_json).await; + send_cmd_json("remove_old_posts", Some(json!([max_age_secs]))).await; + } + + // Removing duplicate posts + p_text = update_progress(ctx, progress.clone(), p_text.clone(), "✅\nRemoving duplicate posts...".to_string()).await; + remove_dupes(http, c_id, &msgs_json).await; + + // Done + update_progress(ctx, progress.clone(), p_text, "✅\n## Done!".to_string()).await; + send_msg(ctx, "`/bk_week_update`\n## Done!".to_string(), true, true).await; + + return Ok(()); +} + + +async fn update_progress(ctx: Context<'_>, p: ReplyHandle<'_>, t: String, added_t: String) -> String { + let p_text = format!("{} {}", t, added_t); + + edit_reply(ctx, p, p_text.clone()).await; + return p_text; +} + + +async fn get_c_id(ctx: Context<'_>) -> Option { + if !data::dc_contains_server(ctx.data(), ctx.guild_id().unwrap().into()).await { + send_msg(ctx, lang!("dc_404"), false, false).await; + return None; + } + + let d = get_mutex_data(&ctx.data().discord_data).await.unwrap(); + let c_id_u = + d["servers"] + [ctx.guild_id().unwrap().to_string()] + ["bk_week_channel"].as_u64().unwrap(); + + let c_id = ChannelId::new(c_id_u); + + return Some(c_id); +} + + +async fn read_msgs(http: &Http, bot_id: UserId, c_id: ChannelId) -> Vec { + let b = GetMessages::new().limit(100); + let mut msgs = c_id.messages(http, b).await.unwrap(); + msgs.retain(|item| item.author.id == bot_id); + + let mut last_msg: Option = msgs.last().cloned(); + + while last_msg.is_some() { + let new_b = GetMessages::new().limit(100).before(last_msg.clone().unwrap()); + let new_msgs = c_id.messages(http, new_b).await.unwrap(); + + last_msg = new_msgs.last().cloned(); + + if new_msgs.is_empty() { + break; + } + + let filtered_msgs: Vec = new_msgs + .into_iter() + .filter(|item| item.author.id == bot_id) + .collect(); + + msgs.extend(filtered_msgs); + } + + return msgs; +} + + +async fn msgs_to_json(msgs: Vec, reddit_data: &Value, max_age: u64) -> Value { + let mut msgs_json: Value = json!({"no_change": {}, "updated": {}, "removed": {}, "duplicates": {}, "old": {}}); + let now = SystemTime::now() + .duration_since(UNIX_EPOCH) + .expect("Time went backwards") + .as_secs(); + + for msg in msgs { + if msg.embeds.is_empty() { continue; } + if msg.embeds[0].url.is_none() { continue; } + + let url = msg.embeds[0].url.clone().unwrap(); + + // duplicates + if ["no_change", "updated", "removed", "old"] + .iter() + .any(|key| msgs_json[key].as_object().unwrap().contains_key(&url)) + { + let dupes_mut = msgs_json["duplicates"].as_object_mut().unwrap(); + if !dupes_mut.contains_key(&url) { + dupes_mut.insert(url.clone(), json!(msg.id.get())); + } + continue; + } + + let msg_desc = &msg.embeds[0].description.clone().unwrap(); + let msg_lines = msg_desc.split("\n"); + let msg_last_len = msg_lines.clone().last().unwrap().len(); + + if msg_last_len < 13 { continue; } + + let msg_json_str = &msg_lines.clone().last().unwrap()[9..msg_last_len - 3]; + + let msg_json = serde_json::from_str(msg_json_str); + if msg_json.is_err() { continue; } + + let mut u_json: Value = msg_json.unwrap(); + let re_url = &reddit_data[BK_WEEK][&url]; + + let post_date = re_url["post_data"]["date_unix"].as_u64().unwrap_or(0); + + // old + if now - post_date > max_age { + if let Some(obj) = msgs_json["old"].as_object_mut() { + obj.insert(url.clone(), json!(msg.id.get())); + continue; + } + } + + // removed + if re_url.get("removed").is_some() { + if u_json.get("removed").is_some() { + // no change + if let Some(obj) = msgs_json["no_change"].as_object_mut() { + obj.insert(url.clone(), json!(msg.id.get())); + continue; + } + } + + // removed + if let Some(obj) = msgs_json["removed"].as_object_mut() { + obj.insert(url.clone(), json!(msg.id.get())); + continue; + } + } + + // updated + if u_json["added"] != re_url["added"] + || u_json["approved"] != re_url["approved"] + || u_json["post_data"]["upvotes"] != re_url["post_data"]["upvotes"] + || u_json["votes"]["mod_voters"] != re_url["votes"]["mod_voters"] + { + u_json.as_object_mut().unwrap().insert("msg_id".to_string(), Value::String(msg.id.clone().to_string())); + + if let Some(obj) = msgs_json["updated"].as_object_mut() { + obj.insert(url.clone(), json!(msg.id.get())); + continue; + } + } + + // no change + if let Some(obj) = msgs_json["no_change"].as_object_mut() { + obj.insert(url.clone(), json!(msg.id.get())); + } + } + + return msgs_json; +} + + +async fn add_posts(http: &Http, c_id: ChannelId, r_data: &Map, msgs_json: &Value, max_age: u64) { + let now = SystemTime::now() + .duration_since(UNIX_EPOCH) + .expect("Time went backwards") + .as_secs(); + + for url in r_data.keys() { + if ["no_change", "updated", "removed", "old", "duplicates"] + .iter() + .any(|key| msgs_json[key].as_object().unwrap().contains_key(url)) + { continue; } + + let post_date = r_data[url]["post_data"]["date_unix"].as_u64().unwrap(); + if now - post_date > max_age { continue; } + + if r_data[url].get("removed").is_some() { + http_send_embed(http, c_id, make_removed_embed(&r_data[url], url, false)).await; + continue; + } + + http_send_embed(http, c_id, make_post_embed(&r_data[url], url, false)).await; + } +} + + +async fn edit_posts(http: &Http, c_id: ChannelId, r_data: &Map, msgs_json: &Value) { + for (url, msg_id) in msgs_json["updated"].as_object().unwrap() { + let mut msg = http.get_message(c_id, MessageId::new(msg_id.as_u64().unwrap())).await.unwrap(); + let r = EditMessage::new() + .embeds(vec![embed_from_options(make_post_embed(&r_data[url], url, false))]); + + let _ = msg.edit(http, r).await; + } +} + + +async fn remove_posts(http: &Http, c_id: ChannelId, r_data: &Map, msgs_json: &Value) { + for (url, msg_id) in msgs_json["removed"].as_object().unwrap() { + let mut msg = http.get_message(c_id, MessageId::new(msg_id.as_u64().unwrap())).await.unwrap(); + let r = EditMessage::new() + .embeds(vec![embed_from_options(make_removed_embed(&r_data[url], url, false))]); + + let _ = msg.edit(http, r).await; + } +} + + +async fn remove_old(http: &Http, c_id: ChannelId, msgs_json: &Value) { + for (_url, msg_id) in msgs_json["old"].as_object().unwrap() { + let msg = http.get_message(c_id, MessageId::new(msg_id.as_u64().unwrap())).await.unwrap(); + let _ = msg.delete(http).await; + } +} + + +async fn remove_dupes(http: &Http, c_id: ChannelId, msgs_json: &Value) { + for (_url, msgs) in msgs_json["duplicates"].as_object().unwrap() { + for msg_id in msgs.as_array().unwrap() { + let msg = http.get_message(c_id, MessageId::new(msg_id.as_u64().unwrap())).await.unwrap(); + let _ = msg.delete(http).await; + } + } +} \ No newline at end of file diff --git a/src/re_cmds/vote.rs b/src/re_cmds/vote.rs new file mode 100644 index 0000000..b8e5413 --- /dev/null +++ b/src/re_cmds/vote.rs @@ -0,0 +1,67 @@ +use serde_json::json; + +use crate::{data::{self, get_mutex_data}, lang, messages::send_msg, re_cmds::generic_fns::{is_bk_mod, send_embed_for_removed}, websocket::send_cmd_json, Context, Error, BK_WEEK}; + +#[poise::command( + slash_command, + prefix_command, + rename = "re_vote", + category = "re", + required_bot_permissions = "SEND_MESSAGES | VIEW_CHANNEL" +)] +/// Adds/removes a vote from a post. These votes are not tied to Reddit upvotes. +pub async fn cmd( + ctx: Context<'_>, + #[description = "The post URL."] url: String, + #[description = "Wether to undo your vote or not"] un_vote: Option +) -> Result<(), Error> +{ + data::update_re_data(ctx.data()).await; + let uid = ctx.author().id.get(); + let re_data = get_mutex_data(&ctx.data().reddit_data).await?; + let post_data = re_data[BK_WEEK].clone(); + let unw_vote = un_vote.unwrap_or(false); + + if post_data.get(&url).is_none() { + send_msg(ctx, lang!("re_404"), false, false).await; + return Ok(()); + } + if post_data[&url].get("removed").is_some() { + send_embed_for_removed(ctx, &url, &post_data[&url]).await; + return Ok(()); + } + + let url_data = &post_data[&url]; + + let is_mod = is_bk_mod(ctx.data().bk_mods.clone(), ctx.author().id.get()); + let voters_dc = url_data["votes"]["voters_dc"].as_array().unwrap(); + let mod_voters = url_data["votes"]["mod_voters"].as_array().unwrap(); + let voters = if is_mod { mod_voters } else { voters_dc }; + + if voters.contains(&json!(uid)) && !unw_vote { + send_msg(ctx, lang!("re_already_voted"), true, true).await; + return Ok(()); + } + else if !voters.contains(&json!(uid)) && unw_vote { + send_msg(ctx, lang!("re_havent_voted_remove_vote"), true, true).await; + return Ok(()); + } + + let r = send_cmd_json("set_vote_post", Some(json!([url, uid, is_mod, true, unw_vote]))).await.unwrap(); + let unw_r = r["value"].as_bool().unwrap(); + + if unw_r && !unw_vote && is_mod { + send_msg(ctx, lang!("re_vote_success_mod"), true, true).await; + } + else if unw_r && !unw_vote && !is_mod { + send_msg(ctx, lang!("re_vote_success"), true, true).await; + } + else if unw_r && unw_vote { + send_msg(ctx, lang!("re_vote_remove_success"), true, true).await; + } + else { + send_msg(ctx, lang!("re_vote_err"), true, true).await; + } + + return Ok(()); +} \ No newline at end of file diff --git a/src/schedule.rs b/src/schedule.rs new file mode 100644 index 0000000..683fd7b --- /dev/null +++ b/src/schedule.rs @@ -0,0 +1,35 @@ +use std::future::Future; +use std::pin::Pin; +use std::time::Duration; + +use tokio::task::JoinHandle; +use tokio::time; + +use crate::{lang, rs_println}; + + +pub type Schedule = (Duration, fn() -> Pin + Send>>); + + +pub async fn run_schedule Pin + Send>>>(d: Duration, f: F) { + let mut ticker = time::interval(d); + loop { + ticker.tick().await; + f().await; + } +} + + +pub async fn run_schedules(schedules: Vec) { + let mut handles: Vec> = vec![]; + + rs_println!("{}", lang!("starting_schedules")); + for (d, f) in schedules { + let handle = tokio::spawn(run_schedule(d, f)); + handles.push(handle); + } + + for handle in handles { + let _ = handle.await; + } +} \ No newline at end of file diff --git a/src/websocket.rs b/src/websocket.rs index eba23a3..9a03bcd 100644 --- a/src/websocket.rs +++ b/src/websocket.rs @@ -9,7 +9,7 @@ use std::sync::Arc; use serde_json::{Value, json}; use crate::messages::send_dm; -use crate::rs_println; +use crate::{lang, rs_println}; use crate::Args; type Sender = Arc, tungstenite::Message>>>>; @@ -66,11 +66,11 @@ pub async fn send_cmd_json(func_name: &str, func_args: Option) -> Option< let r = receive_response().await; if !["respond_mentions"].contains(&func_name) || ::parse().dev { - rs_println!("Received from Python: [RESPONSE] {:?}", r); + rs_println!("{}", lang!("python_socket_response", format!("{:?}", r))); } if r.is_none() { - rs_println!("--- Response from Python is None!"); + rs_println!("{}", lang!("python_socket_null")); } return r; @@ -99,10 +99,10 @@ async fn receive_response() -> Option { pub async fn start(args: Args, owners: Vec) { - rs_println!("Starting local websocket..."); + rs_println!("{}", lang!("starting_socket")); let ip = format!("127.0.0.1:{}", args.port); let listener = TcpListener::bind(&ip).await.unwrap(); - rs_println!("WebSocket server running on ws://{}", ip); + rs_println!("{}", lang!("started_socket", ip)); tokio::spawn(handle_connections(listener, args, owners)); } @@ -129,18 +129,18 @@ async fn handle_connections(listener: TcpListener, args: Args, owners: Vec) async fn handle_message(msg: tungstenite::protocol::Message, args: Args, owners: Vec) { match msg { tungstenite::Message::Text(text) => { - rs_println!("Received from Python: {}", text); + rs_println!("{}", lang!("socket_received_python", text.clone())); if let Some(stripped) = text.strip_prefix("json:") { let t_json: Value = serde_json::from_str(stripped).unwrap(); if t_json.get("error").is_some() { - send_dm("Unknown internal Python error occurred: Websocket response error.".to_string(), args, owners).await; + send_dm(lang!("python_socket_err"), args, owners).await; } } unsafe { if !REPLY_HELLO { - send_msg("[Connection test] Hello from Rust!").await; + send_msg(&lang!("socket_rust_connection_test")).await; REPLY_HELLO = true; HAS_CONNECTED = true; } @@ -148,12 +148,12 @@ async fn handle_message(msg: tungstenite::protocol::Message, args: Args, owners: } tungstenite::Message::Binary(bytes) => { if args.dev && !args.noping { - rs_println!("[Binary] from Python: {:?}", bytes); + rs_println!("{}", lang!("python_socket_binary_response", format!("{:?}", bytes))); } } _ => { if args.dev && !args.noping { - rs_println!("Received from Python: [UNKNOWN / OTHER]"); + rs_println!("{}", lang!("python_socket_unknown_response")); } } } From 4f18e942a26368d5e806b5750a686fa2212966ae Mon Sep 17 00:00:00 2001 From: Byte Dice Date: Sun, 6 Apr 2025 16:14:47 +0200 Subject: [PATCH 02/39] Update TODO.md & added new language --- TODO.md | 2 +- data/lang/en.json | 2 +- data/lang/miserable.json | 62 ++++++++++++++++++++++++++++++++++++++++ 3 files changed, 64 insertions(+), 2 deletions(-) create mode 100644 data/lang/miserable.json diff --git a/TODO.md b/TODO.md index a152e3f..6f458fe 100644 --- a/TODO.md +++ b/TODO.md @@ -11,7 +11,7 @@ - [ ] Allow disabling bk_week - - [ ] Language files + - [ ] Allow updating the data autonomously and via manual commands. diff --git a/data/lang/en.json b/data/lang/en.json index 59749e6..fac98e4 100644 --- a/data/lang/en.json +++ b/data/lang/en.json @@ -22,7 +22,7 @@ "help_text_removed": "Help text not found. Someone deleted it. :(", "invalid_confirm_shutdown": "Failed to shut down: Invalid confirmation.", "invalid_os": "Man what kinda OS do you have? Neither unix or windows, what the hell!? I can't process this anymore, you're too weird!", - "lang_data_404": "file for language \"{}\" not found!", + "lang_data_404": "File for language \"{0}\" not found!", "lang_load_success": "Successfully loaded the english language file!", "mandatory_response": "Mandatory response message, please ignore.", "python_err": "Unknown internal Python Error: {0}", diff --git a/data/lang/miserable.json b/data/lang/miserable.json new file mode 100644 index 0000000..ac287f5 --- /dev/null +++ b/data/lang/miserable.json @@ -0,0 +1,62 @@ +{ + "8-ball_answer": "?: {0}\n!: {1}", + "add_to_data": "Something something selling data something.", + "bound_dc_channel": "Random numbers, go! `{0}`", + "cfg_data_404": "I'm not telling you the error but I think it's something about your pc having malware :) ", + "cfg_data_wipe": "[WIPE MY ASS] file.file_extention something.", + "cmd_404": "Great, you executed `{0}`!", + "corrupted_data": "p4Oad3!an5!#####", + "couldnt_shorturl": "ShortURL: https://ILoveBurritos.com/yourRedditURL.", + "creating_data_file": "{0} (not taking care of the malware).", + "data_post_embed": "## Figure it out:\n **Figure it out:** ||`{0:>6}`||\n **Figure it out:** ||`{1:>6}`||\n **Figure it out:** `{2}`\n **Figure it out:** ||<{3}>||\n\n ## Figure it out:\n **Figure it out:** `{{ human: {4}, bot: {5} }}`\n **Figure it out:** `{{ human: {6}, bot: [not implemented] }}`", + "data_post_removed_embed": "## Approved by `{0}`\n**Reason:** {1}\nURL: ||<{2}>||\n\nJSON: ||`{3}`||", + "data_save_complete": "Discarding data... Done!\nShutting down...", + "data_save_progress": "Discarding data...", + "dc_404": "Everything went well!", + "dc_bot_started": "Couldn't start bot \"{0}\" with id {1}: Someone else is currently running the same bot.", + "dc_bot_starting": "Starting bot...", + "dc_data_404": "I'm not telling you the error but I think it's something about your pc having malware :) ", + "dc_data_wipe": "[WIPE MY ASS] file.file_extention something", + "default_embed_desc": "The person who created this is stupid for not adding a description.", + "err_read_py_file": "Successfully read Python file: (Path) {0}", + "help_text_removed": "We hate providing help to you specifically!", + "invalid_confirm_shutdown": "Discarding data... Done!\nShutting down...", + "invalid_os": "Man what kinda OS do you have? Neither unix or windows, what the hell!? I can't process this anymore, you're too weird!", + "lang_data_404": "Successfully loaded language \"{0}\"!", + "lang_load_success": "Failed to load the english language file!", + "mandatory_response": "Annoying message you can't get rid of.", + "python_err": "Looking good! {0}\nIgnore what's above me.", + "python_socket_binary_response": "Corrupted data found: {0}", + "python_socket_err": "Websocket response test successfully completed!", + "python_socket_null": "Received from Python: [UNKNOWN / OTHER]", + "python_socket_response": "Received from alien contact: {0}", + "python_socket_unknown_response": "Lettuce", + "re_404": "\"<{0}>\" is a funny post URL, am I right?", + "re_add_post_success": "Removed post with URL \"<{0}>\"!", + "re_already_voted": "Successfully voted!", + "re_also_approved": "Nevermind, that post sucks.", + "re_approved_post_success": "Successfully added the post!", + "re_data_404": "I'm not telling you the error but I think it's something about your pc having malware :) ", + "re_data_corrupted": "\"<{0}>\" is a funny post URL, am I right?", + "re_data_wipe": "[WIPE MY ASS] file.file_extention something", + "re_disapproved_post_success": "Successfully removed the post!", + "re_err_trace": "I got this random file path thingy, here you go: {0}.", + "re_havent_voted_remove_vote": "Successfully voted!", + "re_permdeny_bk_mod": "Welcome to the {0} team!", + "re_remove_post_success": "Successfully", + "re_unremove_post_success": "Successfully removed post with URL \"<{0}>\"!", + "re_update_post_success": "\"<{0}>\"!", + "re_vote_err": "Successfully voted!", + "re_vote_remove_success": "Successfully voted!", + "re_vote_success_mod": "Successfully voted!", + "re_vote_success": "Successfully voted (as moderator vote)!", + "reload_config_python_fail": "Successfully reloaded the configs!", + "reload_config_success": "I didn't update them, but here you go anyways:\n```\n{0}\n```", + "shorturl": "Invalid shortURL, try again. Here's a random one though: <{0}>", + "socket_received_python": "Sent to Python: {0}", + "socket_rust_connection_test": "------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------", + "started_socket": "Your IPv4 address: {0}", + "starting_python_program": "Running Rust program...", + "starting_schedules": "Starting spyware...", + "starting_socket": "Starting global websocket..." +} \ No newline at end of file From 149a22ff7fd35e9fbed7f02258c8c82737cc90f3 Mon Sep 17 00:00:00 2001 From: ByteDice Date: Sat, 12 Apr 2025 22:32:50 +0200 Subject: [PATCH 03/39] removed different language logs & updated language key names --- data/lang/en.json | 98 +++++++++++++++------------------------ data/lang/miserable.json | 62 ------------------------- src/cmds.rs | 33 +++++++------ src/data.rs | 21 ++++----- src/events.rs | 11 ++--- src/main.rs | 5 +- src/messages.rs | 7 ++- src/python.rs | 6 +-- src/re_cmds/add.rs | 10 ++-- src/re_cmds/admin_bind.rs | 4 +- src/re_cmds/approve.rs | 10 ++-- src/re_cmds/get.rs | 4 +- src/re_cmds/remove.rs | 6 +-- src/re_cmds/update.rs | 2 +- src/re_cmds/vote.rs | 14 +++--- src/schedule.rs | 2 +- src/websocket.rs | 18 +++---- 17 files changed, 113 insertions(+), 200 deletions(-) delete mode 100644 data/lang/miserable.json diff --git a/data/lang/en.json b/data/lang/en.json index fac98e4..c8e2c85 100644 --- a/data/lang/en.json +++ b/data/lang/en.json @@ -1,62 +1,40 @@ { - "8-ball_answer": "Q: {0}\nA: {1}", - "add_to_data": "Added your server to my data! Thanks for letting me steal it! (/s)", - "bound_dc_channel": "Successfully bound channel ID `{0}` as the bk_week channel!", - "cfg_data_404": "cfg.json not found,", - "cfg_data_wipe": "[WIPE] (cfg.json)", - "cmd_404": "No command with the name \"{0}\" found!\nHint: Try `/help` without any arguments or `/help `", - "corrupted_data": "Oopsies `(。>\\\\<)`. It looks like my data i-is \\**sob*\\*... c-corrupted!", - "couldnt_shorturl": "Couldn't convert to shortURL: Invalid URL", - "creating_data_file": "{0} creating new from preset...", - "data_post_embed": "Spoilers and vote length anonymizer for fair review!\n ## Post Data:\n **Post upvotes:** ||`{0:>6}`||\n **Moderator votes:** ||`{1:>6}`||\n **Media type:** `{2}`\n **URL:** ||<{3}>||\n\n ## Listing Data:\n **Added by:** `{{ human: {4}, bot: {5} }}`\n **Approved by:** `{{ human: {6}, bot: [not implemented] }}`", - "data_post_removed_embed": "## Removed by `{0}`\n**Reason:** {1}\nURL: ||<{2}>||\n\nJSON: ||`{3}`||", - "data_save_complete": "Saving data... Done!\nShutting down...", - "data_save_progress": "Saving data...", - "dc_404": "Your server is not in the data!\n Hint: Run the command `/add_server` inside of a Discord server.", - "dc_bot_started": "Bot started as user \"{0}\" with id {1}", - "dc_bot_starting": "Starting bot...", - "dc_data_404": "discord_data.json not found,", - "dc_data_wipe": "[WIPE] (discord_data.json)", - "default_embed_desc": "Default description", - "err_read_py_file": "Failed to read Python file!\nPath: {0}", - "help_text_removed": "Help text not found. Someone deleted it. :(", - "invalid_confirm_shutdown": "Failed to shut down: Invalid confirmation.", - "invalid_os": "Man what kinda OS do you have? Neither unix or windows, what the hell!? I can't process this anymore, you're too weird!", - "lang_data_404": "File for language \"{0}\" not found!", - "lang_load_success": "Successfully loaded the english language file!", - "mandatory_response": "Mandatory response message, please ignore.", - "python_err": "Unknown internal Python Error: {0}", - "python_socket_binary_response": "[Binary] from Python: {0}", - "python_socket_err": "Unknown internal Python error occurred: Websocket response error.", - "python_socket_null": "Response from Python is None!", - "python_socket_response": "Received from Python: [RESPONSE] {0}", - "python_socket_unknown_response": "Received from Python: [UNKNOWN / OTHER]", - "re_404": "Post URL \"<{0}>\" not found: Post doesn't exist in the data!\n Hint: Run the command `/re_addPost [URL]` in a Discord channel or `u/ByteDiceAssistant bk_week_add` in a Reddit post.", - "re_add_post_success": "Added post with URL \"<{0}>\"!", - "re_already_voted": "Couldn't cast a vote: You have already voted on this post!", - "re_also_approved": "Also approved it!", - "re_approved_post_success": "Successfully approved the post!", - "re_data_404": "reddit_data.json not found,", - "re_data_corrupted": "Post URL \"<{0}>\" not found: Post data is corrupted!\n Full details: Could not find key \"bk_weekly_art_posts\" in data file \"reddit_data.json\"", - "re_data_wipe": "[WIPE] (reddit_data.json)", - "re_disapproved_post_success": "Successfully disapproved the post!", - "re_err_trace": "Unknown error!\nError trace: {0}.", - "re_havent_voted_remove_vote": "Couldn't remove your vote: You haven't voted on this post yet!", - "re_permdeny_bk_mod": "Permission denied: You are not a moderator of {0}", - "re_remove_post_success": "Successfully removed post!", - "re_unremove_post_success": "Successfully un-removed post with URL \"<{0}>\"!", - "re_update_post_success": "Updated post with URL \"<{0}>\"!", - "re_vote_err": "Failed to vote/un-vote: Unknown internal error", - "re_vote_remove_success": "Successfully removed vote!", - "re_vote_success_mod": "Successfully voted (as moderator vote)!", - "re_vote_success": "Successfully voted!", - "reload_config_python_fail": "Failed to reload configs: Failed-type response from Python.", - "reload_config_success": "Successfully reloaded the configs!\nNew configs:\n```\n{0}\n```", - "shorturl": "ShortURL: <{0}>", - "socket_received_python": "Received from Python: {0}", - "socket_rust_connection_test": "[Connection test] Hello from Rust!", - "started_socket": "WebSocket server running on ws://{0}", - "starting_python_program": "Running Python program...", - "starting_schedules": "Starting schedules...", - "starting_socket": "Starting local websocket..." + "dc_msg_8-ball_answer": "## You shook a magic 8-ball\nQ: {0}\nA: {1}", + "dc_msg_add_to_data": "Added your server to my data! Thanks for letting me steal it! (/s)", + "dc_msg_bound_channel": "Successfully bound channel ID `{0}` as the \"where all collected Reddit data gets dumped\" channel!", + "dc_msg_cmd_404": "No command \"{0}\" found!\nHint: Try `/help` without any arguments or `/help `", + "dc_msg_corrupted_data": "Oopsies `(。>\\\\<)`. It looks like my data i-is \\**sob*\\*... c-corrupted!\n[From Byte Dice]: I have no idea what I was thinking while writing this at 2am. I'm not removing it.", + "dc_msg_dm_python_err_socket": "Unknown internal Python error occurred: Websocket response error", + "dc_msg_dm_python_err": "Unknown internal Python Error: {0}", + "dc_msg_embed_default_embed_desc": "Default english embed description.", + "dc_msg_embed_re_post": "Spoilers and vote length anonymizer for fair review!\n## Post Data:\n**Post upvotes:** ||`{0:>6}`||\n**Moderator votes:** ||`{1:>6}`||\n**Media type:** `{2}`\n**URL:** ||<{3}>||\n\n## Listing Data:\n**Added by:** `{{ human: {4}, bot: {5} }}`\n**Approved by:** `{{ human: {6}, bot: [not implemented] }}`", + "dc_msg_embed_re_removed": "## Removed by `{0}`\n**Reason:** {1}\nURL: ||<{2}>||\n\nJSON: ||`{3}`||", + "dc_msg_err_trace": "Unknown error!\nError trace: {0}", + "dc_msg_failed_shorturl_conversion": "Couldn't convert to shortURL: Invalid Reddit URL format.", + "dc_msg_mandatory_response": "Mandatory response message, please ignore.", + "dc_msg_owner_data_save_complete": "Saving data... Done!\nShutting down...", + "dc_msg_owner_data_save": "Saving data...", + "dc_msg_owner_shutdown_failed_confirmation": "Failed to shut down: Invalid confirmation.", + "dc_msg_re_already_voted": "Couldn't cast a vote: You have already voted on this post!", + "dc_msg_re_also_approved": "Also approved it!", + "dc_msg_re_data_corrupted": "Post URL \"<{0}>\" not found: Post data is corrupted!\n Full details: Could not find key \"bk_weekly_art_posts\" in data file \"reddit_data.json\"", + "dc_msg_re_help_removed": "Reddit help file not found, someone likely deleted it.\n Hint: You can find the official help text at https://github.com/ByteDice/ByteDiceAssistant in `bk_week_help_re.md`.\n**CAUTION:** The official help file may contain incorrect information if you're not using the official bot by Byte Dice.", + "dc_msg_re_permdeny_not_re_mod": "Permission denied: You are not a moderator of the subreddit(s) {0}", + "dc_msg_re_post_404": "Post URL \"<{0}>\" not found: Post doesn't exist in the data!\n Hint: Run the command `/re_addPost [URL]` in a Discord channel or `u/ByteDiceAssistant bk_week_add` in a Reddit post.", + "dc_msg_re_post_add_success": "Added post with URL \"<{0}>\"!", + "dc_msg_re_post_approve_success": "Successfully approved the post!", + "dc_msg_re_post_disapprove_success": "Successfully disapproved the post!", + "dc_msg_re_post_remove_success": "Successfully removed post!", + "dc_msg_re_post_unremove_success": "Successfully un-removed post with URL \"<{0}>\"!", + "dc_msg_re_post_update_success": "Updated post with URL \"<{0}>\"!", + "dc_msg_re_vote_err": "Failed to vote/un-vote: Unknown internal error.", + "dc_msg_re_vote_mod_success": "Successfully voted (as moderator vote)!", + "dc_msg_re_vote_remove_havent": "Couldn't remove your vote: You haven't voted on this post yet!", + "dc_msg_re_vote_remove_success": "Successfully removed vote!", + "dc_msg_re_vote_success": "Successfully voted!", + "dc_msg_reload_cfg_python_fail": "Failed to reload configs: Failed-type response from Python.", + "dc_msg_reload_cfg_success": "Successfully reloaded the configs!\nNew configs:\n```\n{0}\n```", + "dc_msg_server_data_404": "This server is not in the data!\n Hint: Run the command `/add_server` inside of a Discord server (requires administrator permission).", + "dc_msg_shorturl": "ShortURL: <{0}>", + "log_lang_load_success": "Successfully loaded the english language file!" } \ No newline at end of file diff --git a/data/lang/miserable.json b/data/lang/miserable.json deleted file mode 100644 index ac287f5..0000000 --- a/data/lang/miserable.json +++ /dev/null @@ -1,62 +0,0 @@ -{ - "8-ball_answer": "?: {0}\n!: {1}", - "add_to_data": "Something something selling data something.", - "bound_dc_channel": "Random numbers, go! `{0}`", - "cfg_data_404": "I'm not telling you the error but I think it's something about your pc having malware :) ", - "cfg_data_wipe": "[WIPE MY ASS] file.file_extention something.", - "cmd_404": "Great, you executed `{0}`!", - "corrupted_data": "p4Oad3!an5!#####", - "couldnt_shorturl": "ShortURL: https://ILoveBurritos.com/yourRedditURL.", - "creating_data_file": "{0} (not taking care of the malware).", - "data_post_embed": "## Figure it out:\n **Figure it out:** ||`{0:>6}`||\n **Figure it out:** ||`{1:>6}`||\n **Figure it out:** `{2}`\n **Figure it out:** ||<{3}>||\n\n ## Figure it out:\n **Figure it out:** `{{ human: {4}, bot: {5} }}`\n **Figure it out:** `{{ human: {6}, bot: [not implemented] }}`", - "data_post_removed_embed": "## Approved by `{0}`\n**Reason:** {1}\nURL: ||<{2}>||\n\nJSON: ||`{3}`||", - "data_save_complete": "Discarding data... Done!\nShutting down...", - "data_save_progress": "Discarding data...", - "dc_404": "Everything went well!", - "dc_bot_started": "Couldn't start bot \"{0}\" with id {1}: Someone else is currently running the same bot.", - "dc_bot_starting": "Starting bot...", - "dc_data_404": "I'm not telling you the error but I think it's something about your pc having malware :) ", - "dc_data_wipe": "[WIPE MY ASS] file.file_extention something", - "default_embed_desc": "The person who created this is stupid for not adding a description.", - "err_read_py_file": "Successfully read Python file: (Path) {0}", - "help_text_removed": "We hate providing help to you specifically!", - "invalid_confirm_shutdown": "Discarding data... Done!\nShutting down...", - "invalid_os": "Man what kinda OS do you have? Neither unix or windows, what the hell!? I can't process this anymore, you're too weird!", - "lang_data_404": "Successfully loaded language \"{0}\"!", - "lang_load_success": "Failed to load the english language file!", - "mandatory_response": "Annoying message you can't get rid of.", - "python_err": "Looking good! {0}\nIgnore what's above me.", - "python_socket_binary_response": "Corrupted data found: {0}", - "python_socket_err": "Websocket response test successfully completed!", - "python_socket_null": "Received from Python: [UNKNOWN / OTHER]", - "python_socket_response": "Received from alien contact: {0}", - "python_socket_unknown_response": "Lettuce", - "re_404": "\"<{0}>\" is a funny post URL, am I right?", - "re_add_post_success": "Removed post with URL \"<{0}>\"!", - "re_already_voted": "Successfully voted!", - "re_also_approved": "Nevermind, that post sucks.", - "re_approved_post_success": "Successfully added the post!", - "re_data_404": "I'm not telling you the error but I think it's something about your pc having malware :) ", - "re_data_corrupted": "\"<{0}>\" is a funny post URL, am I right?", - "re_data_wipe": "[WIPE MY ASS] file.file_extention something", - "re_disapproved_post_success": "Successfully removed the post!", - "re_err_trace": "I got this random file path thingy, here you go: {0}.", - "re_havent_voted_remove_vote": "Successfully voted!", - "re_permdeny_bk_mod": "Welcome to the {0} team!", - "re_remove_post_success": "Successfully", - "re_unremove_post_success": "Successfully removed post with URL \"<{0}>\"!", - "re_update_post_success": "\"<{0}>\"!", - "re_vote_err": "Successfully voted!", - "re_vote_remove_success": "Successfully voted!", - "re_vote_success_mod": "Successfully voted!", - "re_vote_success": "Successfully voted (as moderator vote)!", - "reload_config_python_fail": "Successfully reloaded the configs!", - "reload_config_success": "I didn't update them, but here you go anyways:\n```\n{0}\n```", - "shorturl": "Invalid shortURL, try again. Here's a random one though: <{0}>", - "socket_received_python": "Sent to Python: {0}", - "socket_rust_connection_test": "------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------", - "started_socket": "Your IPv4 address: {0}", - "starting_python_program": "Running Rust program...", - "starting_schedules": "Starting spyware...", - "starting_socket": "Starting global websocket..." -} \ No newline at end of file diff --git a/src/cmds.rs b/src/cmds.rs index 18c9b18..5fb9afb 100644 --- a/src/cmds.rs +++ b/src/cmds.rs @@ -6,7 +6,7 @@ use crate::data::{dc_add_server, get_mutex_data, read_cfg_data}; use crate::re_cmds::generic_fns::to_shorturl; use crate::websocket::send_cmd_json; use crate::{data, lang, Context, Data, Error}; -use crate::messages::{edit_reply, send_embed, send_msg, Author, EmbedOptions, MANDATORY_MSG}; +use crate::messages::{edit_reply, send_embed, send_msg, Author, EmbedOptions}; use poise::serenity_prelude::{OnlineStatus, Timestamp}; use poise::Command; @@ -15,6 +15,9 @@ use serde_json::json; use tokio::fs; +// TODO: separate to multiple files + + #[derive(poise::ChoiceParameter, PartialEq)] enum HelpOptions { Admin, @@ -63,19 +66,19 @@ pub async fn stop( || confirmation.unwrap_or_default().to_lowercase() == "i want to stop the bot now"; if should_stop { - let msg = send_msg(ctx, lang!("data_save_progress"), true, true).await.unwrap(); + let msg = send_msg(ctx, lang!("dc_msg_owner_data_save"), true, true).await.unwrap(); data::write_dc_data(ctx.data()).await; data::write_re_data().await; send_cmd_json("stop_praw", None).await; - edit_reply(ctx, msg, lang!("data_save_complete")).await; + edit_reply(ctx, msg, lang!("dc_msg_owner_data_save_complete")).await; ctx.serenity_context().set_presence(None, OnlineStatus::Invisible); ctx.framework().shard_manager.shutdown_all().await; process::exit(0); } else { - send_msg(ctx, lang!("invalid_confirm_shutdown"), true, true).await; + send_msg(ctx, lang!("dc_msg_owner_shutdown_failed_confirmation"), true, true).await; } return Ok(()); @@ -124,7 +127,7 @@ pub async fn embed( ).await; if !reply_unwrap { - send_msg(ctx, MANDATORY_MSG.to_string(), true, true).await; + send_msg(ctx, lang!("dc_msg_mandatory_response"), true, true).await; } return Ok(()); @@ -146,7 +149,7 @@ pub async fn send( ) -> Result<(), Error> { send_msg(ctx, msg.replace("\\n", "\n"), false, false).await; - send_msg(ctx, MANDATORY_MSG.to_string(), true, true).await; + send_msg(ctx, lang!("dc_msg_mandatory_response"), true, true).await; return Ok(()); } @@ -171,7 +174,7 @@ pub async fn eight_ball( send_msg( ctx, - lang!("8-ball_answer", question, rand_item.unwrap()), + lang!("dc_msg_8-ball_answer", question, rand_item.unwrap()), true, true ).await; @@ -194,10 +197,10 @@ pub async fn re_shorturl( let shorturl = to_shorturl(&url); if shorturl.is_ok() { - send_msg(ctx, lang!("shorturl", shorturl.unwrap()), true, true).await; + send_msg(ctx, lang!("dc_msg_shorturl", shorturl.unwrap()), true, true).await; } else { - send_msg(ctx, lang!("couldnt_shorturl"), true, true).await; + send_msg(ctx, lang!("dc_msg_failed_shorturl_conversion"), true, true).await; } return Ok(()); @@ -220,10 +223,10 @@ pub async fn add_server( let r = dc_add_server(ctx.data(), ctx.guild_id().unwrap().into()).await; if r.is_ok() { - send_msg(ctx, lang!("add_to_data"), true, true).await; + send_msg(ctx, lang!("dc_msg_added_to_data"), true, true).await; } else { - send_msg(ctx, lang!("corrupted_data"), true, true).await; + send_msg(ctx, lang!("dc_msg_corrupted_data"), true, true).await; } return Ok(()); @@ -250,14 +253,14 @@ pub async fn reload_cfg( if r.is_some() && r.unwrap()["value"].as_bool().unwrap() { send_msg( ctx, - lang!("reload_config_success", serde_json::to_string_pretty(&d).unwrap()), + lang!("dc_msg_reload_cfg_success", serde_json::to_string_pretty(&d).unwrap()), true, true ).await; return Ok(()); } - send_msg(ctx, lang!("reload_config_python_fail"), true, true).await; + send_msg(ctx, lang!("dc_msg_reload_cfg_python_fail"), true, true).await; return Ok(()); } @@ -329,7 +332,7 @@ async fn send_single_help(ctx: Context<'_>, mut cmd_name: String) { if cmd.is_none() { send_msg( ctx, - lang!("cmd_404", cmd_name), + lang!("dc_msg_cmd_404", cmd_name), true, true ).await; @@ -355,7 +358,7 @@ async fn send_category_help(ctx: Context<'_>, category: HelpOptions) { async fn send_bk_week_help_re(ctx: Context<'_>) { let t: String = fs::read_to_string("./bk_week_help_re.md").await - .unwrap_or(lang!("help_text_removed")); + .unwrap_or(lang!("dc_msg_re_help_removed")); send_msg(ctx, t, true, true).await; } diff --git a/src/data.rs b/src/data.rs index f3eca25..1f8a81a 100644 --- a/src/data.rs +++ b/src/data.rs @@ -4,7 +4,7 @@ use std::path::Path; use serde_json::{self, Value, json}; use tokio::sync::Mutex; -use crate::{lang, rs_println, Data, Error, BK_WEEK, LANG}; +use crate::{errln, rs_println, Data, Error, BK_WEEK, LANG}; use crate::websocket::send_cmd_json; @@ -20,8 +20,8 @@ static DATA_PATH_LANG: &str = "./data/lang/"; pub async fn read_dc_data(data: &Data, wipe: bool) { if !Path::new(DATA_PATH_DC).exists() || wipe { rs_println!( - "{}", - lang!("creating_data_file", if !wipe { lang!("dc_data_404") } else { lang!("dc_data_wipe") }) + "{} creating new from preset...", + if !wipe { "discord_data.json not found," } else { "[WIPE] (discord_data.json)" } ); generate_dc_data(); } @@ -71,8 +71,8 @@ pub async fn write_dc_data(data: &Data) { pub async fn read_re_data(data: &Data, wipe: bool) { if !Path::new(DATA_PATH_RE).exists() || wipe { rs_println!( - "{}", - lang!("creating_data_file", if !wipe { lang!("re_data_404") } else { lang!("re_data_wipe") }) + "{} creating new from preset...", + if !wipe { "reddit_data.json not found," } else { "[WIPE] (reddit_data.json)" } ); generate_re_data(); } @@ -114,8 +114,8 @@ pub async fn write_re_data() { pub async fn read_cfg_data(data: &Data, wipe: bool) { if !Path::new(DATA_PATH_CFG).exists() || wipe { rs_println!( - "{}", - lang!("creating_data_file", if !wipe { lang!("cfg_data_404") } else { lang!("cfg_data_wipe") }) + "{} creating new from preset...", + if !wipe { "cfg.json not found," } else { "[WIPE] (cfg.json)" } ); generate_cfg_data(); } @@ -202,11 +202,10 @@ pub fn load_lang_data(lang: String) { let full_path = format!("{}{}.json", DATA_PATH_LANG, lang); if !Path::new(&full_path).exists() { - rs_println!( - "{}", - lang!("creating_data_file", lang!("cfg_data_404", lang)) + errln!( + "File for language \"{0}\" ({0}.json) not found!\n Hint: You can download official language files at https://github.com/ByteDice/ByteDiceAssistant in the data/langs/... folder", + lang ); - generate_cfg_data(); } let str_data = fs::read_to_string(full_path).unwrap(); diff --git a/src/events.rs b/src/events.rs index 098d575..aa8f1d9 100644 --- a/src/events.rs +++ b/src/events.rs @@ -1,4 +1,4 @@ -use crate::{lang, rs_println, Data, Error}; +use crate::{rs_println, Data, Error}; use poise::serenity_prelude::{self as serenity, ActivityData}; @@ -14,12 +14,9 @@ pub fn event_handler<'a>( Box::pin(async move { if let serenity::FullEvent::Ready { data_about_bot } = event { rs_println!( - "{}", - lang!( - "dc_bot_started", - data_about_bot.user.name.clone(), - data_about_bot.user.id - ) + "Bot started as user \"{}\" with id {}", + data_about_bot.user.name, + data_about_bot.user.id ); let file_text = std::fs::read_to_string("./data/status.txt").unwrap(); diff --git a/src/main.rs b/src/main.rs index bf17a00..13a1315 100644 --- a/src/main.rs +++ b/src/main.rs @@ -40,7 +40,6 @@ use websocket::send_cmd_json; use crate::schedule::Schedule; -// TODO: convert to lang! #[derive(Parser, Serialize, Clone)] struct Args { @@ -92,7 +91,7 @@ async fn main() { rs_println!("Fetching language file..."); data::load_lang_data(args.clone().lang); - rs_println!("{}", lang!("lang_load_success")); + rs_println!("[IMPORTANT] The below message is a test message, it should be written in the language you've selected\n{}", lang!("log_lang_load_success")); let own_env = std::env::var("ASSISTANT_OWNERS").unwrap_or("0".to_string()); let own_vec_str: Vec = own_env.split(",").map(String::from).collect(); @@ -156,7 +155,7 @@ async fn start(args: Args, owners: Vec) { let data = gen_data(args.clone(), owners).await; let mut bot = gen_bot(data, args).await; - rs_println!("{}", lang!("dc_bot_starting")); + rs_println!("Starting Discord bot..."); bot.start().await.unwrap(); } diff --git a/src/messages.rs b/src/messages.rs index 5b5ccc9..3c5191e 100644 --- a/src/messages.rs +++ b/src/messages.rs @@ -29,7 +29,7 @@ pub struct EmbedOptions { impl Default for EmbedOptions { fn default() -> Self { return EmbedOptions { - desc: lang!("default_embed_desc"), + desc: lang!("dc_msg_embed_default_embed_desc"), title: None, col: None, url: None, @@ -45,7 +45,6 @@ impl Default for EmbedOptions { static DEFAULT_DC_COL: u32 = 5793266; static REMOVED_DC_COL: u32 = 16716032; -pub static MANDATORY_MSG: &str = "Mandatory response, please ignore."; fn none_to_empty(string: Option) -> String { @@ -198,7 +197,7 @@ pub fn make_post_embed(post_data: &Value, url: &str, ephemeral: bool) -> EmbedOp let media_type = &post_data["post_data"]["media_type"]; let desc_str = lang!( - "data_post_embed", + "dc_msg_embed_re_post", post_data["post_data"]["upvotes"].as_i64().unwrap(), post_data["votes"]["mod_voters"].as_array().unwrap().len(), if !media_type.is_null() { media_type.as_str().unwrap() } else { "None" }, @@ -242,7 +241,7 @@ pub fn make_removed_embed(post_data: &Value, url: &str, ephemeral: bool) -> Embe return EmbedOptions { title: Some("REMOVED!".to_string()), desc: lang!( - "data_post_removed_embed", + "dc_msg_embed_re_removed", post_data["removed_by"].as_str().unwrap(), if !post_data["remove_reason"].is_null() { post_data["remove_reason"].as_str().unwrap() } else { "None" }, diff --git a/src/python.rs b/src/python.rs index d6146b2..fce7731 100644 --- a/src/python.rs +++ b/src/python.rs @@ -10,12 +10,12 @@ use pyo3::types::PyList; pub async fn start(args: Args) -> PyResult<()> { - rs_println!("{}", lang!("starting_python_program")); + rs_println!("Running Python program..."); let args_str = serde_json::to_string(&args).expect("Error serializing args to JSON"); let slash = if cfg!(windows) { "\\" } else if cfg!(unix) { "/" } else { "" }; - if slash.is_empty() { errln!("{}", lang!("invalid_os")); } + if slash.is_empty() { errln!("Man what kinda OS do you have? Neither unix or windows, what the hell!? I can't process this anymore, you're too weird!"); } let path = format!("{0}{1}src{1}python", env!("CARGO_MANIFEST_DIR"), slash); @@ -43,7 +43,7 @@ pub async fn start(args: Args) -> PyResult<()> { .map(|s| s.parse::().expect("Failed to parse ASSISTANT_OWNERS. Invalid syntax.")) .collect(); - send_dm(lang!("python_err", format!("{:?}", from_python)), args, own_vec_u64).await; + send_dm(lang!("dc_msg_dm_python_err", format!("{:?}", from_python)), args, own_vec_u64).await; errln!("pyO3: {:?}", from_python); } return Ok(()); diff --git a/src/re_cmds/add.rs b/src/re_cmds/add.rs index 689fad3..7e24838 100644 --- a/src/re_cmds/add.rs +++ b/src/re_cmds/add.rs @@ -21,7 +21,7 @@ pub async fn cmd( ) -> Result<(), Error> { if !is_bk_mod(ctx.data().bk_mods.clone(), ctx.author().id.get()) { - send_msg(ctx, lang!("re_permdeny_bk_mod"), false, false).await; + send_msg(ctx, lang!("dc_msg_re_permdeny_not_re_mod"), false, false).await; // TODO: add subreddit arg to lang return Ok(()); } @@ -49,18 +49,18 @@ pub async fn cmd( if let Some(post) = bk_week.get(shorturl) { if post.get("removed").is_some() { - send_msg(ctx, lang!("re_unremove_post_success", url), true, true).await; + send_msg(ctx, lang!("dc_msg_re_post_unremove_success", url), true, true).await; } else { - send_msg(ctx, lang!("re_update_post_success", url), true, true).await; + send_msg(ctx, lang!("dc_msg_re_post_update_success", url), true, true).await; } } else { - send_msg(ctx, lang!("re_add_post_success", &shorturl), true, true).await; + send_msg(ctx, lang!("dc_msg_re_post_add_success", &shorturl), true, true).await; } if a { - send_msg(ctx, lang!("re_also_approved"), true, true).await; + send_msg(ctx, lang!("dc_msg_re_also_approved"), true, true).await; } } diff --git a/src/re_cmds/admin_bind.rs b/src/re_cmds/admin_bind.rs index 9336004..675ea39 100644 --- a/src/re_cmds/admin_bind.rs +++ b/src/re_cmds/admin_bind.rs @@ -18,10 +18,10 @@ pub async fn cmd( let r = dc_bind_bk(ctx.data(), ctx.guild_id().unwrap().into(), c_id).await; if r.is_ok() { - send_msg(ctx, lang!("bound_dc_channel", c_id), true, true).await; + send_msg(ctx, lang!("dc_msg_bound_channel", c_id), true, true).await; } else { - send_msg(ctx, lang!("dc_404"), false, false).await; + send_msg(ctx, lang!("dc_msg_server_data_404"), false, false).await; } return Ok(()); diff --git a/src/re_cmds/approve.rs b/src/re_cmds/approve.rs index ed3eeaf..da3b8c2 100644 --- a/src/re_cmds/approve.rs +++ b/src/re_cmds/approve.rs @@ -19,7 +19,7 @@ pub async fn cmd( ) -> Result<(), Error> { if !is_bk_mod(ctx.data().bk_mods.clone(), ctx.author().id.get()) { - send_msg(ctx, lang!("re_permdeny_bk_mod"), false, false).await; + send_msg(ctx, lang!("dc_msg_re_permdeny_not_re_mod"), false, false).await; return Ok(()); } @@ -42,17 +42,17 @@ async fn approve_cmd(ctx: Context<'_>, url: &str, reddit_data: &Value, approve: let r = websocket::send_cmd_json("set_approve_post", Some(json!([approve, &url]))).await.unwrap(); if r.get("value").is_some() { if approve { - send_msg(ctx, lang!("re_approve_post_success"), true, true).await; + send_msg(ctx, lang!("dc_msg_re_post_approve_success"), true, true).await; } else { - send_msg(ctx, lang!("re_disapprove_post_success"), true, true).await; + send_msg(ctx, lang!("dc_msg_re_post_disapprove_success"), true, true).await; } } else { - send_msg(ctx, lang!("re_err_trace", "`re_cmds -> approve.rs -> cmd() -> unwrap websocket result error`"), true, true).await; + send_msg(ctx, lang!("dc_msg_err_trace", "`re_cmds -> approve.rs -> cmd() -> unwrap websocket result error`"), true, true).await; } } else { - send_msg(ctx, lang!("re_404"), false, false).await; + send_msg(ctx, lang!("dc_msg_re_post_404"), false, false).await; } } \ No newline at end of file diff --git a/src/re_cmds/get.rs b/src/re_cmds/get.rs index 54f34ce..7e99817 100644 --- a/src/re_cmds/get.rs +++ b/src/re_cmds/get.rs @@ -39,11 +39,11 @@ async fn get_post_from_data(ctx: Context<'_>, reddit_data: &Value, url: &str) -> return Ok(Some(post.clone())); } else { - send_msg(ctx, lang!("re_404", url), true, true).await; + send_msg(ctx, lang!("dc_msg_re_post_404", url), true, true).await; } } else { - send_msg(ctx, lang!("re_data_corrupted", url), true, true).await; + send_msg(ctx, lang!("dc_msg_re_data_corrupted", url), true, true).await; rs_println!("{}", serde_json::to_string_pretty(reddit_data)?); } return Ok(None); diff --git a/src/re_cmds/remove.rs b/src/re_cmds/remove.rs index f19dfb0..7a60046 100644 --- a/src/re_cmds/remove.rs +++ b/src/re_cmds/remove.rs @@ -17,7 +17,7 @@ pub async fn cmd( ) -> Result<(), Error> { if !is_bk_mod(ctx.data().bk_mods.clone(), ctx.author().id.get()) { - send_msg(ctx, lang!("re_permdeny_bk_mod"), false, false).await; + send_msg(ctx, lang!("dc_msg_re_permdeny_not_re_mod"), false, false).await; return Ok(()); } @@ -27,13 +27,13 @@ pub async fn cmd( if r["value"].as_bool().unwrap() { send_msg( ctx, - lang!("re_remove_post_success"), + lang!("dc_msg_re_post_remove_success"), true, true ).await; } else { - send_msg(ctx, lang!("re_404"), false, false).await; + send_msg(ctx, lang!("dc_msg_re_post_404"), false, false).await; } return Ok(()); diff --git a/src/re_cmds/update.rs b/src/re_cmds/update.rs index da8af2a..6197b6f 100644 --- a/src/re_cmds/update.rs +++ b/src/re_cmds/update.rs @@ -105,7 +105,7 @@ async fn update_progress(ctx: Context<'_>, p: ReplyHandle<'_>, t: String, added_ async fn get_c_id(ctx: Context<'_>) -> Option { if !data::dc_contains_server(ctx.data(), ctx.guild_id().unwrap().into()).await { - send_msg(ctx, lang!("dc_404"), false, false).await; + send_msg(ctx, lang!("dc_msg_server_data_404"), false, false).await; return None; } diff --git a/src/re_cmds/vote.rs b/src/re_cmds/vote.rs index b8e5413..6cb05ad 100644 --- a/src/re_cmds/vote.rs +++ b/src/re_cmds/vote.rs @@ -23,7 +23,7 @@ pub async fn cmd( let unw_vote = un_vote.unwrap_or(false); if post_data.get(&url).is_none() { - send_msg(ctx, lang!("re_404"), false, false).await; + send_msg(ctx, lang!("dc_msg_re_post_404"), false, false).await; return Ok(()); } if post_data[&url].get("removed").is_some() { @@ -39,11 +39,11 @@ pub async fn cmd( let voters = if is_mod { mod_voters } else { voters_dc }; if voters.contains(&json!(uid)) && !unw_vote { - send_msg(ctx, lang!("re_already_voted"), true, true).await; + send_msg(ctx, lang!("dc_msg_re_already_voted"), true, true).await; return Ok(()); } else if !voters.contains(&json!(uid)) && unw_vote { - send_msg(ctx, lang!("re_havent_voted_remove_vote"), true, true).await; + send_msg(ctx, lang!("dc_msg_re_vote_remove_havent"), true, true).await; return Ok(()); } @@ -51,16 +51,16 @@ pub async fn cmd( let unw_r = r["value"].as_bool().unwrap(); if unw_r && !unw_vote && is_mod { - send_msg(ctx, lang!("re_vote_success_mod"), true, true).await; + send_msg(ctx, lang!("dc_msg_re_vote_mod_success"), true, true).await; } else if unw_r && !unw_vote && !is_mod { - send_msg(ctx, lang!("re_vote_success"), true, true).await; + send_msg(ctx, lang!("dc_msg_re_vote_success"), true, true).await; } else if unw_r && unw_vote { - send_msg(ctx, lang!("re_vote_remove_success"), true, true).await; + send_msg(ctx, lang!("dc_msg_re_vote_remove_success"), true, true).await; } else { - send_msg(ctx, lang!("re_vote_err"), true, true).await; + send_msg(ctx, lang!("dc_msg_re_vote_err"), true, true).await; } return Ok(()); diff --git a/src/schedule.rs b/src/schedule.rs index 683fd7b..0ab5cbd 100644 --- a/src/schedule.rs +++ b/src/schedule.rs @@ -23,7 +23,7 @@ pub async fn run_schedule Pin + Send>>>(d pub async fn run_schedules(schedules: Vec) { let mut handles: Vec> = vec![]; - rs_println!("{}", lang!("starting_schedules")); + rs_println!("Starting schedules..."); for (d, f) in schedules { let handle = tokio::spawn(run_schedule(d, f)); handles.push(handle); diff --git a/src/websocket.rs b/src/websocket.rs index 9a03bcd..834acba 100644 --- a/src/websocket.rs +++ b/src/websocket.rs @@ -66,11 +66,11 @@ pub async fn send_cmd_json(func_name: &str, func_args: Option) -> Option< let r = receive_response().await; if !["respond_mentions"].contains(&func_name) || ::parse().dev { - rs_println!("{}", lang!("python_socket_response", format!("{:?}", r))); + rs_println!("Received from Python: [RESPONSE] {:?}", r); } if r.is_none() { - rs_println!("{}", lang!("python_socket_null")); + rs_println!("[IMPORTANT] Response from Python is None!"); } return r; @@ -99,10 +99,10 @@ async fn receive_response() -> Option { pub async fn start(args: Args, owners: Vec) { - rs_println!("{}", lang!("starting_socket")); + rs_println!("Running local websocket..."); let ip = format!("127.0.0.1:{}", args.port); let listener = TcpListener::bind(&ip).await.unwrap(); - rs_println!("{}", lang!("started_socket", ip)); + rs_println!("WebSocket server running on ws://{}", ip); tokio::spawn(handle_connections(listener, args, owners)); } @@ -129,18 +129,18 @@ async fn handle_connections(listener: TcpListener, args: Args, owners: Vec) async fn handle_message(msg: tungstenite::protocol::Message, args: Args, owners: Vec) { match msg { tungstenite::Message::Text(text) => { - rs_println!("{}", lang!("socket_received_python", text.clone())); + rs_println!("Received from Python: {}", text); if let Some(stripped) = text.strip_prefix("json:") { let t_json: Value = serde_json::from_str(stripped).unwrap(); if t_json.get("error").is_some() { - send_dm(lang!("python_socket_err"), args, owners).await; + send_dm(lang!("dc_msg_dm_python_err_socket"), args, owners).await; } } unsafe { if !REPLY_HELLO { - send_msg(&lang!("socket_rust_connection_test")).await; + send_msg("[Connection test] Hello from Rust!").await; REPLY_HELLO = true; HAS_CONNECTED = true; } @@ -148,12 +148,12 @@ async fn handle_message(msg: tungstenite::protocol::Message, args: Args, owners: } tungstenite::Message::Binary(bytes) => { if args.dev && !args.noping { - rs_println!("{}", lang!("python_socket_binary_response", format!("{:?}", bytes))); + rs_println!("[Binary] from Python: {:?}", bytes); } } _ => { if args.dev && !args.noping { - rs_println!("{}", lang!("python_socket_unknown_response")); + rs_println!("Received from Python: [UNKNOWN / OTHER]"); } } } From 854809e3214110212f4936853238070e07321862 Mon Sep 17 00:00:00 2001 From: ByteDice Date: Sat, 12 Apr 2025 23:14:56 +0200 Subject: [PATCH 04/39] too much to remember, mostly refactoring/enhancing/changing/re-doing/re-writing stuff --- README.md | 14 +++++++------- TODO.md | 30 +----------------------------- data/cfg_default.json | 2 +- data/dc_data_preset.json | 9 ++------- data/lang/en.json | 11 ++++++----- data/re_data_preset.json | 2 +- src/data.rs | 10 ++++++---- src/main.rs | 14 +++++++------- src/python/bot.py | 14 +++++++------- src/python/cmds.py | 13 ++++++------- src/python/data.py | 32 ++++++++++++++++---------------- src/re_cmds/add.rs | 9 +++++---- src/re_cmds/approve.rs | 7 ++++--- src/re_cmds/generic_fns.rs | 12 +++++++++++- src/re_cmds/get.rs | 4 ++-- src/re_cmds/remove.rs | 5 +++-- src/re_cmds/top.rs | 4 ++-- src/re_cmds/update.rs | 12 ++++++------ src/re_cmds/vote.rs | 4 ++-- src/schedule.rs | 2 +- 20 files changed, 96 insertions(+), 114 deletions(-) diff --git a/README.md b/README.md index a5cf63e..4694233 100644 --- a/README.md +++ b/README.md @@ -1,11 +1,11 @@ # ByteDiceAssistant -An automation tool for Byte Dice. It's both a Discord and Reddit bot in one program. +An automation tool primarily made for myself (Byte Dice) but publicly available for anyone to use. It's both a Discord and Reddit bot in one program. > [!CAUTION] > This tool is not intended for public use outside of the official *Byte Dice Assistant* bots. Expect issues if you host this yourself.\ -> This tool is only designed to run on Windows (10 and 11) and XUbuntu and may not work on any other OS. +> This tool is only designed to run on Windows (10 and 11) and XUbuntu (24.04 and above) and may not work on any other OS. -# Open-source - Copyright +## Open-source - Copyright **ByteDiceAssistant © 2025 by Byte Dice is licensed under CC BY-NC-SA 4.0.**\ **You can learn more about copyright by reading the full [license](/LICENSE.txt).** @@ -42,7 +42,7 @@ You can install Python modules by running `$ pip install {module}` or `$ python | `ASSISTANT_BK_MODS` | (OPTIONAL) Same format as `ASSISTANT_OWNERS` but for people who are allowed to use the `/bk_week` commands. | ### Required permissions: -**These are automatically set if you use the [official invite link](https://discord.com/oauth2/authorize?client_id=1212127255795335208&permissions=84992&integration_type=0&scope=bot) or an invite link with the permissions integer set to `84992`.** +**These are automatically set if you use the [official invite link](https://discord.com/oauth2/authorize?client_id=1212127255795335208&permissions=84992&integration_type=0&scope=bot) or an invite link with the permissions integer set to `84992`.** (The permission integer is this part of the URL `&permissions=84992`) * Send Messages * Read Message History * View Channels @@ -62,7 +62,7 @@ You can install Python modules by running `$ pip install {module}` or `$ python * write `VARIABLE_NAME="value"` + a new line for every variable. * if nano: `ctrl + O` (and press enter) then `ctrl + X`. * if VIM: press `esc` then write `:wq` (and press enter). -* Restart the terminal if you added/changed the environment variables. -* Run `$ cd path/to/extracted/folder`. -* Run `$ cargo run`. You can view a list of options by running `$ cargo run -- --help` or `$ cargo run -- -h`. +* Restart the terminal if you added/changed any environment variables. +* Run `$ cd path/to/extracted/folder` to navigate to the downloaded files (replace `path/to/extracted/folder` with your actual path). +* Run `$ cargo run` to start the program. You can view a list of options by running `$ cargo run -- --help` or `$ cargo run -- -h`. * If you only want to run the Python code, you can either run `$ cargo run -- --py`, or `$ python ./src/python/main.py`. The second option is recommended for better error output. \ No newline at end of file diff --git a/TODO.md b/TODO.md index 6f458fe..c6daea6 100644 --- a/TODO.md +++ b/TODO.md @@ -1,39 +1,12 @@ ### High priority: - - [ ] Reddit bot that scrapes images with tag "Original Art" and posts them in Discord server - [ ] Rename commands from "bk_week_{name}" to "re_{name}" - - - - - - - - - [ ] Allow disabling bk_week - - [ ] Allow updating the data autonomously and via manual commands. - - - - - - - - - - - [ ] Automatically approve posts that don't get caught by reverse image search (ris) - - - - - - - + ### Medium priority: - - [ ] View single rule (/rule {rule_name}) - [ ] Postfix calculator - [ ] Postfix generator @@ -41,7 +14,6 @@ - [ ] BPS args -> JSON - [ ] Random tip (from ByteDice.net/data/loadingScreenTips.json) - [ ] A command that just sends my socials - ### Low priority: - [ ] Particle of the week diff --git a/data/cfg_default.json b/data/cfg_default.json index 5f92ab3..96f17ae 100644 --- a/data/cfg_default.json +++ b/data/cfg_default.json @@ -1,5 +1,5 @@ { - "bk_week": { + "reddit": { "subreddits": "bytedicetesting", "fetch_limit": 100 } diff --git a/data/dc_data_preset.json b/data/dc_data_preset.json index 47bc7fb..502a5b4 100644 --- a/data/dc_data_preset.json +++ b/data/dc_data_preset.json @@ -1,13 +1,8 @@ { "servers": { "SERVER ID": { - "bk_week_channel": "CHANNEL ID INT", - "bk_mod_role": "bk mod", - "bk_mods": [ - "USER ID 1", - "USER ID 2" - ], - "bk_week_disabled": false + "re_posts_channel": 0, + "re_disabled": false } } } \ No newline at end of file diff --git a/data/lang/en.json b/data/lang/en.json index c8e2c85..e62c139 100644 --- a/data/lang/en.json +++ b/data/lang/en.json @@ -4,6 +4,7 @@ "dc_msg_bound_channel": "Successfully bound channel ID `{0}` as the \"where all collected Reddit data gets dumped\" channel!", "dc_msg_cmd_404": "No command \"{0}\" found!\nHint: Try `/help` without any arguments or `/help `", "dc_msg_corrupted_data": "Oopsies `(。>\\\\<)`. It looks like my data i-is \\**sob*\\*... c-corrupted!\n[From Byte Dice]: I have no idea what I was thinking while writing this at 2am. I'm not removing it.", + "dc_msg_data_server_404": "This server is not in the data!\n Hint: Run the command `/add_server` inside of a Discord server (requires administrator permission).", "dc_msg_dm_python_err_socket": "Unknown internal Python error occurred: Websocket response error", "dc_msg_dm_python_err": "Unknown internal Python Error: {0}", "dc_msg_embed_default_embed_desc": "Default english embed description.", @@ -17,24 +18,24 @@ "dc_msg_owner_shutdown_failed_confirmation": "Failed to shut down: Invalid confirmation.", "dc_msg_re_already_voted": "Couldn't cast a vote: You have already voted on this post!", "dc_msg_re_also_approved": "Also approved it!", - "dc_msg_re_data_corrupted": "Post URL \"<{0}>\" not found: Post data is corrupted!\n Full details: Could not find key \"bk_weekly_art_posts\" in data file \"reddit_data.json\"", - "dc_msg_re_help_removed": "Reddit help file not found, someone likely deleted it.\n Hint: You can find the official help text at https://github.com/ByteDice/ByteDiceAssistant in `bk_week_help_re.md`.\n**CAUTION:** The official help file may contain incorrect information if you're not using the official bot by Byte Dice.", + "dc_msg_re_data_corrupted": "Post URL \"<{0}>\" not found: Post data is corrupted!\n Full details: Could not find key \"posts\" in data file \"re_data.json\"", + "dc_msg_re_help_removed": "Reddit help file not found, someone likely deleted it.\n Hint: You can find the official help text at https://github.com/ByteDice/ByteDiceAssistant in `re_help.md`.\n**CAUTION:** The official help file may contain incorrect information if you're not using the official bot by Byte Dice.", "dc_msg_re_permdeny_not_re_mod": "Permission denied: You are not a moderator of the subreddit(s) {0}", - "dc_msg_re_post_404": "Post URL \"<{0}>\" not found: Post doesn't exist in the data!\n Hint: Run the command `/re_addPost [URL]` in a Discord channel or `u/ByteDiceAssistant bk_week_add` in a Reddit post.", + "dc_msg_re_post_404": "Post URL \"<{0}>\" not found: Post doesn't exist in the data!\n Hint: Run the command `/re_addpost [URL]` in a Discord channel or `u/ByteDiceAssistant add_post` in a Reddit post.", "dc_msg_re_post_add_success": "Added post with URL \"<{0}>\"!", "dc_msg_re_post_approve_success": "Successfully approved the post!", "dc_msg_re_post_disapprove_success": "Successfully disapproved the post!", "dc_msg_re_post_remove_success": "Successfully removed post!", "dc_msg_re_post_unremove_success": "Successfully un-removed post with URL \"<{0}>\"!", "dc_msg_re_post_update_success": "Updated post with URL \"<{0}>\"!", - "dc_msg_re_vote_err": "Failed to vote/un-vote: Unknown internal error.", + "dc_msg_re_posts_channel_404": "Could not find `re_posts_channel` in data!\nHint: Run `/admin_re_bindchannel` in a (preferably read-only) channel (requires administrator permission).", + "dc_msg_re_vote_err": "Failed to [vote / un-vote]: Unknown internal error.", "dc_msg_re_vote_mod_success": "Successfully voted (as moderator vote)!", "dc_msg_re_vote_remove_havent": "Couldn't remove your vote: You haven't voted on this post yet!", "dc_msg_re_vote_remove_success": "Successfully removed vote!", "dc_msg_re_vote_success": "Successfully voted!", "dc_msg_reload_cfg_python_fail": "Failed to reload configs: Failed-type response from Python.", "dc_msg_reload_cfg_success": "Successfully reloaded the configs!\nNew configs:\n```\n{0}\n```", - "dc_msg_server_data_404": "This server is not in the data!\n Hint: Run the command `/add_server` inside of a Discord server (requires administrator permission).", "dc_msg_shorturl": "ShortURL: <{0}>", "log_lang_load_success": "Successfully loaded the english language file!" } \ No newline at end of file diff --git a/data/re_data_preset.json b/data/re_data_preset.json index 587fc1c..f5ec648 100644 --- a/data/re_data_preset.json +++ b/data/re_data_preset.json @@ -1,5 +1,5 @@ { - "bk_weekly_art_posts": { + "posts": { "EXAMPLE VALUE": { "post_data": { "title": "I JUST BOUGHT THE CONTINENT OF NORTH AMERICA FOR A DOLLAR!", diff --git a/src/data.rs b/src/data.rs index 1f8a81a..133243e 100644 --- a/src/data.rs +++ b/src/data.rs @@ -4,7 +4,7 @@ use std::path::Path; use serde_json::{self, Value, json}; use tokio::sync::Mutex; -use crate::{errln, rs_println, Data, Error, BK_WEEK, LANG}; +use crate::{errln, rs_println, Data, Error, CFG_DATA_RE, LANG}; use crate::websocket::send_cmd_json; @@ -16,6 +16,8 @@ static DATA_PATH_CFG: &str = "./data/cfg.json"; static PRESET_PATH_CFG: &str = "./data/cfg_default.json"; static DATA_PATH_LANG: &str = "./data/lang/"; +pub static DC_POSTS_CHANNEL_KEY: &str = "re_posts_channel"; + pub async fn read_dc_data(data: &Data, wipe: bool) { if !Path::new(DATA_PATH_DC).exists() || wipe { @@ -88,7 +90,7 @@ fn generate_re_data() { let preset_str = fs::read_to_string(PRESET_PATH_RE).unwrap(); let mut preset_json: Value = serde_json::from_str(&preset_str).unwrap(); - if let Some(bk_week) = preset_json[BK_WEEK].as_object_mut() { + if let Some(bk_week) = preset_json[CFG_DATA_RE].as_object_mut() { bk_week.remove("EXAMPLE VALUE"); bk_week.remove("EXAMPLE VALUE DELETED"); } @@ -149,7 +151,7 @@ pub async fn dc_add_server(data: &Data, server_id: u64) -> Result<(), ()> { let servers = dc_data["servers"].as_object_mut().unwrap(); if !servers.contains_key(&server_id.to_string()) { - servers.insert(server_id.to_string(), json!({ "bk_week_channel": 0 })); + servers.insert(server_id.to_string(), json!({ DC_POSTS_CHANNEL_KEY: 0 })); } return Ok(()); @@ -170,7 +172,7 @@ pub async fn dc_bind_bk(data: &Data, server_id: u64, channel_id: u64) -> Result< let server = servers[&server_id.to_string()].as_object_mut().unwrap(); - server.insert("bk_week_channel".to_string(), channel_id.into()); + server.insert(DC_POSTS_CHANNEL_KEY.to_string(), channel_id.into()); return Ok(()); } diff --git a/src/main.rs b/src/main.rs index 13a1315..09ba2d3 100644 --- a/src/main.rs +++ b/src/main.rs @@ -59,7 +59,7 @@ struct Args { noping: bool, #[arg(long, help = "Makes the program not use the schedules.")] nosched: bool, - #[arg(long, default_value = "en", help = "Which language file to use (Do not include file extention)")] + #[arg(long, default_value = "en", help = "Which language file to use (Do not include file extensions)")] lang: String } @@ -79,7 +79,7 @@ struct Data { } -static BK_WEEK: &str = "bk_weekly_art_posts"; +static CFG_DATA_RE: &str = "posts"; pub static mut LANG: Option = None; @@ -100,19 +100,19 @@ async fn main() { .map(|s| s.parse::().expect("Failed to parse ASSISTANT_OWNERS. Invalid syntax.")) .collect(); - if args.test { println!("----- USING TEST BOT -----"); } - if args.dev { println!("----- DEV MODE ENABLED -----"); } + if args.test { println!("----- USING TEST BOT -----"); } + if args.dev { println!("----- DEV MODE ENABLED -----"); } if args.dev && args.wipe { println!("----- \"DON'T WORRY ABOUT IT\" MODE ENABLED -----"); } - if args.nosched { println!("----- NO SCHEDULES -----") } + if args.nosched { println!("----- NO SCHEDULES -----"); } if args.py && !args.rs { - println!("----- PYTHON ONLY MODE -----"); + println!("----- PYTHON ONLY MODE -----"); rs_println!("ARGS: {}", args_str); let _ = python::start(args).await; process::exit(0); } else if args.rs && ! args.py { - println!("----- RUST ONLY MODE -----"); + println!("----- RUST ONLY MODE -----"); rs_println!("ARGS: {}", args_str); start(args, own_vec_u64.clone()).await; process::exit(0); diff --git a/src/python/bot.py b/src/python/bot.py index ba57c7f..c622ce1 100644 --- a/src/python/bot.py +++ b/src/python/bot.py @@ -6,8 +6,8 @@ from macros import * import json -BK_WEEKLY: Final[str] = "bk_weekly_art_posts" -BK_WEEK: Final[str] = "bk_week" +RE_DATA_POSTS: Final[str] = "posts" +CFG_DATA_RE: Final[str] = "reddit" class Bot: @@ -22,7 +22,7 @@ class Bot: useragent: str =\ f"{username} by u/RandomPersonDotExe aka u/Byte_Dice"\ if r_id == "YmZjr4zLr2qtHdpQXtj0sBOOdJzrXQ"\ - else f"{username} (original program by u/RandomPersonDotExe aka u/Byte_Dice)" + else f"{username} (Original program by u/RandomPersonDotExe aka u/Byte_Dice)" if password is None: py_error("Environment variable \"ASSISTANT_R_PASS\" is null!") @@ -57,11 +57,11 @@ class Bot: async def update_cfg_str(self, new_cfg: str) -> bool: json_cfg = json.loads(new_cfg) - self.sr = await self.r.subreddit(json_cfg[BK_WEEK]["subreddits"]) - self.fetch_limit = json_cfg[BK_WEEK]["fetch_limit"] + self.sr = await self.r.subreddit(json_cfg[CFG_DATA_RE]["subreddits"]) + self.fetch_limit = json_cfg[CFG_DATA_RE]["fetch_limit"] return True async def update_cfg(self, new_cfg: dict) -> bool: - self.sr = await self.r.subreddit(new_cfg[BK_WEEK]["subreddits"]) - self.fetch_limit = new_cfg[BK_WEEK]["fetch_limit"] + self.sr = await self.r.subreddit(new_cfg[CFG_DATA_RE]["subreddits"]) + self.fetch_limit = new_cfg[CFG_DATA_RE]["fetch_limit"] return True \ No newline at end of file diff --git a/src/python/cmds.py b/src/python/cmds.py index 4e9b0f9..3f2e2f2 100644 --- a/src/python/cmds.py +++ b/src/python/cmds.py @@ -6,7 +6,7 @@ import data import posts -BOT_ACTION_POSTFIX = "\n\n^(I am not an AI, I am just a bot. This action was performed automatically by the way.)" +BOT_ACTION_POSTFIX = "^(I am not an AI, I am just a bot. This action was performed automatically by the way. You can report bugs and view my source code [here](https://github.com/ByteDice/ByteDiceAssistant)!)" async def make_cmd(cmd: str, bot: botPy.Bot) -> str: @@ -29,7 +29,7 @@ async def respond_to_mention(bot: botPy.Bot) -> bool: py_print(f"New mention: {truncated}") - if await is_cmd("bk_week_add", body, bot): + if await is_cmd("add_post", body, bot): await bk_week_add(mention, bot) else: @@ -57,17 +57,16 @@ async def bk_week_add(mention: models.Comment, bot: botPy.Bot): short_url = mention.submission.shortlink r = "" - bd = bot.data[botPy.BK_WEEKLY] - # TODO: ask if the messages should be changed + bd = bot.data[botPy.RE_DATA_POSTS] if short_url not in bd: posts.add_post_url(bot, short_url) r = "Successfully added this post to the data!" if short_url in bd and is_mod: if "removed" in bd[short_url]: - r = "(Mod action) Successfully un-removed this post from the data!" + r = "[MOD ACTION] Successfully un-removed this post from the data!" else: - r = "(Mod action) Successfully added this post to the data!" + r = "[MOD ACTION] Successfully added this post to the data!" post = await posts.from_url(bot, short_url) post_data = posts.get_post_details(post[1]) @@ -77,5 +76,5 @@ async def bk_week_add(mention: models.Comment, bot: botPy.Bot): r = "Could not add this post to the data. Luckily, it's already there, so there's nothing to worry about!" - await mention.reply(r + " Thank you for participating!" + BOT_ACTION_POSTFIX) + await mention.reply(r + " Thank you for participating!" + "\n\n" + BOT_ACTION_POSTFIX) await mention.mark_read() \ No newline at end of file diff --git a/src/python/data.py b/src/python/data.py index de6dfde..794b3c3 100644 --- a/src/python/data.py +++ b/src/python/data.py @@ -79,8 +79,8 @@ def read_data(bot: botPy.Bot) -> bool: with open(os.path.join(DATA_PATH, "re_data_preset.json", "r")) as f: data_preset_json = json.load(f) - data_preset_json[botPy.BK_WEEKLY].pop("EXAMPLE VALUE", None) - data_preset_json[botPy.BK_WEEKLY].pop("EXAMPLE VALUE DELETED", None) + data_preset_json[botPy.RE_DATA_POSTS].pop("EXAMPLE VALUE", None) + data_preset_json[botPy.RE_DATA_POSTS].pop("EXAMPLE VALUE DELETED", None) with open(r_path, "w") as f: json.dump(data_preset_json, f, indent = 2) @@ -126,7 +126,7 @@ async def read_cfg(bot: botPy.Bot) -> bool: def add_post_to_data(bot: botPy.Bot, new_data: PostData, bypass_conditions: bool = False) -> bool: if bypass_conditions: - bot.data[botPy.BK_WEEKLY][new_data.url] = new_data.to_json() + bot.data[botPy.RE_DATA_POSTS][new_data.url] = new_data.to_json() if bot.args["dev"]: py_print(f"Added post \"{new_data.url}\" (Conditions bypassed)") return True @@ -134,14 +134,14 @@ def add_post_to_data(bot: botPy.Bot, new_data: PostData, bypass_conditions: bool # not sure what this is for updated = False - if new_data.url not in bot.data[botPy.BK_WEEKLY] or updated: - bot.data[botPy.BK_WEEKLY][new_data.url] = new_data.to_json() + if new_data.url not in bot.data[botPy.RE_DATA_POSTS] or updated: + bot.data[botPy.RE_DATA_POSTS][new_data.url] = new_data.to_json() if bot.args["dev"]: py_print(f"Added post \"{new_data.url}\"") return True - if "removed" not in bot.data[botPy.BK_WEEKLY][new_data.url]: - updated = new_data.upvotes != bot.data[botPy.BK_WEEKLY][new_data.url]["post_data"] + if "removed" not in bot.data[botPy.RE_DATA_POSTS][new_data.url]: + updated = new_data.upvotes != bot.data[botPy.RE_DATA_POSTS][new_data.url]["post_data"] else: py_print(f"Failed to add post \"{new_data.url}\": Removed flag is True.") @@ -149,15 +149,15 @@ def add_post_to_data(bot: botPy.Bot, new_data: PostData, bypass_conditions: bool def set_approve_post(bot: botPy.Bot, approved: bool, url: str) -> bool: - if not hasattr(bot.data[botPy.BK_WEEKLY][url], "removed"): - bot.data[botPy.BK_WEEKLY][url]["approved"]["by_human"] = approved + if not hasattr(bot.data[botPy.RE_DATA_POSTS][url], "removed"): + bot.data[botPy.RE_DATA_POSTS][url]["approved"]["by_human"] = approved return True return False def remove_post(bot: botPy.Bot, url: str, removed_by: str = "UNKNOWN", reason: str = "None") -> bool: - weekly = bot.data[botPy.BK_WEEKLY] + weekly = bot.data[botPy.RE_DATA_POSTS] if url in weekly: weekly[url] = { @@ -173,7 +173,7 @@ def remove_post(bot: botPy.Bot, url: str, removed_by: str = "UNKNOWN", reason: s def remove_old_posts(bot: botPy.Bot, max_age: int) -> bool: now = int(time.time()) - weekly = bot.data[botPy.BK_WEEKLY] + weekly = bot.data[botPy.RE_DATA_POSTS] remove: list[str] = [] for url, post in weekly.items(): @@ -194,10 +194,10 @@ def set_vote_post( from_dc: bool = False, remove_vote: bool = False, ) -> bool: - if url not in bot.data[botPy.BK_WEEKLY]: + if url not in bot.data[botPy.RE_DATA_POSTS]: return False - votes = bot.data[botPy.BK_WEEKLY][url]["votes"] + votes = bot.data[botPy.RE_DATA_POSTS][url]["votes"] re_voters: set[str] = set(votes["voters_re"]) dc_voters: set[int] = set(votes["voters_dc"]) mod_voters: set[int] = set(votes["mod_voters"]) @@ -214,8 +214,8 @@ def set_vote_post( return False target_voters.add(user) - bot.data[botPy.BK_WEEKLY][url]["votes"]["voters_re"] = list(re_voters) - bot.data[botPy.BK_WEEKLY][url]["votes"]["voters_dc"] = list(dc_voters) - bot.data[botPy.BK_WEEKLY][url]["votes"]["mod_voters"] = list(mod_voters) + bot.data[botPy.RE_DATA_POSTS][url]["votes"]["voters_re"] = list(re_voters) + bot.data[botPy.RE_DATA_POSTS][url]["votes"]["voters_dc"] = list(dc_voters) + bot.data[botPy.RE_DATA_POSTS][url]["votes"]["mod_voters"] = list(mod_voters) return True diff --git a/src/re_cmds/add.rs b/src/re_cmds/add.rs index 7e24838..06e8813 100644 --- a/src/re_cmds/add.rs +++ b/src/re_cmds/add.rs @@ -2,8 +2,8 @@ use serde_json::json; use crate::data::get_mutex_data; use crate::messages::send_msg; -use crate::{data, websocket, Context, Error, BK_WEEK}; -use crate::re_cmds::generic_fns::{is_bk_mod, to_shorturl}; +use crate::{data, websocket, Context, Error, CFG_DATA_RE}; +use crate::re_cmds::generic_fns::{get_readable_subreddits, is_bk_mod, to_shorturl}; use crate::lang; #[poise::command( @@ -21,7 +21,8 @@ pub async fn cmd( ) -> Result<(), Error> { if !is_bk_mod(ctx.data().bk_mods.clone(), ctx.author().id.get()) { - send_msg(ctx, lang!("dc_msg_re_permdeny_not_re_mod"), false, false).await; // TODO: add subreddit arg to lang + let sr = get_readable_subreddits(ctx).await?; + send_msg(ctx, lang!("dc_msg_re_permdeny_not_re_mod", sr), false, false).await; return Ok(()); } @@ -31,7 +32,7 @@ pub async fn cmd( data::update_re_data(ctx.data()).await; let reddit_data = get_mutex_data(&ctx.data().reddit_data).await?; - if let Some(bk_week) = reddit_data.get(BK_WEEK) { + if let Some(bk_week) = reddit_data.get(CFG_DATA_RE) { let a = approve.unwrap_or(false); let r = websocket::send_cmd_json("add_post_url", Some(json!([&shorturl, a, true]))).await.unwrap(); diff --git a/src/re_cmds/approve.rs b/src/re_cmds/approve.rs index da3b8c2..115be82 100644 --- a/src/re_cmds/approve.rs +++ b/src/re_cmds/approve.rs @@ -1,6 +1,6 @@ use serde_json::{json, Value}; -use crate::{data::{self, get_mutex_data}, lang, messages::send_msg, re_cmds::generic_fns::is_bk_mod, websocket, Context, Error, BK_WEEK}; +use crate::{data::{self, get_mutex_data}, lang, messages::send_msg, re_cmds::generic_fns::{get_readable_subreddits, is_bk_mod}, websocket, Context, Error, CFG_DATA_RE}; use super::generic_fns::send_embed_for_removed; @@ -19,7 +19,8 @@ pub async fn cmd( ) -> Result<(), Error> { if !is_bk_mod(ctx.data().bk_mods.clone(), ctx.author().id.get()) { - send_msg(ctx, lang!("dc_msg_re_permdeny_not_re_mod"), false, false).await; + let sr = get_readable_subreddits(ctx).await?; + send_msg(ctx, lang!("dc_msg_re_permdeny_not_re_mod", sr), false, false).await; return Ok(()); } @@ -33,7 +34,7 @@ pub async fn cmd( async fn approve_cmd(ctx: Context<'_>, url: &str, reddit_data: &Value, approve: bool) { - if let Some(post) = reddit_data.get(BK_WEEK).unwrap().get(url) { + if let Some(post) = reddit_data.get(CFG_DATA_RE).unwrap().get(url) { if post.get("removed").is_some() { send_embed_for_removed(ctx, url, post).await; return; diff --git a/src/re_cmds/generic_fns.rs b/src/re_cmds/generic_fns.rs index e5a6b2d..2f104d1 100644 --- a/src/re_cmds/generic_fns.rs +++ b/src/re_cmds/generic_fns.rs @@ -1,7 +1,7 @@ use regex::Regex; use serde_json::Value; -use crate::{messages::{make_post_embed, make_removed_embed, send_embed}, Context, Error}; +use crate::{data::get_mutex_data, messages::{make_post_embed, make_removed_embed, send_embed}, Context, Error}; pub fn is_bk_mod(mod_list: Vec, uid: u64) -> bool { return mod_list.contains(&uid); @@ -31,4 +31,14 @@ pub async fn send_embed_for_removed(ctx: Context<'_>, url: &str, post: &Value) { make_removed_embed(post, url, true), true ).await; +} + + +pub async fn get_readable_subreddits(ctx: Context<'_>) -> Result { + let d = get_mutex_data(&ctx.data().cfg).await?; + let sr = d["reddit"]["subreddits"].as_str().ok_or("Item of key \"subreddit\" is not a string type.\nTrace: get_readable_subreddits -> let sr = ...")?; + let split: Vec<&str> = sr.split("+").collect(); + let join = split.join(", r/"); + + return Ok(join); } \ No newline at end of file diff --git a/src/re_cmds/get.rs b/src/re_cmds/get.rs index 7e99817..5292461 100644 --- a/src/re_cmds/get.rs +++ b/src/re_cmds/get.rs @@ -1,6 +1,6 @@ use serde_json::Value; -use crate::{data::{self, get_mutex_data}, lang, messages::send_msg, re_cmds::generic_fns::send_embed_for_post, rs_println, Context, Error, BK_WEEK}; +use crate::{data::{self, get_mutex_data}, lang, messages::send_msg, re_cmds::generic_fns::send_embed_for_post, rs_println, Context, Error, CFG_DATA_RE}; use super::generic_fns::send_embed_for_removed; @@ -30,7 +30,7 @@ pub async fn cmd( async fn get_post_from_data(ctx: Context<'_>, reddit_data: &Value, url: &str) -> Result, Error> { - if let Some(bk_week) = reddit_data.get(BK_WEEK) { + if let Some(bk_week) = reddit_data.get(CFG_DATA_RE) { if let Some(post) = bk_week.get(url) { if post.get("removed").is_some() { send_embed_for_removed(ctx, url, post).await; diff --git a/src/re_cmds/remove.rs b/src/re_cmds/remove.rs index 7a60046..d08a785 100644 --- a/src/re_cmds/remove.rs +++ b/src/re_cmds/remove.rs @@ -1,6 +1,6 @@ use serde_json::json; -use crate::{lang, messages::send_msg, re_cmds::generic_fns::is_bk_mod, websocket::send_cmd_json, Context, Error}; +use crate::{lang, messages::send_msg, re_cmds::generic_fns::{get_readable_subreddits, is_bk_mod}, websocket::send_cmd_json, Context, Error}; #[poise::command( slash_command, @@ -17,7 +17,8 @@ pub async fn cmd( ) -> Result<(), Error> { if !is_bk_mod(ctx.data().bk_mods.clone(), ctx.author().id.get()) { - send_msg(ctx, lang!("dc_msg_re_permdeny_not_re_mod"), false, false).await; + let sr = get_readable_subreddits(ctx).await?; + send_msg(ctx, lang!("dc_msg_re_permdeny_not_re_mod", sr), false, false).await; return Ok(()); } diff --git a/src/re_cmds/top.rs b/src/re_cmds/top.rs index e723d11..5f4873e 100644 --- a/src/re_cmds/top.rs +++ b/src/re_cmds/top.rs @@ -1,6 +1,6 @@ use std::collections::HashMap; -use crate::{data::get_mutex_data, re_cmds::generic_fns::send_embed_for_post, Context, Error, BK_WEEK}; +use crate::{data::get_mutex_data, re_cmds::generic_fns::send_embed_for_post, Context, Error, CFG_DATA_RE}; #[derive(poise::ChoiceParameter, PartialEq)] enum TopCategory { @@ -30,7 +30,7 @@ pub async fn cmd( ) -> Result<(), Error> { let mut all: HashMap<&str, i32> = HashMap::new(); - let posts = &get_mutex_data(&ctx.data().reddit_data).await?[BK_WEEK]; + let posts = &get_mutex_data(&ctx.data().reddit_data).await?[CFG_DATA_RE]; let posts_u = posts.as_object().unwrap(); for (url, dat) in posts_u { diff --git a/src/re_cmds/update.rs b/src/re_cmds/update.rs index 6197b6f..d522e76 100644 --- a/src/re_cmds/update.rs +++ b/src/re_cmds/update.rs @@ -3,7 +3,7 @@ use std::time::{SystemTime, UNIX_EPOCH}; use poise::{serenity_prelude::{ChannelId, EditMessage, GetMessages, Http, Message, MessageId, UserId}, ReplyHandle}; use serde_json::{json, Map, Value}; -use crate::{data::{self, get_mutex_data}, lang, messages::{edit_reply, embed_from_options, http_send_embed, make_post_embed, make_removed_embed, send_msg}, websocket::send_cmd_json, Context, Error, BK_WEEK}; +use crate::{data::{self, get_mutex_data, DC_POSTS_CHANNEL_KEY}, lang, messages::{edit_reply, embed_from_options, http_send_embed, make_post_embed, make_removed_embed, send_msg}, websocket::send_cmd_json, Context, Error, CFG_DATA_RE}; #[poise::command( slash_command, @@ -27,7 +27,7 @@ pub async fn cmd( { let http = ctx.http(); - let mut p_text = "`/bk_week_update`:".to_string(); + let mut p_text = "`/re_updatediscord`:".to_string(); let progress = send_msg(ctx, p_text.clone(), true, true).await.unwrap(); p_text = update_progress(ctx, progress.clone(), p_text, "\nFetching new posts & updating data file...".to_string()).await; @@ -42,7 +42,7 @@ pub async fn cmd( let c_id_u = get_c_id(ctx).await; if c_id_u.is_none() { - send_msg(ctx, "Could not find bk_week_channel in data!\nHint: Run (or tell an admin to run) `/bk_admin_bind` in a (preferably read-only) channel.".to_string(), true, true).await; + send_msg(ctx, lang!("dc_msg_re_posts_channel_404"), true, true).await; return Ok(()); } @@ -58,7 +58,7 @@ pub async fn cmd( // Adding new posts p_text = update_progress(ctx, progress.clone(), p_text.clone(), "✅\nAdding new posts...".to_string()).await; - let weekly_art = r_data[BK_WEEK].as_object().unwrap(); + let weekly_art = r_data[CFG_DATA_RE].as_object().unwrap(); add_posts(http, c_id, weekly_art, &msgs_json, max_age_secs).await; // Stop if only_add @@ -113,7 +113,7 @@ async fn get_c_id(ctx: Context<'_>) -> Option { let c_id_u = d["servers"] [ctx.guild_id().unwrap().to_string()] - ["bk_week_channel"].as_u64().unwrap(); + [DC_POSTS_CHANNEL_KEY].as_u64().unwrap(); let c_id = ChannelId::new(c_id_u); @@ -187,7 +187,7 @@ async fn msgs_to_json(msgs: Vec, reddit_data: &Value, max_age: u64) -> if msg_json.is_err() { continue; } let mut u_json: Value = msg_json.unwrap(); - let re_url = &reddit_data[BK_WEEK][&url]; + let re_url = &reddit_data[CFG_DATA_RE][&url]; let post_date = re_url["post_data"]["date_unix"].as_u64().unwrap_or(0); diff --git a/src/re_cmds/vote.rs b/src/re_cmds/vote.rs index 6cb05ad..f6549e4 100644 --- a/src/re_cmds/vote.rs +++ b/src/re_cmds/vote.rs @@ -1,6 +1,6 @@ use serde_json::json; -use crate::{data::{self, get_mutex_data}, lang, messages::send_msg, re_cmds::generic_fns::{is_bk_mod, send_embed_for_removed}, websocket::send_cmd_json, Context, Error, BK_WEEK}; +use crate::{data::{self, get_mutex_data}, lang, messages::send_msg, re_cmds::generic_fns::{is_bk_mod, send_embed_for_removed}, websocket::send_cmd_json, Context, Error, CFG_DATA_RE}; #[poise::command( slash_command, @@ -19,7 +19,7 @@ pub async fn cmd( data::update_re_data(ctx.data()).await; let uid = ctx.author().id.get(); let re_data = get_mutex_data(&ctx.data().reddit_data).await?; - let post_data = re_data[BK_WEEK].clone(); + let post_data = re_data[CFG_DATA_RE].clone(); let unw_vote = un_vote.unwrap_or(false); if post_data.get(&url).is_none() { diff --git a/src/schedule.rs b/src/schedule.rs index 0ab5cbd..6f915ba 100644 --- a/src/schedule.rs +++ b/src/schedule.rs @@ -5,7 +5,7 @@ use std::time::Duration; use tokio::task::JoinHandle; use tokio::time; -use crate::{lang, rs_println}; +use crate::rs_println; pub type Schedule = (Duration, fn() -> Pin + Send>>); From d9759f86d7908035403e72c68608e4b9578ff0dd Mon Sep 17 00:00:00 2001 From: Byte Dice <165556900+ByteDice@users.noreply.github.com> Date: Thu, 17 Apr 2025 02:47:01 +0200 Subject: [PATCH 05/39] Update README.md --- README.md | 27 +++++++++++++++++++-------- 1 file changed, 19 insertions(+), 8 deletions(-) diff --git a/README.md b/README.md index 4694233..368db55 100644 --- a/README.md +++ b/README.md @@ -49,20 +49,31 @@ You can install Python modules by running `$ pip install {module}` or `$ python * Embed Links ### How to run: +### Short answer for experienced people: +* Download the code. +* Set the environment variables (listed above). +* Restart the terminal. +* Navigate to the project root directory. +* Run with `cargo run`. + * Alrenatively, run `cargo run -- {args here}` to add args. + * For help, run `cargo run -- -h` or `cargo run -- --help`. + * To only run the Python part, use `cargo run -- --py`, or for a better error output, `python ./src/python/main.py` + +### Long answer for absolute beginners: * Download the code (and extract it if needed). * Open a terminal. -* Set the environment variables. +* Set the environment variables (listed above). * On Windows: - * Run `$ setx VARIABLE_NAME "value"` in a terminal. + * Run `setx VARIABLE_NAME "value in quotes"` in a terminal. * On Unix (Linux / Mac): - * Run `$ sudo nano /etc/environment` or `$ sudo vim /etc/environment` in the terminal (and enter your password if prompted to). - * press `i` (only if you used VIM). - * write `VARIABLE_NAME="value"` + a new line for every variable. + * Run `sudo nano /etc/environment` or `sudo vim /etc/environment` in the terminal (and enter your password if prompted to). + * Press `i` (only if you used VIM). + * Write `VARIABLE_NAME="value"` + a new line for every variable. * if nano: `ctrl + O` (and press enter) then `ctrl + X`. * if VIM: press `esc` then write `:wq` (and press enter). * Restart the terminal if you added/changed any environment variables. -* Run `$ cd path/to/extracted/folder` to navigate to the downloaded files (replace `path/to/extracted/folder` with your actual path). -* Run `$ cargo run` to start the program. You can view a list of options by running `$ cargo run -- --help` or `$ cargo run -- -h`. - * If you only want to run the Python code, you can either run `$ cargo run -- --py`, or `$ python ./src/python/main.py`. The second option is recommended for better error output. \ No newline at end of file +* Run `cd path/to/extracted/folder` to navigate to the downloaded files (replace `path/to/extracted/folder` with your actual path). +* Run `cargo run` to start the program. You can view a list of options by running `cargo run -- --help` or `cargo run -- -h`. + * If you only want to run the Python code, you can either run `cargo run -- --py` or `python ./src/python/main.py`. The second option is recommended for better error output. \ No newline at end of file From 823a354a6bec408d9a52a5c7a2478e4a127250ee Mon Sep 17 00:00:00 2001 From: Byte Dice <165556900+ByteDice@users.noreply.github.com> Date: Thu, 17 Apr 2025 02:49:36 +0200 Subject: [PATCH 06/39] Update TODO.md --- TODO.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/TODO.md b/TODO.md index c6daea6..fa6c258 100644 --- a/TODO.md +++ b/TODO.md @@ -26,3 +26,6 @@ * Shows a TLDR with pros/cons on a programming language - [ ] PowerPlate info viewer * Shows basic info on a PowerPlate + +### Random little things: +- [ ] Update ASSISTANT_BK_MODS env var name to be reddit focused instead of bk focused \ No newline at end of file From 7922bc50a9b11d3d638ba4dc6f401b380fc68879 Mon Sep 17 00:00:00 2001 From: ByteDice Date: Thu, 5 Jun 2025 23:15:55 +0200 Subject: [PATCH 07/39] restructured cmds.rs to multiple files --- README.md | 4 +- TODO.md | 16 +- src/cmds.rs | 495 ---------------------------------------- src/cmds/add_server.rs | 28 +++ src/cmds/eight_ball.rs | 31 +++ src/cmds/embed.rs | 52 +++++ src/cmds/help.rs | 250 ++++++++++++++++++++ src/cmds/ping.rs | 20 ++ src/cmds/reload_cfg.rs | 36 +++ src/cmds/send.rs | 21 ++ src/cmds/stop.rs | 42 ++++ src/gen.rs | 18 +- src/main.rs | 12 +- src/re_cmds/shorturl.rs | 26 +++ 14 files changed, 529 insertions(+), 522 deletions(-) delete mode 100644 src/cmds.rs create mode 100644 src/cmds/add_server.rs create mode 100644 src/cmds/eight_ball.rs create mode 100644 src/cmds/embed.rs create mode 100644 src/cmds/help.rs create mode 100644 src/cmds/ping.rs create mode 100644 src/cmds/reload_cfg.rs create mode 100644 src/cmds/send.rs create mode 100644 src/cmds/stop.rs create mode 100644 src/re_cmds/shorturl.rs diff --git a/README.md b/README.md index 368db55..acb926c 100644 --- a/README.md +++ b/README.md @@ -39,7 +39,7 @@ You can install Python modules by running `$ pip install {module}` or `$ python | `ASSISTANT_R_NAME` | The username of the Reddit bot/account. | | `ASSISTANT_R_PASS` | The password for the Reddit bot/account. | | `ASSISTANT_OWNERS` | (OPTIONAL) A list of Discord user IDs that "own" the bot. Separate each ID with a single comma and **no** spaces. This will allow the specified user IDs to run root commands such as `/stop`, it will also DM these users when *certain* errors occur. | -| `ASSISTANT_BK_MODS` | (OPTIONAL) Same format as `ASSISTANT_OWNERS` but for people who are allowed to use the `/bk_week` commands. | +| `ASSISTANT_BK_MODS` | (OPTIONAL) Same format as `ASSISTANT_OWNERS` but for people who are allowed to use the `/re_*` commands. | ### Required permissions: **These are automatically set if you use the [official invite link](https://discord.com/oauth2/authorize?client_id=1212127255795335208&permissions=84992&integration_type=0&scope=bot) or an invite link with the permissions integer set to `84992`.** (The permission integer is this part of the URL `&permissions=84992`) @@ -55,7 +55,7 @@ You can install Python modules by running `$ pip install {module}` or `$ python * Restart the terminal. * Navigate to the project root directory. * Run with `cargo run`. - * Alrenatively, run `cargo run -- {args here}` to add args. + * Alternatively, run `cargo run -- {args here}` to add args. * For help, run `cargo run -- -h` or `cargo run -- --help`. * To only run the Python part, use `cargo run -- --py`, or for a better error output, `python ./src/python/main.py` diff --git a/TODO.md b/TODO.md index fa6c258..e242ade 100644 --- a/TODO.md +++ b/TODO.md @@ -7,25 +7,11 @@ ### Medium priority: -- [ ] View single rule (/rule {rule_name}) - [ ] Postfix calculator - [ ] Postfix generator -- [ ] JSON -> BPS class init -- [ ] BPS args -> JSON - [ ] Random tip (from ByteDice.net/data/loadingScreenTips.json) - [ ] A command that just sends my socials ### Low priority: -- [ ] Particle of the week - * Starts a 1 week contest where people make particles based on a theme using BDE_ParticleSys -- [ ] Weekly coding competition - * Same as particle of the week but with coding - [ ] Content update sender - * Automatically sends sneak peeks (like commit history or manual) of projects when they're updated -- [ ] Language TLDR command - * Shows a TLDR with pros/cons on a programming language -- [ ] PowerPlate info viewer - * Shows basic info on a PowerPlate - -### Random little things: -- [ ] Update ASSISTANT_BK_MODS env var name to be reddit focused instead of bk focused \ No newline at end of file + * Automatically sends sneak peeks (like commit history or manual) of projects when they're updated \ No newline at end of file diff --git a/src/cmds.rs b/src/cmds.rs deleted file mode 100644 index 5fb9afb..0000000 --- a/src/cmds.rs +++ /dev/null @@ -1,495 +0,0 @@ -use std::collections::HashMap; -use std::process; -use std::error::Error as StdErr; - -use crate::data::{dc_add_server, get_mutex_data, read_cfg_data}; -use crate::re_cmds::generic_fns::to_shorturl; -use crate::websocket::send_cmd_json; -use crate::{data, lang, Context, Data, Error}; -use crate::messages::{edit_reply, send_embed, send_msg, Author, EmbedOptions}; - -use poise::serenity_prelude::{OnlineStatus, Timestamp}; -use poise::Command; -use rand::{seq::IteratorRandom, Rng}; -use serde_json::json; -use tokio::fs; - - -// TODO: separate to multiple files - - -#[derive(poise::ChoiceParameter, PartialEq)] -enum HelpOptions { - Admin, - All, - BkWeek, - BkWeekReddit, - Generic -} - - -type Cmd = Command>; - - -#[poise::command( - slash_command, - prefix_command, - category = "fun", - required_bot_permissions = "SEND_MESSAGES | VIEW_CHANNEL" -)] -/// Check if you have connection to the bot. -pub async fn ping( - ctx: Context<'_>, - #[description = "The text to echo back."] text: Option, -) -> Result<(), Error> -{ - send_msg(ctx, text.unwrap_or_else(|| "Pong".to_string()), true, true).await; - - return Ok(()); -} - - -#[poise::command( - slash_command, - prefix_command, - category = "owner", - owners_only, - required_bot_permissions = "SEND_MESSAGES | VIEW_CHANNEL" -)] -/// I have security measures, even in developer mode. You wont access this without being a bot "owner". -pub async fn stop( - ctx: Context<'_>, - #[description = "Type \"i want to stop the bot now\" to confirm."] confirmation: Option, -) -> Result<(), Error> -{ - let should_stop = ctx.data().args.dev - || confirmation.unwrap_or_default().to_lowercase() == "i want to stop the bot now"; - - if should_stop { - let msg = send_msg(ctx, lang!("dc_msg_owner_data_save"), true, true).await.unwrap(); - data::write_dc_data(ctx.data()).await; - data::write_re_data().await; - send_cmd_json("stop_praw", None).await; - - edit_reply(ctx, msg, lang!("dc_msg_owner_data_save_complete")).await; - ctx.serenity_context().set_presence(None, OnlineStatus::Invisible); - ctx.framework().shard_manager.shutdown_all().await; - - process::exit(0); - } - else { - send_msg(ctx, lang!("dc_msg_owner_shutdown_failed_confirmation"), true, true).await; - } - - return Ok(()); -} - - -#[allow(clippy::too_many_arguments)] -#[poise::command( - slash_command, - prefix_command, - category = "owner", - owners_only, - required_bot_permissions = "SEND_MESSAGES | VIEW_CHANNEL | EMBED_LINKS" -)] -/// Creates an embed. -pub async fn embed( - ctx: Context<'_>, - #[description = "Title of embed."] title: Option, - #[description = "Body text of embed."] description: String, - #[description = "Color of side strip."] color: Option, - #[description = "A URL the title is bound to."] url: Option, - #[description = "Timestamp at bottom (best to leave empty)."] timestamp: Option, - #[description = "Ephemeral (only visible to you)."] ephemeral: Option, - #[description = "Shows \"used {Command}\" reply text."] reply: Option, - #[description = "Text that appears above and outside of the embed."] message: Option, - #[description = "A URL for a thumbnail image."] thumbnail: Option, - #[description = "Sets yourself as the author."] author: Option -) -> Result<(), Error> -{ - let reply_unwrap = reply.unwrap_or(false); - - send_embed( - ctx, - EmbedOptions { - desc: description.replace("\\n", "\n"), - title: title.map(|t| t.replace("\\n", "\n")), - col: color, - url, - ts: timestamp, - ephemeral: ephemeral.unwrap_or(false), - message, - thumbnail, - author: if author.unwrap_or(false) { Some(Author { name: ctx.author().name.clone(), url: "".to_string(), icon_url: ctx.author().avatar_url().unwrap() }) } else { None } - }, - reply_unwrap - ).await; - - if !reply_unwrap { - send_msg(ctx, lang!("dc_msg_mandatory_response"), true, true).await; - } - - return Ok(()); -} - - - -#[poise::command( - slash_command, - prefix_command, - category = "owner", - owners_only, - required_bot_permissions = "SEND_MESSAGES | VIEW_CHANNEL" -)] -/// Sends a message. -pub async fn send( - ctx: Context<'_>, - #[description = "The message to send (NOT EPHEMERAL)"] msg: String -) -> Result<(), Error> -{ - send_msg(ctx, msg.replace("\\n", "\n"), false, false).await; - send_msg(ctx, lang!("dc_msg_mandatory_response"), true, true).await; - return Ok(()); -} - - - -#[poise::command( - slash_command, - prefix_command, - category = "fun", - rename = "8_ball", - required_bot_permissions = "SEND_MESSAGES | VIEW_CHANNEL" -)] -/// Magic 8-ball. Ask a question, get an answer. -pub async fn eight_ball( - ctx: Context<'_>, - #[description = "Question to ask."] question: String -) -> Result<(), Error> -{ - let is_quirky = rand::rng().random_bool(0.2); - let list = &ctx.data().ball_prompts[if is_quirky { 1 } else { 0 }]; - let rand_item = list.iter().choose(&mut rand::rng()); - - send_msg( - ctx, - lang!("dc_msg_8-ball_answer", question, rand_item.unwrap()), - true, - true - ).await; - - return Ok(()); -} - - -#[poise::command( - slash_command, - prefix_command, - required_bot_permissions = "SEND_MESSAGES | VIEW_CHANNEL" -)] -/// Convert a long reddit URL to a short one. The bot ONLY uses shortURLs when asking for one. -pub async fn re_shorturl( - ctx: Context<'_>, - #[description = "A Reddit post URL"] url: String -) -> Result<(), Error> -{ - let shorturl = to_shorturl(&url); - - if shorturl.is_ok() { - send_msg(ctx, lang!("dc_msg_shorturl", shorturl.unwrap()), true, true).await; - } - else { - send_msg(ctx, lang!("dc_msg_failed_shorturl_conversion"), true, true).await; - } - - return Ok(()); -} - - -#[poise::command( - slash_command, - prefix_command, - category = "admin", - default_member_permissions = "ADMINISTRATOR", - guild_only, - required_bot_permissions = "SEND_MESSAGES | VIEW_CHANNEL" -)] -/// Add your server to my database so I can sell it! (/s), I only store some minimal data the bot needs. -pub async fn add_server( - ctx: Context<'_> -) -> Result<(), Error> -{ - let r = dc_add_server(ctx.data(), ctx.guild_id().unwrap().into()).await; - - if r.is_ok() { - send_msg(ctx, lang!("dc_msg_added_to_data"), true, true).await; - } - else { - send_msg(ctx, lang!("dc_msg_corrupted_data"), true, true).await; - } - - return Ok(()); -} - - -#[poise::command( - slash_command, - prefix_command, - category = "owner", - owners_only, - required_bot_permissions = "SEND_MESSAGES | VIEW_CHANNEL" -)] -/// Reloads the entire config file. -pub async fn reload_cfg( - ctx: Context<'_> -) -> Result<(), Error> -{ - read_cfg_data(&ctx.data(), false).await; - let d = get_mutex_data(&ctx.data().cfg).await?; - let d_str = serde_json::to_string(&d)?; - let r = send_cmd_json("update_cfg", Some(json!([d_str]))).await; - - if r.is_some() && r.unwrap()["value"].as_bool().unwrap() { - send_msg( - ctx, - lang!("dc_msg_reload_cfg_success", serde_json::to_string_pretty(&d).unwrap()), - true, - true - ).await; - return Ok(()); - } - - send_msg(ctx, lang!("dc_msg_reload_cfg_python_fail"), true, true).await; - return Ok(()); -} - - -#[poise::command( - slash_command, - prefix_command, - category = "help", - required_bot_permissions = "SEND_MESSAGES | VIEW_CHANNEL" -)] -/// Shows helpful information on how to use the bk_week section of the bot. -pub async fn help( - ctx: Context<'_>, - #[description = "A full category of commands."] category: Option, - #[description = "The name of a single command. This argument will be prioritized over `category`."] command: Option -) -> Result<(), Error> -{ - match (category.is_some(), command.is_some()) { - (false, false) => send_category_help(ctx, HelpOptions::Generic).await, - (true, false) => send_category_help(ctx, category.unwrap()).await, - _ => send_single_help(ctx, command.unwrap()).await - } - - return Ok(()); -} - - -fn format_cmd(cmd: &Cmd) -> String { - let arg_names: Vec<_> = cmd.parameters - .iter() - .map(|a| a.name.as_str()) - .collect(); - - let cmds_max_len = arg_names - .iter() - .max_by_key(|&&s| s.len()) - .map(|&s| s.len()) - .unwrap_or(0); - - let args_format: Vec<_> = cmd.parameters - .iter() - .map( - |a| - format!( - "{} {}{}{}", - a.name, - " ".repeat(cmds_max_len - a.name.len()), - if !a.required { "(OPTIONAL) " } else { "" }, - a.description.as_ref().unwrap_or(&"".to_string()) - ) - ) - .collect(); - - let t = format!( - "**`{}`**:\n```{}```", - cmd.name, - args_format.join("\n") - ); - - return t; -} - - -async fn send_single_help(ctx: Context<'_>, mut cmd_name: String) { - if cmd_name.starts_with("/") { cmd_name = cmd_name[1..].to_string(); } - let cmds = &ctx.framework().options().commands; - let cmd = cmds.iter().find(|c| c.name == cmd_name); - - if cmd.is_none() { - send_msg( - ctx, - lang!("dc_msg_cmd_404", cmd_name), - true, - true - ).await; - - return; - } - - let t = format_cmd(cmd.unwrap()); - send_msg(ctx, t, true, true).await; -} - - -async fn send_category_help(ctx: Context<'_>, category: HelpOptions) { - match category { - HelpOptions::BkWeekReddit => send_bk_week_help_re(ctx).await, - HelpOptions::BkWeek => send_bk_week_help (ctx).await, - HelpOptions::Generic => send_generic_help (ctx).await, - HelpOptions::Admin => send_admin_help (ctx).await, - HelpOptions::All => send_all_help (ctx).await - } -} - - -async fn send_bk_week_help_re(ctx: Context<'_>) { - let t: String = fs::read_to_string("./bk_week_help_re.md").await - .unwrap_or(lang!("dc_msg_re_help_removed")); - - send_msg(ctx, t, true, true).await; -} - - -fn format_cmds(cmds: Vec<(&str, Vec<&Cmd>)>) -> String { - let cmd_names: Vec<_> = cmds - .iter() - .flat_map( - |t| - t.1.iter().map(|c| c.name.as_str()) - ) - .collect(); - - let cmds_max_len = cmd_names - .iter() - .max_by_key(|&&s| s.len()) - .map(|&s| s.len()) - .unwrap_or(0); - - let mut categories: Vec<(&str, Vec)> = Vec::new(); - - for c_tuple in cmds { - let cmds_format: Vec<_> = c_tuple.1 - .iter() - .map( - |c| - format!( - "{} {}{}", - c.name, - " ".repeat(cmds_max_len - c.name.len()), - c.description.as_ref().unwrap_or(&"".to_string()) - ) - ) - .collect(); - - categories.push((c_tuple.0, cmds_format)); - } - - let c_text: Vec = categories - .iter() - .map(|i| format!("{}:\n {}", i.0, i.1.join("\n "))) - .collect(); - - let t = format!("```{}```", c_text.join("\n\n")); - return t; -} - - -fn separate_by_category(cmds: Vec<&Cmd>) -> Vec<(String, Vec<&Cmd>)> { - let mut grouped: HashMap> = HashMap::new(); - - for cmd in cmds { - grouped - .entry(cmd.category.clone().unwrap_or("No category".to_string())) - .or_insert_with(Vec::new).push(cmd); - } - - return grouped.into_iter().collect(); -} - - -async fn send_bk_week_help(ctx: Context<'_>) { - let cmds = &ctx.framework().options().commands; - let bk_week_cmds: Vec<_> = cmds - .iter() - .filter(|cmd| cmd.category == Some("re".to_string())) - .collect(); - - let t = format_cmds(vec![("re", bk_week_cmds)]); - send_msg(ctx, t, true, true).await; -} - - -async fn send_generic_help(ctx: Context<'_>) { - let cmds = &ctx.framework().options().commands; - let filtered_cmds: Vec<_> = cmds - .iter() - .filter( - |cmd| - cmd.category != Some("re".to_string()) - || cmd.category != Some("owner".to_string()) - || cmd.category != Some("admin".to_string()) - ) - .collect(); - - let categories = separate_by_category(filtered_cmds); - - let cmds_format: Vec<(&str, Vec<&Cmd>)> = categories - .iter() - .map(|c| (c.0.as_str(), c.1.clone())) - .collect(); - - let t = format_cmds(cmds_format); - send_msg(ctx, t, true, true).await; -} - - -async fn send_admin_help(ctx: Context<'_>) { - let cmds = &ctx.framework().options().commands; - let filtered_cmds: Vec<_> = cmds - .iter() - .filter( - |cmd| - cmd.category == Some("owner".to_string()) - || cmd.category == Some("admin".to_string()) - ) - .collect(); - - let categories = separate_by_category(filtered_cmds); - - let cmds_format: Vec<(&str, Vec<&Cmd>)> = categories - .iter() - .map(|c| (c.0.as_str(), c.1.clone())) - .collect(); - - let t = format_cmds(cmds_format); - send_msg(ctx, t, true, true).await; -} - - -async fn send_all_help(ctx: Context<'_>) { - let cmds = &ctx.framework().options().commands; - let cmds_clone: Vec<_> = cmds.iter().clone().collect(); - let categories = separate_by_category(cmds_clone); - - let cmds_format: Vec<(&str, Vec<&Cmd>)> = categories - .iter() - .map(|c| (c.0.as_str(), c.1.clone())) - .collect(); - - let t = format_cmds(cmds_format); - send_msg(ctx, t, true, true).await; -} \ No newline at end of file diff --git a/src/cmds/add_server.rs b/src/cmds/add_server.rs new file mode 100644 index 0000000..c09f3a4 --- /dev/null +++ b/src/cmds/add_server.rs @@ -0,0 +1,28 @@ +use crate::{data::dc_add_server, lang, messages::send_msg, Context, Error}; + + +#[poise::command( + slash_command, + prefix_command, + rename = "add_server", + category = "admin", + default_member_permissions = "ADMINISTRATOR", + guild_only, + required_bot_permissions = "SEND_MESSAGES | VIEW_CHANNEL" +)] +/// Add your server to my database so I can sell it! (/s), I only store some minimal data the bot needs. +pub async fn cmd( + ctx: Context<'_> +) -> Result<(), Error> +{ + let r = dc_add_server(ctx.data(), ctx.guild_id().unwrap().into()).await; + + if r.is_ok() { + send_msg(ctx, lang!("dc_msg_added_to_data"), true, true).await; + } + else { + send_msg(ctx, lang!("dc_msg_corrupted_data"), true, true).await; + } + + return Ok(()); +} \ No newline at end of file diff --git a/src/cmds/eight_ball.rs b/src/cmds/eight_ball.rs new file mode 100644 index 0000000..8100688 --- /dev/null +++ b/src/cmds/eight_ball.rs @@ -0,0 +1,31 @@ +use rand::{seq::IteratorRandom, Rng}; + +use crate::{lang, messages::send_msg, Context, Error}; + + +#[poise::command( + slash_command, + prefix_command, + category = "fun", + rename = "8_ball", + required_bot_permissions = "SEND_MESSAGES | VIEW_CHANNEL" +)] +/// Magic 8-ball. Ask a question, get an answer. +pub async fn cmd( + ctx: Context<'_>, + #[description = "Question to ask."] question: String +) -> Result<(), Error> +{ + let is_quirky = rand::rng().random_bool(0.2); + let list = &ctx.data().ball_prompts[if is_quirky { 1 } else { 0 }]; + let rand_item = list.iter().choose(&mut rand::rng()); + + send_msg( + ctx, + lang!("dc_msg_8-ball_answer", question, rand_item.unwrap()), + true, + true + ).await; + + return Ok(()); +} \ No newline at end of file diff --git a/src/cmds/embed.rs b/src/cmds/embed.rs new file mode 100644 index 0000000..ebae7ae --- /dev/null +++ b/src/cmds/embed.rs @@ -0,0 +1,52 @@ +use poise::serenity_prelude::Timestamp; + +use crate::{lang, messages::{send_embed, send_msg, Author, EmbedOptions}, Context, Error}; + +#[allow(clippy::too_many_arguments)] +#[poise::command( + slash_command, + prefix_command, + rename = "embed", + category = "owner", + owners_only, + required_bot_permissions = "SEND_MESSAGES | VIEW_CHANNEL | EMBED_LINKS" +)] +/// Creates an embed. +pub async fn cmd( + ctx: Context<'_>, + #[description = "Title of embed."] title: Option, + #[description = "Body text of embed."] description: String, + #[description = "Color of side strip."] color: Option, + #[description = "A URL the title is bound to."] url: Option, + #[description = "Timestamp at bottom (best to leave empty)."] timestamp: Option, + #[description = "Ephemeral (only visible to you)."] ephemeral: Option, + #[description = "Shows \"used {Command}\" reply text."] reply: Option, + #[description = "Text that appears above and outside of the embed."] message: Option, + #[description = "A URL for a thumbnail image."] thumbnail: Option, + #[description = "Sets yourself as the author."] author: Option +) -> Result<(), Error> +{ + let reply_unwrap = reply.unwrap_or(false); + + send_embed( + ctx, + EmbedOptions { + desc: description.replace("\\n", "\n"), + title: title.map(|t| t.replace("\\n", "\n")), + col: color, + url, + ts: timestamp, + ephemeral: ephemeral.unwrap_or(false), + message, + thumbnail, + author: if author.unwrap_or(false) { Some(Author { name: ctx.author().name.clone(), url: "".to_string(), icon_url: ctx.author().avatar_url().unwrap() }) } else { None } + }, + reply_unwrap + ).await; + + if !reply_unwrap { + send_msg(ctx, lang!("dc_msg_mandatory_response"), true, true).await; + } + + return Ok(()); +} \ No newline at end of file diff --git a/src/cmds/help.rs b/src/cmds/help.rs new file mode 100644 index 0000000..d197600 --- /dev/null +++ b/src/cmds/help.rs @@ -0,0 +1,250 @@ +use poise::Command; +use std::{collections::HashMap, error::Error as StdErr}; +use tokio::fs; + +use crate::{lang, messages::send_msg, Context, Data, Error}; + + +#[derive(poise::ChoiceParameter, PartialEq)] +enum HelpOptions { + Admin, + All, + BkWeek, + BkWeekReddit, + Generic +} + + +type Cmd = Command>; + + + +#[poise::command( + slash_command, + prefix_command, + rename = "help", + category = "help", + required_bot_permissions = "SEND_MESSAGES | VIEW_CHANNEL" +)] +/// Shows helpful information on how to use the bk_week section of the bot. +pub async fn cmd( + ctx: Context<'_>, + #[description = "A full category of commands."] category: Option, + #[description = "The name of a single command. This argument will be prioritized over `category`."] command: Option +) -> Result<(), Error> +{ + match (category.is_some(), command.is_some()) { + (false, false) => send_category_help(ctx, HelpOptions::Generic).await, + (true, false) => send_category_help(ctx, category.unwrap()).await, + _ => send_single_help(ctx, command.unwrap()).await + } + + return Ok(()); +} + + +fn format_cmd(cmd: &Cmd) -> String { + let arg_names: Vec<_> = cmd.parameters + .iter() + .map(|a| a.name.as_str()) + .collect(); + + let cmds_max_len = arg_names + .iter() + .max_by_key(|&&s| s.len()) + .map(|&s| s.len()) + .unwrap_or(0); + + let args_format: Vec<_> = cmd.parameters + .iter() + .map( + |a| + format!( + "{} {}{}{}", + a.name, + " ".repeat(cmds_max_len - a.name.len()), + if !a.required { "(OPTIONAL) " } else { "" }, + a.description.as_ref().unwrap_or(&"".to_string()) + ) + ) + .collect(); + + let t = format!( + "**`{}`**:\n```{}```", + cmd.name, + args_format.join("\n") + ); + + return t; +} + + +async fn send_single_help(ctx: Context<'_>, mut cmd_name: String) { + if cmd_name.starts_with("/") { cmd_name = cmd_name[1..].to_string(); } + let cmds = &ctx.framework().options().commands; + let cmd = cmds.iter().find(|c| c.name == cmd_name); + + if cmd.is_none() { + send_msg( + ctx, + lang!("dc_msg_cmd_404", cmd_name), + true, + true + ).await; + + return; + } + + let t = format_cmd(cmd.unwrap()); + send_msg(ctx, t, true, true).await; +} + + +async fn send_category_help(ctx: Context<'_>, category: HelpOptions) { + match category { + HelpOptions::BkWeekReddit => send_bk_week_help_re(ctx).await, + HelpOptions::BkWeek => send_bk_week_help (ctx).await, + HelpOptions::Generic => send_generic_help (ctx).await, + HelpOptions::Admin => send_admin_help (ctx).await, + HelpOptions::All => send_all_help (ctx).await + } +} + + +async fn send_bk_week_help_re(ctx: Context<'_>) { + let t: String = fs::read_to_string("./bk_week_help_re.md").await + .unwrap_or(lang!("dc_msg_re_help_removed")); + + send_msg(ctx, t, true, true).await; +} + + +fn format_cmds(cmds: Vec<(&str, Vec<&Cmd>)>) -> String { + let cmd_names: Vec<_> = cmds + .iter() + .flat_map( + |t| + t.1.iter().map(|c| c.name.as_str()) + ) + .collect(); + + let cmds_max_len = cmd_names + .iter() + .max_by_key(|&&s| s.len()) + .map(|&s| s.len()) + .unwrap_or(0); + + let mut categories: Vec<(&str, Vec)> = Vec::new(); + + for c_tuple in cmds { + let cmds_format: Vec<_> = c_tuple.1 + .iter() + .map( + |c| + format!( + "{} {}{}", + c.name, + " ".repeat(cmds_max_len - c.name.len()), + c.description.as_ref().unwrap_or(&"".to_string()) + ) + ) + .collect(); + + categories.push((c_tuple.0, cmds_format)); + } + + let c_text: Vec = categories + .iter() + .map(|i| format!("{}:\n {}", i.0, i.1.join("\n "))) + .collect(); + + let t = format!("```{}```", c_text.join("\n\n")); + return t; +} + + +fn separate_by_category(cmds: Vec<&Cmd>) -> Vec<(String, Vec<&Cmd>)> { + let mut grouped: HashMap> = HashMap::new(); + + for cmd in cmds { + grouped + .entry(cmd.category.clone().unwrap_or("No category".to_string())) + .or_insert_with(Vec::new).push(cmd); + } + + return grouped.into_iter().collect(); +} + + +async fn send_bk_week_help(ctx: Context<'_>) { + let cmds = &ctx.framework().options().commands; + let bk_week_cmds: Vec<_> = cmds + .iter() + .filter(|cmd| cmd.category == Some("re".to_string())) + .collect(); + + let t = format_cmds(vec![("re", bk_week_cmds)]); + send_msg(ctx, t, true, true).await; +} + + +async fn send_generic_help(ctx: Context<'_>) { + let cmds = &ctx.framework().options().commands; + let filtered_cmds: Vec<_> = cmds + .iter() + .filter( + |cmd| + cmd.category != Some("re".to_string()) + || cmd.category != Some("owner".to_string()) + || cmd.category != Some("admin".to_string()) + ) + .collect(); + + let categories = separate_by_category(filtered_cmds); + + let cmds_format: Vec<(&str, Vec<&Cmd>)> = categories + .iter() + .map(|c| (c.0.as_str(), c.1.clone())) + .collect(); + + let t = format_cmds(cmds_format); + send_msg(ctx, t, true, true).await; +} + + +async fn send_admin_help(ctx: Context<'_>) { + let cmds = &ctx.framework().options().commands; + let filtered_cmds: Vec<_> = cmds + .iter() + .filter( + |cmd| + cmd.category == Some("owner".to_string()) + || cmd.category == Some("admin".to_string()) + ) + .collect(); + + let categories = separate_by_category(filtered_cmds); + + let cmds_format: Vec<(&str, Vec<&Cmd>)> = categories + .iter() + .map(|c| (c.0.as_str(), c.1.clone())) + .collect(); + + let t = format_cmds(cmds_format); + send_msg(ctx, t, true, true).await; +} + + +async fn send_all_help(ctx: Context<'_>) { + let cmds = &ctx.framework().options().commands; + let cmds_clone: Vec<_> = cmds.iter().clone().collect(); + let categories = separate_by_category(cmds_clone); + + let cmds_format: Vec<(&str, Vec<&Cmd>)> = categories + .iter() + .map(|c| (c.0.as_str(), c.1.clone())) + .collect(); + + let t = format_cmds(cmds_format); + send_msg(ctx, t, true, true).await; +} \ No newline at end of file diff --git a/src/cmds/ping.rs b/src/cmds/ping.rs new file mode 100644 index 0000000..d62b114 --- /dev/null +++ b/src/cmds/ping.rs @@ -0,0 +1,20 @@ +use crate::{messages::send_msg, Context, Error}; + + +#[poise::command( + slash_command, + prefix_command, + rename = "ping", + category = "fun", + required_bot_permissions = "SEND_MESSAGES | VIEW_CHANNEL" +)] +/// Check if you have connection to the bot. +pub async fn cmd( + ctx: Context<'_>, + #[description = "The text to echo back."] text: Option, +) -> Result<(), Error> +{ + send_msg(ctx, text.unwrap_or_else(|| "Pong".to_string()), true, true).await; + + return Ok(()); +} \ No newline at end of file diff --git a/src/cmds/reload_cfg.rs b/src/cmds/reload_cfg.rs new file mode 100644 index 0000000..737fbb1 --- /dev/null +++ b/src/cmds/reload_cfg.rs @@ -0,0 +1,36 @@ +use serde_json::json; + +use crate::{data::{get_mutex_data, read_cfg_data}, lang, messages::send_msg, websocket::send_cmd_json, Context, Error}; + + +#[poise::command( + slash_command, + prefix_command, + rename = "reload_cfg", + category = "owner", + owners_only, + required_bot_permissions = "SEND_MESSAGES | VIEW_CHANNEL" +)] +/// Reloads the entire config file. +pub async fn cmd( + ctx: Context<'_> +) -> Result<(), Error> +{ + read_cfg_data(&ctx.data(), false).await; + let d = get_mutex_data(&ctx.data().cfg).await?; + let d_str = serde_json::to_string(&d)?; + let r = send_cmd_json("update_cfg", Some(json!([d_str]))).await; + + if r.is_some() && r.unwrap()["value"].as_bool().unwrap() { + send_msg( + ctx, + lang!("dc_msg_reload_cfg_success", serde_json::to_string_pretty(&d).unwrap()), + true, + true + ).await; + return Ok(()); + } + + send_msg(ctx, lang!("dc_msg_reload_cfg_python_fail"), true, true).await; + return Ok(()); +} \ No newline at end of file diff --git a/src/cmds/send.rs b/src/cmds/send.rs new file mode 100644 index 0000000..eee3085 --- /dev/null +++ b/src/cmds/send.rs @@ -0,0 +1,21 @@ +use crate::{lang, messages::send_msg, Context, Error}; + + +#[poise::command( + slash_command, + prefix_command, + rename = "send", + category = "owner", + owners_only, + required_bot_permissions = "SEND_MESSAGES | VIEW_CHANNEL" +)] +/// Sends a message. +pub async fn cmd( + ctx: Context<'_>, + #[description = "The message to send (NOT EPHEMERAL)"] msg: String +) -> Result<(), Error> +{ + send_msg(ctx, msg.replace("\\n", "\n"), false, false).await; + send_msg(ctx, lang!("dc_msg_mandatory_response"), true, true).await; + return Ok(()); +} \ No newline at end of file diff --git a/src/cmds/stop.rs b/src/cmds/stop.rs new file mode 100644 index 0000000..ad38001 --- /dev/null +++ b/src/cmds/stop.rs @@ -0,0 +1,42 @@ +use std::process; + +use poise::serenity_prelude::OnlineStatus; + +use crate::{data, lang, messages::{edit_reply, send_msg}, websocket::send_cmd_json, Context, Error}; + + +#[poise::command( + slash_command, + prefix_command, + rename = "stop", + category = "owner", + owners_only, + required_bot_permissions = "SEND_MESSAGES | VIEW_CHANNEL" +)] +/// I have security measures, even in developer mode. You wont access this without being a bot "owner". +pub async fn cmd( + ctx: Context<'_>, + #[description = "Type \"i want to stop the bot now\" to confirm."] confirmation: Option, +) -> Result<(), Error> +{ + let should_stop = ctx.data().args.dev + || confirmation.unwrap_or_default().to_lowercase() == "i want to stop the bot now"; + + if should_stop { + let msg = send_msg(ctx, lang!("dc_msg_owner_data_save"), true, true).await.unwrap(); + data::write_dc_data(ctx.data()).await; + data::write_re_data().await; + send_cmd_json("stop_praw", None).await; + + edit_reply(ctx, msg, lang!("dc_msg_owner_data_save_complete")).await; + ctx.serenity_context().set_presence(None, OnlineStatus::Invisible); + ctx.framework().shard_manager.shutdown_all().await; + + process::exit(0); + } + else { + send_msg(ctx, lang!("dc_msg_owner_shutdown_failed_confirmation"), true, true).await; + } + + return Ok(()); +} \ No newline at end of file diff --git a/src/gen.rs b/src/gen.rs index c93b32c..01e32e4 100644 --- a/src/gen.rs +++ b/src/gen.rs @@ -57,14 +57,13 @@ pub async fn gen_bot(data: Data, args: Args) -> Client { .options(poise::FrameworkOptions { owners: own, commands: vec![ - cmds::help(), - cmds::ping(), - cmds::embed(), - cmds::send(), - cmds::stop(), - cmds::eight_ball(), - cmds::re_shorturl(), - cmds::add_server(), + cmds::help::cmd(), + cmds::ping::cmd(), + cmds::embed::cmd(), + cmds::send::cmd(), + cmds::stop::cmd(), + cmds::eight_ball::cmd(), + cmds::add_server::cmd(), // reddit re_cmds::add::cmd(), re_cmds::approve::cmd(), @@ -73,10 +72,11 @@ pub async fn gen_bot(data: Data, args: Args) -> Client { re_cmds::top::cmd(), re_cmds::update::cmd(), re_cmds::vote::cmd(), + re_cmds::shorturl::cmd(), // reddit admin re_cmds::admin_bind::cmd(), // cfg - cmds::reload_cfg() + cmds::reload_cfg::cmd() ], event_handler: events::event_handler, ..Default::default() diff --git a/src/main.rs b/src/main.rs index 09ba2d3..1bf13b7 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,7 +1,16 @@ #![warn(unused_extern_crates)] #![allow(clippy::needless_return)] -mod cmds; +mod cmds { + pub mod add_server; + pub mod eight_ball; + pub mod embed; + pub mod help; + pub mod ping; + pub mod reload_cfg; + pub mod send; + pub mod stop; +} mod re_cmds { pub mod add; pub mod admin_bind; @@ -9,6 +18,7 @@ mod re_cmds { pub mod generic_fns; pub mod get; pub mod remove; + pub mod shorturl; pub mod top; pub mod update; pub mod vote; diff --git a/src/re_cmds/shorturl.rs b/src/re_cmds/shorturl.rs new file mode 100644 index 0000000..f22b6b1 --- /dev/null +++ b/src/re_cmds/shorturl.rs @@ -0,0 +1,26 @@ +use crate::{lang, messages::send_msg, re_cmds::generic_fns::to_shorturl, Context, Error}; + + +#[poise::command( + slash_command, + prefix_command, + rename = "re_shorturl", + required_bot_permissions = "SEND_MESSAGES | VIEW_CHANNEL" +)] +/// Convert a long reddit URL to a short one. The bot ONLY uses shortURLs when asking for one. +pub async fn cmd( + ctx: Context<'_>, + #[description = "A Reddit post URL"] url: String +) -> Result<(), Error> +{ + let shorturl = to_shorturl(&url); + + if shorturl.is_ok() { + send_msg(ctx, lang!("dc_msg_shorturl", shorturl.unwrap()), true, true).await; + } + else { + send_msg(ctx, lang!("dc_msg_failed_shorturl_conversion"), true, true).await; + } + + return Ok(()); +} \ No newline at end of file From 71da587242d39db0b216ef1aefd5868aa87d6581 Mon Sep 17 00:00:00 2001 From: ByteDice Date: Thu, 5 Jun 2025 23:32:33 +0200 Subject: [PATCH 08/39] added new config option, refactored other code --- README.md | 20 ++++++------- TODO.md | 2 -- data/cfg_default.json | 4 ++- src/cmds/help.rs | 9 ++---- src/gen.rs | 65 ++++++++++++++++++++++++++++--------------- src/main.rs | 5 +++- 6 files changed, 61 insertions(+), 44 deletions(-) diff --git a/README.md b/README.md index acb926c..25decfc 100644 --- a/README.md +++ b/README.md @@ -30,16 +30,16 @@ This program uses Rust (v1.82.0) and Python (v3.11.4), you can likely use other You can install Python modules by running `$ pip install {module}` or `$ python -m pip install {module}` in a terminal. ### Environment variables: -| **Name** | **Description** | -| --- | --- | -| `ASSISTANT_TOKEN` | The Discord bot token. (Create a Discord bot [here](https://discord.com/developers/docs/intro)!) | +| **Name** | **Description** | +| ---------------------- | --- | +| `ASSISTANT_TOKEN` | The Discord bot token. (Create a Discord bot [here](https://discord.com/developers/docs/intro)!) | | `ASSISTANT_TOKEN_TEST` | (Optional) A testing Discord bot token. This is only needed when the program is run with `-t` or `--test`. | -| `ASSISTANT_R_ID` | The id for the Reddit bot/account. (Create a Reddit bot [here](https://www.reddit.com/prefs/apps)!) | -| `ASSISTANT_R_TOKEN` | The token for the Reddit bot/account. | -| `ASSISTANT_R_NAME` | The username of the Reddit bot/account. | -| `ASSISTANT_R_PASS` | The password for the Reddit bot/account. | -| `ASSISTANT_OWNERS` | (OPTIONAL) A list of Discord user IDs that "own" the bot. Separate each ID with a single comma and **no** spaces. This will allow the specified user IDs to run root commands such as `/stop`, it will also DM these users when *certain* errors occur. | -| `ASSISTANT_BK_MODS` | (OPTIONAL) Same format as `ASSISTANT_OWNERS` but for people who are allowed to use the `/re_*` commands. | +| `ASSISTANT_R_ID` | The id for the Reddit bot/account. (Create a Reddit bot [here](https://www.reddit.com/prefs/apps)!) | +| `ASSISTANT_R_TOKEN` | The token for the Reddit bot/account. | +| `ASSISTANT_R_NAME` | The username of the Reddit bot/account. | +| `ASSISTANT_R_PASS` | The password for the Reddit bot/account. | +| `ASSISTANT_OWNERS` | (OPTIONAL) A list of Discord user IDs that "own" the bot. Separate each ID with a single comma and **no** spaces. This will allow the specified user IDs to run root commands such as `/stop`, it will also DM these users when *certain* errors occur. | +| `ASSISTANT_BK_MODS` | (OPTIONAL) Same format as `ASSISTANT_OWNERS` but for people who are allowed to use the `/re_*` commands. | ### Required permissions: **These are automatically set if you use the [official invite link](https://discord.com/oauth2/authorize?client_id=1212127255795335208&permissions=84992&integration_type=0&scope=bot) or an invite link with the permissions integer set to `84992`.** (The permission integer is this part of the URL `&permissions=84992`) @@ -59,7 +59,7 @@ You can install Python modules by running `$ pip install {module}` or `$ python * For help, run `cargo run -- -h` or `cargo run -- --help`. * To only run the Python part, use `cargo run -- --py`, or for a better error output, `python ./src/python/main.py` -### Long answer for absolute beginners: +### Long answer for beginners: * Download the code (and extract it if needed). * Open a terminal. * Set the environment variables (listed above). diff --git a/TODO.md b/TODO.md index e242ade..03c100a 100644 --- a/TODO.md +++ b/TODO.md @@ -1,7 +1,5 @@ ### High priority: - [ ] Reddit bot that scrapes images with tag "Original Art" and posts them in Discord server - - [ ] Rename commands from "bk_week_{name}" to "re_{name}" - - [ ] Allow disabling bk_week - [ ] Allow updating the data autonomously and via manual commands. - [ ] Automatically approve posts that don't get caught by reverse image search (ris) diff --git a/data/cfg_default.json b/data/cfg_default.json index 96f17ae..25ddd6e 100644 --- a/data/cfg_default.json +++ b/data/cfg_default.json @@ -1,6 +1,8 @@ { + "comment": "IMPORTANT!!! Anything labeled with 'RESTART_{cfg-name-here}' requires a restart to apply", "reddit": { + "RESTART_enabled": true, "subreddits": "bytedicetesting", - "fetch_limit": 100 + "fetch_limit": 100 } } \ No newline at end of file diff --git a/src/cmds/help.rs b/src/cmds/help.rs index d197600..2f93a57 100644 --- a/src/cmds/help.rs +++ b/src/cmds/help.rs @@ -1,8 +1,7 @@ -use poise::Command; -use std::{collections::HashMap, error::Error as StdErr}; +use std::collections::HashMap; use tokio::fs; -use crate::{lang, messages::send_msg, Context, Data, Error}; +use crate::{lang, messages::send_msg, Context, Cmd, Error}; #[derive(poise::ChoiceParameter, PartialEq)] @@ -15,10 +14,6 @@ enum HelpOptions { } -type Cmd = Command>; - - - #[poise::command( slash_command, prefix_command, diff --git a/src/gen.rs b/src/gen.rs index 01e32e4..9e59d83 100644 --- a/src/gen.rs +++ b/src/gen.rs @@ -4,7 +4,7 @@ use poise::serenity_prelude::UserId; use poise::serenity_prelude as serenity; use poise::serenity_prelude::Client; -use crate::{cmds, data, events, re_cmds, rs_println, Args, Data}; +use crate::{cmds, data::{self, get_mutex_data}, events, re_cmds, rs_println, Args, Cmd, Data}; pub async fn gen_data(args: Args, owners: Vec) -> Data { @@ -56,28 +56,7 @@ pub async fn gen_bot(data: Data, args: Args) -> Client { let framework = poise::Framework::builder() .options(poise::FrameworkOptions { owners: own, - commands: vec![ - cmds::help::cmd(), - cmds::ping::cmd(), - cmds::embed::cmd(), - cmds::send::cmd(), - cmds::stop::cmd(), - cmds::eight_ball::cmd(), - cmds::add_server::cmd(), - // reddit - re_cmds::add::cmd(), - re_cmds::approve::cmd(), - re_cmds::get::cmd(), - re_cmds::remove::cmd(), - re_cmds::top::cmd(), - re_cmds::update::cmd(), - re_cmds::vote::cmd(), - re_cmds::shorturl::cmd(), - // reddit admin - re_cmds::admin_bind::cmd(), - // cfg - cmds::reload_cfg::cmd() - ], + commands: make_cmd_vec(&data).await, event_handler: events::event_handler, ..Default::default() }) @@ -93,4 +72,44 @@ pub async fn gen_bot(data: Data, args: Args) -> Client { .framework(framework) .await .unwrap(); +} + + +async fn make_cmd_vec(data: &Data) -> Vec { + let mut cmds = vec![]; + let cfg = get_mutex_data(&data.cfg).await.unwrap(); + + // GENERIC + cmds.extend([ + cmds::help::cmd(), + cmds::ping::cmd(), + cmds::embed::cmd(), + cmds::send::cmd(), + cmds::stop::cmd(), + cmds::eight_ball::cmd(), + cmds::add_server::cmd() + ]); + + // REDDIT + if cfg["RESTART_enabled"].as_bool().unwrap() { + cmds.extend([ + re_cmds::add::cmd(), + re_cmds::approve::cmd(), + re_cmds::get::cmd(), + re_cmds::remove::cmd(), + re_cmds::top::cmd(), + re_cmds::update::cmd(), + re_cmds::vote::cmd(), + re_cmds::shorturl::cmd() + ]); + } + + cmds.extend([ + // reddit admin + re_cmds::admin_bind::cmd(), + // cfg + cmds::reload_cfg::cmd() + ]); + + return cmds } \ No newline at end of file diff --git a/src/main.rs b/src/main.rs index 1bf13b7..9f76be2 100644 --- a/src/main.rs +++ b/src/main.rs @@ -37,10 +37,12 @@ use std::process; use std::thread; use std::time::Duration; use std::vec; +use std::error::Error as StdErr; use clap::Parser; use r#gen::gen_bot; use r#gen::gen_data; +use poise::Command; use schedule::run_schedules; use serde::Serialize; use serde_json::Value; @@ -74,8 +76,9 @@ struct Args { } -type Error = Box; +type Error = Box; type Context<'a> = poise::Context<'a, Data, Error>; +type Cmd = Command>; struct Data { From 53a1fdee199737edf8027a067d2c396e18e29fd2 Mon Sep 17 00:00:00 2001 From: ByteDice Date: Fri, 6 Jun 2025 00:30:59 +0200 Subject: [PATCH 09/39] figured out how to add buttons --- TODO.md | 2 ++ src/cmds/add_server.rs | 2 +- src/cmds/embed.rs | 3 ++- src/gen.rs | 2 +- src/main.rs | 2 +- src/messages.rs | 31 ++++++++++++++++++++++++++----- src/re_cmds/admin_bind.rs | 2 +- src/re_cmds/generic_fns.rs | 2 +- src/re_cmds/update.rs | 2 +- 9 files changed, 36 insertions(+), 12 deletions(-) diff --git a/TODO.md b/TODO.md index 03c100a..bc19d03 100644 --- a/TODO.md +++ b/TODO.md @@ -2,6 +2,8 @@ - [ ] Reddit bot that scrapes images with tag "Original Art" and posts them in Discord server - [ ] Allow updating the data autonomously and via manual commands. - [ ] Automatically approve posts that don't get caught by reverse image search (ris) + - [ ] Make buttons do stuff + - [ ] Add buttons to removed post embed ### Medium priority: diff --git a/src/cmds/add_server.rs b/src/cmds/add_server.rs index c09f3a4..6017ec3 100644 --- a/src/cmds/add_server.rs +++ b/src/cmds/add_server.rs @@ -18,7 +18,7 @@ pub async fn cmd( let r = dc_add_server(ctx.data(), ctx.guild_id().unwrap().into()).await; if r.is_ok() { - send_msg(ctx, lang!("dc_msg_added_to_data"), true, true).await; + send_msg(ctx, lang!("dc_msg_add_to_data"), true, true).await; } else { send_msg(ctx, lang!("dc_msg_corrupted_data"), true, true).await; diff --git a/src/cmds/embed.rs b/src/cmds/embed.rs index ebae7ae..11fefff 100644 --- a/src/cmds/embed.rs +++ b/src/cmds/embed.rs @@ -39,7 +39,8 @@ pub async fn cmd( ephemeral: ephemeral.unwrap_or(false), message, thumbnail, - author: if author.unwrap_or(false) { Some(Author { name: ctx.author().name.clone(), url: "".to_string(), icon_url: ctx.author().avatar_url().unwrap() }) } else { None } + author: if author.unwrap_or(false) { Some(Author { name: ctx.author().name.clone(), url: "".to_string(), icon_url: ctx.author().avatar_url().unwrap() }) } else { None }, + ..Default::default() }, reply_unwrap ).await; diff --git a/src/gen.rs b/src/gen.rs index 9e59d83..07c6c76 100644 --- a/src/gen.rs +++ b/src/gen.rs @@ -91,7 +91,7 @@ async fn make_cmd_vec(data: &Data) -> Vec { ]); // REDDIT - if cfg["RESTART_enabled"].as_bool().unwrap() { + if cfg["reddit"]["RESTART_enabled"].as_bool().unwrap() { cmds.extend([ re_cmds::add::cmd(), re_cmds::approve::cmd(), diff --git a/src/main.rs b/src/main.rs index 9f76be2..30b6bcb 100644 --- a/src/main.rs +++ b/src/main.rs @@ -104,7 +104,7 @@ async fn main() { rs_println!("Fetching language file..."); data::load_lang_data(args.clone().lang); - rs_println!("[IMPORTANT] The below message is a test message, it should be written in the language you've selected\n{}", lang!("log_lang_load_success")); + rs_println!("[IMPORTANT] The below message is a test message, it should be written in the language you've selected\nTest message: {}", lang!("log_lang_load_success")); let own_env = std::env::var("ASSISTANT_OWNERS").unwrap_or("0".to_string()); let own_vec_str: Vec = own_env.split(",").map(String::from).collect(); diff --git a/src/messages.rs b/src/messages.rs index 3c5191e..278cc86 100644 --- a/src/messages.rs +++ b/src/messages.rs @@ -4,7 +4,7 @@ use crate::{lang, Args, Context}; use poise::serenity_prelude::json::Value; use poise::{serenity_prelude::CreateMessage, CreateReply, ReplyHandle}; -use poise::serenity_prelude::{ChannelId, Color, CreateEmbed, CreateEmbedAuthor, EditMessage, Http, Message, Timestamp, UserId}; +use poise::serenity_prelude::{ChannelId, Color, CreateActionRow, CreateButton, CreateEmbed, CreateEmbedAuthor, EditMessage, Http, Message, ReactionType, Timestamp, UserId}; use serde_json::json; @@ -24,7 +24,8 @@ pub struct EmbedOptions { pub ephemeral: bool, pub message: Option, pub author: Option, - pub thumbnail: Option + pub thumbnail: Option, + pub actionrows: Option> } impl Default for EmbedOptions { fn default() -> Self { @@ -37,7 +38,8 @@ impl Default for EmbedOptions { ephemeral: false, message: None, author: None, - thumbnail: None + thumbnail: None, + actionrows: None }; } } @@ -104,6 +106,7 @@ pub async fn send_embed( embeds: vec![embed], content: options.message, ephemeral: Some(options.ephemeral), + components: options.actionrows, ..Default::default() }; @@ -111,7 +114,12 @@ pub async fn send_embed( return Some(msg.unwrap()); } else { - let r = CreateMessage::new().embeds(vec![embed]); + let mut r = CreateMessage::new().embeds(vec![embed]); + + if let Some(actionrows) = options.actionrows { + r = r.components(actionrows); + } + let _ = ctx.channel_id().send_message(ctx.http(), r).await; return None; } @@ -126,7 +134,11 @@ pub async fn http_send_embed( { let embed = embed_from_options(options.clone()); - let r = CreateMessage::new().embeds(vec![embed]); + let mut r = CreateMessage::new().embeds(vec![embed]); + + if let Some(actionrows) = options.actionrows { + r = r.components(actionrows); + } let msg = c_id.send_message(http, r).await; return msg.ok(); @@ -222,6 +234,14 @@ pub fn make_post_embed(post_data: &Value, url: &str, ephemeral: bool) -> EmbedOp ); let media_urls = post_data["post_data"]["media_urls"].as_array().unwrap(); + let action_row = CreateActionRow::Buttons(vec![ + CreateButton::new("upvote_btn") .label("Upvote") .emoji(ReactionType::Unicode("⬆️".to_string())), + CreateButton::new("unupvote_btn") .label("Un-upvote"), + CreateButton::new("approve_btn") .label("Approve") .emoji(ReactionType::Unicode("✅".to_string())), + CreateButton::new("unapprove_btn") .label("Disapprove") .emoji(ReactionType::Unicode("❌".to_string())), + CreateButton::new("remove_btn") .label("Remove") .emoji(ReactionType::Unicode("🗑️".to_string())) + ]); + return EmbedOptions { title: Some(post_data["post_data"]["title"].as_str().unwrap().to_string()), desc: format!("{}\n\nJSON: ||`{}`||", trimmed, serde_json::to_string(&json_min).unwrap()), @@ -232,6 +252,7 @@ pub fn make_post_embed(post_data: &Value, url: &str, ephemeral: bool) -> EmbedOp thumbnail: media_urls.first() .and_then(|url| url.as_str().map(|s| s.to_string())) .or(None), + actionrows: Some(vec![action_row]), ..Default::default() }; } diff --git a/src/re_cmds/admin_bind.rs b/src/re_cmds/admin_bind.rs index 675ea39..49cf188 100644 --- a/src/re_cmds/admin_bind.rs +++ b/src/re_cmds/admin_bind.rs @@ -21,7 +21,7 @@ pub async fn cmd( send_msg(ctx, lang!("dc_msg_bound_channel", c_id), true, true).await; } else { - send_msg(ctx, lang!("dc_msg_server_data_404"), false, false).await; + send_msg(ctx, lang!("dc_msg_data_server_404"), true, true).await; } return Ok(()); diff --git a/src/re_cmds/generic_fns.rs b/src/re_cmds/generic_fns.rs index 2f104d1..4c3cbab 100644 --- a/src/re_cmds/generic_fns.rs +++ b/src/re_cmds/generic_fns.rs @@ -22,7 +22,7 @@ pub fn to_shorturl(url: &str) -> Result { pub async fn send_embed_for_post(ctx: Context<'_>, post: Value, url: &str) -> Result<(), Error> { send_embed(ctx, make_post_embed(&post, url, true), true).await; - Ok(()) + return Ok(()); } pub async fn send_embed_for_removed(ctx: Context<'_>, url: &str, post: &Value) { diff --git a/src/re_cmds/update.rs b/src/re_cmds/update.rs index d522e76..1db5a61 100644 --- a/src/re_cmds/update.rs +++ b/src/re_cmds/update.rs @@ -105,7 +105,7 @@ async fn update_progress(ctx: Context<'_>, p: ReplyHandle<'_>, t: String, added_ async fn get_c_id(ctx: Context<'_>) -> Option { if !data::dc_contains_server(ctx.data(), ctx.guild_id().unwrap().into()).await { - send_msg(ctx, lang!("dc_msg_server_data_404"), false, false).await; + send_msg(ctx, lang!("dc_msg_data_server_404"), true, true).await; return None; } From c0793562e3c6dc6b65620170df520d07950164fe Mon Sep 17 00:00:00 2001 From: ByteDice Date: Fri, 6 Jun 2025 15:37:06 +0200 Subject: [PATCH 10/39] added rest of the buttons (still needs listeners) & reformatted/improved other code --- TODO.md | 2 ++ data/lang/en.json | 6 ++-- data/re_data_preset.json | 12 ++++---- src/cmds/reload_cfg.rs | 2 +- src/cmds/stop.rs | 2 +- src/data.rs | 6 ++-- src/gen.rs | 6 ++-- src/main.rs | 12 ++++++-- src/messages.rs | 46 ++++++++++++++-------------- src/python/data.py | 61 +++++++++++++++++++++----------------- src/python/py_websocket.py | 12 ++++---- src/re_cmds/add.rs | 28 ++++++++--------- src/re_cmds/approve.rs | 2 +- src/re_cmds/generic_fns.rs | 2 +- src/re_cmds/get.rs | 4 +-- src/re_cmds/remove.rs | 15 ++++++++-- src/re_cmds/update.rs | 8 ++--- src/re_cmds/vote.rs | 2 +- src/websocket.rs | 9 ++++-- 19 files changed, 132 insertions(+), 105 deletions(-) diff --git a/TODO.md b/TODO.md index bc19d03..eda773d 100644 --- a/TODO.md +++ b/TODO.md @@ -1,5 +1,7 @@ ### High priority: - [ ] Reddit bot that scrapes images with tag "Original Art" and posts them in Discord server + - [ ] handle dm_on_error cfg + - [ ] Add button event listeners - [ ] Allow updating the data autonomously and via manual commands. - [ ] Automatically approve posts that don't get caught by reverse image search (ris) - [ ] Make buttons do stuff diff --git a/data/lang/en.json b/data/lang/en.json index e62c139..df224ae 100644 --- a/data/lang/en.json +++ b/data/lang/en.json @@ -6,10 +6,10 @@ "dc_msg_corrupted_data": "Oopsies `(。>\\\\<)`. It looks like my data i-is \\**sob*\\*... c-corrupted!\n[From Byte Dice]: I have no idea what I was thinking while writing this at 2am. I'm not removing it.", "dc_msg_data_server_404": "This server is not in the data!\n Hint: Run the command `/add_server` inside of a Discord server (requires administrator permission).", "dc_msg_dm_python_err_socket": "Unknown internal Python error occurred: Websocket response error", - "dc_msg_dm_python_err": "Unknown internal Python Error: {0}", + "dc_msg_dm_python_err": "Unknown internal Python Error: `{0}`", "dc_msg_embed_default_embed_desc": "Default english embed description.", "dc_msg_embed_re_post": "Spoilers and vote length anonymizer for fair review!\n## Post Data:\n**Post upvotes:** ||`{0:>6}`||\n**Moderator votes:** ||`{1:>6}`||\n**Media type:** `{2}`\n**URL:** ||<{3}>||\n\n## Listing Data:\n**Added by:** `{{ human: {4}, bot: {5} }}`\n**Approved by:** `{{ human: {6}, bot: [not implemented] }}`", - "dc_msg_embed_re_removed": "## Removed by `{0}`\n**Reason:** {1}\nURL: ||<{2}>||\n\nJSON: ||`{3}`||", + "dc_msg_embed_re_removed": "## Removed by `{0}`\n**Reason:** {1}\n**URL**: ||<{2}>||", "dc_msg_err_trace": "Unknown error!\nError trace: {0}", "dc_msg_failed_shorturl_conversion": "Couldn't convert to shortURL: Invalid Reddit URL format.", "dc_msg_mandatory_response": "Mandatory response message, please ignore.", @@ -25,7 +25,7 @@ "dc_msg_re_post_add_success": "Added post with URL \"<{0}>\"!", "dc_msg_re_post_approve_success": "Successfully approved the post!", "dc_msg_re_post_disapprove_success": "Successfully disapproved the post!", - "dc_msg_re_post_remove_success": "Successfully removed post!", + "dc_msg_re_post_remove_success": "Successfully removed post with URL \"<{0}>\"!", "dc_msg_re_post_unremove_success": "Successfully un-removed post with URL \"<{0}>\"!", "dc_msg_re_post_update_success": "Updated post with URL \"<{0}>\"!", "dc_msg_re_posts_channel_404": "Could not find `re_posts_channel` in data!\nHint: Run `/admin_re_bindchannel` in a (preferably read-only) channel (requires administrator permission).", diff --git a/data/re_data_preset.json b/data/re_data_preset.json index f5ec648..100d4b4 100644 --- a/data/re_data_preset.json +++ b/data/re_data_preset.json @@ -1,6 +1,11 @@ { "posts": { - "EXAMPLE VALUE": { + "EXAMPLE URL": { + "removed": { + "removed": false, + "by": null, + "reason": null + }, "post_data": { "title": "I JUST BOUGHT THE CONTINENT OF NORTH AMERICA FOR A DOLLAR!", "upvotes": 69420, @@ -21,11 +26,6 @@ "by_human": true, "by_ris": true } - }, - "EXAMPLE VALUE DELETED": { - "removed": true, - "removed_by": "ME!!!!", - "remove_reason": "i HATED that post >:(" } } } \ No newline at end of file diff --git a/src/cmds/reload_cfg.rs b/src/cmds/reload_cfg.rs index 737fbb1..e4e5fa3 100644 --- a/src/cmds/reload_cfg.rs +++ b/src/cmds/reload_cfg.rs @@ -19,7 +19,7 @@ pub async fn cmd( read_cfg_data(&ctx.data(), false).await; let d = get_mutex_data(&ctx.data().cfg).await?; let d_str = serde_json::to_string(&d)?; - let r = send_cmd_json("update_cfg", Some(json!([d_str]))).await; + let r = send_cmd_json("update_cfg", Some(json!([d_str])), true).await; if r.is_some() && r.unwrap()["value"].as_bool().unwrap() { send_msg( diff --git a/src/cmds/stop.rs b/src/cmds/stop.rs index ad38001..3622602 100644 --- a/src/cmds/stop.rs +++ b/src/cmds/stop.rs @@ -26,7 +26,7 @@ pub async fn cmd( let msg = send_msg(ctx, lang!("dc_msg_owner_data_save"), true, true).await.unwrap(); data::write_dc_data(ctx.data()).await; data::write_re_data().await; - send_cmd_json("stop_praw", None).await; + send_cmd_json("stop_praw", None, true).await; edit_reply(ctx, msg, lang!("dc_msg_owner_data_save_complete")).await; ctx.serenity_context().set_presence(None, OnlineStatus::Invisible); diff --git a/src/data.rs b/src/data.rs index 133243e..c23f19a 100644 --- a/src/data.rs +++ b/src/data.rs @@ -103,13 +103,13 @@ fn generate_re_data() { pub async fn update_re_data(data: &Data) { - send_cmd_json("update_data_file", None).await; + send_cmd_json("update_data_file", None, true).await; read_re_data(data, false).await; } pub async fn write_re_data() { - send_cmd_json("update_data_file", None).await; + send_cmd_json("update_data_file", None, true).await; } @@ -127,7 +127,7 @@ pub async fn read_cfg_data(data: &Data, wipe: bool) { let mut cfg_data = data.cfg.lock().await; *cfg_data = json_data; - send_cmd_json("update_cfg", Some(json!([str_data]))).await; + send_cmd_json("update_cfg", Some(json!([str_data])), true).await; } diff --git a/src/gen.rs b/src/gen.rs index 07c6c76..df85ea8 100644 --- a/src/gen.rs +++ b/src/gen.rs @@ -100,14 +100,12 @@ async fn make_cmd_vec(data: &Data) -> Vec { re_cmds::top::cmd(), re_cmds::update::cmd(), re_cmds::vote::cmd(), - re_cmds::shorturl::cmd() + re_cmds::shorturl::cmd(), + re_cmds::admin_bind::cmd(), ]); } cmds.extend([ - // reddit admin - re_cmds::admin_bind::cmd(), - // cfg cmds::reload_cfg::cmd() ]); diff --git a/src/main.rs b/src/main.rs index 30b6bcb..a95d3b5 100644 --- a/src/main.rs +++ b/src/main.rs @@ -94,13 +94,15 @@ struct Data { static CFG_DATA_RE: &str = "posts"; -pub static mut LANG: Option = None; +pub static mut LANG: Option = None; +pub static mut NOPING: bool = false; #[tokio::main] async fn main() { let args = ::parse(); let args_str = serde_json::to_string(&args).expect("Error serializing args to JSON"); + unsafe { NOPING = args.noping; } rs_println!("Fetching language file..."); data::load_lang_data(args.clone().lang); @@ -118,6 +120,8 @@ async fn main() { if args.dev && args.wipe { println!("----- \"DON'T WORRY ABOUT IT\" MODE ENABLED -----"); } if args.nosched { println!("----- NO SCHEDULES -----"); } + // TODO: handle if config for reddit is disabled to not start python + if args.py && !args.rs { println!("----- PYTHON ONLY MODE -----"); rs_println!("ARGS: {}", args_str); @@ -174,6 +178,8 @@ async fn start(args: Args, owners: Vec) { async fn read_reddit_inbox() { - unsafe { if !websocket::HAS_CONNECTED { return; } } - send_cmd_json("respond_mentions", None).await; + unsafe { + if !websocket::HAS_CONNECTED { return; } + send_cmd_json("respond_mentions", None, !NOPING).await; + } } \ No newline at end of file diff --git a/src/messages.rs b/src/messages.rs index 278cc86..d8b3318 100644 --- a/src/messages.rs +++ b/src/messages.rs @@ -5,7 +5,6 @@ use crate::{lang, Args, Context}; use poise::serenity_prelude::json::Value; use poise::{serenity_prelude::CreateMessage, CreateReply, ReplyHandle}; use poise::serenity_prelude::{ChannelId, Color, CreateActionRow, CreateButton, CreateEmbed, CreateEmbedAuthor, EditMessage, Http, Message, ReactionType, Timestamp, UserId}; -use serde_json::json; #[derive(Clone)] @@ -48,6 +47,9 @@ impl Default for EmbedOptions { static DEFAULT_DC_COL: u32 = 5793266; static REMOVED_DC_COL: u32 = 16716032; +pub static JSON_TEXT_START: &str = "-# JSON: ||`"; +pub static JSON_TEXT_END: &str = "`||"; + fn none_to_empty(string: Option) -> String { return string.unwrap_or_default(); @@ -226,25 +228,19 @@ pub fn make_post_embed(post_data: &Value, url: &str, ephemeral: bool) -> EmbedOp .collect::>() .join("\n"); - let json_min = json!( - {"post_data": json!({ "upvotes": post_data["post_data"]["upvotes"] }), - "added": post_data["added"], - "approved": post_data["approved"], - "votes": json!({"mod_voters": post_data["votes"]["mod_voters"]})} - ); let media_urls = post_data["post_data"]["media_urls"].as_array().unwrap(); let action_row = CreateActionRow::Buttons(vec![ - CreateButton::new("upvote_btn") .label("Upvote") .emoji(ReactionType::Unicode("⬆️".to_string())), - CreateButton::new("unupvote_btn") .label("Un-upvote"), - CreateButton::new("approve_btn") .label("Approve") .emoji(ReactionType::Unicode("✅".to_string())), - CreateButton::new("unapprove_btn") .label("Disapprove") .emoji(ReactionType::Unicode("❌".to_string())), - CreateButton::new("remove_btn") .label("Remove") .emoji(ReactionType::Unicode("🗑️".to_string())) + CreateButton::new("vote_btn") .label("Vote") .emoji(ReactionType::Unicode("⬆️".to_string())), + CreateButton::new("unvote_btn") .label("Un-vote"), + CreateButton::new("approve_btn") .label("Approve") .emoji(ReactionType::Unicode("✅".to_string())), + CreateButton::new("unapprove_btn").label("Disapprove") .emoji(ReactionType::Unicode("❌".to_string())), + CreateButton::new("remove_btn") .label("Remove") .emoji(ReactionType::Unicode("🗑️".to_string())) ]); return EmbedOptions { title: Some(post_data["post_data"]["title"].as_str().unwrap().to_string()), - desc: format!("{}\n\nJSON: ||`{}`||", trimmed, serde_json::to_string(&json_min).unwrap()), + desc: format!("{}\n\n{}{}{}", trimmed, JSON_TEXT_START, serde_json::to_string(&post_data).unwrap(), JSON_TEXT_END), col: Some(DEFAULT_DC_COL), url: Some(url.to_string()), ts: Some(Timestamp::from_unix_timestamp(post_data["post_data"]["date_unix"].as_i64().unwrap()).unwrap()), @@ -259,20 +255,26 @@ pub fn make_post_embed(post_data: &Value, url: &str, ephemeral: bool) -> EmbedOp pub fn make_removed_embed(post_data: &Value, url: &str, ephemeral: bool) -> EmbedOptions { + let action_row = CreateActionRow::Buttons(vec![ + CreateButton::new("unremove_btn").label("Un-remove").emoji(ReactionType::Unicode("↩️".to_string())) + ]); + + let desc = lang!( + "dc_msg_embed_re_removed", + post_data["removed"]["by"].as_str().unwrap(), + if !post_data["removed"]["reason"].is_null() { post_data["removed"]["reason"].as_str().unwrap() } + else { "None" }, + url + ); + return EmbedOptions { - title: Some("REMOVED!".to_string()), - desc: lang!( - "dc_msg_embed_re_removed", - post_data["removed_by"].as_str().unwrap(), - if !post_data["remove_reason"].is_null() { post_data["remove_reason"].as_str().unwrap() } - else { "None" }, - url, - serde_json::to_string(&post_data).unwrap() - ), + title: Some(format!("[REMOVED] {}", post_data["post_data"]["title"])), + desc: format!("{}\n\n{}{}{}", desc, JSON_TEXT_START, serde_json::to_string(&post_data).unwrap(), JSON_TEXT_END), col: Some(REMOVED_DC_COL), url: Some(url.to_string()), ts: Some(Timestamp::from_unix_timestamp(post_data["post_data"]["date_unix"].as_i64().unwrap()).unwrap()), ephemeral, + actionrows: Some(vec![action_row]), ..Default::default() }; } \ No newline at end of file diff --git a/src/python/data.py b/src/python/data.py index 794b3c3..5d4a4fd 100644 --- a/src/python/data.py +++ b/src/python/data.py @@ -18,14 +18,20 @@ class PostData: date_unix: int, media_type: str, media_urls: list[str], - voters_re: list[str] = [], - voters_dc: list[int] = [], - mod_voters: list[int] = [], - added_by_human: bool = False, - added_by_bot: bool = False, - approved_by_human: bool = False, - approved_by_ris: bool = False + removed: bool = False, + removed_by: str | None = None, + removed_reason: str | None = None, + voters_re: list[str] = [], + voters_dc: list[int] = [], + mod_voters: list[int] = [], + added_by_human: bool = False, + added_by_bot: bool = False, + approved_by_human: bool = False, + approved_by_ris: bool = False ): + self.removed = removed + self.removed_by = removed_by + self.removed_reason = removed_reason self.url = url self.title = title self.upvotes = upvotes @@ -42,6 +48,11 @@ class PostData: def to_json(self): return { + "removed": { + "removed": self.removed, + "by": self.removed_reason, + "reason": self.removed_reason + }, "post_data": { "title": self.title, "upvotes": self.upvotes, @@ -125,31 +136,26 @@ async def read_cfg(bot: botPy.Bot) -> bool: def add_post_to_data(bot: botPy.Bot, new_data: PostData, bypass_conditions: bool = False) -> bool: + if new_data.removed: + new_data.removed = False + new_data.removed_by = None + new_data.removed_reason = None + if bypass_conditions: bot.data[botPy.RE_DATA_POSTS][new_data.url] = new_data.to_json() - if bot.args["dev"]: - py_print(f"Added post \"{new_data.url}\" (Conditions bypassed)") + if bot.args["dev"]: py_print(f"Added post \"{new_data.url}\" (Conditions bypassed)") return True - # not sure what this is for - updated = False - - if new_data.url not in bot.data[botPy.RE_DATA_POSTS] or updated: + if new_data.url not in bot.data[botPy.RE_DATA_POSTS]: bot.data[botPy.RE_DATA_POSTS][new_data.url] = new_data.to_json() - if bot.args["dev"]: - py_print(f"Added post \"{new_data.url}\"") + if bot.args["dev"]: py_print(f"Added post \"{new_data.url}\"") return True - - if "removed" not in bot.data[botPy.RE_DATA_POSTS][new_data.url]: - updated = new_data.upvotes != bot.data[botPy.RE_DATA_POSTS][new_data.url]["post_data"] - else: - py_print(f"Failed to add post \"{new_data.url}\": Removed flag is True.") - return False + return False def set_approve_post(bot: botPy.Bot, approved: bool, url: str) -> bool: - if not hasattr(bot.data[botPy.RE_DATA_POSTS][url], "removed"): + if not bot.data[botPy.RE_DATA_POSTS][url]["removed"]["removed"]: bot.data[botPy.RE_DATA_POSTS][url]["approved"]["by_human"] = approved return True @@ -160,12 +166,11 @@ def remove_post(bot: botPy.Bot, url: str, removed_by: str = "UNKNOWN", reason: s weekly = bot.data[botPy.RE_DATA_POSTS] if url in weekly: - weekly[url] = { - "removed": True, - "removed_by": removed_by, - "remove_reason": reason, - "post_data": { "date_unix": weekly[url]["post_data"]["date_unix"] } - } + rm = weekly[url]["removed"] + rm["removed"] = True + rm["by"] = removed_by + rm["reason"] = reason + weekly[url]["removed"] = rm return True else: return False diff --git a/src/python/py_websocket.py b/src/python/py_websocket.py index 2111ccf..08279dd 100644 --- a/src/python/py_websocket.py +++ b/src/python/py_websocket.py @@ -46,7 +46,7 @@ async def parse_json(response: str, bot: botPy.Bot): try: json_response = json.loads(json_str) if json_response["value"] not in ["respond_mentions"] or bot.args["dev"]: - py_print(f"Received from Rust: {response}") + if json_response["print"]: py_print(f"Received from Rust: {response}") result = await json_to_func(json_response, bot) await ws_global.ping() @@ -86,13 +86,15 @@ async def json_to_func(v: dict, bot: botPy.Bot) -> dict: case "stop_praw": r = await bot .stop () case _: value_supported = False + print_result = v["print"] + if not value_supported: val = v["value"] py_print(f"Value \"{val}\" is not supported") - return {"type": "result", "value": False} + return result_json(False, print_result) - return result_json(r) + return result_json(r, print_result) -def result_json(bool: bool) -> dict: - return {"type": "result", "value": bool} \ No newline at end of file +def result_json(bool: bool, print_result: bool) -> dict: + return {"type": "result", "value": bool, "print": print_result} \ No newline at end of file diff --git a/src/re_cmds/add.rs b/src/re_cmds/add.rs index 06e8813..570ac95 100644 --- a/src/re_cmds/add.rs +++ b/src/re_cmds/add.rs @@ -2,8 +2,9 @@ use serde_json::json; use crate::data::get_mutex_data; use crate::messages::send_msg; -use crate::{data, websocket, Context, Error, CFG_DATA_RE}; -use crate::re_cmds::generic_fns::{get_readable_subreddits, is_bk_mod, to_shorturl}; +use crate::re_cmds::get::get_post_from_data; +use crate::{data, websocket::send_cmd_json, Context, Error, CFG_DATA_RE}; +use crate::re_cmds::generic_fns::{get_readable_subreddits, is_bk_mod, send_embed_for_post, to_shorturl}; use crate::lang; #[poise::command( @@ -34,7 +35,7 @@ pub async fn cmd( if let Some(bk_week) = reddit_data.get(CFG_DATA_RE) { let a = approve.unwrap_or(false); - let r = websocket::send_cmd_json("add_post_url", Some(json!([&shorturl, a, true]))).await.unwrap(); + let r = send_cmd_json("add_post_url", Some(json!([&shorturl, a, true])), true).await.unwrap(); if !r["value"].as_bool().unwrap() { send_msg( @@ -49,20 +50,17 @@ pub async fn cmd( } if let Some(post) = bk_week.get(shorturl) { - if post.get("removed").is_some() { - send_msg(ctx, lang!("dc_msg_re_post_unremove_success", url), true, true).await; - } - else { - send_msg(ctx, lang!("dc_msg_re_post_update_success", url), true, true).await; - } - } - else { - send_msg(ctx, lang!("dc_msg_re_post_add_success", &shorturl), true, true).await; + if post["removed"]["removed"].as_bool().unwrap() + { send_msg(ctx, lang!("dc_msg_re_post_unremove_success", &shorturl), true, true).await; } + else { send_msg(ctx, lang!("dc_msg_re_post_update_success", &shorturl), true, true).await; } } + else { send_msg(ctx, lang!("dc_msg_re_post_add_success", &shorturl), true, true).await; } - if a { - send_msg(ctx, lang!("dc_msg_re_also_approved"), true, true).await; - } + if a { send_msg(ctx, lang!("dc_msg_re_also_approved"), true, true).await; } + } + + if let Some(post) = get_post_from_data(ctx, &reddit_data, &url).await? { + send_embed_for_post(ctx, post, &url).await?; } return Ok(()); diff --git a/src/re_cmds/approve.rs b/src/re_cmds/approve.rs index 115be82..6901ca0 100644 --- a/src/re_cmds/approve.rs +++ b/src/re_cmds/approve.rs @@ -40,7 +40,7 @@ async fn approve_cmd(ctx: Context<'_>, url: &str, reddit_data: &Value, approve: return; } - let r = websocket::send_cmd_json("set_approve_post", Some(json!([approve, &url]))).await.unwrap(); + let r = websocket::send_cmd_json("set_approve_post", Some(json!([approve, &url])), true).await.unwrap(); if r.get("value").is_some() { if approve { send_msg(ctx, lang!("dc_msg_re_post_approve_success"), true, true).await; diff --git a/src/re_cmds/generic_fns.rs b/src/re_cmds/generic_fns.rs index 4c3cbab..1b0eed0 100644 --- a/src/re_cmds/generic_fns.rs +++ b/src/re_cmds/generic_fns.rs @@ -36,7 +36,7 @@ pub async fn send_embed_for_removed(ctx: Context<'_>, url: &str, post: &Value) { pub async fn get_readable_subreddits(ctx: Context<'_>) -> Result { let d = get_mutex_data(&ctx.data().cfg).await?; - let sr = d["reddit"]["subreddits"].as_str().ok_or("Item of key \"subreddit\" is not a string type.\nTrace: get_readable_subreddits -> let sr = ...")?; + let sr = d["reddit"]["subreddits"].as_str().ok_or("Item of key \"subreddit\" is not a string type.\nTrace: `get_readable_subreddits -> let sr = ...`")?; let split: Vec<&str> = sr.split("+").collect(); let join = split.join(", r/"); diff --git a/src/re_cmds/get.rs b/src/re_cmds/get.rs index 5292461..ad5a869 100644 --- a/src/re_cmds/get.rs +++ b/src/re_cmds/get.rs @@ -29,10 +29,10 @@ pub async fn cmd( } -async fn get_post_from_data(ctx: Context<'_>, reddit_data: &Value, url: &str) -> Result, Error> { +pub async fn get_post_from_data(ctx: Context<'_>, reddit_data: &Value, url: &str) -> Result, Error> { if let Some(bk_week) = reddit_data.get(CFG_DATA_RE) { if let Some(post) = bk_week.get(url) { - if post.get("removed").is_some() { + if post["removed"]["removed"].as_bool().unwrap() { send_embed_for_removed(ctx, url, post).await; return Ok(None); } diff --git a/src/re_cmds/remove.rs b/src/re_cmds/remove.rs index d08a785..0755f8e 100644 --- a/src/re_cmds/remove.rs +++ b/src/re_cmds/remove.rs @@ -1,6 +1,6 @@ use serde_json::json; -use crate::{lang, messages::send_msg, re_cmds::generic_fns::{get_readable_subreddits, is_bk_mod}, websocket::send_cmd_json, Context, Error}; +use crate::{data::{self, get_mutex_data}, lang, messages::send_msg, re_cmds::{generic_fns::{get_readable_subreddits, is_bk_mod, send_embed_for_removed}, get::get_post_from_data}, websocket::send_cmd_json, Context, Error}; #[poise::command( slash_command, @@ -23,12 +23,12 @@ pub async fn cmd( } let auth = &ctx.author().name; - let r = send_cmd_json("remove_post_url", Some(json!([&url, &auth, &reason]))).await.unwrap(); + let r = send_cmd_json("remove_post_url", Some(json!([&url, &auth, &reason])), true).await.unwrap(); if r["value"].as_bool().unwrap() { send_msg( ctx, - lang!("dc_msg_re_post_remove_success"), + lang!("dc_msg_re_post_remove_success", &url), true, true ).await; @@ -37,5 +37,14 @@ pub async fn cmd( send_msg(ctx, lang!("dc_msg_re_post_404"), false, false).await; } + data::update_re_data(ctx.data()).await; + let reddit_data = get_mutex_data(&ctx.data().reddit_data).await?; + + if let Some(post) = get_post_from_data(ctx, &reddit_data, &url).await? { + if post["removed"]["removed"].as_bool().unwrap() { + send_embed_for_removed(ctx, &url, &post).await; + } + } + return Ok(()); } \ No newline at end of file diff --git a/src/re_cmds/update.rs b/src/re_cmds/update.rs index 1db5a61..a2586db 100644 --- a/src/re_cmds/update.rs +++ b/src/re_cmds/update.rs @@ -3,7 +3,7 @@ use std::time::{SystemTime, UNIX_EPOCH}; use poise::{serenity_prelude::{ChannelId, EditMessage, GetMessages, Http, Message, MessageId, UserId}, ReplyHandle}; use serde_json::{json, Map, Value}; -use crate::{data::{self, get_mutex_data, DC_POSTS_CHANNEL_KEY}, lang, messages::{edit_reply, embed_from_options, http_send_embed, make_post_embed, make_removed_embed, send_msg}, websocket::send_cmd_json, Context, Error, CFG_DATA_RE}; +use crate::{data::{self, get_mutex_data, DC_POSTS_CHANNEL_KEY}, lang, messages::{edit_reply, embed_from_options, http_send_embed, make_post_embed, make_removed_embed, send_msg, JSON_TEXT_END, JSON_TEXT_START}, websocket::send_cmd_json, Context, Error, CFG_DATA_RE}; #[poise::command( slash_command, @@ -35,7 +35,7 @@ pub async fn cmd( let max_age_u = max_age.unwrap_or(8); let max_age_secs = max_age_u as u64 * (60 * 60 * 24); - send_cmd_json("add_new_posts", Some(json!([max_age_secs]))).await; + send_cmd_json("add_new_posts", Some(json!([max_age_secs])), true).await; data::update_re_data(ctx.data()).await; let r_data = get_mutex_data(&ctx.data().reddit_data).await?; @@ -80,7 +80,7 @@ pub async fn cmd( if max_age_u > 0 { p_text = update_progress(ctx, progress.clone(), p_text.clone(), format!("✅\nRemoving old posts (threshold: {}d)...", max_age_u)).await; remove_old(http, c_id, &msgs_json).await; - send_cmd_json("remove_old_posts", Some(json!([max_age_secs]))).await; + send_cmd_json("remove_old_posts", Some(json!([max_age_secs])), true).await; } // Removing duplicate posts @@ -181,7 +181,7 @@ async fn msgs_to_json(msgs: Vec, reddit_data: &Value, max_age: u64) -> if msg_last_len < 13 { continue; } - let msg_json_str = &msg_lines.clone().last().unwrap()[9..msg_last_len - 3]; + let msg_json_str = &msg_lines.clone().last().unwrap()[JSON_TEXT_START.len()..msg_last_len - JSON_TEXT_END.len()]; let msg_json = serde_json::from_str(msg_json_str); if msg_json.is_err() { continue; } diff --git a/src/re_cmds/vote.rs b/src/re_cmds/vote.rs index f6549e4..0e2e168 100644 --- a/src/re_cmds/vote.rs +++ b/src/re_cmds/vote.rs @@ -47,7 +47,7 @@ pub async fn cmd( return Ok(()); } - let r = send_cmd_json("set_vote_post", Some(json!([url, uid, is_mod, true, unw_vote]))).await.unwrap(); + let r = send_cmd_json("set_vote_post", Some(json!([url, uid, is_mod, true, unw_vote])), true).await.unwrap(); let unw_r = r["value"].as_bool().unwrap(); if unw_r && !unw_vote && is_mod { diff --git a/src/websocket.rs b/src/websocket.rs index 834acba..764d5c9 100644 --- a/src/websocket.rs +++ b/src/websocket.rs @@ -47,7 +47,7 @@ pub async fn send_msg(msg: &str) { #[allow(static_mut_refs)] -pub async fn send_cmd_json(func_name: &str, func_args: Option) -> Option { +pub async fn send_cmd_json(func_name: &str, func_args: Option, print_output: bool) -> Option { unsafe { let Some(sender) = &GLOBAL_SENDER else { return None }; let mut sender = sender.lock().await; @@ -56,7 +56,7 @@ pub async fn send_cmd_json(func_name: &str, func_args: Option) -> Option< let unw_args = func_args.unwrap_or(json!([])); let json_str = format!( - "json:{{\"type\": \"function\", \"value\":\"{}\", \"args\": {}}}", + "json:{{\"type\": \"function\", \"value\":\"{}\", \"args\": {}, \"print\": {print_output}}}", func_name, unw_args ); @@ -65,6 +65,11 @@ pub async fn send_cmd_json(func_name: &str, func_args: Option) -> Option< } let r = receive_response().await; + if let Some(rs) = r.clone() { + if !rs.get("print").unwrap_or(&json![false]).as_bool().unwrap() + { return r; } + } + if !["respond_mentions"].contains(&func_name) || ::parse().dev { rs_println!("Received from Python: [RESPONSE] {:?}", r); } From 4c21b6da4ede44b82aaffa99c36aa7510972b16a Mon Sep 17 00:00:00 2001 From: ByteDice Date: Sat, 7 Jun 2025 13:14:50 +0200 Subject: [PATCH 11/39] i kinda forgot to commit. Anyways, i made the config into toml, edited readme, and... refactored code... --- .gitignore | 12 ++--- Cargo.toml | 1 + README.md | 67 +++++++++++++++---------- {data => cfg}/8-ball_classic.txt | 0 {data => cfg}/8-ball_quirky.txt | 0 {data => cfg}/status.txt | 0 data/cfg_default.json | 8 --- data/defaults/cfg_default.toml | 19 +++++++ data/{ => defaults}/dc_data_preset.json | 0 data/{ => defaults}/re_data_preset.json | 0 req.txt | 5 ++ src/cmds/reload_cfg.rs | 8 +-- src/data.rs | 34 +++++++++---- src/events.rs | 6 +-- src/gen.rs | 59 +++++++++++----------- src/main.rs | 2 +- src/python/bot.py | 4 +- src/python/data.py | 20 +++++--- src/re_cmds/generic_fns.rs | 4 +- src/re_cmds/shorturl.rs | 1 + 20 files changed, 149 insertions(+), 101 deletions(-) rename {data => cfg}/8-ball_classic.txt (100%) rename {data => cfg}/8-ball_quirky.txt (100%) rename {data => cfg}/status.txt (100%) delete mode 100644 data/cfg_default.json create mode 100644 data/defaults/cfg_default.toml rename data/{ => defaults}/dc_data_preset.json (100%) rename data/{ => defaults}/re_data_preset.json (100%) create mode 100644 req.txt diff --git a/.gitignore b/.gitignore index d226673..022b0e9 100644 --- a/.gitignore +++ b/.gitignore @@ -3,12 +3,12 @@ target/ Cargo.lock **/*.rs.bk *.pdb -__pycache__/ +**/__pycache__/ .vscode/ # program-created data -data/reddit_data.json -data/discord_data.json -data/re_data.json -data/dc_data.json -data/cfg.json \ No newline at end of file +**/reddit_data.json +**/discord_data.json +**/re_data.json +**/dc_data.json +**/cfg.toml \ No newline at end of file diff --git a/Cargo.toml b/Cargo.toml index f35a927..933bcba 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -17,3 +17,4 @@ serde = "1.0.217" serde_json = "1.0.138" tokio = { version = "1.43.0", features = ["rt-multi-thread"] } tokio-tungstenite = "0.26.1" +toml = "0.8.23" diff --git a/README.md b/README.md index 25decfc..d86e802 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,4 @@ + # ByteDiceAssistant An automation tool primarily made for myself (Byte Dice) but publicly available for anyone to use. It's both a Discord and Reddit bot in one program. @@ -10,36 +11,45 @@ An automation tool primarily made for myself (Byte Dice) but publicly available **ByteDiceAssistant © 2025 by Byte Dice is licensed under CC BY-NC-SA 4.0.**\ **You can learn more about copyright by reading the full [license](/LICENSE.txt).** +## Commands + +| Name | Category | Description | +| -----------------------| -------- | -------------------------------------------------------------------- | +| `help` | help | Sends a help menu. | +| `8_ball` | fun | Sends a random answer to a prompt. | +| `add_server` | admin | Adds your server to the bots database for storage (no data is sold). | +| `embed` | owner | Creates an embed (requires `ASSISTANT_OWNERS` for security reasons). | +| `ping` | fun | Makes the bot reply with "pong" or a custom message. | +| `send` | owner | Sends a message (requires `ASSISTANT_OWNERS` for security reasons). | +| `stop` | owner | Stops the bot (requires `ASSISTANT_OWNERS` for security reasons). | +| ---------------------- | -------- | -------------------------------------------------------------------- | +| `admin_re_bindchannel` | admin | Sets the channel the command was run in as the one where `re_updatediscord` dumps information. This command is required for any of the other "re" commands to work. | +| `re_addpost` | re | Adds a post to the database. | +| `re_approvepost` | re | Flags a post in the database as approved. | +| `re_getpost` | re | Sends information about a post in the database. | +| `re_removepost` | re | "Removes" a post from the database (It actually only flags it as removed). | +| `re_shorturl` | re | Converts a long URL `https://www.reddit.com/r/SUBREDDIT/comments/POST_ID/POST_TITLE/` to a short one `https://redd.it/POST_ID`. | +| `re_topposts` | re | Sends the top posts in a category (such as upvotes). The posts have to be within the database. | +| `re_updatediscord` | re | Dumps the entire database (with a few restrictions) in the `admin_re_bindchannel` channel. | +| `re_vote` | re | Adds a vote (separate from Reddit upvotes) to a post. Use votes however you'd like. | + ## How to run ### Dependencies: -This program uses Rust (v1.82.0) and Python (v3.11.4), you can likely use other versions if they are compatible. This program also uses these Python modules: -* asyncio -* asyncpraw -* asyncprawcore -* emoji -* io -* json -* os -* sys -* threading -* time -* typing -* websockets - -You can install Python modules by running `$ pip install {module}` or `$ python -m pip install {module}` in a terminal. +This program uses Rust (v1.82.0) and Python (v3.11.4), you can likely use other versions if they are compatible.\ +It is required to install all used Python modules. You can find those in [req.txt](/req.txt). Installation instructions are in the *How to Run* section. ### Environment variables: -| **Name** | **Description** | -| ---------------------- | --- | -| `ASSISTANT_TOKEN` | The Discord bot token. (Create a Discord bot [here](https://discord.com/developers/docs/intro)!) | -| `ASSISTANT_TOKEN_TEST` | (Optional) A testing Discord bot token. This is only needed when the program is run with `-t` or `--test`. | -| `ASSISTANT_R_ID` | The id for the Reddit bot/account. (Create a Reddit bot [here](https://www.reddit.com/prefs/apps)!) | -| `ASSISTANT_R_TOKEN` | The token for the Reddit bot/account. | -| `ASSISTANT_R_NAME` | The username of the Reddit bot/account. | -| `ASSISTANT_R_PASS` | The password for the Reddit bot/account. | +| **Name** | **Description** | +| ---------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `ASSISTANT_TOKEN` | The Discord bot token. (Create a Discord bot [here](https://discord.com/developers/docs/intro)!) | +| `ASSISTANT_TOKEN_TEST` | (Optional) A testing Discord bot token. This is only needed when the program is run with `-t` or `--test`. | +| `ASSISTANT_R_ID` | The id for the Reddit bot/account. (Create a Reddit bot [here](https://www.reddit.com/prefs/apps)!) | +| `ASSISTANT_R_TOKEN` | The token for the Reddit bot/account. | +| `ASSISTANT_R_NAME` | The username of the Reddit bot/account. | +| `ASSISTANT_R_PASS` | The password for the Reddit bot/account. | | `ASSISTANT_OWNERS` | (OPTIONAL) A list of Discord user IDs that "own" the bot. Separate each ID with a single comma and **no** spaces. This will allow the specified user IDs to run root commands such as `/stop`, it will also DM these users when *certain* errors occur. | -| `ASSISTANT_BK_MODS` | (OPTIONAL) Same format as `ASSISTANT_OWNERS` but for people who are allowed to use the `/re_*` commands. | +| `ASSISTANT_BK_MODS` | (OPTIONAL) Same format as `ASSISTANT_OWNERS` but for people who are allowed to use the `/re_*` commands. | ### Required permissions: **These are automatically set if you use the [official invite link](https://discord.com/oauth2/authorize?client_id=1212127255795335208&permissions=84992&integration_type=0&scope=bot) or an invite link with the permissions integer set to `84992`.** (The permission integer is this part of the URL `&permissions=84992`) @@ -54,6 +64,7 @@ You can install Python modules by running `$ pip install {module}` or `$ python * Set the environment variables (listed above). * Restart the terminal. * Navigate to the project root directory. +* Install all Python modules. (`pip install -r req.txt`) * Run with `cargo run`. * Alternatively, run `cargo run -- {args here}` to add args. * For help, run `cargo run -- -h` or `cargo run -- --help`. @@ -64,16 +75,20 @@ You can install Python modules by running `$ pip install {module}` or `$ python * Open a terminal. * Set the environment variables (listed above). * On Windows: - * Run `setx VARIABLE_NAME "value in quotes"` in a terminal. - * On Unix (Linux / Mac): + * On Linux: * Run `sudo nano /etc/environment` or `sudo vim /etc/environment` in the terminal (and enter your password if prompted to). * Press `i` (only if you used VIM). * Write `VARIABLE_NAME="value"` + a new line for every variable. * if nano: `ctrl + O` (and press enter) then `ctrl + X`. * if VIM: press `esc` then write `:wq` (and press enter). + + * On macOS: + * Probably the same as Linux, but I don't use this OS so I have no clue. + * Restart the terminal if you added/changed any environment variables. * Run `cd path/to/extracted/folder` to navigate to the downloaded files (replace `path/to/extracted/folder` with your actual path). +* Install all python modules with `pip install -r req.txt`. * Run `cargo run` to start the program. You can view a list of options by running `cargo run -- --help` or `cargo run -- -h`. * If you only want to run the Python code, you can either run `cargo run -- --py` or `python ./src/python/main.py`. The second option is recommended for better error output. \ No newline at end of file diff --git a/data/8-ball_classic.txt b/cfg/8-ball_classic.txt similarity index 100% rename from data/8-ball_classic.txt rename to cfg/8-ball_classic.txt diff --git a/data/8-ball_quirky.txt b/cfg/8-ball_quirky.txt similarity index 100% rename from data/8-ball_quirky.txt rename to cfg/8-ball_quirky.txt diff --git a/data/status.txt b/cfg/status.txt similarity index 100% rename from data/status.txt rename to cfg/status.txt diff --git a/data/cfg_default.json b/data/cfg_default.json deleted file mode 100644 index 25ddd6e..0000000 --- a/data/cfg_default.json +++ /dev/null @@ -1,8 +0,0 @@ -{ - "comment": "IMPORTANT!!! Anything labeled with 'RESTART_{cfg-name-here}' requires a restart to apply", - "reddit": { - "RESTART_enabled": true, - "subreddits": "bytedicetesting", - "fetch_limit": 100 - } -} \ No newline at end of file diff --git a/data/defaults/cfg_default.toml b/data/defaults/cfg_default.toml new file mode 100644 index 0000000..7fca7cc --- /dev/null +++ b/data/defaults/cfg_default.toml @@ -0,0 +1,19 @@ +[reddit] +# Which subreddits the bot will scan when executing "re"-category commands. +# Is automatically disabled when `disabled_categories` includes "re". +# Separate multiple subreddits with a "+", e.g "memes+askreddit". +subreddits = "bytedicetesting" + +# How many posts the bot scans when running `/re_updatediscord`. +fetch_limit = 100 + +[commands] +# TODO: readme +# Disabled selected command categories. All commands and their categories can be viewed in the README. +# "admin" and "owner" categories are always enabled. +# [REQUIRES RESTART] +disabled_categories = [ + # "fun", + # "help" + # "re" +] \ No newline at end of file diff --git a/data/dc_data_preset.json b/data/defaults/dc_data_preset.json similarity index 100% rename from data/dc_data_preset.json rename to data/defaults/dc_data_preset.json diff --git a/data/re_data_preset.json b/data/defaults/re_data_preset.json similarity index 100% rename from data/re_data_preset.json rename to data/defaults/re_data_preset.json diff --git a/req.txt b/req.txt new file mode 100644 index 0000000..e0b97af --- /dev/null +++ b/req.txt @@ -0,0 +1,5 @@ +asyncpraw +asyncprawcore +emoji +toml +websockets \ No newline at end of file diff --git a/src/cmds/reload_cfg.rs b/src/cmds/reload_cfg.rs index e4e5fa3..e874040 100644 --- a/src/cmds/reload_cfg.rs +++ b/src/cmds/reload_cfg.rs @@ -1,6 +1,6 @@ use serde_json::json; -use crate::{data::{get_mutex_data, read_cfg_data}, lang, messages::send_msg, websocket::send_cmd_json, Context, Error}; +use crate::{data::{get_toml_mutex, read_cfg_data}, lang, messages::send_msg, websocket::send_cmd_json, Context, Error}; #[poise::command( @@ -17,14 +17,14 @@ pub async fn cmd( ) -> Result<(), Error> { read_cfg_data(&ctx.data(), false).await; - let d = get_mutex_data(&ctx.data().cfg).await?; - let d_str = serde_json::to_string(&d)?; + let d = get_toml_mutex(&ctx.data().cfg).await.unwrap(); + let d_str = toml::to_string(&d)?; let r = send_cmd_json("update_cfg", Some(json!([d_str])), true).await; if r.is_some() && r.unwrap()["value"].as_bool().unwrap() { send_msg( ctx, - lang!("dc_msg_reload_cfg_success", serde_json::to_string_pretty(&d).unwrap()), + lang!("dc_msg_reload_cfg_success", toml::to_string_pretty(&d).unwrap()), true, true ).await; diff --git a/src/data.rs b/src/data.rs index c23f19a..9e973f7 100644 --- a/src/data.rs +++ b/src/data.rs @@ -8,12 +8,15 @@ use crate::{errln, rs_println, Data, Error, CFG_DATA_RE, LANG}; use crate::websocket::send_cmd_json; -static DATA_PATH_DC: &str = "./data/dc_data.json"; -static PRESET_PATH_DC: &str = "./data/dc_data_preset.json"; -static DATA_PATH_RE: &str = "./data/re_data.json"; -static PRESET_PATH_RE: &str = "./data/re_data_preset.json"; -static DATA_PATH_CFG: &str = "./data/cfg.json"; -static PRESET_PATH_CFG: &str = "./data/cfg_default.json"; +static DATA_PATH_DC: &str = "./data/db/dc_data.json"; +static PRESET_PATH_DC: &str = "./data/defaults/dc_data_preset.json"; + +static DATA_PATH_RE: &str = "./data/db/re_data.json"; +static PRESET_PATH_RE: &str = "./data/defaults/re_data_preset.json"; + +static DATA_PATH_CFG: &str = "./cfg/cfg.toml"; +static PRESET_PATH_CFG: &str = "./data/defaults/cfg_default.toml"; + static DATA_PATH_LANG: &str = "./data/lang/"; pub static DC_POSTS_CHANNEL_KEY: &str = "re_posts_channel"; @@ -117,15 +120,15 @@ pub async fn read_cfg_data(data: &Data, wipe: bool) { if !Path::new(DATA_PATH_CFG).exists() || wipe { rs_println!( "{} creating new from preset...", - if !wipe { "cfg.json not found," } else { "[WIPE] (cfg.json)" } + if !wipe { "cfg.toml not found," } else { "[WIPE] (cfg.toml)" } ); generate_cfg_data(); } let str_data = fs::read_to_string(DATA_PATH_CFG).unwrap(); - let json_data = serde_json::from_str(&str_data).unwrap(); + let json_data: toml::Value = str_data.parse().unwrap(); let mut cfg_data = data.cfg.lock().await; - *cfg_data = json_data; + *cfg_data = Some(json_data); send_cmd_json("update_cfg", Some(json!([str_data])), true).await; } @@ -133,9 +136,9 @@ pub async fn read_cfg_data(data: &Data, wipe: bool) { fn generate_cfg_data() { let preset_str = fs::read_to_string(PRESET_PATH_CFG).unwrap(); - let preset_json: Value = serde_json::from_str(&preset_str).unwrap(); + let preset_json: toml::Value = preset_str.parse().unwrap(); - let json_str = serde_json::to_string_pretty(&preset_json).unwrap(); + let json_str = toml::to_string_pretty(&preset_json).unwrap(); let mut file = fs::File::create(DATA_PATH_CFG).unwrap(); file.write_all(json_str.as_bytes()).unwrap(); @@ -200,6 +203,15 @@ pub async fn get_mutex_data(data: &Mutex>) -> Result } +pub async fn get_toml_mutex(data: &Mutex>) -> Result { + let data_lock = data.lock().await; + return match data_lock.as_ref() { + Some(data) => Ok(data.clone()), + None => Err("Cannot get mutex data: The data is corrupted!".into()), + }; +} + + pub fn load_lang_data(lang: String) { let full_path = format!("{}{}.json", DATA_PATH_LANG, lang); diff --git a/src/events.rs b/src/events.rs index aa8f1d9..b3eeba8 100644 --- a/src/events.rs +++ b/src/events.rs @@ -19,11 +19,9 @@ pub fn event_handler<'a>( data_about_bot.user.id ); - let file_text = std::fs::read_to_string("./data/status.txt").unwrap(); + let file_text = std::fs::read_to_string("./cfg/status.txt").unwrap(); let custom_activity = ActivityData::custom(file_text); - // TODO: make custom rich presence - //let playing_activity - + ctx.online(); ctx.set_activity(Some(custom_activity)); } diff --git a/src/gen.rs b/src/gen.rs index df85ea8..00bcf49 100644 --- a/src/gen.rs +++ b/src/gen.rs @@ -4,12 +4,12 @@ use poise::serenity_prelude::UserId; use poise::serenity_prelude as serenity; use poise::serenity_prelude::Client; -use crate::{cmds, data::{self, get_mutex_data}, events, re_cmds, rs_println, Args, Cmd, Data}; +use crate::{cmds, data::{self, get_toml_mutex}, events, re_cmds, rs_println, Args, Cmd, Data}; pub async fn gen_data(args: Args, owners: Vec) -> Data { - let ball_classic_str = std::fs::read_to_string("./data/8-ball_classic.txt").unwrap(); - let ball_quirk_str = std::fs::read_to_string("./data/8-ball_quirky.txt").unwrap(); + let ball_classic_str = std::fs::read_to_string("./cfg/8-ball_classic.txt").unwrap(); + let ball_quirk_str = std::fs::read_to_string("./cfg/8-ball_quirky.txt").unwrap(); let ball_classic: Vec = ball_classic_str.lines().map(String::from).collect(); let ball_quirk: Vec = ball_quirk_str .lines().map(String::from).collect(); @@ -76,38 +76,39 @@ pub async fn gen_bot(data: Data, args: Args) -> Client { async fn make_cmd_vec(data: &Data) -> Vec { - let mut cmds = vec![]; - let cfg = get_mutex_data(&data.cfg).await.unwrap(); - - // GENERIC - cmds.extend([ + let mut cmds = vec![ + // GENERIC cmds::help::cmd(), cmds::ping::cmd(), + cmds::eight_ball::cmd(), + // REDDIT + re_cmds::add::cmd(), + re_cmds::approve::cmd(), + re_cmds::get::cmd(), + re_cmds::remove::cmd(), + re_cmds::top::cmd(), + re_cmds::update::cmd(), + re_cmds::vote::cmd(), + re_cmds::shorturl::cmd(), + // [ADMIN / OWNER] cmds::embed::cmd(), cmds::send::cmd(), cmds::stop::cmd(), - cmds::eight_ball::cmd(), - cmds::add_server::cmd() - ]); + cmds::add_server::cmd(), + cmds::reload_cfg::cmd(), + // REDDIT [ADMIN / OWNER] + re_cmds::admin_bind::cmd() + ]; + let cfg = get_toml_mutex(&data.cfg).await.unwrap(); - // REDDIT - if cfg["reddit"]["RESTART_enabled"].as_bool().unwrap() { - cmds.extend([ - re_cmds::add::cmd(), - re_cmds::approve::cmd(), - re_cmds::get::cmd(), - re_cmds::remove::cmd(), - re_cmds::top::cmd(), - re_cmds::update::cmd(), - re_cmds::vote::cmd(), - re_cmds::shorturl::cmd(), - re_cmds::admin_bind::cmd(), - ]); - } + let disabled = cfg["commands"]["disabled_categories"] + .as_array() + .unwrap() + .iter() + .filter_map(|v| v.as_str()) + .collect::>(); - cmds.extend([ - cmds::reload_cfg::cmd() - ]); + cmds.retain(|cmd| !disabled.contains(&cmd.category.as_ref().unwrap().as_str())); - return cmds + return cmds; } \ No newline at end of file diff --git a/src/main.rs b/src/main.rs index a95d3b5..e559eb3 100644 --- a/src/main.rs +++ b/src/main.rs @@ -86,7 +86,7 @@ struct Data { ball_prompts: [Vec; 2], reddit_data: Mutex>, discord_data: Mutex>, - cfg: Mutex>, + cfg: Mutex>, bk_mods: Vec, args: Args, } diff --git a/src/python/bot.py b/src/python/bot.py index c622ce1..2c4c3c5 100644 --- a/src/python/bot.py +++ b/src/python/bot.py @@ -3,7 +3,7 @@ import asyncpraw as praw import os from typing import Final from macros import * -import json +import toml RE_DATA_POSTS: Final[str] = "posts" @@ -56,7 +56,7 @@ class Bot: return False async def update_cfg_str(self, new_cfg: str) -> bool: - json_cfg = json.loads(new_cfg) + json_cfg = toml.loads(new_cfg) self.sr = await self.r.subreddit(json_cfg[CFG_DATA_RE]["subreddits"]) self.fetch_limit = json_cfg[CFG_DATA_RE]["fetch_limit"] return True diff --git a/src/python/data.py b/src/python/data.py index 5d4a4fd..bfcc516 100644 --- a/src/python/data.py +++ b/src/python/data.py @@ -1,4 +1,5 @@ import os +import toml import json import time @@ -6,7 +7,10 @@ import bot as botPy from macros import * -DATA_PATH = os.path.abspath(os.path.join(os.path.join(os.getcwd(), "data"))) +DATA_PATH = os.path.join(os.path.join(os.getcwd(), "data")) +DB_PATH = os.path.join(DATA_PATH, "db") +DEFAULT_PATH = os.path.join(DATA_PATH, "default") +CFG_PATH = os.path.join(os.path.join(os.getcwd(), "cfg")) class PostData: @@ -77,7 +81,7 @@ class PostData: def read_data(bot: botPy.Bot) -> bool: - r_path = os.path.join(DATA_PATH, "re_data.json") + r_path = os.path.join(DB_PATH, "re_data.json") if os.path.isfile(r_path): bot.data_f = open(r_path, "r+") @@ -113,23 +117,23 @@ def write_data(bot: botPy.Bot) -> bool: async def read_cfg(bot: botPy.Bot) -> bool: - r_path = os.path.join(DATA_PATH, "cfg.json") + r_path = os.path.join(CFG_PATH, "cfg.toml") if os.path.isfile(r_path): bot.data_f = open(r_path, "r+") else: - py_print("cfg.json not found, creating new from preset...") - with open(os.path.join(DATA_PATH, "cfg_default.json", "r")) as f: - data_preset_json = json.load(f) + py_print("cfg.toml not found, creating new from preset...") + with open(os.path.join(DEFAULT_PATH, "cfg_default.toml", "r")) as f: + data_preset_json = toml.load(f) with open(r_path, "w") as f: - json.dump(data_preset_json, f, indent = 2) + toml.dump(data_preset_json, f, indent = 2) bot.data_f = open(r_path, "r+") data_str = bot.data_f.read() - json_data = json.loads(data_str) + json_data = toml.loads(data_str) await bot.update_cfg(json_data) return True diff --git a/src/re_cmds/generic_fns.rs b/src/re_cmds/generic_fns.rs index 1b0eed0..e8758f2 100644 --- a/src/re_cmds/generic_fns.rs +++ b/src/re_cmds/generic_fns.rs @@ -1,7 +1,7 @@ use regex::Regex; use serde_json::Value; -use crate::{data::get_mutex_data, messages::{make_post_embed, make_removed_embed, send_embed}, Context, Error}; +use crate::{data::get_toml_mutex, messages::{make_post_embed, make_removed_embed, send_embed}, Context, Error}; pub fn is_bk_mod(mod_list: Vec, uid: u64) -> bool { return mod_list.contains(&uid); @@ -35,7 +35,7 @@ pub async fn send_embed_for_removed(ctx: Context<'_>, url: &str, post: &Value) { pub async fn get_readable_subreddits(ctx: Context<'_>) -> Result { - let d = get_mutex_data(&ctx.data().cfg).await?; + let d = get_toml_mutex(&ctx.data().cfg).await.unwrap(); let sr = d["reddit"]["subreddits"].as_str().ok_or("Item of key \"subreddit\" is not a string type.\nTrace: `get_readable_subreddits -> let sr = ...`")?; let split: Vec<&str> = sr.split("+").collect(); let join = split.join(", r/"); diff --git a/src/re_cmds/shorturl.rs b/src/re_cmds/shorturl.rs index f22b6b1..a30f50d 100644 --- a/src/re_cmds/shorturl.rs +++ b/src/re_cmds/shorturl.rs @@ -5,6 +5,7 @@ use crate::{lang, messages::send_msg, re_cmds::generic_fns::to_shorturl, Context slash_command, prefix_command, rename = "re_shorturl", + category = "re", required_bot_permissions = "SEND_MESSAGES | VIEW_CHANNEL" )] /// Convert a long reddit URL to a short one. The bot ONLY uses shortURLs when asking for one. From 203b7499ddbfb0d62ec5a9d5c65e328a535e86d3 Mon Sep 17 00:00:00 2001 From: ByteDice Date: Sat, 7 Jun 2025 16:58:51 +0200 Subject: [PATCH 12/39] added handling for disabling "re" commands & updated readme --- BDA_icon_64x64.png | Bin 0 -> 3928 bytes README.md | 11 ++++++++--- src/data.rs | 5 +---- src/main.rs | 24 ++++++++++++++++-------- 4 files changed, 25 insertions(+), 15 deletions(-) create mode 100644 BDA_icon_64x64.png diff --git a/BDA_icon_64x64.png b/BDA_icon_64x64.png new file mode 100644 index 0000000000000000000000000000000000000000..76de73e30f6dd25fbac735a8781d42f60661d023 GIT binary patch literal 3928 zcmV-e52x^nP)zcsh~@BXe(HCNn;;E8bi_|#Z_=|C^(2N zz6SpTT?JPKK~Mw{S65$)lz46fA7!LvYd8w@HGjK>Xuy)mT8Et6)eSD!kv0f zTNXYL9<*gZ;Y+2bJbqG~_4rM(orq0>U-b-Se{kvt>p43wJRsbgH;VfAJnBn=X>%?v zwOe)%4l$;Q5F$m96$}c5rJ9o2-@c)$ZEGVT&V6PP~Jn7Nu)iYDV<5JO(9| z`|muR%*e3UJs5$8N7sGrgTOXa%C7r)=(?37sC$5Ov*urwoW|ZoFW0o*eQ4i+(~Fvx z+JUo8=(@KwTdPWSvi+G1yqrL55<0G-dC|-2`<&xDNUX~*x`N$3=$HHLOJ5R@XcEwU ztN#E1010qNS#tmYE+YT{E+YYWr9XB601c2yL_t(|Ufo-Hj9pb3KX3Ym=`1ZRtut+b zDlUPD31W?iq)-8=Kxiqgf`4GC!PM50Ku}PYKv|m#XpD#~64Zp4D7Lh+6(vJh1jVSh z1;wZ=?QEqjoz8To%skKUy!X5Fd*3_n&V5%+l#|Td<(>1LZ~wmY-E#`gw=+IEzcjt0 z4YzK`&VAv^HaN6D+;+`uP2TMrDJA#qEroFZ|2Y21-@IBVoOJBrftp)BJK*LWQgCP9 z(CQAZFk{c)P;!qxcpf=09tdt}ZVFfS)4G4F1#l$|p?+&zBIX$3zY;1vRlCO^?zV+{G| z-go$`XNL0xI4B^LudImfsjE&2*C)@|5UwXZ{FN}@`1p9ZKD74O@c!)Y{4TtEYW;3^ z^x;kJ&7t!6pvRxNt?!*W9xl&uq!)MWkMl}G;0^#Z} z$P~_(hY1?hdUlng0P>&Wn_m6V`h4!+edD5Vuc5_;oh(G#K7jGH6Xpi#S#KV5RN9X= zug(?lmo8nJOrwsLf{H~A!opgHbSJqYvqmDM2@!!cqRPqy5=7)^$Fxg*w$fI$*Zjb0 zf#RmP+Z+PV)BM7XtqE&;IKh@mJkSADUkxjHG(f{A#?!v7f6!z`wU9W)R9}6B5*`cD z6a+#(=}t7DZEAHQq?1|#8exK}uqI5J&{%cFvt3W9nsa{j?2H;5rzQiRqR|6OaJF!Z zA_vygkE~lzL)+=!`gO3~yiSl3Ct$)lVg2Kc@Ud>+U#wpoetTl^jp2IoV^>!7J^q@< z!t=@xy_me;`}C@C54xLJ(mJARV(IuF!K?zrW`B9(cp6SI)38oc$G7`p2I&q|V_p2x z>+v2E7wvS%T`@gK#{$uHc|F4To(!|AHvL_FNfTxYgOGq9(vN8ZT?eN-OQxDi1PXn^TjrPk!NS-l`APELcuYBjN zst0g6eA|hK1$Z$;7~@s4=!eAya`5o}(TTBnG=Smkx$8cee9ei4IL$LO)1*n-FshNK zrMXaB513y5SXz?^k-M6_kU{Fb6-mt}8j%kB>dMZ}tIK4Ctt-kje}sNjt3j@d@g>$~ zXy7uJlH|Bu#XWoNX<@#tUpOXQm)`aJJTxa`Y{*oCW7*~7{$noMX+OUv5gy0jdfe5I z)ZnvsctLGFfYMj>C-DG;`&g@IDwJX#ZfI(&#MnxY&TTc&rqYTBKE<;o^?<+kmfTO* ze6TLQOYWRlPsci`U8xYtFtbXyyYCJP@9Hn_jL4dpkbnmrYlTA!+`{ ze`W}+sva;)l7krH^#H~kBZXn%%~cX1k>r$C8H2#xM zYJS9x?}*!$OLw@H-|VQ~AAm$iy{wh0+9uUfosT7w2*o6!NaA`uM`+Rh=2OoT}N*O2n8q3aopSvikj+-t#I&@if z&mV)++Wh$sRKY*{?mr~j2m)-lwa!wRhx;F|I;yVmys>wCZ9QOnW!Hqw;?itCqj^@9 z)U9urOmnnGfO~p)C72A#>UU84pbtpu0oaFUTP!4HC0hFdX+#HY%e+^o`o~~W5UnR` z0v1cg6mMVb=q`uBw59VhG#t+Q{6o>{Q1y6GT0r zh;=b;u-g;%ul7dg1O%5YTUO;8h@d(!oy4Z5h~XMp)YOo~EWh{9HM`R$BP308(ipaH zf4LqI3L>-xCPK`l2tYl)RclMUW?<@}X}h{&(;Q!ISA9-ao3@OfY@F^BO!f!RY+|8D zk+N}L zmrXjXda6Xp2#w$Mn4{z9+39@COl$0$rD~a)X^i1`S7be4q_TAkUsGqcmHD!32s@Nr zc*eb6DFwdQXWXNr0|d66vOfH@_=#(S4`BS?F!z@5{#Q4i8Ll6>>fvzx$~TyyrS<>jBezMM$M;3DXJ47+%RNeYRO$te8QVF=v;@0TLelSiMY9*F8x+ zz;ck%CUN>~`>K}IBZ?+<&UgP*k$AZ$7cm^CrdRd>>8n(W%VZ7^{c(N)t*Ra{SlK&A zfN2^KV}MFO#)Mb~hkiE7sKaF*5}U8yJ6-tH(<1DKVLc#d0M^CWIi&+8lXy&Hs4ckK zL`EH48bRuWD>~8${lV+2_rGKD`>H!n8&I@3>JQ@>+zP`RdBna&7?G5*MhY5MF6QK{ej;qFFzPB2o{mB%X4ilPK^w2b$ z_w*M$DxtcdPDeB#k?2n! zVVA$kG?nC!E;!OEQtI=0KX^IJ@x$}q>xM^PudN5LRNN3$>1=&O%n9nMPrbkPrTW|G zpc;`E$0QxEs6f^J06Ab4;9H67I}MSVEuxPy!X_eI^{FOgCFWE2y;Ku2vyP>yVP0rL z_mxr-!;e@N4kHz=d^Jt;_?&dID4_A22+#}H%h0kA^zUYfWm7B9l+**hec^z6|NJKR z^*283E;%3v8na-O(#Imp)!k$jn;1yI(kOz}UY@q?^4I+7)RMbz-A=b`L#r#Ccj{54 zThD*Ry*f~GD2xvKFHK|qWFbBwV?*H#e?n%5KPKDZvnTYyS7aPXo#RVr_?(7P8XPDV zZY?+h>$lv3!8nbI1B%$0jnuPmw?B>Q#~$iWsN#t26c2pFAA!XesaN_-Zkpe{8II@r zv)a@A(On#IMSr|Q+(sE|$m51%vw)3f;D`hc$8vEFXxPsKJRo8Gp?_;-=L?SfVjB>S z2fz5waW~&zFSvcSE8yffIC<|E2g3HTvu>VM#n`vo>GAt78E`#bpsVki9q0_!z!T)K m3ADHK>2W>%{@cIqEc^>BqT-yEOA-VC0000 # ByteDiceAssistant +![](/BDA_icon_64x64.png)\ An automation tool primarily made for myself (Byte Dice) but publicly available for anyone to use. It's both a Discord and Reddit bot in one program. > [!CAUTION] @@ -58,8 +59,12 @@ It is required to install all used Python modules. You can find those in [req.tx * View Channels * Embed Links -### How to run: -### Short answer for experienced people: +### Configuration: +You can find config files in the [cfg/](cfg/) folder. You can also find the default config in the [data/defaults/cfg_default.toml](data/defaults/cfg_default.toml) file, where comments are also listed.\ +**NOTE:** Some config files are automatically generated, and you will need to run the app once for them to generate. + +### How to start the program: +#### Short answer for experienced people: * Download the code. * Set the environment variables (listed above). * Restart the terminal. @@ -70,7 +75,7 @@ It is required to install all used Python modules. You can find those in [req.tx * For help, run `cargo run -- -h` or `cargo run -- --help`. * To only run the Python part, use `cargo run -- --py`, or for a better error output, `python ./src/python/main.py` -### Long answer for beginners: +#### Long answer for beginners: * Download the code (and extract it if needed). * Open a terminal. * Set the environment variables (listed above). diff --git a/src/data.rs b/src/data.rs index 9e973f7..c55c227 100644 --- a/src/data.rs +++ b/src/data.rs @@ -136,12 +136,9 @@ pub async fn read_cfg_data(data: &Data, wipe: bool) { fn generate_cfg_data() { let preset_str = fs::read_to_string(PRESET_PATH_CFG).unwrap(); - let preset_json: toml::Value = preset_str.parse().unwrap(); - - let json_str = toml::to_string_pretty(&preset_json).unwrap(); let mut file = fs::File::create(DATA_PATH_CFG).unwrap(); - file.write_all(json_str.as_bytes()).unwrap(); + file.write_all(preset_str.as_bytes()).unwrap(); } diff --git a/src/main.rs b/src/main.rs index e559eb3..79268ce 100644 --- a/src/main.rs +++ b/src/main.rs @@ -50,6 +50,7 @@ use tokio::runtime::Runtime; use tokio::sync::Mutex; use websocket::send_cmd_json; +use crate::data::get_toml_mutex; use crate::schedule::Schedule; @@ -103,6 +104,7 @@ async fn main() { let args = ::parse(); let args_str = serde_json::to_string(&args).expect("Error serializing args to JSON"); unsafe { NOPING = args.noping; } + rs_println!("Fetching language file..."); data::load_lang_data(args.clone().lang); @@ -115,13 +117,13 @@ async fn main() { .map(|s| s.parse::().expect("Failed to parse ASSISTANT_OWNERS. Invalid syntax.")) .collect(); + let data = gen_data(args.clone(), own_vec_u64.clone()).await; + if args.test { println!("----- USING TEST BOT -----"); } if args.dev { println!("----- DEV MODE ENABLED -----"); } if args.dev && args.wipe { println!("----- \"DON'T WORRY ABOUT IT\" MODE ENABLED -----"); } if args.nosched { println!("----- NO SCHEDULES -----"); } - // TODO: handle if config for reddit is disabled to not start python - if args.py && !args.rs { println!("----- PYTHON ONLY MODE -----"); rs_println!("ARGS: {}", args_str); @@ -131,27 +133,34 @@ async fn main() { else if args.rs && ! args.py { println!("----- RUST ONLY MODE -----"); rs_println!("ARGS: {}", args_str); - start(args, own_vec_u64.clone()).await; + start(args, data).await; process::exit(0); } rs_println!("ARGS: {}", args_str); + let cfg = get_toml_mutex(&data.cfg).await.unwrap(); + let cfg_arr = cfg["commands"]["disabled_categories"].as_array().unwrap(); + let contains: toml::Value = "re".parse().unwrap(); + let run_py = !cfg_arr.contains(&contains); + let rt_rs = Runtime::new().unwrap(); let rt_py = Runtime::new().unwrap(); let python_args = args.clone(); let rust_args = args.clone(); + if !run_py { rs_println!("[IMPORTANT] You have disabled the \"re\" commands in the CFG. The app will not run the Python code nor the websockets to save resources!"); } + let rust = thread::spawn(move || { rt_rs.block_on(async { - websocket::start(rust_args.clone(), own_vec_u64.clone()).await; - start(rust_args, own_vec_u64).await; + if run_py { websocket::start(rust_args.clone(), own_vec_u64.clone()).await; } + start(rust_args, data).await; }); }); let python = thread::spawn(move || { rt_py.block_on(async { - let _ = python::start(python_args).await; + if run_py { let _ = python::start(python_args).await; } }); }); @@ -168,8 +177,7 @@ async fn main() { } -async fn start(args: Args, owners: Vec) { - let data = gen_data(args.clone(), owners).await; +async fn start(args: Args, data: Data) { let mut bot = gen_bot(data, args).await; rs_println!("Starting Discord bot..."); From 8c5de5d10bccaa239cd260cc94be90db9dc10f28 Mon Sep 17 00:00:00 2001 From: ByteDice Date: Sat, 7 Jun 2025 17:08:40 +0200 Subject: [PATCH 13/39] fixed a bug with checking for strings in toml arrays --- data/defaults/cfg_default.toml | 1 - src/main.rs | 3 +-- 2 files changed, 1 insertion(+), 3 deletions(-) diff --git a/data/defaults/cfg_default.toml b/data/defaults/cfg_default.toml index 7fca7cc..0a6689e 100644 --- a/data/defaults/cfg_default.toml +++ b/data/defaults/cfg_default.toml @@ -8,7 +8,6 @@ subreddits = "bytedicetesting" fetch_limit = 100 [commands] -# TODO: readme # Disabled selected command categories. All commands and their categories can be viewed in the README. # "admin" and "owner" categories are always enabled. # [REQUIRES RESTART] diff --git a/src/main.rs b/src/main.rs index 79268ce..34c97f8 100644 --- a/src/main.rs +++ b/src/main.rs @@ -141,8 +141,7 @@ async fn main() { let cfg = get_toml_mutex(&data.cfg).await.unwrap(); let cfg_arr = cfg["commands"]["disabled_categories"].as_array().unwrap(); - let contains: toml::Value = "re".parse().unwrap(); - let run_py = !cfg_arr.contains(&contains); + let run_py = !cfg_arr.iter().any(|val| val.as_str() == Some("re")); let rt_rs = Runtime::new().unwrap(); let rt_py = Runtime::new().unwrap(); From b68586261c280cbfbdc6624176b9824b535cca63 Mon Sep 17 00:00:00 2001 From: ByteDice Date: Sat, 7 Jun 2025 17:11:22 +0200 Subject: [PATCH 14/39] typo --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 0ac33b9..da3801d 100644 --- a/README.md +++ b/README.md @@ -60,7 +60,7 @@ It is required to install all used Python modules. You can find those in [req.tx * Embed Links ### Configuration: -You can find config files in the [cfg/](cfg/) folder. You can also find the default config in the [data/defaults/cfg_default.toml](data/defaults/cfg_default.toml) file, where comments are also listed.\ +You can find config files in the [cfg/](cfg/) folder. You can also find the default configs in the [data/defaults/cfg_default.toml](data/defaults/cfg_default.toml) file.\ **NOTE:** Some config files are automatically generated, and you will need to run the app once for them to generate. ### How to start the program: From f0e3ab5e8f7168e6a06c3595caebd0cc80b61967 Mon Sep 17 00:00:00 2001 From: ByteDice Date: Sat, 7 Jun 2025 22:20:29 +0200 Subject: [PATCH 15/39] made 1 button do something. Only all of them left because it doesn't do what it should. --- src/events.rs | 70 ++++++++++++++++++++++++++++++-------- src/re_cmds/add.rs | 5 +-- src/re_cmds/approve.rs | 8 ++--- src/re_cmds/generic_fns.rs | 43 +++++++++++++++++++++-- src/re_cmds/remove.rs | 2 +- src/re_cmds/update.rs | 12 ++----- 6 files changed, 104 insertions(+), 36 deletions(-) diff --git a/src/events.rs b/src/events.rs index b3eeba8..5789f55 100644 --- a/src/events.rs +++ b/src/events.rs @@ -1,6 +1,7 @@ +use crate::re_cmds::generic_fns::{embed_to_json, is_bk_mod_serenity, serenity_send_msg}; use crate::{rs_println, Data, Error}; -use poise::serenity_prelude::{self as serenity, ActivityData}; +use poise::serenity_prelude::{self as serenity, ActivityData, ComponentInteraction, Interaction, Member, Ready}; use std::future::Future; use std::pin::Pin; @@ -9,22 +10,63 @@ pub fn event_handler<'a>( ctx: &'a serenity::Context, event: &'a serenity::FullEvent, _framework: poise::FrameworkContext<'a, Data, Error>, - _data: &'a Data, + data: &'a Data, ) -> Pin> + Send + 'a>> { Box::pin(async move { - if let serenity::FullEvent::Ready { data_about_bot } = event { - rs_println!( - "Bot started as user \"{}\" with id {}", - data_about_bot.user.name, - data_about_bot.user.id - ); - - let file_text = std::fs::read_to_string("./cfg/status.txt").unwrap(); - let custom_activity = ActivityData::custom(file_text); - - ctx.online(); - ctx.set_activity(Some(custom_activity)); + match event { + serenity::FullEvent::Ready { data_about_bot } => on_ready(ctx, data_about_bot), + serenity::FullEvent::InteractionCreate { interaction } => { let _ = handle_buttons(ctx, data, interaction).await; }, + _ => {} } return Ok(()); }) +} + + +fn on_ready(ctx: &serenity::Context, data_about_bot: &Ready) { + rs_println!( + "Bot started as user \"{}\" with id {}", + data_about_bot.user.name, + data_about_bot.user.id + ); + + let file_text = std::fs::read_to_string("./cfg/status.txt").unwrap(); + let custom_activity = ActivityData::custom(file_text); + + ctx.online(); + ctx.set_activity(Some(custom_activity)); +} + + +async fn handle_buttons(ctx: &serenity::Context, data: &Data, interaction: &Interaction) -> Result<(), Error> { + let component = match interaction { + Interaction::Component(component) => component, + _ => return Err(Error::from("Not a message component interaction")) + }; + + let i_msg = interaction.clone().message_component(); + if i_msg.is_none() { return Err(Error::from("message_component is None!")); } + let i_embed = i_msg.unwrap().message.embeds[0].clone(); + + let json = embed_to_json(&i_embed); + if json.is_err() { return Err(Error::from("Failed to pase message JSON!")); } + + return match component.data.custom_id.as_str() { + "approve_btn" => approve_btn(ctx, data, &component.member.as_ref().unwrap(), component).await, + "remove_btn" => Ok(()), + "unapprove_btn" => Ok(()), + "unremove_btn" => Ok(()), + "unvote_btn" => Ok(()), + "vote_btn" => Ok(()), + _ => Err("Message button with that ID isn't handled.".into()) + } +} + + +async fn approve_btn(ctx: &serenity::Context, data: &Data, c_member: &Member, component: &ComponentInteraction) -> Result<(), Error> { + if !is_bk_mod_serenity(ctx, data, c_member, component).await { return Ok(()); } + + serenity_send_msg(ctx, component, "Hello from this stupid program that tastes oddly like pasta.".to_string(), true).await; + + return Ok(()); } \ No newline at end of file diff --git a/src/re_cmds/add.rs b/src/re_cmds/add.rs index 570ac95..c623a95 100644 --- a/src/re_cmds/add.rs +++ b/src/re_cmds/add.rs @@ -1,6 +1,6 @@ use serde_json::json; -use crate::data::get_mutex_data; +use crate::data::{get_mutex_data, update_re_data}; use crate::messages::send_msg; use crate::re_cmds::get::get_post_from_data; use crate::{data, websocket::send_cmd_json, Context, Error, CFG_DATA_RE}; @@ -22,7 +22,7 @@ pub async fn cmd( ) -> Result<(), Error> { if !is_bk_mod(ctx.data().bk_mods.clone(), ctx.author().id.get()) { - let sr = get_readable_subreddits(ctx).await?; + let sr = get_readable_subreddits(ctx.data()).await?; send_msg(ctx, lang!("dc_msg_re_permdeny_not_re_mod", sr), false, false).await; return Ok(()); } @@ -59,6 +59,7 @@ pub async fn cmd( if a { send_msg(ctx, lang!("dc_msg_re_also_approved"), true, true).await; } } + update_re_data(ctx.data()).await; if let Some(post) = get_post_from_data(ctx, &reddit_data, &url).await? { send_embed_for_post(ctx, post, &url).await?; } diff --git a/src/re_cmds/approve.rs b/src/re_cmds/approve.rs index 6901ca0..84ce74d 100644 --- a/src/re_cmds/approve.rs +++ b/src/re_cmds/approve.rs @@ -1,6 +1,6 @@ use serde_json::{json, Value}; -use crate::{data::{self, get_mutex_data}, lang, messages::send_msg, re_cmds::generic_fns::{get_readable_subreddits, is_bk_mod}, websocket, Context, Error, CFG_DATA_RE}; +use crate::{data::{self, get_mutex_data}, lang, messages::send_msg, re_cmds::generic_fns::is_bk_mod_msg, websocket, Context, Error, CFG_DATA_RE}; use super::generic_fns::send_embed_for_removed; @@ -18,11 +18,7 @@ pub async fn cmd( #[description = "Wether to approve or disapprove the post"] disapprove: Option ) -> Result<(), Error> { - if !is_bk_mod(ctx.data().bk_mods.clone(), ctx.author().id.get()) { - let sr = get_readable_subreddits(ctx).await?; - send_msg(ctx, lang!("dc_msg_re_permdeny_not_re_mod", sr), false, false).await; - return Ok(()); - } + if is_bk_mod_msg(ctx).await { return Ok(()); } data::update_re_data(ctx.data()).await; let reddit_data = get_mutex_data(&ctx.data().reddit_data).await?; diff --git a/src/re_cmds/generic_fns.rs b/src/re_cmds/generic_fns.rs index e8758f2..f4aedce 100644 --- a/src/re_cmds/generic_fns.rs +++ b/src/re_cmds/generic_fns.rs @@ -1,12 +1,38 @@ +use poise::serenity_prelude::{self as serenity, ComponentInteraction, CreateInteractionResponse, CreateInteractionResponseMessage, Embed, Member}; use regex::Regex; use serde_json::Value; -use crate::{data::get_toml_mutex, messages::{make_post_embed, make_removed_embed, send_embed}, Context, Error}; +use crate::{data::get_toml_mutex, lang, messages::{make_post_embed, make_removed_embed, send_embed, send_msg, JSON_TEXT_END, JSON_TEXT_START}, Context, Data, Error}; pub fn is_bk_mod(mod_list: Vec, uid: u64) -> bool { return mod_list.contains(&uid); } + +pub async fn is_bk_mod_msg(ctx: Context<'_>) -> bool { + if is_bk_mod(ctx.data().bk_mods.clone(), ctx.author().id.get()) { return true; } + + let sr = get_readable_subreddits(ctx.data()).await.unwrap(); + send_msg(ctx, lang!("dc_msg_re_permdeny_not_re_mod", sr), false, false).await; + return true +} + + +pub async fn is_bk_mod_serenity(ctx: &serenity::Context, data: &Data, author: &Member, component: &ComponentInteraction) -> bool { + if is_bk_mod(data.bk_mods.clone(), author.user.id.get()) { return true; } + + let sr = get_readable_subreddits(data).await.unwrap(); + serenity_send_msg(ctx, component, lang!("dc_msg_re_permdeny_not_re_mod", sr), true).await; + return true +} + + +pub async fn serenity_send_msg(ctx: &serenity::Context, component: &ComponentInteraction, t: String, ephemeral: bool) { + let r = CreateInteractionResponse::Message(CreateInteractionResponseMessage::new().content(t).ephemeral(ephemeral)); + let _ = component.create_response(ctx.http.clone(), r).await; +} + + pub fn to_shorturl(url: &str) -> Result { let re = Regex::new(r"comments/([a-zA-Z0-9]+)").unwrap(); @@ -34,11 +60,22 @@ pub async fn send_embed_for_removed(ctx: Context<'_>, url: &str, post: &Value) { } -pub async fn get_readable_subreddits(ctx: Context<'_>) -> Result { - let d = get_toml_mutex(&ctx.data().cfg).await.unwrap(); +pub async fn get_readable_subreddits(data: &Data) -> Result { + let d = get_toml_mutex(&data.cfg).await.unwrap(); let sr = d["reddit"]["subreddits"].as_str().ok_or("Item of key \"subreddit\" is not a string type.\nTrace: `get_readable_subreddits -> let sr = ...`")?; let split: Vec<&str> = sr.split("+").collect(); let join = split.join(", r/"); return Ok(join); +} + + +pub fn embed_to_json(embed: &Embed) -> Result { + let msg_desc = embed.description.clone().unwrap(); + let msg_lines = msg_desc.split("\n"); + let msg_last_len = msg_lines.clone().last().unwrap().len(); + + let msg_json_str = &msg_lines.clone().last().unwrap()[JSON_TEXT_START.len()..msg_last_len - JSON_TEXT_END.len()]; + let msg_json: Result = serde_json::from_str(msg_json_str); + return msg_json; } \ No newline at end of file diff --git a/src/re_cmds/remove.rs b/src/re_cmds/remove.rs index 0755f8e..bd240bd 100644 --- a/src/re_cmds/remove.rs +++ b/src/re_cmds/remove.rs @@ -17,7 +17,7 @@ pub async fn cmd( ) -> Result<(), Error> { if !is_bk_mod(ctx.data().bk_mods.clone(), ctx.author().id.get()) { - let sr = get_readable_subreddits(ctx).await?; + let sr = get_readable_subreddits(ctx.data()).await?; send_msg(ctx, lang!("dc_msg_re_permdeny_not_re_mod", sr), false, false).await; return Ok(()); } diff --git a/src/re_cmds/update.rs b/src/re_cmds/update.rs index a2586db..efa36f5 100644 --- a/src/re_cmds/update.rs +++ b/src/re_cmds/update.rs @@ -3,7 +3,7 @@ use std::time::{SystemTime, UNIX_EPOCH}; use poise::{serenity_prelude::{ChannelId, EditMessage, GetMessages, Http, Message, MessageId, UserId}, ReplyHandle}; use serde_json::{json, Map, Value}; -use crate::{data::{self, get_mutex_data, DC_POSTS_CHANNEL_KEY}, lang, messages::{edit_reply, embed_from_options, http_send_embed, make_post_embed, make_removed_embed, send_msg, JSON_TEXT_END, JSON_TEXT_START}, websocket::send_cmd_json, Context, Error, CFG_DATA_RE}; +use crate::{data::{self, get_mutex_data, DC_POSTS_CHANNEL_KEY}, lang, messages::{edit_reply, embed_from_options, http_send_embed, make_post_embed, make_removed_embed, send_msg}, re_cmds::generic_fns::embed_to_json, websocket::send_cmd_json, Context, Error, CFG_DATA_RE}; #[poise::command( slash_command, @@ -175,15 +175,7 @@ async fn msgs_to_json(msgs: Vec, reddit_data: &Value, max_age: u64) -> continue; } - let msg_desc = &msg.embeds[0].description.clone().unwrap(); - let msg_lines = msg_desc.split("\n"); - let msg_last_len = msg_lines.clone().last().unwrap().len(); - - if msg_last_len < 13 { continue; } - - let msg_json_str = &msg_lines.clone().last().unwrap()[JSON_TEXT_START.len()..msg_last_len - JSON_TEXT_END.len()]; - - let msg_json = serde_json::from_str(msg_json_str); + let msg_json = embed_to_json(&msg.embeds[0]); if msg_json.is_err() { continue; } let mut u_json: Value = msg_json.unwrap(); From 5acdebbb3d4e9314a4c73f0749d6e95c1795a827 Mon Sep 17 00:00:00 2001 From: ByteDice Date: Sun, 8 Jun 2025 14:02:28 +0200 Subject: [PATCH 16/39] forgor to commit again. I made the approve button work as intended, refactored code, and added more language support --- data/lang/en.json | 19 ++++++++++++++- src/data.rs | 2 +- src/events.rs | 37 +++++++++++++++++++++------- src/messages.rs | 19 +++++++++------ src/python/posts.py | 8 +++--- src/re_cmds/add.rs | 8 ++---- src/re_cmds/approve.rs | 2 +- src/re_cmds/generic_fns.rs | 11 +++++++-- src/re_cmds/remove.rs | 8 ++---- src/re_cmds/update.rs | 50 ++++++++++++++++++-------------------- 10 files changed, 99 insertions(+), 65 deletions(-) diff --git a/data/lang/en.json b/data/lang/en.json index df224ae..1a779b9 100644 --- a/data/lang/en.json +++ b/data/lang/en.json @@ -1,4 +1,10 @@ { + "dc_btn_approve": "Approve", + "dc_btn_remove": "Remove", + "dc_btn_unapprove": "Disapprove", + "dc_btn_unremove": "Restore", + "dc_btn_unvote": "Un-vote", + "dc_btn_vote": "Vote", "dc_msg_8-ball_answer": "## You shook a magic 8-ball\nQ: {0}\nA: {1}", "dc_msg_add_to_data": "Added your server to my data! Thanks for letting me steal it! (/s)", "dc_msg_bound_channel": "Successfully bound channel ID `{0}` as the \"where all collected Reddit data gets dumped\" channel!", @@ -36,6 +42,17 @@ "dc_msg_re_vote_success": "Successfully voted!", "dc_msg_reload_cfg_python_fail": "Failed to reload configs: Failed-type response from Python.", "dc_msg_reload_cfg_success": "Successfully reloaded the configs!\nNew configs:\n```\n{0}\n```", + "dc_msg_removed_square_brackets": "[REMOVED] {0}", "dc_msg_shorturl": "ShortURL: <{0}>", - "log_lang_load_success": "Successfully loaded the english language file!" + "dc_msg_update_add": "{0}Adding new posts...", + "dc_msg_update_done": "{0}Done!", + "dc_msg_update_editing": "{0}Editing updated posts...", + "dc_msg_update_fetch": "{0}Fetching new posts & updating data file...", + "dc_msg_update_parse": "{0}Parsing messages to JSON...", + "dc_msg_update_read": "{0}Reading messages in <#{1}>...", + "dc_msg_update_removing_dupe": "{0}Removing duplicate posts...", + "dc_msg_update_removing_old": "{0}Removing old posts (threshold: {1}d)...", + "dc_msg_update_removing": "{0}Removing removed posts...", + "log_lang_load_success": "Successfully loaded the english language file!", + "none": "None" } \ No newline at end of file diff --git a/src/data.rs b/src/data.rs index c55c227..28437ec 100644 --- a/src/data.rs +++ b/src/data.rs @@ -83,7 +83,7 @@ pub async fn read_re_data(data: &Data, wipe: bool) { } let str_data = fs::read_to_string(DATA_PATH_RE).unwrap(); - let json_data = serde_json::from_str(&str_data).unwrap(); + let json_data: Option = serde_json::from_str(&str_data).unwrap(); let mut re_data = data.reddit_data.lock().await; *re_data = json_data; } diff --git a/src/events.rs b/src/events.rs index 5789f55..d58259e 100644 --- a/src/events.rs +++ b/src/events.rs @@ -1,7 +1,10 @@ -use crate::re_cmds::generic_fns::{embed_to_json, is_bk_mod_serenity, serenity_send_msg}; -use crate::{rs_println, Data, Error}; +use crate::data::{get_mutex_data, update_re_data}; +use crate::messages::make_post_embed; +use crate::re_cmds::generic_fns::{is_bk_mod_serenity, serenity_edit_msg_embed, serenity_send_msg}; +use crate::{lang, rs_println, websocket, Data, Error, CFG_DATA_RE}; use poise::serenity_prelude::{self as serenity, ActivityData, ComponentInteraction, Interaction, Member, Ready}; +use serde_json::json; use std::future::Future; use std::pin::Pin; @@ -47,14 +50,12 @@ async fn handle_buttons(ctx: &serenity::Context, data: &Data, interaction: &Inte let i_msg = interaction.clone().message_component(); if i_msg.is_none() { return Err(Error::from("message_component is None!")); } let i_embed = i_msg.unwrap().message.embeds[0].clone(); - - let json = embed_to_json(&i_embed); - if json.is_err() { return Err(Error::from("Failed to pase message JSON!")); } + let url = i_embed.url.clone().unwrap(); return match component.data.custom_id.as_str() { - "approve_btn" => approve_btn(ctx, data, &component.member.as_ref().unwrap(), component).await, + "approve_btn" => approve_btn(ctx, data, &component.member.as_ref().unwrap(), component, url, true).await, "remove_btn" => Ok(()), - "unapprove_btn" => Ok(()), + "unapprove_btn" => approve_btn(ctx, data, &component.member.as_ref().unwrap(), component, url, false).await, "unremove_btn" => Ok(()), "unvote_btn" => Ok(()), "vote_btn" => Ok(()), @@ -63,10 +64,28 @@ async fn handle_buttons(ctx: &serenity::Context, data: &Data, interaction: &Inte } -async fn approve_btn(ctx: &serenity::Context, data: &Data, c_member: &Member, component: &ComponentInteraction) -> Result<(), Error> { +async fn approve_btn(ctx: &serenity::Context, data: &Data, c_member: &Member, component: &ComponentInteraction, url: String, approve: bool) -> Result<(), Error> { if !is_bk_mod_serenity(ctx, data, c_member, component).await { return Ok(()); } - serenity_send_msg(ctx, component, "Hello from this stupid program that tastes oddly like pasta.".to_string(), true).await; + let r = websocket::send_cmd_json("set_approve_post", Some(json!([approve, url])), true).await.unwrap(); + + let c_id = component.channel_id; + let m_id = component.message.id; + + if r["value"].as_bool().unwrap() { + update_re_data(data).await; + let new_data = &get_mutex_data(&data.reddit_data).await.unwrap()[CFG_DATA_RE][&url]; + let e = make_post_embed(new_data, &url, true); + + if approve { + serenity_edit_msg_embed(ctx, &c_id, &m_id, e).await; + serenity_send_msg(ctx, component, lang!("dc_msg_re_post_approve_success"), true).await; + } + else { + serenity_edit_msg_embed(ctx, &c_id, &m_id, e).await; + serenity_send_msg(ctx, component, lang!("dc_msg_re_post_disapprove_success"), true).await; + } + } return Ok(()); } \ No newline at end of file diff --git a/src/messages.rs b/src/messages.rs index d8b3318..b058933 100644 --- a/src/messages.rs +++ b/src/messages.rs @@ -128,6 +128,7 @@ pub async fn send_embed( } +#[allow(dead_code)] pub async fn http_send_embed( http: &Http, c_id: ChannelId, @@ -231,11 +232,11 @@ pub fn make_post_embed(post_data: &Value, url: &str, ephemeral: bool) -> EmbedOp let media_urls = post_data["post_data"]["media_urls"].as_array().unwrap(); let action_row = CreateActionRow::Buttons(vec![ - CreateButton::new("vote_btn") .label("Vote") .emoji(ReactionType::Unicode("⬆️".to_string())), - CreateButton::new("unvote_btn") .label("Un-vote"), - CreateButton::new("approve_btn") .label("Approve") .emoji(ReactionType::Unicode("✅".to_string())), - CreateButton::new("unapprove_btn").label("Disapprove") .emoji(ReactionType::Unicode("❌".to_string())), - CreateButton::new("remove_btn") .label("Remove") .emoji(ReactionType::Unicode("🗑️".to_string())) + CreateButton::new("vote_btn") .label(lang!("dc_btn_vote")) .emoji(ReactionType::Unicode("⬆️".to_string())), + CreateButton::new("unvote_btn") .label(lang!("dc_btn_unvote")), + CreateButton::new("approve_btn") .label(lang!("dc_btn_approve")) .emoji(ReactionType::Unicode("✅".to_string())), + CreateButton::new("unapprove_btn").label(lang!("dc_btn_unapprove")) .emoji(ReactionType::Unicode("❌".to_string())), + CreateButton::new("remove_btn") .label(lang!("dc_btn_remove")) .emoji(ReactionType::Unicode("🗑️".to_string())) ]); return EmbedOptions { @@ -256,19 +257,21 @@ pub fn make_post_embed(post_data: &Value, url: &str, ephemeral: bool) -> EmbedOp pub fn make_removed_embed(post_data: &Value, url: &str, ephemeral: bool) -> EmbedOptions { let action_row = CreateActionRow::Buttons(vec![ - CreateButton::new("unremove_btn").label("Un-remove").emoji(ReactionType::Unicode("↩️".to_string())) + CreateButton::new("unremove_btn").label(lang!("dc_btn_unremove")).emoji(ReactionType::Unicode("↩️".to_string())) ]); + let none = lang!("none"); + let desc = lang!( "dc_msg_embed_re_removed", post_data["removed"]["by"].as_str().unwrap(), if !post_data["removed"]["reason"].is_null() { post_data["removed"]["reason"].as_str().unwrap() } - else { "None" }, + else { &none }, url ); return EmbedOptions { - title: Some(format!("[REMOVED] {}", post_data["post_data"]["title"])), + title: Some(lang!("dc_msg_removed_square_brackets", post_data["post_data"]["title"].clone())), desc: format!("{}\n\n{}{}{}", desc, JSON_TEXT_START, serde_json::to_string(&post_data).unwrap(), JSON_TEXT_END), col: Some(REMOVED_DC_COL), url: Some(url.to_string()), diff --git a/src/python/posts.py b/src/python/posts.py index 5677cd0..9a08a9e 100644 --- a/src/python/posts.py +++ b/src/python/posts.py @@ -45,8 +45,6 @@ async def add_new_posts(bot: botPy.Bot, max_age: int) -> bool: if not media[0]: without_media += 1 continue - - post_added = False post_added = data.add_post_to_data( bot, @@ -57,11 +55,13 @@ async def add_new_posts(bot: botPy.Bot, max_age: int) -> bool: else: not_added += 1 py_print(f"Successfully fetched {len(posts)} posts.\n" + - f" Out of which were {added_posts} added.\n" + - f" {without_media} had no media, " + + f" Out of which were {added_posts} added.\n" + + f" {without_media} had no media, " + f"{not_added} are removed or already existed, " + f"and {old_posts} were older than the max age threshold.") + data.write_data(bot) + return True diff --git a/src/re_cmds/add.rs b/src/re_cmds/add.rs index c623a95..6418a65 100644 --- a/src/re_cmds/add.rs +++ b/src/re_cmds/add.rs @@ -4,7 +4,7 @@ use crate::data::{get_mutex_data, update_re_data}; use crate::messages::send_msg; use crate::re_cmds::get::get_post_from_data; use crate::{data, websocket::send_cmd_json, Context, Error, CFG_DATA_RE}; -use crate::re_cmds::generic_fns::{get_readable_subreddits, is_bk_mod, send_embed_for_post, to_shorturl}; +use crate::re_cmds::generic_fns::{is_bk_mod_msg, send_embed_for_post, to_shorturl}; use crate::lang; #[poise::command( @@ -21,11 +21,7 @@ pub async fn cmd( #[description = "Wether to approve it after adding it"] approve: Option ) -> Result<(), Error> { - if !is_bk_mod(ctx.data().bk_mods.clone(), ctx.author().id.get()) { - let sr = get_readable_subreddits(ctx.data()).await?; - send_msg(ctx, lang!("dc_msg_re_permdeny_not_re_mod", sr), false, false).await; - return Ok(()); - } + if is_bk_mod_msg(ctx).await { return Ok(()); } let shorturl_u = to_shorturl(&url); let shorturl = &shorturl_u.unwrap_or(url.clone()); diff --git a/src/re_cmds/approve.rs b/src/re_cmds/approve.rs index 84ce74d..a023983 100644 --- a/src/re_cmds/approve.rs +++ b/src/re_cmds/approve.rs @@ -37,7 +37,7 @@ async fn approve_cmd(ctx: Context<'_>, url: &str, reddit_data: &Value, approve: } let r = websocket::send_cmd_json("set_approve_post", Some(json!([approve, &url])), true).await.unwrap(); - if r.get("value").is_some() { + if r["value"].as_bool().unwrap() { if approve { send_msg(ctx, lang!("dc_msg_re_post_approve_success"), true, true).await; } diff --git a/src/re_cmds/generic_fns.rs b/src/re_cmds/generic_fns.rs index f4aedce..df38e0c 100644 --- a/src/re_cmds/generic_fns.rs +++ b/src/re_cmds/generic_fns.rs @@ -1,8 +1,8 @@ -use poise::serenity_prelude::{self as serenity, ComponentInteraction, CreateInteractionResponse, CreateInteractionResponseMessage, Embed, Member}; +use poise::serenity_prelude::{self as serenity, ChannelId, ComponentInteraction, CreateInteractionResponse, CreateInteractionResponseMessage, EditMessage, Embed, Member, MessageId}; use regex::Regex; use serde_json::Value; -use crate::{data::get_toml_mutex, lang, messages::{make_post_embed, make_removed_embed, send_embed, send_msg, JSON_TEXT_END, JSON_TEXT_START}, Context, Data, Error}; +use crate::{data::get_toml_mutex, lang, messages::{embed_from_options, make_post_embed, make_removed_embed, send_embed, send_msg, EmbedOptions, JSON_TEXT_END, JSON_TEXT_START}, Context, Data, Error}; pub fn is_bk_mod(mod_list: Vec, uid: u64) -> bool { return mod_list.contains(&uid); @@ -33,6 +33,12 @@ pub async fn serenity_send_msg(ctx: &serenity::Context, component: &ComponentInt } +pub async fn serenity_edit_msg_embed(ctx: &serenity::Context, c_id: &ChannelId, m_id: &MessageId, e: EmbedOptions) { + let r = EditMessage::new().embed(embed_from_options(e)); + let _ = c_id.edit_message(ctx.http.clone(), m_id, r).await; +} + + pub fn to_shorturl(url: &str) -> Result { let re = Regex::new(r"comments/([a-zA-Z0-9]+)").unwrap(); @@ -51,6 +57,7 @@ pub async fn send_embed_for_post(ctx: Context<'_>, post: Value, url: &str) -> Re return Ok(()); } + pub async fn send_embed_for_removed(ctx: Context<'_>, url: &str, post: &Value) { send_embed( ctx, diff --git a/src/re_cmds/remove.rs b/src/re_cmds/remove.rs index bd240bd..c34cf69 100644 --- a/src/re_cmds/remove.rs +++ b/src/re_cmds/remove.rs @@ -1,6 +1,6 @@ use serde_json::json; -use crate::{data::{self, get_mutex_data}, lang, messages::send_msg, re_cmds::{generic_fns::{get_readable_subreddits, is_bk_mod, send_embed_for_removed}, get::get_post_from_data}, websocket::send_cmd_json, Context, Error}; +use crate::{data::{self, get_mutex_data}, lang, messages::send_msg, re_cmds::{generic_fns::{is_bk_mod_msg, send_embed_for_removed}, get::get_post_from_data}, websocket::send_cmd_json, Context, Error}; #[poise::command( slash_command, @@ -16,11 +16,7 @@ pub async fn cmd( #[description = "The reason of the removal."] reason: Option ) -> Result<(), Error> { - if !is_bk_mod(ctx.data().bk_mods.clone(), ctx.author().id.get()) { - let sr = get_readable_subreddits(ctx.data()).await?; - send_msg(ctx, lang!("dc_msg_re_permdeny_not_re_mod", sr), false, false).await; - return Ok(()); - } + if is_bk_mod_msg(ctx).await { return Ok(()); } let auth = &ctx.author().name; let r = send_cmd_json("remove_post_url", Some(json!([&url, &auth, &reason])), true).await.unwrap(); diff --git a/src/re_cmds/update.rs b/src/re_cmds/update.rs index efa36f5..0093867 100644 --- a/src/re_cmds/update.rs +++ b/src/re_cmds/update.rs @@ -3,7 +3,7 @@ use std::time::{SystemTime, UNIX_EPOCH}; use poise::{serenity_prelude::{ChannelId, EditMessage, GetMessages, Http, Message, MessageId, UserId}, ReplyHandle}; use serde_json::{json, Map, Value}; -use crate::{data::{self, get_mutex_data, DC_POSTS_CHANNEL_KEY}, lang, messages::{edit_reply, embed_from_options, http_send_embed, make_post_embed, make_removed_embed, send_msg}, re_cmds::generic_fns::embed_to_json, websocket::send_cmd_json, Context, Error, CFG_DATA_RE}; +use crate::{data::{self, get_mutex_data, DC_POSTS_CHANNEL_KEY}, lang, messages::{edit_reply, embed_from_options, make_post_embed, make_removed_embed, send_embed, send_msg}, re_cmds::generic_fns::embed_to_json, rs_println, websocket::send_cmd_json, Context, Error, CFG_DATA_RE}; #[poise::command( slash_command, @@ -30,7 +30,7 @@ pub async fn cmd( let mut p_text = "`/re_updatediscord`:".to_string(); let progress = send_msg(ctx, p_text.clone(), true, true).await.unwrap(); - p_text = update_progress(ctx, progress.clone(), p_text, "\nFetching new posts & updating data file...".to_string()).await; + p_text = update_progress(ctx, progress.clone(), p_text, lang!("dc_msg_update_fetch", "\n")).await; let max_age_u = max_age.unwrap_or(8); let max_age_secs = max_age_u as u64 * (60 * 60 * 24); @@ -49,47 +49,47 @@ pub async fn cmd( let c_id = c_id_u.unwrap(); // Reading messages - p_text = update_progress(ctx, progress.clone(), p_text.clone(), format!("✅\nReading messages in <#{}>...", c_id)).await; + p_text = update_progress(ctx, progress.clone(), p_text.clone(), lang!("dc_msg_update_read", "✅\n", c_id)).await; let msgs = read_msgs(http, ctx.framework().bot_id, c_id).await; // Parsing messages to JSON - p_text = update_progress(ctx, progress.clone(), p_text.clone(), "✅\nParsing messages to JSON...".to_string()).await; + p_text = update_progress(ctx, progress.clone(), p_text.clone(), lang!("dc_msg_update_parse", "✅\n")).await; let msgs_json = msgs_to_json(msgs, &r_data, max_age_secs).await; // Adding new posts - p_text = update_progress(ctx, progress.clone(), p_text.clone(), "✅\nAdding new posts...".to_string()).await; + p_text = update_progress(ctx, progress.clone(), p_text.clone(), lang!("dc_msg_update_add", "✅\n")).await; let weekly_art = r_data[CFG_DATA_RE].as_object().unwrap(); - add_posts(http, c_id, weekly_art, &msgs_json, max_age_secs).await; + add_posts(ctx, weekly_art, &msgs_json, max_age_secs).await; // Stop if only_add if only_add.unwrap_or(false) { - send_msg(ctx, "`/bk_week_update`\n## Done!".to_string(), true, true).await; - update_progress(ctx, progress.clone(), p_text, "✅\n## Done!".to_string()).await; + send_msg(ctx, lang!("dc_msg_update_done", "`/bk_week_update`\n## "), true, true).await; + update_progress(ctx, progress.clone(), p_text, lang!("dc_msg_update_done", "✅\n## ")).await; return Ok(()); } // Editing updated posts - p_text = update_progress(ctx, progress.clone(), p_text.clone(), "✅\nEditing updated posts...".to_string()).await; + p_text = update_progress(ctx, progress.clone(), p_text.clone(), lang!("dc_msg_update_editing", "✅\n")).await; edit_posts(http, c_id, weekly_art, &msgs_json).await; // Removing removed posts - p_text = update_progress(ctx, progress.clone(), p_text.clone(), "✅\nRemoving removed posts...".to_string()).await; + p_text = update_progress(ctx, progress.clone(), p_text.clone(), lang!("dc_msg_update_removing", "✅\n")).await; remove_posts(http, c_id, weekly_art, &msgs_json).await; // Removing old posts if max_age_u > 0 { - p_text = update_progress(ctx, progress.clone(), p_text.clone(), format!("✅\nRemoving old posts (threshold: {}d)...", max_age_u)).await; + p_text = update_progress(ctx, progress.clone(), p_text.clone(), lang!("dc_msg_update_removing_old", "✅\n", max_age_u)).await; remove_old(http, c_id, &msgs_json).await; send_cmd_json("remove_old_posts", Some(json!([max_age_secs])), true).await; } // Removing duplicate posts - p_text = update_progress(ctx, progress.clone(), p_text.clone(), "✅\nRemoving duplicate posts...".to_string()).await; + p_text = update_progress(ctx, progress.clone(), p_text.clone(), lang!("dc_msg_update_removing_dupe", "✅\n")).await; remove_dupes(http, c_id, &msgs_json).await; // Done - update_progress(ctx, progress.clone(), p_text, "✅\n## Done!".to_string()).await; - send_msg(ctx, "`/bk_week_update`\n## Done!".to_string(), true, true).await; + update_progress(ctx, progress.clone(), p_text, lang!("dc_msg_update_done", "✅\n## ")).await; + send_msg(ctx, lang!("dc_msg_update_done", "`/bk_week_update`\n## "), true, true).await; return Ok(()); } @@ -178,14 +178,15 @@ async fn msgs_to_json(msgs: Vec, reddit_data: &Value, max_age: u64) -> let msg_json = embed_to_json(&msg.embeds[0]); if msg_json.is_err() { continue; } - let mut u_json: Value = msg_json.unwrap(); + let u_json: Value = msg_json.unwrap(); let re_url = &reddit_data[CFG_DATA_RE][&url]; let post_date = re_url["post_data"]["date_unix"].as_u64().unwrap_or(0); // old - if now - post_date > max_age { + if now - post_date > max_age && max_age > 0 { if let Some(obj) = msgs_json["old"].as_object_mut() { + rs_println!("old: {}", url); obj.insert(url.clone(), json!(msg.id.get())); continue; } @@ -209,13 +210,8 @@ async fn msgs_to_json(msgs: Vec, reddit_data: &Value, max_age: u64) -> } // updated - if u_json["added"] != re_url["added"] - || u_json["approved"] != re_url["approved"] - || u_json["post_data"]["upvotes"] != re_url["post_data"]["upvotes"] - || u_json["votes"]["mod_voters"] != re_url["votes"]["mod_voters"] + if &u_json != re_url { - u_json.as_object_mut().unwrap().insert("msg_id".to_string(), Value::String(msg.id.clone().to_string())); - if let Some(obj) = msgs_json["updated"].as_object_mut() { obj.insert(url.clone(), json!(msg.id.get())); continue; @@ -232,7 +228,7 @@ async fn msgs_to_json(msgs: Vec, reddit_data: &Value, max_age: u64) -> } -async fn add_posts(http: &Http, c_id: ChannelId, r_data: &Map, msgs_json: &Value, max_age: u64) { +async fn add_posts(ctx: Context<'_>, r_data: &Map, msgs_json: &Value, max_age: u64) { let now = SystemTime::now() .duration_since(UNIX_EPOCH) .expect("Time went backwards") @@ -245,14 +241,14 @@ async fn add_posts(http: &Http, c_id: ChannelId, r_data: &Map, ms { continue; } let post_date = r_data[url]["post_data"]["date_unix"].as_u64().unwrap(); - if now - post_date > max_age { continue; } + if now - post_date > max_age && max_age > 0 { continue; } - if r_data[url].get("removed").is_some() { - http_send_embed(http, c_id, make_removed_embed(&r_data[url], url, false)).await; + if r_data[url]["removed"]["removed"].as_bool().unwrap() { + send_embed(ctx, make_removed_embed(&r_data[url], url, false), false).await; continue; } - http_send_embed(http, c_id, make_post_embed(&r_data[url], url, false)).await; + send_embed(ctx, make_post_embed(&r_data[url], url, false), false).await; } } From bea192849d7cb915c64d56eeaf54c7ef77a4af5b Mon Sep 17 00:00:00 2001 From: ByteDice Date: Sun, 8 Jun 2025 22:12:46 +0200 Subject: [PATCH 17/39] made remove button work --- README.md | 4 ++++ data/lang/en.json | 2 +- src/events.rs | 49 +++++++++++++++++++++++++++++++++----- src/messages.rs | 30 +++++++++++++++-------- src/python/data.py | 2 +- src/re_cmds/generic_fns.rs | 4 +++- src/re_cmds/update.rs | 17 ++++++------- 7 files changed, 81 insertions(+), 27 deletions(-) diff --git a/README.md b/README.md index da3801d..430a66f 100644 --- a/README.md +++ b/README.md @@ -7,6 +7,10 @@ An automation tool primarily made for myself (Byte Dice) but publicly available > This tool is not intended for public use outside of the official *Byte Dice Assistant* bots. Expect issues if you host this yourself.\ > This tool is only designed to run on Windows (10 and 11) and XUbuntu (24.04 and above) and may not work on any other OS. +> [!NOTE] +> This bot never modifies any Reddit content. All it does and will ever do is read/scrape. +> The data will never be sold and will only be shared with permission from subreddit moderators. + ## Open-source - Copyright **ByteDiceAssistant © 2025 by Byte Dice is licensed under CC BY-NC-SA 4.0.**\ diff --git a/data/lang/en.json b/data/lang/en.json index 1a779b9..eca80b3 100644 --- a/data/lang/en.json +++ b/data/lang/en.json @@ -32,7 +32,7 @@ "dc_msg_re_post_approve_success": "Successfully approved the post!", "dc_msg_re_post_disapprove_success": "Successfully disapproved the post!", "dc_msg_re_post_remove_success": "Successfully removed post with URL \"<{0}>\"!", - "dc_msg_re_post_unremove_success": "Successfully un-removed post with URL \"<{0}>\"!", + "dc_msg_re_post_unremove_success": "Successfully restored post with URL \"<{0}>\"!", "dc_msg_re_post_update_success": "Updated post with URL \"<{0}>\"!", "dc_msg_re_posts_channel_404": "Could not find `re_posts_channel` in data!\nHint: Run `/admin_re_bindchannel` in a (preferably read-only) channel (requires administrator permission).", "dc_msg_re_vote_err": "Failed to [vote / un-vote]: Unknown internal error.", diff --git a/src/events.rs b/src/events.rs index d58259e..0bcda76 100644 --- a/src/events.rs +++ b/src/events.rs @@ -1,10 +1,11 @@ use crate::data::{get_mutex_data, update_re_data}; -use crate::messages::make_post_embed; +use crate::messages::{make_post_embed, make_removed_embed, EmbedOptions}; use crate::re_cmds::generic_fns::{is_bk_mod_serenity, serenity_edit_msg_embed, serenity_send_msg}; -use crate::{lang, rs_println, websocket, Data, Error, CFG_DATA_RE}; +use crate::websocket::send_cmd_json; +use crate::{lang, rs_println, Data, Error, CFG_DATA_RE}; use poise::serenity_prelude::{self as serenity, ActivityData, ComponentInteraction, Interaction, Member, Ready}; -use serde_json::json; +use serde_json::{json, Value}; use std::future::Future; use std::pin::Pin; @@ -54,9 +55,9 @@ async fn handle_buttons(ctx: &serenity::Context, data: &Data, interaction: &Inte return match component.data.custom_id.as_str() { "approve_btn" => approve_btn(ctx, data, &component.member.as_ref().unwrap(), component, url, true).await, - "remove_btn" => Ok(()), + "remove_btn" => remove_btn(ctx, data, &component.member.as_ref().unwrap(), component, url, true).await, "unapprove_btn" => approve_btn(ctx, data, &component.member.as_ref().unwrap(), component, url, false).await, - "unremove_btn" => Ok(()), + "unremove_btn" => remove_btn(ctx, data, &component.member.as_ref().unwrap(), component, url, false).await, "unvote_btn" => Ok(()), "vote_btn" => Ok(()), _ => Err("Message button with that ID isn't handled.".into()) @@ -67,7 +68,7 @@ async fn handle_buttons(ctx: &serenity::Context, data: &Data, interaction: &Inte async fn approve_btn(ctx: &serenity::Context, data: &Data, c_member: &Member, component: &ComponentInteraction, url: String, approve: bool) -> Result<(), Error> { if !is_bk_mod_serenity(ctx, data, c_member, component).await { return Ok(()); } - let r = websocket::send_cmd_json("set_approve_post", Some(json!([approve, url])), true).await.unwrap(); + let r = send_cmd_json("set_approve_post", Some(json!([approve, url])), true).await.unwrap(); let c_id = component.channel_id; let m_id = component.message.id; @@ -87,5 +88,41 @@ async fn approve_btn(ctx: &serenity::Context, data: &Data, c_member: &Member, co } } + return Ok(()); +} + + +async fn remove_btn(ctx: &serenity::Context, data: &Data, c_member: &Member, component: &ComponentInteraction, url: String, remove: bool) -> Result<(), Error> { + if !is_bk_mod_serenity(ctx, data, c_member, component).await { return Ok(()); } + + let r: Value; + + if remove { + r = send_cmd_json("remove_post_url", Some(json!([&url, &c_member.user.name, None::])), true).await.unwrap(); + } + else { + r = send_cmd_json("add_post_url", Some(json!([&url, false, true])), true).await.unwrap(); + } + + let c_id = component.channel_id; + let m_id = component.message.id; + + if r["value"].as_bool().unwrap() { + update_re_data(data).await; + let new_data = &get_mutex_data(&data.reddit_data).await.unwrap()[CFG_DATA_RE][&url]; + let e: EmbedOptions; + if remove { e = make_removed_embed(new_data, &url, true); } + else { e = make_post_embed (new_data, &url, true); } + + if remove { + serenity_edit_msg_embed(ctx, &c_id, &m_id, e).await; + serenity_send_msg(ctx, component, lang!("dc_msg_re_post_remove_success", &url), true).await; + } + else { + serenity_edit_msg_embed(ctx, &c_id, &m_id, e).await; + serenity_send_msg(ctx, component, lang!("dc_msg_re_post_unremove_success", &url), true).await; + } + } + return Ok(()); } \ No newline at end of file diff --git a/src/messages.rs b/src/messages.rs index b058933..6c514c3 100644 --- a/src/messages.rs +++ b/src/messages.rs @@ -231,13 +231,7 @@ pub fn make_post_embed(post_data: &Value, url: &str, ephemeral: bool) -> EmbedOp let media_urls = post_data["post_data"]["media_urls"].as_array().unwrap(); - let action_row = CreateActionRow::Buttons(vec![ - CreateButton::new("vote_btn") .label(lang!("dc_btn_vote")) .emoji(ReactionType::Unicode("⬆️".to_string())), - CreateButton::new("unvote_btn") .label(lang!("dc_btn_unvote")), - CreateButton::new("approve_btn") .label(lang!("dc_btn_approve")) .emoji(ReactionType::Unicode("✅".to_string())), - CreateButton::new("unapprove_btn").label(lang!("dc_btn_unapprove")) .emoji(ReactionType::Unicode("❌".to_string())), - CreateButton::new("remove_btn") .label(lang!("dc_btn_remove")) .emoji(ReactionType::Unicode("🗑️".to_string())) - ]); + let action_row = make_post_components(); return EmbedOptions { title: Some(post_data["post_data"]["title"].as_str().unwrap().to_string()), @@ -256,9 +250,7 @@ pub fn make_post_embed(post_data: &Value, url: &str, ephemeral: bool) -> EmbedOp pub fn make_removed_embed(post_data: &Value, url: &str, ephemeral: bool) -> EmbedOptions { - let action_row = CreateActionRow::Buttons(vec![ - CreateButton::new("unremove_btn").label(lang!("dc_btn_unremove")).emoji(ReactionType::Unicode("↩️".to_string())) - ]); + let action_row = make_removed_components(); let none = lang!("none"); @@ -280,4 +272,22 @@ pub fn make_removed_embed(post_data: &Value, url: &str, ephemeral: bool) -> Embe actionrows: Some(vec![action_row]), ..Default::default() }; +} + + +fn make_post_components() -> CreateActionRow { + return CreateActionRow::Buttons(vec![ + CreateButton::new("vote_btn") .label(lang!("dc_btn_vote")) .emoji(ReactionType::Unicode("⬆️".to_string())), + CreateButton::new("unvote_btn") .label(lang!("dc_btn_unvote")), + CreateButton::new("approve_btn") .label(lang!("dc_btn_approve")) .emoji(ReactionType::Unicode("✅".to_string())), + CreateButton::new("unapprove_btn").label(lang!("dc_btn_unapprove")) .emoji(ReactionType::Unicode("❌".to_string())), + CreateButton::new("remove_btn") .label(lang!("dc_btn_remove")) .emoji(ReactionType::Unicode("🗑️".to_string())) + ]); +} + + +fn make_removed_components() -> CreateActionRow { + return CreateActionRow::Buttons(vec![ + CreateButton::new("unremove_btn").label(lang!("dc_btn_unremove")).emoji(ReactionType::Unicode("↩️".to_string())) + ]); } \ No newline at end of file diff --git a/src/python/data.py b/src/python/data.py index bfcc516..e783bc6 100644 --- a/src/python/data.py +++ b/src/python/data.py @@ -190,7 +190,7 @@ def remove_old_posts(bot: botPy.Bot, max_age: int) -> bool: remove.append(url) for key in remove: - weekly.pop(key) + weekly[key]["removed"]["removed"] = True return True diff --git a/src/re_cmds/generic_fns.rs b/src/re_cmds/generic_fns.rs index df38e0c..be65d58 100644 --- a/src/re_cmds/generic_fns.rs +++ b/src/re_cmds/generic_fns.rs @@ -34,7 +34,9 @@ pub async fn serenity_send_msg(ctx: &serenity::Context, component: &ComponentInt pub async fn serenity_edit_msg_embed(ctx: &serenity::Context, c_id: &ChannelId, m_id: &MessageId, e: EmbedOptions) { - let r = EditMessage::new().embed(embed_from_options(e)); + let r = EditMessage::new() + .embed(embed_from_options(e.clone())) + .components(e.actionrows.unwrap()); let _ = c_id.edit_message(ctx.http.clone(), m_id, r).await; } diff --git a/src/re_cmds/update.rs b/src/re_cmds/update.rs index 0093867..9aeaef1 100644 --- a/src/re_cmds/update.rs +++ b/src/re_cmds/update.rs @@ -55,6 +55,7 @@ pub async fn cmd( // Parsing messages to JSON p_text = update_progress(ctx, progress.clone(), p_text.clone(), lang!("dc_msg_update_parse", "✅\n")).await; let msgs_json = msgs_to_json(msgs, &r_data, max_age_secs).await; + if ctx.data().args.dev { rs_println!("Posts changelog: {}", msgs_json); } // Adding new posts p_text = update_progress(ctx, progress.clone(), p_text.clone(), lang!("dc_msg_update_add", "✅\n")).await; @@ -68,9 +69,9 @@ pub async fn cmd( return Ok(()); } - // Editing updated posts - p_text = update_progress(ctx, progress.clone(), p_text.clone(), lang!("dc_msg_update_editing", "✅\n")).await; - edit_posts(http, c_id, weekly_art, &msgs_json).await; + // Removing duplicate posts + p_text = update_progress(ctx, progress.clone(), p_text.clone(), lang!("dc_msg_update_removing_dupe", "✅\n")).await; + remove_dupes(http, c_id, &msgs_json).await; // Removing removed posts p_text = update_progress(ctx, progress.clone(), p_text.clone(), lang!("dc_msg_update_removing", "✅\n")).await; @@ -83,9 +84,9 @@ pub async fn cmd( send_cmd_json("remove_old_posts", Some(json!([max_age_secs])), true).await; } - // Removing duplicate posts - p_text = update_progress(ctx, progress.clone(), p_text.clone(), lang!("dc_msg_update_removing_dupe", "✅\n")).await; - remove_dupes(http, c_id, &msgs_json).await; + // Editing updated posts + p_text = update_progress(ctx, progress.clone(), p_text.clone(), lang!("dc_msg_update_editing", "✅\n")).await; + edit_posts(http, c_id, weekly_art, &msgs_json).await; // Done update_progress(ctx, progress.clone(), p_text, lang!("dc_msg_update_done", "✅\n## ")).await; @@ -193,8 +194,8 @@ async fn msgs_to_json(msgs: Vec, reddit_data: &Value, max_age: u64) -> } // removed - if re_url.get("removed").is_some() { - if u_json.get("removed").is_some() { + if re_url["removed"]["removed"].as_bool().unwrap() { + if u_json["removed"]["removed"].as_bool().unwrap() { // no change if let Some(obj) = msgs_json["no_change"].as_object_mut() { obj.insert(url.clone(), json!(msg.id.get())); From 20d906c3ba1b364a708f235b983c353019ec7a3c Mon Sep 17 00:00:00 2001 From: ByteDice Date: Sun, 8 Jun 2025 22:25:46 +0200 Subject: [PATCH 18/39] made all buttons work! Now just QA and bugfixing before merging to main and publishing! --- src/events.rs | 50 +++++++++++++++++++++++++++++++++++++--------- src/python/data.py | 10 +++++----- 2 files changed, 46 insertions(+), 14 deletions(-) diff --git a/src/events.rs b/src/events.rs index 0bcda76..b4e48df 100644 --- a/src/events.rs +++ b/src/events.rs @@ -1,6 +1,6 @@ use crate::data::{get_mutex_data, update_re_data}; use crate::messages::{make_post_embed, make_removed_embed, EmbedOptions}; -use crate::re_cmds::generic_fns::{is_bk_mod_serenity, serenity_edit_msg_embed, serenity_send_msg}; +use crate::re_cmds::generic_fns::{is_bk_mod, is_bk_mod_serenity, serenity_edit_msg_embed, serenity_send_msg}; use crate::websocket::send_cmd_json; use crate::{lang, rs_println, Data, Error, CFG_DATA_RE}; @@ -55,11 +55,11 @@ async fn handle_buttons(ctx: &serenity::Context, data: &Data, interaction: &Inte return match component.data.custom_id.as_str() { "approve_btn" => approve_btn(ctx, data, &component.member.as_ref().unwrap(), component, url, true).await, - "remove_btn" => remove_btn(ctx, data, &component.member.as_ref().unwrap(), component, url, true).await, + "remove_btn" => remove_btn (ctx, data, &component.member.as_ref().unwrap(), component, url, true).await, "unapprove_btn" => approve_btn(ctx, data, &component.member.as_ref().unwrap(), component, url, false).await, - "unremove_btn" => remove_btn(ctx, data, &component.member.as_ref().unwrap(), component, url, false).await, - "unvote_btn" => Ok(()), - "vote_btn" => Ok(()), + "unremove_btn" => remove_btn (ctx, data, &component.member.as_ref().unwrap(), component, url, false).await, + "unvote_btn" => vote_btn (ctx, data, &component.member.as_ref().unwrap(), component, url, false).await, + "vote_btn" => vote_btn (ctx, data, &component.member.as_ref().unwrap(), component, url, true).await, _ => Err("Message button with that ID isn't handled.".into()) } } @@ -78,12 +78,12 @@ async fn approve_btn(ctx: &serenity::Context, data: &Data, c_member: &Member, co let new_data = &get_mutex_data(&data.reddit_data).await.unwrap()[CFG_DATA_RE][&url]; let e = make_post_embed(new_data, &url, true); + serenity_edit_msg_embed(ctx, &c_id, &m_id, e).await; + if approve { - serenity_edit_msg_embed(ctx, &c_id, &m_id, e).await; serenity_send_msg(ctx, component, lang!("dc_msg_re_post_approve_success"), true).await; } else { - serenity_edit_msg_embed(ctx, &c_id, &m_id, e).await; serenity_send_msg(ctx, component, lang!("dc_msg_re_post_disapprove_success"), true).await; } } @@ -114,15 +114,47 @@ async fn remove_btn(ctx: &serenity::Context, data: &Data, c_member: &Member, com if remove { e = make_removed_embed(new_data, &url, true); } else { e = make_post_embed (new_data, &url, true); } + serenity_edit_msg_embed(ctx, &c_id, &m_id, e).await; + if remove { - serenity_edit_msg_embed(ctx, &c_id, &m_id, e).await; serenity_send_msg(ctx, component, lang!("dc_msg_re_post_remove_success", &url), true).await; } else { - serenity_edit_msg_embed(ctx, &c_id, &m_id, e).await; serenity_send_msg(ctx, component, lang!("dc_msg_re_post_unremove_success", &url), true).await; } } + return Ok(()); +} + + +async fn vote_btn(ctx: &serenity::Context, data: &Data, c_member: &Member, component: &ComponentInteraction, url: String, vote: bool) -> Result<(), Error> { + let uid: u64 = c_member.user.id.into(); + let is_mod = is_bk_mod(data.bk_mods.clone(), uid); + + let r = send_cmd_json("set_vote_post", Some(json!([&url, uid, is_mod, true, !vote])), true).await.unwrap(); + + let c_id = component.channel_id; + let m_id = component.message.id; + + if r["value"].as_bool().unwrap() { + update_re_data(data).await; + let new_data = &get_mutex_data(&data.reddit_data).await.unwrap()[CFG_DATA_RE][&url]; + let e = make_post_embed(new_data, &url, true); + + serenity_edit_msg_embed(ctx, &c_id, &m_id, e).await; + + if vote { + if is_mod { serenity_send_msg(ctx, component, lang!("dc_msg_re_vote_mod_success"), true).await; } + else { serenity_send_msg(ctx, component, lang!("dc_msg_re_vote_success"), true).await; } + } + else { + serenity_send_msg(ctx, component, lang!("dc_msg_re_vote_remove_success"), true).await; + } + } + else { + if !vote { serenity_send_msg(ctx, component, lang!("dc_msg_re_vote_remove_havent"), true).await; } + } + return Ok(()); } \ No newline at end of file diff --git a/src/python/data.py b/src/python/data.py index e783bc6..74b5c12 100644 --- a/src/python/data.py +++ b/src/python/data.py @@ -207,8 +207,8 @@ def set_vote_post( return False votes = bot.data[botPy.RE_DATA_POSTS][url]["votes"] - re_voters: set[str] = set(votes["voters_re"]) - dc_voters: set[int] = set(votes["voters_dc"]) + re_voters: set[str] = set(votes["voters_re"]) + dc_voters: set[int] = set(votes["voters_dc"]) mod_voters: set[int] = set(votes["mod_voters"]) target_voters = mod_voters if mod_vote else (dc_voters if from_dc else re_voters) @@ -222,9 +222,9 @@ def set_vote_post( if user in target_voters: return False target_voters.add(user) - - bot.data[botPy.RE_DATA_POSTS][url]["votes"]["voters_re"] = list(re_voters) - bot.data[botPy.RE_DATA_POSTS][url]["votes"]["voters_dc"] = list(dc_voters) + + bot.data[botPy.RE_DATA_POSTS][url]["votes"]["voters_re"] = list(re_voters) + bot.data[botPy.RE_DATA_POSTS][url]["votes"]["voters_dc"] = list(dc_voters) bot.data[botPy.RE_DATA_POSTS][url]["votes"]["mod_voters"] = list(mod_voters) return True From e4bb754ef0c9c8a0b63d37f97554f6ab45a802d6 Mon Sep 17 00:00:00 2001 From: ByteDice Date: Tue, 10 Jun 2025 23:02:46 +0200 Subject: [PATCH 19/39] i decided i wanted to compress the embed JSON... and fix some bugs :3 --- Cargo.toml | 2 ++ src/messages.rs | 42 ++++++++++++++++++++++++++++++++++---- src/re_cmds/add.rs | 2 +- src/re_cmds/approve.rs | 4 ++-- src/re_cmds/generic_fns.rs | 7 +++---- src/re_cmds/remove.rs | 2 +- src/re_cmds/top.rs | 2 +- src/re_cmds/update.rs | 10 +++++---- src/re_cmds/vote.rs | 2 +- 9 files changed, 55 insertions(+), 18 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 933bcba..cd577ac 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -6,7 +6,9 @@ edition = "2021" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies] +base64 = "0.22.1" clap = { version = "4.5.28", features = ["derive"] } +flate2 = "1.1.2" formatx = "0.2.3" futures = "0.3.31" poise = "0.6.1" diff --git a/src/messages.rs b/src/messages.rs index 6c514c3..763fd88 100644 --- a/src/messages.rs +++ b/src/messages.rs @@ -1,7 +1,13 @@ use std::env; +use std::io::{Read, Write}; use crate::{lang, Args, Context}; +use base64::engine::general_purpose; +use base64::Engine; +use flate2::read::ZlibDecoder; +use flate2::write::ZlibEncoder; +use flate2::Compression; use poise::serenity_prelude::json::Value; use poise::{serenity_prelude::CreateMessage, CreateReply, ReplyHandle}; use poise::serenity_prelude::{ChannelId, Color, CreateActionRow, CreateButton, CreateEmbed, CreateEmbedAuthor, EditMessage, Http, Message, ReactionType, Timestamp, UserId}; @@ -47,7 +53,7 @@ impl Default for EmbedOptions { static DEFAULT_DC_COL: u32 = 5793266; static REMOVED_DC_COL: u32 = 16716032; -pub static JSON_TEXT_START: &str = "-# JSON: ||`"; +pub static JSON_TEXT_START: &str = "-# Data: ||`"; pub static JSON_TEXT_END: &str = "`||"; @@ -230,12 +236,13 @@ pub fn make_post_embed(post_data: &Value, url: &str, ephemeral: bool) -> EmbedOp .join("\n"); let media_urls = post_data["post_data"]["media_urls"].as_array().unwrap(); - let action_row = make_post_components(); + let json_encoded = trim_compress_and_encode_json(post_data); + return EmbedOptions { title: Some(post_data["post_data"]["title"].as_str().unwrap().to_string()), - desc: format!("{}\n\n{}{}{}", trimmed, JSON_TEXT_START, serde_json::to_string(&post_data).unwrap(), JSON_TEXT_END), + desc: format!("{}\n\n{}{}{}", trimmed, JSON_TEXT_START, json_encoded, JSON_TEXT_END), col: Some(DEFAULT_DC_COL), url: Some(url.to_string()), ts: Some(Timestamp::from_unix_timestamp(post_data["post_data"]["date_unix"].as_i64().unwrap()).unwrap()), @@ -262,9 +269,11 @@ pub fn make_removed_embed(post_data: &Value, url: &str, ephemeral: bool) -> Embe url ); + let json_encoded = trim_compress_and_encode_json(post_data); + return EmbedOptions { title: Some(lang!("dc_msg_removed_square_brackets", post_data["post_data"]["title"].clone())), - desc: format!("{}\n\n{}{}{}", desc, JSON_TEXT_START, serde_json::to_string(&post_data).unwrap(), JSON_TEXT_END), + desc: format!("{}\n\n{}{}{}", desc, JSON_TEXT_START, json_encoded, JSON_TEXT_END), col: Some(REMOVED_DC_COL), url: Some(url.to_string()), ts: Some(Timestamp::from_unix_timestamp(post_data["post_data"]["date_unix"].as_i64().unwrap()).unwrap()), @@ -275,6 +284,31 @@ pub fn make_removed_embed(post_data: &Value, url: &str, ephemeral: bool) -> Embe } +pub fn trim_post_json(j: &Value) -> Value { + let mut json_trimmed = j.clone(); + json_trimmed["post_data"].as_object_mut().unwrap().remove("media_urls"); + return json_trimmed; +} + + +pub fn trim_compress_and_encode_json(j: &Value) -> String { + let trim = trim_post_json(j); + let mut encoder = ZlibEncoder::new(Vec::new(), Compression::default()); + encoder.write_all(serde_json::to_string(&trim).unwrap().as_bytes()).unwrap(); + let compressed = encoder.finish().unwrap(); + return general_purpose::STANDARD.encode(&compressed); +} + + +pub fn decode_and_decompress_json(t: String) -> Result { + let compressed = general_purpose::STANDARD.decode(t).unwrap(); + let mut decoder = ZlibDecoder::new(&compressed[..]); + let mut decompressed = String::new(); + decoder.read_to_string(&mut decompressed).unwrap(); + return serde_json::from_str(&decompressed); +} + + fn make_post_components() -> CreateActionRow { return CreateActionRow::Buttons(vec![ CreateButton::new("vote_btn") .label(lang!("dc_btn_vote")) .emoji(ReactionType::Unicode("⬆️".to_string())), diff --git a/src/re_cmds/add.rs b/src/re_cmds/add.rs index 6418a65..ebe2cc3 100644 --- a/src/re_cmds/add.rs +++ b/src/re_cmds/add.rs @@ -21,7 +21,7 @@ pub async fn cmd( #[description = "Wether to approve it after adding it"] approve: Option ) -> Result<(), Error> { - if is_bk_mod_msg(ctx).await { return Ok(()); } + if !is_bk_mod_msg(ctx).await { return Ok(()); } let shorturl_u = to_shorturl(&url); let shorturl = &shorturl_u.unwrap_or(url.clone()); diff --git a/src/re_cmds/approve.rs b/src/re_cmds/approve.rs index a023983..1e52fba 100644 --- a/src/re_cmds/approve.rs +++ b/src/re_cmds/approve.rs @@ -18,7 +18,7 @@ pub async fn cmd( #[description = "Wether to approve or disapprove the post"] disapprove: Option ) -> Result<(), Error> { - if is_bk_mod_msg(ctx).await { return Ok(()); } + if !is_bk_mod_msg(ctx).await { return Ok(()); } data::update_re_data(ctx.data()).await; let reddit_data = get_mutex_data(&ctx.data().reddit_data).await?; @@ -31,7 +31,7 @@ pub async fn cmd( async fn approve_cmd(ctx: Context<'_>, url: &str, reddit_data: &Value, approve: bool) { if let Some(post) = reddit_data.get(CFG_DATA_RE).unwrap().get(url) { - if post.get("removed").is_some() { + if post["removed"]["removed"].as_bool().unwrap() { send_embed_for_removed(ctx, url, post).await; return; } diff --git a/src/re_cmds/generic_fns.rs b/src/re_cmds/generic_fns.rs index be65d58..a67d810 100644 --- a/src/re_cmds/generic_fns.rs +++ b/src/re_cmds/generic_fns.rs @@ -2,7 +2,7 @@ use poise::serenity_prelude::{self as serenity, ChannelId, ComponentInteraction, use regex::Regex; use serde_json::Value; -use crate::{data::get_toml_mutex, lang, messages::{embed_from_options, make_post_embed, make_removed_embed, send_embed, send_msg, EmbedOptions, JSON_TEXT_END, JSON_TEXT_START}, Context, Data, Error}; +use crate::{data::get_toml_mutex, lang, messages::{decode_and_decompress_json, embed_from_options, make_post_embed, make_removed_embed, send_embed, send_msg, EmbedOptions, JSON_TEXT_END, JSON_TEXT_START}, Context, Data, Error}; pub fn is_bk_mod(mod_list: Vec, uid: u64) -> bool { return mod_list.contains(&uid); @@ -14,7 +14,7 @@ pub async fn is_bk_mod_msg(ctx: Context<'_>) -> bool { let sr = get_readable_subreddits(ctx.data()).await.unwrap(); send_msg(ctx, lang!("dc_msg_re_permdeny_not_re_mod", sr), false, false).await; - return true + return false } @@ -85,6 +85,5 @@ pub fn embed_to_json(embed: &Embed) -> Result { let msg_last_len = msg_lines.clone().last().unwrap().len(); let msg_json_str = &msg_lines.clone().last().unwrap()[JSON_TEXT_START.len()..msg_last_len - JSON_TEXT_END.len()]; - let msg_json: Result = serde_json::from_str(msg_json_str); - return msg_json; + return decode_and_decompress_json(msg_json_str.to_string()); } \ No newline at end of file diff --git a/src/re_cmds/remove.rs b/src/re_cmds/remove.rs index c34cf69..d1819f4 100644 --- a/src/re_cmds/remove.rs +++ b/src/re_cmds/remove.rs @@ -16,7 +16,7 @@ pub async fn cmd( #[description = "The reason of the removal."] reason: Option ) -> Result<(), Error> { - if is_bk_mod_msg(ctx).await { return Ok(()); } + if !is_bk_mod_msg(ctx).await { return Ok(()); } let auth = &ctx.author().name; let r = send_cmd_json("remove_post_url", Some(json!([&url, &auth, &reason])), true).await.unwrap(); diff --git a/src/re_cmds/top.rs b/src/re_cmds/top.rs index 5f4873e..4a5c5e5 100644 --- a/src/re_cmds/top.rs +++ b/src/re_cmds/top.rs @@ -34,7 +34,7 @@ pub async fn cmd( let posts_u = posts.as_object().unwrap(); for (url, dat) in posts_u { - if dat.get("removed").is_some() { continue; } + if dat["removed"]["removed"].as_bool().unwrap() { continue; } let val: i32 = match category { TopCategory::Upvotes => dat["post_data"]["upvotes"].as_i64().unwrap() as i32, diff --git a/src/re_cmds/update.rs b/src/re_cmds/update.rs index 9aeaef1..3b5750b 100644 --- a/src/re_cmds/update.rs +++ b/src/re_cmds/update.rs @@ -3,7 +3,7 @@ use std::time::{SystemTime, UNIX_EPOCH}; use poise::{serenity_prelude::{ChannelId, EditMessage, GetMessages, Http, Message, MessageId, UserId}, ReplyHandle}; use serde_json::{json, Map, Value}; -use crate::{data::{self, get_mutex_data, DC_POSTS_CHANNEL_KEY}, lang, messages::{edit_reply, embed_from_options, make_post_embed, make_removed_embed, send_embed, send_msg}, re_cmds::generic_fns::embed_to_json, rs_println, websocket::send_cmd_json, Context, Error, CFG_DATA_RE}; +use crate::{data::{self, get_mutex_data, DC_POSTS_CHANNEL_KEY}, lang, messages::{edit_reply, embed_from_options, make_post_embed, make_removed_embed, send_embed, send_msg, trim_post_json}, re_cmds::generic_fns::embed_to_json, rs_println, websocket::send_cmd_json, Context, Error, CFG_DATA_RE}; #[poise::command( slash_command, @@ -14,7 +14,7 @@ use crate::{data::{self, get_mutex_data, DC_POSTS_CHANNEL_KEY}, lang, messages:: guild_cooldown = 120, required_bot_permissions = "SEND_MESSAGES | VIEW_CHANNEL | READ_MESSAGE_HISTORY | EMBED_LINKS" )] -/// Updates the binded Discord channel with the bot's current Reddit data. +/// Updates the bound Discord channel with the bot's current Reddit data. pub async fn cmd( ctx: Context<'_>, #[description = "Only adds new posts, leaves everything else unchanged."] @@ -182,6 +182,8 @@ async fn msgs_to_json(msgs: Vec, reddit_data: &Value, max_age: u64) -> let u_json: Value = msg_json.unwrap(); let re_url = &reddit_data[CFG_DATA_RE][&url]; + let json_trimmed = trim_post_json(re_url); + let post_date = re_url["post_data"]["date_unix"].as_u64().unwrap_or(0); // old @@ -194,7 +196,7 @@ async fn msgs_to_json(msgs: Vec, reddit_data: &Value, max_age: u64) -> } // removed - if re_url["removed"]["removed"].as_bool().unwrap() { + if json_trimmed["removed"]["removed"].as_bool().unwrap() { if u_json["removed"]["removed"].as_bool().unwrap() { // no change if let Some(obj) = msgs_json["no_change"].as_object_mut() { @@ -211,7 +213,7 @@ async fn msgs_to_json(msgs: Vec, reddit_data: &Value, max_age: u64) -> } // updated - if &u_json != re_url + if u_json != json_trimmed { if let Some(obj) = msgs_json["updated"].as_object_mut() { obj.insert(url.clone(), json!(msg.id.get())); diff --git a/src/re_cmds/vote.rs b/src/re_cmds/vote.rs index 0e2e168..a027084 100644 --- a/src/re_cmds/vote.rs +++ b/src/re_cmds/vote.rs @@ -26,7 +26,7 @@ pub async fn cmd( send_msg(ctx, lang!("dc_msg_re_post_404"), false, false).await; return Ok(()); } - if post_data[&url].get("removed").is_some() { + if post_data[&url]["removed"]["removed"].as_bool().unwrap() { send_embed_for_removed(ctx, &url, &post_data[&url]).await; return Ok(()); } From e53b5ba67735d8a1f5edc53c8d6ecf19412aebfd Mon Sep 17 00:00:00 2001 From: ByteDice Date: Tue, 10 Jun 2025 23:08:59 +0200 Subject: [PATCH 20/39] .gitkeep to /data/db to include it in the repo (and avoid making a generator). --- data/db/.gitkeep | 2 ++ 1 file changed, 2 insertions(+) create mode 100644 data/db/.gitkeep diff --git a/data/db/.gitkeep b/data/db/.gitkeep new file mode 100644 index 0000000..69abd31 --- /dev/null +++ b/data/db/.gitkeep @@ -0,0 +1,2 @@ +This file is only here so git saves this folder. +The file is not important and may be removed unless you are contributing to the project. \ No newline at end of file From 2a951723b14e198dc1f010b8642ece2cb21be774 Mon Sep 17 00:00:00 2001 From: Byte Dice Date: Sat, 14 Jun 2025 14:09:46 +0200 Subject: [PATCH 21/39] hotfixes & QOL --- TODO.md | 1 - cfg/status.txt | 1 - data/defaults/cfg_default.toml | 4 +++ data/lang/en.json | 4 ++- src/events.rs | 62 +++++++++++++++++++--------------- src/main.rs | 4 ++- src/python.rs | 29 ++++++++++++---- src/python/bot.py | 9 +++-- src/python/cmds.py | 33 ++++++++---------- src/python/data.py | 12 +++++-- 10 files changed, 98 insertions(+), 61 deletions(-) delete mode 100644 cfg/status.txt diff --git a/TODO.md b/TODO.md index eda773d..ebc5214 100644 --- a/TODO.md +++ b/TODO.md @@ -1,7 +1,6 @@ ### High priority: - [ ] Reddit bot that scrapes images with tag "Original Art" and posts them in Discord server - [ ] handle dm_on_error cfg - - [ ] Add button event listeners - [ ] Allow updating the data autonomously and via manual commands. - [ ] Automatically approve posts that don't get caught by reverse image search (ris) - [ ] Make buttons do stuff diff --git a/cfg/status.txt b/cfg/status.txt deleted file mode 100644 index 4251794..0000000 --- a/cfg/status.txt +++ /dev/null @@ -1 +0,0 @@ -🎲 https://bytedice.net \ No newline at end of file diff --git a/data/defaults/cfg_default.toml b/data/defaults/cfg_default.toml index 0a6689e..d0e85a0 100644 --- a/data/defaults/cfg_default.toml +++ b/data/defaults/cfg_default.toml @@ -1,3 +1,7 @@ +[general] +# The discord bots status text +status = "🎲 https://bytedice.net" + [reddit] # Which subreddits the bot will scan when executing "re"-category commands. # Is automatically disabled when `disabled_categories` includes "re". diff --git a/data/lang/en.json b/data/lang/en.json index eca80b3..3304727 100644 --- a/data/lang/en.json +++ b/data/lang/en.json @@ -12,7 +12,7 @@ "dc_msg_corrupted_data": "Oopsies `(。>\\\\<)`. It looks like my data i-is \\**sob*\\*... c-corrupted!\n[From Byte Dice]: I have no idea what I was thinking while writing this at 2am. I'm not removing it.", "dc_msg_data_server_404": "This server is not in the data!\n Hint: Run the command `/add_server` inside of a Discord server (requires administrator permission).", "dc_msg_dm_python_err_socket": "Unknown internal Python error occurred: Websocket response error", - "dc_msg_dm_python_err": "Unknown internal Python Error: `{0}`", + "dc_msg_dm_python_err": "Unknown internal Python Error:\n```\n{0}\n```", "dc_msg_embed_default_embed_desc": "Default english embed description.", "dc_msg_embed_re_post": "Spoilers and vote length anonymizer for fair review!\n## Post Data:\n**Post upvotes:** ||`{0:>6}`||\n**Moderator votes:** ||`{1:>6}`||\n**Media type:** `{2}`\n**URL:** ||<{3}>||\n\n## Listing Data:\n**Added by:** `{{ human: {4}, bot: {5} }}`\n**Approved by:** `{{ human: {6}, bot: [not implemented] }}`", "dc_msg_embed_re_removed": "## Removed by `{0}`\n**Reason:** {1}\n**URL**: ||<{2}>||", @@ -29,11 +29,13 @@ "dc_msg_re_permdeny_not_re_mod": "Permission denied: You are not a moderator of the subreddit(s) {0}", "dc_msg_re_post_404": "Post URL \"<{0}>\" not found: Post doesn't exist in the data!\n Hint: Run the command `/re_addpost [URL]` in a Discord channel or `u/ByteDiceAssistant add_post` in a Reddit post.", "dc_msg_re_post_add_success": "Added post with URL \"<{0}>\"!", + "dc_msg_re_post_approve_remove": "Couldn't approve the post because it has been removed!", "dc_msg_re_post_approve_success": "Successfully approved the post!", "dc_msg_re_post_disapprove_success": "Successfully disapproved the post!", "dc_msg_re_post_remove_success": "Successfully removed post with URL \"<{0}>\"!", "dc_msg_re_post_unremove_success": "Successfully restored post with URL \"<{0}>\"!", "dc_msg_re_post_update_success": "Updated post with URL \"<{0}>\"!", + "dc_msg_re_post_vote_removed_post": "Couldn't [vote / un-vote] the post because it has been removed!", "dc_msg_re_posts_channel_404": "Could not find `re_posts_channel` in data!\nHint: Run `/admin_re_bindchannel` in a (preferably read-only) channel (requires administrator permission).", "dc_msg_re_vote_err": "Failed to [vote / un-vote]: Unknown internal error.", "dc_msg_re_vote_mod_success": "Successfully voted (as moderator vote)!", diff --git a/src/events.rs b/src/events.rs index b4e48df..590f5d3 100644 --- a/src/events.rs +++ b/src/events.rs @@ -1,10 +1,10 @@ -use crate::data::{get_mutex_data, update_re_data}; +use crate::data::{get_mutex_data, get_toml_mutex, update_re_data}; use crate::messages::{make_post_embed, make_removed_embed, EmbedOptions}; use crate::re_cmds::generic_fns::{is_bk_mod, is_bk_mod_serenity, serenity_edit_msg_embed, serenity_send_msg}; use crate::websocket::send_cmd_json; use crate::{lang, rs_println, Data, Error, CFG_DATA_RE}; -use poise::serenity_prelude::{self as serenity, ActivityData, ComponentInteraction, Interaction, Member, Ready}; +use poise::serenity_prelude::{self as serenity, ActivityData, ChannelId, ComponentInteraction, Interaction, Member, MessageId, Ready}; use serde_json::{json, Value}; use std::future::Future; @@ -18,7 +18,7 @@ pub fn event_handler<'a>( ) -> Pin> + Send + 'a>> { Box::pin(async move { match event { - serenity::FullEvent::Ready { data_about_bot } => on_ready(ctx, data_about_bot), + serenity::FullEvent::Ready { data_about_bot } => on_ready(ctx, data_about_bot, data).await, serenity::FullEvent::InteractionCreate { interaction } => { let _ = handle_buttons(ctx, data, interaction).await; }, _ => {} } @@ -27,15 +27,15 @@ pub fn event_handler<'a>( } -fn on_ready(ctx: &serenity::Context, data_about_bot: &Ready) { +async fn on_ready(ctx: &serenity::Context, data_about_bot: &Ready, data: &Data) { rs_println!( "Bot started as user \"{}\" with id {}", data_about_bot.user.name, data_about_bot.user.id ); - let file_text = std::fs::read_to_string("./cfg/status.txt").unwrap(); - let custom_activity = ActivityData::custom(file_text); + let m_data = get_toml_mutex(&data.cfg).await.unwrap(); + let custom_activity = ActivityData::custom(m_data["general"]["status"].as_str().unwrap()); ctx.online(); ctx.set_activity(Some(custom_activity)); @@ -65,6 +65,16 @@ async fn handle_buttons(ctx: &serenity::Context, data: &Data, interaction: &Inte } +async fn update_embed(ctx: &serenity::Context, url: &str, new_data: &Value, c_id: &ChannelId, m_id: &MessageId) { + let e: EmbedOptions; + let remove = new_data["removed"]["removed"].as_bool().unwrap(); + if remove { e = make_removed_embed(new_data, url, true); } + else { e = make_post_embed (new_data, url, true); } + + serenity_edit_msg_embed(ctx, &c_id, &m_id, e).await; +} + + async fn approve_btn(ctx: &serenity::Context, data: &Data, c_member: &Member, component: &ComponentInteraction, url: String, approve: bool) -> Result<(), Error> { if !is_bk_mod_serenity(ctx, data, c_member, component).await { return Ok(()); } @@ -73,13 +83,11 @@ async fn approve_btn(ctx: &serenity::Context, data: &Data, c_member: &Member, co let c_id = component.channel_id; let m_id = component.message.id; + update_re_data(data).await; + let new_data = &get_mutex_data(&data.reddit_data).await.unwrap()[CFG_DATA_RE][&url]; + update_embed(ctx, &url, new_data, &c_id, &m_id).await; + if r["value"].as_bool().unwrap() { - update_re_data(data).await; - let new_data = &get_mutex_data(&data.reddit_data).await.unwrap()[CFG_DATA_RE][&url]; - let e = make_post_embed(new_data, &url, true); - - serenity_edit_msg_embed(ctx, &c_id, &m_id, e).await; - if approve { serenity_send_msg(ctx, component, lang!("dc_msg_re_post_approve_success"), true).await; } @@ -87,6 +95,9 @@ async fn approve_btn(ctx: &serenity::Context, data: &Data, c_member: &Member, co serenity_send_msg(ctx, component, lang!("dc_msg_re_post_disapprove_success"), true).await; } } + else { + serenity_send_msg(ctx, component, lang!("dc_msg_re_post_approve_remove"), true).await; + } return Ok(()); } @@ -106,16 +117,12 @@ async fn remove_btn(ctx: &serenity::Context, data: &Data, c_member: &Member, com let c_id = component.channel_id; let m_id = component.message.id; + + update_re_data(data).await; + let new_data = &get_mutex_data(&data.reddit_data).await.unwrap()[CFG_DATA_RE][&url]; + update_embed(ctx, &url, new_data, &c_id, &m_id).await; if r["value"].as_bool().unwrap() { - update_re_data(data).await; - let new_data = &get_mutex_data(&data.reddit_data).await.unwrap()[CFG_DATA_RE][&url]; - let e: EmbedOptions; - if remove { e = make_removed_embed(new_data, &url, true); } - else { e = make_post_embed (new_data, &url, true); } - - serenity_edit_msg_embed(ctx, &c_id, &m_id, e).await; - if remove { serenity_send_msg(ctx, component, lang!("dc_msg_re_post_remove_success", &url), true).await; } @@ -137,13 +144,11 @@ async fn vote_btn(ctx: &serenity::Context, data: &Data, c_member: &Member, compo let c_id = component.channel_id; let m_id = component.message.id; + update_re_data(data).await; + let new_data = &get_mutex_data(&data.reddit_data).await.unwrap()[CFG_DATA_RE][&url]; + update_embed(ctx, &url, new_data, &c_id, &m_id).await; + if r["value"].as_bool().unwrap() { - update_re_data(data).await; - let new_data = &get_mutex_data(&data.reddit_data).await.unwrap()[CFG_DATA_RE][&url]; - let e = make_post_embed(new_data, &url, true); - - serenity_edit_msg_embed(ctx, &c_id, &m_id, e).await; - if vote { if is_mod { serenity_send_msg(ctx, component, lang!("dc_msg_re_vote_mod_success"), true).await; } else { serenity_send_msg(ctx, component, lang!("dc_msg_re_vote_success"), true).await; } @@ -153,7 +158,10 @@ async fn vote_btn(ctx: &serenity::Context, data: &Data, c_member: &Member, compo } } else { - if !vote { serenity_send_msg(ctx, component, lang!("dc_msg_re_vote_remove_havent"), true).await; } + if new_data["removed"]["removed"].as_bool().unwrap() { + serenity_send_msg(ctx, component, lang!("dc_msg_re_post_vote_removed_post"), true).await; + } + else if !vote { serenity_send_msg(ctx, component, lang!("dc_msg_re_vote_remove_havent"), true).await; } } return Ok(()); diff --git a/src/main.rs b/src/main.rs index 34c97f8..ccbe105 100644 --- a/src/main.rs +++ b/src/main.rs @@ -164,8 +164,10 @@ async fn main() { }); if !args.nosched { + let dur = if args.test { Duration::from_secs(60) } else { Duration::from_secs(60 * 10) }; + let schedules: Vec = vec![ - (Duration::from_secs(2 * 60), || Box::pin(read_reddit_inbox())) + (dur, || Box::pin(read_reddit_inbox())) ]; run_schedules(schedules).await; diff --git a/src/python.rs b/src/python.rs index fce7731..09b9a4f 100644 --- a/src/python.rs +++ b/src/python.rs @@ -23,19 +23,30 @@ pub async fn start(args: Args) -> PyResult<()> { let py_args = args_str.replace(":true", ":True").replace(":false", ":False"); let app_path = CString::new(format!("args = {}\n{}", py_args, code)).unwrap(); + let mut traceback: String = String::new(); + let mut is_error = false; + pyo3::prepare_freethreaded_python(); - let from_python = Python::with_gil(|py| -> PyResult> { + let _ = Python::with_gil(|py| -> Result<(), PyErr> { let syspath = py.import("sys")?.getattr("path")?.downcast_into::()?; syspath.insert(0, path)?; let empty = CString::new("").unwrap(); - let app: Py = PyModule::from_code(py, &app_path, &empty, &empty)?.into(); + let py_result = PyModule::from_code(py, &app_path, &empty, &empty); - return Ok(app); + if let Err(ref e) = py_result { + traceback = py.import("traceback")? + .call_method1("format_exception", (e.get_type(py), e.value(py), e.traceback(py)))? + .extract::>()? + .join(""); + is_error = true; + } + + return Ok(()); }); - if from_python.is_err() { + if is_error { let own_env = std::env::var("ASSISTANT_OWNERS").unwrap_or("0".to_string()); let own_vec_str: Vec = own_env.split(",").map(String::from).collect(); let own_vec_u64: Vec = own_vec_str @@ -43,9 +54,15 @@ pub async fn start(args: Args) -> PyResult<()> { .map(|s| s.parse::().expect("Failed to parse ASSISTANT_OWNERS. Invalid syntax.")) .collect(); - send_dm(lang!("dc_msg_dm_python_err", format!("{:?}", from_python)), args, own_vec_u64).await; - errln!("pyO3: {:?}", from_python); + send_dm( + lang!("dc_msg_dm_python_err", format!("{}", traceback)), + args, + own_vec_u64 + ).await; + + errln!("pyO3: {}", traceback); } + return Ok(()); } diff --git a/src/python/bot.py b/src/python/bot.py index 2c4c3c5..e939470 100644 --- a/src/python/bot.py +++ b/src/python/bot.py @@ -37,12 +37,13 @@ class Bot: password = self.password, user_agent = self.useragent ) + self.sr_list: list[str] = ["bytedicetesting"] self.sr = None self.data_f: TextIOWrapper = None self.data: dict = {} async def initialize(self): - self.sr = await self.r.subreddit("bytedicetesting") + self.sr = await self.r.subreddit("+".join(self.sr_list)) async def set_args(self, args: dict): self.args = args @@ -57,11 +58,13 @@ class Bot: async def update_cfg_str(self, new_cfg: str) -> bool: json_cfg = toml.loads(new_cfg) - self.sr = await self.r.subreddit(json_cfg[CFG_DATA_RE]["subreddits"]) + self.sr_list = json_cfg[CFG_DATA_RE]["subreddits"].split("+") + self.sr = await self.r.subreddit("+".join(self.sr_list)) self.fetch_limit = json_cfg[CFG_DATA_RE]["fetch_limit"] return True async def update_cfg(self, new_cfg: dict) -> bool: - self.sr = await self.r.subreddit(new_cfg[CFG_DATA_RE]["subreddits"]) + self.sr_list = new_cfg[CFG_DATA_RE]["subreddits"].split("+") + self.sr = await self.r.subreddit("+".join(self.sr_list)) self.fetch_limit = new_cfg[CFG_DATA_RE]["fetch_limit"] return True \ No newline at end of file diff --git a/src/python/cmds.py b/src/python/cmds.py index 3f2e2f2..7a06f59 100644 --- a/src/python/cmds.py +++ b/src/python/cmds.py @@ -19,7 +19,7 @@ async def is_cmd(cmd: str, text: str, bot: botPy.Bot) -> bool: async def respond_to_mention(bot: botPy.Bot) -> bool: - async for mention in bot.r.inbox.mentions(limit=25): + async for mention in bot.r.inbox.mentions(limit=100): if not mention.new: continue @@ -33,13 +33,15 @@ async def respond_to_mention(bot: botPy.Bot) -> bool: await bk_week_add(mention, bot) else: + py_print("Mention was not a command.") await mention.mark_read() return True async def bk_week_add(mention: models.Comment, bot: botPy.Bot): - if not mention.subreddit.display_name not in bot.sr: + if bot.args["dev"]: py_print("Mention was a command: add_post") + if not mention.subreddit.display_name not in bot.sr_list: await mention.mark_read() return @@ -58,23 +60,18 @@ async def bk_week_add(mention: models.Comment, bot: botPy.Bot): r = "" bd = bot.data[botPy.RE_DATA_POSTS] + if short_url not in bd: - posts.add_post_url(bot, short_url) - r = "Successfully added this post to the data!" + if not is_mod: r = "Successfully added your post to the weekly art submissions!" + if is_mod: r = "[MOD ACTION] Successfully added this post to the weekly art submissions!" + else: + if bd[short_url]["removed"]["removed"] and is_mod: + r = "[MOD ACTION] Successfully un-removed this post from the weekly art submissions!" + elif not bd[short_url]["removed"]["removed"]: + r = "Couldn't add this post to the submissions! Luckily, it's already there!" - if short_url in bd and is_mod: - if "removed" in bd[short_url]: - r = "[MOD ACTION] Successfully un-removed this post from the data!" - else: - r = "[MOD ACTION] Successfully added this post to the data!" + await posts.add_post_url(bot, short_url) - post = await posts.from_url(bot, short_url) - post_data = posts.get_post_details(post[1]) - data.add_post_to_data(bot, post_data, True) - - elif short_url in bd: - r = "Could not add this post to the data. Luckily, it's already there, so there's nothing to worry about!" - - - await mention.reply(r + " Thank you for participating!" + "\n\n" + BOT_ACTION_POSTFIX) + if r != "": + await mention.reply(r + " Thank you for participating!" + "\n\n" + BOT_ACTION_POSTFIX) await mention.mark_read() \ No newline at end of file diff --git a/src/python/data.py b/src/python/data.py index 74b5c12..061339f 100644 --- a/src/python/data.py +++ b/src/python/data.py @@ -9,7 +9,7 @@ from macros import * DATA_PATH = os.path.join(os.path.join(os.getcwd(), "data")) DB_PATH = os.path.join(DATA_PATH, "db") -DEFAULT_PATH = os.path.join(DATA_PATH, "default") +DEFAULT_PATH = os.path.join(DATA_PATH, "defaults") CFG_PATH = os.path.join(os.path.join(os.getcwd(), "cfg")) @@ -91,7 +91,7 @@ def read_data(bot: botPy.Bot) -> bool: return False py_print("re_data.json not found, creating new from preset...") - with open(os.path.join(DATA_PATH, "re_data_preset.json", "r")) as f: + with open(os.path.join(DEFAULT_PATH, "re_data_preset.json"), "r") as f: data_preset_json = json.load(f) data_preset_json[botPy.RE_DATA_POSTS].pop("EXAMPLE VALUE", None) @@ -150,7 +150,7 @@ def add_post_to_data(bot: botPy.Bot, new_data: PostData, bypass_conditions: bool if bot.args["dev"]: py_print(f"Added post \"{new_data.url}\" (Conditions bypassed)") return True - if new_data.url not in bot.data[botPy.RE_DATA_POSTS]: + elif new_data.url not in bot.data[botPy.RE_DATA_POSTS]: bot.data[botPy.RE_DATA_POSTS][new_data.url] = new_data.to_json() if bot.args["dev"]: py_print(f"Added post \"{new_data.url}\"") return True @@ -159,6 +159,9 @@ def add_post_to_data(bot: botPy.Bot, new_data: PostData, bypass_conditions: bool def set_approve_post(bot: botPy.Bot, approved: bool, url: str) -> bool: + if url not in bot.data[botPy.RE_DATA_POSTS]: + return False + if not bot.data[botPy.RE_DATA_POSTS][url]["removed"]["removed"]: bot.data[botPy.RE_DATA_POSTS][url]["approved"]["by_human"] = approved return True @@ -205,6 +208,9 @@ def set_vote_post( ) -> bool: if url not in bot.data[botPy.RE_DATA_POSTS]: return False + + if bot.data[botPy.RE_DATA_POSTS][url]["removed"]["removed"]: + return False votes = bot.data[botPy.RE_DATA_POSTS][url]["votes"] re_voters: set[str] = set(votes["voters_re"]) From fcf95dd9ebf6bf89dcc000fa8eeb2c137ac08791 Mon Sep 17 00:00:00 2001 From: Byte Dice Date: Wed, 18 Jun 2025 21:10:51 +0200 Subject: [PATCH 22/39] Added language support to python. TODO: add language placeholders --- data/defaults/cfg_default.toml | 4 ++++ src/cmds/reload_cfg.rs | 4 +++- src/data.rs | 5 +++-- src/main.rs | 5 +++-- src/python.rs | 14 +++++++++++-- src/python/bot.py | 6 +++--- src/python/macros.py | 36 +++++++++++++++++++++++++++++++++- src/python/main.py | 7 +++++-- src/python/posts.py | 10 +++++++--- src/python/py_websocket.py | 21 ++++++++++---------- src/websocket.rs | 2 +- 11 files changed, 87 insertions(+), 27 deletions(-) diff --git a/data/defaults/cfg_default.toml b/data/defaults/cfg_default.toml index d0e85a0..3ba1e6e 100644 --- a/data/defaults/cfg_default.toml +++ b/data/defaults/cfg_default.toml @@ -11,6 +11,10 @@ subreddits = "bytedicetesting" # How many posts the bot scans when running `/re_updatediscord`. fetch_limit = 100 +# A list of flairs the post must be tagged under to be scraped. +# Leave empty to search for everything +search_flairs = ["Original Art"] + [commands] # Disabled selected command categories. All commands and their categories can be viewed in the README. # "admin" and "owner" categories are always enabled. diff --git a/src/cmds/reload_cfg.rs b/src/cmds/reload_cfg.rs index e874040..e9400f9 100644 --- a/src/cmds/reload_cfg.rs +++ b/src/cmds/reload_cfg.rs @@ -21,7 +21,9 @@ pub async fn cmd( let d_str = toml::to_string(&d)?; let r = send_cmd_json("update_cfg", Some(json!([d_str])), true).await; - if r.is_some() && r.unwrap()["value"].as_bool().unwrap() { + if r.is_none() { return Ok(()); } + + if r.unwrap()["value"].as_bool().unwrap() { send_msg( ctx, lang!("dc_msg_reload_cfg_success", toml::to_string_pretty(&d).unwrap()), diff --git a/src/data.rs b/src/data.rs index 28437ec..ff51ca3 100644 --- a/src/data.rs +++ b/src/data.rs @@ -4,7 +4,7 @@ use std::path::Path; use serde_json::{self, Value, json}; use tokio::sync::Mutex; -use crate::{errln, rs_println, Data, Error, CFG_DATA_RE, LANG}; +use crate::{errln, rs_println, Data, Error, CFG_DATA_RE, LANG, LANG_NAME}; use crate::websocket::send_cmd_json; @@ -220,9 +220,10 @@ pub fn load_lang_data(lang: String) { } let str_data = fs::read_to_string(full_path).unwrap(); - let json_data = serde_json::from_str(&str_data).unwrap(); + let json_data: Option = serde_json::from_str(&str_data).unwrap(); unsafe { LANG = json_data; + LANG_NAME = Some(lang); }; } \ No newline at end of file diff --git a/src/main.rs b/src/main.rs index ccbe105..da0efba 100644 --- a/src/main.rs +++ b/src/main.rs @@ -95,8 +95,9 @@ struct Data { static CFG_DATA_RE: &str = "posts"; -pub static mut LANG: Option = None; -pub static mut NOPING: bool = false; +pub static mut LANG_NAME: Option = None; +pub static mut LANG: Option = None; +pub static mut NOPING: bool = false; #[tokio::main] diff --git a/src/python.rs b/src/python.rs index 09b9a4f..89c96c3 100644 --- a/src/python.rs +++ b/src/python.rs @@ -1,5 +1,5 @@ use crate::messages::send_dm; -use crate::{errln, lang, rs_println, Args}; +use crate::{errln, lang, rs_println, Args, LANG_NAME}; use std::fs; use std::ffi::CString; @@ -21,7 +21,17 @@ pub async fn start(args: Args) -> PyResult<()> { let code = get_code(&format!("{}{}main.py", path, slash)); let py_args = args_str.replace(":true", ":True").replace(":false", ":False"); - let app_path = CString::new(format!("args = {}\n{}", py_args, code)).unwrap(); + let app_path: CString; + + unsafe { + app_path = CString::new( + format!("args = {}\nlang_name = {}\n{}", + py_args, + LANG_NAME.clone().unwrap(), + code + ) + ).unwrap(); + } let mut traceback: String = String::new(); let mut is_error = false; diff --git a/src/python/bot.py b/src/python/bot.py index e939470..0d1dc92 100644 --- a/src/python/bot.py +++ b/src/python/bot.py @@ -41,6 +41,7 @@ class Bot: self.sr = None self.data_f: TextIOWrapper = None self.data: dict = {} + self.flairs: list[str] = [] async def initialize(self): self.sr = await self.r.subreddit("+".join(self.sr_list)) @@ -58,13 +59,12 @@ class Bot: async def update_cfg_str(self, new_cfg: str) -> bool: json_cfg = toml.loads(new_cfg) - self.sr_list = json_cfg[CFG_DATA_RE]["subreddits"].split("+") - self.sr = await self.r.subreddit("+".join(self.sr_list)) - self.fetch_limit = json_cfg[CFG_DATA_RE]["fetch_limit"] + self.update_cfg(json_cfg) return True async def update_cfg(self, new_cfg: dict) -> bool: self.sr_list = new_cfg[CFG_DATA_RE]["subreddits"].split("+") self.sr = await self.r.subreddit("+".join(self.sr_list)) self.fetch_limit = new_cfg[CFG_DATA_RE]["fetch_limit"] + self.flairs = new_cfg[CFG_DATA_RE]["search_flair"] return True \ No newline at end of file diff --git a/src/python/macros.py b/src/python/macros.py index 239822d..3518f80 100644 --- a/src/python/macros.py +++ b/src/python/macros.py @@ -1,5 +1,13 @@ +import json +import os from printColors import PrintColors + +G_LANG_NAME: str = "" +G_LANG: dict[str, str] = {} +DATA_PATH_LANG: str = "./data/lang/" + + def py_print(*args): print( PrintColors.FG.blue + "Py", @@ -14,4 +22,30 @@ def py_error(*args): "-", " ".join(args) + PrintColors.Special.reset ) - quit() \ No newline at end of file + quit() + + +def lang(k: str) -> str: + t = G_LANG.get(k) + if k is None: py_error(f"Key not found in language \"{G_LANG_NAME}\": {k}") + return str(t) + + +def init_lang(lang_name: str): + global G_LANG, G_LANG_NAME + G_LANG_NAME = lang_name + + full_path = f"{DATA_PATH_LANG}{lang_name}.json" + + if not os.path.exists(full_path): + py_error(f"File for language \"{lang_name}\" ({lang_name}.json) not found!\n Hint: You can download official language files at https://github.com/ByteDice/ByteDiceAssistant in the data/langs/... folder") + + with open(full_path, "r") as f: + str_data = f.read() + + try: + json_data = json.loads(str_data) + except json.JSONDecodeError as e: + py_error(f"Failed to parse JSON for language \"{lang_name}\":\n{e}") + + G_LANG = json_data \ No newline at end of file diff --git a/src/python/main.py b/src/python/main.py index 72f32b9..8cf8ce3 100644 --- a/src/python/main.py +++ b/src/python/main.py @@ -19,9 +19,12 @@ async def main(): # args is supposed to be undefined. # It gets defined in Rust. - try: await bot.set_args(args) + try: + await bot.set_args(args) + init_lang(lang_name) except NameError: - py_print("No command args found from Rust. Don't worry though, we have backup in place.") + py_print("No command args or language name found from Rust. Don't worry though, we have backup in place.") + init_lang("en") if bot.args["dev"]: py_print("ARGS:", str(bot.args)) diff --git a/src/python/posts.py b/src/python/posts.py index 9a08a9e..f9651a7 100644 --- a/src/python/posts.py +++ b/src/python/posts.py @@ -14,7 +14,7 @@ async def add_new_posts(bot: botPy.Bot, max_age: int) -> bool: cross_emoji = emoji.emojize(":cross_mark:") py_print("Fetching posts...") - posts = await fetch_posts_with_flair(bot, "Original Art") + posts = await fetch_posts_with_flair(bot, bot.flairs) py_print("Evaluating posts...") @@ -65,11 +65,15 @@ async def add_new_posts(bot: botPy.Bot, max_age: int) -> bool: return True -async def fetch_posts_with_flair(bot: botPy.Bot, flair_name: str) -> list[models.Submission]: +async def fetch_posts_with_flair(bot: botPy.Bot, flair_names: list[str]) -> list[models.Submission]: posts: list[models.Submission] = [] + flair_names_str = \ + f"flair:{flair_names[0]}" if len(flair_names) == 1\ + else " OR ".join(f"flair:{flair}" for flair in flair_names) + # ~36 OG-art posts per week, round limit to 50, 75 or 100 - async for post in bot.sr.search(f"flair:\"{flair_name}\"", sort="new", limit=bot.fetch_limit): + async for post in bot.sr.search(f"{flair_names_str}", sort="new", limit=bot.fetch_limit): posts.append(post) return posts diff --git a/src/python/py_websocket.py b/src/python/py_websocket.py index 08279dd..94b9e70 100644 --- a/src/python/py_websocket.py +++ b/src/python/py_websocket.py @@ -7,6 +7,7 @@ import bot as botPy import data import posts import cmds +import macros ws_global = None is_connected = False @@ -74,16 +75,16 @@ async def json_to_func(v: dict, bot: botPy.Bot) -> dict: r = False match v["value"]: - case "update_data_file": r = data .write_data (bot) - case "respond_mentions": r = await cmds .respond_to_mention(bot) - case "add_new_posts": r = await posts.add_new_posts (bot, *v["args"]) - case "add_post_url": r = await posts.add_post_url (bot, *v["args"]) - case "remove_post_url": r = data .remove_post (bot, *v["args"]) - case "set_approve_post": r = data .set_approve_post (bot, *v["args"]) - case "set_vote_post": r = data .set_vote_post (bot, *v["args"]) - case "remove_old_posts": r = data .remove_old_posts (bot, *v["args"]) - case "update_cfg": r = await bot .update_cfg_str (*v["args"]) - case "stop_praw": r = await bot .stop () + case "update_data_file": r = data .write_data (bot) + case "respond_mentions": r = await cmds .respond_to_mention(bot) + case "add_new_posts": r = await posts .add_new_posts (bot, *v["args"]) + case "add_post_url": r = await posts .add_post_url (bot, *v["args"]) + case "remove_post_url": r = data .remove_post (bot, *v["args"]) + case "set_approve_post": r = data .set_approve_post (bot, *v["args"]) + case "set_vote_post": r = data .set_vote_post (bot, *v["args"]) + case "remove_old_posts": r = data .remove_old_posts (bot, *v["args"]) + case "update_cfg": r = await bot .update_cfg_str (*v["args"]) + case "stop_praw": r = await bot .stop () case _: value_supported = False print_result = v["print"] diff --git a/src/websocket.rs b/src/websocket.rs index 764d5c9..3b2a0f4 100644 --- a/src/websocket.rs +++ b/src/websocket.rs @@ -70,7 +70,7 @@ pub async fn send_cmd_json(func_name: &str, func_args: Option, print_outp { return r; } } - if !["respond_mentions"].contains(&func_name) || ::parse().dev { + if ::parse().dev { rs_println!("Received from Python: [RESPONSE] {:?}", r); } From 2c797ad69dfc1c4186f03bd3daa6f256ce74b7c8 Mon Sep 17 00:00:00 2001 From: Byte Dice Date: Thu, 19 Jun 2025 10:06:39 +0200 Subject: [PATCH 23/39] added full language support for python & fixed reddut "add_post" cmd. --- data/lang/en.json | 7 ++++++- src/python.rs | 2 +- src/python/bot.py | 2 +- src/python/cmds.py | 26 ++++++++++++++------------ src/python/data.py | 2 ++ src/python/macros.py | 2 ++ 6 files changed, 26 insertions(+), 15 deletions(-) diff --git a/data/lang/en.json b/data/lang/en.json index 3304727..a30af42 100644 --- a/data/lang/en.json +++ b/data/lang/en.json @@ -56,5 +56,10 @@ "dc_msg_update_removing_old": "{0}Removing old posts (threshold: {1}d)...", "dc_msg_update_removing": "{0}Removing removed posts...", "log_lang_load_success": "Successfully loaded the english language file!", - "none": "None" + "none": "None", + "py_re_response_suffix": "^(I am not an AI, I am just a bot. This action was performed automatically by the way. You can report bugs and view my source code [here](https://github.com/ByteDice/ByteDiceAssistant)!)", + "py_re_response_weekly_add": "Successfully added your post to the weekly art submissions! Thank you for participating!", + "py_re_response_weekly_exists": "Couldn't add this post to the submissions! Luckily, it's already there! Thank you for participating!", + "py_re_response_weekly_mod_add": "[MOD ACTION] Successfully added this post to the weekly art submissions!", + "py_re_response_weekly_mod_unremove": "[MOD ACTION] Successfully un-removed this post from the weekly art submissions!" } \ No newline at end of file diff --git a/src/python.rs b/src/python.rs index 89c96c3..d382dcd 100644 --- a/src/python.rs +++ b/src/python.rs @@ -25,7 +25,7 @@ pub async fn start(args: Args) -> PyResult<()> { unsafe { app_path = CString::new( - format!("args = {}\nlang_name = {}\n{}", + format!("args = {}\nlang_name = \"{}\"\n{}", py_args, LANG_NAME.clone().unwrap(), code diff --git a/src/python/bot.py b/src/python/bot.py index 0d1dc92..40ed8e1 100644 --- a/src/python/bot.py +++ b/src/python/bot.py @@ -66,5 +66,5 @@ class Bot: self.sr_list = new_cfg[CFG_DATA_RE]["subreddits"].split("+") self.sr = await self.r.subreddit("+".join(self.sr_list)) self.fetch_limit = new_cfg[CFG_DATA_RE]["fetch_limit"] - self.flairs = new_cfg[CFG_DATA_RE]["search_flair"] + self.flairs = new_cfg[CFG_DATA_RE]["search_flairs"] return True \ No newline at end of file diff --git a/src/python/cmds.py b/src/python/cmds.py index 7a06f59..6727792 100644 --- a/src/python/cmds.py +++ b/src/python/cmds.py @@ -2,13 +2,9 @@ import asyncpraw.models as models from macros import * import bot as botPy -import data import posts -BOT_ACTION_POSTFIX = "^(I am not an AI, I am just a bot. This action was performed automatically by the way. You can report bugs and view my source code [here](https://github.com/ByteDice/ByteDiceAssistant)!)" - - async def make_cmd(cmd: str, bot: botPy.Bot) -> str: return f"u/{await bot.r.user.me()} {cmd}" @@ -33,7 +29,7 @@ async def respond_to_mention(bot: botPy.Bot) -> bool: await bk_week_add(mention, bot) else: - py_print("Mention was not a command.") + if bot.args["dev"]: py_print("Mention was not a command.") await mention.mark_read() return True @@ -41,8 +37,11 @@ async def respond_to_mention(bot: botPy.Bot) -> bool: async def bk_week_add(mention: models.Comment, bot: botPy.Bot): if bot.args["dev"]: py_print("Mention was a command: add_post") - if not mention.subreddit.display_name not in bot.sr_list: + + subreddit = mention.subreddit.display_name + if subreddit not in bot.sr_list: await mention.mark_read() + if bot.args["dev"]: py_print(f"Mention wasn't in a selected subreddit. Subreddit: {subreddit}") return await mention.submission.load() @@ -52,8 +51,9 @@ async def bk_week_add(mention: models.Comment, bot: botPy.Bot): is_op = author == mention.submission.author is_mod = author in await mention.subreddit.moderator() - if not is_op and not is_mod: + if (not is_op) and (not is_mod): await mention.mark_read() + if bot.args["dev"]: py_print("Mention wasn't by the OP or a moderator.") return short_url = mention.submission.shortlink @@ -62,16 +62,18 @@ async def bk_week_add(mention: models.Comment, bot: botPy.Bot): bd = bot.data[botPy.RE_DATA_POSTS] if short_url not in bd: - if not is_mod: r = "Successfully added your post to the weekly art submissions!" - if is_mod: r = "[MOD ACTION] Successfully added this post to the weekly art submissions!" + if not is_mod: r = lang("py_re_response_weekly_add") + if is_mod: r = lang("py_re_response_weekly_mod_add") else: if bd[short_url]["removed"]["removed"] and is_mod: - r = "[MOD ACTION] Successfully un-removed this post from the weekly art submissions!" + r = lang("py_re_response_weekly_mod_unremove") elif not bd[short_url]["removed"]["removed"]: - r = "Couldn't add this post to the submissions! Luckily, it's already there!" + r = lang("py_re_response_weekly_exists") await posts.add_post_url(bot, short_url) if r != "": - await mention.reply(r + " Thank you for participating!" + "\n\n" + BOT_ACTION_POSTFIX) + await mention.reply(r + "\n\n" + lang("py_re_response_suffix")) + if bot.args["dev"]: py_print("Responded to mention.") + elif bot.args["dev"]: py_print("Response is empty.") await mention.mark_read() \ No newline at end of file diff --git a/src/python/data.py b/src/python/data.py index 061339f..19767ab 100644 --- a/src/python/data.py +++ b/src/python/data.py @@ -144,6 +144,8 @@ def add_post_to_data(bot: botPy.Bot, new_data: PostData, bypass_conditions: bool new_data.removed = False new_data.removed_by = None new_data.removed_reason = None + if bot.args["dev"]: py_print(f"Un-removed post \"{new_data.url}\"") + return True if bypass_conditions: bot.data[botPy.RE_DATA_POSTS][new_data.url] = new_data.to_json() diff --git a/src/python/macros.py b/src/python/macros.py index 3518f80..0248f40 100644 --- a/src/python/macros.py +++ b/src/python/macros.py @@ -26,6 +26,8 @@ def py_error(*args): def lang(k: str) -> str: + if G_LANG == {}: + py_error("Language must be initialized before use!") t = G_LANG.get(k) if k is None: py_error(f"Key not found in language \"{G_LANG_NAME}\": {k}") return str(t) From 486b88d90e829a9dc3734bbf0b957a7650aaf6d6 Mon Sep 17 00:00:00 2001 From: Byte Dice Date: Thu, 19 Jun 2025 10:12:56 +0200 Subject: [PATCH 24/39] French. (translated by chatGPT) --- data/lang/gpt_fr.json | 65 +++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 65 insertions(+) create mode 100644 data/lang/gpt_fr.json diff --git a/data/lang/gpt_fr.json b/data/lang/gpt_fr.json new file mode 100644 index 0000000..361a651 --- /dev/null +++ b/data/lang/gpt_fr.json @@ -0,0 +1,65 @@ +{ + "dc_btn_approve": "Approuver", + "dc_btn_remove": "Supprimer", + "dc_btn_unapprove": "Désapprouver", + "dc_btn_unremove": "Restaurer", + "dc_btn_unvote": "Annuler le vote", + "dc_btn_vote": "Voter", + "dc_msg_8-ball_answer": "## Vous avez secoué une boule magique 8\nQ : {0}\nR : {1}", + "dc_msg_add_to_data": "Serveur ajouté à mes données ! Merci de me laisser les voler ! (/s)", + "dc_msg_bound_channel": "Canal avec l’identifiant `{0}` lié avec succès comme canal \"où toutes les données Reddit collectées sont envoyées\" !", + "dc_msg_cmd_404": "Commande \"{0}\" introuvable !\nAstuce : Essayez `/help` sans arguments ou `/help `", + "dc_msg_corrupted_data": "Oups `(。>\\\\<)`. On dirait que mes données sont \\**snif*\\*... c-corrompues !\n[De Byte Dice] : Je n’ai aucune idée de ce que je faisais à 2h du matin. Je ne le supprime pas.", + "dc_msg_data_server_404": "Ce serveur n’est pas dans les données !\n Astuce : Exécutez la commande `/add_server` dans un serveur Discord (nécessite les droits administrateur).", + "dc_msg_dm_python_err_socket": "Erreur Python interne inconnue : erreur de réponse WebSocket", + "dc_msg_dm_python_err": "Erreur Python interne inconnue :\n```\n{0}\n```", + "dc_msg_embed_default_embed_desc": "Description par défaut de l'encart en anglais.", + "dc_msg_embed_re_post": "Spoilers et anonymisation des votes pour une évaluation équitable !\n## Données du post :\n**Votes positifs du post :** ||`{0:>6}`||\n**Votes des modérateurs :** ||`{1:>6}`||\n**Type de média :** `{2}`\n**URL :** ||<{3}>||\n\n## Données de la liste :\n**Ajouté par :** `{{ humain : {4}, bot : {5} }}`\n**Approuvé par :** `{{ humain : {6}, bot : [non implémenté] }}`", + "dc_msg_embed_re_removed": "## Supprimé par `{0}`\n**Raison :** {1}\n**URL** : ||<{2}>||", + "dc_msg_err_trace": "Erreur inconnue !\nTrace de l’erreur : {0}", + "dc_msg_failed_shorturl_conversion": "Échec de la conversion en shortURL : format d’URL Reddit invalide.", + "dc_msg_mandatory_response": "Message de réponse obligatoire, merci d’ignorer.", + "dc_msg_owner_data_save_complete": "Sauvegarde des données... Terminé !\nFermeture...", + "dc_msg_owner_data_save": "Sauvegarde des données...", + "dc_msg_owner_shutdown_failed_confirmation": "Échec de l’arrêt : confirmation invalide.", + "dc_msg_re_already_voted": "Impossible de voter : vous avez déjà voté pour ce post !", + "dc_msg_re_also_approved": "Également approuvé !", + "dc_msg_re_data_corrupted": "URL du post \"<{0}>\" introuvable : données du post corrompues !\n Détails : clé \"posts\" introuvable dans le fichier \"re_data.json\"", + "dc_msg_re_help_removed": "Fichier d’aide Reddit introuvable, il a probablement été supprimé.\n Astuce : Vous pouvez trouver le fichier d’aide officiel à https://github.com/ByteDice/ByteDiceAssistant dans `re_help.md`.\n**ATTENTION :** Le fichier d’aide peut contenir des informations incorrectes si vous n’utilisez pas le bot officiel de Byte Dice.", + "dc_msg_re_permdeny_not_re_mod": "Permission refusée : vous n’êtes pas modérateur des subreddit(s) {0}", + "dc_msg_re_post_404": "URL du post \"<{0}>\" introuvable : le post n’existe pas dans les données !\n Astuce : Exécutez la commande `/re_addpost [URL]` dans un canal Discord ou `u/ByteDiceAssistant add_post` sur un post Reddit.", + "dc_msg_re_post_add_success": "Post ajouté avec succès à l’URL \"<{0}>\" !", + "dc_msg_re_post_approve_remove": "Impossible d’approuver le post car il a été supprimé !", + "dc_msg_re_post_approve_success": "Post approuvé avec succès !", + "dc_msg_re_post_disapprove_success": "Post désapprouvé avec succès !", + "dc_msg_re_post_remove_success": "Post supprimé avec succès à l’URL \"<{0}>\" !", + "dc_msg_re_post_unremove_success": "Post restauré avec succès à l’URL \"<{0}>\" !", + "dc_msg_re_post_update_success": "Post mis à jour avec succès à l’URL \"<{0}>\" !", + "dc_msg_re_post_vote_removed_post": "Impossible de [voter / annuler le vote] : le post a été supprimé !", + "dc_msg_re_posts_channel_404": "Impossible de trouver `re_posts_channel` dans les données !\nAstuce : Exécutez `/admin_re_bindchannel` dans un canal (de préférence en lecture seule) (nécessite les droits administrateur).", + "dc_msg_re_vote_err": "Échec du [vote / annulation du vote] : erreur interne inconnue.", + "dc_msg_re_vote_mod_success": "Vote enregistré avec succès (vote modérateur) !", + "dc_msg_re_vote_remove_havent": "Impossible de retirer le vote : vous n’avez pas encore voté pour ce post !", + "dc_msg_re_vote_remove_success": "Vote retiré avec succès !", + "dc_msg_re_vote_success": "Vote enregistré avec succès !", + "dc_msg_reload_cfg_python_fail": "Échec du rechargement des configurations : réponse invalide de Python.", + "dc_msg_reload_cfg_success": "Configurations rechargées avec succès !\nNouvelles configurations :\n```\n{0}\n```", + "dc_msg_removed_square_brackets": "[SUPPRIMÉ] {0}", + "dc_msg_shorturl": "ShortURL : <{0}>", + "dc_msg_update_add": "{0}Ajout de nouveaux posts...", + "dc_msg_update_done": "{0}Terminé !", + "dc_msg_update_editing": "{0}Modification des posts mis à jour...", + "dc_msg_update_fetch": "{0}Récupération des nouveaux posts et mise à jour du fichier de données...", + "dc_msg_update_parse": "{0}Analyse des messages vers JSON...", + "dc_msg_update_read": "{0}Lecture des messages dans <#{1}>...", + "dc_msg_update_removing_dupe": "{0}Suppression des posts en double...", + "dc_msg_update_removing_old": "{0}Suppression des anciens posts (seuil : {1}j)...", + "dc_msg_update_removing": "{0}Suppression des posts supprimés...", + "log_lang_load_success": "Fichier de langue anglais chargé avec succès !", + "none": "Aucun", + "py_re_response_suffix": "^(Je ne suis pas une IA, juste un bot. Cette action a été effectuée automatiquement. Vous pouvez signaler des bugs et consulter mon code source [ici](https://github.com/ByteDice/ByteDiceAssistant) !)", + "py_re_response_weekly_add": "Post ajouté avec succès aux soumissions hebdomadaires d'art ! Merci pour votre participation !", + "py_re_response_weekly_exists": "Impossible d’ajouter ce post, il est déjà présent ! Merci pour votre participation !", + "py_re_response_weekly_mod_add": "[ACTION MOD] Post ajouté avec succès aux soumissions hebdomadaires d’art !", + "py_re_response_weekly_mod_unremove": "[ACTION MOD] Post restauré avec succès dans les soumissions hebdomadaires d’art !" +} \ No newline at end of file From fa76443735508f0f38ed2df76555d52aa62f2329 Mon Sep 17 00:00:00 2001 From: ByteDice Date: Thu, 26 Jun 2025 11:16:17 +0200 Subject: [PATCH 25/39] added more config options & made python type-safe(r). --- data/defaults/cfg_default.toml | 15 ++++++++++- data/defaults/re_data_preset.json | 1 + data/lang/en.json | 4 +-- data/lang/gpt_fr.json | 6 ++--- src/cmds/eight_ball.rs | 7 ++--- src/cmds/reload_cfg.rs | 12 ++++----- src/data.rs | 8 +++--- src/main.rs | 22 +++++++-------- src/messages.rs | 1 + src/python/bot.py | 25 ++++++++--------- src/python/macros.py | 14 +++++----- src/python/main.py | 16 ++++++----- src/python/posts.py | 35 +++++++++++++----------- src/python/{cmds.py => py_cmds.py} | 2 +- src/python/{data.py => py_data.py} | 19 ++++++++----- src/python/py_websocket.py | 43 +++++++++++++++--------------- src/websocket.rs | 4 +-- 17 files changed, 132 insertions(+), 102 deletions(-) rename src/python/{cmds.py => py_cmds.py} (96%) rename src/python/{data.py => py_data.py} (93%) diff --git a/data/defaults/cfg_default.toml b/data/defaults/cfg_default.toml index 3ba1e6e..8be26b9 100644 --- a/data/defaults/cfg_default.toml +++ b/data/defaults/cfg_default.toml @@ -1,4 +1,8 @@ [general] +# The language of the bot. Available languages can be found in the data/lang folder. +# NOTE: Do not include the file extension of the language. For example, "en.json" is just "en". +lang = "en" + # The discord bots status text status = "🎲 https://bytedice.net" @@ -15,6 +19,12 @@ fetch_limit = 100 # Leave empty to search for everything search_flairs = ["Original Art"] +# Command aliases for the Reddit bot, since those aren't autocomplete-able. +# TODO: this +[reddit.aliases] +add_post = [] + + [commands] # Disabled selected command categories. All commands and their categories can be viewed in the README. # "admin" and "owner" categories are always enabled. @@ -23,4 +33,7 @@ disabled_categories = [ # "fun", # "help" # "re" -] \ No newline at end of file +] + +# The chance (between 0..1) for the `/8_ball` command to output a "quirky" answer. +eight_ball_quirky_chance = 0.2 \ No newline at end of file diff --git a/data/defaults/re_data_preset.json b/data/defaults/re_data_preset.json index 100d4b4..bc8ffcf 100644 --- a/data/defaults/re_data_preset.json +++ b/data/defaults/re_data_preset.json @@ -7,6 +7,7 @@ "reason": null }, "post_data": { + "subreddit": "bytedicetesting", "title": "I JUST BOUGHT THE CONTINENT OF NORTH AMERICA FOR A DOLLAR!", "upvotes": 69420, "date_unix": 1738614657, diff --git a/data/lang/en.json b/data/lang/en.json index a30af42..27e38b4 100644 --- a/data/lang/en.json +++ b/data/lang/en.json @@ -5,7 +5,7 @@ "dc_btn_unremove": "Restore", "dc_btn_unvote": "Un-vote", "dc_btn_vote": "Vote", - "dc_msg_8-ball_answer": "## You shook a magic 8-ball\nQ: {0}\nA: {1}", + "dc_msg_8-ball_answer": "## You shook a magic 8-ball...\n**Q:** {0}\n**A:** {1}", "dc_msg_add_to_data": "Added your server to my data! Thanks for letting me steal it! (/s)", "dc_msg_bound_channel": "Successfully bound channel ID `{0}` as the \"where all collected Reddit data gets dumped\" channel!", "dc_msg_cmd_404": "No command \"{0}\" found!\nHint: Try `/help` without any arguments or `/help `", @@ -14,7 +14,7 @@ "dc_msg_dm_python_err_socket": "Unknown internal Python error occurred: Websocket response error", "dc_msg_dm_python_err": "Unknown internal Python Error:\n```\n{0}\n```", "dc_msg_embed_default_embed_desc": "Default english embed description.", - "dc_msg_embed_re_post": "Spoilers and vote length anonymizer for fair review!\n## Post Data:\n**Post upvotes:** ||`{0:>6}`||\n**Moderator votes:** ||`{1:>6}`||\n**Media type:** `{2}`\n**URL:** ||<{3}>||\n\n## Listing Data:\n**Added by:** `{{ human: {4}, bot: {5} }}`\n**Approved by:** `{{ human: {6}, bot: [not implemented] }}`", + "dc_msg_embed_re_post": "Spoilers and vote length anonymizer for fair review!\n## Post Data:\n**Subreddit:** {0}\n**Post upvotes:** ||`{1:>6}`||\n**Moderator votes:** ||`{2:>6}`||\n**Media type:** `{3}`\n**URL:** ||<{4}>||\n\n## Listing Data:\n**Added by:** human: {5} | bot: {6}\n**Approved by:** human: {7} | bot: [not implemented]", "dc_msg_embed_re_removed": "## Removed by `{0}`\n**Reason:** {1}\n**URL**: ||<{2}>||", "dc_msg_err_trace": "Unknown error!\nError trace: {0}", "dc_msg_failed_shorturl_conversion": "Couldn't convert to shortURL: Invalid Reddit URL format.", diff --git a/data/lang/gpt_fr.json b/data/lang/gpt_fr.json index 361a651..4086afd 100644 --- a/data/lang/gpt_fr.json +++ b/data/lang/gpt_fr.json @@ -5,7 +5,7 @@ "dc_btn_unremove": "Restaurer", "dc_btn_unvote": "Annuler le vote", "dc_btn_vote": "Voter", - "dc_msg_8-ball_answer": "## Vous avez secoué une boule magique 8\nQ : {0}\nR : {1}", + "dc_msg_8-ball_answer": "## Vous avez secoué une boule magique 8...\n**Q :** {0}\n**R :** {1}", "dc_msg_add_to_data": "Serveur ajouté à mes données ! Merci de me laisser les voler ! (/s)", "dc_msg_bound_channel": "Canal avec l’identifiant `{0}` lié avec succès comme canal \"où toutes les données Reddit collectées sont envoyées\" !", "dc_msg_cmd_404": "Commande \"{0}\" introuvable !\nAstuce : Essayez `/help` sans arguments ou `/help `", @@ -14,7 +14,7 @@ "dc_msg_dm_python_err_socket": "Erreur Python interne inconnue : erreur de réponse WebSocket", "dc_msg_dm_python_err": "Erreur Python interne inconnue :\n```\n{0}\n```", "dc_msg_embed_default_embed_desc": "Description par défaut de l'encart en anglais.", - "dc_msg_embed_re_post": "Spoilers et anonymisation des votes pour une évaluation équitable !\n## Données du post :\n**Votes positifs du post :** ||`{0:>6}`||\n**Votes des modérateurs :** ||`{1:>6}`||\n**Type de média :** `{2}`\n**URL :** ||<{3}>||\n\n## Données de la liste :\n**Ajouté par :** `{{ humain : {4}, bot : {5} }}`\n**Approuvé par :** `{{ humain : {6}, bot : [non implémenté] }}`", + "dc_msg_embed_re_post": "Spoilers et anonymiseur de durée des votes pour une évaluation équitable !\n## Données du post :\n**Subreddit :** {0}\n**Votes positifs du post :** ||`{1:>6}`||\n**Votes des modérateurs :** ||`{2:>6}`||\n**Type de média :** `{3}`\n**URL :** ||<{4}>||\n\n## Données d'inscription :\n**Ajouté par :** humain : {5} | bot : {6}\n**Approuvé par :** humain : {7} | bot : [non implémenté]", "dc_msg_embed_re_removed": "## Supprimé par `{0}`\n**Raison :** {1}\n**URL** : ||<{2}>||", "dc_msg_err_trace": "Erreur inconnue !\nTrace de l’erreur : {0}", "dc_msg_failed_shorturl_conversion": "Échec de la conversion en shortURL : format d’URL Reddit invalide.", @@ -55,7 +55,7 @@ "dc_msg_update_removing_dupe": "{0}Suppression des posts en double...", "dc_msg_update_removing_old": "{0}Suppression des anciens posts (seuil : {1}j)...", "dc_msg_update_removing": "{0}Suppression des posts supprimés...", - "log_lang_load_success": "Fichier de langue anglais chargé avec succès !", + "log_lang_load_success": "Fichier de langue français chargé avec succès !", "none": "Aucun", "py_re_response_suffix": "^(Je ne suis pas une IA, juste un bot. Cette action a été effectuée automatiquement. Vous pouvez signaler des bugs et consulter mon code source [ici](https://github.com/ByteDice/ByteDiceAssistant) !)", "py_re_response_weekly_add": "Post ajouté avec succès aux soumissions hebdomadaires d'art ! Merci pour votre participation !", diff --git a/src/cmds/eight_ball.rs b/src/cmds/eight_ball.rs index 8100688..77d560f 100644 --- a/src/cmds/eight_ball.rs +++ b/src/cmds/eight_ball.rs @@ -1,6 +1,6 @@ use rand::{seq::IteratorRandom, Rng}; -use crate::{lang, messages::send_msg, Context, Error}; +use crate::{data::get_toml_mutex, lang, messages::send_msg, Context, Error}; #[poise::command( @@ -16,14 +16,15 @@ pub async fn cmd( #[description = "Question to ask."] question: String ) -> Result<(), Error> { - let is_quirky = rand::rng().random_bool(0.2); + let quirky_chance = get_toml_mutex(&ctx.data().cfg).await.unwrap()["commands"]["eight_ball_quirky_chance"].as_float().unwrap(); + let is_quirky = rand::rng().random_bool(quirky_chance.clamp(0.0, 1.0)); let list = &ctx.data().ball_prompts[if is_quirky { 1 } else { 0 }]; let rand_item = list.iter().choose(&mut rand::rng()); send_msg( ctx, lang!("dc_msg_8-ball_answer", question, rand_item.unwrap()), - true, + false, true ).await; diff --git a/src/cmds/reload_cfg.rs b/src/cmds/reload_cfg.rs index e9400f9..dcb0d43 100644 --- a/src/cmds/reload_cfg.rs +++ b/src/cmds/reload_cfg.rs @@ -1,6 +1,4 @@ -use serde_json::json; - -use crate::{data::{get_toml_mutex, read_cfg_data}, lang, messages::send_msg, websocket::send_cmd_json, Context, Error}; +use crate::{data::{self, get_toml_mutex, read_cfg_data}, lang, messages::send_msg, Context, Error}; #[poise::command( @@ -16,13 +14,15 @@ pub async fn cmd( ctx: Context<'_> ) -> Result<(), Error> { - read_cfg_data(&ctx.data(), false).await; + let r = read_cfg_data(&ctx.data(), false).await; let d = get_toml_mutex(&ctx.data().cfg).await.unwrap(); - let d_str = toml::to_string(&d)?; - let r = send_cmd_json("update_cfg", Some(json!([d_str])), true).await; if r.is_none() { return Ok(()); } + let data_binding = get_toml_mutex(&ctx.data().cfg).await.unwrap(); + let lang_cfg = data_binding["general"]["lang"].as_str().unwrap(); + data::load_lang_data(lang_cfg.to_string()); + if r.unwrap()["value"].as_bool().unwrap() { send_msg( ctx, diff --git a/src/data.rs b/src/data.rs index ff51ca3..5ae6eda 100644 --- a/src/data.rs +++ b/src/data.rs @@ -95,7 +95,6 @@ fn generate_re_data() { if let Some(bk_week) = preset_json[CFG_DATA_RE].as_object_mut() { bk_week.remove("EXAMPLE VALUE"); - bk_week.remove("EXAMPLE VALUE DELETED"); } let json_str = serde_json::to_string_pretty(&preset_json).unwrap(); @@ -116,7 +115,7 @@ pub async fn write_re_data() { } -pub async fn read_cfg_data(data: &Data, wipe: bool) { +pub async fn read_cfg_data(data: &Data, wipe: bool) -> Option { if !Path::new(DATA_PATH_CFG).exists() || wipe { rs_println!( "{} creating new from preset...", @@ -128,9 +127,10 @@ pub async fn read_cfg_data(data: &Data, wipe: bool) { let str_data = fs::read_to_string(DATA_PATH_CFG).unwrap(); let json_data: toml::Value = str_data.parse().unwrap(); let mut cfg_data = data.cfg.lock().await; - *cfg_data = Some(json_data); + *cfg_data = Some(json_data.clone()); - send_cmd_json("update_cfg", Some(json!([str_data])), true).await; + let r = send_cmd_json("update_cfg", Some(json!([toml::to_string(&json_data).unwrap()])), true).await; + return r; } diff --git a/src/main.rs b/src/main.rs index da0efba..99712c7 100644 --- a/src/main.rs +++ b/src/main.rs @@ -68,12 +68,10 @@ struct Args { wipe: bool, #[arg(short = 't', long, help = "Makes the program use the ASSISTANT_TOKEN_TEST env var instead of ASSISTANT_TOKEN. This env var should hold the token of a non-production bot.")] test: bool, - #[arg(long, help = "Removes the annoying ping prints.")] - noping: bool, + #[arg(long, help = "Adds annoying ping prints.")] + ping: bool, #[arg(long, help = "Makes the program not use the schedules.")] - nosched: bool, - #[arg(long, default_value = "en", help = "Which language file to use (Do not include file extensions)")] - lang: String + nosched: bool } @@ -104,12 +102,7 @@ pub static mut NOPING: bool = false; async fn main() { let args = ::parse(); let args_str = serde_json::to_string(&args).expect("Error serializing args to JSON"); - unsafe { NOPING = args.noping; } - - - rs_println!("Fetching language file..."); - data::load_lang_data(args.clone().lang); - rs_println!("[IMPORTANT] The below message is a test message, it should be written in the language you've selected\nTest message: {}", lang!("log_lang_load_success")); + unsafe { NOPING = !args.ping; } let own_env = std::env::var("ASSISTANT_OWNERS").unwrap_or("0".to_string()); let own_vec_str: Vec = own_env.split(",").map(String::from).collect(); @@ -118,8 +111,15 @@ async fn main() { .map(|s| s.parse::().expect("Failed to parse ASSISTANT_OWNERS. Invalid syntax.")) .collect(); + rs_println!("Generating and/or fetching data and config..."); let data = gen_data(args.clone(), own_vec_u64.clone()).await; + rs_println!("Fetching language file..."); + let data_binding = get_toml_mutex(&data.cfg).await.unwrap(); + let lang_cfg = data_binding["general"]["lang"].as_str().unwrap(); + data::load_lang_data(lang_cfg.to_string()); + rs_println!("[IMPORTANT] The below message is a test message, it should be written in the language you've selected\nTest message: {}", lang!("log_lang_load_success")); + if args.test { println!("----- USING TEST BOT -----"); } if args.dev { println!("----- DEV MODE ENABLED -----"); } if args.dev && args.wipe { println!("----- \"DON'T WORRY ABOUT IT\" MODE ENABLED -----"); } diff --git a/src/messages.rs b/src/messages.rs index 763fd88..5853300 100644 --- a/src/messages.rs +++ b/src/messages.rs @@ -219,6 +219,7 @@ pub fn make_post_embed(post_data: &Value, url: &str, ephemeral: bool) -> EmbedOp let desc_str = lang!( "dc_msg_embed_re_post", + post_data["post_data"]["subreddit"].as_str().unwrap(), post_data["post_data"]["upvotes"].as_i64().unwrap(), post_data["votes"]["mod_voters"].as_array().unwrap().len(), if !media_type.is_null() { media_type.as_str().unwrap() } else { "None" }, diff --git a/src/python/bot.py b/src/python/bot.py index 40ed8e1..19e48c0 100644 --- a/src/python/bot.py +++ b/src/python/bot.py @@ -1,7 +1,7 @@ from io import TextIOWrapper -import asyncpraw as praw +import asyncpraw as praw # type: ignore import os -from typing import Final +from typing import Final, Any from macros import * import toml @@ -11,11 +11,11 @@ CFG_DATA_RE: Final[str] = "reddit" class Bot: - args: dict = {"NO_RUST": True, "dev": True, "py": True, "port": 2920} - r_id: str = os.environ.get("ASSISTANT_R_ID") - secret: str = os.environ.get("ASSISTANT_R_TOKEN") - username: str = os.environ.get("ASSISTANT_R_NAME") - password: str = os.environ.get("ASSISTANT_R_PASS") + args: dict[str, Any] = {"NO_RUST": True, "dev": True, "py": True, "port": 2920} + r_id: str | None = os.environ.get("ASSISTANT_R_ID") + secret: str | None = os.environ.get("ASSISTANT_R_TOKEN") + username: str | None = os.environ.get("ASSISTANT_R_NAME") + password: str | None = os.environ.get("ASSISTANT_R_PASS") fetch_limit = 0 @@ -39,14 +39,14 @@ class Bot: ) self.sr_list: list[str] = ["bytedicetesting"] self.sr = None - self.data_f: TextIOWrapper = None - self.data: dict = {} + self.data_f: TextIOWrapper | None = None + self.data: dict[str, Any] = {} self.flairs: list[str] = [] async def initialize(self): self.sr = await self.r.subreddit("+".join(self.sr_list)) - async def set_args(self, args: dict): + async def set_args(self, args: dict[str, Any]): self.args = args async def stop(self) -> bool: @@ -59,12 +59,13 @@ class Bot: async def update_cfg_str(self, new_cfg: str) -> bool: json_cfg = toml.loads(new_cfg) - self.update_cfg(json_cfg) + await self.update_cfg(json_cfg) return True - async def update_cfg(self, new_cfg: dict) -> bool: + async def update_cfg(self, new_cfg: dict[str, Any]) -> bool: self.sr_list = new_cfg[CFG_DATA_RE]["subreddits"].split("+") self.sr = await self.r.subreddit("+".join(self.sr_list)) self.fetch_limit = new_cfg[CFG_DATA_RE]["fetch_limit"] self.flairs = new_cfg[CFG_DATA_RE]["search_flairs"] + init_lang(new_cfg["general"]["lang"]) return True \ No newline at end of file diff --git a/src/python/macros.py b/src/python/macros.py index 0248f40..61bc398 100644 --- a/src/python/macros.py +++ b/src/python/macros.py @@ -1,5 +1,6 @@ -import json import os +import json + from printColors import PrintColors @@ -8,14 +9,15 @@ G_LANG: dict[str, str] = {} DATA_PATH_LANG: str = "./data/lang/" -def py_print(*args): +def py_print(*args: str): print( PrintColors.FG.blue + "Py", "-", " ".join(args) + PrintColors.Special.reset ) -def py_error(*args): + +def py_error(*args: str): print( PrintColors.BG.red + "ERROR" + PrintColors.Special.reset, PrintColors.FG.blue + "Py", @@ -29,13 +31,13 @@ def lang(k: str) -> str: if G_LANG == {}: py_error("Language must be initialized before use!") t = G_LANG.get(k) - if k is None: py_error(f"Key not found in language \"{G_LANG_NAME}\": {k}") + if t is None: py_error(f"Key not found in language \"{G_LANG_NAME}\": {k}") return str(t) def init_lang(lang_name: str): global G_LANG, G_LANG_NAME - G_LANG_NAME = lang_name + G_LANG_NAME = lang_name # type: ignore full_path = f"{DATA_PATH_LANG}{lang_name}.json" @@ -50,4 +52,4 @@ def init_lang(lang_name: str): except json.JSONDecodeError as e: py_error(f"Failed to parse JSON for language \"{lang_name}\":\n{e}") - G_LANG = json_data \ No newline at end of file + G_LANG = json_data # type: ignore \ No newline at end of file diff --git a/src/python/main.py b/src/python/main.py index 8cf8ce3..f2a8592 100644 --- a/src/python/main.py +++ b/src/python/main.py @@ -4,12 +4,12 @@ import time from macros import * import bot as botPy -import data +import py_data import py_websocket async def main(): - sys.stdout.reconfigure(encoding="utf-8") + sys.stdout.reconfigure(encoding="utf-8") # type: ignore py_print("Creating Reddit bot...") bot = botPy.Bot() @@ -20,8 +20,10 @@ async def main(): # args is supposed to be undefined. # It gets defined in Rust. try: - await bot.set_args(args) - init_lang(lang_name) + await bot.set_args(args) # type: ignore + py_print("Fetching language file...") + init_lang(lang_name) # type: ignore + py_print(f"[IMPORTANT] The below message is a test message, it should be written in the language you've selected\nTest message: {lang('log_lang_load_success')}") except NameError: py_print("No command args or language name found from Rust. Don't worry though, we have backup in place.") init_lang("en") @@ -30,17 +32,17 @@ async def main(): py_print("ARGS:", str(bot.args)) py_print("Reading config file...") - await data.read_cfg(bot) + await py_data.read_cfg(bot) py_print("Reading Reddit data...") - rd = data.read_data(bot) + rd = py_data.read_data(bot) data_retries = 0 while not rd : data_retries += 1 time.sleep(1) py_print(f"Failed to read data: File doesn't exist yet. Retrying (#{data_retries}/5)...") - rd = data.read_data(bot) + rd = py_data.read_data(bot) if data_retries == 5 and not rd: raise Exception("Couldn't read re_data.json: File doesn't exist") diff --git a/src/python/posts.py b/src/python/posts.py index f9651a7..176fd55 100644 --- a/src/python/posts.py +++ b/src/python/posts.py @@ -1,10 +1,10 @@ import emoji -from asyncpraw import models -import asyncprawcore as prawcore -import asyncpraw.exceptions as exc +from asyncpraw import models # type: ignore +import asyncprawcore as prawcore # type: ignore +import asyncpraw.exceptions as exc # type: ignore import time -import data +import py_data import bot as botPy from macros import * @@ -46,7 +46,7 @@ async def add_new_posts(bot: botPy.Bot, max_age: int) -> bool: without_media += 1 continue - post_added = data.add_post_to_data( + post_added = py_data.add_post_to_data( bot, details ) @@ -60,7 +60,7 @@ async def add_new_posts(bot: botPy.Bot, max_age: int) -> bool: f"{not_added} are removed or already existed, " + f"and {old_posts} were older than the max age threshold.") - data.write_data(bot) + py_data.write_data(bot) return True @@ -72,6 +72,8 @@ async def fetch_posts_with_flair(bot: botPy.Bot, flair_names: list[str]) -> list f"flair:{flair_names[0]}" if len(flair_names) == 1\ else " OR ".join(f"flair:{flair}" for flair in flair_names) + if bot.sr is None: return [] + # ~36 OG-art posts per week, round limit to 50, 75 or 100 async for post in bot.sr.search(f"{flair_names_str}", sort="new", limit=bot.fetch_limit): posts.append(post) @@ -79,8 +81,8 @@ async def fetch_posts_with_flair(bot: botPy.Bot, flair_names: list[str]) -> list return posts -def has_media(post: models.Submission) -> tuple[bool, str, int, list[str]]: - media_type: str = None +def has_media(post: models.Submission) -> tuple[bool, str | None, int, list[str]]: + media_type: str | None = None media_count = 0 media_urls: list[str] = [] @@ -109,7 +111,7 @@ def has_media(post: models.Submission) -> tuple[bool, str, int, list[str]]: return (media_type != None, media_type, media_count, media_urls) -async def from_url(bot: botPy.Bot, url: str) -> tuple[bool, models.Submission]: +async def from_url(bot: botPy.Bot, url: str) -> tuple[bool, models.Submission | None]: try: post: models.Submission = await bot.r.submission(url=url) return True, post @@ -120,11 +122,12 @@ async def from_url(bot: botPy.Bot, url: str) -> tuple[bool, models.Submission]: return False, None -def get_post_details(post: models.Submission, added_by_h: bool = False) -> data.PostData: +def get_post_details(post: models.Submission, added_by_h: bool = False) -> py_data.PostData: media = has_media(post) - return data.PostData( + return py_data.PostData( post.shortlink, + post.subreddit, post.title, post.score, int(post.created_utc), @@ -135,12 +138,12 @@ def get_post_details(post: models.Submission, added_by_h: bool = False) -> data. ) -async def add_post_url(bot, url: str, approve: bool = False, added_by_h: bool = False) -> bool: +async def add_post_url(bot: botPy.Bot, url: str, approve: bool = False, added_by_h: bool = False) -> bool: result, post = await from_url(bot, url) - if not result: - return False - + if not result: return False + if post is None: return False + post_data = get_post_details(post, added_by_h) post_data.approved_by_human = approve - return data.add_post_to_data(bot, post_data, True) \ No newline at end of file + return py_data.add_post_to_data(bot, post_data, True) \ No newline at end of file diff --git a/src/python/cmds.py b/src/python/py_cmds.py similarity index 96% rename from src/python/cmds.py rename to src/python/py_cmds.py index 6727792..4a0df57 100644 --- a/src/python/cmds.py +++ b/src/python/py_cmds.py @@ -1,4 +1,4 @@ -import asyncpraw.models as models +import asyncpraw.models as models # type: ignore from macros import * import bot as botPy diff --git a/src/python/data.py b/src/python/py_data.py similarity index 93% rename from src/python/data.py rename to src/python/py_data.py index 19767ab..3bbe78d 100644 --- a/src/python/data.py +++ b/src/python/py_data.py @@ -2,6 +2,7 @@ import os import toml import json import time +from typing import Any import bot as botPy from macros import * @@ -17,10 +18,11 @@ class PostData: def __init__( self, url: str, + subreddit: str, title: str, upvotes: int, date_unix: int, - media_type: str, + media_type: str | None, media_urls: list[str], removed: bool = False, removed_by: str | None = None, @@ -33,6 +35,7 @@ class PostData: approved_by_human: bool = False, approved_by_ris: bool = False ): + self.subreddit = subreddit self.removed = removed self.removed_by = removed_by self.removed_reason = removed_reason @@ -50,7 +53,7 @@ class PostData: self.approved_by_human = approved_by_human self.approved_by_ris = approved_by_ris - def to_json(self): + def to_json(self) -> dict[str, Any]: return { "removed": { "removed": self.removed, @@ -58,6 +61,7 @@ class PostData: "reason": self.removed_reason }, "post_data": { + "subreddit": self.subreddit, "title": self.title, "upvotes": self.upvotes, "date_unix": self.date_unix, @@ -92,10 +96,9 @@ def read_data(bot: botPy.Bot) -> bool: py_print("re_data.json not found, creating new from preset...") with open(os.path.join(DEFAULT_PATH, "re_data_preset.json"), "r") as f: - data_preset_json = json.load(f) + data_preset_json: dict[str, Any] = json.load(f) data_preset_json[botPy.RE_DATA_POSTS].pop("EXAMPLE VALUE", None) - data_preset_json[botPy.RE_DATA_POSTS].pop("EXAMPLE VALUE DELETED", None) with open(r_path, "w") as f: json.dump(data_preset_json, f, indent = 2) @@ -110,6 +113,8 @@ def read_data(bot: botPy.Bot) -> bool: def write_data(bot: botPy.Bot) -> bool: + if bot.data_f is None: return False + bot.data_f.seek(0) json.dump(bot.data, bot.data_f, indent=2) bot.data_f.truncate() @@ -128,7 +133,7 @@ async def read_cfg(bot: botPy.Bot) -> bool: data_preset_json = toml.load(f) with open(r_path, "w") as f: - toml.dump(data_preset_json, f, indent = 2) + toml.dump(data_preset_json, f, indent = 2) # type: ignore bot.data_f = open(r_path, "r+") @@ -224,12 +229,12 @@ def set_vote_post( if remove_vote: if user not in target_voters: return False - target_voters.remove(user) + target_voters.remove(user) # type: ignore else: if user in target_voters: return False - target_voters.add(user) + target_voters.add(user) # type: ignore bot.data[botPy.RE_DATA_POSTS][url]["votes"]["voters_re"] = list(re_voters) bot.data[botPy.RE_DATA_POSTS][url]["votes"]["voters_dc"] = list(dc_voters) diff --git a/src/python/py_websocket.py b/src/python/py_websocket.py index 94b9e70..dbe547c 100644 --- a/src/python/py_websocket.py +++ b/src/python/py_websocket.py @@ -1,13 +1,13 @@ import websockets import asyncio import json +from typing import Any from macros import * import bot as botPy -import data +import py_data import posts -import cmds -import macros +import py_cmds ws_global = None is_connected = False @@ -36,9 +36,10 @@ async def websocket_client(bot: botPy.Bot): while True: response = await ws.recv() - if not response.startswith("json:"): + str_response = str(response) + if not str_response.startswith("json:"): # type: ignore py_print(f"Received from Rust: {response}") - await parse_json(response, bot) + await parse_json(str_response, bot) async def parse_json(response: str, bot: botPy.Bot): @@ -50,7 +51,7 @@ async def parse_json(response: str, bot: botPy.Bot): if json_response["print"]: py_print(f"Received from Rust: {response}") result = await json_to_func(json_response, bot) - await ws_global.ping() + if ws_global is not None: await ws_global.ping() await send_message(f"json:{json.dumps(result)}") except json.JSONDecodeError as e: if bot.args["dev"]: py_print(f"failed to parse json: {json_str}\n reason: {e}") @@ -62,29 +63,29 @@ def run_thread(bot: botPy.Bot): loop.run_until_complete(websocket_client(bot)) -async def json_to_func(v: dict, bot: botPy.Bot) -> dict: - if "type" not in v or "value" not in v or not isinstance(v, dict): +async def json_to_func(v: dict[str, Any], bot: botPy.Bot) -> dict[str, Any]: + if "type" not in v or "value" not in v: if bot.args["dev"]: py_print("JSON is not a dictionary or does not include \"type\" and \"value\" keys.") - return + return result_json(False, True) if v["type"] != "function": v_type = v["type"] if bot.args["dev"]: py_print(f"Type \"{v_type}\" is not supported.") - return + return result_json(False, True) value_supported = True r = False match v["value"]: - case "update_data_file": r = data .write_data (bot) - case "respond_mentions": r = await cmds .respond_to_mention(bot) - case "add_new_posts": r = await posts .add_new_posts (bot, *v["args"]) - case "add_post_url": r = await posts .add_post_url (bot, *v["args"]) - case "remove_post_url": r = data .remove_post (bot, *v["args"]) - case "set_approve_post": r = data .set_approve_post (bot, *v["args"]) - case "set_vote_post": r = data .set_vote_post (bot, *v["args"]) - case "remove_old_posts": r = data .remove_old_posts (bot, *v["args"]) - case "update_cfg": r = await bot .update_cfg_str (*v["args"]) - case "stop_praw": r = await bot .stop () + case "update_data_file": r = py_data.write_data (bot) + case "respond_mentions": r = await py_cmds.respond_to_mention(bot) + case "add_new_posts": r = await posts .add_new_posts (bot, *v["args"]) + case "add_post_url": r = await posts .add_post_url (bot, *v["args"]) + case "remove_post_url": r = py_data.remove_post (bot, *v["args"]) + case "set_approve_post": r = py_data.set_approve_post (bot, *v["args"]) + case "set_vote_post": r = py_data.set_vote_post (bot, *v["args"]) + case "remove_old_posts": r = py_data.remove_old_posts (bot, *v["args"]) + case "update_cfg": r = await bot .update_cfg_str (*v["args"]) + case "stop_praw": r = await bot .stop () case _: value_supported = False print_result = v["print"] @@ -97,5 +98,5 @@ async def json_to_func(v: dict, bot: botPy.Bot) -> dict: return result_json(r, print_result) -def result_json(bool: bool, print_result: bool) -> dict: +def result_json(bool: bool, print_result: bool) -> dict[str, Any]: return {"type": "result", "value": bool, "print": print_result} \ No newline at end of file diff --git a/src/websocket.rs b/src/websocket.rs index 3b2a0f4..fe8826c 100644 --- a/src/websocket.rs +++ b/src/websocket.rs @@ -152,12 +152,12 @@ async fn handle_message(msg: tungstenite::protocol::Message, args: Args, owners: } } tungstenite::Message::Binary(bytes) => { - if args.dev && !args.noping { + if args.dev && args.ping { rs_println!("[Binary] from Python: {:?}", bytes); } } _ => { - if args.dev && !args.noping { + if args.dev && args.ping { rs_println!("Received from Python: [UNKNOWN / OTHER]"); } } From d720dc143c59f63407b2f02884be0d70295545fe Mon Sep 17 00:00:00 2001 From: ByteDice Date: Thu, 26 Jun 2025 11:28:54 +0200 Subject: [PATCH 26/39] added command aliases for Reddit --- data/defaults/cfg_default.toml | 3 +-- src/python/bot.py | 6 ++++-- src/python/py_cmds.py | 9 ++++++++- 3 files changed, 13 insertions(+), 5 deletions(-) diff --git a/data/defaults/cfg_default.toml b/data/defaults/cfg_default.toml index 8be26b9..c26cd7b 100644 --- a/data/defaults/cfg_default.toml +++ b/data/defaults/cfg_default.toml @@ -20,9 +20,8 @@ fetch_limit = 100 search_flairs = ["Original Art"] # Command aliases for the Reddit bot, since those aren't autocomplete-able. -# TODO: this [reddit.aliases] -add_post = [] +add_post = ["weekly_art", "weekly"] [commands] diff --git a/src/python/bot.py b/src/python/bot.py index 19e48c0..0f79879 100644 --- a/src/python/bot.py +++ b/src/python/bot.py @@ -42,6 +42,7 @@ class Bot: self.data_f: TextIOWrapper | None = None self.data: dict[str, Any] = {} self.flairs: list[str] = [] + self.aliases: dict[str, list[str]] = {} async def initialize(self): self.sr = await self.r.subreddit("+".join(self.sr_list)) @@ -63,9 +64,10 @@ class Bot: return True async def update_cfg(self, new_cfg: dict[str, Any]) -> bool: - self.sr_list = new_cfg[CFG_DATA_RE]["subreddits"].split("+") self.sr = await self.r.subreddit("+".join(self.sr_list)) + self.sr_list = new_cfg[CFG_DATA_RE]["subreddits"].split("+") self.fetch_limit = new_cfg[CFG_DATA_RE]["fetch_limit"] - self.flairs = new_cfg[CFG_DATA_RE]["search_flairs"] + self.flairs = new_cfg[CFG_DATA_RE]["search_flairs"] + self.aliases = new_cfg[CFG_DATA_RE]["aliases"] init_lang(new_cfg["general"]["lang"]) return True \ No newline at end of file diff --git a/src/python/py_cmds.py b/src/python/py_cmds.py index 4a0df57..d087396 100644 --- a/src/python/py_cmds.py +++ b/src/python/py_cmds.py @@ -11,7 +11,14 @@ async def make_cmd(cmd: str, bot: botPy.Bot) -> str: async def is_cmd(cmd: str, text: str, bot: botPy.Bot) -> bool: command: str = await make_cmd(cmd, bot) - return command.lower() in text.lower() + + if command.lower() in text.lower(): return True + + for alias in bot.aliases[cmd]: + a_cmd = await make_cmd(alias, bot) + if a_cmd.lower() in text.lower(): return True + + return False async def respond_to_mention(bot: botPy.Bot) -> bool: From e35870c57dd20e9aa44f30d8e9c09906e163cfce Mon Sep 17 00:00:00 2001 From: Byte Dice <165556900+ByteDice@users.noreply.github.com> Date: Thu, 26 Jun 2025 12:34:29 +0200 Subject: [PATCH 27/39] Update README.md --- README.md | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index 430a66f..1728878 100644 --- a/README.md +++ b/README.md @@ -42,7 +42,7 @@ An automation tool primarily made for myself (Byte Dice) but publicly available ### Dependencies: This program uses Rust (v1.82.0) and Python (v3.11.4), you can likely use other versions if they are compatible.\ -It is required to install all used Python modules. You can find those in [req.txt](/req.txt). Installation instructions are in the *How to Run* section. +It is required to install all used Python modules. You can find those in [req.txt](/req.txt). Installation instructions are in the *How to start the program* section. ### Environment variables: | **Name** | **Description** | @@ -85,19 +85,16 @@ You can find config files in the [cfg/](cfg/) folder. You can also find the defa * Set the environment variables (listed above). * On Windows: * Run `setx VARIABLE_NAME "value in quotes"` in a terminal. - * On Linux: * Run `sudo nano /etc/environment` or `sudo vim /etc/environment` in the terminal (and enter your password if prompted to). * Press `i` (only if you used VIM). * Write `VARIABLE_NAME="value"` + a new line for every variable. * if nano: `ctrl + O` (and press enter) then `ctrl + X`. * if VIM: press `esc` then write `:wq` (and press enter). - * On macOS: * Probably the same as Linux, but I don't use this OS so I have no clue. - * Restart the terminal if you added/changed any environment variables. * Run `cd path/to/extracted/folder` to navigate to the downloaded files (replace `path/to/extracted/folder` with your actual path). * Install all python modules with `pip install -r req.txt`. * Run `cargo run` to start the program. You can view a list of options by running `cargo run -- --help` or `cargo run -- -h`. - * If you only want to run the Python code, you can either run `cargo run -- --py` or `python ./src/python/main.py`. The second option is recommended for better error output. \ No newline at end of file + * If you only want to run the Python code, you can either run `cargo run -- --py` or `python ./src/python/main.py`. The second option is recommended for better error output. From 8f90ea74d4599b031f8a9a29c932530b8c7ff6bb Mon Sep 17 00:00:00 2001 From: Byte Dice Date: Fri, 27 Jun 2025 16:09:27 +0200 Subject: [PATCH 28/39] fixed a typo --- src/python/posts.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/python/posts.py b/src/python/posts.py index 176fd55..eb17555 100644 --- a/src/python/posts.py +++ b/src/python/posts.py @@ -127,7 +127,7 @@ def get_post_details(post: models.Submission, added_by_h: bool = False) -> py_da return py_data.PostData( post.shortlink, - post.subreddit, + post.subreddit.display_name, post.title, post.score, int(post.created_utc), From 6ff9cafc3495084a15f1ca0b3d3f3cc2c205848f Mon Sep 17 00:00:00 2001 From: Byte Dice Date: Fri, 27 Jun 2025 16:46:32 +0200 Subject: [PATCH 29/39] fixed bugs when not using shorturl --- src/data.rs | 7 +++++-- src/macros.rs | 25 +++++++++++++++++++++++++ src/python/main.py | 6 ++++-- src/python/py_data.py | 2 +- src/re_cmds/add.rs | 7 +++---- src/re_cmds/approve.rs | 7 +++++-- src/re_cmds/get.rs | 9 ++++++--- src/re_cmds/remove.rs | 15 +++++++++------ src/re_cmds/vote.rs | 15 +++++++++------ 9 files changed, 67 insertions(+), 26 deletions(-) diff --git a/src/data.rs b/src/data.rs index 5ae6eda..b3c8b9c 100644 --- a/src/data.rs +++ b/src/data.rs @@ -4,7 +4,7 @@ use std::path::Path; use serde_json::{self, Value, json}; use tokio::sync::Mutex; -use crate::{errln, rs_println, Data, Error, CFG_DATA_RE, LANG, LANG_NAME}; +use crate::{errln, rs_println, rs_warnln, Data, Error, CFG_DATA_RE, LANG, LANG_NAME}; use crate::websocket::send_cmd_json; @@ -94,7 +94,10 @@ fn generate_re_data() { let mut preset_json: Value = serde_json::from_str(&preset_str).unwrap(); if let Some(bk_week) = preset_json[CFG_DATA_RE].as_object_mut() { - bk_week.remove("EXAMPLE VALUE"); + bk_week.remove("EXAMPLE URL"); + } + else { + rs_warnln!("Couldn't find key \"{}\" in the Reddit data file ({})!", CFG_DATA_RE, DATA_PATH_RE); } let json_str = serde_json::to_string_pretty(&preset_json).unwrap(); diff --git a/src/macros.rs b/src/macros.rs index 6873725..b7ec8cd 100644 --- a/src/macros.rs +++ b/src/macros.rs @@ -24,6 +24,19 @@ macro_rules! rs_errln { } +#[macro_export] +macro_rules! rs_warnln { + ($($arg:tt)*) => { + println!("{}WARNING{} RS - {}{}", + "\x1b[33m", + "\x1b[0m\x1b[31m", + format!($($arg)*), + "\x1b[0m" + ); + }; +} + + #[macro_export] macro_rules! errln { ($($arg:tt)*) => { @@ -37,6 +50,18 @@ macro_rules! errln { } +#[macro_export] +macro_rules! warnln { + ($($arg:tt)*) => { + println!("{}WARNING{} - {}", + "\x1b[33m", + "\x1b[0m", + format!($($arg)*) + ); + }; +} + + #[macro_export] macro_rules! lang { ($key:expr) => { diff --git a/src/python/main.py b/src/python/main.py index f2a8592..1504289 100644 --- a/src/python/main.py +++ b/src/python/main.py @@ -58,5 +58,7 @@ async def main(): await bot.stop() - -asyncio.run(main()) \ No newline at end of file +try: + asyncio.run(main()) +except KeyboardInterrupt: + raise SystemExit \ No newline at end of file diff --git a/src/python/py_data.py b/src/python/py_data.py index 3bbe78d..0887999 100644 --- a/src/python/py_data.py +++ b/src/python/py_data.py @@ -98,7 +98,7 @@ def read_data(bot: botPy.Bot) -> bool: with open(os.path.join(DEFAULT_PATH, "re_data_preset.json"), "r") as f: data_preset_json: dict[str, Any] = json.load(f) - data_preset_json[botPy.RE_DATA_POSTS].pop("EXAMPLE VALUE", None) + data_preset_json[botPy.RE_DATA_POSTS].pop("EXAMPLE URL", None) with open(r_path, "w") as f: json.dump(data_preset_json, f, indent = 2) diff --git a/src/re_cmds/add.rs b/src/re_cmds/add.rs index ebe2cc3..1a56ab7 100644 --- a/src/re_cmds/add.rs +++ b/src/re_cmds/add.rs @@ -1,6 +1,6 @@ use serde_json::json; -use crate::data::{get_mutex_data, update_re_data}; +use crate::data::{get_mutex_data}; use crate::messages::send_msg; use crate::re_cmds::get::get_post_from_data; use crate::{data, websocket::send_cmd_json, Context, Error, CFG_DATA_RE}; @@ -37,7 +37,7 @@ pub async fn cmd( send_msg( ctx, r#"Unknown error! - Error trace: `bk_week_cmds.rs -> bk_week_add() -> Unknown error`. + Error trace: `re_cmds/add.rs -> cmd() -> Unknown error`. Common reasons: The URL provided was likely invalid or 403: forbidden (e.g a private subreddit)."#.to_string(), true, true @@ -55,8 +55,7 @@ pub async fn cmd( if a { send_msg(ctx, lang!("dc_msg_re_also_approved"), true, true).await; } } - update_re_data(ctx.data()).await; - if let Some(post) = get_post_from_data(ctx, &reddit_data, &url).await? { + if let Some(post) = get_post_from_data(ctx, &reddit_data, &shorturl).await? { send_embed_for_post(ctx, post, &url).await?; } diff --git a/src/re_cmds/approve.rs b/src/re_cmds/approve.rs index 1e52fba..832992f 100644 --- a/src/re_cmds/approve.rs +++ b/src/re_cmds/approve.rs @@ -1,6 +1,6 @@ use serde_json::{json, Value}; -use crate::{data::{self, get_mutex_data}, lang, messages::send_msg, re_cmds::generic_fns::is_bk_mod_msg, websocket, Context, Error, CFG_DATA_RE}; +use crate::{data::{self, get_mutex_data}, lang, messages::send_msg, re_cmds::generic_fns::{is_bk_mod_msg, to_shorturl}, websocket, Context, Error, CFG_DATA_RE}; use super::generic_fns::send_embed_for_removed; @@ -20,10 +20,13 @@ pub async fn cmd( { if !is_bk_mod_msg(ctx).await { return Ok(()); } + let shorturl_u = to_shorturl(&url); + let shorturl = &shorturl_u.unwrap_or(url.clone()); + data::update_re_data(ctx.data()).await; let reddit_data = get_mutex_data(&ctx.data().reddit_data).await?; - approve_cmd(ctx, &url, &reddit_data, !disapprove.unwrap_or(false)).await; + approve_cmd(ctx, &shorturl, &reddit_data, !disapprove.unwrap_or(false)).await; return Ok(()); } diff --git a/src/re_cmds/get.rs b/src/re_cmds/get.rs index ad5a869..c1d10e4 100644 --- a/src/re_cmds/get.rs +++ b/src/re_cmds/get.rs @@ -1,6 +1,6 @@ use serde_json::Value; -use crate::{data::{self, get_mutex_data}, lang, messages::send_msg, re_cmds::generic_fns::send_embed_for_post, rs_println, Context, Error, CFG_DATA_RE}; +use crate::{data::{self, get_mutex_data}, lang, messages::send_msg, re_cmds::generic_fns::{send_embed_for_post, to_shorturl}, rs_println, Context, Error, CFG_DATA_RE}; use super::generic_fns::send_embed_for_removed; @@ -19,10 +19,13 @@ pub async fn cmd( { data::update_re_data(ctx.data()).await; + let shorturl_u = to_shorturl(&url); + let shorturl = &shorturl_u.unwrap_or(url.clone()); + let reddit_data = get_mutex_data(&ctx.data().reddit_data).await?; - if let Some(post) = get_post_from_data(ctx, &reddit_data, &url).await? { - send_embed_for_post(ctx, post, &url).await?; + if let Some(post) = get_post_from_data(ctx, &reddit_data, &shorturl).await? { + send_embed_for_post(ctx, post, &shorturl).await?; } return Ok(()); diff --git a/src/re_cmds/remove.rs b/src/re_cmds/remove.rs index d1819f4..c4bec26 100644 --- a/src/re_cmds/remove.rs +++ b/src/re_cmds/remove.rs @@ -1,6 +1,6 @@ use serde_json::json; -use crate::{data::{self, get_mutex_data}, lang, messages::send_msg, re_cmds::{generic_fns::{is_bk_mod_msg, send_embed_for_removed}, get::get_post_from_data}, websocket::send_cmd_json, Context, Error}; +use crate::{data::{self, get_mutex_data}, lang, messages::send_msg, re_cmds::{generic_fns::{is_bk_mod_msg, send_embed_for_removed, to_shorturl}, get::get_post_from_data}, websocket::send_cmd_json, Context, Error}; #[poise::command( slash_command, @@ -18,27 +18,30 @@ pub async fn cmd( { if !is_bk_mod_msg(ctx).await { return Ok(()); } + let shorturl_u = to_shorturl(&url); + let shorturl = &shorturl_u.unwrap_or(url.clone()); + let auth = &ctx.author().name; - let r = send_cmd_json("remove_post_url", Some(json!([&url, &auth, &reason])), true).await.unwrap(); + let r = send_cmd_json("remove_post_url", Some(json!([&shorturl, &auth, &reason])), true).await.unwrap(); if r["value"].as_bool().unwrap() { send_msg( ctx, - lang!("dc_msg_re_post_remove_success", &url), + lang!("dc_msg_re_post_remove_success", &shorturl), true, true ).await; } else { - send_msg(ctx, lang!("dc_msg_re_post_404"), false, false).await; + send_msg(ctx, lang!("dc_msg_re_post_404"), true, true).await; } data::update_re_data(ctx.data()).await; let reddit_data = get_mutex_data(&ctx.data().reddit_data).await?; - if let Some(post) = get_post_from_data(ctx, &reddit_data, &url).await? { + if let Some(post) = get_post_from_data(ctx, &reddit_data, &shorturl).await? { if post["removed"]["removed"].as_bool().unwrap() { - send_embed_for_removed(ctx, &url, &post).await; + send_embed_for_removed(ctx, &shorturl, &post).await; } } diff --git a/src/re_cmds/vote.rs b/src/re_cmds/vote.rs index a027084..4e3b4dd 100644 --- a/src/re_cmds/vote.rs +++ b/src/re_cmds/vote.rs @@ -1,6 +1,6 @@ use serde_json::json; -use crate::{data::{self, get_mutex_data}, lang, messages::send_msg, re_cmds::generic_fns::{is_bk_mod, send_embed_for_removed}, websocket::send_cmd_json, Context, Error, CFG_DATA_RE}; +use crate::{data::{self, get_mutex_data}, lang, messages::send_msg, re_cmds::generic_fns::{is_bk_mod, send_embed_for_removed, to_shorturl}, websocket::send_cmd_json, Context, Error, CFG_DATA_RE}; #[poise::command( slash_command, @@ -21,17 +21,20 @@ pub async fn cmd( let re_data = get_mutex_data(&ctx.data().reddit_data).await?; let post_data = re_data[CFG_DATA_RE].clone(); let unw_vote = un_vote.unwrap_or(false); + + let shorturl_u = to_shorturl(&url); + let shorturl = &shorturl_u.unwrap_or(url.clone()); - if post_data.get(&url).is_none() { + if post_data.get(&shorturl).is_none() { send_msg(ctx, lang!("dc_msg_re_post_404"), false, false).await; return Ok(()); } - if post_data[&url]["removed"]["removed"].as_bool().unwrap() { - send_embed_for_removed(ctx, &url, &post_data[&url]).await; + if post_data[&shorturl]["removed"]["removed"].as_bool().unwrap() { + send_embed_for_removed(ctx, &shorturl, &post_data[&shorturl]).await; return Ok(()); } - let url_data = &post_data[&url]; + let url_data = &post_data[&shorturl]; let is_mod = is_bk_mod(ctx.data().bk_mods.clone(), ctx.author().id.get()); let voters_dc = url_data["votes"]["voters_dc"].as_array().unwrap(); @@ -47,7 +50,7 @@ pub async fn cmd( return Ok(()); } - let r = send_cmd_json("set_vote_post", Some(json!([url, uid, is_mod, true, unw_vote])), true).await.unwrap(); + let r = send_cmd_json("set_vote_post", Some(json!([shorturl, uid, is_mod, true, unw_vote])), true).await.unwrap(); let unw_r = r["value"].as_bool().unwrap(); if unw_r && !unw_vote && is_mod { From 4dc878f1c47fb466495f217dc42aa73748fdd071 Mon Sep 17 00:00:00 2001 From: Byte Dice Date: Sat, 28 Jun 2025 18:29:25 +0200 Subject: [PATCH 30/39] hopefully fixed a bug --- src/re_cmds/add.rs | 30 +++++++++++++++--------------- 1 file changed, 15 insertions(+), 15 deletions(-) diff --git a/src/re_cmds/add.rs b/src/re_cmds/add.rs index 1a56ab7..cc44f36 100644 --- a/src/re_cmds/add.rs +++ b/src/re_cmds/add.rs @@ -26,25 +26,25 @@ pub async fn cmd( let shorturl_u = to_shorturl(&url); let shorturl = &shorturl_u.unwrap_or(url.clone()); + let a = approve.unwrap_or(false); + let r = send_cmd_json("add_post_url", Some(json!([&shorturl, a, true])), true).await.unwrap(); + + if !r["value"].as_bool().unwrap() { + send_msg( + ctx, + r#"Unknown error! + Error trace: `re_cmds/add.rs -> cmd() -> Unknown error`. + Common reasons: The URL provided was likely invalid or 403: forbidden (e.g a private subreddit)."#.to_string(), + true, + true + ).await; + return Ok(()); + } + data::update_re_data(ctx.data()).await; let reddit_data = get_mutex_data(&ctx.data().reddit_data).await?; if let Some(bk_week) = reddit_data.get(CFG_DATA_RE) { - let a = approve.unwrap_or(false); - let r = send_cmd_json("add_post_url", Some(json!([&shorturl, a, true])), true).await.unwrap(); - - if !r["value"].as_bool().unwrap() { - send_msg( - ctx, - r#"Unknown error! - Error trace: `re_cmds/add.rs -> cmd() -> Unknown error`. - Common reasons: The URL provided was likely invalid or 403: forbidden (e.g a private subreddit)."#.to_string(), - true, - true - ).await; - return Ok(()); - } - if let Some(post) = bk_week.get(shorturl) { if post["removed"]["removed"].as_bool().unwrap() { send_msg(ctx, lang!("dc_msg_re_post_unremove_success", &shorturl), true, true).await; } From 1a3a981a992814a99e6bb35973f385e17dd8378c Mon Sep 17 00:00:00 2001 From: Byte Dice Date: Sat, 28 Jun 2025 19:17:38 +0200 Subject: [PATCH 31/39] Added disclaimer to cfg --- data/defaults/cfg_default.toml | 1 + 1 file changed, 1 insertion(+) diff --git a/data/defaults/cfg_default.toml b/data/defaults/cfg_default.toml index c26cd7b..2b3d08c 100644 --- a/data/defaults/cfg_default.toml +++ b/data/defaults/cfg_default.toml @@ -10,6 +10,7 @@ status = "🎲 https://bytedice.net" # Which subreddits the bot will scan when executing "re"-category commands. # Is automatically disabled when `disabled_categories` includes "re". # Separate multiple subreddits with a "+", e.g "memes+askreddit". +# [REQUIRES RESTART] subreddits = "bytedicetesting" # How many posts the bot scans when running `/re_updatediscord`. From aad562cb2dd4543b21c942913c410e7c3f6eef07 Mon Sep 17 00:00:00 2001 From: ByteDice Date: Tue, 8 Jul 2025 12:30:35 +0200 Subject: [PATCH 32/39] removed ability to change subreddit without restart (cuz broken or something) --- src/python/bot.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/python/bot.py b/src/python/bot.py index 0f79879..cb34ff9 100644 --- a/src/python/bot.py +++ b/src/python/bot.py @@ -64,8 +64,6 @@ class Bot: return True async def update_cfg(self, new_cfg: dict[str, Any]) -> bool: - self.sr = await self.r.subreddit("+".join(self.sr_list)) - self.sr_list = new_cfg[CFG_DATA_RE]["subreddits"].split("+") self.fetch_limit = new_cfg[CFG_DATA_RE]["fetch_limit"] self.flairs = new_cfg[CFG_DATA_RE]["search_flairs"] self.aliases = new_cfg[CFG_DATA_RE]["aliases"] From 358b16249efb08cc707898c4bf8112f847b7dd16 Mon Sep 17 00:00:00 2001 From: Byte Dice Date: Sat, 12 Jul 2025 12:04:53 +0200 Subject: [PATCH 33/39] added a WhoAmI command --- bk_week_help_re.md | 16 +++++++--------- data/lang/en.json | 1 + src/cmds/help.rs | 8 ++++---- src/cmds/whoami.rs | 20 ++++++++++++++++++++ src/gen.rs | 1 + src/main.rs | 1 + 6 files changed, 34 insertions(+), 13 deletions(-) create mode 100644 src/cmds/whoami.rs diff --git a/bk_week_help_re.md b/bk_week_help_re.md index ae4c77e..3f7a834 100644 --- a/bk_week_help_re.md +++ b/bk_week_help_re.md @@ -1,12 +1,10 @@ # Reddit Commands -To execute a command on the Reddit bot, include `u/ByteDiceAssistant [cmd]` in a comment. -- **`[cmd]`**: The command you want to run and its arguments. -## `bk_week_add` -Adds the post to the list of posts. +To execute a command on the Reddit bot, include `u/{BOTNAME} [cmd]` in a comment. (Replace {BOTNAME} with the actual bot name, and [cmd] with any command listed below). + +## `add_post` +Adds a post to the bots database. - **Only moderators of a subreddit or the OP (Original Poster) can use this command.** ### **Examples** -``` -"u/ByteDiceAssistant bk_week_add" -"Cool art, let me add that. u/ByteDiceAssistant bk_week_add" -"Cool art. Just gonna u/ByteDiceAssistant bk_week_add so it can become featured." -``` \ No newline at end of file +* `u/{BOTNAME} add_post` +* `Cool art, let me add that. u/{BOTNAME} weekly_art` (weekly art is an alias for add_post that is enabled by default) +* `Cool art. Just gonna u/{BOTNAME} add_post so it can become featured.` \ No newline at end of file diff --git a/data/lang/en.json b/data/lang/en.json index 27e38b4..e7f6363 100644 --- a/data/lang/en.json +++ b/data/lang/en.json @@ -55,6 +55,7 @@ "dc_msg_update_removing_dupe": "{0}Removing duplicate posts...", "dc_msg_update_removing_old": "{0}Removing old posts (threshold: {1}d)...", "dc_msg_update_removing": "{0}Removing removed posts...", + "dc_msg_whoami": "**Bot \"owner\":** {0}\n**BK moderator:** {1}", "log_lang_load_success": "Successfully loaded the english language file!", "none": "None", "py_re_response_suffix": "^(I am not an AI, I am just a bot. This action was performed automatically by the way. You can report bugs and view my source code [here](https://github.com/ByteDice/ByteDiceAssistant)!)", diff --git a/src/cmds/help.rs b/src/cmds/help.rs index 2f93a57..7f00dfd 100644 --- a/src/cmds/help.rs +++ b/src/cmds/help.rs @@ -8,8 +8,8 @@ use crate::{lang, messages::send_msg, Context, Cmd, Error}; enum HelpOptions { Admin, All, - BkWeek, - BkWeekReddit, + Reddit, + RedditBot, Generic } @@ -97,8 +97,8 @@ async fn send_single_help(ctx: Context<'_>, mut cmd_name: String) { async fn send_category_help(ctx: Context<'_>, category: HelpOptions) { match category { - HelpOptions::BkWeekReddit => send_bk_week_help_re(ctx).await, - HelpOptions::BkWeek => send_bk_week_help (ctx).await, + HelpOptions::RedditBot => send_bk_week_help_re(ctx).await, + HelpOptions::Reddit => send_bk_week_help (ctx).await, HelpOptions::Generic => send_generic_help (ctx).await, HelpOptions::Admin => send_admin_help (ctx).await, HelpOptions::All => send_all_help (ctx).await diff --git a/src/cmds/whoami.rs b/src/cmds/whoami.rs new file mode 100644 index 0000000..0823104 --- /dev/null +++ b/src/cmds/whoami.rs @@ -0,0 +1,20 @@ +use crate::{lang, messages::send_msg, Context, Error}; + +#[poise::command( + slash_command, + prefix_command, + rename = "whoami", + category = "help", + required_bot_permissions = "SEND_MESSAGES | VIEW_CHANNEL" +)] +pub async fn cmd(ctx: Context<'_>) -> Result<(), Error> { + let data = ctx.data(); + let uid: u64 = ctx.author().id.into(); + + let is_owner = data.owners .contains(&uid); + let is_bk_mod = data.bk_mods.contains(&uid); + + send_msg(ctx, lang!("dc_msg_whoami", is_owner, is_bk_mod), true, true).await; + + return Ok(()); +} \ No newline at end of file diff --git a/src/gen.rs b/src/gen.rs index 00bcf49..eb3e2c7 100644 --- a/src/gen.rs +++ b/src/gen.rs @@ -79,6 +79,7 @@ async fn make_cmd_vec(data: &Data) -> Vec { let mut cmds = vec![ // GENERIC cmds::help::cmd(), + cmds::whoami::cmd(), cmds::ping::cmd(), cmds::eight_ball::cmd(), // REDDIT diff --git a/src/main.rs b/src/main.rs index 99712c7..588653b 100644 --- a/src/main.rs +++ b/src/main.rs @@ -10,6 +10,7 @@ mod cmds { pub mod reload_cfg; pub mod send; pub mod stop; + pub mod whoami; } mod re_cmds { pub mod add; From 613075463624ca90692fc54222aaa5b8a912bb0f Mon Sep 17 00:00:00 2001 From: Byte Dice Date: Sun, 13 Jul 2025 13:42:15 +0200 Subject: [PATCH 34/39] update french language --- data/lang/gpt_fr.json | 1 + 1 file changed, 1 insertion(+) diff --git a/data/lang/gpt_fr.json b/data/lang/gpt_fr.json index 4086afd..d36e78d 100644 --- a/data/lang/gpt_fr.json +++ b/data/lang/gpt_fr.json @@ -55,6 +55,7 @@ "dc_msg_update_removing_dupe": "{0}Suppression des posts en double...", "dc_msg_update_removing_old": "{0}Suppression des anciens posts (seuil : {1}j)...", "dc_msg_update_removing": "{0}Suppression des posts supprimés...", + "dc_msg_whoami": "**Propriétaire du bot :** {0}\n**Modérateur BK :** {1}", "log_lang_load_success": "Fichier de langue français chargé avec succès !", "none": "Aucun", "py_re_response_suffix": "^(Je ne suis pas une IA, juste un bot. Cette action a été effectuée automatiquement. Vous pouvez signaler des bugs et consulter mon code source [ici](https://github.com/ByteDice/ByteDiceAssistant) !)", From f1f52aa3aa848be09abd55f3264e841e392bb64a Mon Sep 17 00:00:00 2001 From: Byte Dice Date: Thu, 7 Aug 2025 17:56:51 +0200 Subject: [PATCH 35/39] massively reduced the size of the data section in the discord embeds --- cfg/8-ball_quirky.txt | 6 +++--- data/lang/en.json | 2 +- data/lang/gpt_fr.json | 2 +- src/messages.rs | 22 ++++++++++++++++++++-- 4 files changed, 25 insertions(+), 7 deletions(-) diff --git a/cfg/8-ball_quirky.txt b/cfg/8-ball_quirky.txt index a530560..0918a09 100644 --- a/cfg/8-ball_quirky.txt +++ b/cfg/8-ball_quirky.txt @@ -4,11 +4,11 @@ Error 404: Answer not found. Consult your nearest wizard. Why should I know? Go ask your cat or something. The stars say yes, but the moon disagrees. -You don’t want to know... trust me. -Sure, but don’t quote me on that. +You don't want to know... trust me. +Sure, but don't quote me on that. In an alternate universe, yes. Why are you asking a stupid bot? Only if you bring snacks. -Flip a coin, I’m on break. +Flip a coin, I'm on break. The spirits are buffering... please wait. You already know the answer. \ No newline at end of file diff --git a/data/lang/en.json b/data/lang/en.json index e7f6363..4b342e8 100644 --- a/data/lang/en.json +++ b/data/lang/en.json @@ -14,7 +14,7 @@ "dc_msg_dm_python_err_socket": "Unknown internal Python error occurred: Websocket response error", "dc_msg_dm_python_err": "Unknown internal Python Error:\n```\n{0}\n```", "dc_msg_embed_default_embed_desc": "Default english embed description.", - "dc_msg_embed_re_post": "Spoilers and vote length anonymizer for fair review!\n## Post Data:\n**Subreddit:** {0}\n**Post upvotes:** ||`{1:>6}`||\n**Moderator votes:** ||`{2:>6}`||\n**Media type:** `{3}`\n**URL:** ||<{4}>||\n\n## Listing Data:\n**Added by:** human: {5} | bot: {6}\n**Approved by:** human: {7} | bot: [not implemented]", + "dc_msg_embed_re_post": "Spoilers and vote length anonymizer for fair review!\n## Post Data:\n**Subreddit:** {0}\n**Post upvotes:** ||`{1:>6}`||\n**Moderator votes:** ||`{2:>6}`||\n**Media type:** `{3}`\n## Listing Data:\n**Added by:** human: {4} | bot: {5}\n**Approved:** {6}", "dc_msg_embed_re_removed": "## Removed by `{0}`\n**Reason:** {1}\n**URL**: ||<{2}>||", "dc_msg_err_trace": "Unknown error!\nError trace: {0}", "dc_msg_failed_shorturl_conversion": "Couldn't convert to shortURL: Invalid Reddit URL format.", diff --git a/data/lang/gpt_fr.json b/data/lang/gpt_fr.json index d36e78d..a510adc 100644 --- a/data/lang/gpt_fr.json +++ b/data/lang/gpt_fr.json @@ -14,7 +14,7 @@ "dc_msg_dm_python_err_socket": "Erreur Python interne inconnue : erreur de réponse WebSocket", "dc_msg_dm_python_err": "Erreur Python interne inconnue :\n```\n{0}\n```", "dc_msg_embed_default_embed_desc": "Description par défaut de l'encart en anglais.", - "dc_msg_embed_re_post": "Spoilers et anonymiseur de durée des votes pour une évaluation équitable !\n## Données du post :\n**Subreddit :** {0}\n**Votes positifs du post :** ||`{1:>6}`||\n**Votes des modérateurs :** ||`{2:>6}`||\n**Type de média :** `{3}`\n**URL :** ||<{4}>||\n\n## Données d'inscription :\n**Ajouté par :** humain : {5} | bot : {6}\n**Approuvé par :** humain : {7} | bot : [non implémenté]", + "dc_msg_embed_re_post": "Spoilers et anonymiseur de durée des votes pour une évaluation équitable !\n## Données du post :\n**Subreddit :** {0}\n**Votes positifs du post :** ||`{1:>6}`||\n**Votes des modérateurs :** ||`{2:>6}`||\n**Type de média :** `{3}`\n## Données d'inscription :\n**Ajouté par :** humain : {4} | bot : {5}\n**Approuvé:** {6}", "dc_msg_embed_re_removed": "## Supprimé par `{0}`\n**Raison :** {1}\n**URL** : ||<{2}>||", "dc_msg_err_trace": "Erreur inconnue !\nTrace de l’erreur : {0}", "dc_msg_failed_shorturl_conversion": "Échec de la conversion en shortURL : format d’URL Reddit invalide.", diff --git a/src/messages.rs b/src/messages.rs index 5853300..64b3ded 100644 --- a/src/messages.rs +++ b/src/messages.rs @@ -11,6 +11,7 @@ use flate2::Compression; use poise::serenity_prelude::json::Value; use poise::{serenity_prelude::CreateMessage, CreateReply, ReplyHandle}; use poise::serenity_prelude::{ChannelId, Color, CreateActionRow, CreateButton, CreateEmbed, CreateEmbedAuthor, EditMessage, Http, Message, ReactionType, Timestamp, UserId}; +use serde_json::json; #[derive(Clone)] @@ -223,7 +224,6 @@ pub fn make_post_embed(post_data: &Value, url: &str, ephemeral: bool) -> EmbedOp post_data["post_data"]["upvotes"].as_i64().unwrap(), post_data["votes"]["mod_voters"].as_array().unwrap().len(), if !media_type.is_null() { media_type.as_str().unwrap() } else { "None" }, - url, if post_data["added"] ["by_human"].as_bool().unwrap() { "✅" } else { "❌" }, if post_data["added"] ["by_bot"].as_bool().unwrap() { "✅" } else { "❌" }, @@ -287,7 +287,25 @@ pub fn make_removed_embed(post_data: &Value, url: &str, ephemeral: bool) -> Embe pub fn trim_post_json(j: &Value) -> Value { let mut json_trimmed = j.clone(); - json_trimmed["post_data"].as_object_mut().unwrap().remove("media_urls"); + + if let Some(obj) = json_trimmed["post_data"].as_object_mut() { + obj.remove("media_urls"); + obj.remove("subreddit"); + obj.remove("title"); + obj.remove("date_unix"); + obj.remove("media_type"); + } + + if let Some(obj) = json_trimmed["votes"].as_object_mut() { + obj.remove("voters_re"); + obj["voters_dc"] = json!(obj["voters_dc"].as_array().unwrap().len()); + obj["mod_voters"] = json!(obj["mod_voters"].as_array().unwrap().len()); + } + + if let Some(obj) = json_trimmed["approved"].as_object_mut() { + obj.remove("by_ris"); + } + return json_trimmed; } From b4044956decb828add6141f62ff1e8a854178c30 Mon Sep 17 00:00:00 2001 From: Byte Dice <165556900+ByteDice@users.noreply.github.com> Date: Fri, 8 Aug 2025 17:23:12 +0200 Subject: [PATCH 36/39] Update TODO.md cuz i forgot that for like 20 years --- TODO.md | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/TODO.md b/TODO.md index ebc5214..b20de1d 100644 --- a/TODO.md +++ b/TODO.md @@ -2,10 +2,7 @@ - [ ] Reddit bot that scrapes images with tag "Original Art" and posts them in Discord server - [ ] handle dm_on_error cfg - [ ] Allow updating the data autonomously and via manual commands. - - [ ] Automatically approve posts that don't get caught by reverse image search (ris) - - [ ] Make buttons do stuff - - [ ] Add buttons to removed post embed - + - [ ] 10-minute schedule for updating Discord channel (IMPOSSIBLE / REALLY FUCKING HARD) ### Medium priority: - [ ] Postfix calculator @@ -15,4 +12,4 @@ ### Low priority: - [ ] Content update sender - * Automatically sends sneak peeks (like commit history or manual) of projects when they're updated \ No newline at end of file + * Automatically sends sneak peeks (like commit history or manual) of projects when they're updated From 0db5937c3903986f8a1d45dee346cd07a5327a67 Mon Sep 17 00:00:00 2001 From: Byte Dice Date: Sat, 9 Aug 2025 14:37:15 +0200 Subject: [PATCH 37/39] major update thingy. Clippy is happy, bugfixes, new "max_results" arg to /re_updateDiscord, etc. --- data/defaults/cfg_default.toml | 4 +-- src/cmds/help.rs | 2 +- src/cmds/reload_cfg.rs | 2 +- src/cmds/stop.rs | 5 ++-- src/events.rs | 42 +++++++++++++++---------------- src/macros.rs | 4 +-- src/main.rs | 6 ++--- src/python/bot.py | 13 +++++----- src/python/main.py | 10 +++----- src/python/posts.py | 46 +++++++++++++++++----------------- src/re_cmds/add.rs | 2 +- src/re_cmds/approve.rs | 2 +- src/re_cmds/generic_fns.rs | 15 ++++++++--- src/re_cmds/get.rs | 4 +-- src/re_cmds/remove.rs | 4 +-- src/re_cmds/update.rs | 18 +++++++++---- src/re_cmds/vote.rs | 4 +-- 17 files changed, 97 insertions(+), 86 deletions(-) diff --git a/data/defaults/cfg_default.toml b/data/defaults/cfg_default.toml index 2b3d08c..a0bcc72 100644 --- a/data/defaults/cfg_default.toml +++ b/data/defaults/cfg_default.toml @@ -9,9 +9,7 @@ status = "🎲 https://bytedice.net" [reddit] # Which subreddits the bot will scan when executing "re"-category commands. # Is automatically disabled when `disabled_categories` includes "re". -# Separate multiple subreddits with a "+", e.g "memes+askreddit". -# [REQUIRES RESTART] -subreddits = "bytedicetesting" +subreddits = ["bytedicetesting"] # How many posts the bot scans when running `/re_updatediscord`. fetch_limit = 100 diff --git a/src/cmds/help.rs b/src/cmds/help.rs index 7f00dfd..3431a60 100644 --- a/src/cmds/help.rs +++ b/src/cmds/help.rs @@ -164,7 +164,7 @@ fn separate_by_category(cmds: Vec<&Cmd>) -> Vec<(String, Vec<&Cmd>)> { for cmd in cmds { grouped .entry(cmd.category.clone().unwrap_or("No category".to_string())) - .or_insert_with(Vec::new).push(cmd); + .or_default().push(cmd); } return grouped.into_iter().collect(); diff --git a/src/cmds/reload_cfg.rs b/src/cmds/reload_cfg.rs index dcb0d43..4326022 100644 --- a/src/cmds/reload_cfg.rs +++ b/src/cmds/reload_cfg.rs @@ -14,7 +14,7 @@ pub async fn cmd( ctx: Context<'_> ) -> Result<(), Error> { - let r = read_cfg_data(&ctx.data(), false).await; + let r = read_cfg_data(ctx.data(), false).await; let d = get_toml_mutex(&ctx.data().cfg).await.unwrap(); if r.is_none() { return Ok(()); } diff --git a/src/cmds/stop.rs b/src/cmds/stop.rs index 3622602..e81d983 100644 --- a/src/cmds/stop.rs +++ b/src/cmds/stop.rs @@ -19,8 +19,9 @@ pub async fn cmd( #[description = "Type \"i want to stop the bot now\" to confirm."] confirmation: Option, ) -> Result<(), Error> { - let should_stop = ctx.data().args.dev - || confirmation.unwrap_or_default().to_lowercase() == "i want to stop the bot now"; + let stop_confirm = "i want to stop the bot now".replace(" ", ""); + let confirm_formatted = confirmation.unwrap_or_default().to_lowercase().replace(" ", ""); + let should_stop = ctx.data().args.dev || confirm_formatted == stop_confirm; if should_stop { let msg = send_msg(ctx, lang!("dc_msg_owner_data_save"), true, true).await.unwrap(); diff --git a/src/events.rs b/src/events.rs index 590f5d3..abe4aea 100644 --- a/src/events.rs +++ b/src/events.rs @@ -54,24 +54,24 @@ async fn handle_buttons(ctx: &serenity::Context, data: &Data, interaction: &Inte let url = i_embed.url.clone().unwrap(); return match component.data.custom_id.as_str() { - "approve_btn" => approve_btn(ctx, data, &component.member.as_ref().unwrap(), component, url, true).await, - "remove_btn" => remove_btn (ctx, data, &component.member.as_ref().unwrap(), component, url, true).await, - "unapprove_btn" => approve_btn(ctx, data, &component.member.as_ref().unwrap(), component, url, false).await, - "unremove_btn" => remove_btn (ctx, data, &component.member.as_ref().unwrap(), component, url, false).await, - "unvote_btn" => vote_btn (ctx, data, &component.member.as_ref().unwrap(), component, url, false).await, - "vote_btn" => vote_btn (ctx, data, &component.member.as_ref().unwrap(), component, url, true).await, + "approve_btn" => approve_btn(ctx, data, component.member.as_ref().unwrap(), component, url, true).await, + "remove_btn" => remove_btn (ctx, data, component.member.as_ref().unwrap(), component, url, true).await, + "unapprove_btn" => approve_btn(ctx, data, component.member.as_ref().unwrap(), component, url, false).await, + "unremove_btn" => remove_btn (ctx, data, component.member.as_ref().unwrap(), component, url, false).await, + "unvote_btn" => vote_btn (ctx, data, component.member.as_ref().unwrap(), component, url, false).await, + "vote_btn" => vote_btn (ctx, data, component.member.as_ref().unwrap(), component, url, true).await, _ => Err("Message button with that ID isn't handled.".into()) } } async fn update_embed(ctx: &serenity::Context, url: &str, new_data: &Value, c_id: &ChannelId, m_id: &MessageId) { - let e: EmbedOptions; let remove = new_data["removed"]["removed"].as_bool().unwrap(); - if remove { e = make_removed_embed(new_data, url, true); } - else { e = make_post_embed (new_data, url, true); } - - serenity_edit_msg_embed(ctx, &c_id, &m_id, e).await; + let e: EmbedOptions = + if remove { make_removed_embed(new_data, url, true) } + else { make_post_embed (new_data, url, true) }; + + serenity_edit_msg_embed(ctx, c_id, m_id, e).await; } @@ -106,14 +106,12 @@ async fn approve_btn(ctx: &serenity::Context, data: &Data, c_member: &Member, co async fn remove_btn(ctx: &serenity::Context, data: &Data, c_member: &Member, component: &ComponentInteraction, url: String, remove: bool) -> Result<(), Error> { if !is_bk_mod_serenity(ctx, data, c_member, component).await { return Ok(()); } - let r: Value; - - if remove { - r = send_cmd_json("remove_post_url", Some(json!([&url, &c_member.user.name, None::])), true).await.unwrap(); + let r: Value = if remove { + send_cmd_json("remove_post_url", Some(json!([&url, &c_member.user.name, None::])), true).await.unwrap() } else { - r = send_cmd_json("add_post_url", Some(json!([&url, false, true])), true).await.unwrap(); - } + send_cmd_json("add_post_url", Some(json!([&url, false, true])), true).await.unwrap() + }; let c_id = component.channel_id; let m_id = component.message.id; @@ -157,11 +155,11 @@ async fn vote_btn(ctx: &serenity::Context, data: &Data, c_member: &Member, compo serenity_send_msg(ctx, component, lang!("dc_msg_re_vote_remove_success"), true).await; } } - else { - if new_data["removed"]["removed"].as_bool().unwrap() { - serenity_send_msg(ctx, component, lang!("dc_msg_re_post_vote_removed_post"), true).await; - } - else if !vote { serenity_send_msg(ctx, component, lang!("dc_msg_re_vote_remove_havent"), true).await; } + else if new_data["removed"]["removed"].as_bool().unwrap() { + serenity_send_msg(ctx, component, lang!("dc_msg_re_post_vote_removed_post"), true).await; + } + else if !vote { + serenity_send_msg(ctx, component, lang!("dc_msg_re_vote_remove_havent"), true).await; } return Ok(()); diff --git a/src/macros.rs b/src/macros.rs index b7ec8cd..4fa57e1 100644 --- a/src/macros.rs +++ b/src/macros.rs @@ -66,7 +66,7 @@ macro_rules! warnln { macro_rules! lang { ($key:expr) => { { - use crate::{LANG, errln}; + use $crate::{LANG, errln}; let value = unsafe { LANG .as_ref() @@ -80,7 +80,7 @@ macro_rules! lang { } }; ($key:expr, $($arg:expr),*) => {{ - use crate::{LANG, errln}; + use $crate::{LANG, errln}; use formatx::formatx; let value = unsafe { diff --git a/src/main.rs b/src/main.rs index 588653b..7d62d56 100644 --- a/src/main.rs +++ b/src/main.rs @@ -69,9 +69,9 @@ struct Args { wipe: bool, #[arg(short = 't', long, help = "Makes the program use the ASSISTANT_TOKEN_TEST env var instead of ASSISTANT_TOKEN. This env var should hold the token of a non-production bot.")] test: bool, - #[arg(long, help = "Adds annoying ping prints.")] + #[arg(long, help = "Adds annoying prints when the websockets send a ping. Why though?")] ping: bool, - #[arg(long, help = "Makes the program not use the schedules.")] + #[arg(long, help = "Makes the program not use the schedule system.")] nosched: bool } @@ -150,7 +150,7 @@ async fn main() { let python_args = args.clone(); let rust_args = args.clone(); - if !run_py { rs_println!("[IMPORTANT] You have disabled the \"re\" commands in the CFG. The app will not run the Python code nor the websockets to save resources!"); } + if !run_py { rs_println!("[IMPORTANT] You have disabled the \"re\" commands in the CFG. The app will not run the Python code and the websockets to save resources!"); } let rust = thread::spawn(move || { rt_rs.block_on(async { diff --git a/src/python/bot.py b/src/python/bot.py index cb34ff9..09d71c3 100644 --- a/src/python/bot.py +++ b/src/python/bot.py @@ -9,7 +9,8 @@ import toml RE_DATA_POSTS: Final[str] = "posts" CFG_DATA_RE: Final[str] = "reddit" - +# TODO: add wipe arg +# TODO: add test-bot arg class Bot: args: dict[str, Any] = {"NO_RUST": True, "dev": True, "py": True, "port": 2920} r_id: str | None = os.environ.get("ASSISTANT_R_ID") @@ -21,7 +22,7 @@ class Bot: useragent: str =\ f"{username} by u/RandomPersonDotExe aka u/Byte_Dice"\ - if r_id == "YmZjr4zLr2qtHdpQXtj0sBOOdJzrXQ"\ + if r_id == "YmZjr4zLr2qtHdpQXtj0sBOOdJzrXQ" or r_id == "Q-eBDGS8sFHlUCi9kpBepQ"\ else f"{username} (Original program by u/RandomPersonDotExe aka u/Byte_Dice)" if password is None: @@ -37,16 +38,13 @@ class Bot: password = self.password, user_agent = self.useragent ) - self.sr_list: list[str] = ["bytedicetesting"] + self.sr_list: list[str] = [] self.sr = None self.data_f: TextIOWrapper | None = None self.data: dict[str, Any] = {} self.flairs: list[str] = [] self.aliases: dict[str, list[str]] = {} - async def initialize(self): - self.sr = await self.r.subreddit("+".join(self.sr_list)) - async def set_args(self, args: dict[str, Any]): self.args = args @@ -67,5 +65,8 @@ class Bot: self.fetch_limit = new_cfg[CFG_DATA_RE]["fetch_limit"] self.flairs = new_cfg[CFG_DATA_RE]["search_flairs"] self.aliases = new_cfg[CFG_DATA_RE]["aliases"] + self.sr_list = new_cfg[CFG_DATA_RE]["subreddits"] + self.sr = await self.r.subreddit("+".join(self.sr_list)) init_lang(new_cfg["general"]["lang"]) + py_print("Successfully updated the configs!") return True \ No newline at end of file diff --git a/src/python/main.py b/src/python/main.py index 1504289..8a91ec6 100644 --- a/src/python/main.py +++ b/src/python/main.py @@ -7,17 +7,13 @@ import bot as botPy import py_data import py_websocket - async def main(): sys.stdout.reconfigure(encoding="utf-8") # type: ignore py_print("Creating Reddit bot...") bot = botPy.Bot() - await bot.initialize() - py_print(f"Successfully created Reddit bot: {await bot.r.user.me()}") - - # args is supposed to be undefined. + # args and lang_name are supposed to be undefined. # It gets defined in Rust. try: await bot.set_args(args) # type: ignore @@ -47,7 +43,9 @@ async def main(): if data_retries == 5 and not rd: raise Exception("Couldn't read re_data.json: File doesn't exist") - py_print("Successfully read data!") + py_print("Successfully read all data!") + + py_print(f"Successfully created Reddit bot: {await bot.r.user.me()}") if not bot.args["py"]: py_print("Connecting to local websocket...") diff --git a/src/python/posts.py b/src/python/posts.py index eb17555..b54568d 100644 --- a/src/python/posts.py +++ b/src/python/posts.py @@ -9,40 +9,33 @@ import bot as botPy from macros import * -async def add_new_posts(bot: botPy.Bot, max_age: int) -> bool: +async def add_new_posts(bot: botPy.Bot, max_age: int, max_results: int) -> bool: check_emoji = emoji.emojize(":check_mark_button:") cross_emoji = emoji.emojize(":cross_mark:") py_print("Fetching posts...") - posts = await fetch_posts_with_flair(bot, bot.flairs) + posts = await fetch_posts_with_flair(bot, bot.flairs, max_age, max_results) py_print("Evaluating posts...") added_posts = 0 without_media = 0 not_added = 0 - old_posts = 0 for post in posts: - media = has_media(post) + details = get_post_details(post) - media_urls = "\n ".join(media[3]) + media_urls = "\n ".join(details.media_urls) + media_check = check_emoji if details.media_type is not None else cross_emoji if bot.args["dev"]: py_print( - f"\n{post.title}", - f"\n {post.shortlink}" - f"\n {check_emoji if media[0] else cross_emoji} Media ({media[1]}) [{media[2]}]", + f"\n{details.title}", + f"\n {details.url}" + f"\n {media_check} Media ({details.media_type}) [{len(details.media_urls)}]", f"\n {media_urls}\n" ) - - details = get_post_details(post) - now = int(time.time()) - if now - details.date_unix > max_age and max_age > 0: - old_posts += 1 - continue - - if not media[0]: + if details.media_type is not None: without_media += 1 continue @@ -57,25 +50,32 @@ async def add_new_posts(bot: botPy.Bot, max_age: int) -> bool: py_print(f"Successfully fetched {len(posts)} posts.\n" + f" Out of which were {added_posts} added.\n" + f" {without_media} had no media, " + - f"{not_added} are removed or already existed, " + - f"and {old_posts} were older than the max age threshold.") + f" {not_added} are removed or already existed, ") py_data.write_data(bot) return True -async def fetch_posts_with_flair(bot: botPy.Bot, flair_names: list[str]) -> list[models.Submission]: +async def fetch_posts_with_flair( + bot: botPy.Bot, + flair_names: list[str], + max_age_secs: int, + max_results: int +) -> list[models.Submission]: posts: list[models.Submission] = [] flair_names_str = \ - f"flair:{flair_names[0]}" if len(flair_names) == 1\ - else " OR ".join(f"flair:{flair}" for flair in flair_names) + f"flair:{flair_names[0].replace(" ", "_")}" if len(flair_names) == 1\ + else " OR ".join(f"flair:{flair.replace(" ", "_")}" for flair in flair_names) if bot.sr is None: return [] - # ~36 OG-art posts per week, round limit to 50, 75 or 100 - async for post in bot.sr.search(f"{flair_names_str}", sort="new", limit=bot.fetch_limit): + now = int(time.time()) + + # ~20 OG-art posts per week, round limit to 50, 75 or 100 for 2 subreddits + async for post in bot.sr.search(f"{flair_names_str}", sort="new", limit=max_results): + if now - int(post.created_utc) > max_age_secs and max_age_secs > 0: continue posts.append(post) return posts diff --git a/src/re_cmds/add.rs b/src/re_cmds/add.rs index cc44f36..1066ea8 100644 --- a/src/re_cmds/add.rs +++ b/src/re_cmds/add.rs @@ -55,7 +55,7 @@ pub async fn cmd( if a { send_msg(ctx, lang!("dc_msg_re_also_approved"), true, true).await; } } - if let Some(post) = get_post_from_data(ctx, &reddit_data, &shorturl).await? { + if let Some(post) = get_post_from_data(ctx, &reddit_data, shorturl).await? { send_embed_for_post(ctx, post, &url).await?; } diff --git a/src/re_cmds/approve.rs b/src/re_cmds/approve.rs index 832992f..b30e79c 100644 --- a/src/re_cmds/approve.rs +++ b/src/re_cmds/approve.rs @@ -26,7 +26,7 @@ pub async fn cmd( data::update_re_data(ctx.data()).await; let reddit_data = get_mutex_data(&ctx.data().reddit_data).await?; - approve_cmd(ctx, &shorturl, &reddit_data, !disapprove.unwrap_or(false)).await; + approve_cmd(ctx, shorturl, &reddit_data, !disapprove.unwrap_or(false)).await; return Ok(()); } diff --git a/src/re_cmds/generic_fns.rs b/src/re_cmds/generic_fns.rs index a67d810..55939fb 100644 --- a/src/re_cmds/generic_fns.rs +++ b/src/re_cmds/generic_fns.rs @@ -13,7 +13,7 @@ pub async fn is_bk_mod_msg(ctx: Context<'_>) -> bool { if is_bk_mod(ctx.data().bk_mods.clone(), ctx.author().id.get()) { return true; } let sr = get_readable_subreddits(ctx.data()).await.unwrap(); - send_msg(ctx, lang!("dc_msg_re_permdeny_not_re_mod", sr), false, false).await; + send_msg(ctx, lang!("dc_msg_re_permdeny_not_re_mod", sr), true, true).await; return false } @@ -71,9 +71,16 @@ pub async fn send_embed_for_removed(ctx: Context<'_>, url: &str, post: &Value) { pub async fn get_readable_subreddits(data: &Data) -> Result { let d = get_toml_mutex(&data.cfg).await.unwrap(); - let sr = d["reddit"]["subreddits"].as_str().ok_or("Item of key \"subreddit\" is not a string type.\nTrace: `get_readable_subreddits -> let sr = ...`")?; - let split: Vec<&str> = sr.split("+").collect(); - let join = split.join(", r/"); + let sr = d["reddit"]["subreddits"].as_array().unwrap(); + let sr_str: Vec<&str> = sr + .iter() + .map(|v| v.as_str().unwrap()) + .collect(); + + let mut join = sr_str.join(", r/"); + + if join.len() != 0 { join = format!("r/{}", join); } + else { join = "[no subreddits assigned]".to_string(); } return Ok(join); } diff --git a/src/re_cmds/get.rs b/src/re_cmds/get.rs index c1d10e4..5391d80 100644 --- a/src/re_cmds/get.rs +++ b/src/re_cmds/get.rs @@ -24,8 +24,8 @@ pub async fn cmd( let reddit_data = get_mutex_data(&ctx.data().reddit_data).await?; - if let Some(post) = get_post_from_data(ctx, &reddit_data, &shorturl).await? { - send_embed_for_post(ctx, post, &shorturl).await?; + if let Some(post) = get_post_from_data(ctx, &reddit_data, shorturl).await? { + send_embed_for_post(ctx, post, shorturl).await?; } return Ok(()); diff --git a/src/re_cmds/remove.rs b/src/re_cmds/remove.rs index c4bec26..399d051 100644 --- a/src/re_cmds/remove.rs +++ b/src/re_cmds/remove.rs @@ -39,9 +39,9 @@ pub async fn cmd( data::update_re_data(ctx.data()).await; let reddit_data = get_mutex_data(&ctx.data().reddit_data).await?; - if let Some(post) = get_post_from_data(ctx, &reddit_data, &shorturl).await? { + if let Some(post) = get_post_from_data(ctx, &reddit_data, shorturl).await? { if post["removed"]["removed"].as_bool().unwrap() { - send_embed_for_removed(ctx, &shorturl, &post).await; + send_embed_for_removed(ctx, shorturl, &post).await; } } diff --git a/src/re_cmds/update.rs b/src/re_cmds/update.rs index 3b5750b..9549a72 100644 --- a/src/re_cmds/update.rs +++ b/src/re_cmds/update.rs @@ -3,7 +3,7 @@ use std::time::{SystemTime, UNIX_EPOCH}; use poise::{serenity_prelude::{ChannelId, EditMessage, GetMessages, Http, Message, MessageId, UserId}, ReplyHandle}; use serde_json::{json, Map, Value}; -use crate::{data::{self, get_mutex_data, DC_POSTS_CHANNEL_KEY}, lang, messages::{edit_reply, embed_from_options, make_post_embed, make_removed_embed, send_embed, send_msg, trim_post_json}, re_cmds::generic_fns::embed_to_json, rs_println, websocket::send_cmd_json, Context, Error, CFG_DATA_RE}; +use crate::{data::{self, get_mutex_data, get_toml_mutex, DC_POSTS_CHANNEL_KEY}, lang, messages::{edit_reply, embed_from_options, make_post_embed, make_removed_embed, send_embed, send_msg, trim_post_json}, re_cmds::generic_fns::embed_to_json, rs_println, websocket::send_cmd_json, Context, Error, CFG_DATA_RE}; #[poise::command( slash_command, @@ -17,12 +17,16 @@ use crate::{data::{self, get_mutex_data, DC_POSTS_CHANNEL_KEY}, lang, messages:: /// Updates the bound Discord channel with the bot's current Reddit data. pub async fn cmd( ctx: Context<'_>, - #[description = "Only adds new posts, leaves everything else unchanged."] + #[description = "Make this true to only add new posts and leave everything else unchanged."] only_add: Option, - #[description = "The max age of a post (in days). Any post older than this will be removed. (0 is infinite.)"] + #[description = "The max age of a post in days. Any post older than this will be removed. (0 is infinite)"] #[min = 0] #[max = 65535] - max_age: Option + max_age: Option, + #[description = "The max amount of posts to fetch (no value uses default value)."] + #[min = 1] + #[max = 100] + max_results: Option ) -> Result<(), Error> { let http = ctx.http(); @@ -35,7 +39,11 @@ pub async fn cmd( let max_age_u = max_age.unwrap_or(8); let max_age_secs = max_age_u as u64 * (60 * 60 * 24); - send_cmd_json("add_new_posts", Some(json!([max_age_secs])), true).await; + let max_results_toml = &get_toml_mutex(&ctx.data().cfg).await.unwrap(); + let max_results_pre = max_results_toml["reddit"]["fetch_limit"].as_integer().unwrap(); + let max_results_final = max_results.unwrap_or(max_results_pre as u16); + + send_cmd_json("add_new_posts", Some(json!([max_age_secs, max_results_final])), true).await; data::update_re_data(ctx.data()).await; let r_data = get_mutex_data(&ctx.data().reddit_data).await?; diff --git a/src/re_cmds/vote.rs b/src/re_cmds/vote.rs index 4e3b4dd..0fd9d6f 100644 --- a/src/re_cmds/vote.rs +++ b/src/re_cmds/vote.rs @@ -25,12 +25,12 @@ pub async fn cmd( let shorturl_u = to_shorturl(&url); let shorturl = &shorturl_u.unwrap_or(url.clone()); - if post_data.get(&shorturl).is_none() { + if post_data.get(shorturl).is_none() { send_msg(ctx, lang!("dc_msg_re_post_404"), false, false).await; return Ok(()); } if post_data[&shorturl]["removed"]["removed"].as_bool().unwrap() { - send_embed_for_removed(ctx, &shorturl, &post_data[&shorturl]).await; + send_embed_for_removed(ctx, shorturl, &post_data[&shorturl]).await; return Ok(()); } From eb018a69f3482d9d756f1e2aa08c3915452d24d2 Mon Sep 17 00:00:00 2001 From: Byte Dice Date: Sat, 9 Aug 2025 16:51:37 +0200 Subject: [PATCH 38/39] minor bugfixes --- data/lang/en.json | 2 +- data/lang/gpt_fr.json | 2 +- src/messages.rs | 6 +++--- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/data/lang/en.json b/data/lang/en.json index 4b342e8..edc8d3f 100644 --- a/data/lang/en.json +++ b/data/lang/en.json @@ -15,7 +15,7 @@ "dc_msg_dm_python_err": "Unknown internal Python Error:\n```\n{0}\n```", "dc_msg_embed_default_embed_desc": "Default english embed description.", "dc_msg_embed_re_post": "Spoilers and vote length anonymizer for fair review!\n## Post Data:\n**Subreddit:** {0}\n**Post upvotes:** ||`{1:>6}`||\n**Moderator votes:** ||`{2:>6}`||\n**Media type:** `{3}`\n## Listing Data:\n**Added by:** human: {4} | bot: {5}\n**Approved:** {6}", - "dc_msg_embed_re_removed": "## Removed by `{0}`\n**Reason:** {1}\n**URL**: ||<{2}>||", + "dc_msg_embed_re_removed": "## Removed by `{0}`\n**Reason:** {1}", "dc_msg_err_trace": "Unknown error!\nError trace: {0}", "dc_msg_failed_shorturl_conversion": "Couldn't convert to shortURL: Invalid Reddit URL format.", "dc_msg_mandatory_response": "Mandatory response message, please ignore.", diff --git a/data/lang/gpt_fr.json b/data/lang/gpt_fr.json index a510adc..e04b2de 100644 --- a/data/lang/gpt_fr.json +++ b/data/lang/gpt_fr.json @@ -15,7 +15,7 @@ "dc_msg_dm_python_err": "Erreur Python interne inconnue :\n```\n{0}\n```", "dc_msg_embed_default_embed_desc": "Description par défaut de l'encart en anglais.", "dc_msg_embed_re_post": "Spoilers et anonymiseur de durée des votes pour une évaluation équitable !\n## Données du post :\n**Subreddit :** {0}\n**Votes positifs du post :** ||`{1:>6}`||\n**Votes des modérateurs :** ||`{2:>6}`||\n**Type de média :** `{3}`\n## Données d'inscription :\n**Ajouté par :** humain : {4} | bot : {5}\n**Approuvé:** {6}", - "dc_msg_embed_re_removed": "## Supprimé par `{0}`\n**Raison :** {1}\n**URL** : ||<{2}>||", + "dc_msg_embed_re_removed": "## Supprimé par `{0}`\n**Raison :** {1}", "dc_msg_err_trace": "Erreur inconnue !\nTrace de l’erreur : {0}", "dc_msg_failed_shorturl_conversion": "Échec de la conversion en shortURL : format d’URL Reddit invalide.", "dc_msg_mandatory_response": "Message de réponse obligatoire, merci d’ignorer.", diff --git a/src/messages.rs b/src/messages.rs index 64b3ded..d5002bb 100644 --- a/src/messages.rs +++ b/src/messages.rs @@ -264,10 +264,10 @@ pub fn make_removed_embed(post_data: &Value, url: &str, ephemeral: bool) -> Embe let desc = lang!( "dc_msg_embed_re_removed", - post_data["removed"]["by"].as_str().unwrap(), - if !post_data["removed"]["reason"].is_null() { post_data["removed"]["reason"].as_str().unwrap() } + if !post_data["removed"]["by"].is_null() { post_data["removed"]["by"].as_str().unwrap() } else { &none }, - url + if !post_data["removed"]["reason"].is_null() { post_data["removed"]["reason"].as_str().unwrap() } + else { &none } ); let json_encoded = trim_compress_and_encode_json(post_data); From 4b564c45404b425e4b7a077c8f2a2a64ee853383 Mon Sep 17 00:00:00 2001 From: Byte Dice Date: Sat, 9 Aug 2025 17:04:50 +0200 Subject: [PATCH 39/39] minor bugfix --- src/re_cmds/update.rs | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/src/re_cmds/update.rs b/src/re_cmds/update.rs index 9549a72..e0237b3 100644 --- a/src/re_cmds/update.rs +++ b/src/re_cmds/update.rs @@ -68,7 +68,7 @@ pub async fn cmd( // Adding new posts p_text = update_progress(ctx, progress.clone(), p_text.clone(), lang!("dc_msg_update_add", "✅\n")).await; let weekly_art = r_data[CFG_DATA_RE].as_object().unwrap(); - add_posts(ctx, weekly_art, &msgs_json, max_age_secs).await; + add_posts(ctx, weekly_art, &msgs_json, max_age_secs, max_results_final).await; // Stop if only_add if only_add.unwrap_or(false) { @@ -239,13 +239,15 @@ async fn msgs_to_json(msgs: Vec, reddit_data: &Value, max_age: u64) -> } -async fn add_posts(ctx: Context<'_>, r_data: &Map, msgs_json: &Value, max_age: u64) { +async fn add_posts(ctx: Context<'_>, r_data: &Map, msgs_json: &Value, max_age: u64, max_results: u16) { let now = SystemTime::now() .duration_since(UNIX_EPOCH) .expect("Time went backwards") .as_secs(); - for url in r_data.keys() { + for (i, url) in r_data.keys().enumerate() { + if i + 1 > max_results.into() { break } + if ["no_change", "updated", "removed", "old", "duplicates"] .iter() .any(|key| msgs_json[key].as_object().unwrap().contains_key(url))