From 2a951723b14e198dc1f010b8642ece2cb21be774 Mon Sep 17 00:00:00 2001 From: Byte Dice Date: Sat, 14 Jun 2025 14:09:46 +0200 Subject: [PATCH] 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"])