major update thingy. Clippy is happy, bugfixes, new "max_results" arg to /re_updateDiscord, etc.

This commit is contained in:
2025-08-09 14:37:15 +02:00
parent b4044956de
commit 0db5937c39
17 changed files with 97 additions and 86 deletions
+1 -3
View File
@@ -9,9 +9,7 @@ status = "🎲 https://bytedice.net"
[reddit]
# Which subreddits the bot will scan when executing "re"-category commands.
# Is automatically disabled when `disabled_categories` includes "re".
# Separate multiple subreddits with a "+", e.g "memes+askreddit".
# [REQUIRES RESTART]
subreddits = "bytedicetesting"
subreddits = ["bytedicetesting"]
# How many posts the bot scans when running `/re_updatediscord`.
fetch_limit = 100
+1 -1
View File
@@ -164,7 +164,7 @@ fn separate_by_category(cmds: Vec<&Cmd>) -> Vec<(String, Vec<&Cmd>)> {
for cmd in cmds {
grouped
.entry(cmd.category.clone().unwrap_or("No category".to_string()))
.or_insert_with(Vec::new).push(cmd);
.or_default().push(cmd);
}
return grouped.into_iter().collect();
+1 -1
View File
@@ -14,7 +14,7 @@ pub async fn cmd(
ctx: Context<'_>
) -> Result<(), Error>
{
let r = read_cfg_data(&ctx.data(), false).await;
let r = read_cfg_data(ctx.data(), false).await;
let d = get_toml_mutex(&ctx.data().cfg).await.unwrap();
if r.is_none() { return Ok(()); }
+3 -2
View File
@@ -19,8 +19,9 @@ pub async fn cmd(
#[description = "Type \"i want to stop the bot now\" to confirm."] confirmation: Option<String>,
) -> Result<(), Error>
{
let should_stop = ctx.data().args.dev
|| confirmation.unwrap_or_default().to_lowercase() == "i want to stop the bot now";
let stop_confirm = "i want to stop the bot now".replace(" ", "");
let confirm_formatted = confirmation.unwrap_or_default().to_lowercase().replace(" ", "");
let should_stop = ctx.data().args.dev || confirm_formatted == stop_confirm;
if should_stop {
let msg = send_msg(ctx, lang!("dc_msg_owner_data_save"), true, true).await.unwrap();
+17 -19
View File
@@ -54,24 +54,24 @@ async fn handle_buttons(ctx: &serenity::Context, data: &Data, interaction: &Inte
let url = i_embed.url.clone().unwrap();
return match component.data.custom_id.as_str() {
"approve_btn" => approve_btn(ctx, data, &component.member.as_ref().unwrap(), component, url, true).await,
"remove_btn" => remove_btn (ctx, data, &component.member.as_ref().unwrap(), component, url, true).await,
"unapprove_btn" => approve_btn(ctx, data, &component.member.as_ref().unwrap(), component, url, false).await,
"unremove_btn" => remove_btn (ctx, data, &component.member.as_ref().unwrap(), component, url, false).await,
"unvote_btn" => vote_btn (ctx, data, &component.member.as_ref().unwrap(), component, url, false).await,
"vote_btn" => vote_btn (ctx, data, &component.member.as_ref().unwrap(), component, url, true).await,
"approve_btn" => approve_btn(ctx, data, component.member.as_ref().unwrap(), component, url, true).await,
"remove_btn" => remove_btn (ctx, data, component.member.as_ref().unwrap(), component, url, true).await,
"unapprove_btn" => approve_btn(ctx, data, component.member.as_ref().unwrap(), component, url, false).await,
"unremove_btn" => remove_btn (ctx, data, component.member.as_ref().unwrap(), component, url, false).await,
"unvote_btn" => vote_btn (ctx, data, component.member.as_ref().unwrap(), component, url, false).await,
"vote_btn" => vote_btn (ctx, data, component.member.as_ref().unwrap(), component, url, true).await,
_ => Err("Message button with that ID isn't handled.".into())
}
}
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); }
let e: EmbedOptions =
if remove { make_removed_embed(new_data, url, true) }
else { make_post_embed (new_data, url, true) };
serenity_edit_msg_embed(ctx, &c_id, &m_id, e).await;
serenity_edit_msg_embed(ctx, c_id, m_id, e).await;
}
@@ -106,14 +106,12 @@ async fn approve_btn(ctx: &serenity::Context, data: &Data, c_member: &Member, co
async fn remove_btn(ctx: &serenity::Context, data: &Data, c_member: &Member, component: &ComponentInteraction, url: String, remove: bool) -> Result<(), Error> {
if !is_bk_mod_serenity(ctx, data, c_member, component).await { return Ok(()); }
let r: Value;
if remove {
r = send_cmd_json("remove_post_url", Some(json!([&url, &c_member.user.name, None::<String>])), true).await.unwrap();
let r: Value = if remove {
send_cmd_json("remove_post_url", Some(json!([&url, &c_member.user.name, None::<String>])), true).await.unwrap()
}
else {
r = send_cmd_json("add_post_url", Some(json!([&url, false, true])), true).await.unwrap();
}
send_cmd_json("add_post_url", Some(json!([&url, false, true])), true).await.unwrap()
};
let c_id = component.channel_id;
let m_id = component.message.id;
@@ -157,11 +155,11 @@ async fn vote_btn(ctx: &serenity::Context, data: &Data, c_member: &Member, compo
serenity_send_msg(ctx, component, lang!("dc_msg_re_vote_remove_success"), true).await;
}
}
else {
if new_data["removed"]["removed"].as_bool().unwrap() {
else 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; }
else if !vote {
serenity_send_msg(ctx, component, lang!("dc_msg_re_vote_remove_havent"), true).await;
}
return Ok(());
+2 -2
View File
@@ -66,7 +66,7 @@ macro_rules! warnln {
macro_rules! lang {
($key:expr) => {
{
use crate::{LANG, errln};
use $crate::{LANG, errln};
let value = unsafe {
LANG
.as_ref()
@@ -80,7 +80,7 @@ macro_rules! lang {
}
};
($key:expr, $($arg:expr),*) => {{
use crate::{LANG, errln};
use $crate::{LANG, errln};
use formatx::formatx;
let value = unsafe {
+3 -3
View File
@@ -69,9 +69,9 @@ struct Args {
wipe: bool,
#[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 = "Adds annoying ping prints.")]
#[arg(long, help = "Adds annoying prints when the websockets send a ping. Why though?")]
ping: bool,
#[arg(long, help = "Makes the program not use the schedules.")]
#[arg(long, help = "Makes the program not use the schedule system.")]
nosched: bool
}
@@ -150,7 +150,7 @@ async fn main() {
let python_args = args.clone();
let rust_args = args.clone();
if !run_py { rs_println!("[IMPORTANT] You have disabled the \"re\" commands in the CFG. The app will not run the Python code nor the websockets to save resources!"); }
if !run_py { rs_println!("[IMPORTANT] You have disabled the \"re\" commands in the CFG. The app will not run the Python code and the websockets to save resources!"); }
let rust = thread::spawn(move || {
rt_rs.block_on(async {
+7 -6
View File
@@ -9,7 +9,8 @@ import toml
RE_DATA_POSTS: Final[str] = "posts"
CFG_DATA_RE: Final[str] = "reddit"
# TODO: add wipe arg
# TODO: add test-bot arg
class Bot:
args: dict[str, Any] = {"NO_RUST": True, "dev": True, "py": True, "port": 2920}
r_id: str | None = os.environ.get("ASSISTANT_R_ID")
@@ -21,7 +22,7 @@ class Bot:
useragent: str =\
f"{username} by u/RandomPersonDotExe aka u/Byte_Dice"\
if r_id == "YmZjr4zLr2qtHdpQXtj0sBOOdJzrXQ"\
if r_id == "YmZjr4zLr2qtHdpQXtj0sBOOdJzrXQ" or r_id == "Q-eBDGS8sFHlUCi9kpBepQ"\
else f"{username} (Original program by u/RandomPersonDotExe aka u/Byte_Dice)"
if password is None:
@@ -37,16 +38,13 @@ class Bot:
password = self.password,
user_agent = self.useragent
)
self.sr_list: list[str] = ["bytedicetesting"]
self.sr_list: list[str] = []
self.sr = None
self.data_f: TextIOWrapper | None = None
self.data: dict[str, Any] = {}
self.flairs: list[str] = []
self.aliases: dict[str, list[str]] = {}
async def initialize(self):
self.sr = await self.r.subreddit("+".join(self.sr_list))
async def set_args(self, args: dict[str, Any]):
self.args = args
@@ -67,5 +65,8 @@ class Bot:
self.fetch_limit = new_cfg[CFG_DATA_RE]["fetch_limit"]
self.flairs = new_cfg[CFG_DATA_RE]["search_flairs"]
self.aliases = new_cfg[CFG_DATA_RE]["aliases"]
self.sr_list = new_cfg[CFG_DATA_RE]["subreddits"]
self.sr = await self.r.subreddit("+".join(self.sr_list))
init_lang(new_cfg["general"]["lang"])
py_print("Successfully updated the configs!")
return True
+4 -6
View File
@@ -7,17 +7,13 @@ import bot as botPy
import py_data
import py_websocket
async def main():
sys.stdout.reconfigure(encoding="utf-8") # type: ignore
py_print("Creating Reddit bot...")
bot = botPy.Bot()
await bot.initialize()
py_print(f"Successfully created Reddit bot: {await bot.r.user.me()}")
# args is supposed to be undefined.
# args and lang_name are supposed to be undefined.
# It gets defined in Rust.
try:
await bot.set_args(args) # type: ignore
@@ -47,7 +43,9 @@ async def main():
if data_retries == 5 and not rd:
raise Exception("Couldn't read re_data.json: File doesn't exist")
py_print("Successfully read data!")
py_print("Successfully read all data!")
py_print(f"Successfully created Reddit bot: {await bot.r.user.me()}")
if not bot.args["py"]:
py_print("Connecting to local websocket...")
+23 -23
View File
@@ -9,40 +9,33 @@ import bot as botPy
from macros import *
async def add_new_posts(bot: botPy.Bot, max_age: int) -> bool:
async def add_new_posts(bot: botPy.Bot, max_age: int, max_results: int) -> bool:
check_emoji = emoji.emojize(":check_mark_button:")
cross_emoji = emoji.emojize(":cross_mark:")
py_print("Fetching posts...")
posts = await fetch_posts_with_flair(bot, bot.flairs)
posts = await fetch_posts_with_flair(bot, bot.flairs, max_age, max_results)
py_print("Evaluating posts...")
added_posts = 0
without_media = 0
not_added = 0
old_posts = 0
for post in posts:
media = has_media(post)
details = get_post_details(post)
media_urls = "\n ".join(media[3])
media_urls = "\n ".join(details.media_urls)
media_check = check_emoji if details.media_type is not None else cross_emoji
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{details.title}",
f"\n {details.url}"
f"\n {media_check} Media ({details.media_type}) [{len(details.media_urls)}]",
f"\n {media_urls}\n"
)
details = get_post_details(post)
now = int(time.time())
if now - details.date_unix > max_age and max_age > 0:
old_posts += 1
continue
if not media[0]:
if details.media_type is not None:
without_media += 1
continue
@@ -57,25 +50,32 @@ async def add_new_posts(bot: botPy.Bot, max_age: int) -> bool:
py_print(f"Successfully fetched {len(posts)} posts.\n" +
f" Out of which were {added_posts} added.\n" +
f" {without_media} had no media, " +
f"{not_added} are removed or already existed, " +
f"and {old_posts} were older than the max age threshold.")
f" {not_added} are removed or already existed, ")
py_data.write_data(bot)
return True
async def fetch_posts_with_flair(bot: botPy.Bot, flair_names: list[str]) -> list[models.Submission]:
async def fetch_posts_with_flair(
bot: botPy.Bot,
flair_names: list[str],
max_age_secs: int,
max_results: int
) -> list[models.Submission]:
posts: list[models.Submission] = []
flair_names_str = \
f"flair:{flair_names[0]}" if len(flair_names) == 1\
else " OR ".join(f"flair:{flair}" for flair in flair_names)
f"flair:{flair_names[0].replace(" ", "_")}" if len(flair_names) == 1\
else " OR ".join(f"flair:{flair.replace(" ", "_")}" for flair in flair_names)
if bot.sr is None: return []
# ~36 OG-art posts per week, round limit to 50, 75 or 100
async for post in bot.sr.search(f"{flair_names_str}", sort="new", limit=bot.fetch_limit):
now = int(time.time())
# ~20 OG-art posts per week, round limit to 50, 75 or 100 for 2 subreddits
async for post in bot.sr.search(f"{flair_names_str}", sort="new", limit=max_results):
if now - int(post.created_utc) > max_age_secs and max_age_secs > 0: continue
posts.append(post)
return posts
+1 -1
View File
@@ -55,7 +55,7 @@ pub async fn cmd(
if a { send_msg(ctx, lang!("dc_msg_re_also_approved"), true, true).await; }
}
if let Some(post) = get_post_from_data(ctx, &reddit_data, &shorturl).await? {
if let Some(post) = get_post_from_data(ctx, &reddit_data, shorturl).await? {
send_embed_for_post(ctx, post, &url).await?;
}
+1 -1
View File
@@ -26,7 +26,7 @@ pub async fn cmd(
data::update_re_data(ctx.data()).await;
let reddit_data = get_mutex_data(&ctx.data().reddit_data).await?;
approve_cmd(ctx, &shorturl, &reddit_data, !disapprove.unwrap_or(false)).await;
approve_cmd(ctx, shorturl, &reddit_data, !disapprove.unwrap_or(false)).await;
return Ok(());
}
+11 -4
View File
@@ -13,7 +13,7 @@ pub async fn is_bk_mod_msg(ctx: Context<'_>) -> bool {
if is_bk_mod(ctx.data().bk_mods.clone(), ctx.author().id.get()) { return true; }
let sr = get_readable_subreddits(ctx.data()).await.unwrap();
send_msg(ctx, lang!("dc_msg_re_permdeny_not_re_mod", sr), false, false).await;
send_msg(ctx, lang!("dc_msg_re_permdeny_not_re_mod", sr), true, true).await;
return false
}
@@ -71,9 +71,16 @@ pub async fn send_embed_for_removed(ctx: Context<'_>, url: &str, post: &Value) {
pub async fn get_readable_subreddits(data: &Data) -> Result<String, Error> {
let d = get_toml_mutex(&data.cfg).await.unwrap();
let sr = d["reddit"]["subreddits"].as_str().ok_or("Item of key \"subreddit\" is not a string type.\nTrace: `get_readable_subreddits -> let sr = ...`")?;
let split: Vec<&str> = sr.split("+").collect();
let join = split.join(", r/");
let sr = d["reddit"]["subreddits"].as_array().unwrap();
let sr_str: Vec<&str> = sr
.iter()
.map(|v| v.as_str().unwrap())
.collect();
let mut join = sr_str.join(", r/");
if join.len() != 0 { join = format!("r/{}", join); }
else { join = "[no subreddits assigned]".to_string(); }
return Ok(join);
}
+2 -2
View File
@@ -24,8 +24,8 @@ pub async fn cmd(
let reddit_data = get_mutex_data(&ctx.data().reddit_data).await?;
if let Some(post) = get_post_from_data(ctx, &reddit_data, &shorturl).await? {
send_embed_for_post(ctx, post, &shorturl).await?;
if let Some(post) = get_post_from_data(ctx, &reddit_data, shorturl).await? {
send_embed_for_post(ctx, post, shorturl).await?;
}
return Ok(());
+2 -2
View File
@@ -39,9 +39,9 @@ pub async fn cmd(
data::update_re_data(ctx.data()).await;
let reddit_data = get_mutex_data(&ctx.data().reddit_data).await?;
if let Some(post) = get_post_from_data(ctx, &reddit_data, &shorturl).await? {
if let Some(post) = get_post_from_data(ctx, &reddit_data, shorturl).await? {
if post["removed"]["removed"].as_bool().unwrap() {
send_embed_for_removed(ctx, &shorturl, &post).await;
send_embed_for_removed(ctx, shorturl, &post).await;
}
}
+13 -5
View File
@@ -3,7 +3,7 @@ use std::time::{SystemTime, UNIX_EPOCH};
use poise::{serenity_prelude::{ChannelId, EditMessage, GetMessages, Http, Message, MessageId, UserId}, ReplyHandle};
use serde_json::{json, Map, Value};
use crate::{data::{self, get_mutex_data, DC_POSTS_CHANNEL_KEY}, lang, messages::{edit_reply, embed_from_options, make_post_embed, make_removed_embed, send_embed, send_msg, trim_post_json}, re_cmds::generic_fns::embed_to_json, rs_println, websocket::send_cmd_json, Context, Error, CFG_DATA_RE};
use crate::{data::{self, get_mutex_data, get_toml_mutex, DC_POSTS_CHANNEL_KEY}, lang, messages::{edit_reply, embed_from_options, make_post_embed, make_removed_embed, send_embed, send_msg, trim_post_json}, re_cmds::generic_fns::embed_to_json, rs_println, websocket::send_cmd_json, Context, Error, CFG_DATA_RE};
#[poise::command(
slash_command,
@@ -17,12 +17,16 @@ use crate::{data::{self, get_mutex_data, DC_POSTS_CHANNEL_KEY}, lang, messages::
/// Updates the bound Discord channel with the bot's current Reddit data.
pub async fn cmd(
ctx: Context<'_>,
#[description = "Only adds new posts, leaves everything else unchanged."]
#[description = "Make this true to only add new posts and leave everything else unchanged."]
only_add: Option<bool>,
#[description = "The max age of a post (in days). Any post older than this will be removed. (0 is infinite.)"]
#[description = "The max age of a post in days. Any post older than this will be removed. (0 is infinite)"]
#[min = 0]
#[max = 65535]
max_age: Option<u16>
max_age: Option<u16>,
#[description = "The max amount of posts to fetch (no value uses default value)."]
#[min = 1]
#[max = 100]
max_results: Option<u16>
) -> Result<(), Error>
{
let http = ctx.http();
@@ -35,7 +39,11 @@ pub async fn cmd(
let max_age_u = max_age.unwrap_or(8);
let max_age_secs = max_age_u as u64 * (60 * 60 * 24);
send_cmd_json("add_new_posts", Some(json!([max_age_secs])), true).await;
let max_results_toml = &get_toml_mutex(&ctx.data().cfg).await.unwrap();
let max_results_pre = max_results_toml["reddit"]["fetch_limit"].as_integer().unwrap();
let max_results_final = max_results.unwrap_or(max_results_pre as u16);
send_cmd_json("add_new_posts", Some(json!([max_age_secs, max_results_final])), true).await;
data::update_re_data(ctx.data()).await;
let r_data = get_mutex_data(&ctx.data().reddit_data).await?;
+2 -2
View File
@@ -25,12 +25,12 @@ pub async fn cmd(
let shorturl_u = to_shorturl(&url);
let shorturl = &shorturl_u.unwrap_or(url.clone());
if post_data.get(&shorturl).is_none() {
if post_data.get(shorturl).is_none() {
send_msg(ctx, lang!("dc_msg_re_post_404"), false, false).await;
return Ok(());
}
if post_data[&shorturl]["removed"]["removed"].as_bool().unwrap() {
send_embed_for_removed(ctx, &shorturl, &post_data[&shorturl]).await;
send_embed_for_removed(ctx, shorturl, &post_data[&shorturl]).await;
return Ok(());
}