i kinda forgot to commit. Anyways, i made the config into toml, edited readme, and... refactored code...

This commit is contained in:
2025-06-07 13:14:50 +02:00
parent c0793562e3
commit 4c21b6da4e
20 changed files with 149 additions and 101 deletions
+6 -6
View File
@@ -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
**/reddit_data.json
**/discord_data.json
**/re_data.json
**/dc_data.json
**/cfg.toml
+1
View File
@@ -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"
+41 -26
View File
@@ -1,3 +1,4 @@
<!-- If for some reason you're reading this without MD formatting - please disable word-wrap for your own good. -->
# 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.
View File
-8
View File
@@ -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
}
}
+19
View File
@@ -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"
]
+5
View File
@@ -0,0 +1,5 @@
asyncpraw
asyncprawcore
emoji
toml
websockets
+4 -4
View File
@@ -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;
+23 -11
View File
@@ -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<Option<Value>>) -> Result<Value, Error>
}
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);
+2 -4
View File
@@ -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));
}
+30 -29
View File
@@ -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<u64>) -> Data {
let ball_classic_str = std::fs::read_to_string("./data/8-ball_classic.txt").unwrap();
let ball_quirk_str = std::fs::read_to_string("./data/8-ball_quirky.txt").unwrap();
let ball_classic_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<String> = ball_classic_str.lines().map(String::from).collect();
let ball_quirk: Vec<String> = 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<Cmd> {
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::<Vec<_>>();
cmds.extend([
cmds::reload_cfg::cmd()
]);
cmds.retain(|cmd| !disabled.contains(&cmd.category.as_ref().unwrap().as_str()));
return cmds
return cmds;
}
+1 -1
View File
@@ -86,7 +86,7 @@ struct Data {
ball_prompts: [Vec<String>; 2],
reddit_data: Mutex<Option<Value>>,
discord_data: Mutex<Option<Value>>,
cfg: Mutex<Option<Value>>,
cfg: Mutex<Option<toml::Value>>,
bk_mods: Vec<u64>,
args: Args,
}
+2 -2
View File
@@ -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
+12 -8
View File
@@ -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
+2 -2
View File
@@ -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<u64>, 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<String, Error> {
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/");
+1
View File
@@ -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.