diff --git a/.gitignore b/.gitignore index 1a34f67..60675f9 100644 --- a/.gitignore +++ b/.gitignore @@ -7,4 +7,5 @@ __pycache__/ .vscode/ # program-created data -data/reddit_data.json \ No newline at end of file +data/reddit_data.json +data/discord_data.json \ No newline at end of file diff --git a/TODO.md b/TODO.md index f0508f3..c6fac16 100644 --- a/TODO.md +++ b/TODO.md @@ -16,11 +16,13 @@ - [ ] Automate - [ ] Automatically approve posts that dont get caught by reverse image search (ris) - [ ] Log all posts in a Discord thread - - [ ] `/bk_week_get` command - - [ ] Send all posts data as embeds - - [ ] Compare all posts in the JSON with the posts in the channel - - [ ] If the JSON is empty, remove the entire channel and make a new one - - [ ] Else, remove each embed and add new ones to be up-to-date with the JSON + - [ ] `/bk_week_bind` to bind a channel for bk_week logs + - [ ] Add post if it exists in data but not in channel + - [ ] Edit post if it exists in channel and is different in data + - [ ] Remove post if its `"removed": true` in data + - [ ] Add posts to data from channel + - [ ] `/bk_week_update` to forcefully trigger this ^ + - [x] `/bk_week_get [url]` get the data of a single post from the data ### Medium priority: - [x] ~~JSON -> Rules list~~ diff --git a/data/discord_data_preset.json b/data/discord_data_preset.json new file mode 100644 index 0000000..e5dccbf --- /dev/null +++ b/data/discord_data_preset.json @@ -0,0 +1,11 @@ +{ + "servers": { + "SERVER ID": { + "bk_week_channel": "CHANNEL ID INT", + "bk_week_users": [ + "USER ID 1", + "USER ID 2" + ] + } + } +} \ No newline at end of file diff --git a/src/bk_week_cmds.rs b/src/bk_week_cmds.rs index 1ad2abf..e4b1d52 100644 --- a/src/bk_week_cmds.rs +++ b/src/bk_week_cmds.rs @@ -1,11 +1,13 @@ -use serde_json::json; - -use crate::websocket::send_cmd_json; -use crate::{Context, Error}; -use crate::messages::send_msg; +use crate::{rs_println, Context, Error}; +use crate::messages::{send_embed, send_msg, EmbedOptions}; +use crate::data; use std::fs; +use poise::serenity_prelude::Timestamp; +use serde_json::Value; + + #[poise::command(slash_command, prefix_command)] pub async fn bk_week_help( ctx: Context<'_>, @@ -13,21 +15,130 @@ pub async fn bk_week_help( { let help = fs::read_to_string("./bk_week_help.md").unwrap(); send_msg(ctx, help, true, true).await; + data::read_dc_data(ctx.data()); return Ok(()); } + + #[poise::command(slash_command, prefix_command)] pub async fn bk_week_get( ctx: Context<'_>, - #[description = "The post URL"] url: Option -) -> Result<(), Error> -{ - send_cmd_json("update_data_file", json!([])).await; - return Ok(()); + #[description = "The post URL"] url: String +) -> Result<(), Error> { + data::update_re_data(ctx.data()).await; + + let reddit_data = get_reddit_data(ctx).await?; + + if let Some(post) = get_post_from_data(ctx, &reddit_data, &url).await? { + send_embed_for_post(ctx, post, &url).await?; + } + + Ok(()) } +async fn get_reddit_data(ctx: Context<'_>) -> Result { + let data_lock = ctx.data().reddit_data.lock().unwrap(); + match data_lock.as_ref() { + Some(data) => Ok(data.clone()), + None => Err("Reddit data is corrupted".into()), + } +} + + +async fn get_post_from_data(ctx: Context<'_>, reddit_data: &Value, url: &str) -> Result, Error> { + if let Some(bk_week) = reddit_data.get("bk_weekly_art_posts") { + if let Some(post) = bk_week.get(url) { + if post.get("removed").is_some() { + send_post_removed_message(ctx, url).await; + } + 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).unwrap()); + } + return Ok(None); +} + + +async fn send_embed_for_post(ctx: Context<'_>, post: Value, url: &str) -> Result<(), Error> { + let embed_options = EmbedOptions { + desc: format!( + r#"**Spoilers for fair review!** + Upvotes: ||`{}`|| + URL: ||<{}>|| + Added by human: `{}` + Added by bot: `{}` + Approved by human: `{}` + Approved by bot: `[not implemented]`"#, + post["post_data"]["upvotes"], + url, + post["added"]["by_human"], + post["added"]["by_bot"], + post["approved"]["by_human"] + ).trim().to_string(), + title: Some(post["post_data"]["title"].as_str().unwrap().to_string()), + url: Some(url.to_string()), + ts: Some(Timestamp::from_unix_timestamp(post["post_data"]["date_unix"].as_i64().unwrap()).unwrap()), + empheral: true, + ..Default::default() + }; + + send_embed(ctx, embed_options, 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) { + send_msg( + ctx, + format!( + r#"Post url \"<{}>\" is removed: Post is removed from 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_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)] pub async fn bk_week_add( diff --git a/src/cmds.rs b/src/cmds.rs index c11e8e1..2ce510a 100644 --- a/src/cmds.rs +++ b/src/cmds.rs @@ -1,6 +1,6 @@ use std::process; -use crate::{Context, Error}; +use crate::{data, Context, Error}; use crate::messages::{send_embed, send_msg, edit_msg, EmbedOptions}; use poise::serenity_prelude::{GetMessages, OnlineStatus, Timestamp, UserId}; @@ -28,12 +28,17 @@ pub async fn stop( let should_stop = ctx.data().args.dev || confirmation.unwrap_or_else(|| "".to_string()).to_lowercase() == "i want to stop the bot now"; - let is_creator = ctx.author().id == UserId::new(ctx.data().creator_id); + let is_creator = ctx.author().id == UserId::new(ctx.data().byte_dice_id); if should_stop && is_creator { - send_msg(ctx, "Shutting down...".to_string(), true, true).await; + let msg = send_msg(ctx, "Saving data...".to_string(), true, true).await.unwrap(); + data::write_dc_data(ctx.data()); + data::write_re_data().await; + + edit_msg(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; + process::exit(0); } else if !is_creator { diff --git a/src/data.rs b/src/data.rs new file mode 100644 index 0000000..3b90f00 --- /dev/null +++ b/src/data.rs @@ -0,0 +1,96 @@ +use std::{fs, io::Write}; +use std::path::Path; + +use serde_json::{self, Value, json}; + +use crate::Data; +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"; + + +pub fn read_dc_data(data: &Data) { + if !Path::new(DATA_PATH_DC).exists() { + generate_dc_data(); + } + + let str_data = fs::read_to_string(DATA_PATH_DC).unwrap(); + let json_data = serde_json::from_str(&str_data).unwrap(); + let mut dc_data = data.discord_data.lock().unwrap(); + *dc_data = json_data; +} + + +fn generate_dc_data() { + let preset_str = fs::read_to_string(PRESET_PATH_DC).unwrap(); + let mut preset_json: Value = serde_json::from_str(&preset_str).unwrap(); + + if let Some(servers) = preset_json["servers"].as_object_mut() { + servers.remove("SERVER ID"); + } + + let json_str = serde_json::to_string_pretty(&preset_json).unwrap(); + + let mut file = fs::File::create(DATA_PATH_DC).unwrap(); + file.write_all(json_str.as_bytes()).unwrap(); +} + + +pub fn write_dc_data(data: &Data) { + if !Path::new(DATA_PATH_DC).exists() { + generate_dc_data(); + } + + let mut file = fs::OpenOptions::new() + .write(true) + .truncate(true) + .open(DATA_PATH_DC) + .unwrap(); + + let json_str = serde_json::to_string_pretty(&data.discord_data).unwrap(); + + file.write_all(json_str.as_bytes()).unwrap(); +} + + +pub fn read_re_data(data: &Data) { + if !Path::new(DATA_PATH_RE).exists() { + generate_re_data(); + } + + let str_data = fs::read_to_string(DATA_PATH_RE).unwrap(); + let json_data = serde_json::from_str(&str_data).unwrap(); + let mut re_data = data.reddit_data.lock().unwrap(); + *re_data = json_data; +} + + +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_weekly_art_posts"].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(); + + let mut file = fs::File::create(DATA_PATH_RE).unwrap(); + file.write_all(json_str.as_bytes()).unwrap(); +} + + +pub async fn update_re_data(data: &Data) { + send_cmd_json("update_data_file", json!([])).await; + read_re_data(data); +} + + +pub async fn write_re_data() { + send_cmd_json("update_data_file", json!([])).await; +} \ No newline at end of file diff --git a/src/macros.rs b/src/macros.rs index 0cc7c59..1e6e040 100644 --- a/src/macros.rs +++ b/src/macros.rs @@ -1,9 +1,10 @@ #[macro_export] macro_rules! rs_println { ($($arg:tt)*) => { - println!("{}RS - {}", + println!("{}RS - {}{}", "\x1b[31m", - format!($($arg)*) + format!($($arg)*), + "\x1b[0m" ); }; } @@ -12,10 +13,11 @@ macro_rules! rs_println { #[macro_export] macro_rules! rs_errln { ($($arg:tt)*) => { - println!("{}ERROR{} RS - {}", + println!("{}ERROR{} RS - {}{}", "\x1b[41m", "\x1b[0m\x1b[31m", - format!($($arg)*) + format!($($arg)*), + "\x1b[0m" ); process::exit(1); }; diff --git a/src/main.rs b/src/main.rs index 904f00f..d70ec50 100644 --- a/src/main.rs +++ b/src/main.rs @@ -8,9 +8,11 @@ mod messages; mod python; mod macros; mod websocket; +mod data; use std::process; use std::thread; +use std::sync::Mutex; use clap::Parser; use poise::serenity_prelude as serenity; @@ -35,8 +37,9 @@ struct Args { struct Data { ball_prompts: [Vec; 2], - creator_id: u64, - reddit_data: Option, + byte_dice_id: u64, + reddit_data: Mutex>, + discord_data: Mutex>, args: Args // TODO: schedules } @@ -100,12 +103,18 @@ fn gen_data(args: Args) -> Data { let ball_classic: Vec = ball_classic_str.lines().map(String::from).collect(); let ball_quirk: Vec = ball_quirk_str .lines().map(String::from).collect(); - return Data { + let data = Data { ball_prompts: [ball_classic, ball_quirk], - creator_id: 697149665166229614, - reddit_data: None, + byte_dice_id: 697149665166229614, + reddit_data: None.into(), + discord_data: None.into(), args }; + + data::read_dc_data(&data); + data::read_re_data(&data); + + return data; } diff --git a/src/messages.rs b/src/messages.rs index 9a6e21a..9bb7766 100644 --- a/src/messages.rs +++ b/src/messages.rs @@ -102,7 +102,4 @@ pub async fn edit_msg( }; let _ = msg.edit(ctx, r).await; - - let msg_text = &msg.message().await.unwrap().content; - rs_println!("Edited message: {} -> {}", msg_text, new_text); } \ No newline at end of file diff --git a/src/python/data.py b/src/python/data.py index 6e9012d..fd68cef 100644 --- a/src/python/data.py +++ b/src/python/data.py @@ -66,20 +66,26 @@ def read_data(bot: botPy.Bot): except FileNotFoundError: py_print("reddit_data.json not found, creating new from preset...") + with open(data_path + "\\reddit_data_preset.json", "r") as f: + data_preset_json = json.load(f) + + data_preset_json["bk_weekly_art_posts"].pop("EXAMPLE VALUE", None) + data_preset_json["bk_weekly_art_posts"].pop("EXAMPLE VALUE DELETED", None) + with open(data_path + "\\reddit_data.json", "w") as f: - f.write(open(data_path + "\\reddit_data_preset.json", "r").read()) + json.dump(data_preset_json, f, indent = 2) bot.data_f = open(data_path + "\\reddit_data.json", "r+") data_str = bot.data_f.read() - bot.data = json.loads(data_str) + json_data = json.loads(data_str) + bot.data = json_data if not bot.data["file_created_correctly"]: raise Exception("reddit_data.json file wasn't created properly. Delete the file and retry.") def write_data(bot: botPy.Bot): - bot.data["TEST"] = True bot.data_f.seek(0) json.dump(bot.data, bot.data_f, indent=2) bot.data_f.truncate() diff --git a/src/python/macros.py b/src/python/macros.py index 0390624..3ac0d16 100644 --- a/src/python/macros.py +++ b/src/python/macros.py @@ -4,7 +4,7 @@ def py_print(*args: str): print( PrintColors.FG.blue + "Py", "-", - " ".join(args) + " ".join(args) + PrintColors.Special.reset ) def py_error(*args: str): @@ -12,6 +12,6 @@ def py_error(*args: str): PrintColors.BG.red + "ERROR" + PrintColors.Special.reset, PrintColors.FG.blue + "Py", "-", - " ".join(args) + " ".join(args) + PrintColors.Special.reset ) quit() \ No newline at end of file diff --git a/src/python/main.py b/src/python/main.py index 0e8d103..ef1ecd2 100644 --- a/src/python/main.py +++ b/src/python/main.py @@ -7,6 +7,7 @@ from macros import * import bot as botPy import data import py_websocket +import posts def main(): @@ -32,4 +33,8 @@ def main(): time.sleep(1) continue - asyncio.run(py_websocket.send_message("[Connection test] Hello from Python!")) \ No newline at end of file + asyncio.run(py_websocket.send_message("[Connection test] Hello from Python!")) + + +if __name__ == "__main__": + main() \ No newline at end of file diff --git a/src/python/posts.py b/src/python/posts.py index e67c9e8..24cac92 100644 --- a/src/python/posts.py +++ b/src/python/posts.py @@ -4,7 +4,7 @@ import data import bot as botPy from macros import * -def add_new_posts(bot: botPy.Bot, debug_print: bool = False): +def add_new_posts(bot: botPy.Bot): check_emoji = emoji.emojize(":check_mark_button:") cross_emoji = emoji.emojize(":cross_mark:") @@ -21,7 +21,7 @@ def add_new_posts(bot: botPy.Bot, debug_print: bool = False): media_urls = "\n ".join(media[3]) - if debug_print: print( + 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]}]", diff --git a/src/python/py_websocket.py b/src/python/py_websocket.py index fcf4b55..426907c 100644 --- a/src/python/py_websocket.py +++ b/src/python/py_websocket.py @@ -58,4 +58,4 @@ def json_to_func(v: dict, bot: botPy.Bot): case _: value_supported = False if bot.args["dev"] and not value_supported: - print(f"Value {v['value']} is not supported") \ No newline at end of file + py_print(f"Value {v['value']} is not supported") \ No newline at end of file