added support for language files
This commit is contained in:
@@ -9,4 +9,6 @@ __pycache__/
|
||||
# program-created data
|
||||
data/reddit_data.json
|
||||
data/discord_data.json
|
||||
data/re_data.json
|
||||
data/dc_data.json
|
||||
data/cfg.json
|
||||
@@ -7,6 +7,7 @@ edition = "2021"
|
||||
|
||||
[dependencies]
|
||||
clap = { version = "4.5.28", features = ["derive"] }
|
||||
formatx = "0.2.3"
|
||||
futures = "0.3.31"
|
||||
poise = "0.6.1"
|
||||
pyo3 = "0.23.4"
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
### High priority:
|
||||
<!-- - [x] ~~Embed creation tool~~ -->
|
||||
- [ ] Reddit bot that scrapes images with tag "Original Art" and posts them in Discord server
|
||||
- [ ] Rename commands from "bk_week_{name}" to "re_{name}"
|
||||
<!-- - [x] ~~Discord bot /bk_help command~~ -->
|
||||
<!-- - [x] ~~Scrape the data~~ -->
|
||||
<!-- - [x] ~~Put it in a JSON~~ -->
|
||||
@@ -10,7 +11,7 @@
|
||||
<!-- - [x] ~~`/bk_week_top [category] [amount]` to get the top N posts in a category (e.g upvotes)~~ -->
|
||||
<!-- - [x] ~~`/bk_cfg_sr [subreddit]` to change the target subreddit(s)~~ -->
|
||||
- [ ] Allow disabling bk_week
|
||||
- [ ] Language files?
|
||||
- [ ] Language files
|
||||
- [ ] Allow updating the data autonomously and via manual commands.
|
||||
<!-- - [ ] 10-minute schedule for updating Discord channel (IMPOSSIBLE / REALLY FUCKING HARD) -->
|
||||
<!-- - [x] ~~Manually add posts~~ -->
|
||||
|
||||
@@ -0,0 +1,62 @@
|
||||
{
|
||||
"8-ball_answer": "Q: {0}\nA: {1}",
|
||||
"add_to_data": "Added your server to my data! Thanks for letting me steal it! (/s)",
|
||||
"bound_dc_channel": "Successfully bound channel ID `{0}` as the bk_week channel!",
|
||||
"cfg_data_404": "cfg.json not found,",
|
||||
"cfg_data_wipe": "[WIPE] (cfg.json)",
|
||||
"cmd_404": "No command with the name \"{0}\" found!\nHint: Try `/help` without any arguments or `/help <category>`",
|
||||
"corrupted_data": "Oopsies `(。>\\\\<)`. It looks like my data i-is \\**sob*\\*... c-corrupted!",
|
||||
"couldnt_shorturl": "Couldn't convert to shortURL: Invalid URL",
|
||||
"creating_data_file": "{0} creating new from preset...",
|
||||
"data_post_embed": "Spoilers and vote length anonymizer for fair review!\n ## Post Data:\n **Post upvotes:** ||`{0:>6}`||\n **Moderator votes:** ||`{1:>6}`||\n **Media type:** `{2}`\n **URL:** ||<{3}>||\n\n ## Listing Data:\n **Added by:** `{{ human: {4}, bot: {5} }}`\n **Approved by:** `{{ human: {6}, bot: [not implemented] }}`",
|
||||
"data_post_removed_embed": "## Removed by `{0}`\n**Reason:** {1}\nURL: ||<{2}>||\n\nJSON: ||`{3}`||",
|
||||
"data_save_complete": "Saving data... Done!\nShutting down...",
|
||||
"data_save_progress": "Saving data...",
|
||||
"dc_404": "Your server is not in the data!\n Hint: Run the command `/add_server` inside of a Discord server.",
|
||||
"dc_bot_started": "Bot started as user \"{0}\" with id {1}",
|
||||
"dc_bot_starting": "Starting bot...",
|
||||
"dc_data_404": "discord_data.json not found,",
|
||||
"dc_data_wipe": "[WIPE] (discord_data.json)",
|
||||
"default_embed_desc": "Default description",
|
||||
"err_read_py_file": "Failed to read Python file!\nPath: {0}",
|
||||
"help_text_removed": "Help text not found. Someone deleted it. :(",
|
||||
"invalid_confirm_shutdown": "Failed to shut down: Invalid confirmation.",
|
||||
"invalid_os": "Man what kinda OS do you have? Neither unix or windows, what the hell!? I can't process this anymore, you're too weird!",
|
||||
"lang_data_404": "file for language \"{}\" not found!",
|
||||
"lang_load_success": "Successfully loaded the english language file!",
|
||||
"mandatory_response": "Mandatory response message, please ignore.",
|
||||
"python_err": "Unknown internal Python Error: {0}",
|
||||
"python_socket_binary_response": "[Binary] from Python: {0}",
|
||||
"python_socket_err": "Unknown internal Python error occurred: Websocket response error.",
|
||||
"python_socket_null": "Response from Python is None!",
|
||||
"python_socket_response": "Received from Python: [RESPONSE] {0}",
|
||||
"python_socket_unknown_response": "Received from Python: [UNKNOWN / OTHER]",
|
||||
"re_404": "Post URL \"<{0}>\" not found: Post doesn't exist in the data!\n Hint: Run the command `/re_addPost [URL]` in a Discord channel or `u/ByteDiceAssistant bk_week_add` in a Reddit post.",
|
||||
"re_add_post_success": "Added post with URL \"<{0}>\"!",
|
||||
"re_already_voted": "Couldn't cast a vote: You have already voted on this post!",
|
||||
"re_also_approved": "Also approved it!",
|
||||
"re_approved_post_success": "Successfully approved the post!",
|
||||
"re_data_404": "reddit_data.json not found,",
|
||||
"re_data_corrupted": "Post URL \"<{0}>\" not found: Post data is corrupted!\n Full details: Could not find key \"bk_weekly_art_posts\" in data file \"reddit_data.json\"",
|
||||
"re_data_wipe": "[WIPE] (reddit_data.json)",
|
||||
"re_disapproved_post_success": "Successfully disapproved the post!",
|
||||
"re_err_trace": "Unknown error!\nError trace: {0}.",
|
||||
"re_havent_voted_remove_vote": "Couldn't remove your vote: You haven't voted on this post yet!",
|
||||
"re_permdeny_bk_mod": "Permission denied: You are not a moderator of {0}",
|
||||
"re_remove_post_success": "Successfully removed post!",
|
||||
"re_unremove_post_success": "Successfully un-removed post with URL \"<{0}>\"!",
|
||||
"re_update_post_success": "Updated post with URL \"<{0}>\"!",
|
||||
"re_vote_err": "Failed to vote/un-vote: Unknown internal error",
|
||||
"re_vote_remove_success": "Successfully removed vote!",
|
||||
"re_vote_success_mod": "Successfully voted (as moderator vote)!",
|
||||
"re_vote_success": "Successfully voted!",
|
||||
"reload_config_python_fail": "Failed to reload configs: Failed-type response from Python.",
|
||||
"reload_config_success": "Successfully reloaded the configs!\nNew configs:\n```\n{0}\n```",
|
||||
"shorturl": "ShortURL: <{0}>",
|
||||
"socket_received_python": "Received from Python: {0}",
|
||||
"socket_rust_connection_test": "[Connection test] Hello from Rust!",
|
||||
"started_socket": "WebSocket server running on ws://{0}",
|
||||
"starting_python_program": "Running Python program...",
|
||||
"starting_schedules": "Starting schedules...",
|
||||
"starting_socket": "Starting local websocket..."
|
||||
}
|
||||
@@ -1,752 +0,0 @@
|
||||
use crate::websocket::send_cmd_json;
|
||||
use crate::{cmds, rs_println, websocket, Context, Error, BK_WEEK};
|
||||
use crate::messages::*;
|
||||
use crate::data::{self, dc_bind_bk, get_mutex_data};
|
||||
|
||||
use std::collections::HashMap;
|
||||
use std::time::{SystemTime, UNIX_EPOCH};
|
||||
|
||||
use poise::serenity_prelude::{ChannelId, EditMessage, GetMessages, Http, Message, MessageId, UserId};
|
||||
use poise::ReplyHandle;
|
||||
use serde_json::{json, Map, Value};
|
||||
|
||||
|
||||
#[derive(poise::ChoiceParameter, PartialEq)]
|
||||
enum TopCategory {
|
||||
Upvotes,
|
||||
ModVotes,
|
||||
Oldest,
|
||||
Newest
|
||||
}
|
||||
|
||||
|
||||
fn is_bk_mod(mod_list: Vec<u64>, uid: u64) -> bool {
|
||||
return mod_list.contains(&uid);
|
||||
}
|
||||
|
||||
|
||||
async fn not_bk_mod_msg(ctx: Context<'_>) {
|
||||
send_msg(ctx, "Permission denied: You are not a moderator of r/boykisser or r/boykisser2".to_string(), true, true).await;
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
#[poise::command(
|
||||
slash_command,
|
||||
prefix_command,
|
||||
category = "bk_week",
|
||||
required_bot_permissions = "SEND_MESSAGES | VIEW_CHANNEL | EMBED_LINKS"
|
||||
)]
|
||||
/// Fetches the data of a single post, just for you. The data has to be within the database to work.
|
||||
pub async fn bk_week_get(
|
||||
ctx: Context<'_>,
|
||||
#[description = "The post URL."] url: String
|
||||
) -> Result<(), Error>
|
||||
{
|
||||
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, &url).await? {
|
||||
send_embed_for_post(ctx, post, &url).await?;
|
||||
}
|
||||
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
|
||||
async fn get_post_from_data(ctx: Context<'_>, reddit_data: &Value, url: &str) -> Result<Option<Value>, Error> {
|
||||
if let Some(bk_week) = reddit_data.get(BK_WEEK) {
|
||||
if let Some(post) = bk_week.get(url) {
|
||||
if post.get("removed").is_some() {
|
||||
send_post_removed_message(ctx, url, post).await;
|
||||
return Ok(None);
|
||||
}
|
||||
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)?);
|
||||
}
|
||||
return Ok(None);
|
||||
}
|
||||
|
||||
|
||||
async fn send_embed_for_post(ctx: Context<'_>, post: Value, url: &str) -> Result<(), Error> {
|
||||
send_embed(ctx, embed_post(&post, url, true), 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, post: &Value) {
|
||||
send_embed(
|
||||
ctx,
|
||||
embed_post_removed(post, url, 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,
|
||||
category = "bk_week",
|
||||
required_bot_permissions = "SEND_MESSAGES | VIEW_CHANNEL"
|
||||
)]
|
||||
/// Fetches a post from Reddit and adds it to the database.
|
||||
pub async fn bk_week_add(
|
||||
ctx: Context<'_>,
|
||||
#[description = "The post URL."] url: String,
|
||||
#[description = "Wether to approve it after adding it"] approve: Option<bool>
|
||||
) -> Result<(), Error>
|
||||
{
|
||||
if !is_bk_mod(ctx.data().bk_mods.clone(), ctx.author().id.get()) {
|
||||
not_bk_mod_msg(ctx).await;
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
let shorturl_u = cmds::to_shorturl(&url);
|
||||
let shorturl = &shorturl_u.unwrap_or(url.clone());
|
||||
|
||||
data::update_re_data(ctx.data()).await;
|
||||
let reddit_data = get_mutex_data(&ctx.data().reddit_data).await?;
|
||||
|
||||
if let Some(bk_week) = reddit_data.get(BK_WEEK) {
|
||||
let a = approve.unwrap_or(false);
|
||||
let r = websocket::send_cmd_json("add_post_url", Some(json!([&shorturl, a, true]))).await.unwrap();
|
||||
|
||||
if !r["value"].as_bool().unwrap() {
|
||||
send_msg(
|
||||
ctx,
|
||||
r#"Unknown error!
|
||||
Error trace: `bk_week_cmds.rs -> bk_week_add() -> Unknown error`.
|
||||
Common reasons: The URL provided was likely invalid or 403: forbidden (e.g a private subreddit)."#.to_string(),
|
||||
true,
|
||||
true
|
||||
).await;
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
if let Some(post) = bk_week.get(shorturl) {
|
||||
if post.get("removed").is_some() {
|
||||
send_unremove_msg(ctx, shorturl).await;
|
||||
}
|
||||
else {
|
||||
send_updated_msg(ctx, shorturl).await;
|
||||
}
|
||||
}
|
||||
else {
|
||||
send_msg(ctx, format!("Added post with URL \"<{}>\"!", &shorturl), true, true).await;
|
||||
}
|
||||
|
||||
if a {
|
||||
send_msg(ctx, "Also approved it!".to_string(), true, true).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,
|
||||
category = "bk_week",
|
||||
required_bot_permissions = "SEND_MESSAGES | VIEW_CHANNEL"
|
||||
)]
|
||||
/// Removes a post from the database. It will show who last removed it.
|
||||
pub async fn bk_week_remove(
|
||||
ctx: Context<'_>,
|
||||
#[description = "The post URL."] url: String,
|
||||
#[description = "The reason of the removal."] reason: Option<String>
|
||||
) -> Result<(), Error>
|
||||
{
|
||||
if !is_bk_mod(ctx.data().bk_mods.clone(), ctx.author().id.get()) {
|
||||
not_bk_mod_msg(ctx).await;
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
let auth = &ctx.author().name;
|
||||
let r = send_cmd_json("remove_post_url", Some(json!([&url, &auth, &reason]))).await.unwrap();
|
||||
|
||||
if r["value"].as_bool().unwrap() {
|
||||
send_msg(
|
||||
ctx,
|
||||
"Successfully flagged the post as removed!".to_string(),
|
||||
true,
|
||||
true
|
||||
).await;
|
||||
}
|
||||
else {
|
||||
send_post_not_found_message(ctx, &url).await;
|
||||
}
|
||||
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
#[poise::command(
|
||||
slash_command,
|
||||
prefix_command,
|
||||
category = "bk_week",
|
||||
required_bot_permissions = "SEND_MESSAGES | VIEW_CHANNEL"
|
||||
)]
|
||||
/// Approves a post in the database. Approving posts tells the bot that it's original.
|
||||
pub async fn bk_week_approve(
|
||||
ctx: Context<'_>,
|
||||
#[description = "The post URL."] url: String,
|
||||
#[description = "Wether to approve or disapprove the post"] disapprove: Option<bool>
|
||||
) -> Result<(), Error>
|
||||
{
|
||||
if !is_bk_mod(ctx.data().bk_mods.clone(), ctx.author().id.get()) {
|
||||
not_bk_mod_msg(ctx).await;
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
data::update_re_data(ctx.data()).await;
|
||||
let reddit_data = get_mutex_data(&ctx.data().reddit_data).await?;
|
||||
|
||||
approve_cmd(ctx, &url, &reddit_data, !disapprove.unwrap_or(false)).await;
|
||||
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
|
||||
async fn approve_cmd(ctx: Context<'_>, url: &str, reddit_data: &Value, approve: bool) {
|
||||
if let Some(post) = reddit_data.get(BK_WEEK).unwrap().get(url) {
|
||||
if post.get("removed").is_some() {
|
||||
send_post_removed_message(ctx, url, post).await;
|
||||
return;
|
||||
}
|
||||
|
||||
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, "Successfully flagged the post as approved (by a human)!".to_string(), true, true).await;
|
||||
}
|
||||
else {
|
||||
send_msg(ctx, "Successfully removed the \"approved (by a human)\" flag from the post!".to_string(), true, true).await;
|
||||
}
|
||||
}
|
||||
else {
|
||||
send_msg(ctx, "Unknown error!\nError trace: `bk_week_cmds.rs -> bk_week_approve() -> unwrap websocket result error`.".to_string(), true, true).await;
|
||||
}
|
||||
}
|
||||
else {
|
||||
send_post_not_found_message(ctx, url).await;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
#[poise::command(
|
||||
slash_command,
|
||||
prefix_command,
|
||||
category = "admin",
|
||||
default_member_permissions = "ADMINISTRATOR",
|
||||
guild_only,
|
||||
required_bot_permissions = "SEND_MESSAGES | VIEW_CHANNEL"
|
||||
)]
|
||||
/// Sets the channel where the bot will dump all log info. It's recommended to only run this once.
|
||||
pub async fn bk_admin_bind(
|
||||
ctx: Context<'_>
|
||||
) -> Result<(), Error>
|
||||
{
|
||||
let c_id = ctx.channel_id().into();
|
||||
let r = dc_bind_bk(ctx.data(), ctx.guild_id().unwrap().into(), c_id).await;
|
||||
|
||||
if r.is_ok() {
|
||||
send_msg(ctx, format!("Successfully bound channel ID `{}` as the bk_week channel!", c_id), true, true).await;
|
||||
}
|
||||
else {
|
||||
send_server_not_in_data_msg(ctx).await;
|
||||
}
|
||||
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
|
||||
async fn send_server_not_in_data_msg(ctx: Context<'_>) {
|
||||
send_msg(ctx, "Your server is not in the data!\nHint: Run the command `/add_server` inside of a Discord server.".to_string(), true, true).await;
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
#[poise::command(
|
||||
slash_command,
|
||||
prefix_command,
|
||||
category = "bk_week",
|
||||
guild_only,
|
||||
guild_cooldown = 120,
|
||||
required_bot_permissions = "SEND_MESSAGES | VIEW_CHANNEL | READ_MESSAGE_HISTORY | EMBED_LINKS"
|
||||
)]
|
||||
/// Updates all logs
|
||||
pub async fn bk_week_update(
|
||||
ctx: Context<'_>,
|
||||
#[description = "Only adds new posts, leaves 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.)"]
|
||||
#[min = 0]
|
||||
#[max = 65535]
|
||||
max_age: Option<u16>
|
||||
) -> Result<(), Error>
|
||||
{
|
||||
let http = ctx.http();
|
||||
|
||||
let mut p_text = "`/bk_week_update`:".to_string();
|
||||
|
||||
let progress = send_msg(ctx, p_text.clone(), true, true).await.unwrap();
|
||||
p_text = update_progress(ctx, progress.clone(), p_text, "\nFetching new posts & updating data file...".to_string()).await;
|
||||
|
||||
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]))).await;
|
||||
data::update_re_data(ctx.data()).await;
|
||||
let r_data = get_mutex_data(&ctx.data().reddit_data).await?;
|
||||
|
||||
let c_id_u = get_c_id(ctx).await;
|
||||
|
||||
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(());
|
||||
}
|
||||
|
||||
let c_id = c_id_u.unwrap();
|
||||
|
||||
// Reading messages
|
||||
p_text = update_progress(ctx, 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(ctx, progress.clone(), p_text.clone(), "✅\nParsing messages to JSON...".to_string()).await;
|
||||
let msgs_json = msgs_to_json(msgs, &r_data, max_age_secs).await;
|
||||
|
||||
// Adding new posts
|
||||
p_text = update_progress(ctx, 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, max_age_secs).await;
|
||||
|
||||
// Stop if only_add
|
||||
if only_add.unwrap_or(false) {
|
||||
send_msg(ctx, "`/bk_week_update`\n## Done!".to_string(), true, true).await;
|
||||
update_progress(ctx, progress.clone(), p_text, "✅\n## Done!".to_string()).await;
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
// Editing updated posts
|
||||
p_text = update_progress(ctx, progress.clone(), p_text.clone(), "✅\nEditing updated posts...".to_string()).await;
|
||||
edit_posts(http, c_id, weekly_art, &msgs_json).await;
|
||||
|
||||
// Removing removed posts
|
||||
p_text = update_progress(ctx, progress.clone(), p_text.clone(), "✅\nRemoving removed posts...".to_string()).await;
|
||||
remove_posts(http, c_id, weekly_art, &msgs_json).await;
|
||||
|
||||
// Removing old posts
|
||||
if max_age_u > 0 {
|
||||
p_text = update_progress(ctx, progress.clone(), p_text.clone(), format!("✅\nRemoving old posts (threshold: {}d)...", max_age_u)).await;
|
||||
remove_old(http, c_id, &msgs_json).await;
|
||||
send_cmd_json("remove_old_posts", Some(json!([max_age_secs]))).await;
|
||||
}
|
||||
|
||||
// Removing duplicate posts
|
||||
p_text = update_progress(ctx, progress.clone(), p_text.clone(), "✅\nRemoving duplicate posts...".to_string()).await;
|
||||
remove_dupes(http, c_id, &msgs_json).await;
|
||||
|
||||
// Done
|
||||
update_progress(ctx, progress.clone(), p_text, "✅\n## Done!".to_string()).await;
|
||||
send_msg(ctx, "`/bk_week_update`\n## Done!".to_string(), true, true).await;
|
||||
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
|
||||
async fn update_progress(ctx: Context<'_>, p: ReplyHandle<'_>, t: String, added_t: String) -> String {
|
||||
let p_text = format!("{} {}", t, added_t);
|
||||
|
||||
edit_reply(ctx, p, p_text.clone()).await;
|
||||
return p_text;
|
||||
}
|
||||
|
||||
|
||||
async fn get_c_id(ctx: Context<'_>) -> Option<ChannelId> {
|
||||
if !data::dc_contains_server(ctx.data(), ctx.guild_id().unwrap().into()).await {
|
||||
send_server_not_in_data_msg(ctx).await;
|
||||
return None;
|
||||
}
|
||||
|
||||
let d = get_mutex_data(&ctx.data().discord_data).await.unwrap();
|
||||
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(http: &Http, bot_id: UserId, c_id: ChannelId) -> Vec<Message> {
|
||||
let b = GetMessages::new().limit(100);
|
||||
let mut msgs = c_id.messages(http, b).await.unwrap();
|
||||
msgs.retain(|item| item.author.id == bot_id);
|
||||
|
||||
let mut last_msg: Option<Message> = msgs.last().cloned();
|
||||
|
||||
while last_msg.is_some() {
|
||||
let new_b = GetMessages::new().limit(100).before(last_msg.clone().unwrap());
|
||||
let new_msgs = c_id.messages(http, new_b).await.unwrap();
|
||||
|
||||
last_msg = new_msgs.last().cloned();
|
||||
|
||||
if new_msgs.is_empty() {
|
||||
break;
|
||||
}
|
||||
|
||||
let filtered_msgs: Vec<Message> = new_msgs
|
||||
.into_iter()
|
||||
.filter(|item| item.author.id == bot_id)
|
||||
.collect();
|
||||
|
||||
msgs.extend(filtered_msgs);
|
||||
}
|
||||
|
||||
return msgs;
|
||||
}
|
||||
|
||||
|
||||
async fn msgs_to_json(msgs: Vec<Message>, reddit_data: &Value, max_age: u64) -> Value {
|
||||
let mut msgs_json: Value = json!({"no_change": {}, "updated": {}, "removed": {}, "duplicates": {}, "old": {}});
|
||||
let now = SystemTime::now()
|
||||
.duration_since(UNIX_EPOCH)
|
||||
.expect("Time went backwards")
|
||||
.as_secs();
|
||||
|
||||
for msg in msgs {
|
||||
if msg.embeds.is_empty() { continue; }
|
||||
if msg.embeds[0].url.is_none() { continue; }
|
||||
|
||||
let url = msg.embeds[0].url.clone().unwrap();
|
||||
|
||||
// duplicates
|
||||
if ["no_change", "updated", "removed", "old"]
|
||||
.iter()
|
||||
.any(|key| msgs_json[key].as_object().unwrap().contains_key(&url))
|
||||
{
|
||||
let dupes_mut = msgs_json["duplicates"].as_object_mut().unwrap();
|
||||
if !dupes_mut.contains_key(&url) {
|
||||
dupes_mut.insert(url.clone(), json!(msg.id.get()));
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
let msg_desc = &msg.embeds[0].description.clone().unwrap();
|
||||
let msg_lines = msg_desc.split("\n");
|
||||
let msg_last_len = msg_lines.clone().last().unwrap().len();
|
||||
|
||||
if msg_last_len < 13 { continue; }
|
||||
|
||||
let msg_json_str = &msg_lines.clone().last().unwrap()[9..msg_last_len - 3];
|
||||
|
||||
let msg_json = serde_json::from_str(msg_json_str);
|
||||
if msg_json.is_err() { continue; }
|
||||
|
||||
let mut u_json: Value = msg_json.unwrap();
|
||||
let re_url = &reddit_data[BK_WEEK][&url];
|
||||
|
||||
let post_date = re_url["post_data"]["date_unix"].as_u64().unwrap_or(0);
|
||||
|
||||
// old
|
||||
if now - post_date > max_age {
|
||||
if let Some(obj) = msgs_json["old"].as_object_mut() {
|
||||
obj.insert(url.clone(), json!(msg.id.get()));
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
// removed
|
||||
if re_url.get("removed").is_some() {
|
||||
if u_json.get("removed").is_some() {
|
||||
// no change
|
||||
if let Some(obj) = msgs_json["no_change"].as_object_mut() {
|
||||
obj.insert(url.clone(), json!(msg.id.get()));
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
// removed
|
||||
if let Some(obj) = msgs_json["removed"].as_object_mut() {
|
||||
obj.insert(url.clone(), json!(msg.id.get()));
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
// updated
|
||||
if u_json["added"] != re_url["added"]
|
||||
|| u_json["approved"] != re_url["approved"]
|
||||
|| u_json["post_data"]["upvotes"] != re_url["post_data"]["upvotes"]
|
||||
|| u_json["votes"]["mod_voters"] != re_url["votes"]["mod_voters"]
|
||||
{
|
||||
u_json.as_object_mut().unwrap().insert("msg_id".to_string(), Value::String(msg.id.clone().to_string()));
|
||||
|
||||
if let Some(obj) = msgs_json["updated"].as_object_mut() {
|
||||
obj.insert(url.clone(), json!(msg.id.get()));
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
// no change
|
||||
if let Some(obj) = msgs_json["no_change"].as_object_mut() {
|
||||
obj.insert(url.clone(), json!(msg.id.get()));
|
||||
}
|
||||
}
|
||||
|
||||
return msgs_json;
|
||||
}
|
||||
|
||||
|
||||
async fn add_posts(http: &Http, c_id: ChannelId, r_data: &Map<String, Value>, msgs_json: &Value, max_age: u64) {
|
||||
let now = SystemTime::now()
|
||||
.duration_since(UNIX_EPOCH)
|
||||
.expect("Time went backwards")
|
||||
.as_secs();
|
||||
|
||||
for url in r_data.keys() {
|
||||
if ["no_change", "updated", "removed", "old", "duplicates"]
|
||||
.iter()
|
||||
.any(|key| msgs_json[key].as_object().unwrap().contains_key(url))
|
||||
{ continue; }
|
||||
|
||||
let post_date = r_data[url]["post_data"]["date_unix"].as_u64().unwrap();
|
||||
if now - post_date > max_age { 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;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
async fn edit_posts(http: &Http, c_id: ChannelId, r_data: &Map<String, Value>, 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;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
async fn remove_posts(http: &Http, c_id: ChannelId, r_data: &Map<String, Value>, 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;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
async fn remove_old(http: &Http, c_id: ChannelId, msgs_json: &Value) {
|
||||
for (_url, msg_id) in msgs_json["old"].as_object().unwrap() {
|
||||
let msg = http.get_message(c_id, MessageId::new(msg_id.as_u64().unwrap())).await.unwrap();
|
||||
let _ = msg.delete(http).await;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
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,
|
||||
category = "bk_week",
|
||||
required_bot_permissions = "SEND_MESSAGES | VIEW_CHANNEL"
|
||||
)]
|
||||
/// 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,
|
||||
#[description = "Wether to undo your vote or not"] un_vote: Option<bool>
|
||||
) -> Result<(), Error>
|
||||
{
|
||||
data::update_re_data(ctx.data()).await;
|
||||
let uid = ctx.author().id.get();
|
||||
let re_data = get_mutex_data(&ctx.data().reddit_data).await?;
|
||||
let post_data = re_data[BK_WEEK].clone();
|
||||
let unw_vote = un_vote.unwrap_or(false);
|
||||
|
||||
if post_data.get(&url).is_none() {
|
||||
send_post_not_found_message(ctx, &url).await;
|
||||
return Ok(());
|
||||
}
|
||||
if post_data[&url].get("removed").is_some() {
|
||||
send_post_removed_message(ctx, &url, &post_data[&url]).await;
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
let url_data = &post_data[&url];
|
||||
|
||||
let is_mod = is_bk_mod(ctx.data().bk_mods.clone(), ctx.author().id.get());
|
||||
let voters_dc = url_data["votes"]["voters_dc"].as_array().unwrap();
|
||||
let mod_voters = url_data["votes"]["mod_voters"].as_array().unwrap();
|
||||
let voters = if is_mod { mod_voters } else { voters_dc };
|
||||
|
||||
if voters.contains(&json!(uid)) && !unw_vote {
|
||||
send_msg(ctx, "Couldn't cast a vote: You have already voted on this post!".to_string(), true, true).await;
|
||||
return Ok(());
|
||||
}
|
||||
else if !voters.contains(&json!(uid)) && unw_vote {
|
||||
send_msg(ctx, "Couldn't remove your vote: You haven't voted on this post yet!".to_string(), true, true).await;
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
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 {
|
||||
send_msg(ctx, "Successfully voted (as moderator vote)!".to_string(), true, true).await;
|
||||
}
|
||||
else if unw_r && !unw_vote && !is_mod {
|
||||
send_msg(ctx, "Successfully voted!".to_string(), true, true).await;
|
||||
}
|
||||
else if unw_r && unw_vote {
|
||||
send_msg(ctx, "Successfully removed vote!".to_string(), true, true).await;
|
||||
}
|
||||
else {
|
||||
send_msg(ctx, "Failed to vote/un-vote: Unknown internal error".to_string(), true, true).await;
|
||||
}
|
||||
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
|
||||
|
||||
#[poise::command(
|
||||
slash_command,
|
||||
prefix_command,
|
||||
category = "bk_week",
|
||||
required_bot_permissions = "SEND_MESSAGES | VIEW_CHANNEL | EMBED_LINKS"
|
||||
)]
|
||||
/// Gets the top N (up to 10) posts within a certain category, such as upvotes. (Sorted descending.)
|
||||
pub async fn bk_week_top(
|
||||
ctx: Context<'_>,
|
||||
#[description = "The sorting criteria, such as upvotes."]
|
||||
category: TopCategory,
|
||||
#[description = "The amount of posts to show (max 10)."]
|
||||
#[min = 1]
|
||||
#[max = 10]
|
||||
amount: Option<u8>
|
||||
) -> Result<(), Error>
|
||||
{
|
||||
let mut all: HashMap<&str, i32> = HashMap::new();
|
||||
let posts = &get_mutex_data(&ctx.data().reddit_data).await?[BK_WEEK];
|
||||
let posts_u = posts.as_object().unwrap();
|
||||
|
||||
for (url, dat) in posts_u {
|
||||
if dat.get("removed").is_some() { continue; }
|
||||
|
||||
let val: i32 = match category {
|
||||
TopCategory::Upvotes => dat["post_data"]["upvotes"].as_i64().unwrap() as i32,
|
||||
TopCategory::ModVotes => dat["votes"]["mod_voters"].as_array().unwrap().len() as i32,
|
||||
TopCategory::Oldest
|
||||
| TopCategory::Newest => dat["post_data"]["date_unix"].as_i64().unwrap() as i32,
|
||||
};
|
||||
|
||||
all.insert(url, val);
|
||||
}
|
||||
|
||||
let amount_u = amount.unwrap_or(3);
|
||||
let amount_clamped = amount_u.clamp(1, 10);
|
||||
|
||||
let top =
|
||||
if category != TopCategory::Oldest
|
||||
{ largest_n (&all, amount_clamped as usize) }
|
||||
else { smallest_n(&all, amount_clamped as usize) };
|
||||
|
||||
for post in top {
|
||||
let url = post.0;
|
||||
let _ = send_embed_for_post(ctx, posts_u[url].clone(), url).await;
|
||||
}
|
||||
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
|
||||
fn largest_n<'a>(map: &'a HashMap<&'a str, i32>, n: usize) -> Vec<(&'a str, i32)> {
|
||||
let mut vec: Vec<_> = map.iter().collect();
|
||||
vec.sort_unstable_by(|a, b| b.1.cmp(a.1));
|
||||
vec.into_iter().take(n).map(|(&k, &v)| (k, v)).collect()
|
||||
}
|
||||
|
||||
|
||||
fn smallest_n<'a>(map: &'a HashMap<&'a str, i32>, n: usize) -> Vec<(&'a str, i32)> {
|
||||
let mut vec: Vec<_> = map.iter().collect();
|
||||
vec.sort_unstable_by(|a, b| a.1.cmp(b.1));
|
||||
vec.into_iter().take(n).map(|(&k, &v)| (k, v)).collect()
|
||||
}
|
||||
+17
-30
@@ -3,14 +3,14 @@ use std::process;
|
||||
use std::error::Error as StdErr;
|
||||
|
||||
use crate::data::{dc_add_server, get_mutex_data, read_cfg_data};
|
||||
use crate::re_cmds::generic_fns::to_shorturl;
|
||||
use crate::websocket::send_cmd_json;
|
||||
use crate::{data, Context, Data, Error};
|
||||
use crate::{data, lang, Context, Data, Error};
|
||||
use crate::messages::{edit_reply, send_embed, send_msg, Author, EmbedOptions, MANDATORY_MSG};
|
||||
|
||||
use poise::serenity_prelude::{OnlineStatus, Timestamp};
|
||||
use poise::Command;
|
||||
use rand::{seq::IteratorRandom, Rng};
|
||||
use regex::Regex;
|
||||
use serde_json::json;
|
||||
use tokio::fs;
|
||||
|
||||
@@ -63,19 +63,19 @@ pub async fn stop(
|
||||
|| confirmation.unwrap_or_default().to_lowercase() == "i want to stop the bot now";
|
||||
|
||||
if should_stop {
|
||||
let msg = send_msg(ctx, "Saving data...".to_string(), true, true).await.unwrap();
|
||||
let msg = send_msg(ctx, lang!("data_save_progress"), true, true).await.unwrap();
|
||||
data::write_dc_data(ctx.data()).await;
|
||||
data::write_re_data().await;
|
||||
send_cmd_json("stop_praw", None).await;
|
||||
|
||||
edit_reply(ctx, msg, "Saving data... Done!\nShutting down...".to_string()).await;
|
||||
edit_reply(ctx, msg, lang!("data_save_complete")).await;
|
||||
ctx.serenity_context().set_presence(None, OnlineStatus::Invisible);
|
||||
ctx.framework().shard_manager.shutdown_all().await;
|
||||
|
||||
process::exit(0);
|
||||
}
|
||||
else {
|
||||
send_msg(ctx, "Failed to shut down: Invalid confirmation.".to_string(), true, true).await;
|
||||
send_msg(ctx, lang!("invalid_confirm_shutdown"), true, true).await;
|
||||
}
|
||||
|
||||
return Ok(());
|
||||
@@ -171,7 +171,7 @@ pub async fn eight_ball(
|
||||
|
||||
send_msg(
|
||||
ctx,
|
||||
format!("Q: {}\nA: {}", question, rand_item.unwrap()),
|
||||
lang!("8-ball_answer", question, rand_item.unwrap()),
|
||||
true,
|
||||
true
|
||||
).await;
|
||||
@@ -194,29 +194,16 @@ pub async fn re_shorturl(
|
||||
let shorturl = to_shorturl(&url);
|
||||
|
||||
if shorturl.is_ok() {
|
||||
send_msg(ctx, format!("ShortURL: <{}>", shorturl.unwrap()), true, true).await;
|
||||
send_msg(ctx, lang!("shorturl", shorturl.unwrap()), true, true).await;
|
||||
}
|
||||
else {
|
||||
send_msg(ctx, "Couldn't convert to shortURL: Invalid URL".to_string(), true, true).await;
|
||||
send_msg(ctx, lang!("couldnt_shorturl"), true, true).await;
|
||||
}
|
||||
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
|
||||
pub fn to_shorturl(url: &str) -> Result<String, &str> {
|
||||
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,
|
||||
@@ -233,10 +220,10 @@ pub async fn add_server(
|
||||
let r = dc_add_server(ctx.data(), ctx.guild_id().unwrap().into()).await;
|
||||
|
||||
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;
|
||||
send_msg(ctx, lang!("add_to_data"), true, true).await;
|
||||
}
|
||||
else {
|
||||
send_msg(ctx, "Oopsies `(。>\\\\<)`. It looks like my data i-is \\**sob*\\*... c-corrupted!".to_string(), true, true).await;
|
||||
send_msg(ctx, lang!("corrupted_data"), true, true).await;
|
||||
}
|
||||
|
||||
return Ok(());
|
||||
@@ -263,14 +250,14 @@ pub async fn reload_cfg(
|
||||
if r.is_some() && r.unwrap()["value"].as_bool().unwrap() {
|
||||
send_msg(
|
||||
ctx,
|
||||
format!("Successfully reloaded the configs!\nNew configs:\n```\n{}\n```", serde_json::to_string_pretty(&d)?),
|
||||
lang!("reload_config_success", serde_json::to_string_pretty(&d).unwrap()),
|
||||
true,
|
||||
true
|
||||
).await;
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
send_msg(ctx, "Failed to reload configs: Failed-type response from Python.".to_string(), true, true).await;
|
||||
send_msg(ctx, lang!("reload_config_python_fail"), true, true).await;
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
@@ -342,7 +329,7 @@ async fn send_single_help(ctx: Context<'_>, mut cmd_name: String) {
|
||||
if cmd.is_none() {
|
||||
send_msg(
|
||||
ctx,
|
||||
format!("No command with the name \"{}\" found!\nHint: Try `/help` without any arguments or `/help <category>`", cmd_name),
|
||||
lang!("cmd_404", cmd_name),
|
||||
true,
|
||||
true
|
||||
).await;
|
||||
@@ -368,7 +355,7 @@ async fn send_category_help(ctx: Context<'_>, category: HelpOptions) {
|
||||
|
||||
async fn send_bk_week_help_re(ctx: Context<'_>) {
|
||||
let t: String = fs::read_to_string("./bk_week_help_re.md").await
|
||||
.unwrap_or("Help text not found. Someone deleted it. :(".to_string());
|
||||
.unwrap_or(lang!("help_text_removed"));
|
||||
|
||||
send_msg(ctx, t, true, true).await;
|
||||
}
|
||||
@@ -435,10 +422,10 @@ async fn send_bk_week_help(ctx: Context<'_>) {
|
||||
let cmds = &ctx.framework().options().commands;
|
||||
let bk_week_cmds: Vec<_> = cmds
|
||||
.iter()
|
||||
.filter(|cmd| cmd.category == Some("bk_week".to_string()))
|
||||
.filter(|cmd| cmd.category == Some("re".to_string()))
|
||||
.collect();
|
||||
|
||||
let t = format_cmds(vec![("bk_week", bk_week_cmds)]);
|
||||
let t = format_cmds(vec![("re", bk_week_cmds)]);
|
||||
send_msg(ctx, t, true, true).await;
|
||||
}
|
||||
|
||||
@@ -449,7 +436,7 @@ async fn send_generic_help(ctx: Context<'_>) {
|
||||
.iter()
|
||||
.filter(
|
||||
|cmd|
|
||||
cmd.category != Some("bk_week".to_string())
|
||||
cmd.category != Some("re".to_string())
|
||||
|| cmd.category != Some("owner".to_string())
|
||||
|| cmd.category != Some("admin".to_string())
|
||||
)
|
||||
|
||||
+33
-12
@@ -4,23 +4,24 @@ use std::path::Path;
|
||||
use serde_json::{self, Value, json};
|
||||
use tokio::sync::Mutex;
|
||||
|
||||
use crate::{Data, BK_WEEK, rs_println, Error};
|
||||
use crate::{lang, rs_println, Data, Error, BK_WEEK, LANG};
|
||||
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";
|
||||
static DATA_PATH_DC: &str = "./data/dc_data.json";
|
||||
static PRESET_PATH_DC: &str = "./data/dc_data_preset.json";
|
||||
static DATA_PATH_RE: &str = "./data/re_data.json";
|
||||
static PRESET_PATH_RE: &str = "./data/re_data_preset.json";
|
||||
static DATA_PATH_CFG: &str = "./data/cfg.json";
|
||||
static PRESET_PATH_CFG: &str = "./data/cfg_default.json";
|
||||
static DATA_PATH_LANG: &str = "./data/lang/";
|
||||
|
||||
|
||||
pub async fn read_dc_data(data: &Data, wipe: bool) {
|
||||
if !Path::new(DATA_PATH_DC).exists() || wipe {
|
||||
rs_println!(
|
||||
"{} creating new from preset...",
|
||||
if !wipe { "discord_data.json not found," } else { "[WIPE] (discord_data.json)" }
|
||||
"{}",
|
||||
lang!("creating_data_file", if !wipe { lang!("dc_data_404") } else { lang!("dc_data_wipe") })
|
||||
);
|
||||
generate_dc_data();
|
||||
}
|
||||
@@ -70,8 +71,8 @@ pub async fn write_dc_data(data: &Data) {
|
||||
pub async fn read_re_data(data: &Data, wipe: bool) {
|
||||
if !Path::new(DATA_PATH_RE).exists() || wipe {
|
||||
rs_println!(
|
||||
"{} creating new from preset...",
|
||||
if !wipe { "reddit_data.json not found," } else { "[WIPE] (reddit_data.json)" }
|
||||
"{}",
|
||||
lang!("creating_data_file", if !wipe { lang!("re_data_404") } else { lang!("re_data_wipe") })
|
||||
);
|
||||
generate_re_data();
|
||||
}
|
||||
@@ -113,8 +114,8 @@ pub async fn write_re_data() {
|
||||
pub async fn read_cfg_data(data: &Data, wipe: bool) {
|
||||
if !Path::new(DATA_PATH_CFG).exists() || wipe {
|
||||
rs_println!(
|
||||
"{} creating new from preset...",
|
||||
if !wipe { "cfg.json not found," } else { "[WIPE] (cfg.json)" }
|
||||
"{}",
|
||||
lang!("creating_data_file", if !wipe { lang!("cfg_data_404") } else { lang!("cfg_data_wipe") })
|
||||
);
|
||||
generate_cfg_data();
|
||||
}
|
||||
@@ -148,7 +149,7 @@ pub async fn dc_add_server(data: &Data, server_id: u64) -> Result<(), ()> {
|
||||
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": [] }));
|
||||
servers.insert(server_id.to_string(), json!({ "bk_week_channel": 0 }));
|
||||
}
|
||||
|
||||
return Ok(());
|
||||
@@ -194,4 +195,24 @@ pub async fn get_mutex_data(data: &Mutex<Option<Value>>) -> Result<Value, Error>
|
||||
Some(data) => Ok(data.clone()),
|
||||
None => Err("Cannot get mutex data: The data is corrupted!".into()),
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
pub fn load_lang_data(lang: String) {
|
||||
let full_path = format!("{}{}.json", DATA_PATH_LANG, lang);
|
||||
|
||||
if !Path::new(&full_path).exists() {
|
||||
rs_println!(
|
||||
"{}",
|
||||
lang!("creating_data_file", lang!("cfg_data_404", lang))
|
||||
);
|
||||
generate_cfg_data();
|
||||
}
|
||||
|
||||
let str_data = fs::read_to_string(full_path).unwrap();
|
||||
let json_data = serde_json::from_str(&str_data).unwrap();
|
||||
|
||||
unsafe {
|
||||
LANG = json_data;
|
||||
};
|
||||
}
|
||||
+7
-4
@@ -1,4 +1,4 @@
|
||||
use crate::{Data, Error, rs_println};
|
||||
use crate::{lang, rs_println, Data, Error};
|
||||
|
||||
use poise::serenity_prelude::{self as serenity, ActivityData};
|
||||
|
||||
@@ -14,9 +14,12 @@ pub fn event_handler<'a>(
|
||||
Box::pin(async move {
|
||||
if let serenity::FullEvent::Ready { data_about_bot } = event {
|
||||
rs_println!(
|
||||
"Bot started as user \"{}\" with id {}",
|
||||
data_about_bot.user.name,
|
||||
data_about_bot.user.id
|
||||
"{}",
|
||||
lang!(
|
||||
"dc_bot_started",
|
||||
data_about_bot.user.name.clone(),
|
||||
data_about_bot.user.id
|
||||
)
|
||||
);
|
||||
|
||||
let file_text = std::fs::read_to_string("./data/status.txt").unwrap();
|
||||
|
||||
+96
@@ -0,0 +1,96 @@
|
||||
use std::collections::HashSet;
|
||||
|
||||
use poise::serenity_prelude::UserId;
|
||||
use poise::serenity_prelude as serenity;
|
||||
use poise::serenity_prelude::Client;
|
||||
|
||||
use crate::{cmds, data, events, re_cmds, rs_println, Args, Data};
|
||||
|
||||
|
||||
pub async fn gen_data(args: Args, owners: Vec<u64>) -> Data {
|
||||
let ball_classic_str = std::fs::read_to_string("./data/8-ball_classic.txt").unwrap();
|
||||
let ball_quirk_str = std::fs::read_to_string("./data/8-ball_quirky.txt").unwrap();
|
||||
|
||||
let ball_classic: Vec<String> = ball_classic_str.lines().map(String::from).collect();
|
||||
let ball_quirk: Vec<String> = ball_quirk_str .lines().map(String::from).collect();
|
||||
|
||||
let mods_env = std::env::var("ASSISTANT_BK_MODS").unwrap_or("0".to_string());
|
||||
let mods_vec_str: Vec<String> = mods_env.split(",").map(String::from).collect();
|
||||
let mods_vec_u64: Vec<u64> = mods_vec_str
|
||||
.iter()
|
||||
.map(|s| s.parse::<u64>().expect("Failed to parse ASSISTANT_BK_MODS. Invalid syntax."))
|
||||
.collect();
|
||||
|
||||
let data = Data {
|
||||
owners,
|
||||
ball_prompts: [ball_classic, ball_quirk],
|
||||
bk_mods: mods_vec_u64,
|
||||
reddit_data: None.into(),
|
||||
discord_data: None.into(),
|
||||
cfg: None.into(),
|
||||
args: args.clone()
|
||||
};
|
||||
|
||||
data::read_dc_data (&data, args.clone().wipe).await;
|
||||
data::read_re_data (&data, args.clone().wipe).await;
|
||||
data::read_cfg_data(&data, args.clone().wipe).await;
|
||||
|
||||
return data;
|
||||
}
|
||||
|
||||
|
||||
pub async fn gen_bot(data: Data, args: Args) -> Client {
|
||||
let token =
|
||||
if !args.test { std::env::var("ASSISTANT_TOKEN").expect("Missing ASSISTANT_TOKEN env var!") }
|
||||
else { std::env::var("ASSISTANT_TOKEN_TEST").expect("Missing ASSISTANT_TOKEN_TEST env var!") };
|
||||
|
||||
let intents = serenity::GatewayIntents::all();
|
||||
|
||||
let peek_len = 27;
|
||||
let token_peek = &token[..peek_len];
|
||||
let token_end_len = token[peek_len..].len();
|
||||
rs_println!("Token: {}{}", token_peek, "*".repeat(token_end_len));
|
||||
|
||||
let own: HashSet<UserId> = data.owners.clone().into_iter().map(UserId::from).collect();
|
||||
|
||||
let framework = poise::Framework::builder()
|
||||
.options(poise::FrameworkOptions {
|
||||
owners: own,
|
||||
commands: vec![
|
||||
cmds::help(),
|
||||
cmds::ping(),
|
||||
cmds::embed(),
|
||||
cmds::send(),
|
||||
cmds::stop(),
|
||||
cmds::eight_ball(),
|
||||
cmds::re_shorturl(),
|
||||
cmds::add_server(),
|
||||
// reddit
|
||||
re_cmds::add::cmd(),
|
||||
re_cmds::approve::cmd(),
|
||||
re_cmds::get::cmd(),
|
||||
re_cmds::remove::cmd(),
|
||||
re_cmds::top::cmd(),
|
||||
re_cmds::update::cmd(),
|
||||
re_cmds::vote::cmd(),
|
||||
// reddit admin
|
||||
re_cmds::admin_bind::cmd(),
|
||||
// cfg
|
||||
cmds::reload_cfg()
|
||||
],
|
||||
event_handler: events::event_handler,
|
||||
..Default::default()
|
||||
})
|
||||
.setup(|ctx, _ready, framework| {
|
||||
Box::pin(async move {
|
||||
poise::builtins::register_globally(ctx, &framework.options().commands).await?;
|
||||
return Ok(data);
|
||||
})
|
||||
})
|
||||
.build();
|
||||
|
||||
return serenity::ClientBuilder::new(token, intents)
|
||||
.framework(framework)
|
||||
.await
|
||||
.unwrap();
|
||||
}
|
||||
@@ -34,4 +34,40 @@ macro_rules! errln {
|
||||
);
|
||||
std::process::exit(1);
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
#[macro_export]
|
||||
macro_rules! lang {
|
||||
($key:expr) => {
|
||||
{
|
||||
use crate::{LANG, errln};
|
||||
let value = unsafe {
|
||||
LANG
|
||||
.as_ref()
|
||||
.expect("LANG must be initialized before use")
|
||||
.get($key)
|
||||
};
|
||||
|
||||
if value.is_none() { errln!("Key not found in LANG JSON: \"{}\"", $key); }
|
||||
|
||||
value.unwrap().as_str().expect("LANG JSON value is not a string!").to_string()
|
||||
}
|
||||
};
|
||||
($key:expr, $($arg:expr),*) => {{
|
||||
use crate::{LANG, errln};
|
||||
use formatx::formatx;
|
||||
|
||||
let value = unsafe {
|
||||
LANG
|
||||
.as_ref()
|
||||
.expect("LANG must be initialized before use")
|
||||
.get($key)
|
||||
};
|
||||
|
||||
if value.is_none() { errln!("Key not found in LANG JSON: \"{}\"", $key); }
|
||||
|
||||
let format_str = value.unwrap().as_str().expect("LANG JSON value is not a string!");
|
||||
formatx!(format_str, $($arg),*).unwrap()
|
||||
}};
|
||||
}
|
||||
+38
-134
@@ -2,7 +2,17 @@
|
||||
#![allow(clippy::needless_return)]
|
||||
|
||||
mod cmds;
|
||||
mod bk_week_cmds;
|
||||
mod re_cmds {
|
||||
pub mod add;
|
||||
pub mod admin_bind;
|
||||
pub mod approve;
|
||||
pub mod generic_fns;
|
||||
pub mod get;
|
||||
pub mod remove;
|
||||
pub mod top;
|
||||
pub mod update;
|
||||
pub mod vote;
|
||||
}
|
||||
mod events;
|
||||
mod messages;
|
||||
mod python;
|
||||
@@ -10,27 +20,27 @@ mod macros;
|
||||
#[allow(unknown_lints)]
|
||||
mod websocket;
|
||||
mod data;
|
||||
mod schedule;
|
||||
mod gen;
|
||||
|
||||
use std::collections::HashSet;
|
||||
use std::future::Future;
|
||||
use std::pin::Pin;
|
||||
use std::process;
|
||||
use std::thread;
|
||||
use std::time::Duration;
|
||||
use std::vec;
|
||||
|
||||
use clap::Parser;
|
||||
use poise::serenity_prelude::UserId;
|
||||
use poise::serenity_prelude as serenity;
|
||||
use poise::serenity_prelude::Client;
|
||||
use r#gen::gen_bot;
|
||||
use r#gen::gen_data;
|
||||
use schedule::run_schedules;
|
||||
use serde::Serialize;
|
||||
use serde_json::Value;
|
||||
use tokio::runtime::Runtime;
|
||||
use tokio::sync::Mutex;
|
||||
use tokio::task::JoinHandle;
|
||||
use tokio::time;
|
||||
use websocket::send_cmd_json;
|
||||
|
||||
use crate::schedule::Schedule;
|
||||
|
||||
// TODO: convert to lang!
|
||||
|
||||
#[derive(Parser, Serialize, Clone)]
|
||||
struct Args {
|
||||
@@ -49,13 +59,14 @@ struct Args {
|
||||
#[arg(long, help = "Removes the annoying ping prints.")]
|
||||
noping: bool,
|
||||
#[arg(long, help = "Makes the program not use the schedules.")]
|
||||
nosched: bool
|
||||
nosched: bool,
|
||||
#[arg(long, default_value = "en", help = "Which language file to use (Do not include file extention)")]
|
||||
lang: String
|
||||
}
|
||||
|
||||
|
||||
type Error = Box<dyn std::error::Error + Send + Sync>;
|
||||
type Context<'a> = poise::Context<'a, Data, Error>;
|
||||
type Schedule = (Duration, fn() -> Pin<Box<dyn Future<Output = ()> + Send>>);
|
||||
|
||||
|
||||
struct Data {
|
||||
@@ -65,18 +76,24 @@ struct Data {
|
||||
discord_data: Mutex<Option<Value>>,
|
||||
cfg: Mutex<Option<Value>>,
|
||||
bk_mods: Vec<u64>,
|
||||
args: Args
|
||||
args: Args,
|
||||
}
|
||||
|
||||
|
||||
static BK_WEEK: &str = "bk_weekly_art_posts";
|
||||
|
||||
pub static mut LANG: Option<serde_json::Value> = None;
|
||||
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() {
|
||||
let args = <Args as clap::Parser>::parse();
|
||||
let args_str = serde_json::to_string(&args).expect("Error serializing args to JSON");
|
||||
|
||||
rs_println!("Fetching language file...");
|
||||
data::load_lang_data(args.clone().lang);
|
||||
rs_println!("{}", lang!("lang_load_success"));
|
||||
|
||||
let own_env = std::env::var("ASSISTANT_OWNERS").unwrap_or("0".to_string());
|
||||
let own_vec_str: Vec<String> = own_env.split(",").map(String::from).collect();
|
||||
let own_vec_u64: Vec<u64> = own_vec_str
|
||||
@@ -92,7 +109,7 @@ async fn main() {
|
||||
if args.py && !args.rs {
|
||||
println!("----- PYTHON ONLY MODE -----");
|
||||
rs_println!("ARGS: {}", args_str);
|
||||
let _ = python::start(args);
|
||||
let _ = python::start(args).await;
|
||||
process::exit(0);
|
||||
}
|
||||
else if args.rs && ! args.py {
|
||||
@@ -101,25 +118,25 @@ async fn main() {
|
||||
start(args, own_vec_u64.clone()).await;
|
||||
process::exit(0);
|
||||
}
|
||||
else if args.py && args.rs {
|
||||
errln!("Invalid arguments: Arguments cannot include both --rs and --py.");
|
||||
}
|
||||
|
||||
rs_println!("ARGS: {}", args_str);
|
||||
|
||||
let rt = Runtime::new().unwrap();
|
||||
let rt_rs = Runtime::new().unwrap();
|
||||
let rt_py = Runtime::new().unwrap();
|
||||
let python_args = args.clone();
|
||||
let rust_args = args.clone();
|
||||
|
||||
let rust = thread::spawn(move || {
|
||||
rt.block_on(async {
|
||||
rt_rs.block_on(async {
|
||||
websocket::start(rust_args.clone(), own_vec_u64.clone()).await;
|
||||
start(rust_args, own_vec_u64).await;
|
||||
});
|
||||
});
|
||||
|
||||
let python = thread::spawn(|| {
|
||||
let _ = python::start(python_args);
|
||||
let python = thread::spawn(move || {
|
||||
rt_py.block_on(async {
|
||||
let _ = python::start(python_args).await;
|
||||
});
|
||||
});
|
||||
|
||||
if !args.nosched {
|
||||
@@ -139,124 +156,11 @@ async fn start(args: Args, owners: Vec<u64>) {
|
||||
let data = gen_data(args.clone(), owners).await;
|
||||
let mut bot = gen_bot(data, args).await;
|
||||
|
||||
rs_println!("Starting bot...");
|
||||
rs_println!("{}", lang!("dc_bot_starting"));
|
||||
bot.start().await.unwrap();
|
||||
}
|
||||
|
||||
|
||||
async fn gen_data(args: Args, owners: Vec<u64>) -> Data {
|
||||
let ball_classic_str = std::fs::read_to_string("./data/8-ball_classic.txt").unwrap();
|
||||
let ball_quirk_str = std::fs::read_to_string("./data/8-ball_quirky.txt").unwrap();
|
||||
|
||||
let ball_classic: Vec<String> = ball_classic_str.lines().map(String::from).collect();
|
||||
let ball_quirk: Vec<String> = ball_quirk_str .lines().map(String::from).collect();
|
||||
|
||||
let mods_env = std::env::var("ASSISTANT_BK_MODS").unwrap_or("0".to_string());
|
||||
let mods_vec_str: Vec<String> = mods_env.split(",").map(String::from).collect();
|
||||
let mods_vec_u64: Vec<u64> = mods_vec_str
|
||||
.iter()
|
||||
.map(|s| s.parse::<u64>().expect("Failed to parse ASSISTANT_BK_MODS. Invalid syntax."))
|
||||
.collect();
|
||||
|
||||
let data = Data {
|
||||
owners,
|
||||
ball_prompts: [ball_classic, ball_quirk],
|
||||
bk_mods: mods_vec_u64,
|
||||
reddit_data: None.into(),
|
||||
discord_data: None.into(),
|
||||
cfg: None.into(),
|
||||
args: args.clone()
|
||||
};
|
||||
|
||||
data::read_dc_data (&data, args.clone().wipe).await;
|
||||
data::read_re_data (&data, args.clone().wipe).await;
|
||||
data::read_cfg_data(&data, args.clone().wipe).await;
|
||||
|
||||
return data;
|
||||
}
|
||||
|
||||
|
||||
async fn gen_bot(data: Data, args: Args) -> Client {
|
||||
let token =
|
||||
if !args.test { std::env::var("ASSISTANT_TOKEN").expect("Missing ASSISTANT_TOKEN env var!") }
|
||||
else { std::env::var("ASSISTANT_TOKEN_TEST").expect("Missing ASSISTANT_TOKEN_TEST env var!") };
|
||||
|
||||
let intents = serenity::GatewayIntents::all();
|
||||
|
||||
let peek_len = 27;
|
||||
let token_peek = &token[..peek_len];
|
||||
let token_end_len = token[peek_len..].len();
|
||||
rs_println!("Token: {}{}", token_peek, "*".repeat(token_end_len));
|
||||
|
||||
let own: HashSet<UserId> = data.owners.clone().into_iter().map(UserId::from).collect();
|
||||
|
||||
let framework = poise::Framework::builder()
|
||||
.options(poise::FrameworkOptions {
|
||||
owners: own,
|
||||
commands: vec![
|
||||
cmds::help(),
|
||||
cmds::ping(),
|
||||
cmds::embed(),
|
||||
cmds::send(),
|
||||
cmds::stop(),
|
||||
cmds::eight_ball(),
|
||||
cmds::re_shorturl(),
|
||||
cmds::add_server(),
|
||||
// bk_week
|
||||
bk_week_cmds::bk_week_get(),
|
||||
bk_week_cmds::bk_week_add(),
|
||||
bk_week_cmds::bk_week_remove(),
|
||||
bk_week_cmds::bk_week_approve(),
|
||||
bk_week_cmds::bk_week_update(),
|
||||
bk_week_cmds::bk_week_vote(),
|
||||
bk_week_cmds::bk_week_top(),
|
||||
// bk_admin
|
||||
bk_week_cmds::bk_admin_bind(),
|
||||
// cfg
|
||||
cmds::reload_cfg()
|
||||
],
|
||||
event_handler: events::event_handler,
|
||||
..Default::default()
|
||||
})
|
||||
.setup(|ctx, _ready, framework| {
|
||||
Box::pin(async move {
|
||||
poise::builtins::register_globally(ctx, &framework.options().commands).await?;
|
||||
return Ok(data);
|
||||
})
|
||||
})
|
||||
.build();
|
||||
|
||||
return serenity::ClientBuilder::new(token, intents)
|
||||
.framework(framework)
|
||||
.await
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
|
||||
async fn run_schedule<F: Fn() -> Pin<Box<dyn Future<Output = ()> + Send>>>(d: Duration, f: F) {
|
||||
let mut ticker = time::interval(d);
|
||||
loop {
|
||||
ticker.tick().await;
|
||||
f().await;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
async fn run_schedules(schedules: Vec<Schedule>) {
|
||||
let mut handles: Vec<JoinHandle<()>> = vec![];
|
||||
|
||||
rs_println!("Starting schedules...");
|
||||
for (d, f) in schedules {
|
||||
let handle = tokio::spawn(run_schedule(d, f));
|
||||
handles.push(handle);
|
||||
}
|
||||
|
||||
for handle in handles {
|
||||
let _ = handle.await;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
async fn read_reddit_inbox() {
|
||||
unsafe { if !websocket::HAS_CONNECTED { return; } }
|
||||
send_cmd_json("respond_mentions", None).await;
|
||||
|
||||
+9
-19
@@ -1,6 +1,6 @@
|
||||
use std::env;
|
||||
|
||||
use crate::{Args, Context};
|
||||
use crate::{lang, Args, Context};
|
||||
|
||||
use poise::serenity_prelude::json::Value;
|
||||
use poise::{serenity_prelude::CreateMessage, CreateReply, ReplyHandle};
|
||||
@@ -29,7 +29,7 @@ pub struct EmbedOptions {
|
||||
impl Default for EmbedOptions {
|
||||
fn default() -> Self {
|
||||
return EmbedOptions {
|
||||
desc: "default description".to_string(),
|
||||
desc: lang!("default_embed_desc"),
|
||||
title: None,
|
||||
col: None,
|
||||
url: None,
|
||||
@@ -194,24 +194,14 @@ pub async fn send_dm(msg: String, args: Args, owners: Vec<u64>) {
|
||||
}
|
||||
|
||||
|
||||
pub fn embed_post(post_data: &Value, url: &str, ephemeral: bool) -> EmbedOptions {
|
||||
pub fn make_post_embed(post_data: &Value, url: &str, ephemeral: bool) -> EmbedOptions {
|
||||
let media_type = &post_data["post_data"]["media_type"];
|
||||
|
||||
let desc_str = format!(
|
||||
r#"Sorted by what I think will be most important
|
||||
Spoilers and vote length anonymizer for fair review!
|
||||
## Post Data:
|
||||
**Media type:** `{}`
|
||||
**Post upvotes:** ||`{:>6}`||
|
||||
**Moderator votes:** ||`{:>6}`||
|
||||
**URL:** ||<{}>||
|
||||
|
||||
## Listing Data:
|
||||
**Added by:** `{{ human: {}, bot: {} }}`
|
||||
**Approved by:** `{{ human: {}, bot: [not implemented] }}`"#,
|
||||
if !media_type.is_null() { media_type.as_str().unwrap() } else { "None" },
|
||||
let desc_str = lang!(
|
||||
"data_post_embed",
|
||||
post_data["post_data"]["upvotes"].as_i64().unwrap(),
|
||||
post_data["votes"]["mod_voters"].as_array().unwrap().len(),
|
||||
if !media_type.is_null() { media_type.as_str().unwrap() } else { "None" },
|
||||
url,
|
||||
|
||||
if post_data["added"] ["by_human"].as_bool().unwrap() { "✅" } else { "❌" },
|
||||
@@ -248,11 +238,11 @@ pub fn embed_post(post_data: &Value, url: &str, ephemeral: bool) -> EmbedOptions
|
||||
}
|
||||
|
||||
|
||||
pub fn embed_post_removed(post_data: &Value, url: &str, ephemeral: bool) -> EmbedOptions {
|
||||
pub fn make_removed_embed(post_data: &Value, url: &str, ephemeral: bool) -> EmbedOptions {
|
||||
return EmbedOptions {
|
||||
title: Some("REMOVED!".to_string()),
|
||||
desc: format!(
|
||||
"## Removed by `{}`\n**Reason:** {}\nURL: ||<{}>||\n\nJSON: ||`{}`||",
|
||||
desc: lang!(
|
||||
"data_post_removed_embed",
|
||||
post_data["removed_by"].as_str().unwrap(),
|
||||
if !post_data["remove_reason"].is_null() { post_data["remove_reason"].as_str().unwrap() }
|
||||
else { "None" },
|
||||
|
||||
+8
-7
@@ -1,5 +1,5 @@
|
||||
use crate::messages::send_dm;
|
||||
use crate::{errln, rs_println, Args};
|
||||
use crate::{errln, lang, rs_println, Args};
|
||||
|
||||
use std::fs;
|
||||
use std::ffi::CString;
|
||||
@@ -10,12 +10,12 @@ use pyo3::types::PyList;
|
||||
|
||||
|
||||
pub async fn start(args: Args) -> PyResult<()> {
|
||||
rs_println!("Running Python program...");
|
||||
rs_println!("{}", lang!("starting_python_program"));
|
||||
|
||||
let args_str = serde_json::to_string(&args).expect("Error serializing args to JSON");
|
||||
|
||||
let slash = if cfg!(windows) { "\\" } else if cfg!(unix) { "/" } else { "" };
|
||||
if slash.is_empty() { errln!("Man what kinda OS do you have? Neither unix or windows, what the hell!? I can't process this anymore, you're too weird!"); }
|
||||
if slash.is_empty() { errln!("{}", lang!("invalid_os")); }
|
||||
|
||||
let path = format!("{0}{1}src{1}python", env!("CARGO_MANIFEST_DIR"), slash);
|
||||
|
||||
@@ -43,7 +43,7 @@ pub async fn start(args: Args) -> PyResult<()> {
|
||||
.map(|s| s.parse::<u64>().expect("Failed to parse ASSISTANT_OWNERS. Invalid syntax."))
|
||||
.collect();
|
||||
|
||||
send_dm(format!("Unknown internal Python Error: {:?}", from_python), args, own_vec_u64).await;
|
||||
send_dm(lang!("python_err", format!("{:?}", from_python)), args, own_vec_u64).await;
|
||||
errln!("pyO3: {:?}", from_python);
|
||||
}
|
||||
return Ok(());
|
||||
@@ -51,7 +51,8 @@ pub async fn start(args: Args) -> PyResult<()> {
|
||||
|
||||
|
||||
fn get_code(path: &str) -> String {
|
||||
return fs::read_to_string(path)
|
||||
.unwrap_or_else(|_| errln!("Failed to read Python file.\nPath: {}", path))
|
||||
.to_string();
|
||||
let file = fs::read_to_string(path);
|
||||
if file.is_err() { errln!("Failed to read Python file.\nPath: {}", path); }
|
||||
|
||||
return file.unwrap().to_string();
|
||||
}
|
||||
|
||||
+3
-3
@@ -66,7 +66,7 @@ class PostData:
|
||||
|
||||
|
||||
def read_data(bot: botPy.Bot) -> bool:
|
||||
r_path = os.path.join(DATA_PATH, "reddit_data.json")
|
||||
r_path = os.path.join(DATA_PATH, "re_data.json")
|
||||
|
||||
if os.path.isfile(r_path):
|
||||
bot.data_f = open(r_path, "r+")
|
||||
@@ -75,8 +75,8 @@ def read_data(bot: botPy.Bot) -> bool:
|
||||
if not bot.args["py"]:
|
||||
return False
|
||||
|
||||
py_print("reddit_data.json not found, creating new from preset...")
|
||||
with open(os.path.join(DATA_PATH, "reddit_data_preset.json", "r")) as f:
|
||||
py_print("re_data.json not found, creating new from preset...")
|
||||
with open(os.path.join(DATA_PATH, "re_data_preset.json", "r")) as f:
|
||||
data_preset_json = json.load(f)
|
||||
|
||||
data_preset_json[botPy.BK_WEEKLY].pop("EXAMPLE VALUE", None)
|
||||
|
||||
+1
-1
@@ -40,7 +40,7 @@ async def main():
|
||||
rd = data.read_data(bot)
|
||||
|
||||
if data_retries == 5 and not rd:
|
||||
raise Exception("Couldn't read reddit_data.json: File doesn't exist")
|
||||
raise Exception("Couldn't read re_data.json: File doesn't exist")
|
||||
|
||||
py_print("Successfully read data!")
|
||||
|
||||
|
||||
@@ -0,0 +1,68 @@
|
||||
use serde_json::json;
|
||||
|
||||
use crate::data::get_mutex_data;
|
||||
use crate::messages::send_msg;
|
||||
use crate::{data, websocket, Context, Error, BK_WEEK};
|
||||
use crate::re_cmds::generic_fns::{is_bk_mod, to_shorturl};
|
||||
use crate::lang;
|
||||
|
||||
#[poise::command(
|
||||
slash_command,
|
||||
prefix_command,
|
||||
rename = "re_addpost",
|
||||
category = "re",
|
||||
required_bot_permissions = "SEND_MESSAGES | VIEW_CHANNEL"
|
||||
)]
|
||||
/// Fetches a post from Reddit and adds it to the database.
|
||||
pub async fn cmd(
|
||||
ctx: Context<'_>,
|
||||
#[description = "The post URL."] url: String,
|
||||
#[description = "Wether to approve it after adding it"] approve: Option<bool>
|
||||
) -> Result<(), Error>
|
||||
{
|
||||
if !is_bk_mod(ctx.data().bk_mods.clone(), ctx.author().id.get()) {
|
||||
send_msg(ctx, lang!("re_permdeny_bk_mod"), false, false).await;
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
let shorturl_u = to_shorturl(&url);
|
||||
let shorturl = &shorturl_u.unwrap_or(url.clone());
|
||||
|
||||
data::update_re_data(ctx.data()).await;
|
||||
let reddit_data = get_mutex_data(&ctx.data().reddit_data).await?;
|
||||
|
||||
if let Some(bk_week) = reddit_data.get(BK_WEEK) {
|
||||
let a = approve.unwrap_or(false);
|
||||
let r = websocket::send_cmd_json("add_post_url", Some(json!([&shorturl, a, true]))).await.unwrap();
|
||||
|
||||
if !r["value"].as_bool().unwrap() {
|
||||
send_msg(
|
||||
ctx,
|
||||
r#"Unknown error!
|
||||
Error trace: `bk_week_cmds.rs -> bk_week_add() -> Unknown error`.
|
||||
Common reasons: The URL provided was likely invalid or 403: forbidden (e.g a private subreddit)."#.to_string(),
|
||||
true,
|
||||
true
|
||||
).await;
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
if let Some(post) = bk_week.get(shorturl) {
|
||||
if post.get("removed").is_some() {
|
||||
send_msg(ctx, lang!("re_unremove_post_success", url), true, true).await;
|
||||
}
|
||||
else {
|
||||
send_msg(ctx, lang!("re_update_post_success", url), true, true).await;
|
||||
}
|
||||
}
|
||||
else {
|
||||
send_msg(ctx, lang!("re_add_post_success", &shorturl), true, true).await;
|
||||
}
|
||||
|
||||
if a {
|
||||
send_msg(ctx, lang!("re_also_approved"), true, true).await;
|
||||
}
|
||||
}
|
||||
|
||||
return Ok(());
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
use crate::{data::dc_bind_bk, lang, messages::send_msg, Context, Error};
|
||||
|
||||
#[poise::command(
|
||||
slash_command,
|
||||
prefix_command,
|
||||
rename = "admin_re_bindchannel",
|
||||
category = "admin",
|
||||
default_member_permissions = "ADMINISTRATOR",
|
||||
guild_only,
|
||||
required_bot_permissions = "SEND_MESSAGES | VIEW_CHANNEL"
|
||||
)]
|
||||
/// Sets the channel where the bot will dump all Reddit data upon using /re_updateDiscord.
|
||||
pub async fn cmd(
|
||||
ctx: Context<'_>
|
||||
) -> Result<(), Error>
|
||||
{
|
||||
let c_id = ctx.channel_id().into();
|
||||
let r = dc_bind_bk(ctx.data(), ctx.guild_id().unwrap().into(), c_id).await;
|
||||
|
||||
if r.is_ok() {
|
||||
send_msg(ctx, lang!("bound_dc_channel", c_id), true, true).await;
|
||||
}
|
||||
else {
|
||||
send_msg(ctx, lang!("dc_404"), false, false).await;
|
||||
}
|
||||
|
||||
return Ok(());
|
||||
}
|
||||
@@ -0,0 +1,58 @@
|
||||
use serde_json::{json, Value};
|
||||
|
||||
use crate::{data::{self, get_mutex_data}, lang, messages::send_msg, re_cmds::generic_fns::is_bk_mod, websocket, Context, Error, BK_WEEK};
|
||||
|
||||
use super::generic_fns::send_embed_for_removed;
|
||||
|
||||
#[poise::command(
|
||||
slash_command,
|
||||
prefix_command,
|
||||
rename = "re_approvepost",
|
||||
category = "re",
|
||||
required_bot_permissions = "SEND_MESSAGES | VIEW_CHANNEL"
|
||||
)]
|
||||
/// Approves a post in the database.
|
||||
pub async fn cmd(
|
||||
ctx: Context<'_>,
|
||||
#[description = "The post URL."] url: String,
|
||||
#[description = "Wether to approve or disapprove the post"] disapprove: Option<bool>
|
||||
) -> Result<(), Error>
|
||||
{
|
||||
if !is_bk_mod(ctx.data().bk_mods.clone(), ctx.author().id.get()) {
|
||||
send_msg(ctx, lang!("re_permdeny_bk_mod"), false, false).await;
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
data::update_re_data(ctx.data()).await;
|
||||
let reddit_data = get_mutex_data(&ctx.data().reddit_data).await?;
|
||||
|
||||
approve_cmd(ctx, &url, &reddit_data, !disapprove.unwrap_or(false)).await;
|
||||
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
|
||||
async fn approve_cmd(ctx: Context<'_>, url: &str, reddit_data: &Value, approve: bool) {
|
||||
if let Some(post) = reddit_data.get(BK_WEEK).unwrap().get(url) {
|
||||
if post.get("removed").is_some() {
|
||||
send_embed_for_removed(ctx, url, post).await;
|
||||
return;
|
||||
}
|
||||
|
||||
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, lang!("re_approve_post_success"), true, true).await;
|
||||
}
|
||||
else {
|
||||
send_msg(ctx, lang!("re_disapprove_post_success"), true, true).await;
|
||||
}
|
||||
}
|
||||
else {
|
||||
send_msg(ctx, lang!("re_err_trace", "`re_cmds -> approve.rs -> cmd() -> unwrap websocket result error`"), true, true).await;
|
||||
}
|
||||
}
|
||||
else {
|
||||
send_msg(ctx, lang!("re_404"), false, false).await;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
use regex::Regex;
|
||||
use serde_json::Value;
|
||||
|
||||
use crate::{messages::{make_post_embed, make_removed_embed, send_embed}, Context, Error};
|
||||
|
||||
pub fn is_bk_mod(mod_list: Vec<u64>, uid: u64) -> bool {
|
||||
return mod_list.contains(&uid);
|
||||
}
|
||||
|
||||
pub fn to_shorturl(url: &str) -> Result<String, &str> {
|
||||
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");
|
||||
}
|
||||
|
||||
|
||||
pub async fn send_embed_for_post(ctx: Context<'_>, post: Value, url: &str) -> Result<(), Error> {
|
||||
send_embed(ctx, make_post_embed(&post, url, true), true).await;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn send_embed_for_removed(ctx: Context<'_>, url: &str, post: &Value) {
|
||||
send_embed(
|
||||
ctx,
|
||||
make_removed_embed(post, url, true),
|
||||
true
|
||||
).await;
|
||||
}
|
||||
@@ -0,0 +1,50 @@
|
||||
use serde_json::Value;
|
||||
|
||||
use crate::{data::{self, get_mutex_data}, lang, messages::send_msg, re_cmds::generic_fns::send_embed_for_post, rs_println, Context, Error, BK_WEEK};
|
||||
|
||||
use super::generic_fns::send_embed_for_removed;
|
||||
|
||||
#[poise::command(
|
||||
slash_command,
|
||||
prefix_command,
|
||||
rename = "re_getpost",
|
||||
category = "re",
|
||||
required_bot_permissions = "SEND_MESSAGES | VIEW_CHANNEL | EMBED_LINKS"
|
||||
)]
|
||||
/// Fetches and shows a single post, just for you. The post has to be within the database.
|
||||
pub async fn cmd(
|
||||
ctx: Context<'_>,
|
||||
#[description = "The post URL."] url: String
|
||||
) -> Result<(), Error>
|
||||
{
|
||||
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, &url).await? {
|
||||
send_embed_for_post(ctx, post, &url).await?;
|
||||
}
|
||||
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
|
||||
async fn get_post_from_data(ctx: Context<'_>, reddit_data: &Value, url: &str) -> Result<Option<Value>, Error> {
|
||||
if let Some(bk_week) = reddit_data.get(BK_WEEK) {
|
||||
if let Some(post) = bk_week.get(url) {
|
||||
if post.get("removed").is_some() {
|
||||
send_embed_for_removed(ctx, url, post).await;
|
||||
return Ok(None);
|
||||
}
|
||||
return Ok(Some(post.clone()));
|
||||
}
|
||||
else {
|
||||
send_msg(ctx, lang!("re_404", url), true, true).await;
|
||||
}
|
||||
}
|
||||
else {
|
||||
send_msg(ctx, lang!("re_data_corrupted", url), true, true).await;
|
||||
rs_println!("{}", serde_json::to_string_pretty(reddit_data)?);
|
||||
}
|
||||
return Ok(None);
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
use serde_json::json;
|
||||
|
||||
use crate::{lang, messages::send_msg, re_cmds::generic_fns::is_bk_mod, websocket::send_cmd_json, Context, Error};
|
||||
|
||||
#[poise::command(
|
||||
slash_command,
|
||||
prefix_command,
|
||||
rename = "re_removepost",
|
||||
category = "re",
|
||||
required_bot_permissions = "SEND_MESSAGES | VIEW_CHANNEL"
|
||||
)]
|
||||
/// Removes a post from the database.
|
||||
pub async fn cmd(
|
||||
ctx: Context<'_>,
|
||||
#[description = "The post URL."] url: String,
|
||||
#[description = "The reason of the removal."] reason: Option<String>
|
||||
) -> Result<(), Error>
|
||||
{
|
||||
if !is_bk_mod(ctx.data().bk_mods.clone(), ctx.author().id.get()) {
|
||||
send_msg(ctx, lang!("re_permdeny_bk_mod"), false, false).await;
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
let auth = &ctx.author().name;
|
||||
let r = send_cmd_json("remove_post_url", Some(json!([&url, &auth, &reason]))).await.unwrap();
|
||||
|
||||
if r["value"].as_bool().unwrap() {
|
||||
send_msg(
|
||||
ctx,
|
||||
lang!("re_remove_post_success"),
|
||||
true,
|
||||
true
|
||||
).await;
|
||||
}
|
||||
else {
|
||||
send_msg(ctx, lang!("re_404"), false, false).await;
|
||||
}
|
||||
|
||||
return Ok(());
|
||||
}
|
||||
@@ -0,0 +1,77 @@
|
||||
use std::collections::HashMap;
|
||||
|
||||
use crate::{data::get_mutex_data, re_cmds::generic_fns::send_embed_for_post, Context, Error, BK_WEEK};
|
||||
|
||||
#[derive(poise::ChoiceParameter, PartialEq)]
|
||||
enum TopCategory {
|
||||
Upvotes,
|
||||
ModVotes,
|
||||
Oldest,
|
||||
Newest
|
||||
}
|
||||
|
||||
|
||||
#[poise::command(
|
||||
slash_command,
|
||||
prefix_command,
|
||||
rename = "re_topposts",
|
||||
category = "re",
|
||||
required_bot_permissions = "SEND_MESSAGES | VIEW_CHANNEL | EMBED_LINKS"
|
||||
)]
|
||||
/// Shows the top N (up to 10, default is 3) posts within a certain category, such as upvotes.
|
||||
pub async fn cmd(
|
||||
ctx: Context<'_>,
|
||||
#[description = "The sorting criteria, such as upvotes."]
|
||||
category: TopCategory,
|
||||
#[description = "The amount of posts to show (max 10, default is 3)."]
|
||||
#[min = 1]
|
||||
#[max = 10]
|
||||
amount: Option<u8>
|
||||
) -> Result<(), Error>
|
||||
{
|
||||
let mut all: HashMap<&str, i32> = HashMap::new();
|
||||
let posts = &get_mutex_data(&ctx.data().reddit_data).await?[BK_WEEK];
|
||||
let posts_u = posts.as_object().unwrap();
|
||||
|
||||
for (url, dat) in posts_u {
|
||||
if dat.get("removed").is_some() { continue; }
|
||||
|
||||
let val: i32 = match category {
|
||||
TopCategory::Upvotes => dat["post_data"]["upvotes"].as_i64().unwrap() as i32,
|
||||
TopCategory::ModVotes => dat["votes"]["mod_voters"].as_array().unwrap().len() as i32,
|
||||
TopCategory::Oldest
|
||||
| TopCategory::Newest => dat["post_data"]["date_unix"].as_i64().unwrap() as i32,
|
||||
};
|
||||
|
||||
all.insert(url, val);
|
||||
}
|
||||
|
||||
let amount_u = amount.unwrap_or(3);
|
||||
let amount_clamped = amount_u.clamp(1, 10);
|
||||
|
||||
let top =
|
||||
if category != TopCategory::Oldest
|
||||
{ largest_n (&all, amount_clamped as usize) }
|
||||
else { smallest_n(&all, amount_clamped as usize) };
|
||||
|
||||
for post in top {
|
||||
let url = post.0;
|
||||
let _ = send_embed_for_post(ctx, posts_u[url].clone(), url).await;
|
||||
}
|
||||
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
|
||||
fn largest_n<'a>(map: &'a HashMap<&'a str, i32>, n: usize) -> Vec<(&'a str, i32)> {
|
||||
let mut vec: Vec<_> = map.iter().collect();
|
||||
vec.sort_unstable_by(|a, b| b.1.cmp(a.1));
|
||||
vec.into_iter().take(n).map(|(&k, &v)| (k, v)).collect()
|
||||
}
|
||||
|
||||
|
||||
fn smallest_n<'a>(map: &'a HashMap<&'a str, i32>, n: usize) -> Vec<(&'a str, i32)> {
|
||||
let mut vec: Vec<_> = map.iter().collect();
|
||||
vec.sort_unstable_by(|a, b| a.1.cmp(b.1));
|
||||
vec.into_iter().take(n).map(|(&k, &v)| (k, v)).collect()
|
||||
}
|
||||
@@ -0,0 +1,305 @@
|
||||
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}, lang, messages::{edit_reply, embed_from_options, http_send_embed, make_post_embed, make_removed_embed, send_msg}, websocket::send_cmd_json, Context, Error, BK_WEEK};
|
||||
|
||||
#[poise::command(
|
||||
slash_command,
|
||||
prefix_command,
|
||||
rename = "re_updatediscord",
|
||||
category = "re",
|
||||
guild_only,
|
||||
guild_cooldown = 120,
|
||||
required_bot_permissions = "SEND_MESSAGES | VIEW_CHANNEL | READ_MESSAGE_HISTORY | EMBED_LINKS"
|
||||
)]
|
||||
/// Updates the binded Discord channel with the bot's current Reddit data.
|
||||
pub async fn cmd(
|
||||
ctx: Context<'_>,
|
||||
#[description = "Only adds new posts, leaves 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.)"]
|
||||
#[min = 0]
|
||||
#[max = 65535]
|
||||
max_age: Option<u16>
|
||||
) -> Result<(), Error>
|
||||
{
|
||||
let http = ctx.http();
|
||||
|
||||
let mut p_text = "`/bk_week_update`:".to_string();
|
||||
|
||||
let progress = send_msg(ctx, p_text.clone(), true, true).await.unwrap();
|
||||
p_text = update_progress(ctx, progress.clone(), p_text, "\nFetching new posts & updating data file...".to_string()).await;
|
||||
|
||||
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]))).await;
|
||||
data::update_re_data(ctx.data()).await;
|
||||
let r_data = get_mutex_data(&ctx.data().reddit_data).await?;
|
||||
|
||||
let c_id_u = get_c_id(ctx).await;
|
||||
|
||||
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(());
|
||||
}
|
||||
|
||||
let c_id = c_id_u.unwrap();
|
||||
|
||||
// Reading messages
|
||||
p_text = update_progress(ctx, 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(ctx, progress.clone(), p_text.clone(), "✅\nParsing messages to JSON...".to_string()).await;
|
||||
let msgs_json = msgs_to_json(msgs, &r_data, max_age_secs).await;
|
||||
|
||||
// Adding new posts
|
||||
p_text = update_progress(ctx, 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, max_age_secs).await;
|
||||
|
||||
// Stop if only_add
|
||||
if only_add.unwrap_or(false) {
|
||||
send_msg(ctx, "`/bk_week_update`\n## Done!".to_string(), true, true).await;
|
||||
update_progress(ctx, progress.clone(), p_text, "✅\n## Done!".to_string()).await;
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
// Editing updated posts
|
||||
p_text = update_progress(ctx, progress.clone(), p_text.clone(), "✅\nEditing updated posts...".to_string()).await;
|
||||
edit_posts(http, c_id, weekly_art, &msgs_json).await;
|
||||
|
||||
// Removing removed posts
|
||||
p_text = update_progress(ctx, progress.clone(), p_text.clone(), "✅\nRemoving removed posts...".to_string()).await;
|
||||
remove_posts(http, c_id, weekly_art, &msgs_json).await;
|
||||
|
||||
// Removing old posts
|
||||
if max_age_u > 0 {
|
||||
p_text = update_progress(ctx, progress.clone(), p_text.clone(), format!("✅\nRemoving old posts (threshold: {}d)...", max_age_u)).await;
|
||||
remove_old(http, c_id, &msgs_json).await;
|
||||
send_cmd_json("remove_old_posts", Some(json!([max_age_secs]))).await;
|
||||
}
|
||||
|
||||
// Removing duplicate posts
|
||||
p_text = update_progress(ctx, progress.clone(), p_text.clone(), "✅\nRemoving duplicate posts...".to_string()).await;
|
||||
remove_dupes(http, c_id, &msgs_json).await;
|
||||
|
||||
// Done
|
||||
update_progress(ctx, progress.clone(), p_text, "✅\n## Done!".to_string()).await;
|
||||
send_msg(ctx, "`/bk_week_update`\n## Done!".to_string(), true, true).await;
|
||||
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
|
||||
async fn update_progress(ctx: Context<'_>, p: ReplyHandle<'_>, t: String, added_t: String) -> String {
|
||||
let p_text = format!("{} {}", t, added_t);
|
||||
|
||||
edit_reply(ctx, p, p_text.clone()).await;
|
||||
return p_text;
|
||||
}
|
||||
|
||||
|
||||
async fn get_c_id(ctx: Context<'_>) -> Option<ChannelId> {
|
||||
if !data::dc_contains_server(ctx.data(), ctx.guild_id().unwrap().into()).await {
|
||||
send_msg(ctx, lang!("dc_404"), false, false).await;
|
||||
return None;
|
||||
}
|
||||
|
||||
let d = get_mutex_data(&ctx.data().discord_data).await.unwrap();
|
||||
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(http: &Http, bot_id: UserId, c_id: ChannelId) -> Vec<Message> {
|
||||
let b = GetMessages::new().limit(100);
|
||||
let mut msgs = c_id.messages(http, b).await.unwrap();
|
||||
msgs.retain(|item| item.author.id == bot_id);
|
||||
|
||||
let mut last_msg: Option<Message> = msgs.last().cloned();
|
||||
|
||||
while last_msg.is_some() {
|
||||
let new_b = GetMessages::new().limit(100).before(last_msg.clone().unwrap());
|
||||
let new_msgs = c_id.messages(http, new_b).await.unwrap();
|
||||
|
||||
last_msg = new_msgs.last().cloned();
|
||||
|
||||
if new_msgs.is_empty() {
|
||||
break;
|
||||
}
|
||||
|
||||
let filtered_msgs: Vec<Message> = new_msgs
|
||||
.into_iter()
|
||||
.filter(|item| item.author.id == bot_id)
|
||||
.collect();
|
||||
|
||||
msgs.extend(filtered_msgs);
|
||||
}
|
||||
|
||||
return msgs;
|
||||
}
|
||||
|
||||
|
||||
async fn msgs_to_json(msgs: Vec<Message>, reddit_data: &Value, max_age: u64) -> Value {
|
||||
let mut msgs_json: Value = json!({"no_change": {}, "updated": {}, "removed": {}, "duplicates": {}, "old": {}});
|
||||
let now = SystemTime::now()
|
||||
.duration_since(UNIX_EPOCH)
|
||||
.expect("Time went backwards")
|
||||
.as_secs();
|
||||
|
||||
for msg in msgs {
|
||||
if msg.embeds.is_empty() { continue; }
|
||||
if msg.embeds[0].url.is_none() { continue; }
|
||||
|
||||
let url = msg.embeds[0].url.clone().unwrap();
|
||||
|
||||
// duplicates
|
||||
if ["no_change", "updated", "removed", "old"]
|
||||
.iter()
|
||||
.any(|key| msgs_json[key].as_object().unwrap().contains_key(&url))
|
||||
{
|
||||
let dupes_mut = msgs_json["duplicates"].as_object_mut().unwrap();
|
||||
if !dupes_mut.contains_key(&url) {
|
||||
dupes_mut.insert(url.clone(), json!(msg.id.get()));
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
let msg_desc = &msg.embeds[0].description.clone().unwrap();
|
||||
let msg_lines = msg_desc.split("\n");
|
||||
let msg_last_len = msg_lines.clone().last().unwrap().len();
|
||||
|
||||
if msg_last_len < 13 { continue; }
|
||||
|
||||
let msg_json_str = &msg_lines.clone().last().unwrap()[9..msg_last_len - 3];
|
||||
|
||||
let msg_json = serde_json::from_str(msg_json_str);
|
||||
if msg_json.is_err() { continue; }
|
||||
|
||||
let mut u_json: Value = msg_json.unwrap();
|
||||
let re_url = &reddit_data[BK_WEEK][&url];
|
||||
|
||||
let post_date = re_url["post_data"]["date_unix"].as_u64().unwrap_or(0);
|
||||
|
||||
// old
|
||||
if now - post_date > max_age {
|
||||
if let Some(obj) = msgs_json["old"].as_object_mut() {
|
||||
obj.insert(url.clone(), json!(msg.id.get()));
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
// removed
|
||||
if re_url.get("removed").is_some() {
|
||||
if u_json.get("removed").is_some() {
|
||||
// no change
|
||||
if let Some(obj) = msgs_json["no_change"].as_object_mut() {
|
||||
obj.insert(url.clone(), json!(msg.id.get()));
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
// removed
|
||||
if let Some(obj) = msgs_json["removed"].as_object_mut() {
|
||||
obj.insert(url.clone(), json!(msg.id.get()));
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
// updated
|
||||
if u_json["added"] != re_url["added"]
|
||||
|| u_json["approved"] != re_url["approved"]
|
||||
|| u_json["post_data"]["upvotes"] != re_url["post_data"]["upvotes"]
|
||||
|| u_json["votes"]["mod_voters"] != re_url["votes"]["mod_voters"]
|
||||
{
|
||||
u_json.as_object_mut().unwrap().insert("msg_id".to_string(), Value::String(msg.id.clone().to_string()));
|
||||
|
||||
if let Some(obj) = msgs_json["updated"].as_object_mut() {
|
||||
obj.insert(url.clone(), json!(msg.id.get()));
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
// no change
|
||||
if let Some(obj) = msgs_json["no_change"].as_object_mut() {
|
||||
obj.insert(url.clone(), json!(msg.id.get()));
|
||||
}
|
||||
}
|
||||
|
||||
return msgs_json;
|
||||
}
|
||||
|
||||
|
||||
async fn add_posts(http: &Http, c_id: ChannelId, r_data: &Map<String, Value>, msgs_json: &Value, max_age: u64) {
|
||||
let now = SystemTime::now()
|
||||
.duration_since(UNIX_EPOCH)
|
||||
.expect("Time went backwards")
|
||||
.as_secs();
|
||||
|
||||
for url in r_data.keys() {
|
||||
if ["no_change", "updated", "removed", "old", "duplicates"]
|
||||
.iter()
|
||||
.any(|key| msgs_json[key].as_object().unwrap().contains_key(url))
|
||||
{ continue; }
|
||||
|
||||
let post_date = r_data[url]["post_data"]["date_unix"].as_u64().unwrap();
|
||||
if now - post_date > max_age { continue; }
|
||||
|
||||
if r_data[url].get("removed").is_some() {
|
||||
http_send_embed(http, c_id, make_removed_embed(&r_data[url], url, false)).await;
|
||||
continue;
|
||||
}
|
||||
|
||||
http_send_embed(http, c_id, make_post_embed(&r_data[url], url, false)).await;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
async fn edit_posts(http: &Http, c_id: ChannelId, r_data: &Map<String, Value>, 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(make_post_embed(&r_data[url], url, false))]);
|
||||
|
||||
let _ = msg.edit(http, r).await;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
async fn remove_posts(http: &Http, c_id: ChannelId, r_data: &Map<String, Value>, 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(make_removed_embed(&r_data[url], url, false))]);
|
||||
|
||||
let _ = msg.edit(http, r).await;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
async fn remove_old(http: &Http, c_id: ChannelId, msgs_json: &Value) {
|
||||
for (_url, msg_id) in msgs_json["old"].as_object().unwrap() {
|
||||
let msg = http.get_message(c_id, MessageId::new(msg_id.as_u64().unwrap())).await.unwrap();
|
||||
let _ = msg.delete(http).await;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,67 @@
|
||||
use serde_json::json;
|
||||
|
||||
use crate::{data::{self, get_mutex_data}, lang, messages::send_msg, re_cmds::generic_fns::{is_bk_mod, send_embed_for_removed}, websocket::send_cmd_json, Context, Error, BK_WEEK};
|
||||
|
||||
#[poise::command(
|
||||
slash_command,
|
||||
prefix_command,
|
||||
rename = "re_vote",
|
||||
category = "re",
|
||||
required_bot_permissions = "SEND_MESSAGES | VIEW_CHANNEL"
|
||||
)]
|
||||
/// Adds/removes a vote from a post. These votes are not tied to Reddit upvotes.
|
||||
pub async fn cmd(
|
||||
ctx: Context<'_>,
|
||||
#[description = "The post URL."] url: String,
|
||||
#[description = "Wether to undo your vote or not"] un_vote: Option<bool>
|
||||
) -> Result<(), Error>
|
||||
{
|
||||
data::update_re_data(ctx.data()).await;
|
||||
let uid = ctx.author().id.get();
|
||||
let re_data = get_mutex_data(&ctx.data().reddit_data).await?;
|
||||
let post_data = re_data[BK_WEEK].clone();
|
||||
let unw_vote = un_vote.unwrap_or(false);
|
||||
|
||||
if post_data.get(&url).is_none() {
|
||||
send_msg(ctx, lang!("re_404"), false, false).await;
|
||||
return Ok(());
|
||||
}
|
||||
if post_data[&url].get("removed").is_some() {
|
||||
send_embed_for_removed(ctx, &url, &post_data[&url]).await;
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
let url_data = &post_data[&url];
|
||||
|
||||
let is_mod = is_bk_mod(ctx.data().bk_mods.clone(), ctx.author().id.get());
|
||||
let voters_dc = url_data["votes"]["voters_dc"].as_array().unwrap();
|
||||
let mod_voters = url_data["votes"]["mod_voters"].as_array().unwrap();
|
||||
let voters = if is_mod { mod_voters } else { voters_dc };
|
||||
|
||||
if voters.contains(&json!(uid)) && !unw_vote {
|
||||
send_msg(ctx, lang!("re_already_voted"), true, true).await;
|
||||
return Ok(());
|
||||
}
|
||||
else if !voters.contains(&json!(uid)) && unw_vote {
|
||||
send_msg(ctx, lang!("re_havent_voted_remove_vote"), true, true).await;
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
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 {
|
||||
send_msg(ctx, lang!("re_vote_success_mod"), true, true).await;
|
||||
}
|
||||
else if unw_r && !unw_vote && !is_mod {
|
||||
send_msg(ctx, lang!("re_vote_success"), true, true).await;
|
||||
}
|
||||
else if unw_r && unw_vote {
|
||||
send_msg(ctx, lang!("re_vote_remove_success"), true, true).await;
|
||||
}
|
||||
else {
|
||||
send_msg(ctx, lang!("re_vote_err"), true, true).await;
|
||||
}
|
||||
|
||||
return Ok(());
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
use std::future::Future;
|
||||
use std::pin::Pin;
|
||||
use std::time::Duration;
|
||||
|
||||
use tokio::task::JoinHandle;
|
||||
use tokio::time;
|
||||
|
||||
use crate::{lang, rs_println};
|
||||
|
||||
|
||||
pub type Schedule = (Duration, fn() -> Pin<Box<dyn Future<Output = ()> + Send>>);
|
||||
|
||||
|
||||
pub async fn run_schedule<F: Fn() -> Pin<Box<dyn Future<Output = ()> + Send>>>(d: Duration, f: F) {
|
||||
let mut ticker = time::interval(d);
|
||||
loop {
|
||||
ticker.tick().await;
|
||||
f().await;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
pub async fn run_schedules(schedules: Vec<Schedule>) {
|
||||
let mut handles: Vec<JoinHandle<()>> = vec![];
|
||||
|
||||
rs_println!("{}", lang!("starting_schedules"));
|
||||
for (d, f) in schedules {
|
||||
let handle = tokio::spawn(run_schedule(d, f));
|
||||
handles.push(handle);
|
||||
}
|
||||
|
||||
for handle in handles {
|
||||
let _ = handle.await;
|
||||
}
|
||||
}
|
||||
+10
-10
@@ -9,7 +9,7 @@ use std::sync::Arc;
|
||||
use serde_json::{Value, json};
|
||||
|
||||
use crate::messages::send_dm;
|
||||
use crate::rs_println;
|
||||
use crate::{lang, rs_println};
|
||||
use crate::Args;
|
||||
|
||||
type Sender = Arc<Mutex<Option<futures::stream::SplitSink<tokio_tungstenite::WebSocketStream<tokio::net::TcpStream>, tungstenite::Message>>>>;
|
||||
@@ -66,11 +66,11 @@ pub async fn send_cmd_json(func_name: &str, func_args: Option<Value>) -> Option<
|
||||
|
||||
let r = receive_response().await;
|
||||
if !["respond_mentions"].contains(&func_name) || <Args as clap::Parser>::parse().dev {
|
||||
rs_println!("Received from Python: [RESPONSE] {:?}", r);
|
||||
rs_println!("{}", lang!("python_socket_response", format!("{:?}", r)));
|
||||
}
|
||||
|
||||
if r.is_none() {
|
||||
rs_println!("--- Response from Python is None!");
|
||||
rs_println!("{}", lang!("python_socket_null"));
|
||||
}
|
||||
|
||||
return r;
|
||||
@@ -99,10 +99,10 @@ async fn receive_response() -> Option<Value> {
|
||||
|
||||
|
||||
pub async fn start(args: Args, owners: Vec<u64>) {
|
||||
rs_println!("Starting local websocket...");
|
||||
rs_println!("{}", lang!("starting_socket"));
|
||||
let ip = format!("127.0.0.1:{}", args.port);
|
||||
let listener = TcpListener::bind(&ip).await.unwrap();
|
||||
rs_println!("WebSocket server running on ws://{}", ip);
|
||||
rs_println!("{}", lang!("started_socket", ip));
|
||||
|
||||
tokio::spawn(handle_connections(listener, args, owners));
|
||||
}
|
||||
@@ -129,18 +129,18 @@ async fn handle_connections(listener: TcpListener, args: Args, owners: Vec<u64>)
|
||||
async fn handle_message(msg: tungstenite::protocol::Message, args: Args, owners: Vec<u64>) {
|
||||
match msg {
|
||||
tungstenite::Message::Text(text) => {
|
||||
rs_println!("Received from Python: {}", text);
|
||||
rs_println!("{}", lang!("socket_received_python", text.clone()));
|
||||
|
||||
if let Some(stripped) = text.strip_prefix("json:") {
|
||||
let t_json: Value = serde_json::from_str(stripped).unwrap();
|
||||
if t_json.get("error").is_some() {
|
||||
send_dm("Unknown internal Python error occurred: Websocket response error.".to_string(), args, owners).await;
|
||||
send_dm(lang!("python_socket_err"), args, owners).await;
|
||||
}
|
||||
}
|
||||
|
||||
unsafe {
|
||||
if !REPLY_HELLO {
|
||||
send_msg("[Connection test] Hello from Rust!").await;
|
||||
send_msg(&lang!("socket_rust_connection_test")).await;
|
||||
REPLY_HELLO = true;
|
||||
HAS_CONNECTED = true;
|
||||
}
|
||||
@@ -148,12 +148,12 @@ async fn handle_message(msg: tungstenite::protocol::Message, args: Args, owners:
|
||||
}
|
||||
tungstenite::Message::Binary(bytes) => {
|
||||
if args.dev && !args.noping {
|
||||
rs_println!("[Binary] from Python: {:?}", bytes);
|
||||
rs_println!("{}", lang!("python_socket_binary_response", format!("{:?}", bytes)));
|
||||
}
|
||||
}
|
||||
_ => {
|
||||
if args.dev && !args.noping {
|
||||
rs_println!("Received from Python: [UNKNOWN / OTHER]");
|
||||
rs_println!("{}", lang!("python_socket_unknown_response"));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user