15 Commits
68 changed files with 1394 additions and 1099 deletions
-1
View File
@@ -1,4 +1,3 @@
debug/
target/ target/
Cargo.lock Cargo.lock
**/*.rs.bk **/*.rs.bk
+1 -1
View File
@@ -8,8 +8,8 @@ edition = "2021"
[dependencies] [dependencies]
base64 = "0.22.1" base64 = "0.22.1"
clap = { version = "4.5.28", features = ["derive"] } clap = { version = "4.5.28", features = ["derive"] }
dynfmt = { version = "0.1.5", features = ["curly"] }
flate2 = "1.1.2" flate2 = "1.1.2"
formatx = "0.2.3"
futures = "0.3.31" futures = "0.3.31"
poise = "0.6.1" poise = "0.6.1"
pyo3 = "0.28.2" pyo3 = "0.28.2"
+16 -13
View File
@@ -5,7 +5,6 @@ An automation tool primarily made for myself (Byte Dice) but publicly available
> [!CAUTION] > [!CAUTION]
> This tool is not intended for public use outside of the official *Byte Dice Assistant* bots. Expect issues if you host this yourself.\ > This tool is not intended for public use outside of the official *Byte Dice Assistant* bots. Expect issues if you host this yourself.\
> This tool is only designed to run on Windows (10 and 11) and XUbuntu (24.04 and above) and may not work on any other OS.
> [!NOTE] > [!NOTE]
> This bot <ins>never modifies any Reddit content</ins>. All it does and will ever do is read/scrape. > This bot <ins>never modifies any Reddit content</ins>. All it does and will ever do is read/scrape.
@@ -16,6 +15,21 @@ An automation tool primarily made for myself (Byte Dice) but publicly available
**ByteDiceAssistant © 2025 by Byte Dice is licensed under CC BY-NC-SA 4.0.**\ **ByteDiceAssistant © 2025 by Byte Dice is licensed under CC BY-NC-SA 4.0.**\
**You can learn more about copyright by reading the full [license](/LICENSE.txt).** **You can learn more about copyright by reading the full [license](/LICENSE.txt).**
## Environment variables:
(Any Reddit stuff can be disabled.)
| **Name** | **Description** |
| ---------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `ASSISTANT_TOKEN` | The Discord bot token. (Create a Discord bot [here](https://discord.com/developers/docs/intro)!) |
| `ASSISTANT_TOKEN_TEST` | (Optional) A testing Discord bot token. This is only needed when the program is run with `-t` or `--test`. |
| `ASSISTANT_R_ID` | The id for the Reddit bot/account. (Create a Reddit bot [here](https://www.reddit.com/prefs/apps)!) |
| `ASSISTANT_R_TOKEN` | The token for the Reddit bot/account. |
| `ASSISTANT_R_NAME` | The username of the Reddit bot/account. |
| `ASSISTANT_R_PASS` | The password for the Reddit bot/account. |
| `ASSISTANT_OWNERS` | (OPTIONAL) A list of Discord user IDs that "own" the bot. Separate each ID with a single comma and **no** spaces. This will allow the specified user IDs to run root commands such as `/stop`, it will also DM these users when *certain* errors occur. |
| `ASSISTANT_BK_MODS` | (OPTIONAL) Same format as `ASSISTANT_OWNERS` but for people who are allowed to use the `/re_*` commands. |
<!--
## Commands ## Commands
| Name | Category | Description | | Name | Category | Description |
@@ -44,18 +58,6 @@ An automation tool primarily made for myself (Byte Dice) but publicly available
This program uses Rust (v1.82.0) and Python (v3.11.4), you can likely use other versions if they are compatible.\ This program uses Rust (v1.82.0) and Python (v3.11.4), you can likely use other versions if they are compatible.\
It is required to install all used Python modules. You can find those in [req.txt](/req.txt). Installation instructions are in the *How to start the program* section. It is required to install all used Python modules. You can find those in [req.txt](/req.txt). Installation instructions are in the *How to start the program* section.
### Environment variables:
| **Name** | **Description** |
| ---------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `ASSISTANT_TOKEN` | The Discord bot token. (Create a Discord bot [here](https://discord.com/developers/docs/intro)!) |
| `ASSISTANT_TOKEN_TEST` | (Optional) A testing Discord bot token. This is only needed when the program is run with `-t` or `--test`. |
| `ASSISTANT_R_ID` | The id for the Reddit bot/account. (Create a Reddit bot [here](https://www.reddit.com/prefs/apps)!) |
| `ASSISTANT_R_TOKEN` | The token for the Reddit bot/account. |
| `ASSISTANT_R_NAME` | The username of the Reddit bot/account. |
| `ASSISTANT_R_PASS` | The password for the Reddit bot/account. |
| `ASSISTANT_OWNERS` | (OPTIONAL) A list of Discord user IDs that "own" the bot. Separate each ID with a single comma and **no** spaces. This will allow the specified user IDs to run root commands such as `/stop`, it will also DM these users when *certain* errors occur. |
| `ASSISTANT_BK_MODS` | (OPTIONAL) Same format as `ASSISTANT_OWNERS` but for people who are allowed to use the `/re_*` commands. |
### Required permissions: ### Required permissions:
**These are automatically set if you use the [official invite link](https://discord.com/oauth2/authorize?client_id=1212127255795335208&permissions=84992&integration_type=0&scope=bot) or an invite link with the permissions integer set to `84992`.** (The permission integer is this part of the URL `&permissions=84992`) **These are automatically set if you use the [official invite link](https://discord.com/oauth2/authorize?client_id=1212127255795335208&permissions=84992&integration_type=0&scope=bot) or an invite link with the permissions integer set to `84992`.** (The permission integer is this part of the URL `&permissions=84992`)
* Create Invites * Create Invites
@@ -89,3 +91,4 @@ You can find config files in the [cfg/](cfg/) folder. You can also find the defa
* Run `$ cd path/to/extracted/folder`. * Run `$ cd path/to/extracted/folder`.
* Run `$ cargo run`. You can view a list of options by running `$ cargo run -- --help` or `$ cargo run -- -h`. * Run `$ cargo run`. You can view a list of options by running `$ cargo run -- --help` or `$ cargo run -- -h`.
* If you only want to run the Python code, you can either run `$ cargo run -- --py`, or `$ python ./src/python/main.py`. The second option is recommended for better performance. * If you only want to run the Python code, you can either run `$ cargo run -- --py`, or `$ python ./src/python/main.py`. The second option is recommended for better performance.
-->
+6 -1
View File
@@ -1,9 +1,14 @@
{ {
"servers": { "servers": {
"SERVER ID": { "SERVER_ID": {
"re_posts_channel": 0, "re_posts_channel": 0,
"re_disabled": false, "re_disabled": false,
"wwrps_channel": 0 "wwrps_channel": 0
} }
},
"users": {
"USER_ID": {
"wwrps_elo": 0
}
} }
} }
+94 -74
View File
@@ -1,78 +1,98 @@
{ {
"dc_btn_approve": "Approve", "generic": {
"dc_btn_remove": "Remove",
"dc_btn_unapprove": "Disapprove",
"dc_btn_unremove": "Restore",
"dc_btn_unvote": "Un-vote",
"dc_btn_vote": "Vote",
"dc_msg_8-ball_answer": "**8-ball Question:** {0}\n**Answer:** {1}",
"dc_msg_add_to_data": "Added your server to my data! Thanks for letting me steal it! (/s)",
"dc_msg_bound_channel": "Successfully added channel ID `{0}` to the database!",
"dc_msg_cmd_404": "No command \"{0}\" found!\nHint: Try `/help` without any arguments or `/help <category>`",
"dc_msg_corrupted_data": "Oopsies `(。>\\\\<)`. It looks like my data i-is \\**sob*\\*... c-corrupted!",
"dc_msg_data_server_404": "This server is not in the data!\n Hint: Run the command `/database AddServer` inside of a Discord server (requires administrator permission).",
"dc_msg_dm_python_err_socket": "Unknown internal Python error occurred: Websocket response error",
"dc_msg_dm_python_err": "Unknown internal Python Error:\n```\n{0}\n```",
"dc_msg_embed_default_embed_desc": "Default english embed description.",
"dc_msg_embed_re_post": "Spoilers and vote length anonymizer for fair review!\n## Post Data:\n**Subreddit:** {0}\n**Post upvotes:** ||`{1:>6}`||\n**Moderator votes:** ||`{2:>6}`||\n**Media type:** `{3}`\n## Listing Data:\n**Added by:** human: {4} | bot: {5}\n**Approved:** {6}",
"dc_msg_embed_re_removed": "## Removed by `{0}`\n**Reason:** {1}",
"dc_msg_err_trace": "Unknown error!\nError trace: {0}",
"dc_msg_failed_shorturl_conversion": "Couldn't convert to shortURL: Invalid Reddit URL format.",
"dc_msg_mandatory_response": "Mandatory response message, please ignore.",
"dc_msg_no_perms": "Missing permission in that server: {0}",
"dc_msg_not_in_guild": "I am not a part of that guild.",
"dc_msg_owner_data_save_complete": "Saving data... Done!",
"dc_msg_owner_data_save": "Saving data...",
"dc_msg_owner_shutdown": "Shutting down...",
"dc_msg_owner_shutdown_failed_confirmation": "Failed to shut down: Invalid confirmation.",
"dc_msg_re_already_voted": "Couldn't cast a vote: You have already voted on this post!",
"dc_msg_re_also_approved": "Also approved it!",
"dc_msg_re_data_corrupted": "Post URL \"<{0}>\" not found: Post data is corrupted!\n Full details: Could not find key \"posts\" in data file \"re_data.json\"",
"dc_msg_re_help_removed": "Reddit help file not found, someone likely deleted it.\n Hint: You can find the official help text at https://github.com/ByteDice/ByteDiceAssistant in `re_help.md`.\n**CAUTION:** The official help file may contain incorrect information if you're not using the official bot by Byte Dice.",
"dc_msg_re_permdeny_not_re_mod": "Permission denied: You are not a moderator of the subreddit(s) {0}",
"dc_msg_re_post_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 add_post` in a Reddit post.",
"dc_msg_re_post_add_success": "Added post with URL \"<{0}>\"!",
"dc_msg_re_post_approve_remove": "Couldn't approve the post because it has been removed!",
"dc_msg_re_post_approve_success": "Successfully approved the post!",
"dc_msg_re_post_disapprove_success": "Successfully disapproved the post!",
"dc_msg_re_post_remove_success": "Successfully removed post with URL \"<{0}>\"!",
"dc_msg_re_post_unremove_success": "Successfully restored post with URL \"<{0}>\"!",
"dc_msg_re_post_update_success": "Updated post with URL \"<{0}>\"!",
"dc_msg_re_post_vote_removed_post": "Couldn't [vote / un-vote] the post because it has been removed!",
"dc_msg_re_posts_channel_404": "Could not find `re_posts_channel` in data!\nHint: Run `/database RedditChannel` in a (preferably read-only) channel (requires administrator permission).",
"dc_msg_re_vote_err": "Failed to [vote / un-vote]: Unknown internal error.",
"dc_msg_re_vote_mod_success": "Successfully voted (as moderator vote)!",
"dc_msg_re_vote_remove_havent": "Couldn't remove your vote: You haven't voted on this post yet!",
"dc_msg_re_vote_remove_success": "Successfully removed vote!",
"dc_msg_re_vote_success": "Successfully voted!",
"dc_msg_reload_cfg_python_fail": "Failed to reload configs: Failed-type response from Python.",
"dc_msg_reload_cfg_success": "Successfully reloaded the configs!\nNew configs:\n```\n{0}\n```",
"dc_msg_removed_square_brackets": "[REMOVED] {0}",
"dc_msg_shorturl": "ShortURL: <{0}>",
"dc_msg_update_add": "{0}Adding new posts...",
"dc_msg_update_done": "{0}Done!",
"dc_msg_update_editing": "{0}Editing updated posts...",
"dc_msg_update_fetch": "{0}Fetching new posts & updating data file...",
"dc_msg_update_parse": "{0}Parsing messages to JSON...",
"dc_msg_update_read": "{0}Reading messages in <#{1}>...",
"dc_msg_update_removing_dupe": "{0}Removing duplicate posts...",
"dc_msg_update_removing_old": "{0}Removing old posts (threshold: {1}d)...",
"dc_msg_update_removing": "{0}Removing removed posts...",
"dc_msg_whoami": "**Bot \"owner\":** {0}\n**BK moderator:** {1}",
"dc_msg_wwrps_already_submitted": "You can't compete against yourself! Please wait until another player has submitted their RPS.",
"dc_msg_wwrps_anon": "[Anonymous]",
"dc_msg_wwrps_draw": "DRAW",
"dc_msg_wwrps_fight": "### {0} (P1) vs {1} (P2)...\n## {2}\n**P1:** {3}\n**P2:** {4}",
"dc_msg_wwrps_not_in_data": "Could not find `wwrps_channel` in data!\nHint: Run `/database WWRPSChannel` in a channel (requires administrator permission).",
"dc_msg_wwrps_left_win": "P1 WINS",
"dc_msg_wwrps_right_win": "P2 WINS",
"dc_msg_wwrps_submitting": "Submitting your RPS... Please wait for another contestant to compete against you.",
"log_lang_load_success": "Successfully loaded the english language file!",
"none": "None", "none": "None",
"py_re_response_suffix": "^(I am not an AI, I am just a bot. This action was performed automatically by the way. You can report bugs and view my source code [here](https://github.com/ByteDice/ByteDiceAssistant)!)",
"py_re_response_weekly_add": "Successfully added your post to the weekly art submissions! Thank you for participating!",
"py_re_response_weekly_exists": "Couldn't add this post to the submissions! Luckily, it's already there! Thank you for participating!",
"py_re_response_weekly_mod_add": "[MOD ACTION] Successfully added this post to the weekly art submissions!",
"py_re_response_weekly_mod_unremove": "[MOD ACTION] Successfully un-removed this post from the weekly art submissions!",
"success": "Success!" "success": "Success!"
},
"dc": {
"re": {
"buttons": {
"approve": "Approve",
"remove": "Remove",
"disapprove": "Disapprove",
"unremove": "Restore",
"vote": "Vote",
"unvote": "Un-vote"
},
"add": {
"success": "Added post with URL \"<{0}>\"!",
"approved": "Also approved it!"
},
"approve": {
"success": "Successfully approved the post!",
"disapprove": "Successfully disapproved the post!",
"post_removed": "Couldn't approve the post because it has been removed!"
},
"remove": {
"success": "Successfully removed post with URL \"<{0}>\"!",
"unremove": "Successfully restored post with URL \"<{0}>\"!"
},
"vote": {
"success": "Successfully voted!",
"removed": "Successfully removed vote!",
"mod": "Successfully voted (as moderator vote)!",
"post_removed": "Couldn't [vote / un-vote] the post because the post has been removed!",
"remove_hasnt_voted": "Couldn't remove your vote: You haven't voted on this post yet!",
"already": "Couldn't cast a vote: You have already voted on this post!",
"error": "Failed to [vote / un-vote]: Unknown internal error."
},
"update": {
"fetch": "\nFetching new posts & updating data file...",
"read": "✅\nReading messages in <#{1}>...",
"parse": "✅\nParsing messages to JSON...",
"add": "✅\nAdding new posts...",
"rm_dupe": "✅\nRemoving duplicate posts...",
"rm": "✅\nRemoving removed posts...",
"rm_old": "✅\nRemoving old posts (threshold: {1}d)...",
"editing": "✅\nEditing updated posts...",
"done": "✅\n## Done!"
},
"embed": {
"post": "Spoilers and vote length anonymizer for fair review!\n## Post Data:\n**Subreddit:** {0}\n**Post upvotes:** ||`{1:>6}`||\n**Moderator votes:** ||`{2:>6}`||\n**Media type:** `{3}`\n## Listing Data:\n**Added by:** human: {4} | bot: {5}\n**Approved:** {6}",
"post_removed": "## Removed by `{0}`\n**Reason:** {1}",
"removed_square_brackets": "[REMOVED] {0}"
},
"update_post": "Updated post with URL \"<{0}>\"!",
"help_404": "Reddit help file not found, someone likely deleted it.\n-# Hint: You can find the official help text at https://github.com/ByteDice/ByteDiceAssistant in `re_help.md`.\n**CAUTION:** The official help file may contain incorrect information if you're not using the official bot by Byte Dice.",
"post_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 add_post` in a Reddit post.",
"not_mod": "Permission denied: You are not a moderator of the subreddit(s) {0}",
"post_corrupted": "Post URL \"<{0}>\" not found: Post data is corrupted!\n-# Full details: Could not find key \"posts\" in data file \"re_data.json\"",
"shorturl": "ShortURL: <{0}>",
"shorturl_fail": "Couldn't convert to shortURL: Invalid Reddit URL format."
},
"db": {
"added_server": "Added your server to my database. Thanks for letting me steal it! (/s)",
"corrupted_data": "Couldn't add your server to the database! The database is corrupted!",
"server_404": "This server is not in the data!\n-# Hint: Run the command `/database AddServer` inside of a Discord server (requires administrator permission).",
"wwrps_404": "Could not find `wwrps_channel` in data!\n-# Hint: Run `/database WWRPSChannel` in a channel (requires administrator permission).",
"bk_404": "Could not find `re_posts_channel` in data!\n-# Hint: Run `/database RedditChannel` in a (preferably read-only) channel (requires administrator permission).",
"channel_bind": "Successfully added channel ID `{0}` to the database!",
"not_in_guild": "I am not a part of that guild.",
"saving_progress": "Saving data...",
"saving_done": "Saving data... Done!"
},
"wwrps": {
"submitting": "Submitting your RPS... Please wait for another contestant to compete against you.",
"already_submitted": "You can't compete against yourself! Please wait until another player has submitted their RPS.",
"anon": "[Anonymous]",
"draw": "DRAW",
"match": "### {0} (P1) vs {1} (P2)...\n## {2}\n**P1:** {3} **[{4} +{5}] {6}**\n**P2:** {7} **[{8} +{9}] {10}**",
"p1_win": "P1 WINS",
"p2_win": "P2 WINS"
},
"8_ball": "**8-ball Question:** {0}\n**Answer:** {1}",
"no_perms_external": "Missing permission in that server: {0}",
"shutdown": "Shutting down...",
"shutdown_bad_confirm": "Failed to shut down: Invalid confirmation.",
"whoami": "**Bot \"owner\":** {0}\n**BK moderator:** {1}",
"mandatory_response": "Mandatory response message, please ignore.",
"cmd_404": "No command \"{0}\" found!\nHint: Try `/help` without any arguments or `/help <category>`"
},
"re": {
"suffix": "^(I am not an AI, I am just a bot. This action was performed automatically by the way. You can report bugs and view my source code [here](https://github.com/ByteDice/ByteDiceAssistant)!)",
"add": "Successfully added your post to the weekly art submissions! Thank you for participating!",
"exists": "Couldn't add this post to the submissions! Luckily, it's already there! Thank you for participating!",
"mod_add": "[MOD ACTION] Successfully added this post to the weekly art submissions!",
"mod_unremove": "[MOD ACTION] Successfully un-removed this post from the weekly art submissions!"
},
"lang_load": "Successfully loaded the english language file!"
} }
+4
View File
@@ -0,0 +1,4 @@
pub mod db;
pub mod debug;
pub mod generic;
pub mod reddit;
+4
View File
@@ -0,0 +1,4 @@
pub mod add_server;
pub mod main_cmd;
pub mod reddit_channel;
pub mod wwrps_channel;
+15
View File
@@ -0,0 +1,15 @@
use crate::{db::discord::add_server, messages::send_msg, Context, Error};
pub async fn cmd(
ctx: Context<'_>
) -> Result<(), Error>
{
let r = add_server(ctx.data(), ctx.guild_id().unwrap().into()).await;
if r.is_ok()
{ send_msg(ctx, ctx.data().lang.get("dc.db.added_server", &[]), true, true).await; }
else { send_msg(ctx, ctx.data().lang.get("dc.db.corrupted_data", &[]), true, true).await; }
return Ok(());
}
@@ -1,4 +1,4 @@
use crate::{Context, Error, db_cmds::{add_server, reddit_channel, wwrps_channel}}; use crate::{Context, Error, cmds::db::{add_server, reddit_channel, wwrps_channel}};
#[derive(poise::ChoiceParameter, PartialEq)] #[derive(poise::ChoiceParameter, PartialEq)]
+12
View File
@@ -0,0 +1,12 @@
use crate::{db::discord::bind_bk, messages::send_msg, Context, Error};
pub async fn cmd(ctx: Context<'_>) -> Result<(), Error> {
let c_id = ctx.channel_id().into();
let r = bind_bk(ctx.data(), ctx.guild_id().unwrap().into(), c_id).await;
if r.is_ok()
{ send_msg(ctx, ctx.data().lang.get("dc.db.channel_bind", &[c_id.to_string()]), true, true).await; }
else { send_msg(ctx, ctx.data().lang.get("dc.db.server_404", &[]), true, true).await; }
return Ok(());
}
+15
View File
@@ -0,0 +1,15 @@
use crate::{Context, Error, db::discord::bind_wwrps, messages::send_msg};
pub async fn cmd(
ctx: Context<'_>
) -> Result<(), Error>
{
let c_id = ctx.channel_id().into();
let r = bind_wwrps(ctx.data(), ctx.guild_id().unwrap().into(), c_id).await;
if r.is_ok()
{ send_msg(ctx, ctx.data().lang.get("dc.db.channel_bind", &[c_id.to_string()]), true, true).await; }
else { send_msg(ctx, ctx.data().lang.get("dc.db.server_404", &[]), true, true).await; }
return Ok(());
}
+10
View File
@@ -0,0 +1,10 @@
pub mod guild_invite;
pub mod lang;
pub mod leave_guild;
pub mod main_cmd;
pub mod ping;
pub mod reload_cfg;
pub mod save;
pub mod stop;
pub mod view_guilds;
pub mod whoami;
@@ -1,6 +1,6 @@
use poise::serenity_prelude::{CreateInvite, GuildId}; use poise::serenity_prelude::{CreateInvite, GuildId};
use crate::{Context, Error, lang, messages::send_msg}; use crate::{Context, Error, messages::send_msg};
pub async fn cmd(ctx: Context<'_>, guild_id: u64) -> Result<(), Error> { pub async fn cmd(ctx: Context<'_>, guild_id: u64) -> Result<(), Error> {
@@ -8,7 +8,7 @@ pub async fn cmd(ctx: Context<'_>, guild_id: u64) -> Result<(), Error> {
let guild = id.to_partial_guild(ctx.http()).await?; let guild = id.to_partial_guild(ctx.http()).await?;
if !ctx.serenity_context().cache.guilds().contains(&id) { if !ctx.serenity_context().cache.guilds().contains(&id) {
send_msg(ctx, lang!("dc_msg_not_in_guild"), true, true).await; send_msg(ctx, ctx.data().lang.get("dc.db.not_in_guild", &[]), true, true).await;
return Ok(()); return Ok(());
} }
@@ -22,7 +22,12 @@ pub async fn cmd(ctx: Context<'_>, guild_id: u64) -> Result<(), Error> {
let perms = guild.user_permissions_in(&channel, bot_member); let perms = guild.user_permissions_in(&channel, bot_member);
if !perms.create_instant_invite() { if !perms.create_instant_invite() {
send_msg(ctx, lang!("dc_msg_no_perms", "CreateInvite"), true, true).await; send_msg(
ctx,
ctx.data().lang.get("dc.no_perms_external", &["CreateInvite".to_string()]),
true,
true
).await;
return Ok(()); return Ok(());
} }
+7
View File
@@ -0,0 +1,7 @@
use crate::{Context, Error, messages::send_msg};
pub async fn cmd(ctx: Context<'_>, path: String) -> Result<(), Error> {
send_msg(ctx, ctx.data().lang.get(path.as_str(), &[]), true, true).await;
return Ok(());
}
@@ -1,6 +1,6 @@
use poise::serenity_prelude::GuildId; use poise::serenity_prelude::GuildId;
use crate::{Context, Error, lang, messages::send_msg}; use crate::{Context, Error, messages::send_msg};
pub async fn cmd(ctx: Context<'_>, guild_id: u64) -> Result<(), Error> { pub async fn cmd(ctx: Context<'_>, guild_id: u64) -> Result<(), Error> {
@@ -8,12 +8,12 @@ pub async fn cmd(ctx: Context<'_>, guild_id: u64) -> Result<(), Error> {
let guild = id.to_partial_guild(ctx.http()).await?; let guild = id.to_partial_guild(ctx.http()).await?;
if !ctx.serenity_context().cache.guilds().contains(&id) { if !ctx.serenity_context().cache.guilds().contains(&id) {
send_msg(ctx, lang!("dc_msg_not_in_guild"), true, true).await; send_msg(ctx, ctx.data().lang.get("dc.db.not_in_guild", &[]), true, true).await;
return Ok(()); return Ok(());
} }
guild.leave(ctx.http()).await?; guild.leave(ctx.http()).await?;
send_msg(ctx, lang!("success"), true, true).await; send_msg(ctx, ctx.data().lang.get("generic.success", &[]), true, true).await;
return Ok(()); return Ok(());
} }
@@ -1,9 +1,10 @@
use crate::{Context, Error, debug_cmds::{guild_invite, leave_guild, ping, save, stop, view_guilds, whoami}}; use crate::{Context, Error, cmds::debug::{guild_invite, lang, leave_guild, ping, save, stop, view_guilds, whoami}};
#[derive(poise::ChoiceParameter, PartialEq)] #[derive(poise::ChoiceParameter, PartialEq)]
pub enum Subcommands { pub enum Subcommands {
GuildInvite, GuildInvite,
Lang,
LeaveGuild, LeaveGuild,
Ping, Ping,
//ReloadCfg, //ReloadCfg,
@@ -31,9 +32,11 @@ pub async fn cmd(
) -> Result<(), Error> ) -> Result<(), Error>
{ {
let u64_arg_u: u64 = u64_arg.unwrap_or("0".to_string()).as_str().parse()?; let u64_arg_u: u64 = u64_arg.unwrap_or("0".to_string()).as_str().parse()?;
let str_arg_u: String = string_arg.clone().unwrap_or("".to_string());
match subcommand { match subcommand {
Subcommands::GuildInvite => guild_invite::cmd(ctx, u64_arg_u).await?, Subcommands::GuildInvite => guild_invite::cmd(ctx, u64_arg_u).await?,
Subcommands::Lang => lang::cmd(ctx, str_arg_u).await?,
Subcommands::LeaveGuild => leave_guild::cmd(ctx, u64_arg_u).await?, Subcommands::LeaveGuild => leave_guild::cmd(ctx, u64_arg_u).await?,
Subcommands::Ping => ping::cmd(ctx).await?, Subcommands::Ping => ping::cmd(ctx).await?,
//Subcommands::ReloadCfg => reload_cfg::cmd(ctx).await?, //Subcommands::ReloadCfg => reload_cfg::cmd(ctx).await?,
+17
View File
@@ -0,0 +1,17 @@
use crate::{Context, Error, db::{discord, reddit}, messages::{edit_reply, send_msg}};
pub async fn cmd(ctx: Context<'_>) -> Result<(), Error> {
let msg = send_msg(
ctx,
ctx.data().lang.get("dc.db.saving_progress", &[]),
true,
true
).await.unwrap();
discord::write_data(ctx.data()).await;
reddit ::write_data().await;
edit_reply(ctx, msg, ctx.data().lang.get("dc.db.saving_done", &[])).await;
return Ok(());
}
@@ -2,7 +2,7 @@ use std::process;
use poise::serenity_prelude::OnlineStatus; use poise::serenity_prelude::OnlineStatus;
use crate::{data, lang, messages::{edit_reply, send_msg}, websocket::send_cmd_json, Context, Error}; use crate::{db::{discord, reddit}, messages::{edit_reply, send_msg}, websocket::send_cmd_json, Context, Error};
pub async fn cmd(ctx: Context<'_>, confirmation: Option<String>) -> Result<(), Error> { pub async fn cmd(ctx: Context<'_>, confirmation: Option<String>) -> Result<(), Error> {
let stop_confirm = "i want to stop the bot now".replace(" ", ""); let stop_confirm = "i want to stop the bot now".replace(" ", "");
@@ -10,15 +10,21 @@ pub async fn cmd(ctx: Context<'_>, confirmation: Option<String>) -> Result<(), E
let should_stop = ctx.data().args.dev || confirm_formatted == stop_confirm; let should_stop = ctx.data().args.dev || confirm_formatted == stop_confirm;
if should_stop { if should_stop {
let msg = send_msg(ctx, lang!("dc_msg_owner_data_save"), true, true).await.unwrap(); let msg = send_msg(
data::write_dc_data(ctx.data()).await; ctx,
data::write_re_data().await; ctx.data().lang.get("dc.db.saving_progress", &[]),
true,
true
).await.unwrap();
discord::write_data(ctx.data()).await;
reddit ::write_data().await;
send_cmd_json("stop_praw", None, true).await; send_cmd_json("stop_praw", None, true).await;
let complete = format!( let complete = format!(
"{}\n{}", "{}\n{}",
lang!("dc_msg_owner_data_save_complete"), ctx.data().lang.get("dc.db.saving_done", &[]),
lang!("dc_msg_owner_shutdown") ctx.data().lang.get("dc.shutdown", &[])
); );
edit_reply(ctx, msg, complete).await; edit_reply(ctx, msg, complete).await;
@@ -27,9 +33,7 @@ pub async fn cmd(ctx: Context<'_>, confirmation: Option<String>) -> Result<(), E
process::exit(0); process::exit(0);
} }
else { else { send_msg(ctx, ctx.data().lang.get("dc.shutdown_bad_confirm", &[]), true, true).await; }
send_msg(ctx, lang!("dc_msg_owner_shutdown_failed_confirmation"), true, true).await;
}
return Ok(()); return Ok(());
} }
+18
View File
@@ -0,0 +1,18 @@
use crate::{messages::send_msg, Context, Error};
pub async fn cmd(ctx: Context<'_>) -> Result<(), Error> {
let data = ctx.data();
let uid: u64 = ctx.author().id.into();
let is_owner = data.env_vars.bot_owners.contains(&uid);
let is_bk_mod = data.env_vars.reddit_mod_discord_ids.contains(&uid);
send_msg(
ctx,
ctx.data().lang.get("dc.whoami", &[is_owner.to_string(), is_bk_mod.to_string()]),
true,
true
).await;
return Ok(());
}
+5
View File
@@ -0,0 +1,5 @@
pub mod eight_ball;
pub mod embed;
pub mod help;
pub mod send;
pub mod wwrps;
@@ -1,6 +1,6 @@
use rand::{seq::IteratorRandom, Rng}; use rand::{seq::IteratorRandom, Rng};
use crate::{data::get_toml_mutex, lang, messages::send_msg, Context, Error}; use crate::{messages::send_msg, Context, Error};
#[poise::command( #[poise::command(
@@ -16,14 +16,16 @@ pub async fn cmd(
#[description = "Question to ask."] question: String #[description = "Question to ask."] question: String
) -> Result<(), Error> ) -> Result<(), Error>
{ {
let quirky_chance = get_toml_mutex(&ctx.data().cfg).await.unwrap()["commands"]["eight_ball_quirky_chance"].as_float().unwrap(); let quirky_chance = &ctx.data().cfg
["commands"]["eight_ball_quirky_chance"]
.as_float().unwrap();
let is_quirky = rand::rng().random_bool(quirky_chance.clamp(0.0, 1.0)); let is_quirky = rand::rng().random_bool(quirky_chance.clamp(0.0, 1.0));
let list = &ctx.data().ball_prompts[if is_quirky { 1 } else { 0 }]; let list = &ctx.data().ball_prompts[if is_quirky { 1 } else { 0 }];
let rand_item = list.iter().choose(&mut rand::rng()); let rand_item = list.iter().choose(&mut rand::rng());
send_msg( send_msg(
ctx, ctx,
lang!("dc_msg_8-ball_answer", question, rand_item.unwrap()), ctx.data().lang.get("dc.8_ball", &[question, rand_item.unwrap().to_string()]),
false, false,
true true
).await; ).await;
@@ -1,6 +1,6 @@
use poise::serenity_prelude::Timestamp; use poise::serenity_prelude::Timestamp;
use crate::{lang, messages::{send_embed, send_msg, Author, EmbedOptions}, Context, Error}; use crate::{messages::{send_embed, send_msg, Author, EmbedOptions}, Context, Error};
#[allow(clippy::too_many_arguments)] #[allow(clippy::too_many_arguments)]
#[poise::command( #[poise::command(
@@ -46,7 +46,7 @@ pub async fn cmd(
).await; ).await;
if !reply_unwrap { if !reply_unwrap {
send_msg(ctx, lang!("dc_msg_mandatory_response"), true, true).await; send_msg(ctx, ctx.data().lang.get("dc.mandatory_response", &[]), true, true).await;
} }
return Ok(()); return Ok(());
@@ -1,7 +1,7 @@
use std::collections::HashMap; use std::collections::HashMap;
use tokio::fs; use tokio::fs;
use crate::{lang, messages::send_msg, Context, Cmd, Error}; use crate::{messages::send_msg, Context, Cmd, Error};
#[derive(poise::ChoiceParameter, PartialEq)] #[derive(poise::ChoiceParameter, PartialEq)]
@@ -82,7 +82,7 @@ async fn send_single_help(ctx: Context<'_>, mut cmd_name: String) {
if cmd.is_none() { if cmd.is_none() {
send_msg( send_msg(
ctx, ctx,
lang!("dc_msg_cmd_404", cmd_name), ctx.data().lang.get("dc.cmd_404", &[cmd_name]),
true, true,
true true
).await; ).await;
@@ -108,7 +108,7 @@ async fn send_category_help(ctx: Context<'_>, category: HelpOptions) {
async fn send_bk_week_help_re(ctx: Context<'_>) { async fn send_bk_week_help_re(ctx: Context<'_>) {
let t: String = fs::read_to_string("./bk_week_help_re.md").await let t: String = fs::read_to_string("./bk_week_help_re.md").await
.unwrap_or(lang!("dc_msg_re_help_removed")); .unwrap_or(ctx.data().lang.get("dc.re.help_404", &[]));
send_msg(ctx, t, true, true).await; send_msg(ctx, t, true, true).await;
} }
@@ -1,4 +1,4 @@
use crate::{lang, messages::send_msg, Context, Error}; use crate::{messages::send_msg, Context, Error};
#[poise::command( #[poise::command(
@@ -16,6 +16,6 @@ pub async fn cmd(
) -> Result<(), Error> ) -> Result<(), Error>
{ {
send_msg(ctx, msg.replace("\\n", "\n"), false, false).await; send_msg(ctx, msg.replace("\\n", "\n"), false, false).await;
send_msg(ctx, lang!("dc_msg_mandatory_response"), true, true).await; send_msg(ctx, ctx.data().lang.get("dc.mandatory_response", &[]), true, true).await;
return Ok(()); return Ok(());
} }
+156
View File
@@ -0,0 +1,156 @@
use poise::serenity_prelude::{ChannelId, Mentionable};
use serde_json::Value;
use tokio::sync::MutexGuard;
use crate::{Context, Error, games::wwrps::{game::{RPS, RPSGame, RPSPlayer}, ranks::{RPSStats, Ranks}}, lang::Lang, messages::{http_send_msg, send_msg}};
#[poise::command(
slash_command,
prefix_command,
category = "fun",
rename = "wwrps",
required_bot_permissions = "SEND_MESSAGES | VIEW_CHANNEL"
)]
/// World-Wide Rock Paper Scissors. Play with a completely random person!
pub async fn cmd(
ctx: Context<'_>,
selection: RPS,
#[description = "If true, replaces your username with [Anonymous]."] anonymous: bool
) -> Result<(), Error>
{
let c_o = get_wwrps_channel(ctx).await;
if c_o.is_none() { send_msg(ctx, ctx.data().lang.get("dc.db.wwrps_404", &[]), true, true).await; return Ok(()); }
let c = c_o.unwrap();
send_msg(ctx, ctx.data().lang.get("dc.wwrps.submitting", &[]), true, true).await;
let mut game = ctx.data().rps_game.lock().await;
if game.is_full_lobby() { return Ok(()) }
let data_lock = &ctx.data().discord_data.lock().await["users"];
let r = game.add_player(RPSPlayer {
selection,
user: ctx.author().clone(),
wwrps_channel: ChannelId::new(c),
anonymous,
stats: get_player_stats(ctx.author().id.get(), data_lock)
}, ctx.data().args.dev);
if let Err(_) = r {
send_msg(ctx, ctx.data().lang.get("dc.wwrps.already_submitted", &[]), true, true).await;
return Ok(());
}
let full = r.unwrap();
if !full { return Ok(()); }
let winner = game.get_winner();
let p1 = game.players[0].as_ref().unwrap();
let p2 = game.players[1].as_ref().unwrap();
let old_elos = [p1.stats.elo, p2.stats.elo];
let expected = RPSStats::get_elo_expected(old_elos[0], old_elos[1]);
if let Some(w) = winner {
// 0 means p1 wins, and 1 means p2 wins
// we flip it because ELO counts 0 as a loss
game.players[0].as_mut().unwrap().stats.update_elo(expected, (!w) as f32);
game.players[1].as_mut().unwrap().stats.update_elo(expected, w as f32);
}
let r_text = results_text(&game, &ctx.data().lang, winner, old_elos);
// clone and clear here to prevent race conditions while
// sending the results
let game_clone = game.clone();
game.clear();
let mut used_channels: Vec<ChannelId> = Vec::new();
// send the results
for player in &game_clone.players {
if let Some(p) = player {
if used_channels.contains(&p.wwrps_channel) { continue; }
used_channels.push(p.wwrps_channel);
http_send_msg(ctx.http(), p.wwrps_channel, r_text.clone()).await;
}
}
return Ok(());
}
fn results_text(
game: &MutexGuard<'_, RPSGame>,
lang: &Lang,
winner: Option<i8>,
old_elos: [u16; 2]
) -> String
{
let winner_text: String;
let p1 = game.players[0].as_ref().unwrap();
let p2 = game.players[1].as_ref().unwrap();
let p1_n = if !p1.anonymous { p1.user.mention().to_string() }
else { lang.get("dc.wwrps.anon", &[]) };
let p2_n = if !p2.anonymous { p2.user.mention().to_string() }
else { lang.get("dc.wwrps.anon", &[]) };
if let Some(w) = winner {
winner_text = if w == 0 { lang.get("dc.wwrps.p1_win", &[]) }
else { lang.get("dc.wwrps.p2_win", &[]) }; }
else { winner_text = lang.get("dc.wwrps.draw", &[]); }
return lang.get(
"dc.wwrps.match",
&[
p1.selection.to_string(), // {0}
p2.selection.to_string(), // {1}
winner_text, // {2}
p1_n, // {3}
old_elos[0].to_string(), // {4}
(p1.stats.elo - old_elos[0]).to_string(), // {5}
Ranks::from_elo(p1.stats.elo).to_string(), // {6}
p2_n, // {7}
old_elos[1].to_string(), // {8}
(p2.stats.elo - old_elos[1]).to_string(), // {9}
Ranks::from_elo(p2.stats.elo).to_string(), // {10}
]);
}
async fn get_wwrps_channel(ctx: Context<'_>) -> Option<u64> {
let d = &ctx.data().discord_data.lock().await;
let is_guild = ctx.guild_channel().await.is_some();
if !is_guild { return Some(ctx.channel_id().get()); }
let Some(servers) = d.get("servers") else { return None; };
let Some(s) = servers.get(ctx.guild_id().unwrap().get().to_string()) else { return None; };
let Some(c_id) = s.get("wwrps_channel") else { return None; };
return Some(c_id.as_u64().unwrap());
}
fn get_player_stats(uid: u64, db: &Value) -> RPSStats {
if let Some(user) = db.get(uid.to_string()) {
if let Some(elo) = user.get("wwrps_elo") {
if let Some(elo_i64) = elo.as_i64()
{ return RPSStats::from(uid, elo_i64 as u16); }
}
}
return RPSStats::new(uid);
}
+9
View File
@@ -0,0 +1,9 @@
pub mod add;
pub mod approve;
pub mod generic_fns;
pub mod get;
pub mod remove;
pub mod shorturl;
pub mod top;
pub mod update;
pub mod vote;
+12 -12
View File
@@ -1,11 +1,10 @@
use serde_json::json; use serde_json::json;
use crate::data::{get_mutex_data}; use crate::db::reddit::{self, POSTS_KEY};
use crate::messages::send_msg; use crate::messages::send_msg;
use crate::re_cmds::get::get_post_from_data; use crate::cmds::reddit::get::get_post_from_data;
use crate::{data, websocket::send_cmd_json, Context, Error, CFG_DATA_RE}; use crate::{websocket::send_cmd_json, Context, Error};
use crate::re_cmds::generic_fns::{is_bk_mod_msg, send_embed_for_post, to_shorturl}; use crate::cmds::reddit::generic_fns::{is_bk_mod_msg, send_embed_for_post, to_shorturl};
use crate::lang;
#[poise::command( #[poise::command(
slash_command, slash_command,
@@ -25,6 +24,7 @@ pub async fn cmd(
let shorturl_u = to_shorturl(&url); let shorturl_u = to_shorturl(&url);
let shorturl = &shorturl_u.unwrap_or(url.clone()); let shorturl = &shorturl_u.unwrap_or(url.clone());
let short_s = shorturl.to_string();
let a = approve.unwrap_or(false); let a = approve.unwrap_or(false);
let r = send_cmd_json("add_post_url", Some(json!([&shorturl, a, true])), true).await.unwrap(); let r = send_cmd_json("add_post_url", Some(json!([&shorturl, a, true])), true).await.unwrap();
@@ -41,18 +41,18 @@ pub async fn cmd(
return Ok(()); return Ok(());
} }
data::update_re_data(ctx.data()).await; reddit::update_data().await;
let reddit_data = get_mutex_data(&ctx.data().reddit_data).await?; let reddit_data = &ctx.data().reddit_data.lock().await;
if let Some(bk_week) = reddit_data.get(CFG_DATA_RE) { if let Some(bk_week) = reddit_data.get(POSTS_KEY) {
if let Some(post) = bk_week.get(shorturl) { if let Some(post) = bk_week.get(shorturl) {
if post["removed"]["removed"].as_bool().unwrap() if post["removed"]["removed"].as_bool().unwrap()
{ send_msg(ctx, lang!("dc_msg_re_post_unremove_success", &shorturl), true, true).await; } { send_msg(ctx, ctx.data().lang.get("dc.re.remove.unremove", &[short_s]), true, true).await; }
else { send_msg(ctx, lang!("dc_msg_re_post_update_success", &shorturl), true, true).await; } else { send_msg(ctx, ctx.data().lang.get("dc.re.update_post", &[short_s]), true, true).await; }
} }
else { send_msg(ctx, lang!("dc_msg_re_post_add_success", &shorturl), true, true).await; } else { send_msg(ctx, ctx.data().lang.get("dc.re.add.success", &[short_s]), true, true).await; }
if a { send_msg(ctx, lang!("dc_msg_re_also_approved"), true, true).await; } if a { send_msg(ctx, ctx.data().lang.get("dc.re.add.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? {
@@ -1,6 +1,6 @@
use serde_json::{json, Value}; use serde_json::{json, Value};
use crate::{data::{self, get_mutex_data}, lang, messages::send_msg, re_cmds::generic_fns::{is_bk_mod_msg, to_shorturl}, websocket, Context, Error, CFG_DATA_RE}; use crate::{Context, Error, db::{reddit::POSTS_KEY}, messages::send_msg, cmds::reddit::generic_fns::{is_bk_mod_msg, to_shorturl}, websocket};
use super::generic_fns::send_embed_for_removed; use super::generic_fns::send_embed_for_removed;
@@ -23,8 +23,7 @@ pub async fn cmd(
let shorturl_u = to_shorturl(&url); let shorturl_u = to_shorturl(&url);
let shorturl = &shorturl_u.unwrap_or(url.clone()); let shorturl = &shorturl_u.unwrap_or(url.clone());
data::update_re_data(ctx.data()).await; let reddit_data = &ctx.data().reddit_data.lock().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;
@@ -33,7 +32,7 @@ pub async fn cmd(
async fn approve_cmd(ctx: Context<'_>, url: &str, reddit_data: &Value, approve: bool) { async fn approve_cmd(ctx: Context<'_>, url: &str, reddit_data: &Value, approve: bool) {
if let Some(post) = reddit_data.get(CFG_DATA_RE).unwrap().get(url) { if let Some(post) = reddit_data.get(POSTS_KEY).unwrap().get(url) {
if post["removed"]["removed"].as_bool().unwrap() { if post["removed"]["removed"].as_bool().unwrap() {
send_embed_for_removed(ctx, url, post).await; send_embed_for_removed(ctx, url, post).await;
return; return;
@@ -42,17 +41,14 @@ async fn approve_cmd(ctx: Context<'_>, url: &str, reddit_data: &Value, approve:
let r = websocket::send_cmd_json("set_approve_post", Some(json!([approve, &url])), true).await.unwrap(); let r = websocket::send_cmd_json("set_approve_post", Some(json!([approve, &url])), true).await.unwrap();
if r["value"].as_bool().unwrap() { if r["value"].as_bool().unwrap() {
if approve { if approve {
send_msg(ctx, lang!("dc_msg_re_post_approve_success"), true, true).await; send_msg(ctx, ctx.data().lang.get("dc.re.approve.success", &[]), true, true).await;
} }
else { else {
send_msg(ctx, lang!("dc_msg_re_post_disapprove_success"), true, true).await; send_msg(ctx, ctx.data().lang.get("dc.re.approve.disapprove", &[]), true, true).await;
}
} }
} }
else { else {
send_msg(ctx, lang!("dc_msg_err_trace", "`re_cmds -> approve.rs -> cmd() -> unwrap websocket result error`"), true, true).await; send_msg(ctx, ctx.data().lang.get("dc.re.post_404", &[]), false, false).await;
}
}
else {
send_msg(ctx, lang!("dc_msg_re_post_404"), false, false).await;
} }
} }
@@ -2,7 +2,7 @@ use poise::serenity_prelude::{self as serenity, ChannelId, ComponentInteraction,
use regex::Regex; use regex::Regex;
use serde_json::Value; use serde_json::Value;
use crate::{data::get_toml_mutex, lang, messages::{decode_and_decompress_json, embed_from_options, make_post_embed, make_removed_embed, send_embed, send_msg, EmbedOptions, JSON_TEXT_END, JSON_TEXT_START}, Context, Data, Error}; use crate::{Context, Data, Error, messages::{EmbedOptions, JSON_TEXT_END, JSON_TEXT_START, decode_and_decompress_json, embed_from_options, make_post_embed, make_removed_embed, send_embed, send_msg}};
pub fn is_bk_mod(mod_list: Vec<u64>, uid: u64) -> bool { pub fn is_bk_mod(mod_list: Vec<u64>, uid: u64) -> bool {
return mod_list.contains(&uid); return mod_list.contains(&uid);
@@ -10,19 +10,19 @@ pub fn is_bk_mod(mod_list: Vec<u64>, uid: u64) -> bool {
pub async fn is_bk_mod_msg(ctx: Context<'_>) -> bool { 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; } if is_bk_mod(ctx.data().env_vars.reddit_mod_discord_ids.clone(), ctx.author().id.get()) { return true; }
let sr = get_readable_subreddits(ctx.data()).await.unwrap(); let sr = get_readable_subreddits(ctx.data()).await.unwrap();
send_msg(ctx, lang!("dc_msg_re_permdeny_not_re_mod", sr), true, true).await; send_msg(ctx, ctx.data().lang.get("dc.re.not_mod", &[sr]), true, true).await;
return false return false
} }
pub async fn is_bk_mod_serenity(ctx: &serenity::Context, data: &Data, author: &Member, component: &ComponentInteraction) -> bool { pub async fn is_bk_mod_serenity(ctx: &serenity::Context, data: &Data, author: &Member, component: &ComponentInteraction) -> bool {
if is_bk_mod(data.bk_mods.clone(), author.user.id.get()) { return true; } if is_bk_mod(data.env_vars.reddit_mod_discord_ids.clone(), author.user.id.get()) { return true; }
let sr = get_readable_subreddits(data).await.unwrap(); let sr = get_readable_subreddits(data).await.unwrap();
serenity_send_msg(ctx, component, lang!("dc_msg_re_permdeny_not_re_mod", sr), true).await; serenity_send_msg(ctx, component, data.lang.get("dc.re.not_mod", &[sr]), true).await;
return true return true
} }
@@ -55,7 +55,7 @@ pub fn to_shorturl(url: &str) -> Result<String, &str> {
pub async fn send_embed_for_post(ctx: Context<'_>, post: Value, url: &str) -> Result<(), Error> { 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; send_embed(ctx, make_post_embed(&ctx.data().lang, &post, url, true), true).await;
return Ok(()); return Ok(());
} }
@@ -63,15 +63,14 @@ pub async fn send_embed_for_post(ctx: Context<'_>, post: Value, url: &str) -> Re
pub async fn send_embed_for_removed(ctx: Context<'_>, url: &str, post: &Value) { pub async fn send_embed_for_removed(ctx: Context<'_>, url: &str, post: &Value) {
send_embed( send_embed(
ctx, ctx,
make_removed_embed(post, url, true), make_removed_embed(&ctx.data().lang, post, url, true),
true true
).await; ).await;
} }
pub async fn get_readable_subreddits(data: &Data) -> Result<String, Error> { pub async fn get_readable_subreddits(data: &Data) -> Result<String, Error> {
let d = get_toml_mutex(&data.cfg).await.unwrap(); let sr = data.cfg["reddit"]["subreddits"].as_array().unwrap();
let sr = d["reddit"]["subreddits"].as_array().unwrap();
let sr_str: Vec<&str> = sr let sr_str: Vec<&str> = sr
.iter() .iter()
.map(|v| v.as_str().unwrap()) .map(|v| v.as_str().unwrap())
@@ -1,6 +1,6 @@
use serde_json::Value; use serde_json::Value;
use crate::{data::{self, get_mutex_data}, lang, messages::send_msg, re_cmds::generic_fns::{send_embed_for_post, to_shorturl}, rs_println, Context, Error, CFG_DATA_RE}; use crate::{Context, Error, db::{reddit::{self, POSTS_KEY}}, messages::send_msg, cmds::reddit::generic_fns::{send_embed_for_post, to_shorturl}, rs_println};
use super::generic_fns::send_embed_for_removed; use super::generic_fns::send_embed_for_removed;
@@ -17,12 +17,12 @@ pub async fn cmd(
#[description = "The post URL."] url: String #[description = "The post URL."] url: String
) -> Result<(), Error> ) -> Result<(), Error>
{ {
data::update_re_data(ctx.data()).await; reddit::update_data().await;
let shorturl_u = to_shorturl(&url); let shorturl_u = to_shorturl(&url);
let shorturl = &shorturl_u.unwrap_or(url.clone()); let shorturl = &shorturl_u.unwrap_or(url.clone());
let reddit_data = get_mutex_data(&ctx.data().reddit_data).await?; let reddit_data = &ctx.data().reddit_data.lock().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, shorturl).await?; send_embed_for_post(ctx, post, shorturl).await?;
@@ -33,7 +33,7 @@ pub async fn cmd(
pub async fn get_post_from_data(ctx: Context<'_>, reddit_data: &Value, url: &str) -> Result<Option<Value>, Error> { pub 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(CFG_DATA_RE) { if let Some(bk_week) = reddit_data.get(POSTS_KEY) {
if let Some(post) = bk_week.get(url) { if let Some(post) = bk_week.get(url) {
if post["removed"]["removed"].as_bool().unwrap() { if post["removed"]["removed"].as_bool().unwrap() {
send_embed_for_removed(ctx, url, post).await; send_embed_for_removed(ctx, url, post).await;
@@ -42,11 +42,11 @@ pub async fn get_post_from_data(ctx: Context<'_>, reddit_data: &Value, url: &str
return Ok(Some(post.clone())); return Ok(Some(post.clone()));
} }
else { else {
send_msg(ctx, lang!("dc_msg_re_post_404", url), true, true).await; send_msg(ctx, ctx.data().lang.get("dc.re.post_404", &[url.to_string()]), true, true).await;
} }
} }
else { else {
send_msg(ctx, lang!("dc_msg_re_data_corrupted", url), true, true).await; send_msg(ctx, ctx.data().lang.get("cd.re.post_corrupted", &[url.to_string()]), true, true).await;
rs_println!("{}", serde_json::to_string_pretty(reddit_data)?); rs_println!("{}", serde_json::to_string_pretty(reddit_data)?);
} }
return Ok(None); return Ok(None);
@@ -1,6 +1,6 @@
use serde_json::json; use serde_json::json;
use crate::{data::{self, get_mutex_data}, lang, messages::send_msg, re_cmds::{generic_fns::{is_bk_mod_msg, send_embed_for_removed, to_shorturl}, get::get_post_from_data}, websocket::send_cmd_json, Context, Error}; use crate::{Context, Error, db::reddit, messages::send_msg, cmds::reddit::{generic_fns::{is_bk_mod_msg, send_embed_for_removed, to_shorturl}, get::get_post_from_data}, websocket::send_cmd_json};
#[poise::command( #[poise::command(
slash_command, slash_command,
@@ -27,17 +27,17 @@ pub async fn cmd(
if r["value"].as_bool().unwrap() { if r["value"].as_bool().unwrap() {
send_msg( send_msg(
ctx, ctx,
lang!("dc_msg_re_post_remove_success", &shorturl), ctx.data().lang.get("dc.re.remove.success", &[shorturl.to_string()]),
true, true,
true true
).await; ).await;
} }
else { else {
send_msg(ctx, lang!("dc_msg_re_post_404"), true, true).await; send_msg(ctx, ctx.data().lang.get("dc.re.post_404", &[]), true, true).await;
} }
data::update_re_data(ctx.data()).await; reddit::update_data().await;
let reddit_data = get_mutex_data(&ctx.data().reddit_data).await?; let reddit_data = &ctx.data().reddit_data.lock().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() { if post["removed"]["removed"].as_bool().unwrap() {
@@ -1,4 +1,4 @@
use crate::{lang, messages::send_msg, re_cmds::generic_fns::to_shorturl, Context, Error}; use crate::{messages::send_msg, cmds::reddit::generic_fns::to_shorturl, Context, Error};
#[poise::command( #[poise::command(
@@ -17,10 +17,10 @@ pub async fn cmd(
let shorturl = to_shorturl(&url); let shorturl = to_shorturl(&url);
if shorturl.is_ok() { if shorturl.is_ok() {
send_msg(ctx, lang!("dc_msg_shorturl", shorturl.unwrap()), true, true).await; send_msg(ctx, ctx.data().lang.get("dc.re.shorturl", &[shorturl.unwrap()]), true, true).await;
} }
else { else {
send_msg(ctx, lang!("dc_msg_failed_shorturl_conversion"), true, true).await; send_msg(ctx, ctx.data().lang.get("dc.re.shorturl_fail", &[]), true, true).await;
} }
return Ok(()); return Ok(());
@@ -1,6 +1,6 @@
use std::collections::HashMap; use std::collections::HashMap;
use crate::{data::get_mutex_data, re_cmds::generic_fns::send_embed_for_post, Context, Error, CFG_DATA_RE}; use crate::{Context, Error, db::reddit::POSTS_KEY, cmds::reddit::generic_fns::send_embed_for_post};
#[derive(poise::ChoiceParameter, PartialEq)] #[derive(poise::ChoiceParameter, PartialEq)]
enum TopCategory { enum TopCategory {
@@ -30,7 +30,7 @@ pub async fn cmd(
) -> Result<(), Error> ) -> Result<(), Error>
{ {
let mut all: HashMap<&str, i32> = HashMap::new(); let mut all: HashMap<&str, i32> = HashMap::new();
let posts = &get_mutex_data(&ctx.data().reddit_data).await?[CFG_DATA_RE]; let posts = &ctx.data().reddit_data.lock().await[POSTS_KEY];
let posts_u = posts.as_object().unwrap(); let posts_u = posts.as_object().unwrap();
for (url, dat) in posts_u { for (url, dat) in posts_u {
@@ -3,7 +3,7 @@ use std::time::{SystemTime, UNIX_EPOCH};
use poise::{serenity_prelude::{ChannelId, EditMessage, GetMessages, Http, Message, MessageId, UserId}, ReplyHandle}; use poise::{serenity_prelude::{ChannelId, EditMessage, GetMessages, Http, Message, MessageId, UserId}, ReplyHandle};
use serde_json::{json, Map, Value}; use serde_json::{json, Map, Value};
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}; use crate::{Context, Error, cmds::reddit::generic_fns::embed_to_json, db::{discord::contains_server, keys::DC_POSTS_CHANNEL_KEY, reddit::{self, POSTS_KEY}}, lang::Lang, messages::{edit_reply, embed_from_options, make_post_embed, make_removed_embed, send_embed, send_msg, trim_post_json}, rs_println, websocket::send_cmd_json};
#[poise::command( #[poise::command(
slash_command, slash_command,
@@ -34,71 +34,70 @@ pub async fn cmd(
let mut p_text = "`/re_updatediscord`:".to_string(); let mut p_text = "`/re_updatediscord`:".to_string();
let progress = send_msg(ctx, p_text.clone(), true, true).await.unwrap(); let progress = send_msg(ctx, p_text.clone(), true, true).await.unwrap();
p_text = update_progress(ctx, progress.clone(), p_text, lang!("dc_msg_update_fetch", "\n")).await; p_text = update_progress(ctx, progress.clone(), p_text, ctx.data().lang.get("dc.re.update.fetch", &[])).await;
let max_age_u = max_age.unwrap_or(8); let max_age_u = max_age.unwrap_or(8);
let max_age_secs = max_age_u as u64 * (60 * 60 * 24); let max_age_secs = max_age_u as u64 * (60 * 60 * 24);
let max_results_toml = &get_toml_mutex(&ctx.data().cfg).await.unwrap(); let max_results_pre = ctx.data().cfg["reddit"]["fetch_limit"].as_integer().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); 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; send_cmd_json("add_new_posts", Some(json!([max_age_secs, max_results_final])), true).await;
data::update_re_data(ctx.data()).await; reddit::update_data().await;
let r_data = get_mutex_data(&ctx.data().reddit_data).await?; let r_data = &ctx.data().reddit_data.lock().await;
let c_id_u = get_c_id(ctx).await; let c_id_u = get_c_id(ctx).await;
if c_id_u.is_none() { if c_id_u.is_none() {
send_msg(ctx, lang!("dc_msg_re_posts_channel_404"), true, true).await; send_msg(ctx, ctx.data().lang.get("dc.db.bk_404", &[]), true, true).await;
return Ok(()); return Ok(());
} }
let c_id = c_id_u.unwrap(); let c_id = c_id_u.unwrap();
// Reading messages // Reading messages
p_text = update_progress(ctx, progress.clone(), p_text.clone(), lang!("dc_msg_update_read", "\n", c_id)).await; p_text = update_progress(ctx, progress.clone(), p_text.clone(), ctx.data().lang.get("dc.re.update.read", &[c_id.to_string()])).await;
let msgs = read_msgs(http, ctx.framework().bot_id, c_id).await; let msgs = read_msgs(http, ctx.framework().bot_id, c_id).await;
// Parsing messages to JSON // Parsing messages to JSON
p_text = update_progress(ctx, progress.clone(), p_text.clone(), lang!("dc_msg_update_parse", "\n")).await; p_text = update_progress(ctx, progress.clone(), p_text.clone(), ctx.data().lang.get("dc.re.update.parse", &[])).await;
let msgs_json = msgs_to_json(msgs, &r_data, max_age_secs).await; let msgs_json = msgs_to_json(msgs, &r_data, max_age_secs).await;
if ctx.data().args.dev { rs_println!("Posts changelog: {}", msgs_json); } if ctx.data().args.dev { rs_println!("Posts changelog: {}", msgs_json); }
// Adding new posts // Adding new posts
p_text = update_progress(ctx, progress.clone(), p_text.clone(), lang!("dc_msg_update_add", "\n")).await; p_text = update_progress(ctx, progress.clone(), p_text.clone(), ctx.data().lang.get("dc.re.update.add", &[])).await;
let weekly_art = r_data[CFG_DATA_RE].as_object().unwrap(); let weekly_art = r_data[POSTS_KEY].as_object().unwrap();
add_posts(ctx, weekly_art, &msgs_json, max_age_secs, max_results_final).await; add_posts(ctx, weekly_art, &msgs_json, max_age_secs, max_results_final).await;
// Stop if only_add // Stop if only_add
if only_add.unwrap_or(false) { if only_add.unwrap_or(false) {
send_msg(ctx, lang!("dc_msg_update_done", "`/bk_week_update`\n## "), true, true).await; send_msg(ctx, ctx.data().lang.get("dc.re.update.done", &[]), true, true).await;
update_progress(ctx, progress.clone(), p_text, lang!("dc_msg_update_done", "\n## ")).await; update_progress(ctx, progress.clone(), p_text, ctx.data().lang.get("dc.re.update.done", &[])).await;
return Ok(()); return Ok(());
} }
// Removing duplicate posts // Removing duplicate posts
p_text = update_progress(ctx, progress.clone(), p_text.clone(), lang!("dc_msg_update_removing_dupe", "\n")).await; p_text = update_progress(ctx, progress.clone(), p_text.clone(), ctx.data().lang.get("dc.re.update.rm_dupe", &[])).await;
remove_dupes(http, c_id, &msgs_json).await; remove_dupes(http, c_id, &msgs_json).await;
// Removing removed posts // Removing removed posts
p_text = update_progress(ctx, progress.clone(), p_text.clone(), lang!("dc_msg_update_removing", "\n")).await; p_text = update_progress(ctx, progress.clone(), p_text.clone(), ctx.data().lang.get("dc.re.update.rm", &[])).await;
remove_posts(http, c_id, weekly_art, &msgs_json).await; remove_posts(http, &ctx.data().lang, c_id, weekly_art, &msgs_json).await;
// Removing old posts // Removing old posts
if max_age_u > 0 { if max_age_u > 0 {
p_text = update_progress(ctx, progress.clone(), p_text.clone(), lang!("dc_msg_update_removing_old", "\n", max_age_u)).await; p_text = update_progress(ctx, progress.clone(), p_text.clone(), ctx.data().lang.get("dc.re.update.rm_old", &[max_age_u.to_string()])).await;
remove_old(http, c_id, &msgs_json).await; remove_old(http, c_id, &msgs_json).await;
send_cmd_json("remove_old_posts", Some(json!([max_age_secs])), true).await; send_cmd_json("remove_old_posts", Some(json!([max_age_secs])), true).await;
} }
// Editing updated posts // Editing updated posts
p_text = update_progress(ctx, progress.clone(), p_text.clone(), lang!("dc_msg_update_editing", "\n")).await; p_text = update_progress(ctx, progress.clone(), p_text.clone(), ctx.data().lang.get("dc.re.update.editing", &[])).await;
edit_posts(http, c_id, weekly_art, &msgs_json).await; edit_posts(http, &ctx.data().lang, c_id, weekly_art, &msgs_json).await;
// Done // Done
update_progress(ctx, progress.clone(), p_text, lang!("dc_msg_update_done", "\n## ")).await; update_progress(ctx, progress.clone(), p_text, ctx.data().lang.get("dc.re.update.done", &[])).await;
send_msg(ctx, lang!("dc_msg_update_done", "`/bk_week_update`\n## "), true, true).await; send_msg(ctx, ctx.data().lang.get("dc.re.update.parse", &[]), true, true).await;
return Ok(()); return Ok(());
} }
@@ -113,12 +112,12 @@ async fn update_progress(ctx: Context<'_>, p: ReplyHandle<'_>, t: String, added_
async fn get_c_id(ctx: Context<'_>) -> Option<ChannelId> { async fn get_c_id(ctx: Context<'_>) -> Option<ChannelId> {
if !data::dc_contains_server(ctx.data(), ctx.guild_id().unwrap().into()).await { if !contains_server(ctx.data(), ctx.guild_id().unwrap().into()).await {
send_msg(ctx, lang!("dc_msg_data_server_404"), true, true).await; send_msg(ctx, ctx.data().lang.get("dc.db.server_404", &[]), true, true).await;
return None; return None;
} }
let d = get_mutex_data(&ctx.data().discord_data).await.unwrap(); let d = &ctx.data().discord_data.lock().await;
let c_id_u = let c_id_u =
d["servers"] d["servers"]
[ctx.guild_id().unwrap().to_string()] [ctx.guild_id().unwrap().to_string()]
@@ -188,7 +187,7 @@ async fn msgs_to_json(msgs: Vec<Message>, reddit_data: &Value, max_age: u64) ->
if msg_json.is_err() { continue; } if msg_json.is_err() { continue; }
let u_json: Value = msg_json.unwrap(); let u_json: Value = msg_json.unwrap();
let re_url = &reddit_data[CFG_DATA_RE][&url]; let re_url = &reddit_data[POSTS_KEY][&url];
let json_trimmed = trim_post_json(re_url); let json_trimmed = trim_post_json(re_url);
@@ -257,31 +256,31 @@ async fn add_posts(ctx: Context<'_>, r_data: &Map<String, Value>, msgs_json: &Va
if now - post_date > max_age && max_age > 0 { continue; } if now - post_date > max_age && max_age > 0 { continue; }
if r_data[url]["removed"]["removed"].as_bool().unwrap() { if r_data[url]["removed"]["removed"].as_bool().unwrap() {
send_embed(ctx, make_removed_embed(&r_data[url], url, false), false).await; send_embed(ctx, make_removed_embed(&ctx.data().lang, &r_data[url], url, false), false).await;
continue; continue;
} }
send_embed(ctx, make_post_embed(&r_data[url], url, false), false).await; send_embed(ctx, make_post_embed(&ctx.data().lang, &r_data[url], url, false), false).await;
} }
} }
async fn edit_posts(http: &Http, c_id: ChannelId, r_data: &Map<String, Value>, msgs_json: &Value) { async fn edit_posts(http: &Http, lang: &Lang, c_id: ChannelId, r_data: &Map<String, Value>, msgs_json: &Value) {
for (url, msg_id) in msgs_json["updated"].as_object().unwrap() { 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 mut msg = http.get_message(c_id, MessageId::new(msg_id.as_u64().unwrap())).await.unwrap();
let r = EditMessage::new() let r = EditMessage::new()
.embeds(vec![embed_from_options(make_post_embed(&r_data[url], url, false))]); .embeds(vec![embed_from_options(make_post_embed(lang, &r_data[url], url, false))]);
let _ = msg.edit(http, r).await; let _ = msg.edit(http, r).await;
} }
} }
async fn remove_posts(http: &Http, c_id: ChannelId, r_data: &Map<String, Value>, msgs_json: &Value) { async fn remove_posts(http: &Http, lang: &Lang, c_id: ChannelId, r_data: &Map<String, Value>, msgs_json: &Value) {
for (url, msg_id) in msgs_json["removed"].as_object().unwrap() { 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 mut msg = http.get_message(c_id, MessageId::new(msg_id.as_u64().unwrap())).await.unwrap();
let r = EditMessage::new() let r = EditMessage::new()
.embeds(vec![embed_from_options(make_removed_embed(&r_data[url], url, false))]); .embeds(vec![embed_from_options(make_removed_embed(lang, &r_data[url], url, false))]);
let _ = msg.edit(http, r).await; let _ = msg.edit(http, r).await;
} }
+16 -20
View File
@@ -1,6 +1,6 @@
use serde_json::json; 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, to_shorturl}, websocket::send_cmd_json, Context, Error, CFG_DATA_RE}; use crate::{Context, Error, db::reddit::{self, POSTS_KEY}, messages::send_msg, cmds::reddit::generic_fns::{is_bk_mod, send_embed_for_removed, to_shorturl}, websocket::send_cmd_json};
#[poise::command( #[poise::command(
slash_command, slash_command,
@@ -16,17 +16,17 @@ pub async fn cmd(
#[description = "Wether to undo your vote or not"] un_vote: Option<bool> #[description = "Wether to undo your vote or not"] un_vote: Option<bool>
) -> Result<(), Error> ) -> Result<(), Error>
{ {
data::update_re_data(ctx.data()).await; reddit::update_data().await;
let uid = ctx.author().id.get(); let uid = ctx.author().id.get();
let re_data = get_mutex_data(&ctx.data().reddit_data).await?; let re_data = &ctx.data().reddit_data.lock().await;
let post_data = re_data[CFG_DATA_RE].clone(); let post_data = re_data[POSTS_KEY].clone();
let unw_vote = un_vote.unwrap_or(false); let unw_vote = un_vote.unwrap_or(false);
let shorturl_u = to_shorturl(&url); let shorturl_u = to_shorturl(&url);
let shorturl = &shorturl_u.unwrap_or(url.clone()); 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; send_msg(ctx, ctx.data().lang.get("dc.re.post_404", &[]), false, false).await;
return Ok(()); return Ok(());
} }
if post_data[&shorturl]["removed"]["removed"].as_bool().unwrap() { if post_data[&shorturl]["removed"]["removed"].as_bool().unwrap() {
@@ -36,35 +36,31 @@ pub async fn cmd(
let url_data = &post_data[&shorturl]; let url_data = &post_data[&shorturl];
let is_mod = is_bk_mod(ctx.data().bk_mods.clone(), ctx.author().id.get()); let is_mod = is_bk_mod(ctx.data().env_vars.reddit_mod_discord_ids.clone(), ctx.author().id.get());
let voters_dc = url_data["votes"]["voters_dc"].as_array().unwrap(); let voters_dc = url_data["votes"]["voters_dc"].as_array().unwrap();
let mod_voters = url_data["votes"]["mod_voters"].as_array().unwrap(); let mod_voters = url_data["votes"]["mod_voters"].as_array().unwrap();
let voters = if is_mod { mod_voters } else { voters_dc }; let voters = if is_mod { mod_voters } else { voters_dc };
if voters.contains(&json!(uid)) && !unw_vote { if voters.contains(&json!(uid)) && !unw_vote {
send_msg(ctx, lang!("dc_msg_re_already_voted"), true, true).await; send_msg(ctx, ctx.data().lang.get("dc.re.vote.already", &[]), true, true).await;
return Ok(()); return Ok(());
} }
else if !voters.contains(&json!(uid)) && unw_vote { else if !voters.contains(&json!(uid)) && unw_vote {
send_msg(ctx, lang!("dc_msg_re_vote_remove_havent"), true, true).await; send_msg(ctx, ctx.data().lang.get("dc.re.vote.remove_hasnt_voted", &[]), true, true).await;
return Ok(()); return Ok(());
} }
let r = send_cmd_json("set_vote_post", Some(json!([shorturl, uid, is_mod, true, unw_vote])), true).await.unwrap(); let r = send_cmd_json("set_vote_post", Some(json!([shorturl, uid, is_mod, true, unw_vote])), true).await.unwrap();
let unw_r = r["value"].as_bool().unwrap(); let unw_r = r["value"].as_bool().unwrap();
if unw_r && !unw_vote && is_mod { if unw_r && !unw_vote && is_mod
send_msg(ctx, lang!("dc_msg_re_vote_mod_success"), true, true).await; { send_msg(ctx, ctx.data().lang.get("dc.re.vote.mod", &[]), true, true).await; }
} else if unw_r && !unw_vote && !is_mod
else if unw_r && !unw_vote && !is_mod { { send_msg(ctx, ctx.data().lang.get("dc.re.vote.success", &[]), true, true).await; }
send_msg(ctx, lang!("dc_msg_re_vote_success"), true, true).await; else if unw_r && unw_vote
} { send_msg(ctx, ctx.data().lang.get("dc.re.vote.remove", &[]), true, true).await; }
else if unw_r && unw_vote { else
send_msg(ctx, lang!("dc_msg_re_vote_remove_success"), true, true).await; { send_msg(ctx, ctx.data().lang.get("dc.re.vote.error", &[]), true, true).await; }
}
else {
send_msg(ctx, lang!("dc_msg_re_vote_err"), true, true).await;
}
return Ok(()); return Ok(());
} }
-165
View File
@@ -1,165 +0,0 @@
use std::fmt::Display;
use poise::serenity_prelude::{ChannelId, Mentionable, User};
use tokio::sync::MutexGuard;
use crate::{Context, Error, data::get_mutex_data, lang, messages::{http_send_msg, send_msg}};
#[derive(poise::ChoiceParameter, PartialEq, Clone, Debug)]
#[repr(u8)]
pub enum RPS {
Paper = 0,
Rock = 1,
Scissors = 2
}
#[derive(Clone)]
pub struct RPSPlayer {
pub selection: RPS,
pub user: User,
pub wwrps_channel: ChannelId,
pub anonymous: bool
}
#[derive(Clone)]
pub struct RPSGame {
pub players: [Option<RPSPlayer>; 2],
}
impl Display for RPS {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
return match self {
Self::Paper => write!(f, "Paper"),
Self::Rock => write!(f, "Rock"),
Self::Scissors => write!(f, "Scissors")
};
}
}
impl RPSGame {
pub fn new() -> Self {
return RPSGame {
players: [None, None],
}
}
/// Returns wether the lobby is filled or not
pub fn add_player(&mut self, player: RPSPlayer) -> Result<bool, Error> {
if let Some(p1) = &self.players[0]
{ if p1.user == player.user { return Err(Error::from("Cannot add player, it already exists!")); }}
if self.players[0].is_none() { self.players[0] = Some(player); return Ok(false); }
else if self.players[1].is_none() { self.players[1] = Some(player); return Ok(true); }
else { return Err(Error::from("Cannot add player, list is full!")); }
}
pub fn clear(&mut self)
{ self.players = [None, None]; }
pub fn get_winner(&self) -> Option<i8> {
if self.players.iter().any(|i| i.is_none())
{ return None; }
let Some(p1) = self.players[0].clone() else { return None; };
let Some(p2) = self.players[1].clone() else { return None; };
let i1 = p1.selection as u8;
let i2 = p2.selection as u8;
if i1 == i2 { return None; }
else if (i1 + 1) % 3 == i2 { return Some(0); }
else { return Some(1); }
}
pub fn is_full_lobby(&self) -> bool
{ return self.players.iter().all(|i| i.is_some()); }
}
#[poise::command(
slash_command,
prefix_command,
category = "fun",
rename = "wwrps",
required_bot_permissions = "SEND_MESSAGES | VIEW_CHANNEL"
)]
/// World-Wide Rock Paper Scissors. Play with a completely random person!
pub async fn cmd(
ctx: Context<'_>,
selection: RPS,
#[description = "If true, replaces your username with [Anonymous]."] anonymous: bool
) -> Result<(), Error>
{
let c_o = get_wwrps_channel(ctx).await;
if c_o.is_none() { send_msg(ctx, lang!("dc_msg_wwrps_not_in_data"), true, true).await; return Ok(()); }
let c = c_o.unwrap();
send_msg(ctx, lang!("dc_msg_wwrps_submitting"), true, true).await;
let mut game = ctx.data().rps_game.lock().await;
if !game.is_full_lobby() {
let r = game.add_player(
RPSPlayer { selection, user: ctx.author().clone(), wwrps_channel: ChannelId::new(c), anonymous });
if let Err(_) = r
{ send_msg(ctx, lang!("dc_msg_wwrps_already_submitted"), true, true).await; return Ok(()); }
let full = r.unwrap();
if !full { return Ok(()); }
let r_text = results_text(&game);
let game_clone = game.clone();
game.clear();
let mut used_channels: Vec<ChannelId> = Vec::new();
for player in &game_clone.players {
if let Some(p) = player {
if used_channels.contains(&p.wwrps_channel) { continue; }
used_channels.push(p.wwrps_channel);
http_send_msg(ctx.http(), p.wwrps_channel, r_text.clone()).await;
}
}
}
return Ok(());
}
fn results_text(game: &MutexGuard<'_, RPSGame>) -> String {
let winner = game.get_winner();
let winner_text: String;
let p1 = game.players[0].as_ref().unwrap();
let p2 = game.players[1].as_ref().unwrap();
let p1_n = if !p1.anonymous { p1.user.mention().to_string() } else { lang!("dc_msg_wwrps_anon") };
let p2_n = if !p2.anonymous { p2.user.mention().to_string() } else { lang!("dc_msg_wwrps_anon") };
if let Some(w) = winner {
winner_text = if w == 0 { lang!("dc_msg_wwrps_left_win") }
else { lang!("dc_msg_wwrps_right_win") }; }
else { winner_text = lang!("dc_msg_wwrps_draw"); }
return lang!("dc_msg_wwrps_fight", p1.selection.clone(), p2.selection.clone(), winner_text, p1_n, p2_n);
}
async fn get_wwrps_channel(ctx: Context<'_>) -> Option<u64> {
let d = get_mutex_data(&ctx.data().discord_data).await.unwrap();
let is_guild = ctx.guild_channel().await.is_some();
if !is_guild { return Some(ctx.channel_id().get()); }
let Some(servers) = d.get("servers") else { return None; };
let Some(s) = servers.get(ctx.guild_id().unwrap().get().to_string()) else { return None; };
let Some(c_id) = s.get("wwrps_channel") else { return None; };
return Some(c_id.as_u64().unwrap());
}
-252
View File
@@ -1,252 +0,0 @@
use std::{fs, io::Write};
use std::path::Path;
use serde_json::{self, Value, json};
use tokio::sync::Mutex;
use crate::{errln, rs_println, rs_warnln, Data, Error, CFG_DATA_RE, LANG, LANG_NAME};
use crate::websocket::send_cmd_json;
static DATA_PATH_DC: &str = "./data/db/dc_data.json";
static PRESET_PATH_DC: &str = "./data/defaults/dc_data_preset.json";
static DATA_PATH_RE: &str = "./data/db/re_data.json";
static PRESET_PATH_RE: &str = "./data/defaults/re_data_preset.json";
static DATA_PATH_CFG: &str = "./cfg/cfg.toml";
static PRESET_PATH_CFG: &str = "./data/defaults/cfg_default.toml";
static DATA_PATH_LANG: &str = "./data/lang/";
pub static DC_POSTS_CHANNEL_KEY: &str = "re_posts_channel";
pub static DC_WWRPS_CHANNEL_KEY: &str = "wwrps_channel";
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)" }
);
generate_dc_data();
}
let str_data = fs::read_to_string(DATA_PATH_DC).unwrap();
let json_data = serde_json::from_str(&str_data).unwrap();
let mut dc_data = data.discord_data.lock().await;
*dc_data = json_data;
}
fn generate_dc_data() {
let preset_str = fs::read_to_string(PRESET_PATH_DC).unwrap();
let mut preset_json: Value = serde_json::from_str(&preset_str).unwrap();
if let Some(servers) = preset_json["servers"].as_object_mut() {
servers.remove("SERVER ID");
}
let json_str = serde_json::to_string_pretty(&preset_json).unwrap();
let mut file = fs::File::create(DATA_PATH_DC).unwrap();
file.write_all(json_str.as_bytes()).unwrap();
}
pub async fn write_dc_data(data: &Data) {
if !Path::new(DATA_PATH_DC).exists() {
generate_dc_data();
}
let mut file = fs::OpenOptions::new()
.write(true)
.truncate(true)
.open(DATA_PATH_DC)
.unwrap();
let mut dc_data_lock = data.discord_data.lock().await;
let dc_data = dc_data_lock.as_mut().unwrap();
let json_str = serde_json::to_string_pretty(dc_data).unwrap();
file.write_all(json_str.as_bytes()).unwrap();
}
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)" }
);
generate_re_data();
}
let str_data = fs::read_to_string(DATA_PATH_RE).unwrap();
let json_data: Option<Value> = serde_json::from_str(&str_data).unwrap();
let mut re_data = data.reddit_data.lock().await;
*re_data = json_data;
}
fn generate_re_data() {
let preset_str = fs::read_to_string(PRESET_PATH_RE).unwrap();
let mut preset_json: Value = serde_json::from_str(&preset_str).unwrap();
if let Some(bk_week) = preset_json[CFG_DATA_RE].as_object_mut() {
bk_week.remove("EXAMPLE URL");
}
else {
rs_warnln!("Couldn't find key \"{}\" in the Reddit data file ({})!", CFG_DATA_RE, DATA_PATH_RE);
}
let json_str = serde_json::to_string_pretty(&preset_json).unwrap();
let mut file = fs::File::create(DATA_PATH_RE).unwrap();
file.write_all(json_str.as_bytes()).unwrap();
}
pub async fn update_re_data(data: &Data) {
send_cmd_json("update_data_file", None, true).await;
read_re_data(data, false).await;
}
pub async fn write_re_data() {
send_cmd_json("update_data_file", None, true).await;
}
pub async fn read_cfg_data(data: &Data, wipe: bool) -> Option<Value> {
if !Path::new(DATA_PATH_CFG).exists() || wipe {
rs_println!(
"{} creating new from preset...",
if !wipe { "cfg.toml not found," } else { "[WIPE] (cfg.toml)" }
);
generate_cfg_data();
}
let str_data = fs::read_to_string(DATA_PATH_CFG).unwrap();
let json_data: toml::Value = str_data.parse().unwrap();
let mut cfg_data = data.cfg.lock().await;
*cfg_data = Some(json_data.clone());
let r = send_cmd_json("update_cfg", Some(json!([toml::to_string(&json_data).unwrap()])), true).await;
return r;
}
fn generate_cfg_data() {
let preset_str = fs::read_to_string(PRESET_PATH_CFG).unwrap();
let mut file = fs::File::create(DATA_PATH_CFG).unwrap();
file.write_all(preset_str.as_bytes()).unwrap();
}
pub async fn dc_add_server(data: &Data, server_id: u64) -> Result<(), ()> {
let mut dc_data_lock = data.discord_data.lock().await;
let dc_data = dc_data_lock.as_mut().unwrap();
if dc_data.get("servers").is_none() { return Err(()); }
let servers = dc_data["servers"].as_object_mut().unwrap();
if !servers.contains_key(&server_id.to_string()) {
servers.insert(server_id.to_string(), json!({}));
}
return Ok(());
}
pub async fn dc_bind_bk(data: &Data, server_id: u64, channel_id: u64) -> Result<(), ()> {
let mut dc_data_lock = data.discord_data.lock().await;
let dc_data = dc_data_lock.as_mut().unwrap();
if dc_data.get("servers").is_none() { return Err(()); }
let servers = dc_data["servers"].as_object_mut().unwrap();
if !servers.contains_key(&server_id.to_string()) {
return Err(());
}
let server = servers[&server_id.to_string()].as_object_mut().unwrap();
server.insert(DC_POSTS_CHANNEL_KEY.to_string(), channel_id.into());
return Ok(());
}
pub async fn bind_wwrps(data: &Data, server_id: u64, channel_id: u64) -> Result<(), ()> {
let mut dc_data_lock = data.discord_data.lock().await;
let dc_data = dc_data_lock.as_mut().unwrap();
if dc_data.get("servers").is_none() { return Err(()); }
let servers = dc_data["servers"].as_object_mut().unwrap();
if !servers.contains_key(&server_id.to_string())
{ return Err(()); }
let server = servers[&server_id.to_string()].as_object_mut().unwrap();
server.insert(DC_WWRPS_CHANNEL_KEY.to_string(), channel_id.into());
return Ok(());
}
pub async fn dc_contains_server(data: &Data, server_id: u64) -> bool {
let dc_data_lock = data.discord_data.lock().await;
let dc_data = dc_data_lock.as_ref().unwrap();
if dc_data.get("servers").is_none() { return false; }
let mut clone = dc_data.clone();
let servers = clone["servers"].as_object_mut().unwrap();
return servers.contains_key(&server_id.to_string())
}
pub async fn get_mutex_data(data: &Mutex<Option<Value>>) -> Result<Value, Error> {
let data_lock = data.lock().await;
return match data_lock.as_ref() {
Some(data) => Ok(data.clone()),
None => Err("Cannot get mutex data: The data is corrupted!".into()),
};
}
pub async fn get_toml_mutex(data: &Mutex<Option<toml::Value>>) -> Result<toml::Value, Error> {
let data_lock = data.lock().await;
return match data_lock.as_ref() {
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() {
errln!(
"File for language \"{0}\" ({0}.json) not found!\n Hint: You can download official language files at https://github.com/ByteDice/ByteDiceAssistant in the data/langs/... folder",
lang
);
}
let str_data = fs::read_to_string(full_path).unwrap();
let json_data: Option<Value> = serde_json::from_str(&str_data).unwrap();
unsafe {
LANG = json_data;
LANG_NAME = Some(lang);
};
}
+7
View File
@@ -0,0 +1,7 @@
pub mod bot_data;
pub mod cfg;
pub mod discord;
pub mod env_vars;
pub mod keys;
pub mod reddit;
pub mod terminal_args;
+17
View File
@@ -0,0 +1,17 @@
use serde_json::Value;
use tokio::sync::Mutex;
use crate::{db::{env_vars::AssistantEnv, terminal_args::Args}, games::wwrps::game::RPSGame, lang::Lang};
pub struct Data {
pub args: Args,
pub ball_prompts: [Vec<String>; 2],
pub cfg: toml::Value,
pub discord_data: Mutex<Value>,
pub env_vars: AssistantEnv,
pub lang_name: String,
pub lang: Lang,
pub reddit_data: Mutex<Value>,
pub rps_game: Mutex<RPSGame>,
}
+37
View File
@@ -0,0 +1,37 @@
use std::{fs, io::Write, path::Path};
use crate::rs_println;
static DATA_PATH: &str = "./cfg/cfg.toml";
static PRESET_PATH: &str = "./data/defaults/cfg_default.toml";
pub async fn read_data(wipe: bool) -> toml::Value {
if !Path::new(DATA_PATH).exists() || wipe {
rs_println!(
"{} creating new from preset...",
if !wipe { "cfg.toml not found," } else { "[WIPE] (cfg.toml)" }
);
generate_data();
}
let str_data = fs::read_to_string(DATA_PATH).unwrap();
let json_data: toml::Value = str_data.parse().unwrap();
/*let r = send_cmd_json(
"update_cfg",
Some(json!([toml::to_string(&json_data).unwrap()])),
true
).await;*/
return json_data;
}
fn generate_data() {
let preset_str = fs::read_to_string(PRESET_PATH).unwrap();
let mut file = fs::File::create(DATA_PATH).unwrap();
file.write_all(preset_str.as_bytes()).unwrap();
}
+125
View File
@@ -0,0 +1,125 @@
use std::{fs, io::Write, path::Path};
use serde_json::{Value, json};
use crate::{Data, db::keys, rs_println};
static DATA_PATH: &str = "./data/db/dc_data.json";
static PRESET_PATH: &str = "./data/defaults/dc_data_preset.json";
pub async fn read_data(wipe: bool) -> Value {
if !Path::new(DATA_PATH).exists() || wipe {
rs_println!(
"{} creating new from preset...",
if !wipe { "discord_data.json not found," } else { "[WIPE] (discord_data.json)" }
);
generate_data();
}
let str_data = fs::read_to_string(DATA_PATH).unwrap();
let json_data = serde_json::from_str(&str_data).unwrap();
return json_data;
}
fn generate_data() {
let preset_str = fs::read_to_string(PRESET_PATH).unwrap();
let mut preset_json: Value = serde_json::from_str(&preset_str).unwrap();
// remove the examples
if let Some(servers) = preset_json["servers"].as_object_mut() {
servers.remove("SERVER_ID");
}
if let Some(users) = preset_json["users"].as_object_mut() {
users.remove("USER_ID");
}
let json_str = serde_json::to_string_pretty(&preset_json).unwrap();
let mut file = fs::File::create(DATA_PATH).unwrap();
file.write_all(json_str.as_bytes()).unwrap();
}
pub async fn write_data(data: &Data) {
if !Path::new(DATA_PATH).exists() {
generate_data();
}
let mut file = fs::OpenOptions::new()
.write(true)
.truncate(true)
.open(DATA_PATH)
.unwrap();
let dc_data = data.discord_data.lock().await;
let json_str = serde_json::to_string_pretty(&dc_data.clone()).unwrap();
file.write_all(json_str.as_bytes()).unwrap();
}
pub async fn add_server(data: &Data, server_id: u64) -> Result<(), ()> {
let mut dc_data = data.discord_data.lock().await;
if dc_data.get("servers").is_none() { return Err(()); }
let servers = dc_data["servers"].as_object_mut().unwrap();
if !servers.contains_key(&server_id.to_string()) {
servers.insert(server_id.to_string(), json!({}));
}
return Ok(());
}
pub async fn bind_bk(data: &Data, server_id: u64, channel_id: u64) -> Result<(), ()> {
let mut dc_data = data.discord_data.lock().await;
if dc_data.get("servers").is_none() { return Err(()); }
let servers = dc_data["servers"].as_object_mut().unwrap();
if !servers.contains_key(&server_id.to_string()) {
return Err(());
}
let server = servers[&server_id.to_string()].as_object_mut().unwrap();
server.insert(keys::DC_POSTS_CHANNEL_KEY.to_string(), channel_id.into());
return Ok(());
}
pub async fn bind_wwrps(data: &Data, server_id: u64, channel_id: u64) -> Result<(), ()> {
let mut dc_data = data.discord_data.lock().await;
if dc_data.get("servers").is_none() { return Err(()); }
let servers = dc_data["servers"].as_object_mut().unwrap();
if !servers.contains_key(&server_id.to_string())
{ return Err(()); }
let server = servers[&server_id.to_string()].as_object_mut().unwrap();
server.insert(keys::DC_WWRPS_CHANNEL_KEY.to_string(), channel_id.into());
return Ok(());
}
pub async fn contains_server(data: &Data, server_id: u64) -> bool {
let dc_data = data.discord_data.lock().await;
if dc_data.get("servers").is_none() { return false; }
let mut clone = dc_data.clone();
let servers = clone["servers"].as_object_mut().unwrap();
return servers.contains_key(&server_id.to_string())
}
+38
View File
@@ -0,0 +1,38 @@
#[derive(Clone)]
pub struct AssistantEnv {
pub token: String,
pub bot_owners: Vec<u64>,
pub reddit_mod_discord_ids: Vec<u64>
}
impl AssistantEnv {
pub fn new(test: bool) -> Self {
let token_name = if !test { "ASSISTANT_TOKEN" }
else { "ASSISTANT_TOKEN_TEST" };
return AssistantEnv {
token: string_env(token_name),
bot_owners: vec_u64_env("ASSISTANT_OWNERS"),
reddit_mod_discord_ids: vec_u64_env("ASSISTANT_BK_MODS")
};
}
}
fn string_env(name: &str) -> String {
return std::env::var(name)
.expect(format!("Environment variable \"{}\" not found!", name).as_str())
}
fn vec_u64_env(name: &str) -> Vec<u64> {
let var = std::env::var(name)
.unwrap_or("0".to_string());
return var
.split(",")
.map(|s| s.parse::<u64>()
.expect(format!("Failed to parse {}. Invalid syntax.", name).as_str()))
.collect();
}
+2
View File
@@ -0,0 +1,2 @@
pub static DC_POSTS_CHANNEL_KEY: &str = "re_posts_channel";
pub static DC_WWRPS_CHANNEL_KEY: &str = "wwrps_channel";
+55
View File
@@ -0,0 +1,55 @@
use std::{fs, io::Write, path::Path};
use serde_json::Value;
use crate::{rs_println, rs_warnln, websocket::send_cmd_json};
static DATA_PATH: &str = "./data/db/re_data.json";
static PRESET_PATH: &str = "./data/defaults/re_data_preset.json";
pub static POSTS_KEY: &str = "posts";
pub async fn read_data(wipe: bool) -> Value {
if !Path::new(DATA_PATH).exists() || wipe {
rs_println!(
"{} creating new from preset...",
if !wipe { "reddit_data.json not found," } else { "[WIPE] (reddit_data.json)" }
);
generate_data();
}
let str_data = fs::read_to_string(DATA_PATH).unwrap();
let json_data: Value = serde_json::from_str(&str_data).unwrap();
return json_data;
}
fn generate_data() {
let preset_str = fs::read_to_string(PRESET_PATH).unwrap();
let mut preset_json: Value = serde_json::from_str(&preset_str).unwrap();
if let Some(bk_week) = preset_json[POSTS_KEY].as_object_mut() {
bk_week.remove("EXAMPLE URL");
}
else {
rs_warnln!("Couldn't find key \"{}\" in the Reddit data file ({})!", POSTS_KEY, DATA_PATH);
}
let json_str = serde_json::to_string_pretty(&preset_json).unwrap();
let mut file = fs::File::create(DATA_PATH).unwrap();
file.write_all(json_str.as_bytes()).unwrap();
}
pub async fn update_data() {
send_cmd_json("update_data_file", None, true).await;
read_data(false).await;
}
pub async fn write_data() {
send_cmd_json("update_data_file", None, true).await;
}
+44
View File
@@ -0,0 +1,44 @@
use clap::Parser;
use serde::Serialize;
#[derive(Parser, Serialize, Clone)]
pub struct Args {
#[arg(short = 'p', long, default_value = "2920", help = "Sets the port number, e.g 2200.")]
pub port: u16,
#[arg(long, help = "Runs only the Python part of the program.")]
pub py: bool,
#[arg(long, help = "Runs only the Rust part of the program.")]
pub rs: bool,
#[arg(short = 'd', long, help = "Enables dev mode. Dev mode shows more debug info and turns off certain security measures.")]
pub dev: bool,
#[arg(short = 'w', long, help = "Wipes all data before running the program.")]
pub 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.")]
pub test: bool,
#[arg(long, help = "Adds annoying prints when the websockets send a ping. Why though?")]
pub ping: bool,
#[arg(long, help = "Makes the program not use the schedule system.")]
pub nosched: bool
}
impl Args {
pub fn new() -> Self
{ return <Args as clap::Parser>::parse(); }
}
impl ToString for Args {
fn to_string(&self) -> String {
return serde_json::to_string(self)
.unwrap_or("[FAILED TO CONVERT TO STRING]".to_string());
}
}
-18
View File
@@ -1,18 +0,0 @@
use crate::{data::dc_add_server, lang, messages::send_msg, Context, Error};
pub async fn cmd(
ctx: Context<'_>
) -> Result<(), Error>
{
let r = dc_add_server(ctx.data(), ctx.guild_id().unwrap().into()).await;
if r.is_ok() {
send_msg(ctx, lang!("dc_msg_add_to_data"), true, true).await;
}
else {
send_msg(ctx, lang!("dc_msg_corrupted_data"), true, true).await;
}
return Ok(());
}
-18
View File
@@ -1,18 +0,0 @@
use crate::{data::dc_bind_bk, lang, messages::send_msg, Context, Error};
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!("dc_msg_bound_channel", c_id), true, true).await;
}
else {
send_msg(ctx, lang!("dc_msg_data_server_404"), true, true).await;
}
return Ok(());
}
-18
View File
@@ -1,18 +0,0 @@
use crate::{Context, Error, data::bind_wwrps, lang, messages::send_msg};
pub async fn cmd(
ctx: Context<'_>
) -> Result<(), Error>
{
let c_id = ctx.channel_id().into();
let r = bind_wwrps(ctx.data(), ctx.guild_id().unwrap().into(), c_id).await;
if r.is_ok() {
send_msg(ctx, lang!("dc_msg_bound_channel", c_id), true, true).await;
}
else {
send_msg(ctx, lang!("dc_msg_data_server_404"), true, true).await;
}
return Ok(());
}
-12
View File
@@ -1,12 +0,0 @@
use crate::{Context, Error, data, lang, messages::{edit_reply, send_msg}};
pub async fn cmd(ctx: Context<'_>) -> Result<(), Error> {
let msg = send_msg(ctx, lang!("dc_msg_owner_data_save"), true, true).await.unwrap();
data::write_dc_data(ctx.data()).await;
data::write_re_data().await;
edit_reply(ctx, msg, lang!("dc_msg_owner_data_save_complete")).await;
return Ok(());
}
-13
View File
@@ -1,13 +0,0 @@
use crate::{lang, messages::send_msg, Context, Error};
pub async fn cmd(ctx: Context<'_>) -> Result<(), Error> {
let data = ctx.data();
let uid: u64 = ctx.author().id.into();
let is_owner = data.owners .contains(&uid);
let is_bk_mod = data.bk_mods.contains(&uid);
send_msg(ctx, lang!("dc_msg_whoami", is_owner, is_bk_mod), true, true).await;
return Ok(());
}
+43 -45
View File
@@ -1,9 +1,11 @@
use crate::data::{get_mutex_data, get_toml_mutex, update_re_data}; use crate::db::reddit::update_data;
use crate::r#gen::set_status; use crate::r#gen::set_status;
use crate::lang::Lang;
use crate::messages::{make_post_embed, make_removed_embed, EmbedOptions}; use crate::messages::{make_post_embed, make_removed_embed, EmbedOptions};
use crate::re_cmds::generic_fns::{is_bk_mod, is_bk_mod_serenity, serenity_edit_msg_embed, serenity_send_msg}; use crate::cmds::reddit::generic_fns::{is_bk_mod, is_bk_mod_serenity, serenity_edit_msg_embed, serenity_send_msg};
use crate::websocket::send_cmd_json; use crate::websocket::send_cmd_json;
use crate::{CFG_DATA_RE, Data, Error, lang, rs_println}; use crate::{Data, Error, rs_println};
use crate::db::reddit::POSTS_KEY;
use poise::serenity_prelude::{self as serenity, ChannelId, ComponentInteraction, Interaction, Member, MessageId, Ready}; use poise::serenity_prelude::{self as serenity, ChannelId, ComponentInteraction, Interaction, Member, MessageId, Ready};
use serde_json::{json, Value}; use serde_json::{json, Value};
@@ -11,6 +13,10 @@ use serde_json::{json, Value};
use std::future::Future; use std::future::Future;
use std::pin::Pin; use std::pin::Pin;
// TODO: clean up
pub fn event_handler<'a>( pub fn event_handler<'a>(
ctx: &'a serenity::Context, ctx: &'a serenity::Context,
event: &'a serenity::FullEvent, event: &'a serenity::FullEvent,
@@ -35,9 +41,7 @@ async fn on_ready(ctx: &serenity::Context, data_about_bot: &Ready, data: &Data)
data_about_bot.user.id data_about_bot.user.id
); );
let m_data = get_toml_mutex(&data.cfg).await.unwrap(); set_status(data.cfg.clone(), ctx).await;
set_status(m_data, ctx).await;
} }
@@ -64,11 +68,11 @@ async fn handle_buttons(ctx: &serenity::Context, data: &Data, interaction: &Inte
} }
async fn update_embed(ctx: &serenity::Context, url: &str, new_data: &Value, c_id: &ChannelId, m_id: &MessageId) { async fn update_embed(ctx: &serenity::Context, lang: &Lang, url: &str, new_data: &Value, c_id: &ChannelId, m_id: &MessageId) {
let remove = new_data["removed"]["removed"].as_bool().unwrap(); let remove = new_data["removed"]["removed"].as_bool().unwrap();
let e: EmbedOptions = let e: EmbedOptions =
if remove { make_removed_embed(new_data, url, true) } if remove { make_removed_embed(lang, new_data, url, true) }
else { make_post_embed (new_data, url, true) }; else { make_post_embed (lang, 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;
} }
@@ -82,21 +86,18 @@ async fn approve_btn(ctx: &serenity::Context, data: &Data, c_member: &Member, co
let c_id = component.channel_id; let c_id = component.channel_id;
let m_id = component.message.id; let m_id = component.message.id;
update_re_data(data).await; update_data().await;
let new_data = &get_mutex_data(&data.reddit_data).await.unwrap()[CFG_DATA_RE][&url]; let new_data = &data.reddit_data
update_embed(ctx, &url, new_data, &c_id, &m_id).await; .lock().await[POSTS_KEY][&url];
update_embed(ctx, &data.lang, &url, new_data, &c_id, &m_id).await;
if r["value"].as_bool().unwrap() { if r["value"].as_bool().unwrap() {
if approve { if approve
serenity_send_msg(ctx, component, lang!("dc_msg_re_post_approve_success"), true).await; { serenity_send_msg(ctx, component, data.lang.get("dc.re.approve.disapprove", &[]), true).await; }
} else { serenity_send_msg(ctx, component, data.lang.get("dc.re.approve.success", &[]), true).await; }
else {
serenity_send_msg(ctx, component, lang!("dc_msg_re_post_disapprove_success"), true).await;
}
}
else {
serenity_send_msg(ctx, component, lang!("dc_msg_re_post_approve_remove"), true).await;
} }
else { serenity_send_msg(ctx, component, data.lang.get("dc.re.approve.post_removed", &[]), true).await; }
return Ok(()); return Ok(());
} }
@@ -105,27 +106,22 @@ 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> { 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(()); } if !is_bk_mod_serenity(ctx, data, c_member, component).await { return Ok(()); }
let r: Value = if remove { let r: Value =
send_cmd_json("remove_post_url", Some(json!([&url, &c_member.user.name, None::<String>])), true).await.unwrap() if remove { send_cmd_json("remove_post_url", Some(json!([&url, &c_member.user.name, None::<String>])), true).await.unwrap() }
} else { send_cmd_json("add_post_url", Some(json!([&url, false, true])), true).await.unwrap() };
else {
send_cmd_json("add_post_url", Some(json!([&url, false, true])), true).await.unwrap()
};
let c_id = component.channel_id; let c_id = component.channel_id;
let m_id = component.message.id; let m_id = component.message.id;
update_re_data(data).await; update_data().await;
let new_data = &get_mutex_data(&data.reddit_data).await.unwrap()[CFG_DATA_RE][&url]; let new_data = &data.reddit_data
update_embed(ctx, &url, new_data, &c_id, &m_id).await; .lock().await[POSTS_KEY][&url];
update_embed(ctx, &data.lang, &url, new_data, &c_id, &m_id).await;
if r["value"].as_bool().unwrap() { if r["value"].as_bool().unwrap() {
if remove { if remove { serenity_send_msg(ctx, component, data.lang.get("dc.re.remove.success", &[url]), true).await; }
serenity_send_msg(ctx, component, lang!("dc_msg_re_post_remove_success", &url), true).await; else { serenity_send_msg(ctx, component, data.lang.get("dc.re.remove.unremove", &[url]), true).await; }
}
else {
serenity_send_msg(ctx, component, lang!("dc_msg_re_post_unremove_success", &url), true).await;
}
} }
return Ok(()); return Ok(());
@@ -134,31 +130,33 @@ async fn remove_btn(ctx: &serenity::Context, data: &Data, c_member: &Member, com
async fn vote_btn(ctx: &serenity::Context, data: &Data, c_member: &Member, component: &ComponentInteraction, url: String, vote: bool) -> Result<(), Error> { async fn vote_btn(ctx: &serenity::Context, data: &Data, c_member: &Member, component: &ComponentInteraction, url: String, vote: bool) -> Result<(), Error> {
let uid: u64 = c_member.user.id.into(); let uid: u64 = c_member.user.id.into();
let is_mod = is_bk_mod(data.bk_mods.clone(), uid); let is_mod = is_bk_mod(data.env_vars.reddit_mod_discord_ids.clone(), uid);
let r = send_cmd_json("set_vote_post", Some(json!([&url, uid, is_mod, true, !vote])), true).await.unwrap(); let r = send_cmd_json("set_vote_post", Some(json!([&url, uid, is_mod, true, !vote])), true).await.unwrap();
let c_id = component.channel_id; let c_id = component.channel_id;
let m_id = component.message.id; let m_id = component.message.id;
update_re_data(data).await; update_data().await;
let new_data = &get_mutex_data(&data.reddit_data).await.unwrap()[CFG_DATA_RE][&url]; let new_data = &data.reddit_data
update_embed(ctx, &url, new_data, &c_id, &m_id).await; .lock().await[POSTS_KEY][&url];
update_embed(ctx, &data.lang, &url, new_data, &c_id, &m_id).await;
if r["value"].as_bool().unwrap() { if r["value"].as_bool().unwrap() {
if vote { if vote {
if is_mod { serenity_send_msg(ctx, component, lang!("dc_msg_re_vote_mod_success"), true).await; } if is_mod { serenity_send_msg(ctx, component, data.lang.get("dc.re.vote.mod", &[]), true).await; }
else { serenity_send_msg(ctx, component, lang!("dc_msg_re_vote_success"), true).await; } else { serenity_send_msg(ctx, component, data.lang.get("dc.re.vote.success", &[]), true).await; }
} }
else { else {
serenity_send_msg(ctx, component, lang!("dc_msg_re_vote_remove_success"), true).await; serenity_send_msg(ctx, component, data.lang.get("dc.re.vote.removed", &[]), 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; serenity_send_msg(ctx, component, data.lang.get("dc.re.vote.post_removed", &[]), true).await;
} }
else if !vote { else if !vote {
serenity_send_msg(ctx, component, lang!("dc_msg_re_vote_remove_havent"), true).await; serenity_send_msg(ctx, component, data.lang.get("dc.re.vote.remove_hasnt_voted", &[]), true).await;
} }
return Ok(()); return Ok(());
+1
View File
@@ -0,0 +1 @@
pub mod wwrps;
+2
View File
@@ -0,0 +1,2 @@
pub mod game;
pub mod ranks;
+82
View File
@@ -0,0 +1,82 @@
use std::fmt::Display;
use poise::serenity_prelude::{ChannelId, User};
use crate::{Error, games::wwrps::ranks::RPSStats};
#[derive(poise::ChoiceParameter, PartialEq, Clone, Debug)]
#[repr(u8)]
pub enum RPS {
Paper = 0,
Rock = 1,
Scissors = 2
}
#[derive(Clone)]
pub struct RPSPlayer {
pub selection: RPS,
pub user: User,
pub wwrps_channel: ChannelId,
pub anonymous: bool,
pub stats: RPSStats
}
#[derive(Clone)]
pub struct RPSGame {
pub players: [Option<RPSPlayer>; 2],
}
impl Display for RPS {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
return match self {
Self::Paper => write!(f, "Paper"),
Self::Rock => write!(f, "Rock"),
Self::Scissors => write!(f, "Scissors")
};
}
}
impl RPSGame {
pub fn new() -> Self {
return RPSGame {
players: [None, None],
}
}
/// Returns wether the lobby is filled or not
pub fn add_player(&mut self, player: RPSPlayer, allow_dupes: bool) -> Result<bool, Error> {
if let Some(p1) = &self.players[0] {
if p1.user == player.user && !allow_dupes
{ return Err(Error::from("Cannot add player, it already exists!")); }
}
if self.players[0].is_none() { self.players[0] = Some(player); return Ok(false); }
else if self.players[1].is_none() { self.players[1] = Some(player); return Ok(true); }
else { return Err(Error::from("Cannot add player, list is full!")); }
}
pub fn clear(&mut self)
{ self.players = [None, None]; }
pub fn get_winner(&self) -> Option<i8> {
if self.players.iter().any(|i| i.is_none())
{ return None; }
let Some(p1) = self.players[0].clone() else { return None; };
let Some(p2) = self.players[1].clone() else { return None; };
let i1 = p1.selection as u8;
let i2 = p2.selection as u8;
if i1 == i2 { return None; }
else if (i1 + 1) % 3 == i2 { return Some(0); } // calculate if p1 wins
else { return Some(1); } // otherwise p2 wins
}
pub fn is_full_lobby(&self) -> bool
{ return self.players.iter().all(|i| i.is_some()); }
}
+91
View File
@@ -0,0 +1,91 @@
pub enum Ranks {
PlasticScissors,
PrinterPaper,
Pebble,
ArkOfTheElements,
Origami,
Obsidian,
Dwayne,
LiterallyBrokenTheSystem
}
#[derive(Clone)]
pub struct RPSStats {
pub uid: u64,
pub elo: u16
// Win/loss history?
}
static SENSITIVITY: u16 = 32;
pub static START_ELO: u16 = 500;
impl RPSStats {
pub fn new(uid: u64) -> RPSStats
{ return RPSStats { uid: uid, elo: START_ELO } }
pub fn from(uid: u64, elo: u16) -> RPSStats
{ return RPSStats { uid: uid, elo: elo } }
pub fn update_elo(&mut self, expected: f32, score: f32)
{ self.elo += SENSITIVITY * (score - expected) as u16; }
pub fn get_elo_expected(elo_a: u16, elo_b: u16) -> f32
{ return 1.0 / (1.0 + f32::powf(10.0, (elo_b - elo_a) as f32 / 400.0)) }
// TODO: figure out a way to add RAM-efficient win/loss history before doing this
/*pub fn to_b64(&self) -> String {
return String::new();
}
pub fn from_b64(b64: String, uid: u64) -> RPSStats {
return Self::new();
}*/
}
impl Ranks {
pub fn from_elo(elo: u16) -> Ranks {
// formula: start + x * inc + step * (x * (x - 1) / 2)
// start = 100
// inc = 40
// step = 40
return match elo {
n if n > 1500 => Ranks::Dwayne,
n if n > 1180 => Ranks::Obsidian,
n if n > 900 => Ranks::Origami,
n if n > 660 => Ranks::ArkOfTheElements,
n if n > 460 => Ranks::Pebble,
n if n > 300 => Ranks::PrinterPaper,
n if n <= 180 => Ranks::PlasticScissors,
_ => Ranks::LiterallyBrokenTheSystem
}
}
}
impl ToString for Ranks {
fn to_string(&self) -> String {
return match self {
Ranks::PlasticScissors => String::from("Plastic Scissors"),
Ranks::PrinterPaper => String::from("Printer Paper"),
Ranks::Pebble => String::from("Pebble"),
Ranks::ArkOfTheElements => String::from("Ark of the Elements"),
Ranks::Origami => String::from("Origami"),
Ranks::Obsidian => String::from("Obsidian"),
Ranks::Dwayne => String::from("Dwayne"),
Ranks::LiterallyBrokenTheSystem => String::from("YOU HAVE BROKEN THE GAME SOMEHOW"),
}
}
}
+49 -50
View File
@@ -1,3 +1,4 @@
use std::path::PathBuf;
use std::{collections::HashSet, process}; use std::{collections::HashSet, process};
use poise::serenity_prelude::{ActivityData, UserId}; use poise::serenity_prelude::{ActivityData, UserId};
@@ -6,47 +7,46 @@ use poise::serenity_prelude::Client;
use tokio::sync::Mutex; use tokio::sync::Mutex;
use toml::Value; use toml::Value;
use crate::cmds::wwrps::RPSGame; use crate::db::env_vars::AssistantEnv;
use crate::data::get_toml_mutex; use crate::db::{cfg, discord, reddit};
use crate::{Args, Cmd, Data, cmds, data, db_cmds, debug_cmds, events, re_cmds, rs_println}; use crate::games::wwrps::game::RPSGame;
use crate::lang::Lang;
use crate::{Args, Cmd, Data, cmds, events, rs_println};
pub async fn gen_data(args: Args, owners: Vec<u64>) -> Data { pub async fn gen_data(args: Args, env_vars: AssistantEnv) -> Data {
let ball_classic_str = std::fs::read_to_string("./cfg/8-ball_classic.txt").unwrap(); let ball_classic_str = std::fs::read_to_string("./cfg/8-ball_classic.txt").unwrap();
let ball_quirk_str = std::fs::read_to_string("./cfg/8-ball_quirky.txt").unwrap(); let ball_quirk_str = std::fs::read_to_string("./cfg/8-ball_quirky.txt").unwrap();
let ball_classic: Vec<String> = ball_classic_str.lines().map(String::from).collect(); 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 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 re_data = reddit:: read_data(args.clone().wipe).await;
let mods_vec_str: Vec<String> = mods_env.split(",").map(String::from).collect(); let dc_data = discord::read_data(args.clone().wipe).await;
let mods_vec_u64: Vec<u64> = mods_vec_str let cf_data = cfg:: read_data(args.clone().wipe).await;
.iter()
.map(|s| s.parse::<u64>().expect("Failed to parse ASSISTANT_BK_MODS. Invalid syntax."))
.collect();
let data = Data { let cf_bind = cf_data.clone();
owners, let lang_name = cf_bind["general"]["lang"].as_str().unwrap();
let lang_path = ["./data/lang/", lang_name, ".json"].concat();
let lang_pathbuf = PathBuf::from(lang_path);
return Data {
args: args.clone(),
ball_prompts: [ball_classic, ball_quirk], ball_prompts: [ball_classic, ball_quirk],
rps_game: Mutex::new(RPSGame::new()), cfg: cf_data,
bk_mods: mods_vec_u64, discord_data: dc_data.into(),
reddit_data: None.into(), env_vars: env_vars,
discord_data: None.into(), lang_name: lang_name.to_string(),
cfg: None.into(), lang: Lang::from_file(lang_pathbuf).unwrap(),
args: args.clone() reddit_data: re_data.into(),
rps_game: Mutex::new(RPSGame::new())
}; };
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 { pub async fn gen_bot(data: Data) -> Client {
let token = let token =
if !args.test { std::env::var("ASSISTANT_TOKEN").expect("Missing ASSISTANT_TOKEN env var!") } if !data.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!") }; else { std::env::var("ASSISTANT_TOKEN_TEST").expect("Missing ASSISTANT_TOKEN_TEST env var!") };
let intents = serenity::GatewayIntents::all(); let intents = serenity::GatewayIntents::all();
@@ -56,7 +56,7 @@ pub async fn gen_bot(data: Data, args: Args) -> Client {
let token_end_len = token[peek_len..].len(); let token_end_len = token[peek_len..].len();
rs_println!("Token: {}{}", token_peek, "*".repeat(token_end_len)); rs_println!("Token: {}{}", token_peek, "*".repeat(token_end_len));
let own: HashSet<UserId> = data.owners let owner_users: HashSet<UserId> = data.env_vars.bot_owners
.clone() .clone()
.into_iter() .into_iter()
.filter_map(|i| if i == 0 { None } else { Some(UserId::from(i))}) .filter_map(|i| if i == 0 { None } else { Some(UserId::from(i))})
@@ -64,7 +64,7 @@ pub async fn gen_bot(data: Data, args: Args) -> Client {
let framework = poise::Framework::builder() let framework = poise::Framework::builder()
.options(poise::FrameworkOptions { .options(poise::FrameworkOptions {
owners: own, owners: owner_users,
commands: make_cmd_vec(&data).await, commands: make_cmd_vec(&data).await,
event_handler: events::event_handler, event_handler: events::event_handler,
..Default::default() ..Default::default()
@@ -85,30 +85,29 @@ pub async fn gen_bot(data: Data, args: Args) -> Client {
async fn make_cmd_vec(data: &Data) -> Vec<Cmd> { async fn make_cmd_vec(data: &Data) -> Vec<Cmd> {
let mut cmds = vec![ let mut cmds: Vec<Cmd> = vec![
// GENERIC // GENERIC
cmds::help::cmd(), cmds::generic::help::cmd(),
cmds::eight_ball::cmd(), cmds::generic::eight_ball::cmd(),
cmds::wwrps::cmd(), cmds::generic::wwrps::cmd(),
// REDDIT // REDDIT
re_cmds::add::cmd(), cmds::reddit::add::cmd(),
re_cmds::approve::cmd(), cmds::reddit::approve::cmd(),
re_cmds::get::cmd(), cmds::reddit::get::cmd(),
re_cmds::remove::cmd(), cmds::reddit::remove::cmd(),
re_cmds::top::cmd(), cmds::reddit::top::cmd(),
re_cmds::update::cmd(), cmds::reddit::update::cmd(),
re_cmds::vote::cmd(), cmds::reddit::vote::cmd(),
re_cmds::shorturl::cmd(), cmds::reddit::shorturl::cmd(),
// OWNER // OWNER
cmds::embed::cmd(), cmds::generic::embed::cmd(),
cmds::send::cmd(), cmds::generic::send::cmd(),
debug_cmds::main_cmd::cmd(), cmds::debug::main_cmd::cmd(),
// DATABASE // DATABASE
db_cmds::main_cmd::cmd() cmds::db::main_cmd::cmd()
]; ];
let cfg = get_toml_mutex(&data.cfg).await.unwrap();
let disabled = cfg["commands"]["disabled_categories"] let disabled = data.cfg["commands"]["disabled_categories"]
.as_array() .as_array()
.unwrap() .unwrap()
.iter() .iter()
@@ -121,12 +120,12 @@ async fn make_cmd_vec(data: &Data) -> Vec<Cmd> {
} }
pub async fn set_status(m_data: Value, ctx: &serenity::Context) { pub async fn set_status(cfg: Value, ctx: &serenity::Context) {
let status_str: String; let status_str: String;
let status = m_data["general"]["status"].as_str().unwrap(); let status = cfg["general"]["status"].as_str().unwrap();
let status_c = m_data["general"]["statusCommitNumber"].as_bool().unwrap(); let status_c = cfg["general"]["statusCommitNumber"].as_bool().unwrap();
let status_ec = m_data["general"]["statusExperimentalCommit"].as_bool().unwrap(); let status_ec = cfg["general"]["statusExperimentalCommit"].as_bool().unwrap();
if status_c { if status_c {
let commit_num_r = process::Command::new("git") let commit_num_r = process::Command::new("git")
+75
View File
@@ -0,0 +1,75 @@
use std::{fs, path::PathBuf};
use serde_json::Value;
use dynfmt::{Format, SimpleCurlyFormat};
use crate::{Error, rs_warnln};
#[derive(Debug)]
enum LangErrorType {
InvalidArguments,
NotAnEndpoint,
KeyNotFound
}
pub struct Lang {
data: Value
}
impl From<Value> for Lang {
fn from(value: Value) -> Self
{ return Lang { data: value }; }
}
impl Lang {
pub fn from_file(filepath: PathBuf) -> Result<Self, Error> {
if !filepath.exists() { return Err(Error::from("LANG filepath not found!")); }
let file_contents = fs::read_to_string(filepath).unwrap();
let json = serde_json::from_str(&file_contents).unwrap();
return Ok(Lang { data: json });
}
pub fn get(&self, path: &str, args: &[String]) -> String {
let path_arr: Vec<&str> = path.split(".").collect();
return self.get_from_arr(path_arr, args);
}
pub fn get_from_arr(&self, path: Vec<&str>, args: &[String]) -> String {
let str_path = path.join(".");
let mut search: &Value = &self.data;
for i in &path {
let Some(r) = search.get(i)
else { rs_warnln!("LANG warning ({:?})! ({})", LangErrorType::KeyNotFound, str_path); return str_path; };
let str_r = r.as_str();
if let Some(string) = str_r
{ return Lang::format_str(string, args, str_path); }
else { search = r; }
}
rs_warnln!("LANG warning ({:?})! ({})", LangErrorType::NotAnEndpoint, str_path);
return str_path;
}
fn format_str(string: &str, args: &[String], fallback: String) -> String {
let cow = SimpleCurlyFormat.format(string, args);
if let Ok(ok) = cow
{ return ok.to_string(); }
else {
rs_warnln!("LANG warning ({:?})! ({})", LangErrorType::InvalidArguments, fallback);
return fallback;
}
}
}
-36
View File
@@ -60,39 +60,3 @@ macro_rules! warnln {
); );
}; };
} }
#[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()
}};
}
+85 -152
View File
@@ -1,187 +1,132 @@
#![warn(unused_extern_crates)]
#![allow(clippy::needless_return)] #![allow(clippy::needless_return)]
#![allow(static_mut_refs)]
mod cmds {
pub mod eight_ball; mod cmds;
pub mod embed; mod db;
pub mod help;
pub mod send;
pub mod wwrps;
}
mod re_cmds {
pub mod add;
pub mod approve;
pub mod generic_fns;
pub mod get;
pub mod remove;
pub mod shorturl;
pub mod top;
pub mod update;
pub mod vote;
}
mod debug_cmds {
pub mod guild_invite;
pub mod leave_guild;
pub mod main_cmd;
pub mod ping;
pub mod reload_cfg;
pub mod save;
pub mod stop;
pub mod view_guilds;
pub mod whoami;
}
mod db_cmds {
pub mod add_server;
pub mod reddit_channel;
pub mod main_cmd;
pub mod wwrps_channel;
}
mod events; mod events;
mod games;
mod gen;
mod lang;
mod macros;
mod messages; mod messages;
mod python; mod python;
mod macros;
#[allow(unknown_lints)]
mod websocket;
mod data;
mod schedule; mod schedule;
mod gen; mod websocket;
use std::process; use std::process;
use std::thread; use std::thread;
use std::thread::JoinHandle;
use std::time::Duration; use std::time::Duration;
use std::vec; use std::vec;
use std::error::Error as StdErr; use std::error::Error as StdErr;
use clap::Parser;
use r#gen::gen_bot; use r#gen::gen_bot;
use r#gen::gen_data; use r#gen::gen_data;
use poise::Command; use poise::Command;
use schedule::run_schedules; use schedule::run_schedules;
use serde::Serialize;
use serde_json::Value;
use tokio::runtime::Runtime; use tokio::runtime::Runtime;
use tokio::sync::Mutex;
use websocket::send_cmd_json; use websocket::send_cmd_json;
use crate::cmds::wwrps::RPSGame; use crate::db::bot_data::Data;
use crate::data::get_toml_mutex; use crate::db::env_vars::AssistantEnv;
use crate::db::terminal_args::Args;
use crate::schedule::Schedule; use crate::schedule::Schedule;
#[derive(Parser, Serialize, Clone)]
struct Args {
#[arg(short = 'p', long, default_value = "2920", help = "Sets the port number, e.g 2200.")]
port: u16,
#[arg(long, help = "Runs only the Python part of the program.")]
py: bool,
#[arg(long, help = "Runs only the Rust part of the program.")]
rs: bool,
#[arg(short = 'd', long, help = "Enables dev mode. Dev mode shows more debug info and turns off certain security measures.")]
dev: bool,
#[arg(short = 'w', long, help = "Wipes all data before running the program.")]
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 prints when the websockets send a ping. Why though?")]
ping: bool,
#[arg(long, help = "Makes the program not use the schedule system.")]
nosched: bool
}
type Error = Box<dyn std::error::Error + Send + Sync>; type Error = Box<dyn std::error::Error + Send + Sync>;
type Context<'a> = poise::Context<'a, Data, Error>; type Context<'a> = poise::Context<'a, Data, Error>;
type Cmd = Command<Data, Box<dyn StdErr + Send + Sync>>; type Cmd = Command<Data, Box<dyn StdErr + Send + Sync>>;
struct Data {
owners: Vec<u64>,
ball_prompts: [Vec<String>; 2],
rps_game: Mutex<RPSGame>,
reddit_data: Mutex<Option<Value>>,
discord_data: Mutex<Option<Value>>,
cfg: Mutex<Option<toml::Value>>,
bk_mods: Vec<u64>,
args: Args,
}
static CFG_DATA_RE: &str = "posts";
pub static mut LANG_NAME: Option<String> = None;
pub static mut LANG: Option<serde_json::Value> = None;
pub static mut NOPING: bool = false;
#[tokio::main] #[tokio::main]
async fn main() { async fn main() {
let args = <Args as clap::Parser>::parse(); let args = Args::new();
let args_str = serde_json::to_string(&args).expect("Error serializing args to JSON"); rs_println!("ARGS: {}", args.to_string());
unsafe { NOPING = !args.ping; } let env_vars = AssistantEnv::new(args.test);
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
.iter()
.map(|s| s.parse::<u64>().expect("Failed to parse ASSISTANT_OWNERS. Invalid syntax."))
.collect();
rs_println!("Generating and/or fetching data and config..."); rs_println!("Generating and/or fetching data and config...");
let data = gen_data(args.clone(), own_vec_u64.clone()).await; let data = gen_data(args, env_vars).await;
rs_println!("Fetching language file..."); rs_println!("[IMPORTANT] The below message is a test message, it should be written in the language you've selected\nTest message: {}", data.lang.get("lang_load", &[]));
let data_binding = get_toml_mutex(&data.cfg).await.unwrap();
let lang_cfg = data_binding["general"]["lang"].as_str().unwrap();
data::load_lang_data(lang_cfg.to_string());
rs_println!("[IMPORTANT] The below message is a test message, it should be written in the language you've selected\nTest message: {}", lang!("log_lang_load_success"));
if args.test { println!("----- USING TEST BOT -----"); } // We start this here cuz we have all the data that we need
if args.dev { println!("----- DEV MODE ENABLED -----"); } if data.args.py && !data.args.rs {
if args.dev && args.wipe { println!("----- \"DON'T WORRY ABOUT IT\" MODE ENABLED -----"); } let _ = python::start(
if args.nosched { println!("----- NO SCHEDULES -----"); } data.args.clone(),
data.lang_name.clone(),
if args.py && !args.rs { data.env_vars.clone(),
println!("----- PYTHON ONLY MODE -----"); ).await;
rs_println!("ARGS: {}", args_str);
let _ = python::start(args).await;
process::exit(0); process::exit(0);
} }
else if args.rs && ! args.py { else if data.args.rs && ! data.args.py {
println!("----- RUST ONLY MODE -----"); start_bot(data).await;
rs_println!("ARGS: {}", args_str);
start(args, data).await;
process::exit(0); process::exit(0);
} }
rs_println!("ARGS: {}", args_str); let cfg_arr = data.cfg["commands"]["disabled_categories"].as_array().unwrap();
let cfg = get_toml_mutex(&data.cfg).await.unwrap();
let cfg_arr = cfg["commands"]["disabled_categories"].as_array().unwrap();
let run_py = !cfg_arr.iter().any(|val| val.as_str() == Some("re")); let run_py = !cfg_arr.iter().any(|val| val.as_str() == Some("re"));
let rt_rs = Runtime::new().unwrap();
let rt_py = Runtime::new().unwrap();
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 and 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 || { let nosched = data.args.nosched;
rt_rs.block_on(async { let test = data.args.test;
if run_py { websocket::start(rust_args.clone(), own_vec_u64.clone()).await; }
start(rust_args, data).await;
});
});
let python = thread::spawn(move || { let python = start_py(
rt_py.block_on(async { data.args.clone(),
if run_py { let _ = python::start(python_args).await; } data.lang_name.clone(),
}); data.env_vars.clone(),
}); run_py
);
let rust = start_rs(data, run_py);
if !args.nosched { if !nosched { start_schedules(test).await; }
let dur = if args.test { Duration::from_secs(60) } else { Duration::from_secs(60 * 10) };
rust.join().unwrap();
python.join().unwrap();
}
fn start_rs(data: Data, run_py: bool) -> JoinHandle<()> {
let rt = Runtime::new().unwrap();
return thread::spawn(move || {
rt.block_on(async {
if run_py { websocket::start(&data).await; }
start_bot(data).await;
});
});
}
fn start_py(
args: Args,
lang_name: String,
env_vars: AssistantEnv,
run_py: bool
) -> JoinHandle<()> {
let rt = Runtime::new().unwrap();
return thread::spawn(move || {
rt.block_on(async {
if run_py { let _ = python::start(args, lang_name, env_vars).await; }
});
});
}
async fn start_bot(data: Data) {
let mut bot = gen_bot(data).await;
rs_println!("Starting Discord bot...");
bot.start().await.unwrap();
}
async fn start_schedules(test: bool) {
let dur = if test { Duration::from_secs(60) }
else { Duration::from_secs(60 * 10) };
let schedules: Vec<Schedule> = vec![ let schedules: Vec<Schedule> = vec![
(dur, || Box::pin(read_reddit_inbox())) (dur, || Box::pin(read_reddit_inbox()))
@@ -190,22 +135,10 @@ async fn main() {
run_schedules(schedules).await; run_schedules(schedules).await;
} }
rust.join().unwrap();
python.join().unwrap();
}
async fn start(args: Args, data: Data) {
let mut bot = gen_bot(data, args).await;
rs_println!("Starting Discord bot...");
bot.start().await.unwrap();
}
async fn read_reddit_inbox() { async fn read_reddit_inbox() {
unsafe { unsafe {
if !websocket::HAS_CONNECTED { return; } if !websocket::HAS_CONNECTED { return; }
send_cmd_json("respond_mentions", None, !NOPING).await; send_cmd_json("respond_mentions", None, false).await;
} }
} }
+49 -40
View File
@@ -1,7 +1,7 @@
use std::env;
use std::io::{Read, Write}; use std::io::{Read, Write};
use crate::{lang, Args, Context}; use crate::Context;
use crate::lang::Lang;
use base64::engine::general_purpose; use base64::engine::general_purpose;
use base64::Engine; use base64::Engine;
@@ -10,7 +10,7 @@ use flate2::write::ZlibEncoder;
use flate2::Compression; use flate2::Compression;
use poise::serenity_prelude::json::Value; use poise::serenity_prelude::json::Value;
use poise::{serenity_prelude::CreateMessage, CreateReply, ReplyHandle}; use poise::{serenity_prelude::CreateMessage, CreateReply, ReplyHandle};
use poise::serenity_prelude::{ChannelId, Color, CreateActionRow, CreateButton, CreateEmbed, CreateEmbedAuthor, EditMessage, Http, Message, ReactionType, Timestamp, UserId}; use poise::serenity_prelude::{ChannelId, Color, CreateActionRow, CreateButton, CreateEmbed, CreateEmbedAuthor, Http, Message, ReactionType, Timestamp, UserId};
use serde_json::json; use serde_json::json;
@@ -36,7 +36,7 @@ pub struct EmbedOptions {
impl Default for EmbedOptions { impl Default for EmbedOptions {
fn default() -> Self { fn default() -> Self {
return EmbedOptions { return EmbedOptions {
desc: lang!("dc_msg_embed_default_embed_desc"), desc: "Default english embed description.".to_string(),
title: None, title: None,
col: None, col: None,
url: None, url: None,
@@ -87,7 +87,6 @@ pub async fn send_msg(
} }
#[allow(dead_code)]
pub async fn http_send_msg( pub async fn http_send_msg(
http: &Http, http: &Http,
c_id: ChannelId, c_id: ChannelId,
@@ -135,8 +134,7 @@ pub async fn send_embed(
} }
#[allow(dead_code)] /* pub async fn http_send_embed(
pub async fn http_send_embed(
http: &Http, http: &Http,
c_id: ChannelId, c_id: ChannelId,
options: EmbedOptions options: EmbedOptions
@@ -152,7 +150,7 @@ pub async fn http_send_embed(
let msg = c_id.send_message(http, r).await; let msg = c_id.send_message(http, r).await;
return msg.ok(); return msg.ok();
} } */
pub fn embed_from_options(options: EmbedOptions) -> CreateEmbed { pub fn embed_from_options(options: EmbedOptions) -> CreateEmbed {
@@ -189,26 +187,35 @@ pub async fn edit_reply(
} }
#[allow(dead_code)] /* pub async fn http_edit_msg(
pub async fn http_edit_msg(
http: &Http, http: &Http,
mut msg: Message, mut msg: Message,
new_msg: EditMessage new_msg: EditMessage
) { ) {
let _ = msg.edit(http, new_msg).await; let _ = msg.edit(http, new_msg).await;
} */
/*pub async fn send_dm(msg: String, data: &Data) {
let http = Http::new(&data.env_vars.token);
let c_msg = CreateMessage::new().content(msg);
for uid in data.env_vars.bot_owners.clone() {
if uid == 0 { continue; }
let user = UserId::new(uid);
let _ = user.dm(http.as_ref(), c_msg.clone()).await;
} }
}*/
pub async fn send_dm(msg: String, args: Args, receivers: Vec<u64>) {
let token: String =
if !args.test { env::var("ASSISTANT_TOKEN") .expect("Missing ASSISTANT_TOKEN env var!") }
else { env::var("ASSISTANT_TOKEN_TEST").expect("Missing ASSISTANT_TOKEN_TEST env var!") };
pub async fn send_dm_min(msg: String, token: String, bot_owners: Vec<u64>) {
let http = Http::new(&token); let http = Http::new(&token);
let c_msg = CreateMessage::new().content(msg); let c_msg = CreateMessage::new().content(msg);
for uid in receivers { for uid in bot_owners {
if uid == 0 { continue; } if uid == 0 { continue; }
let user = UserId::new(uid); let user = UserId::new(uid);
let _ = user.dm(http.as_ref(), c_msg.clone()).await; let _ = user.dm(http.as_ref(), c_msg.clone()).await;
@@ -216,19 +223,19 @@ pub async fn send_dm(msg: String, args: Args, receivers: Vec<u64>) {
} }
pub fn make_post_embed(post_data: &Value, url: &str, ephemeral: bool) -> EmbedOptions { pub fn make_post_embed(lang: &Lang, post_data: &Value, url: &str, ephemeral: bool) -> EmbedOptions {
let media_type = &post_data["post_data"]["media_type"]; let media_type = &post_data["post_data"]["media_type"];
let desc_str = lang!( let desc_str = lang.get(
"dc_msg_embed_re_post", "dc.re.embed.post",
post_data["post_data"]["subreddit"].as_str().unwrap(), &[post_data["post_data"]["subreddit"].as_str().unwrap(),
post_data["post_data"]["upvotes"].as_i64().unwrap(), post_data["post_data"]["upvotes"].as_i64().unwrap().to_string().as_str(),
post_data["votes"]["mod_voters"].as_array().unwrap().len(), post_data["votes"]["mod_voters"].as_array().unwrap().len().to_string().as_str(),
if !media_type.is_null() { media_type.as_str().unwrap() } else { "None" }, if !media_type.is_null() { media_type.as_str().unwrap() } else { "None" },
if post_data["added"] ["by_human"].as_bool().unwrap() { "" } else { "" }, if post_data["added"] ["by_human"].as_bool().unwrap() { "" } else { "" },
if post_data["added"] ["by_bot"].as_bool().unwrap() { "" } else { "" }, if post_data["added"] ["by_bot"].as_bool().unwrap() { "" } else { "" },
if post_data["approved"]["by_human"].as_bool().unwrap() { "" } else { "" } if post_data["approved"]["by_human"].as_bool().unwrap() { "" } else { "" }].map(String::from)
); );
let trimmed = desc_str let trimmed = desc_str
@@ -238,7 +245,7 @@ pub fn make_post_embed(post_data: &Value, url: &str, ephemeral: bool) -> EmbedOp
.join("\n"); .join("\n");
let media_urls = post_data["post_data"]["media_urls"].as_array().unwrap(); let media_urls = post_data["post_data"]["media_urls"].as_array().unwrap();
let action_row = make_post_components(); let action_row = make_post_components(lang);
let json_encoded = trim_compress_and_encode_json(post_data); let json_encoded = trim_compress_and_encode_json(post_data);
@@ -258,23 +265,25 @@ pub fn make_post_embed(post_data: &Value, url: &str, ephemeral: bool) -> EmbedOp
} }
pub fn make_removed_embed(post_data: &Value, url: &str, ephemeral: bool) -> EmbedOptions { pub fn make_removed_embed(lang: &Lang, post_data: &Value, url: &str, ephemeral: bool) -> EmbedOptions {
let action_row = make_removed_components(); let action_row = make_removed_components(lang);
let none = lang!("none"); let none = lang.get("generic.none", &[]);
let desc = lang!( let desc = lang.get(
"dc_msg_embed_re_removed", "dc.re.embed.post_removed",
if !post_data["removed"]["by"].is_null() { post_data["removed"]["by"].as_str().unwrap() } &[if !post_data["removed"]["by"].is_null()
{ post_data["removed"]["by"].as_str().unwrap() }
else { &none }, else { &none },
if !post_data["removed"]["reason"].is_null() { post_data["removed"]["reason"].as_str().unwrap() } if !post_data["removed"]["reason"].is_null()
else { &none } { post_data["removed"]["reason"].as_str().unwrap() }
else { &none }].map(String::from)
); );
let json_encoded = trim_compress_and_encode_json(post_data); let json_encoded = trim_compress_and_encode_json(post_data);
return EmbedOptions { return EmbedOptions {
title: Some(lang!("dc_msg_removed_square_brackets", post_data["post_data"]["title"].clone())), title: Some(lang.get("dc.re.embed.removed_square_brackets", &[post_data["post_data"]["title"].clone().to_string()])),
desc: format!("{}\n\n{}{}{}", desc, JSON_TEXT_START, json_encoded, JSON_TEXT_END), desc: format!("{}\n\n{}{}{}", desc, JSON_TEXT_START, json_encoded, JSON_TEXT_END),
col: Some(REMOVED_DC_COL), col: Some(REMOVED_DC_COL),
url: Some(url.to_string()), url: Some(url.to_string()),
@@ -329,19 +338,19 @@ pub fn decode_and_decompress_json(t: String) -> Result<Value, serde_json::Error>
} }
fn make_post_components() -> CreateActionRow { fn make_post_components(lang: &Lang) -> CreateActionRow {
return CreateActionRow::Buttons(vec![ return CreateActionRow::Buttons(vec![
CreateButton::new("vote_btn") .label(lang!("dc_btn_vote")) .emoji(ReactionType::Unicode("⬆️".to_string())), CreateButton::new("vote_btn") .label(lang.get("dc.re.buttons.vote", &[])).emoji(ReactionType::Unicode("⬆️".to_string())),
CreateButton::new("unvote_btn") .label(lang!("dc_btn_unvote")), CreateButton::new("unvote_btn") .label(lang.get("dc.re.buttons.unvote", &[])),
CreateButton::new("approve_btn") .label(lang!("dc_btn_approve")) .emoji(ReactionType::Unicode("".to_string())), CreateButton::new("approve_btn") .label(lang.get("dc.re.buttons.approve", &[])).emoji(ReactionType::Unicode("".to_string())),
CreateButton::new("unapprove_btn").label(lang!("dc_btn_unapprove")) .emoji(ReactionType::Unicode("".to_string())), CreateButton::new("unapprove_btn").label(lang.get("dc.re.buttons.disapprove", &[])).emoji(ReactionType::Unicode("".to_string())),
CreateButton::new("remove_btn") .label(lang!("dc_btn_remove")) .emoji(ReactionType::Unicode("🗑️".to_string())) CreateButton::new("remove_btn") .label(lang.get("dc.re.buttons.remove", &[])).emoji(ReactionType::Unicode("🗑️".to_string()))
]); ]);
} }
fn make_removed_components() -> CreateActionRow { fn make_removed_components(lang: &Lang) -> CreateActionRow {
return CreateActionRow::Buttons(vec![ return CreateActionRow::Buttons(vec![
CreateButton::new("unremove_btn").label(lang!("dc_btn_unremove")).emoji(ReactionType::Unicode("↩️".to_string())) CreateButton::new("unremove_btn").label(lang.get("dc.re.buttons.unremove", &[])).emoji(ReactionType::Unicode("↩️".to_string()))
]); ]);
} }
+13 -22
View File
@@ -1,5 +1,7 @@
use crate::messages::send_dm; use crate::db::env_vars::AssistantEnv;
use crate::{errln, lang, rs_println, Args, LANG_NAME}; use crate::db::terminal_args::Args;
use crate::messages::send_dm_min;
use crate::{errln, rs_println};
use std::fs; use std::fs;
use std::ffi::CString; use std::ffi::CString;
@@ -9,7 +11,7 @@ use pyo3::prelude::*;
use pyo3::types::PyList; use pyo3::types::PyList;
pub async fn start(args: Args) -> PyResult<()> { pub async fn start(args: Args, lang_name: String, env_vars: AssistantEnv) -> PyResult<()> {
rs_println!("Running Python program..."); rs_println!("Running Python program...");
let args_str = serde_json::to_string(&args).expect("Error serializing args to JSON"); let args_str = serde_json::to_string(&args).expect("Error serializing args to JSON");
@@ -23,15 +25,12 @@ pub async fn start(args: Args) -> PyResult<()> {
let py_args = args_str.replace(":true", ":True").replace(":false", ":False"); let py_args = args_str.replace(":true", ":True").replace(":false", ":False");
let app_path: CString; let app_path: CString;
unsafe { app_path = CString::new(format!(
app_path = CString::new( "args = {}\nlang_name = \"{}\"\n{}",
format!("args = {}\nlang_name = \"{}\"\n{}",
py_args, py_args,
LANG_NAME.clone().unwrap(), lang_name,
code code
) )).unwrap();
).unwrap();
}
let mut traceback: String = String::new(); let mut traceback: String = String::new();
let mut is_error = false; let mut is_error = false;
@@ -57,19 +56,11 @@ pub async fn start(args: Args) -> PyResult<()> {
}); });
if is_error { if is_error {
let own_env = std::env::var("ASSISTANT_OWNERS").unwrap_or("0".to_string()); send_dm_min(
let own_vec_str: Vec<String> = own_env.split(",").map(String::from).collect(); format!("Unknown internal Python Error:\n```\n{0}\n```", traceback),
let own_vec_u64: Vec<u64> = own_vec_str env_vars.token.clone(),
.iter() env_vars.bot_owners.clone()
.map(|s| s.parse::<u64>().expect("Failed to parse ASSISTANT_OWNERS. Invalid syntax."))
.collect();
send_dm(
lang!("dc_msg_dm_python_err", format!("{}", traceback)),
args,
own_vec_u64
).await; ).await;
errln!("pyO3: {}", traceback); errln!("pyO3: {}", traceback);
} }
+16 -5
View File
@@ -28,11 +28,22 @@ def py_error(*args: str):
def lang(k: str) -> str: def lang(k: str) -> str:
if G_LANG == {}: return lang_from_arr(k.split("."))
py_error("Language must be initialized before use!")
t = G_LANG.get(k)
if t is None: py_error(f"Key not found in language \"{G_LANG_NAME}\": {k}") def lang_from_arr(path: list[str]) -> str:
return str(t) if G_LANG == {}: py_error("Language must be initialized before use!")
str_path = ".".join(path)
search = G_LANG
for i in path:
r = search.get(i) # type: ignore
if r is None: return str_path
elif r is str: return str(r)
else: search = r # type: ignore
return str_path
def init_lang(lang_name: str): def init_lang(lang_name: str):
+1 -1
View File
@@ -19,7 +19,7 @@ async def main():
await bot.set_args(args) # type: ignore await bot.set_args(args) # type: ignore
py_print("Fetching language file...") py_print("Fetching language file...")
init_lang(lang_name) # type: ignore init_lang(lang_name) # type: ignore
py_print(f"[IMPORTANT] The below message is a test message, it should be written in the language you've selected\nTest message: {lang('log_lang_load_success')}") py_print(f"[IMPORTANT] The below message is a test message, it should be written in the language you've selected\nTest message: {lang('lang_load')}")
except NameError: except NameError:
py_print("No command args or language name found from Rust. Don't worry though, we have backup in place.") py_print("No command args or language name found from Rust. Don't worry though, we have backup in place.")
init_lang("en") init_lang("en")
+5 -5
View File
@@ -69,18 +69,18 @@ async def bk_week_add(mention: models.Comment, bot: botPy.Bot):
bd = bot.data[botPy.RE_DATA_POSTS] bd = bot.data[botPy.RE_DATA_POSTS]
if short_url not in bd: if short_url not in bd:
if not is_mod: r = lang("py_re_response_weekly_add") if not is_mod: r = lang("re.add")
if is_mod: r = lang("py_re_response_weekly_mod_add") if is_mod: r = lang("re.mod_add")
else: else:
if bd[short_url]["removed"]["removed"] and is_mod: if bd[short_url]["removed"]["removed"] and is_mod:
r = lang("py_re_response_weekly_mod_unremove") r = lang("re.mod_unremove")
elif not bd[short_url]["removed"]["removed"]: elif not bd[short_url]["removed"]["removed"]:
r = lang("py_re_response_weekly_exists") r = lang("re.exists")
await posts.add_post_url(bot, short_url) await posts.add_post_url(bot, short_url)
if r != "": if r != "":
await mention.reply(r + "\n\n" + lang("py_re_response_suffix")) await mention.reply(r + "\n\n" + lang("re.suffix"))
if bot.args["dev"]: py_print("Responded to mention.") if bot.args["dev"]: py_print("Responded to mention.")
elif bot.args["dev"]: py_print("Response is empty.") elif bot.args["dev"]: py_print("Response is empty.")
await mention.mark_read() await mention.mark_read()
+29 -22
View File
@@ -8,8 +8,9 @@ use futures::StreamExt;
use std::sync::Arc; use std::sync::Arc;
use serde_json::{Value, json}; use serde_json::{Value, json};
use crate::messages::send_dm; use crate::db::bot_data::Data;
use crate::{lang, rs_println}; use crate::messages::send_dm_min;
use crate::rs_println;
use crate::Args; use crate::Args;
type Sender = Arc<Mutex<Option<futures::stream::SplitSink<tokio_tungstenite::WebSocketStream<tokio::net::TcpStream>, tungstenite::Message>>>>; type Sender = Arc<Mutex<Option<futures::stream::SplitSink<tokio_tungstenite::WebSocketStream<tokio::net::TcpStream>, tungstenite::Message>>>>;
@@ -103,17 +104,25 @@ async fn receive_response() -> Option<Value> {
} }
pub async fn start(args: Args, owners: Vec<u64>) { pub async fn start(data: &Data) {
rs_println!("Running local websocket..."); rs_println!("Running local websocket...");
let ip = format!("127.0.0.1:{}", args.port); let ip = format!("127.0.0.1:{}", data.args.port);
let listener = TcpListener::bind(&ip).await.unwrap(); let listener = TcpListener::bind(&ip).await.unwrap();
rs_println!("WebSocket server running on ws://{}", ip); rs_println!("WebSocket server running on ws://{}", ip);
tokio::spawn(handle_connections(listener, args, owners)); tokio::spawn(handle_connections(
listener,
data.env_vars.token.clone(),
data.env_vars.bot_owners.clone()
));
} }
async fn handle_connections(listener: TcpListener, args: Args, owners: Vec<u64>) { async fn handle_connections(
listener: TcpListener,
token: String,
bot_owners: Vec<u64>
) {
while let Ok((stream, _)) = listener.accept().await { while let Ok((stream, _)) = listener.accept().await {
let ws_stream = accept_async(stream).await.unwrap(); let ws_stream = accept_async(stream).await.unwrap();
let (sender, receiver) = ws_stream.split(); let (sender, receiver) = ws_stream.split();
@@ -124,14 +133,17 @@ async fn handle_connections(listener: TcpListener, args: Args, owners: Vec<u64>)
set_sender(sender_arc.clone()).await; set_sender(sender_arc.clone()).await;
set_receiver(receiver_arc.clone()).await; set_receiver(receiver_arc.clone()).await;
while let Some(Ok(msg)) = receiver_arc.lock().await.as_mut().unwrap().next().await { while let Some(Ok(msg)) = receiver_arc.lock().await.as_mut().unwrap().next().await
handle_message(msg, args.clone(), owners.clone()).await; { handle_message(msg, token.clone(), bot_owners.clone()).await; }
}
} }
} }
async fn handle_message(msg: tungstenite::protocol::Message, args: Args, owners: Vec<u64>) { async fn handle_message(
msg: tungstenite::protocol::Message,
token: String,
bot_owners: Vec<u64>,
) {
match msg { match msg {
tungstenite::Message::Text(text) => { tungstenite::Message::Text(text) => {
rs_println!("Received from Python: {}", text); rs_println!("Received from Python: {}", text);
@@ -139,7 +151,11 @@ async fn handle_message(msg: tungstenite::protocol::Message, args: Args, owners:
if let Some(stripped) = text.strip_prefix("json:") { if let Some(stripped) = text.strip_prefix("json:") {
let t_json: Value = serde_json::from_str(stripped).unwrap(); let t_json: Value = serde_json::from_str(stripped).unwrap();
if t_json.get("error").is_some() { if t_json.get("error").is_some() {
send_dm(lang!("dc_msg_dm_python_err_socket"), args, owners).await; send_dm_min(
"Unknown internal Python error occurred: Websocket response error".to_string(),
token,
bot_owners
).await;
} }
} }
@@ -150,16 +166,7 @@ async fn handle_message(msg: tungstenite::protocol::Message, args: Args, owners:
HAS_CONNECTED = true; HAS_CONNECTED = true;
} }
} }
} },
tungstenite::Message::Binary(bytes) => { _ => {}
if args.dev && args.ping {
rs_println!("[Binary] from Python: {:?}", bytes);
}
}
_ => {
if args.dev && args.ping {
rs_println!("Received from Python: [UNKNOWN / OTHER]");
}
}
} }
} }