diff --git a/.gitignore b/.gitignore index d226673..022b0e9 100644 --- a/.gitignore +++ b/.gitignore @@ -3,12 +3,12 @@ target/ Cargo.lock **/*.rs.bk *.pdb -__pycache__/ +**/__pycache__/ .vscode/ # program-created data -data/reddit_data.json -data/discord_data.json -data/re_data.json -data/dc_data.json -data/cfg.json \ No newline at end of file +**/reddit_data.json +**/discord_data.json +**/re_data.json +**/dc_data.json +**/cfg.toml \ No newline at end of file diff --git a/Cargo.toml b/Cargo.toml index f35a927..933bcba 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -17,3 +17,4 @@ serde = "1.0.217" serde_json = "1.0.138" tokio = { version = "1.43.0", features = ["rt-multi-thread"] } tokio-tungstenite = "0.26.1" +toml = "0.8.23" diff --git a/README.md b/README.md index 25decfc..d86e802 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,4 @@ + # ByteDiceAssistant An automation tool primarily made for myself (Byte Dice) but publicly available for anyone to use. It's both a Discord and Reddit bot in one program. @@ -10,36 +11,45 @@ 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.**\ **You can learn more about copyright by reading the full [license](/LICENSE.txt).** +## Commands + +| Name | Category | Description | +| -----------------------| -------- | -------------------------------------------------------------------- | +| `help` | help | Sends a help menu. | +| `8_ball` | fun | Sends a random answer to a prompt. | +| `add_server` | admin | Adds your server to the bots database for storage (no data is sold). | +| `embed` | owner | Creates an embed (requires `ASSISTANT_OWNERS` for security reasons). | +| `ping` | fun | Makes the bot reply with "pong" or a custom message. | +| `send` | owner | Sends a message (requires `ASSISTANT_OWNERS` for security reasons). | +| `stop` | owner | Stops the bot (requires `ASSISTANT_OWNERS` for security reasons). | +| ---------------------- | -------- | -------------------------------------------------------------------- | +| `admin_re_bindchannel` | admin | Sets the channel the command was run in as the one where `re_updatediscord` dumps information. This command is required for any of the other "re" commands to work. | +| `re_addpost` | re | Adds a post to the database. | +| `re_approvepost` | re | Flags a post in the database as approved. | +| `re_getpost` | re | Sends information about a post in the database. | +| `re_removepost` | re | "Removes" a post from the database (It actually only flags it as removed). | +| `re_shorturl` | re | Converts a long URL `https://www.reddit.com/r/SUBREDDIT/comments/POST_ID/POST_TITLE/` to a short one `https://redd.it/POST_ID`. | +| `re_topposts` | re | Sends the top posts in a category (such as upvotes). The posts have to be within the database. | +| `re_updatediscord` | re | Dumps the entire database (with a few restrictions) in the `admin_re_bindchannel` channel. | +| `re_vote` | re | Adds a vote (separate from Reddit upvotes) to a post. Use votes however you'd like. | + ## How to run ### Dependencies: -This program uses Rust (v1.82.0) and Python (v3.11.4), you can likely use other versions if they are compatible. This program also uses these Python modules: -* asyncio -* asyncpraw -* asyncprawcore -* emoji -* io -* json -* os -* sys -* threading -* time -* typing -* websockets - -You can install Python modules by running `$ pip install {module}` or `$ python -m pip install {module}` in a terminal. +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 Run* 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. | +| **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. | +| `ASSISTANT_BK_MODS` | (OPTIONAL) Same format as `ASSISTANT_OWNERS` but for people who are allowed to use the `/re_*` commands. | ### 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`) @@ -54,6 +64,7 @@ You can install Python modules by running `$ pip install {module}` or `$ python * Set the environment variables (listed above). * Restart the terminal. * Navigate to the project root directory. +* Install all Python modules. (`pip install -r req.txt`) * Run with `cargo run`. * Alternatively, run `cargo run -- {args here}` to add args. * For help, run `cargo run -- -h` or `cargo run -- --help`. @@ -64,16 +75,20 @@ You can install Python modules by running `$ pip install {module}` or `$ python * Open a terminal. * Set the environment variables (listed above). * On Windows: - * Run `setx VARIABLE_NAME "value in quotes"` in a terminal. - * On Unix (Linux / Mac): + * On Linux: * Run `sudo nano /etc/environment` or `sudo vim /etc/environment` in the terminal (and enter your password if prompted to). * Press `i` (only if you used VIM). * Write `VARIABLE_NAME="value"` + a new line for every variable. * if nano: `ctrl + O` (and press enter) then `ctrl + X`. * if VIM: press `esc` then write `:wq` (and press enter). + + * On macOS: + * Probably the same as Linux, but I don't use this OS so I have no clue. + * Restart the terminal if you added/changed any environment variables. * Run `cd path/to/extracted/folder` to navigate to the downloaded files (replace `path/to/extracted/folder` with your actual path). +* Install all python modules with `pip install -r req.txt`. * Run `cargo run` to start the program. 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 error output. \ No newline at end of file diff --git a/data/8-ball_classic.txt b/cfg/8-ball_classic.txt similarity index 100% rename from data/8-ball_classic.txt rename to cfg/8-ball_classic.txt diff --git a/data/8-ball_quirky.txt b/cfg/8-ball_quirky.txt similarity index 100% rename from data/8-ball_quirky.txt rename to cfg/8-ball_quirky.txt diff --git a/data/status.txt b/cfg/status.txt similarity index 100% rename from data/status.txt rename to cfg/status.txt diff --git a/data/cfg_default.json b/data/cfg_default.json deleted file mode 100644 index 25ddd6e..0000000 --- a/data/cfg_default.json +++ /dev/null @@ -1,8 +0,0 @@ -{ - "comment": "IMPORTANT!!! Anything labeled with 'RESTART_{cfg-name-here}' requires a restart to apply", - "reddit": { - "RESTART_enabled": true, - "subreddits": "bytedicetesting", - "fetch_limit": 100 - } -} \ No newline at end of file diff --git a/data/defaults/cfg_default.toml b/data/defaults/cfg_default.toml new file mode 100644 index 0000000..7fca7cc --- /dev/null +++ b/data/defaults/cfg_default.toml @@ -0,0 +1,19 @@ +[reddit] +# Which subreddits the bot will scan when executing "re"-category commands. +# Is automatically disabled when `disabled_categories` includes "re". +# Separate multiple subreddits with a "+", e.g "memes+askreddit". +subreddits = "bytedicetesting" + +# How many posts the bot scans when running `/re_updatediscord`. +fetch_limit = 100 + +[commands] +# TODO: readme +# Disabled selected command categories. All commands and their categories can be viewed in the README. +# "admin" and "owner" categories are always enabled. +# [REQUIRES RESTART] +disabled_categories = [ + # "fun", + # "help" + # "re" +] \ No newline at end of file diff --git a/data/dc_data_preset.json b/data/defaults/dc_data_preset.json similarity index 100% rename from data/dc_data_preset.json rename to data/defaults/dc_data_preset.json diff --git a/data/re_data_preset.json b/data/defaults/re_data_preset.json similarity index 100% rename from data/re_data_preset.json rename to data/defaults/re_data_preset.json diff --git a/req.txt b/req.txt new file mode 100644 index 0000000..e0b97af --- /dev/null +++ b/req.txt @@ -0,0 +1,5 @@ +asyncpraw +asyncprawcore +emoji +toml +websockets \ No newline at end of file diff --git a/src/cmds/reload_cfg.rs b/src/cmds/reload_cfg.rs index e4e5fa3..e874040 100644 --- a/src/cmds/reload_cfg.rs +++ b/src/cmds/reload_cfg.rs @@ -1,6 +1,6 @@ use serde_json::json; -use crate::{data::{get_mutex_data, read_cfg_data}, lang, messages::send_msg, websocket::send_cmd_json, Context, Error}; +use crate::{data::{get_toml_mutex, read_cfg_data}, lang, messages::send_msg, websocket::send_cmd_json, Context, Error}; #[poise::command( @@ -17,14 +17,14 @@ pub async fn cmd( ) -> Result<(), Error> { read_cfg_data(&ctx.data(), false).await; - let d = get_mutex_data(&ctx.data().cfg).await?; - let d_str = serde_json::to_string(&d)?; + let d = get_toml_mutex(&ctx.data().cfg).await.unwrap(); + let d_str = toml::to_string(&d)?; let r = send_cmd_json("update_cfg", Some(json!([d_str])), true).await; if r.is_some() && r.unwrap()["value"].as_bool().unwrap() { send_msg( ctx, - lang!("dc_msg_reload_cfg_success", serde_json::to_string_pretty(&d).unwrap()), + lang!("dc_msg_reload_cfg_success", toml::to_string_pretty(&d).unwrap()), true, true ).await; diff --git a/src/data.rs b/src/data.rs index c23f19a..9e973f7 100644 --- a/src/data.rs +++ b/src/data.rs @@ -8,12 +8,15 @@ use crate::{errln, rs_println, Data, Error, CFG_DATA_RE, LANG}; use crate::websocket::send_cmd_json; -static DATA_PATH_DC: &str = "./data/dc_data.json"; -static PRESET_PATH_DC: &str = "./data/dc_data_preset.json"; -static DATA_PATH_RE: &str = "./data/re_data.json"; -static PRESET_PATH_RE: &str = "./data/re_data_preset.json"; -static DATA_PATH_CFG: &str = "./data/cfg.json"; -static PRESET_PATH_CFG: &str = "./data/cfg_default.json"; +static DATA_PATH_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"; @@ -117,15 +120,15 @@ pub async fn read_cfg_data(data: &Data, wipe: bool) { if !Path::new(DATA_PATH_CFG).exists() || wipe { rs_println!( "{} creating new from preset...", - if !wipe { "cfg.json not found," } else { "[WIPE] (cfg.json)" } + 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 = serde_json::from_str(&str_data).unwrap(); + let json_data: toml::Value = str_data.parse().unwrap(); let mut cfg_data = data.cfg.lock().await; - *cfg_data = json_data; + *cfg_data = Some(json_data); send_cmd_json("update_cfg", Some(json!([str_data])), true).await; } @@ -133,9 +136,9 @@ pub async fn read_cfg_data(data: &Data, wipe: bool) { fn generate_cfg_data() { let preset_str = fs::read_to_string(PRESET_PATH_CFG).unwrap(); - let preset_json: Value = serde_json::from_str(&preset_str).unwrap(); + let preset_json: toml::Value = preset_str.parse().unwrap(); - let json_str = serde_json::to_string_pretty(&preset_json).unwrap(); + let json_str = toml::to_string_pretty(&preset_json).unwrap(); let mut file = fs::File::create(DATA_PATH_CFG).unwrap(); file.write_all(json_str.as_bytes()).unwrap(); @@ -200,6 +203,15 @@ pub async fn get_mutex_data(data: &Mutex>) -> Result } +pub async fn get_toml_mutex(data: &Mutex>) -> Result { + 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); diff --git a/src/events.rs b/src/events.rs index aa8f1d9..b3eeba8 100644 --- a/src/events.rs +++ b/src/events.rs @@ -19,11 +19,9 @@ pub fn event_handler<'a>( data_about_bot.user.id ); - let file_text = std::fs::read_to_string("./data/status.txt").unwrap(); + let file_text = std::fs::read_to_string("./cfg/status.txt").unwrap(); let custom_activity = ActivityData::custom(file_text); - // TODO: make custom rich presence - //let playing_activity - + ctx.online(); ctx.set_activity(Some(custom_activity)); } diff --git a/src/gen.rs b/src/gen.rs index df85ea8..00bcf49 100644 --- a/src/gen.rs +++ b/src/gen.rs @@ -4,12 +4,12 @@ use poise::serenity_prelude::UserId; use poise::serenity_prelude as serenity; use poise::serenity_prelude::Client; -use crate::{cmds, data::{self, get_mutex_data}, events, re_cmds, rs_println, Args, Cmd, Data}; +use crate::{cmds, data::{self, get_toml_mutex}, events, re_cmds, rs_println, Args, Cmd, Data}; pub async fn gen_data(args: Args, owners: Vec) -> Data { - let ball_classic_str = std::fs::read_to_string("./data/8-ball_classic.txt").unwrap(); - let ball_quirk_str = std::fs::read_to_string("./data/8-ball_quirky.txt").unwrap(); + let ball_classic_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_classic: Vec = ball_classic_str.lines().map(String::from).collect(); let ball_quirk: Vec = ball_quirk_str .lines().map(String::from).collect(); @@ -76,38 +76,39 @@ pub async fn gen_bot(data: Data, args: Args) -> Client { async fn make_cmd_vec(data: &Data) -> Vec { - let mut cmds = vec![]; - let cfg = get_mutex_data(&data.cfg).await.unwrap(); - - // GENERIC - cmds.extend([ + let mut cmds = vec![ + // GENERIC cmds::help::cmd(), cmds::ping::cmd(), + cmds::eight_ball::cmd(), + // REDDIT + re_cmds::add::cmd(), + re_cmds::approve::cmd(), + re_cmds::get::cmd(), + re_cmds::remove::cmd(), + re_cmds::top::cmd(), + re_cmds::update::cmd(), + re_cmds::vote::cmd(), + re_cmds::shorturl::cmd(), + // [ADMIN / OWNER] cmds::embed::cmd(), cmds::send::cmd(), cmds::stop::cmd(), - cmds::eight_ball::cmd(), - cmds::add_server::cmd() - ]); + cmds::add_server::cmd(), + cmds::reload_cfg::cmd(), + // REDDIT [ADMIN / OWNER] + re_cmds::admin_bind::cmd() + ]; + let cfg = get_toml_mutex(&data.cfg).await.unwrap(); - // REDDIT - if cfg["reddit"]["RESTART_enabled"].as_bool().unwrap() { - cmds.extend([ - re_cmds::add::cmd(), - re_cmds::approve::cmd(), - re_cmds::get::cmd(), - re_cmds::remove::cmd(), - re_cmds::top::cmd(), - re_cmds::update::cmd(), - re_cmds::vote::cmd(), - re_cmds::shorturl::cmd(), - re_cmds::admin_bind::cmd(), - ]); - } + let disabled = cfg["commands"]["disabled_categories"] + .as_array() + .unwrap() + .iter() + .filter_map(|v| v.as_str()) + .collect::>(); - cmds.extend([ - cmds::reload_cfg::cmd() - ]); + cmds.retain(|cmd| !disabled.contains(&cmd.category.as_ref().unwrap().as_str())); - return cmds + return cmds; } \ No newline at end of file diff --git a/src/main.rs b/src/main.rs index a95d3b5..e559eb3 100644 --- a/src/main.rs +++ b/src/main.rs @@ -86,7 +86,7 @@ struct Data { ball_prompts: [Vec; 2], reddit_data: Mutex>, discord_data: Mutex>, - cfg: Mutex>, + cfg: Mutex>, bk_mods: Vec, args: Args, } diff --git a/src/python/bot.py b/src/python/bot.py index c622ce1..2c4c3c5 100644 --- a/src/python/bot.py +++ b/src/python/bot.py @@ -3,7 +3,7 @@ import asyncpraw as praw import os from typing import Final from macros import * -import json +import toml RE_DATA_POSTS: Final[str] = "posts" @@ -56,7 +56,7 @@ class Bot: return False async def update_cfg_str(self, new_cfg: str) -> bool: - json_cfg = json.loads(new_cfg) + json_cfg = toml.loads(new_cfg) self.sr = await self.r.subreddit(json_cfg[CFG_DATA_RE]["subreddits"]) self.fetch_limit = json_cfg[CFG_DATA_RE]["fetch_limit"] return True diff --git a/src/python/data.py b/src/python/data.py index 5d4a4fd..bfcc516 100644 --- a/src/python/data.py +++ b/src/python/data.py @@ -1,4 +1,5 @@ import os +import toml import json import time @@ -6,7 +7,10 @@ import bot as botPy from macros import * -DATA_PATH = os.path.abspath(os.path.join(os.path.join(os.getcwd(), "data"))) +DATA_PATH = os.path.join(os.path.join(os.getcwd(), "data")) +DB_PATH = os.path.join(DATA_PATH, "db") +DEFAULT_PATH = os.path.join(DATA_PATH, "default") +CFG_PATH = os.path.join(os.path.join(os.getcwd(), "cfg")) class PostData: @@ -77,7 +81,7 @@ class PostData: def read_data(bot: botPy.Bot) -> bool: - r_path = os.path.join(DATA_PATH, "re_data.json") + r_path = os.path.join(DB_PATH, "re_data.json") if os.path.isfile(r_path): bot.data_f = open(r_path, "r+") @@ -113,23 +117,23 @@ def write_data(bot: botPy.Bot) -> bool: async def read_cfg(bot: botPy.Bot) -> bool: - r_path = os.path.join(DATA_PATH, "cfg.json") + r_path = os.path.join(CFG_PATH, "cfg.toml") if os.path.isfile(r_path): bot.data_f = open(r_path, "r+") else: - py_print("cfg.json not found, creating new from preset...") - with open(os.path.join(DATA_PATH, "cfg_default.json", "r")) as f: - data_preset_json = json.load(f) + py_print("cfg.toml not found, creating new from preset...") + with open(os.path.join(DEFAULT_PATH, "cfg_default.toml", "r")) as f: + data_preset_json = toml.load(f) with open(r_path, "w") as f: - json.dump(data_preset_json, f, indent = 2) + toml.dump(data_preset_json, f, indent = 2) bot.data_f = open(r_path, "r+") data_str = bot.data_f.read() - json_data = json.loads(data_str) + json_data = toml.loads(data_str) await bot.update_cfg(json_data) return True diff --git a/src/re_cmds/generic_fns.rs b/src/re_cmds/generic_fns.rs index 1b0eed0..e8758f2 100644 --- a/src/re_cmds/generic_fns.rs +++ b/src/re_cmds/generic_fns.rs @@ -1,7 +1,7 @@ use regex::Regex; use serde_json::Value; -use crate::{data::get_mutex_data, messages::{make_post_embed, make_removed_embed, send_embed}, Context, Error}; +use crate::{data::get_toml_mutex, messages::{make_post_embed, make_removed_embed, send_embed}, Context, Error}; pub fn is_bk_mod(mod_list: Vec, uid: u64) -> bool { return mod_list.contains(&uid); @@ -35,7 +35,7 @@ pub async fn send_embed_for_removed(ctx: Context<'_>, url: &str, post: &Value) { pub async fn get_readable_subreddits(ctx: Context<'_>) -> Result { - let d = get_mutex_data(&ctx.data().cfg).await?; + let d = get_toml_mutex(&ctx.data().cfg).await.unwrap(); let sr = d["reddit"]["subreddits"].as_str().ok_or("Item of key \"subreddit\" is not a string type.\nTrace: `get_readable_subreddits -> let sr = ...`")?; let split: Vec<&str> = sr.split("+").collect(); let join = split.join(", r/"); diff --git a/src/re_cmds/shorturl.rs b/src/re_cmds/shorturl.rs index f22b6b1..a30f50d 100644 --- a/src/re_cmds/shorturl.rs +++ b/src/re_cmds/shorturl.rs @@ -5,6 +5,7 @@ use crate::{lang, messages::send_msg, re_cmds::generic_fns::to_shorturl, Context slash_command, prefix_command, rename = "re_shorturl", + category = "re", required_bot_permissions = "SEND_MESSAGES | VIEW_CHANNEL" )] /// Convert a long reddit URL to a short one. The bot ONLY uses shortURLs when asking for one.