diff --git a/TODO.md b/TODO.md index 37521bb..c370be9 100644 --- a/TODO.md +++ b/TODO.md @@ -9,10 +9,10 @@ - [ ] `/bk_week_top [category] [amount]` to get the top N posts in a category (e.g upvotes) - [ ] Allow updating the data autonomously and via manual commands. - - [ ] 30-minute schedule when bot adds posts - - [ ] Manually add posts + - [ ] 10-minute schedule for updating Discord channel + - [ ] Manually add posts - [ ] via `u/[bot] add` - - [ ] 5 minute schedule when user adds post + - [ ] 2 minute schedule for responding to commands diff --git a/bk_week_help_re.md b/bk_week_help_re.md index e3062ca..aee1375 100644 --- a/bk_week_help_re.md +++ b/bk_week_help_re.md @@ -4,9 +4,9 @@ To execute a command on the Reddit bot, include `u/ByteDiceAssistant [args]` in ## `bk_week_add` Adds the post to the list of posts. - **Only moderators of a subreddit or the OP (Original Poster) can use this command.** -## `bk_week_vote` + ### **Examples** ``` "u/ByteDiceAssistant bk_week_add" diff --git a/src/bk_week_cmds.rs b/src/bk_week_cmds.rs index 74c59c0..1ec856e 100644 --- a/src/bk_week_cmds.rs +++ b/src/bk_week_cmds.rs @@ -77,7 +77,7 @@ pub async fn bk_week_get( send_embed_for_post(ctx, post, &url).await?; } - Ok(()) + return Ok(()); } async fn get_reddit_data(ctx: Context<'_>) -> Result { @@ -178,7 +178,7 @@ pub async fn bk_week_add( if let Some(bk_week) = reddit_data.get(BK_WEEK) { let a = approve.unwrap_or_else(|| false); - let r = websocket::send_cmd_json("add_post_url", json!([&url, a, true])).await.unwrap(); + let r = websocket::send_cmd_json("add_post_url", Some(json!([&url, a, true]))).await.unwrap(); if !r["value"].as_bool().unwrap() { send_msg( @@ -239,7 +239,7 @@ pub async fn bk_week_remove( } let auth = &ctx.author().name; - let r = send_cmd_json("remove_post_url", json!([&url, &auth, &reason])).await.unwrap(); + let r = send_cmd_json("remove_post_url", Some(json!([&url, &auth, &reason]))).await.unwrap(); if r["value"].as_bool().unwrap() { send_msg( @@ -287,7 +287,7 @@ async fn approve_cmd(ctx: Context<'_>, url: &str, reddit_data: &Value, approve: send_post_removed_message(ctx, &url, post.get("removed_by").unwrap().as_str().unwrap()).await; } - let r = websocket::send_cmd_json("set_approve_post", json!([approve, &url])).await.unwrap(); + 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, format!("Successfully flagged URL \"<{}>\" as `approved:by_human`!", &url), true, true).await; @@ -345,7 +345,7 @@ pub async fn bk_week_update( let mut p_text = "Fetching new posts & updating data file...".to_string(); let progress = send_msg(ctx, p_text.clone(), true, true).await; - send_cmd_json("add_new_posts", json!([])).await; + send_cmd_json("add_new_posts", None).await; data::update_re_data(ctx.data()).await; let r_data = get_reddit_data(ctx).await.unwrap(); @@ -363,7 +363,7 @@ pub async fn bk_week_update( let msgs_json = msgs_to_json(msgs, &r_data).await; p_text = update_progress(ctx, progress.clone().unwrap(), p_text.clone(), "✅\nAdding new posts...".to_string()).await; - let weekly_art = r_data["bk_weekly_art_posts"].as_object().unwrap(); + let weekly_art = r_data[BK_WEEK].as_object().unwrap(); for url in weekly_art.keys() { if ["no_change", "updated", "removed"] @@ -504,7 +504,7 @@ async fn msgs_to_json<'a>(msgs: Vec, reddit_data: &'a Value) -> Value { if msg_json.is_err() { continue; } let mut u_json: Value = msg_json.unwrap(); - let re_url = &reddit_data["bk_weekly_art_posts"][&url]; + let re_url = &reddit_data[BK_WEEK][&url]; if re_url.get("removed").is_some() { if u_json.get("removed").is_some() { @@ -545,6 +545,7 @@ async fn msgs_to_json<'a>(msgs: Vec, reddit_data: &'a Value) -> Value { #[poise::command(slash_command, prefix_command)] +/// 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, @@ -554,7 +555,7 @@ pub async fn bk_week_vote( data::update_re_data(ctx.data()).await; let uid = ctx.author().id.get(); let re_data = get_reddit_data(ctx).await.unwrap(); - let post_data = re_data["bk_weekly_art_posts"].clone(); + let post_data = re_data[BK_WEEK].clone(); let unw_vote = un_vote.unwrap_or_else(|| false); if post_data.get(&url).is_none() { @@ -582,7 +583,7 @@ pub async fn bk_week_vote( return Ok(()); } - let r = send_cmd_json("set_vote_post", 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]))).await.unwrap(); let unw_r = r["value"].as_bool().unwrap(); if unw_r && !unw_vote && is_mod { diff --git a/src/cmds.rs b/src/cmds.rs index 303b520..409be0c 100644 --- a/src/cmds.rs +++ b/src/cmds.rs @@ -8,7 +8,6 @@ use crate::messages::{edit_msg, send_embed, send_msg, Author, EmbedOptions}; use poise::serenity_prelude::{OnlineStatus, Timestamp, UserId}; use rand::{seq::IteratorRandom, Rng}; use regex::Regex; -use serde_json::json; #[poise::command(slash_command, prefix_command)] @@ -40,7 +39,7 @@ pub async fn stop( let msg = send_msg(ctx, "Saving data...".to_string(), true, true).await.unwrap(); data::write_dc_data(ctx.data()).await; data::write_re_data().await; - send_cmd_json("stop_praw", json!([])).await; + send_cmd_json("stop_praw", None).await; edit_msg(ctx, msg, "Saving data... Done!\nShutting down...".to_string()).await; ctx.serenity_context().set_presence(None, OnlineStatus::Invisible); @@ -153,12 +152,10 @@ pub async fn re_shorturl( #[description = "A Reddit post URL"] url: String ) -> Result<(), Error> { - 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); - send_msg(ctx, format!("ShortURL: <{}>", short_url), true, true).await; + let shorturl = to_shorturl(&url); + + if shorturl.is_ok() { + send_msg(ctx, format!("ShortURL: <{}>", shorturl.unwrap()), true, true).await; } else { send_msg(ctx, "Couldn't convert to shortURL: Invalid URL".to_string(), true, true).await; @@ -168,6 +165,19 @@ pub async fn re_shorturl( } +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, default_member_permissions = "ADMINISTRATOR", guild_only)] /// 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( diff --git a/src/data.rs b/src/data.rs index 84707fb..61d53b3 100644 --- a/src/data.rs +++ b/src/data.rs @@ -96,13 +96,13 @@ fn generate_re_data() { pub async fn update_re_data(data: &Data) { - send_cmd_json("update_data_file", json!([])).await; + send_cmd_json("update_data_file", None).await; read_re_data(data, false).await; } pub async fn write_re_data() { - send_cmd_json("update_data_file", json!([])).await; + send_cmd_json("update_data_file", None).await; } diff --git a/src/python/cmds.py b/src/python/cmds.py new file mode 100644 index 0000000..b934d73 --- /dev/null +++ b/src/python/cmds.py @@ -0,0 +1,78 @@ +import asyncpraw as praw +import asyncpraw.models as models + +from macros import * +import bot as botPy +import data +import posts + + +BOT_ACTION_POSTFIX = "\n\n^(I am not an AI, I am just a bot. This action was preformed automatically by the way.)" + + +async def make_cmd(cmd: str, bot: botPy.Bot) -> str: + return f"u/{await bot.r.user.me()} {cmd}" + + +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() + + +async def respond_to_mention(bot: botPy.Bot) -> bool: + async for mention in bot.r.inbox.mentions(limit=25): + if not mention.new: + continue + + max_len = 100 + body = mention.body + truncated = body[:max_len] + "..." if len(body) > max_len else body + + py_print(f"New mention: {truncated}") + + if await is_cmd("bk_week_add", body, bot): + await bk_week_add(mention, bot) + + else: + await mention.mark_read() + + return True + + +async def bk_week_add(mention: models.Comment, bot: botPy.Bot): + await mention.submission.load() + + author = mention.author + + is_op = author == mention.submission.author + is_mod = author in await mention.subreddit.moderator() + + if not is_op and not is_mod: + await mention.mark_read() + return + + short_url = mention.submission.shortlink + + r = "" + bd = bot.data[data.BK_WEEKLY] + + 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! Glad to see you back!" + else: + 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]) + 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!" + BOT_ACTION_POSTFIX) + await mention.mark_read() \ No newline at end of file diff --git a/src/python/macros.py b/src/python/macros.py index 3ac0d16..239822d 100644 --- a/src/python/macros.py +++ b/src/python/macros.py @@ -1,13 +1,13 @@ from printColors import PrintColors -def py_print(*args: str): +def py_print(*args): print( PrintColors.FG.blue + "Py", "-", " ".join(args) + PrintColors.Special.reset ) -def py_error(*args: str): +def py_error(*args): print( PrintColors.BG.red + "ERROR" + PrintColors.Special.reset, PrintColors.FG.blue + "Py", diff --git a/src/python/main.py b/src/python/main.py index 44a4042..d5ac40f 100644 --- a/src/python/main.py +++ b/src/python/main.py @@ -1,13 +1,11 @@ import sys import asyncio -import threading import time from macros import * import bot as botPy import data import py_websocket -import posts async def main(): diff --git a/src/python/posts.py b/src/python/posts.py index 707f5b6..a8d4ca7 100644 --- a/src/python/posts.py +++ b/src/python/posts.py @@ -119,7 +119,7 @@ def get_post_details(post: models.Submission, added_by_h: bool = False) -> data. ) -async def add_post_url(bot, url: str, approve: bool, added_by_h: bool = False) -> bool: +async def add_post_url(bot, url: str, approve: bool = False, added_by_h: bool = False) -> bool: result, post = await from_url(bot, url) if not result: diff --git a/src/python/py_websocket.py b/src/python/py_websocket.py index adc5500..65a3c27 100644 --- a/src/python/py_websocket.py +++ b/src/python/py_websocket.py @@ -6,6 +6,7 @@ from macros import * import bot as botPy import data import posts +import cmds ws_global = None is_connected = False @@ -66,26 +67,25 @@ async def json_to_func(v: dict, bot: botPy.Bot) -> dict: return value_supported = True - result = {"type": "result", "value": False} + r = False match v["value"]: - case "update_data_file": result = result_json(data.write_data(bot)) - case "add_new_posts": result = result_json(await posts.add_new_posts(bot)) - case "add_post_url": result = result_json(await posts.add_post_url(bot, *v["args"])) - case "remove_post_url": result = result_json(data.remove_post(bot, *v["args"])) - case "set_approve_post": result = result_json(data.set_approve_post(bot, *v["args"])) - case "set_vote_post": result = result_json(data.set_vote_post(bot, *v["args"])) - case "stop_praw": result = result_json(await bot.stop()) + case "update_data_file": r = data .write_data (bot) + case "add_new_posts": r = await posts.add_new_posts (bot) + 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 "respond_mentions": r = await cmds .respond_to_mention(bot) + case "stop_praw": r = await bot .stop() case _: value_supported = False - if not isinstance(result.get("value"), bool): - py_print("Result JSON is invalid:", str(result)) - - if bot.args["dev"] and not value_supported: + if not value_supported: val = v["value"] py_print(f"Value \"{val}\" is not supported") + return {"type": "result", "value": False} - return result + return result_json(r) def result_json(bool: bool) -> dict: diff --git a/src/websocket.rs b/src/websocket.rs index 4356c70..05392c0 100644 --- a/src/websocket.rs +++ b/src/websocket.rs @@ -6,7 +6,7 @@ use tokio_tungstenite::WebSocketStream; use tokio_tungstenite::{accept_async, tungstenite}; use futures::StreamExt; use std::sync::Arc; -use serde_json::Value; +use serde_json::{Value, json}; use crate::messages::send_dm; use crate::rs_println; @@ -46,15 +46,17 @@ pub async fn send_msg(msg: &str) { #[allow(static_mut_refs)] -pub async fn send_cmd_json(func_name: &str, func_args: Value) -> Option { +pub async fn send_cmd_json(func_name: &str, func_args: Option) -> Option { unsafe { let Some(sender) = &GLOBAL_SENDER else { return None }; let mut sender = sender.lock().await; let Some(s) = sender.as_mut() else { return None }; + let unw_args = if func_args.is_some() { func_args.unwrap() } else { json!([]) }; + let json_str = format!( "json:{{\"type\": \"function\", \"value\":\"{}\", \"args\": {}}}", - func_name, func_args + func_name, unw_args ); if s.send(tungstenite::Message::Text(json_str.into())).await.is_err() {