diff --git a/README.md b/README.md index ab55817..8903711 100644 --- a/README.md +++ b/README.md @@ -11,6 +11,7 @@ An automation tool for Byte Dice. It's both a Discord and Reddit bot in one prog ## How to run **Dependecies:** 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: + * sys * io * asyncpraw diff --git a/TODO.md b/TODO.md index 86de13b..f6b9cbe 100644 --- a/TODO.md +++ b/TODO.md @@ -9,7 +9,7 @@ - [ ] `/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. - - [ ] 10-minute schedule for updating Discord channel + diff --git a/src/bk_week_cmds.rs b/src/bk_week_cmds.rs index 1ec856e..26d8743 100644 --- a/src/bk_week_cmds.rs +++ b/src/bk_week_cmds.rs @@ -1,13 +1,12 @@ use crate::websocket::send_cmd_json; -use crate::{rs_println, websocket, Context, Error, BK_WEEK}; -use crate::messages::{edit_msg, embed_from_options, embed_post, embed_post_removed, send_embed, send_msg}; +use crate::{rs_println, websocket, Context, Data, Error, BK_WEEK}; +use crate::messages::*; use crate::data::{self, dc_bind_bk}; use std::fs; -use poise::serenity_prelude::{ChannelId, EditMessage, GetMessages, Message, MessageId}; -use poise::ReplyHandle; -use serde_json::{json, Value}; +use poise::serenity_prelude::{ChannelId, EditMessage, GetMessages, Http, Message, MessageId, UserId}; +use serde_json::{json, Map, Value}; #[derive(poise::ChoiceParameter, PartialEq)] @@ -71,7 +70,7 @@ pub async fn bk_week_get( { data::update_re_data(ctx.data()).await; - let reddit_data = get_reddit_data(ctx).await?; + let reddit_data = get_reddit_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?; @@ -80,8 +79,8 @@ pub async fn bk_week_get( return Ok(()); } -async fn get_reddit_data(ctx: Context<'_>) -> Result { - let data_lock = ctx.data().reddit_data.lock().await; +pub async fn get_reddit_data(data: &Data) -> Result { + let data_lock = data.reddit_data.lock().await; return match data_lock.as_ref() { Some(data) => Ok(data.clone()), None => Err("Reddit data is corrupted".into()), @@ -174,7 +173,7 @@ pub async fn bk_week_add( } data::update_re_data(ctx.data()).await; - let reddit_data = get_reddit_data(ctx).await.unwrap(); + let reddit_data = get_reddit_data(ctx.data()).await.unwrap(); if let Some(bk_week) = reddit_data.get(BK_WEEK) { let a = approve.unwrap_or_else(|| false); @@ -273,7 +272,7 @@ pub async fn bk_week_approve( } data::update_re_data(ctx.data()).await; - let reddit_data = get_reddit_data(ctx).await.unwrap(); + let reddit_data = get_reddit_data(ctx.data()).await.unwrap(); approve_cmd(ctx, &url, &reddit_data, !disapprove.unwrap_or_else(|| false)).await; @@ -317,7 +316,7 @@ pub async fn bk_admin_bind( let c_id = ctx.channel_id().into(); let r = dc_bind_bk(ctx.data(), ctx.guild_id().unwrap().into(), c_id).await; - if r { + if r.is_ok() { send_msg(ctx, format!("Successfully bound channel ID `{}` as the bk_week channel!", c_id), true, true).await; } else { @@ -342,89 +341,80 @@ pub async fn bk_week_update( #[description = "Only adds new posts, leaves everything else unchanged."] only_add: Option ) -> Result<(), Error> { - let mut p_text = "Fetching new posts & updating data file...".to_string(); - let progress = send_msg(ctx, p_text.clone(), true, true).await; + let http = ctx.http(); + + let executed = format!("(Executed `/bk_week_update`, author: `{}`)", ctx.author().name); + let mut p_text = executed.clone(); + + send_msg(ctx, MANDATORY_MSG.to_string(), true, true).await; + + let progress = http_send_msg(http, ctx.channel_id(), p_text.clone()).await.unwrap(); + p_text = update_progress(ctx.http(), progress.clone(), p_text, "\nFetching new posts & updating data file...".to_string()).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(); + let r_data = get_reddit_data(ctx.data()).await.unwrap(); - let c_id = get_c_id(ctx).await.unwrap_or_else(|| 0); + let c_id_u = get_c_id(ctx).await; - if c_id == 0 { + 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(()); } - p_text = update_progress(ctx, progress.clone().unwrap(), p_text.clone(), format!("✅\nReading messages in <#{}>...", c_id)).await; - let msgs = read_msgs(ctx, c_id).await; + let c_id = c_id_u.unwrap(); - p_text = update_progress(ctx, progress.clone().unwrap(), p_text.clone(), "✅\nParsing messages to JSON...".to_string()).await; + // Reading messages + p_text = update_progress(http, 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(http, progress.clone(), p_text.clone(), "✅\nParsing messages to JSON...".to_string()).await; 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; + // Adding new posts + p_text = update_progress(http, 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).await; - for url in weekly_art.keys() { - if ["no_change", "updated", "removed"] - .iter() - .any(|key| msgs_json[key].as_object().unwrap().contains_key(url)) - { continue; } - if msgs_json["duplicates"].as_object().unwrap().contains_key(url) { continue; } - - if weekly_art[url].get("removed").is_some() { - send_embed(ctx, embed_post_removed(&weekly_art[url], url, false), false).await; - continue; - } - - send_embed(ctx, embed_post(&weekly_art[url], url, false), false).await; - } - + // Stop if only_add if only_add.unwrap_or_else(|| false) { - update_progress(ctx, progress.clone().unwrap(), p_text.clone(), "✅\n## Done!".to_string()).await; + send_msg(ctx, "`/bk_week_update`\n## Done!".to_string(), true, true).await; + update_progress(http, progress.clone(), String::new(), executed).await; return Ok(()); } - p_text = update_progress(ctx, progress.clone().unwrap(), p_text.clone(), "✅\nEditing updated posts...".to_string()).await; - for (url, msg_id) in msgs_json["updated"].as_object().unwrap() { - let mut msg = ctx.http().get_message(ctx.channel_id(), MessageId::new(msg_id.as_u64().unwrap())).await.unwrap(); - let r = EditMessage::new() - .embeds(vec![embed_from_options(embed_post(&weekly_art[url], url, false))]); - - let _ = msg.edit(ctx, r).await; - } + // Editing updated posts + p_text = update_progress(http, progress.clone(), p_text.clone(), "✅\nEditing updated posts...".to_string()).await; + edit_posts(http, c_id, weekly_art, &msgs_json).await; - p_text = update_progress(ctx, progress.clone().unwrap(), p_text.clone(), "✅\nRemoving removed posts...".to_string()).await; - for (url, msg_id) in msgs_json["removed"].as_object().unwrap() { - let mut msg = ctx.http().get_message(ctx.channel_id(), MessageId::new(msg_id.as_u64().unwrap())).await.unwrap(); - let r = EditMessage::new() - .embeds(vec![embed_from_options(embed_post_removed(&weekly_art[url], url, false))]); - - let _ = msg.edit(ctx, r).await; - } + // Removing removed posts + p_text = update_progress(http, progress.clone(), p_text.clone(), "✅\nRemoving removed posts...".to_string()).await; + remove_posts(http, c_id, weekly_art, &msgs_json).await; - p_text = update_progress(ctx, progress.clone().unwrap(), p_text.clone(), "✅\nRemoving duplicate posts...".to_string()).await; - for (_url, msgs) in msgs_json["duplicates"].as_object().unwrap() { - for msg_id in msgs.as_array().unwrap() { - let msg = ctx.http().get_message(ctx.channel_id(), MessageId::new(msg_id.as_u64().unwrap())).await.unwrap(); - let _ = msg.delete(ctx.http()).await; - } - } + // Removing duplicate posts + update_progress(http, progress.clone(), p_text.clone(), "✅\nRemoving duplicate posts...".to_string()).await; + remove_dupes(http, c_id, &msgs_json).await; - update_progress(ctx, progress.clone().unwrap(), p_text.clone(), "✅\n## Done!".to_string()).await; + // Done + send_msg(ctx, "`/bk_week_update`\n## Done!".to_string(), true, true).await; + update_progress(http, progress.clone(), String::new(), executed).await; return Ok(()); } -async fn update_progress(ctx: Context<'_>, p: ReplyHandle<'_>, t: String, a_t: String) -> String { - let p_text = format!("{} {}", t, a_t); - edit_msg(ctx, p, p_text.clone()).await; +async fn update_progress(http: &Http, p: Message, t: String, added_t: String) -> String { + let p_text = format!("{} {}", t, added_t); + + let new_msg = EditMessage::new().content(&p_text); + + http_edit_msg(http, p, new_msg).await; return p_text; } -async fn get_c_id(ctx: Context<'_>) -> Option { +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; @@ -432,27 +422,27 @@ async fn get_c_id(ctx: Context<'_>) -> Option { let d_lock = ctx.data().discord_data.lock().await; let d = d_lock.as_ref().unwrap(); - let c_id = + 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(ctx: Context<'_>, c_id: u64) -> Vec { - let c = ChannelId::new(c_id); - +pub async fn read_msgs(http: &Http, bot_id: UserId, c_id: ChannelId) -> Vec { let b = GetMessages::new().limit(100); - let mut msgs = c.messages(ctx.http(), b).await.unwrap(); - msgs = msgs.into_iter().filter(|item| item.author.id == ctx.framework().bot_id).collect(); + let mut msgs = c_id.messages(http, b).await.unwrap(); + msgs = msgs.into_iter().filter(|item| item.author.id == bot_id).collect(); 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.messages(ctx.http(), new_b).await.unwrap(); + let new_msgs = c_id.messages(http, new_b).await.unwrap(); last_msg = new_msgs.last().cloned(); @@ -462,7 +452,7 @@ async fn read_msgs(ctx: Context<'_>, c_id: u64) -> Vec { let filtered_msgs: Vec = new_msgs .into_iter() - .filter(|item| item.author.id == ctx.framework().bot_id) + .filter(|item| item.author.id == bot_id) .collect(); msgs.extend(filtered_msgs); @@ -472,7 +462,7 @@ async fn read_msgs(ctx: Context<'_>, c_id: u64) -> Vec { } -async fn msgs_to_json<'a>(msgs: Vec, reddit_data: &'a Value) -> Value { +pub async fn msgs_to_json<'a>(msgs: Vec, reddit_data: &'a Value) -> Value { let mut msgs_json: Value = json!({"no_change": {}, "updated": {}, "removed": {}, "duplicates": {}}); for msg in msgs { @@ -542,6 +532,54 @@ async fn msgs_to_json<'a>(msgs: Vec, reddit_data: &'a Value) -> Value { } +pub async fn add_posts(http: &Http, c_id: ChannelId, r_data: &Map, msgs_json: &Value) { + for url in r_data.keys() { + if ["no_change", "updated", "removed"] + .iter() + .any(|key| msgs_json[key].as_object().unwrap().contains_key(url)) + { continue; } + if msgs_json["duplicates"].as_object().unwrap().contains_key(url) { 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; + } +} + + +pub 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; + } +} + + +pub 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; + } +} + + +pub 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)] @@ -554,7 +592,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 re_data = get_reddit_data(ctx.data()).await.unwrap(); let post_data = re_data[BK_WEEK].clone(); let unw_vote = un_vote.unwrap_or_else(|| false); @@ -600,4 +638,4 @@ pub async fn bk_week_vote( } return Ok(()); -} \ No newline at end of file +} diff --git a/src/cmds.rs b/src/cmds.rs index 409be0c..534ed17 100644 --- a/src/cmds.rs +++ b/src/cmds.rs @@ -3,7 +3,7 @@ use std::process; use crate::data::dc_add_server; use crate::websocket::send_cmd_json; use crate::{data, Context, Error}; -use crate::messages::{edit_msg, send_embed, send_msg, Author, EmbedOptions}; +use crate::messages::{edit_reply, send_embed, send_msg, Author, EmbedOptions, MANDATORY_MSG}; use poise::serenity_prelude::{OnlineStatus, Timestamp, UserId}; use rand::{seq::IteratorRandom, Rng}; @@ -41,7 +41,7 @@ pub async fn stop( data::write_re_data().await; send_cmd_json("stop_praw", None).await; - edit_msg(ctx, msg, "Saving data... Done!\nShutting down...".to_string()).await; + edit_reply(ctx, msg, "Saving data... Done!\nShutting down...".to_string()).await; ctx.serenity_context().set_presence(None, OnlineStatus::Invisible); ctx.framework().shard_manager.shutdown_all().await; @@ -97,7 +97,7 @@ pub async fn embed( ).await; if !reply_unwrap { - send_msg(ctx, "Mandatory success response, please ignore.".to_string(), true, true).await; + send_msg(ctx, MANDATORY_MSG.to_string(), true, true).await; } return Ok(()); @@ -117,7 +117,7 @@ pub async fn send( ) -> Result<(), Error> { send_msg(ctx, msg.replace("\\n", "\n"), false, false).await; - send_msg(ctx, "Mandatory success response, please ignore.".to_string(), true, true).await; + send_msg(ctx, MANDATORY_MSG.to_string(), true, true).await; return Ok(()); } @@ -186,7 +186,7 @@ pub async fn add_server( { let r = dc_add_server(ctx.data(), ctx.guild_id().unwrap().into()).await; - if r { + 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; } else { diff --git a/src/data.rs b/src/data.rs index 61d53b3..b1ae7f7 100644 --- a/src/data.rs +++ b/src/data.rs @@ -55,10 +55,11 @@ pub async fn write_dc_data(data: &Data) { .open(DATA_PATH_DC) .unwrap(); + let mut dc_data_lock = data.discord_data.lock().await; let dc_data = dc_data_lock.as_mut().unwrap(); let json_str = serde_json::to_string_pretty(dc_data).unwrap(); - + file.write_all(json_str.as_bytes()).unwrap(); } @@ -106,39 +107,43 @@ pub async fn write_re_data() { } -pub async fn dc_add_server(data: &Data, server_id: u64) -> bool { +pub async fn dc_add_server(data: &Data, server_id: u64) -> Result<(), ()> { let mut dc_data_lock = data.discord_data.lock().await; let dc_data = dc_data_lock.as_mut().unwrap(); - if dc_data.get("servers").is_none() { return false; } + if dc_data.get("servers").is_none() { return Err(()); } 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": [] })); } + println!("moo"); + write_dc_data(data).await; + println!("muu"); - return true; + return Ok(()); } -pub async fn dc_bind_bk(data: &Data, server_id: u64, channel_id: u64) -> bool { +pub async fn dc_bind_bk(data: &Data, server_id: u64, channel_id: u64) -> Result<(), ()> { let mut dc_data_lock = data.discord_data.lock().await; let dc_data = dc_data_lock.as_mut().unwrap(); - if dc_data.get("servers").is_none() { return false; } + if dc_data.get("servers").is_none() { return Err(()); } let servers = dc_data["servers"].as_object_mut().unwrap(); if !servers.contains_key(&server_id.to_string()) { - return false; + return Err(()); } let server = servers[&server_id.to_string()].as_object_mut().unwrap(); server.insert("bk_week_channel".to_string(), channel_id.into()); + write_dc_data(data).await; - return true; + return Ok(()); } diff --git a/src/main.rs b/src/main.rs index 5862358..7cb8cea 100644 --- a/src/main.rs +++ b/src/main.rs @@ -6,6 +6,7 @@ mod events; mod messages; mod python; mod macros; +#[allow(unknown_lints)] mod websocket; mod data; @@ -14,6 +15,7 @@ use std::pin::Pin; use std::process; use std::thread; use std::time::Duration; +use std::vec; use clap::Parser; use poise::serenity_prelude as serenity; @@ -46,7 +48,9 @@ struct Args { #[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 + noping: bool, + #[arg(long, help = "Makes the program not use the schedules.")] + nosched: bool } @@ -75,6 +79,7 @@ async fn main() { 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.py && !args.rs { println!("----- PYTHON ONLY MODE -----"); @@ -96,11 +101,12 @@ async fn main() { let rt = Runtime::new().unwrap(); let python_args = args_str; + let rust_args = args.clone(); let rust = thread::spawn(move || { rt.block_on(async { - websocket::start(args.clone()).await; - start(args).await; + websocket::start(rust_args.clone()).await; + start(rust_args).await; }); }); @@ -108,12 +114,13 @@ async fn main() { let _ = python::start(python_args); }); - let schedules: Vec<(Duration, fn() -> Pin + Send>>)> = vec![ - (Duration::from_secs(/* 2 * 60 */ 30), || Box::pin(read_reddit_inbox())), - (Duration::from_secs(/* 10 * 60 */ 60), || Box::pin(update_post_channels())) - ]; + if !args.nosched { + let schedules: Vec<(Duration, fn() -> Pin + Send>>)> = vec![ + (Duration::from_secs(/* 2 * 60 */ 30), || Box::pin(read_reddit_inbox())) + ]; - run_schedules(schedules).await; + run_schedules(schedules).await; + } rust.join().unwrap(); python.join().unwrap(); @@ -236,8 +243,4 @@ async fn run_schedules(schedules: Vec<(Duration, fn() -> Pin) -> String { @@ -76,6 +77,20 @@ pub async fn send_msg( } +pub async fn http_send_msg( + http: &Http, + c_id: ChannelId, + t: String +) -> Option +{ + let r = CreateMessage::new().content(t); + + let msg = c_id.send_message(http, r).await; + + return msg.ok(); +} + + pub async fn send_embed( ctx: Context<'_>, options: EmbedOptions, @@ -103,6 +118,21 @@ pub async fn send_embed( } +pub async fn http_send_embed( + http: &Http, + c_id: ChannelId, + options: EmbedOptions +) -> Option +{ + let embed = embed_from_options(options.clone()); + + let r = CreateMessage::new().embeds(vec![embed]); + + let msg = c_id.send_message(http, r).await; + return msg.ok(); +} + + pub fn embed_from_options(options: EmbedOptions) -> CreateEmbed { let mut author: Option = None; if let Some(o_author) = options.author { @@ -123,7 +153,7 @@ pub fn embed_from_options(options: EmbedOptions) -> CreateEmbed { } -pub async fn edit_msg( +pub async fn edit_reply( ctx: Context<'_>, msg: ReplyHandle<'_>, new_text: String @@ -137,6 +167,15 @@ pub async fn edit_msg( } +pub async fn http_edit_msg( + http: &Http, + mut msg: Message, + new_msg: EditMessage +) { + let _ = msg.edit(http, new_msg).await; +} + + pub async fn send_dm(msg: String, args: Args) { let uid = env::var("ASSISTANT_DM_USER").expect("Missing ASSISTANT_DM_USER env var!").parse::().unwrap(); let user = UserId::new(uid); diff --git a/src/python/cmds.py b/src/python/cmds.py index b934d73..1ceea64 100644 --- a/src/python/cmds.py +++ b/src/python/cmds.py @@ -7,7 +7,7 @@ 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.)" +BOT_ACTION_POSTFIX = "\n\n^(I am not an AI, I am just a bot. This action was performed automatically by the way.)" async def make_cmd(cmd: str, bot: botPy.Bot) -> str: @@ -55,14 +55,14 @@ async def bk_week_add(mention: models.Comment, bot: botPy.Bot): r = "" bd = bot.data[data.BK_WEEKLY] - + # TODO: ask if the messages should be changed 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!" + r = "(Mod action) Successfully un-removed this post from the data!" else: r = "(Mod action) Successfully added this post to the data!"