diff --git a/Cargo.toml b/Cargo.toml index a732d9a..53528d7 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -11,6 +11,7 @@ futures = "0.3.31" poise = "0.6.1" pyo3 = "0.23.4" rand = "0.9.0" +regex = "1.11.1" serde = "1.0.217" serde_json = "1.0.138" tokio = { version = "1.43.0", features = ["rt-multi-thread"] } diff --git a/src/bk_week_cmds.rs b/src/bk_week_cmds.rs index e4b1d52..057d75b 100644 --- a/src/bk_week_cmds.rs +++ b/src/bk_week_cmds.rs @@ -1,11 +1,11 @@ -use crate::{rs_println, Context, Error}; +use crate::{rs_println, websocket, 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; +use serde_json::{json, Value}; #[poise::command(slash_command, prefix_command)] @@ -53,6 +53,7 @@ async fn get_post_from_data(ctx: Context<'_>, reddit_data: &Value, url: &str) -> if let Some(post) = bk_week.get(url) { if post.get("removed").is_some() { send_post_removed_message(ctx, url).await; + return Ok(None); } return Ok(Some(post.clone())); } @@ -71,18 +72,18 @@ async fn get_post_from_data(ctx: Context<'_>, reddit_data: &Value, url: &str) -> 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: ||`{}`|| + r#"**Spoilers and vote length anonymizer for fair review!** + Upvotes: ||`{:>6}`|| URL: ||<{}>|| - Added by human: `{}` - Added by bot: `{}` - Approved by human: `{}` + Added by human: {} + Added by bot: {} + Approved by human: {} Approved by bot: `[not implemented]`"#, - post["post_data"]["upvotes"], + post["post_data"]["upvotes"].as_i64().unwrap(), url, - post["added"]["by_human"], - post["added"]["by_bot"], - post["approved"]["by_human"] + if post["added"] ["by_human"].as_bool().unwrap() { "✅" } else { "❌" }, + if post["added"] ["by_bot"].as_bool().unwrap() { "✅" } else { "❌" }, + if post["approved"]["by_human"].as_bool().unwrap() { "✅" } else { "❌" } ).trim().to_string(), title: Some(post["post_data"]["title"].as_str().unwrap().to_string()), url: Some(url.to_string()), @@ -143,16 +144,43 @@ async fn send_data_corrupted_message(ctx: Context<'_>, url: &str) { #[poise::command(slash_command, prefix_command)] pub async fn bk_week_add( ctx: Context<'_>, - #[description = "The post URL"] url: Option, + #[description = "The post URL"] url: String, #[description = "Wether to approve it after adding it"] approve: Option ) -> Result<(), Error> { - // update data - // use python_comms.rs to tell python to update its data + // TODO: auto approve + data::update_re_data(ctx.data()).await; + let reddit_data = get_reddit_data(ctx).await.unwrap(); + + if let Some(bk_week) = reddit_data.get("bk_weekly_art_posts") { + websocket::send_cmd_json("add_post_url", json!([&url])).await; + + if let Some(post) = bk_week.get(&url) { + if post.get("removed").is_some() { + send_unremove_msg(ctx, &url).await; + } + else { + send_updated_msg(ctx, &url).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)] pub async fn bk_week_remove( ctx: Context<'_>, @@ -165,6 +193,8 @@ pub async fn bk_week_remove( } + + #[poise::command(slash_command, prefix_command)] pub async fn bk_week_approve( ctx: Context<'_>, @@ -177,6 +207,8 @@ pub async fn bk_week_approve( } + + #[poise::command(slash_command, prefix_command)] pub async fn bk_week_disapprove( ctx: Context<'_>, diff --git a/src/cmds.rs b/src/cmds.rs index 2ce510a..1ac825b 100644 --- a/src/cmds.rs +++ b/src/cmds.rs @@ -5,6 +5,7 @@ use crate::messages::{send_embed, send_msg, edit_msg, EmbedOptions}; use poise::serenity_prelude::{GetMessages, OnlineStatus, Timestamp, UserId}; use rand::{seq::IteratorRandom, Rng}; +use regex::Regex; #[poise::command(slash_command, prefix_command)] @@ -185,6 +186,27 @@ pub async fn write_json( } +#[poise::command(slash_command, prefix_command)] +pub async fn re_shorturl( + ctx: Context<'_>, + #[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; + } + else { + println!("Post ID not found in the URL"); + } + + return Ok(()); +} + + /* async fn autocomplete_rule_list(_: Context<'_>, _partial: &str) -> Vec { let json_str = std::fs::read_to_string("./data/write_json.json") .expect("No JSON preset file exists."); diff --git a/src/main.rs b/src/main.rs index d70ec50..f76f4fe 100644 --- a/src/main.rs +++ b/src/main.rs @@ -136,9 +136,11 @@ async fn gen_bot(data: Data) -> Client { cmds::stop(), cmds::eight_ball(), cmds::write_json(), + cmds::re_shorturl(), //cmds::rule(), bk_week_cmds::bk_week_help(), - bk_week_cmds::bk_week_get() + bk_week_cmds::bk_week_get(), + bk_week_cmds::bk_week_add() ], event_handler: events::event_handler, ..Default::default() diff --git a/src/python/data.py b/src/python/data.py index fd68cef..d25ceb9 100644 --- a/src/python/data.py +++ b/src/python/data.py @@ -85,13 +85,19 @@ def read_data(bot: botPy.Bot): raise Exception("reddit_data.json file wasn't created properly. Delete the file and retry.") -def write_data(bot: botPy.Bot): +def write_data(bot: botPy.Bot) -> bool: bot.data_f.seek(0) json.dump(bot.data, bot.data_f, indent=2) bot.data_f.truncate() + return True -def add_post_to_data(bot: botPy.Bot, new_data: PostData) -> bool: +def add_post_to_data(bot: botPy.Bot, new_data: PostData, bypass_conditions: bool = False) -> bool: + if bypass_conditions: + bot.data[BK_WEEKLY][new_data.url] = new_data.to_json() + py_print(f"Added post \"{new_data.url}\" (Conditions bypassed)") + return True + if new_data.url not in bot.data[BK_WEEKLY]: bot.data[BK_WEEKLY][new_data.url] = new_data.to_json() py_print(f"Added post \"{new_data.url}\"") diff --git a/src/python/main.py b/src/python/main.py index ef1ecd2..99c533e 100644 --- a/src/python/main.py +++ b/src/python/main.py @@ -7,7 +7,6 @@ from macros import * import bot as botPy import data import py_websocket -import posts def main(): diff --git a/src/python/posts.py b/src/python/posts.py index 24cac92..2ecdf8c 100644 --- a/src/python/posts.py +++ b/src/python/posts.py @@ -1,15 +1,17 @@ import emoji -import reddit +from praw import models + import data import bot as botPy from macros import * + def add_new_posts(bot: botPy.Bot): check_emoji = emoji.emojize(":check_mark_button:") cross_emoji = emoji.emojize(":cross_mark:") py_print("Fetching posts...") - posts = reddit.fetch_posts_with_flair(bot, "Original Art") + posts = fetch_posts_with_flair(bot, "Original Art") py_print("Evaluating posts...") @@ -17,16 +19,17 @@ def add_new_posts(bot: botPy.Bot): without_media = 0 not_added = 0 for post in posts: - media = reddit.has_media(post) + media = has_media(post) media_urls = "\n ".join(media[3]) - 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 {media_urls}\n" - ) + 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 {media_urls}\n" + ) if not media[0]: without_media += 1 @@ -34,15 +37,7 @@ def add_new_posts(bot: botPy.Bot): post_added = data.add_post_to_data( bot, - data.PostData( - post.shortlink, - post.title, - post.score, - int(post.created_utc), - media[1], - media[3], - added_by_bot = True, - ) + get_post_details(post) ) if post_added: added_posts += 1 @@ -53,4 +48,79 @@ def add_new_posts(bot: botPy.Bot): f" {without_media} had no media, " + f"and {not_added} weren't added because they are removed or already existed") - data.write_data(bot) \ No newline at end of file + data.write_data(bot) + + +def fetch_posts_with_flair(bot: botPy.Bot, flair_name: str) -> list[models.Submission]: + posts: list[models.Submission] = [] + + # ~36 OG-art posts per week, round limit to 50, 75 or 100 + for post in bot.sr.search(f"flair:\"{flair_name}\"", sort="new", limit=10): + posts.append(post) + + return posts + + +def has_media(post: models.Submission) -> tuple[bool, str, int, list[str]]: + media_type: str = None + media_count = 0 + media_urls: list[str] = [] + + if hasattr(post, "post_hint"): + media_type = post.post_hint + media_count = 1 + media_urls.append(post.url) + + elif getattr(post, "is_gallery", False): + if not post.is_gallery: pass + media_type = "multiple" + + gallery_items = getattr(post, "gallery_data", {}).get("items", []) + media_metadata = getattr(post, "media_metadata", {}) + + media_count = len(gallery_items) + + for item in gallery_items: + media_id = item.get("media_id") + image_url = media_metadata.get(media_id, {}).get("s", {}).get("u") + + if image_url: + media_urls.append(image_url) + + + return (media_type != None, media_type, media_count, media_urls) + + +# TODO: convert to asyncpraw because praw wont SHUT THE FUCK UP +# Gosh i gotta handle so much pain dont i? +def from_url(bot: botPy.Bot, url: str) -> tuple[bool, models.Submission]: + post = bot.r.submission(url=url) + + if hasattr(post, "id"): + return True, post + else: + return False, None + + +def get_post_details(post: models.Submission): + media = has_media(post) + + return data.PostData( + post.shortlink, + post.title, + post.score, + int(post.created_utc), + media[1], + media[3], + added_by_bot = True, + ) + + +async def add_post_url(bot, url: str) -> bool: + result, post = from_url(bot, url) + + if not result: + return result + + post_data = get_post_details(post) + data.add_post_to_data(bot, post_data, True) \ No newline at end of file diff --git a/src/python/py_websocket.py b/src/python/py_websocket.py index 426907c..b81b46e 100644 --- a/src/python/py_websocket.py +++ b/src/python/py_websocket.py @@ -5,6 +5,7 @@ import json from macros import py_print import bot as botPy import data +import posts ws_global = None is_connected = False @@ -25,15 +26,17 @@ async def websocket_client(bot: botPy.Bot): while True: response = await ws.recv() py_print(f"Received from Rust: {response}") - parse_json(response, bot) + await parse_json(response, bot) -def parse_json(response: str, bot: botPy.Bot): +async def parse_json(response: str, bot: botPy.Bot): if response.startswith("json:"): json_str = response[5:] try: json_response = json.loads(json_str) - json_to_func(json_response, bot) + result = await json_to_func(json_response, bot) + 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}") @@ -44,7 +47,7 @@ def run_thread(bot: botPy.Bot): loop.run_until_complete(websocket_client(bot)) -def json_to_func(v: dict, bot: botPy.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): if bot.args["dev"]: py_print("JSON is not a dictionary or does not include \"type\" and \"value\" keys.") return @@ -53,9 +56,14 @@ def json_to_func(v: dict, bot: botPy.Bot): return value_supported = True + result = {"type": "result", "value": False} + match v["value"]: - case "update_data_file": data.write_data(bot) + case "update_data_file": result = {"type": "result", "value": data.write_data(bot)} + case "add_post_url": result = {"type": "result", "value": await posts.add_post_url(bot, *v["args"])} case _: value_supported = False if bot.args["dev"] and not value_supported: - py_print(f"Value {v['value']} is not supported") \ No newline at end of file + py_print(f"Value {v['value']} is not supported") + + return result \ No newline at end of file diff --git a/src/websocket.rs b/src/websocket.rs index 646416e..4985061 100644 --- a/src/websocket.rs +++ b/src/websocket.rs @@ -1,6 +1,8 @@ +use futures::stream::SplitStream; use tokio::sync::Mutex; use futures::SinkExt; -use tokio::net::TcpListener; +use tokio::net::{TcpListener, TcpStream}; +use tokio_tungstenite::WebSocketStream; use tokio_tungstenite::{accept_async, tungstenite}; use futures::StreamExt; use std::sync::Arc; @@ -10,8 +12,10 @@ use crate::rs_println; use crate::Args; type Sender = Arc, tungstenite::Message>>>>; +type Receiver = Arc>>>>; static mut GLOBAL_SENDER: Option = None; +static mut GLOBAL_RECEIVER: Option = None; static mut REPLY_HELLO: bool = false; @@ -20,6 +24,11 @@ async fn set_sender(sender: Sender) { GLOBAL_SENDER = Some(sender); } } +async fn set_receiver(receiver: Receiver) { + unsafe { + GLOBAL_RECEIVER = Some(receiver); + } +} pub async fn send_msg(msg: &str) { @@ -34,17 +43,41 @@ pub async fn send_msg(msg: &str) { } -pub async fn send_cmd_json(func_name: &str, func_args: Value) { +pub async fn send_cmd_json(func_name: &str, func_args: Value) -> Option { unsafe { - if let Some(sender) = &GLOBAL_SENDER { - let mut sender = sender.lock().await; - if let Some(s) = sender.as_mut() { - let json_str: String = format!( - "json:{{\"type\": \"function\", \"value\":\"{}\", \"args\": {}}}", - func_name, func_args - ); - s.send(tungstenite::Message::Text(json_str.into())).await.unwrap(); - } + let Some(sender) = &GLOBAL_SENDER else { return None }; + let mut sender = sender.lock().await; + let Some(s) = sender.as_mut() else { return None }; + + let json_str = format!( + "json:{{\"type\": \"function\", \"value\":\"{}\", \"args\": {}}}", + func_name, func_args + ); + + if s.send(tungstenite::Message::Text(json_str.into())).await.is_err() { + return None; + } + + let r = receive_response().await; + return r; + } +} + + +async fn receive_response() -> Option { + unsafe { + let Some(receiver) = &GLOBAL_RECEIVER else { return None }; + let mut receiver = receiver.lock().await; + let Some(r) = receiver.as_mut() else { return None }; + + let Some(Ok(msg)) = r.next().await else { return None }; + let tungstenite::Message::Text(response) = msg else { return None }; + + if response.starts_with("json:") { + return serde_json::from_str(&response[5..]).ok(); + } + else { + return serde_json::from_str(&response).ok(); } } } @@ -63,12 +96,15 @@ pub async fn start(args: Args) { async fn handle_connections(listener: TcpListener, args: Args) { while let Ok((stream, _)) = listener.accept().await { let ws_stream = accept_async(stream).await.unwrap(); - let (sender, mut receiver) = ws_stream.split(); + let (sender, receiver) = ws_stream.split(); let sender_arc = Arc::new(Mutex::new(Some(sender))); - set_sender(sender_arc.clone()).await; + let receiver_arc = Arc::new(Mutex::new(Some(receiver))); - while let Some(Ok(msg)) = receiver.next().await { + set_sender(sender_arc.clone()).await; + set_receiver(receiver_arc.clone()).await; + + while let Some(Ok(msg)) = receiver_arc.lock().await.as_mut().unwrap().next().await { handle_message(msg, args.clone()).await; } }