From 15eec7ada27dce464ac24340f3d458349c1b97ec Mon Sep 17 00:00:00 2001 From: s0lst1ce Date: Mon, 15 Feb 2021 20:41:57 +0100 Subject: [PATCH 1/9] initial commit --- .env.example | 10 + .gitignore | 7 +- Cargo.toml | 21 ++ deploy.sh | 87 ------- requirements.txt | 1 - src/checks.rs | 13 + src/commands/dev.rs | 92 ++++++++ src/commands/misc.rs | 19 ++ src/commands/mod.rs | 2 + src/config.py | 389 ------------------------------ src/exts/development.py | 79 ------- src/exts/embedding.py | 71 ------ src/exts/essentials.py | 269 --------------------- src/exts/poll.py | 373 ----------------------------- src/exts/role.py | 173 -------------- src/exts/slapping.py | 353 --------------------------- src/exts/time.py | 74 ------ src/exts/todo.py | 218 ----------------- src/help.py | 332 -------------------------- src/lang/config/help.en | 6 - src/lang/development/help.en | 5 - src/lang/development/strings.en | 3 - src/lang/embedding/help.en | 3 - src/lang/embedding/strings.en | 1 - src/lang/essentials/help.en | 6 - src/lang/essentials/strings.en | 17 -- src/lang/help/help.en | 12 - src/lang/help/strings.en | 3 - src/lang/poll/help.en | 7 - src/lang/poll/strings.en | 1 - src/lang/role/help.en | 7 - src/lang/role/strings.en | 10 - src/lang/slapping/help.en | 8 - src/lang/slapping/strings.en | 31 --- src/lang/time/help.en | 3 - src/lang/time/strings.en | 1 - src/main.py | 239 ------------------- src/main.rs | 161 +++++++++++++ src/settings.py | 206 ---------------- src/utilities.py | 407 -------------------------------- 40 files changed, 321 insertions(+), 3399 deletions(-) create mode 100644 .env.example create mode 100644 Cargo.toml delete mode 100644 deploy.sh delete mode 100644 requirements.txt create mode 100644 src/checks.rs create mode 100644 src/commands/dev.rs create mode 100644 src/commands/misc.rs create mode 100644 src/commands/mod.rs delete mode 100644 src/config.py delete mode 100644 src/exts/development.py delete mode 100644 src/exts/embedding.py delete mode 100644 src/exts/essentials.py delete mode 100644 src/exts/poll.py delete mode 100644 src/exts/role.py delete mode 100644 src/exts/slapping.py delete mode 100644 src/exts/time.py delete mode 100644 src/exts/todo.py delete mode 100644 src/help.py delete mode 100644 src/lang/config/help.en delete mode 100644 src/lang/development/help.en delete mode 100644 src/lang/development/strings.en delete mode 100644 src/lang/embedding/help.en delete mode 100644 src/lang/embedding/strings.en delete mode 100644 src/lang/essentials/help.en delete mode 100644 src/lang/essentials/strings.en delete mode 100644 src/lang/help/help.en delete mode 100644 src/lang/help/strings.en delete mode 100644 src/lang/poll/help.en delete mode 100644 src/lang/poll/strings.en delete mode 100644 src/lang/role/help.en delete mode 100644 src/lang/role/strings.en delete mode 100644 src/lang/slapping/help.en delete mode 100644 src/lang/slapping/strings.en delete mode 100644 src/lang/time/help.en delete mode 100644 src/lang/time/strings.en delete mode 100644 src/main.py create mode 100644 src/main.rs delete mode 100644 src/settings.py delete mode 100644 src/utilities.py diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..7b013e3 --- /dev/null +++ b/.env.example @@ -0,0 +1,10 @@ +# This declares an environment variable named "DISCORD_TOKEN" with the given +# value. When calling `kankyo::load()`, it will read the `.env` file and parse +# these key-value pairs and insert them into the environment. +# +# Environment variables are separated by newlines and must not have space +# around the equals sign (`=`). +DISCORD_TOKEN=put your token here +# Declares the level of logging to use. Read the documentation for the `log` +# and `env_logger` crates for more information. +RUST_LOG=debug \ No newline at end of file diff --git a/.gitignore b/.gitignore index 161936e..3549fae 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,3 @@ -*.log -*.pyc -*.json -OLD* +/target +Cargo.lock +.env \ No newline at end of file diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 0000000..a897d51 --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,21 @@ +[package] +name = "botanist" +version = "0.1.0" +authors = ["s0lst1ce "] +edition = "2018" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] +dotenv = "0.15" +tracing = "0.1.23" +tracing-subscriber = "0.2" + + +[dependencies.serenity] +version = "0.10" +features = ["framework", "standard_framework", "rustls_backend", "cache"] + +[dependencies.tokio] +version = "1.0" +features = ["macros", "rt-multi-thread", "signal"] \ No newline at end of file diff --git a/deploy.sh b/deploy.sh deleted file mode 100644 index df2c65d..0000000 --- a/deploy.sh +++ /dev/null @@ -1,87 +0,0 @@ -#!/bin/bash - -git_repo="https://github.com/s0lst1ce/Botanist.git" -name="Botanist" -#home_url="https://github.com/s0lst1ce/Botanist" - -helpFunction(){ - echo "" - echo "Usage: $0 TOKEN" - echo " -h shows this help message" - echo " -n name of the bot. Default is Botanist" - echo " -u home url for the bot" - echo " -p set the prefix the bot should listen to" - exit 0 -} - -[ $# -eq 0 ] && helpFunction - -#parsing options -while getopts ":hn:u:p:" option; do - case $option in - h | help ) - helpFunction;; - - n | name ) - name=$OPTARG;; - - u ) - home_url=$OPTARG;; - - p ) - prefix=$OPTARG;; - - \? ) - echo "Invalid option -$OPTARG" - helpFunction;; - - \: ) echo "Missing argument for -$OPTARG" - helpFunction;; - - \- ) echo "Long options are not yet supported!";; - - esac - #shift -done - -#installing the bot -echo "Downloading bot into $name." -git clone $git_repo $name &> /dev/null - -#setting TOKEN as env var -token=$1 -if [[ ! -z $DISCORD_TOKEN ]]; then - echo "Environement variable DISCORD_TOKEN already exists! Please clear it before proceeding to installation of the bot." - exit 1 -fi -echo -e "DISCORD_TOKEN=$token\n" >> ~/bashrc_testing - -#configuring the bot -echo "Applying configuration." -base_path="$name/src/" -settings_path="$name/src/settings.py" - -if [[ ! -z $prefix ]]; then - echo "yup" - sed -i "s/PREFIX\(.*\)/PREFIX = '$prefix'/" $settings_path #-i is not standard POSIX, merely GNU sed -fi - -if [[ ! -z $home_url ]]; then - sed -i "s/WEBSITE\(.*\)/WEBSITE = '$home_url'/" $settings_path -fi - -if ! command -v python3 &> /dev/null; then - echo "Python3 is a required dependency of the bot. If you are sure you have it make sure it points to python3" - exit 1 -fi - -echo "Making python dpy virtual environement." -python3 -m venv $name/dpy -source $name/dpy/bin/activate - -echo "Installing dependencies." -python3 -m pip install -r $base_path/requirements.txt -deactivate - -echo "Finished installing $name. To start it run: source $name/dpy/bin/activate && python3 $base_path/main.py" -exit 0 \ No newline at end of file diff --git a/requirements.txt b/requirements.txt deleted file mode 100644 index 2f0c700..0000000 --- a/requirements.txt +++ /dev/null @@ -1 +0,0 @@ -discord.py >= 1.3.2 diff --git a/src/checks.rs b/src/checks.rs new file mode 100644 index 0000000..62a6da6 --- /dev/null +++ b/src/checks.rs @@ -0,0 +1,13 @@ +use serenity::framework::standard::{macros::check, Args, CommandOptions, Reason}; +use serenity::model::channel::Message; +use serenity::prelude::*; + +#[check] +async fn is_manager( + _: &Context, + _msg: &Message, + _: &mut Args, + _: &CommandOptions, +) -> Result<(), Reason> { + unimplemented!() +} diff --git a/src/commands/dev.rs b/src/commands/dev.rs new file mode 100644 index 0000000..0fcc040 --- /dev/null +++ b/src/commands/dev.rs @@ -0,0 +1,92 @@ +use crate::ShardManagerContainer; +use serenity::model::prelude::*; +use serenity::prelude::*; +use serenity::{ + client::bridge::gateway::ShardId, + framework::standard::{ + macros::{command, group}, + CommandResult, + }, +}; + +#[group] +#[commands(shutdown, latency)] +struct Development; + +#[command] +#[owners_only] +async fn shutdown(ctx: &Context, msg: &Message) -> CommandResult { + let data = ctx.data.read().await; + + if let Some(manager) = data.get::() { + msg.reply(ctx, "Shutting down!").await?; + manager.lock().await.shutdown_all().await; + } else { + msg.author + .dm(ctx, |m| { + { + { + m.content("There was a problem getting the sard manager. ") + } + } + }) + .await?; + return Ok(()); + } + Ok(()) +} + +#[command] +#[owners_only] +async fn latency(ctx: &Context, msg: &Message) -> CommandResult { + // The shard manager is an interface for mutating, stopping, restarting, and + // retrieving information about shards. + let data = ctx.data.read().await; + + let shard_manager = match data.get::() { + Some(v) => v, + None => { + msg.reply(ctx, "There was a problem getting the shard manager") + .await?; + + return Ok(()); + } + }; + + let manager = shard_manager.lock().await; + let runners = manager.runners.lock().await; + + // Shards are backed by a "shard runner" responsible for processing events + // over the shard, so we'll get the information about the shard runner for + // the shard this command was sent over. + let runner = match runners.get(&ShardId(ctx.shard_id)) { + Some(runner) => runner, + None => { + msg.reply(ctx, "No shard found").await?; + + return Ok(()); + } + }; + + msg.reply(ctx, &format!("The shard latency is {:?}", runner.latency)) + .await?; + + Ok(()) +} + +#[command] +#[owners_only] +//Allows owners to send messages to all owners of the guilds the bot is in +async fn update(ctx: &Context, msg: &Message) -> CommandResult { + for guild in ctx.cache.guilds().await { + guild + .to_partial_guild(ctx) + .await? + .owner_id + .to_user(ctx) + .await? + .dm(ctx, |m| m.content(&msg.content)) + .await?; + } + Ok(()) +} diff --git a/src/commands/misc.rs b/src/commands/misc.rs new file mode 100644 index 0000000..42ec6fa --- /dev/null +++ b/src/commands/misc.rs @@ -0,0 +1,19 @@ +use serenity::framework::standard::{ + macros::{command, group}, + CommandResult, +}; +use serenity::model::prelude::*; +use serenity::prelude::*; + +#[group] +#[commands(ping)] +struct Misc; + +#[command] +// Limit command usage to guilds. +#[only_in(guilds)] +async fn ping(ctx: &Context, msg: &Message) -> CommandResult { + msg.channel_id.say(&ctx.http, "Pong!").await?; + + Ok(()) +} diff --git a/src/commands/mod.rs b/src/commands/mod.rs new file mode 100644 index 0000000..20ef5f7 --- /dev/null +++ b/src/commands/mod.rs @@ -0,0 +1,2 @@ +pub mod dev; +pub mod misc; diff --git a/src/config.py b/src/config.py deleted file mode 100644 index cbb8cec..0000000 --- a/src/config.py +++ /dev/null @@ -1,389 +0,0 @@ -import logging -import discord -import asyncio -import os -from settings import * -from utilities import * - -######################################### -# # -# # -# Setting up logging # -# # -# # -######################################### -local_logger = logging.getLogger(__name__) -local_logger.setLevel(LOGGING_LEVEL) -local_logger.addHandler(LOGGING_HANDLER) -local_logger.info("Innitalized {} logger".format(__name__)) - - -######################################### -# # -# # -# Making commands # -# # -# # -######################################### - - -class MendatoryConfigEntries(ConfigEntry): - """docstring for ClearanceConfigEntry""" - - def __init__(self, bot, cfg_chan_id): - super().__init__(bot, cfg_chan_id) - - def is_valid(self, lang): - if lang in ALLOWED_LANGS: - return True - else: - return False - - async def run(self, ctx): - try: - # LANGUAGE CONFIG - good = False - while not good: - lang = await self.get_answer( - ctx, - f"I'm an international robot and tend to opperate in many places. This also means that I speak many language! The list of supported languages can be found on my website {WEBSITE}. So which do you want to speak with? Languages are expressed in their 2 letter code. You can choose from this list: {ALLOWED_LANGS}", - ) - if not self.is_valid(lang.content): - continue - good = True - await ctx.send( - f"You have selected {lang.content}. Glad you could find a language that suits you! If you think the translation is incomplete or could be improved, feel free to improve it. The translations are open to everyone on our {WEBSITE}." - ) - - with ConfigFile(ctx.guild.id) as conf: - conf["lang"] = lang.content - - # ROLE CONFIG - await self.config_channel.send( - "Role setup is **mandatory** for the bot to work correctly. Otherwise no one will be able to use administration commands." - ) - await self.config_channel.send( - "**\nStarting role configuration**\nThis bot uses two level of clearance for its commands.\nThe first one is the **manager** level of clearance. Everyone with a role with this clearance can use commands related to server management. This includes but is not limited to message management and issuing warnings.\nThe second level of clearance is **admin**. Anyone who has a role with this level of clearance can use all commands but the ones related to the bot configuration. This is reserved to the server owner. All roles with this level of clearance inherit **manager** clearance as well." - ) - - new_roles = [] - for role_lvl in ROLES_LEVEL: - retry = True - while retry: - new_role = [] - # asking the owner which roles he wants to give clearance to - pre_roles = await self.get_answer( - ctx, - f"List all the roles you want to be given the **{role_lvl}** level of clearance.", - ) - - # converting to Role obj - roles = [] - for role in pre_roles.content.split(" "): - try: - roles.append( - await discord.ext.commands.RoleConverter().convert( - ctx, role - ) - ) - except: - continue - - # making sure at least a role was selected - if len(roles) == 0: - await self.config_channel.send( - f"You need to set at least one role for the {role_lvl} clearance." - ) - continue - - # building role string - roles_str = "" - for role in roles: - roles_str += f" {role.name}" - - # asking for confirmation - confirmed = await self.get_yn( - ctx, - f"You are about to give{roles_str} roles the **{role_lvl}** level of clearance. Do you confirm this ?", - ) - if not confirmed: - again = await self.get_yn( - ctx, - f"Aborting configuration of {role_lvl}. Do you want to retry?", - ) - if not again: - local_logger.info( - f"The configuration for the {role_lvl} clearance has been cancelled for server {ctx.guild.name}" - ) - retry = False - - else: - retry = False - - local_logger.info( - f"Server {ctx.guild.name} configured its {role_lvl} roles" - ) - for role in roles: - new_role.append(role.id) - - # adding to master role list - new_roles.append(new_role) - - # giving admin roles the manager clearance - for m_role in new_roles[1]: - new_roles[0].append(m_role) - - with ConfigFile(ctx.guild.id) as conf: - conf["roles"]["manager"] = new_roles[0] - conf["roles"]["admin"] = new_roles[1] - - await self.config_channel.send("Successfully updated role configuration") - - except Exception as e: - raise e - - -class Config(commands.Cog, ConfigEntry): - """a suite of commands meant ot give server admins/owners and easy way to setup the bot's - preferences directly from discord.""" - - def __init__(self, bot): - self.config_entry = MendatoryConfigEntries - self.config_channels = {} - self.bot = bot - self.allowed_answers = {1: ["yes", "y"], 0: ["no", "n"]} - - @commands.Cog.listener() - async def on_guild_join(guild): - await self.make_cfg_chan(guild) - - async def make_cfg_chan(self, ctx_or_guild): - if type(ctx_or_guild) == discord.Guild: - g = ctx_or_guild - else: - g = ctx_or_guild.guild - overwrite = { - g.default_role: discord.PermissionOverwrite(read_messages=False), - g.owner: discord.PermissionOverwrite(read_messages=True), - } - self.config_channels[g.id] = await g.create_text_channel( - "cli-bot-config", overwrites=overwrite - ) - if f"{g.id}.json" not in os.listdir(CONFIG_FOLDER): - # making sure there's a file to write on but don't overwrite if there's already one. - with open(os.path.join(CONFIG_FOLDER, str(g.id) + ".json"), "w") as file: - json.dump(DEFAULT_SERVER_FILE, file) - - with open(os.path.join(SLAPPING_FOLDER, str(g.id) + ".json"), "w") as file: - json.dump(DEFAULT_SLAPPED_FILE, file) - - return self.config_channels[g.id] - - @commands.command() - @has_auth("admin") - async def cfg(self, ctx, cog_name: str): - cog = self.bot.get_cog(cog_name.title()) - if not cog: - local_logger.debug( - f"{ctx.author} tried to configure {cog_name} which doesn't exist." - ) - raise discord.ext.commands.errors.ArgumentParsingError( - message=f"{cog_name} cog doesn't exist." - ) - return - - if cog.config_entry: - try: - self.config_channel = await self.make_cfg_chan(ctx) - ctx.channel = self.config_channel - await cog.config_entry(self.bot, self.config_channel).run(ctx) - - finally: - await self.config_channels[ctx.guild.id].send( - f"You Successfully configured {cog.qualified_name}.\nThis channel will be deleted in 10 seconds..." - ) - await asyncio.sleep(10) - await self.config_channels[ctx.guild.id].delete( - reason="Configuration completed" - ) - - else: - await ctx.send("{cog.qualified_name} doesn't have any configuration entry!") - - @commands.command() - @is_server_owner() - async def init(self, ctx): - # creating new hidden channel only the owner/admins can see - self.config_channel = await self.make_cfg_chan(ctx) - ctx.channel = self.config_channel - - try: - # starting all configurations - await self.config_channels[ctx.guild.id].send( - f"""You are about to start the configuration of {ctx.me.mention}. If you are unfamiliar with CLI (Command Line Interface) you may want to check the documentation on github ({WEBSITE}). The same goes if you don't know the bot's functionnalities""" - ) - - await self.config_channels[ctx.guild.id].send( - "**Starting full bot configuration...**" - ) - - for cog in self.bot.cogs: - if self.bot.cogs[cog].config_entry: - await self.bot.cogs[cog].config_entry( - self.bot, self.config_channel - ).run(ctx) - - # asking for permisison to advertise - await self.config_channels[ctx.guild.id].send( - "You're almost done ! Just one more thing..." - ) - allowed = await self.get_yn( - ctx, - "Do you allow me to send a message in a channel of your choice? This message would give out a link to my development server. It would allow me to get more feedback. This would really help me pursue the development of the bot. If you like it please think about it.", - ) - ad_msg = discord.Embed( - description="I ({}) have recently been added to this server! I hope I'll be useful to you. Hopefully you won't find me too many bugs. However if you do I would appreciate it if you could report them to the [server]({}) where my developers are ~~partying~~ working hard to make me better. This is also the place to share your thoughts on how to improve me. Have a nice day and hopefully, see you there {}".format( - ctx.me.mention, DEV_SRV_URL, EMOJIS["wave"] - ) - ) - if not allowed: - return False - - chan = await self.get_answer( - ctx, - "Thank you very much ! In which channel do you want me to post this message?", - filters=["channels"], - ) - - with ConfigFile(ctx.guild.id) as conf: - conf["advertisement"] = chan[0].id - - await chan[0].send(embed=ad_msg) - - local_logger.info( - f"Setup for server {ctx.guild.name}({ctx.guild.id}) is done" - ) - - except Exception as e: - raise e - await ctx.send(embed=get_embed_err(ERR_UNEXCPECTED.format(str(e)))) - await ctx.send( - "Dropping configuration and rolling back unconfirmed changes." - ) - local_logger.exception(e) - - finally: - await self.config_channels[ctx.guild.id].send( - "Thank you for inviting our bot and taking the patience to configure it.\nThis channel will be deleted in 10 seconds..." - ) - await asyncio.sleep(10) - await self.config_channels[ctx.guild.id].delete( - reason="Configuration completed" - ) - - @commands.command() - @has_auth("admin") - async def summary(self, ctx): - config = ConfigFile(ctx.guild.id).read() - - config_sum = discord.Embed( - title="Server settings", - description=f"""This server uses `{config["lang"]}` language and have set advertisement policy to **{config["advertisement"]}**.""", - color=7506394, - ) - - # messages - messages = "" - for msg_type in config["messages"]: - if config["messages"][msg_type]: - messages += ( - f"""**{msg_type.title()}:**\n{config["messages"][msg_type]}\n""" - ) - - if len(messages) == 0: - messages = ( - "No **welcome** or **goodbye** messages were set for this server." - ) - config_sum.add_field(name="Messages", value=messages, inline=False) - - # community moderation - if config["commode"]["reports_chan"]: - try: - value = await discord.ext.commands.TextChannelConverter().convert( - ctx, str(config["commode"]["reports_chan"]) - ) - value = "The channel for reports is:" + value.mention + "\n" - except Exception as e: - local_logger.exception( - f"The report channel for guild {ctx.guild.id} was deleted.", e - ) - # the channel was deleted - value = "The set report channel was deleted. You are advised to set a new one using `::cfg slapping`.\n" - else: - value = "No report channel has been set. You are advised to set one using `::cfg slapping`.\n" - - value += f"""A user is automatically muted in a channel for 10 minutes after **{config["commode"]["spam"]["mute"]}** `spam` reports.""" - config_sum.add_field(name="Community moderation", value=value, inline=True) - - # polls - if len(config["poll_channels"]) > 0: - chans = "The **poll channels** are:" - for chan in config["poll_channels"]: - try: - crt_chan = await discord.ext.commands.TextChannelConverter().convert( - ctx, str(chan) - ) - except Exception as e: - # the channel was deleted - local_logger.exception( - f"A poll channel of guild {ctx.guild.id} was deleted." - ) - continue - - chans += crt_chan.mention - else: - chans = "There are no **poll channels** for this server." - config_sum.add_field(name="Poll Channels", value=chans, inline=True) - - # clearance/roles - clearance = "" - for level in config["roles"]: - clearance += f"**{level.title()}:**\n" - for role_id in config["roles"][level]: - try: - crt_role = await discord.ext.commands.RoleConverter().convert( - ctx, str(role_id) - ) - except Exception as e: - # the role doesn't exist anymore - local_logger.exception( - f"The role associated with {level} clearance doesn't exist anymiore", - e, - ) - continue - clearance += " " + crt_role.mention - clearance += "\n" - - if config["free_roles"]: - clearance += "**Free roles:**\n" - for role_id in config["free_roles"]: - try: - crt_role = await discord.ext.commands.RoleConverter().convert( - ctx, str(role_id) - ) - except Exception as e: - # the role doesn't exist anymore - local_logger.exception(f"The free role doesn't exist anymiore", e) - continue - clearance += " " + crt_role.mention - - else: - clearance += "There are no **free roles** in this server." - - config_sum.add_field(name="Clearance", value=clearance, inline=True) - - await ctx.send(embed=config_sum) - - -def setup(bot): - bot.add_cog(Config(bot)) diff --git a/src/exts/development.py b/src/exts/development.py deleted file mode 100644 index 693c43a..0000000 --- a/src/exts/development.py +++ /dev/null @@ -1,79 +0,0 @@ -import discord -import json -import logging -from settings import * -from utilities import * - -######################################### -# # -# # -# Setting up logging # -# # -# # -######################################### -local_logger = logging.getLogger(__name__) -local_logger.setLevel(LOGGING_LEVEL) -local_logger.addHandler(LOGGING_HANDLER) -local_logger.info("Innitalized {} logger".format(__name__)) - - -######################################### -# # -# # -# Making commands # -# # -# # -######################################### - -name = __name__.split(".")[-1] - - -class Development(commands.Cog): - """A suite of commands meant to let users give feedback about the bot: whether it's suggestions or bug reports. - It's also meant to let server owners know when there's an update requiring their attention.""" - - def __init__(self, bot): - self.bot = bot - self.config_entry = None - - @commands.command() - @is_runner() - async def update(self, ctx, *words): # should message be put in settings.py ? - """Lets the owner of the bot update the bot from github's repositery. It also sends a notification to all server owners who use the bot. The message sent in the notification is the description of the release on github. - NB: as of now the bot only sends a generic notification & doesn't update the bot.""" - # building message - if len(words) == 0: - message = DEFAULT_UPDATE_MESSAGE - else: - message = "" - for w in words: - message += f" {w}" - - owners = [] - for g in self.bot.guilds: - if g.owner not in owners: - owners.append(g.owner) - - for o in owners: - await o.send(message) - - @commands.command() - @is_runner() # -> this needs to be changed to is_dev() - async def log(self, ctx): - """returns the bot's log as an attachement""" - # getting the log - with open(LOG_FILE, "r") as file: - log = discord.File(file) - - # sending the log - await ctx.send(file=log) - - @commands.command() - async def dev(self, ctx): - """sends the developement server URL to the author of the message""" - tr = Translator(name, get_lang(ctx)) - await ctx.author.send(tr["dev"] + DEV_SRV_URL) - - -def setup(bot): - bot.add_cog(Development(bot)) diff --git a/src/exts/embedding.py b/src/exts/embedding.py deleted file mode 100644 index b124c5b..0000000 --- a/src/exts/embedding.py +++ /dev/null @@ -1,71 +0,0 @@ -import logging -import discord -import io -from settings import * -from utilities import * - -######################################### -# # -# # -# Setting up logging # -# # -# # -######################################### -local_logger = logging.getLogger(__name__) -local_logger.setLevel(LOGGING_LEVEL) -local_logger.addHandler(LOGGING_HANDLER) -local_logger.info("Innitalized {} logger".format(__name__)) - - -######################################### -# # -# # -# Making commands # -# # -# # -######################################### - - -class Embedding(commands.Cog): - """A suite of command providing users with embeds manipulation tools.""" - - def __init__(self, bot): - self.bot = bot - self.config_entry = None - # maybe think to block sending an embed in a poll channel - - @commands.command() - async def embed(self, ctx, *args): - """allows you to post a message as an embed. Your msg will be reposted by the bot as an embed ! - NOTE: Does not support aliases!""" - with ConfigFile(ctx.guild.id) as conf: - poll_chans = conf["poll_channels"] - if ctx.channel.id in poll_chans: - local_logger.info("Preventing user from making an embed in a poll channel") - await ctx.message.delete() - return - - # lining attachements - files = [] - for attachment in ctx.message.attachments: - content = await attachment.read() - io_content = io.BytesIO(content) - file = discord.File(io_content, filename=attachment.filename) - files.append(file) - - embed_msg = discord.Embed( - title=None, - description=str(ctx.message.content[8:]), - colour=ctx.author.color, - url=None, - ) - embed_msg.set_author( - name=ctx.message.author.name, icon_url=ctx.message.author.avatar_url - ) - - await ctx.message.delete() - await ctx.message.channel.send(embed=embed_msg, files=files) - - -def setup(bot): - bot.add_cog(Embedding(bot)) diff --git a/src/exts/essentials.py b/src/exts/essentials.py deleted file mode 100644 index 06fcf7c..0000000 --- a/src/exts/essentials.py +++ /dev/null @@ -1,269 +0,0 @@ -"""Essential features all bot built with this template should have. -Do note that disabling it will cause issues to the config extension.""" -import datetime -from os import listdir -import logging -import discord -from settings import * -from utilities import * - - -######################################### -# # -# # -# Setting up logging # -# # -# # -######################################### -local_logger = logging.getLogger(__name__) -local_logger.setLevel(LOGGING_LEVEL) -local_logger.addHandler(LOGGING_HANDLER) -local_logger.info("Innitalized {} logger".format(__name__)) - - -######################################### -# # -# # -# BotEssentials # -# # -# # -######################################### -"""This cog contains all the basemost functions that all bots should contain. -See https://github.com/s0lst1ce/Botanist for more information""" - -name = __name__.split(".")[-1] - - -class EssentialsConfigEntry(ConfigEntry): - """docstring for EssentialsConfigEntry""" - - def __init__(self, bot, cfg_chan_id): - super().__init__(bot, cfg_chan_id) - - async def run(self, ctx): - # welcome & goodbye messages - tr = Translator(name, get_lang(ctx)) - msgs = { - "welcome": [tr["welcome1"], tr["welcome2"]], - "goodbye": [tr["goodbye1"], tr["goodbye2"]], - } - try: - for wg in msgs: - await self.config_channel.send(tr["start_conf"].format(wg)) - retry = await self.get_yn(ctx, msgs[wg][0]) - message = False - - while retry: - - message = await self.get_answer(ctx, msgs[wg][1]) - - await self.config_channel.send(tr["send_check"]) - await self.config_channel.send( - message.content.format(ctx.guild.owner.mention) - ) - response = await self.get_yn(ctx, tr["response"].format(wg)) - # the user has made a mistake - if response == False: - response = await self.get_yn(ctx, tr["retry"]) - if response == False: - message = False - retry = False - # otherwise retry - continue - - else: - retry = False - - if message == False: - return - with ConfigFile(ctx.guild.id) as conf: - conf["messages"][wg] = message.content - - except Exception as e: - local_logger.exception(e) - raise e - - -class Essentials(commands.Cog): - """All of the essential methods all of our bots should have""" - - def __init__(self, bot): - self.bot = bot - self.config_entry = EssentialsConfigEntry - - @commands.Cog.listener() - async def on_command_error(self, ctx, error): - """handles command errors""" - #raise error - local_logger.error(error) - if type(error) in ERRS_MAPPING.keys(): - msg = get_embed_err(ERRS_MAPPING[type(error)]) - if ERRS_MAPPING[error][2] is False: - await ctx.send(embed=msg) - else: - await ctx.author.send(embed=msg) - await ctx.message.delete() - else: - await ctx.send(embed=get_embed_err(ERR_UNEXCPECTED)) - - @commands.Cog.listener() - async def on_guild_join(self, guild): - with open(os.path.join(CONFIG_FOLDER, f"{guild.id}.json"), "w") as file: - json.dump(DEFAULT_SERVER_FILE) - await guild.owner.send( - f"I was just added to your server. For me to work correctly (or at all) on your server you should send `::init` in any channel of your {guild.name} server." - ) - local_logger.info(f"Joined server {guild.name}") - - @commands.Cog.listener() - async def on_guild_leave(self, guild): - remove(os.path.join(CONFIG_FOLDER, str(guild.id) + ".json")) - - @commands.Cog.listener() - async def on_ready(self): - print("Logged in as {0.user}".format(self.bot)) - local_logger.info("Logged in as {0.user}".format(self.bot)) - - @commands.Cog.listener() - async def on_member_join(self, member): - local_logger.info( - "User {0.name}[{0.id}] joined {1.name}[{1.id}]".format(member, member.guild) - ) - with ConfigFile(member.guild.id) as conf: - welcome_msg = conf["messages"]["welcome"] - if welcome_msg != False: - await member.guild.system_channel.send(welcome_msg.format(member.mention)) - - @commands.Cog.listener() - async def on_member_remove(self, member): - local_logger.info( - "User {0.name}[{0.id}] left {1.name}[{1.id}]".format(member, member.guild) - ) - with ConfigFile(member.guild.id) as conf: - goodbye_msg = conf["messages"]["goodbye"] - if goodbye_msg != False: - await member.guild.system_channel.send(goodbye_msg.format(member.mention)) - - @commands.command() - async def ping(self, ctx): - """This command responds with the current latency.""" - tr = Translator(name, get_lang(ctx)) - latency = self.bot.latency - await ctx.send(EMOJIS["ping_pong"] + tr["latency"].format(latency)) - - # Command that shuts down the bot - @commands.command(aliases=["poweroff"]) - @is_runner() - async def shutdown(self, ctx): - print("Goodbye") - local_logger.info("Switching to invisible mode") - await self.bot.change_presence(status=discord.Status.offline) - await ctx.send(f"""Going to sleep {EMOJIS["sleeping"]}""") - local_logger.info("Closing connection to discord") - await self.bot.close() - local_logger.info("Quitting python") - await quit() - - @commands.command() - @has_auth("manager") - async def clear(self, ctx, *filters): - """deletes specified number of messages in the current channel""" - # building arguments - filters = list(filters) - nbr = None - period = None - members = [] - try: - nbr = int(filters[0]) - filters.pop(0) - except: - pass - - if filters: - period = to_datetime(filters[0]) - if period: - filters.pop(0) - - for m in filters: - try: - members.append( - await discord.ext.commands.MemberConverter().convert(ctx, m) - ) - except: - raise discord.ext.commands.ArgumentParsingError( - f"Unrecognized filter {m}" - ) - - hist_args = {} - if period: - hist_args["after"] = period - if nbr and not members: - hist_args["limit"] = nbr + 1 - if not (period or nbr): - raise discord.ext.commands.ArgumentParsingError( - "Can't delete all messages of a user!" - ) - - to_del = [] - now = datetime.datetime.now() - async for msg in ctx.channel.history(**hist_args): - if not members or msg.author in members: - local_logger.info( - f"Deleting message {msg.jump_url} from guild {msg.guild.name}." - ) - if (msg.created_at - now).days >= -14: - await msg.delete() - else: - to_del.append(msg) - - if nbr != None: - if nbr <= 0: - break - nbr -= 1 - - try: - await ctx.channel.delete_messages(to_del) - - except discord.HTTPException as e: - raise e - - except Exception as e: - local_logger.exception("Couldn't delete at least on of{}".format(to_del)) - raise e - - @commands.command() - async def status(self, ctx): - """returns some statistics about the server and their members""" - tr = Translator(name, get_lang(ctx)) - stats = discord.Embed( - name=tr["stats_name"], - description=tr["stats_description"].format( - ctx.guild.name, - str(ctx.guild.created_at)[:10], - ctx.guild.owner.mention, - ctx.guild.member_count - 1, - ), - color=7506394, - ) - stats.set_thumbnail(url=ctx.guild.icon_url) - - # member stats - mstatus = {"online": 0, "idle": 0, "dnd": 0, "offline": 0} - for member in ctx.guild.members: - mstatus[str(member.status)] += 1 - # -> change to make use of custom emojis - status_str = tr["status_str"].format(**mstatus) - stats.add_field(name=tr["mstats_name"], value=status_str, inline=False) - - # structure info - rs = ctx.guild.roles - rs.reverse() - rs_str = "" - for r in rs: - rs_str += f"{r.mention}" - stats.add_field(name=tr["sstats_name"], value=rs_str, inline=False) - await ctx.send(embed=stats) - - -def setup(bot): - bot.add_cog(Essentials(bot)) diff --git a/src/exts/poll.py b/src/exts/poll.py deleted file mode 100644 index b5fa9a1..0000000 --- a/src/exts/poll.py +++ /dev/null @@ -1,373 +0,0 @@ -import os -import logging -import discord -import io -from settings import * -from utilities import * - -######################################### -# # -# # -# Setting up logging # -# # -# # -######################################### -local_logger = logging.getLogger(__name__) -local_logger.setLevel(LOGGING_LEVEL) -local_logger.addHandler(LOGGING_HANDLER) -local_logger.info("Innitalized {} logger".format(__name__)) - - -######################################### -# # -# # -# Making commands # -# # -# # -######################################### - -name = __name__.split(".")[-1] - - -class PollConfigEntry(ConfigEntry): - """docstring for PollConfigEntry""" - - def __init__(self, bot, cfg_chan_id): - super().__init__(bot, cfg_chan_id) - - async def run(self, ctx): - try: - tr = Translator(name, get_lang(ctx)) - await self.config_channel.send(tr["start_conf"]) - pursue = await self.get_yn(ctx, tr["pursue"]) - if not pursue: - return False - retry = True - - while retry: - # getting the list of channels to be marked as polls - poll_channels = await self.get_answer( - ctx, - tr["poll_channels"].format(self.config_channel.mention), - filters=["channels"], - ) - if self.config_channel in poll_channels: - await self.config_channel.send(tr["invalid_chan"]) - continue - - poll_channels_str = "" - for chan in poll_channels: - poll_channels_str += f"{chan.mention}," - poll_channels_str = poll_channels_str[:-1] - - confirmed = await self.get_yn( - ctx, tr["confirmed"].format(poll_channels_str) - ) - if not confirmed: - # making sure the user really wants to quit - drop = await self.get_yn(ctx, tr["drop"]) - if drop: - local_logger.info( - f"Poll configuration has been cancelled for server {ctx.guild.name}" - ) - retry = False - else: - retry = False - - poll_channels_ids = [] - for chan in poll_channels: - poll_channels_ids.append(chan.id) - - with ConfigFile(ctx.guild.id) as conf: - conf["poll_channels"] = poll_channels_ids - - await self.config_channel.send(tr["conf_done"]) - local_logger.info( - f"Configuration of poll for server {ctx.guild.name} ({ctx.guild.id}) has been completed." - ) - except Exception as e: - raise e - - -class Poll(commands.Cog): - """A suite of commands providing users with tools to more easilly get the community's opinion on an idea""" - - def __init__(self, bot): - self.bot = bot - self.config_entry = PollConfigEntry - - @commands.Cog.listener() - async def on_raw_reaction_add(self, payload): - """currently makes this checks for ALL channels. Might want to change the behavior to allow reactions on other msgs""" - # getting poll_allowed_chans - # @is_init - with ConfigFile(payload.guild_id) as conf: - poll_allowed_chans = conf["poll_channels"] - - # checking that user isn't the bot - if (payload.user_id != self.bot.user.id) and ( - payload.channel_id in poll_allowed_chans - ): - - # fetching concerned message and the user who added the reaction - message = await self.bot.get_channel(payload.channel_id).fetch_message( - payload.message_id - ) - user = self.bot.get_user(payload.user_id) - - if f"{message.id}.json" in os.listdir(POLL_FOLDER): - with ConfigFile(message.id, folder=POLL_FOLDER) as settings: - good = False - if payload.emoji.is_unicode_emoji(): - if str(payload.emoji) in settings["unicode"]: - good = True - else: - #this is a custom emoji - if payload.emoji.id in settings["custom"]: - good = True - if not good: - local_logger.debug("User tried to add some forbidden reaction to an extended poll.") - await message.remove_reaction(payload.emoji, user) - return - - # checking wether the reaction should delete the poll - if payload.emoji.name == EMOJIS["x"]: - if payload.user.name == message.embeds[0].title: - return message.delete() - else: - return message.remove_reaction(payload.emoji, user) - - # checking if reaction is allowed - elif payload.emoji.name not in [ - EMOJIS["thumbsdown"], - EMOJIS["thumbsup"], - EMOJIS["shrug"], - ]: - # deleting reaction of the user. Preserves other reactions - try: - # iterating over message's reactions to find out which was added - for reaction in message.reactions: - # testing if current emoji is the one just added - if reaction.emoji == payload.emoji.name: - # removing unauthorized emoji - await reaction.remove(user) - - except Exception as e: - local_logger.exception( - "Couldn't remove reaction {}".format("reaction") - ) - raise e - - # if the reaction is allowed -> recalculating reactions ratio and changing embed's color accordingly - else: - # preventing users from having multiple reactions - for reaction in message.reactions: - if reaction.emoji != payload.emoji.name: - await reaction.remove(user) - - # currently using integers -> may need to change to their values by checcking them one by one - react_for = message.reactions[0].count - react_against = message.reactions[2].count - # changing color of the embed - await self.balance_poll_color( - message, message.reactions[0].count, message.reactions[2].count - ) - - @commands.Cog.listener() - async def on_raw_reaction_remove(self, payload): - - # getting poll_allowed_chans - with ConfigFile(payload.guild_id) as conf: - poll_allowed_chans = conf["poll_channels"] - - # fetching concerned message and the user who added the reaction - message = await self.bot.get_channel(payload.channel_id).fetch_message( - payload.message_id - ) - if len(message.embeds): - # so we're not interacting with something that is not bot-related -> may still interact with other embeds, TODO - if len(message.embeds[0].fields) > 0: - # it's an extended embed, no need to treat it - return - - # checking that user isn't the bot - if (payload.user_id != self.bot.user.id) and ( - payload.channel_id in poll_allowed_chans - ): - # changing color of the embed - await self.balance_poll_color( - message, message.reactions[0].count, message.reactions[2].count - ) - - @commands.Cog.listener() - async def on_message(self, message): - if message.author == self.bot.user: - return - - if not was_init(message): - await message.channel.send(embed=get_embed_err(ERR_NOT_SETUP)) - return - - # getting poll_allowed_chans - # poll_allowed_chans = ConfigFile(message.guild.id)["poll_channels"] - with ConfigFile(message.guild.id) as conf: - poll_allowed_chans = conf["poll_channels"] - - if ( - message.channel.id in poll_allowed_chans - and message.content.startswith(PREFIX) != True - ): - - local_logger.info(f"Message {message} is a poll one") - # rebuilding attachements - files = [] - for attachment in message.attachments: - content = await attachment.read() - io_content = io.BytesIO(content) - file = discord.File(io_content, filename=attachment.filename) - files.append(file) - - mentions = message.mentions - roles = message.role_mentions - final = message.content - for men in mentions: - final = final.replace("<@" + str(men.id) + ">", men.name) - - for role in roles: - final = final.replace("<@" + str(role.id) + ">", role.name) - - # making embed - embed_poll = discord.Embed( - title=message.author.name, - description=final, - colour=discord.Color(16776960), - url=None, - ) - # embed_poll.set_author(name=message.author.name, icon_url=message.author.avatar_url) - embed_poll.set_thumbnail(url=message.author.avatar_url) - # embed_poll.set_footer(text=message.author.name, icon_url=message.author.avatar_url) - - # sending message & adding reactions - try: - await message.delete() - sent_msg = await message.channel.send(embed=embed_poll, files=files) - await sent_msg.add_reaction(EMOJIS["thumbsup"]) - await sent_msg.add_reaction(EMOJIS["shrug"]) - await sent_msg.add_reaction(EMOJIS["thumbsdown"]) - - except Exception as e: - local_logger.exception("Couldn't send and delete all reaction") - - async def balance_poll_color(self, msg, for_count, against_count): - r = g = 128 - diff = for_count - against_count - votes = for_count + against_count - r -= (diff / votes) * 64 - g += (diff / votes) * 64 - - # checking whether the number is over 255 - r = int(min(255, r)) - g = int(min(255, g)) - - color = int((r * 65536) + (g * 256)) - # getting messages's embed (there should only be one) - embed = msg.embeds[0].copy() - embed.color = color - await msg.edit(embed=embed) - - return msg - - @commands.group() - async def poll(self, ctx): - """a suite of commands that lets one have more control over polls""" - if ctx.invoked_subcommand == None: - local_logger.warning("User didn't provide any subcommand") - raise discord.ext.commands.MissingRequiredArgument( - "Group requires a subcommand" - ) - - @poll.command() - async def rm(self, ctx, msg_id): - """allows one to delete one of their poll by issuing its id""" - for chan in ctx.guild.text_channels: - try: - msg = await chan.fetch_message(msg_id) - break - - # if the message isn't in this channel - except discord.NotFound as e: - local_logger.info("poll isn't in {0.name}[{0.id}]".format(chan)) - - except Exception as e: - local_logger.exception("An unexpected error occured") - raise e - - # making sure that the message is a poll. doesn't work, any msg with a embed could be considered a poll - if len(msg.embeds) != 1: - return - # checking if the poll was created by the user. Is name safe enough ? - if msg.embeds[0].title == ctx.author.name: - try: - await msg.delete() - except Exception as e: - local_logger.exception("Couldn't delete poll".format(msg)) - raise e - - @poll.command() - async def status(self, ctx, msg_id: discord.Message): - """returns stats about your running polls. This is also called when one of you poll gets deleted.""" - pass - - @poll.command() - async def extended(self, ctx, *words): - """polls that can have more than the 3 standard reaction but do not support dynamic color. - the way to make one is to be write the following command in a poll channel (message discarded otherswise) - the message is composed of the description then a line break then, one each following line: - an emoji followed by a description - each of these lines are seperated by a line break - TODO: make sure message follows this strict format""" - with ConfigFile(ctx.guild.id) as conf: - poll_allowed_chans = conf["poll_channels"] - - # making sure it's a poll chan - if ctx.channel.id not in poll_allowed_chans: - await ctx.message.delete() - await ctx.author.send("You are not allowed to make polls in this channel.") - - # building the description - description_words, choices = ctx.message.content.split("\n", 1) - description_words = description_words.split(" ")[2:] - description = "" - for word in description_words: - description += f" {word}" - - # making embed - embed_poll = discord.Embed( - title=ctx.author.name, description=description, colour=7506394, - ) - embed_poll.add_field(name="Choices", value=choices) - embed_poll.set_thumbnail(url=ctx.author.avatar_url) - msg = await ctx.send(embed=embed_poll) - - # deleting user message - await ctx.message.delete() - - # getting emojis & react - emotes = {"unicode": [], "custom": []} - for choice in choices.split("\n"): - it = choice.split(" ", 1)[0] - if it.startswith("<"): - #this is a custom emoji - emotes["custom"].append(int(it.split(":", 2)[2][:-1])) - else: - emotes["unicode"].append(it) - await msg.add_reaction(it) - - # saving this for later times - with ConfigFile(msg.id, folder=POLL_FOLDER) as settings: - settings.data = emotes - print(settings) - - -def setup(bot): - bot.add_cog(Poll(bot)) diff --git a/src/exts/role.py b/src/exts/role.py deleted file mode 100644 index ad13009..0000000 --- a/src/exts/role.py +++ /dev/null @@ -1,173 +0,0 @@ -import logging -from settings import * -import discord -from utilities import * - -######################################### -# # -# # -# Setting up logging # -# # -# # -######################################### -local_logger = logging.getLogger(__name__) -local_logger.setLevel(LOGGING_LEVEL) -local_logger.addHandler(LOGGING_HANDLER) -local_logger.info("Innitalized {} logger".format(__name__)) - - -######################################### -# # -# # -# Making commands # -# # -# # -######################################### - -name = __name__.split(".")[-1] - - -class RoleConfigEntry(ConfigEntry): - """user can choose which roles are "free" """ - - def __init__(self, bot, config_chan_id): - super().__init__(bot, config_chan_id) - - async def run(self, ctx): - tr = Translator(name, get_lang(ctx)) - try: - await ctx.send(tr["start_conf"]) - free_roles = [] - pursue = await self.get_yn(ctx, tr["pursue"]) - while pursue: - proles = await self.get_answer(ctx, tr["proles"]) - roles = [] - for role in proles.content.split(" "): - try: - roles.append( - await discord.ext.commands.RoleConverter().convert( - ctx, role - ) - ) - except: - pass - # raise discord.ext.commands.ArgumentParsingError(f"Couldn't find role {role}") - - roles_str = "" - for role in roles: - roles_str += f" {role.name}" - agrees = await self.get_yn(ctx, tr["agrees"].format(roles_str)) - - if not agrees: - retry = await self.get_yn(ctx, tr["retry"]) - if retry: - continue - else: - pursue = False - - else: - pursue = False - with ConfigFile(ctx.guild.id) as conf: - conf["free_roles"] = [role.id for role in roles] - except: - raise - - -class Role(commands.Cog): - """role management utility. Requires a Gestion role""" - - def __init__(self, bot): - self.bot = bot - self.config_entry = RoleConfigEntry - - @commands.group() - async def role(self, ctx): - """role management utility. Requires a Gestion role""" - pass - - @role.command() - async def add(self, ctx, member: discord.Member, *roles: discord.Role): - """Gives listed roles""" - tr = Translator(name, get_lang(ctx)) - # checking if member can self-assing role(s) - if not has_auth("admin")(ctx): - allowed_roles = [] - with ConfigFile(ctx.guild.id) as conf: - for role in roles: - if str(role.id) in conf["free_roles"]: - allowed_roles.append(role) - else: - await ctx.send(tr["not_free_role"].format(role.name)) - - else: - allowed_roles = roles - - if len(allowed_roles) == 0: - local_logger.warning("User didn't provide a role") - raise discord.ext.commands.MissingRequiredArgument( - "You must provide at least one role." - ) - - else: - try: - await member.add_roles(*allowed_roles) - roles_str = "" - for role in allowed_roles: - roles_str += f" {role}" - - await ctx.send(tr["gave"].format(member.name, roles_str)) - except Exception as e: - local_logger.exception( - "Couldn't add {} to {}".format(allowed_roles, member) - ) - raise e - - @role.command() - async def rm(self, ctx, member: discord.Member, *roles: discord.Role): - """Removes 's roles""" - if len(roles) == 0: - local_logger.warning("User didn't provide a role") - raise discord.ext.commands.MissingRequiredArgument( - "You must provide at least one role." - ) - - else: - try: - await member.remove_roles(*roles) - except Exception as e: - local_logger.exception("Couldn't remove roles ") - raise e - - @role.command() - async def free(self, ctx): - """return a list of free role""" - free_roles = "" - with ConfigFile(ctx.guild.id) as conf: - for role in conf["free_roles"]: - try: - resolved = ctx.guild.get_role(role) - free_roles += f"{resolved.mention}\n" - - except discord.ext.commands.ConversionError as e: - raise e - local_logger.error( - "A free role couldn't be found, maybe it was deleted?" - ) - local_logger.exception(e) - - # if no role was added -> report it to the user - if not free_roles: - await ctx.send("There is no free role on this server.") - return - - listing = discord.Embed( - title="Free roles", - description="The list of free roles of this server. Free roles are roles anyone can get, by themselves. They can be obtained using `role add [roles...]`.", - color=7506394, - ) - listing.add_field(name="Listing", value=free_roles) - await ctx.send(embed=listing) - - -def setup(bot): - bot.add_cog(Role(bot)) diff --git a/src/exts/slapping.py b/src/exts/slapping.py deleted file mode 100644 index afc6f50..0000000 --- a/src/exts/slapping.py +++ /dev/null @@ -1,353 +0,0 @@ -import logging -import discord -import asyncio -import datetime -import os -from settings import * -from utilities import * - -######################################### -# # -# # -# Setting up logging # -# # -# # -######################################### -local_logger = logging.getLogger(__name__) -local_logger.setLevel(LOGGING_LEVEL) -local_logger.addHandler(LOGGING_HANDLER) -local_logger.info("Innitalized {} logger".format(__name__)) - - -######################################### -# # -# # -# Making commands # -# # -# # -######################################### - -name = __name__.split(".")[-1] - - -class CommunityModerationConfigEntry(ConfigEntry): - """allows to configure spam and abuse commands""" - - def __init__(self, bot, cfg_chan_id): - super().__init__(bot, cfg_chan_id) - - async def run(self, ctx): - tr = Translator(name, get_lang(ctx)) - try: - await ctx.send(tr["start_conf"]) - pursue = await self.get_yn(ctx, tr["pursue"]) - while pursue: - await ctx.send(tr["ext_explanation"]) - - # spam config - not_integer = True - while not_integer: - mute_nbr = await self.get_answer(ctx, tr["mute_threshold"]) - for i in mute_nbr.content: - if i not in DIGITS: - await ctx.send( - tr["assert_number"].format(EMOJIS["warning"]) - ) - continue - not_integer = False - mute_nbr = int(mute_nbr.content) - - # abuse config - has_chan = False - while not has_chan: - chan = await self.get_answer( - ctx, tr["mod_chan"], filters=["channels"] - ) - if len(chan) != 1: - ctx.send(tr["exactly_one"]) - continue - has_chan = True - - confirm = await self.get_yn( - ctx, tr["confirm_settings"].format(mute_nbr, chan[0].mention) - ) - if confirm: - with ConfigFile(ctx.guild.id) as conf: - conf["commode"]["spam"]["mute"] = mute_nbr - conf["commode"]["reports_chan"] = chan[0].id - pursue = False - else: - retry = await self.get_yn(ctx, tr["retry"]) - if retry: - continue - else: - pursue = False - - except Exception as e: - raise e - - -class Slapping(commands.Cog): - """a suite of commands meant to help moderators handle the server""" - - def __init__(self, bot): - self.bot = bot - self.config_entry = CommunityModerationConfigEntry - self.spams = {} - - @commands.command(aliases=["warn"]) - @is_init() - @has_auth("manager") - async def slap(self, ctx, member: discord.Member, *reason): - """Meant to give a warning to misbehavioring members. Cumulated slaps will result in warnings, role removal and eventually kick. Beware the slaps are loged throughout history and are cross-server""" - tr = Translator(name, get_lang(ctx)) - if len(reason): - reason_str = "" - for w in reason: - reason_str += f" {w}" - else: - reason_str = tr["default_reason"] - - with ConfigFile(ctx.guild.id, folder=SLAPPING_FOLDER) as slaps: - # building audit log entry - audit = f"{ctx.channel.id}/{ctx.message.id}" - - # updating dict - if str(member.id) in slaps: - slaps[str(member.id)].append(audit) - else: - slaps[str(member.id)] = [audit] - - # warning - warning = discord.Embed( - title=tr["slap_title"].format(len(slaps[str(member.id)])), - description=tr["slap_description"].format( - member.mention, len(slaps[str(member.id)]), reason_str - ), - color=16741632, - ) - warning.set_author( - name=ctx.author.display_name, icon_url=ctx.author.avatar_url - ) - await ctx.send(embed=warning) - - @commands.command(alias=["pardon"]) - @is_init() - @has_auth("manager") - async def forgive(self, ctx, member: discord.Member, nbr=0): - tr = Translator(name, get_lang(ctx)) - """Pardonning a member to reduce his slap count""" - - with ConfigFile(ctx.guild.id, folder=SLAPPING_FOLDER) as slaps: - s = slaps[str(member.id)] - if nbr == 0 or len(s) < nbr: - slaps.pop(str(member.id)) - else: - for i in range(nbr): - slaps[str(member.id)].pop() - - # pardon - slp_nbr = nbr or tr["slp_nbr_all"] - pardon = discord.Embed( - title=tr["forgive_title"].format(slp_nbr), - description=tr["forgive_description"].format( - ctx.message.author.name, member.mention - ), - color=6281471, - ) - pardon.set_author( - name=ctx.author.display_name, icon_url=ctx.author.avatar_url - ) - await ctx.send(embed=pardon) - - @commands.command(aliases=["warnings"]) - @is_init() - @has_auth("manager") - async def slaps(self, ctx, *members: discord.Member): - """returns an embed representing the number of slaps of each member. More detailed info can be obtained if member arguments are provided.""" - tr = Translator(name, get_lang(ctx)) - fields = [] - # a single string if there's no member argument - if not len(members): - fields = "" - - m_ids = [str(m.id) for m in members] - - with ConfigFile(ctx.guild.id, folder=SLAPPING_FOLDER) as slaps: - for m in slaps: - # checking member - member = ctx.guild.get_member(int(m)) - if member == None: - continue - - # building string for embed fields - if len(members) == 0: - fields += f"**{member.name}**: {len(slaps[m])}\n" - - elif m in m_ids: - crt_str = "" - for s in slaps[m]: - try: - message = await self.bot.get_channel( - int(s.split("/")[0]) - ).fetch_message(int(s.split("/")[1])) - # building reason - reason = message.content.split(" ", 2) - if len(reason) == 2: - reason = tr["slaps_default_reason"] - else: - reason = tr["other_reason"].format(reason[2]) - author = message.author.name - except discord.NotFound as e: - reason = tr["message_deleted"] - author = None - - # building string - crt_str += tr["crt_str"].format( - author, ctx.guild.id, s, member.name, reason - ) - fields.append( - { - "name": tr["new_field_name"].format( - member.name, len(slaps[m]) - ), - "value": crt_str, - "inline": False, - } - ) - - # checking if a member has been slapped - if not fields: - await ctx.send(tr["no_slaps"] + EMOJIS["tada"]) - return - - # if a user has been slapped - embed = discord.Embed( - title=tr["slaps_title"] + EMOJIS["hammer"], - description=tr["slaps_description"], - colour=16741632, - ) # used to be blurpple 7506394 - - # adding fields - if not len(members): - embed.add_field(name=tr["member_list"], value=fields) - - else: - for field in fields: - embed.add_field(**field) - - await ctx.send(embed=embed) - - async def make_mute(self, channel, member, time): - seconds = time.total_seconds() - - with ConfigFile(channel.guild.id, folder=TIMES_FOLDER) as count: - free_at = datetime.datetime.now().timestamp() + seconds - if str(member.id) in count.keys(): - same = False - for chan in count[str(member.id)]: - if int(chan[0]) == channel.id: - chan[1] = int(chan[1]) + seconds - same = True - - if not same: - count[str(member.id)].append((channel.id, free_at)) - - else: - count[str(member.id)] = [(channel.id, free_at)] - - await channel.set_permissions( - member, overwrite=discord.PermissionOverwrite(send_messages=False) - ) - await asyncio.sleep(seconds) - await channel.set_permissions( - member, overwrite=discord.PermissionOverwrite(send_messages=None) - ) - - @commands.command() - @is_init() - @has_auth("manager") - async def mute(self, ctx, member: discord.Member, time, whole: bool = False): - until = to_datetime(time, sub=False) - if not whole: - await self.make_mute(ctx.channel, member, until) - else: - await ctx.send(COMING_SOON) - - @commands.command() - @is_init() - async def spam(self, ctx, member: discord.Member): - """allows users to report spamming""" - tr = Translator(name, get_lang(ctx)) - if not ctx.guild in self.spams: - self.spams[ctx.guild] = {} - - g_spams = self.spams[ctx.guild] - - if member not in g_spams.keys(): - g_spams[member] = [ctx.author] - else: - if ctx.author in g_spams[member]: - await ctx.send(EMOJIS["warning"] + tr["cant_multi_spam"]) - else: - g_spams[member].append(ctx.author) - - # checking if a threshold was reached - amount = len(g_spams[member]) - with ConfigFile(ctx.guild.id) as conf: - com = conf["commode"]["spam"] - # muting user if necessary - if amount % com["mute"] == 0: - await self.make_mute( - ctx.channel, member, datetime.timedelta(seconds=960) - ) - await ctx.send( - EMOJIS["zip"] + tr["spam_muted"].format(member.mention) - ) - - self.spams[ctx.guild] = g_spams - - @commands.command() - @is_init() - async def abuse(self, ctx, member: discord.Member, *reason): - if len(reason) == 0: - raise discord.ext.commands.MissingRequiredArgument( - "You need to provide a reason." - ) - - tr = Translator(name, get_lang(ctx)) - with ConfigFile(ctx.guild.id) as conf: - mod_chan = conf["commode"]["reports_chan"] - - if mod_chan == False: - await ctx.send( - "The server owner has disabled this feature because he didn't set any moderation channel. Contact him/her if you think this is not right." - ) - else: - mod_chan = ctx.guild.get_channel(mod_chan) - - reason_str = "" - for word in reason: - reason_str += f" {word}" - - report = tr["report"].format( - ctx.author.mention, - ctx.message.jump_url, - member.mention, - ctx.channel.mention, - ) - - card = discord.Embed( - title=tr["report_title"], - timestamp=datetime.datetime.now(), - color=16729127, - description=report, - ) - - card.set_author(name=ctx.author.name, icon_url=ctx.author.avatar_url) - card.add_field(name=tr["reason_name"], value=reason_str) - await mod_chan.send(embed=card) - - -def setup(bot): - bot.add_cog(Slapping(bot)) diff --git a/src/exts/time.py b/src/exts/time.py deleted file mode 100644 index b1e9bf7..0000000 --- a/src/exts/time.py +++ /dev/null @@ -1,74 +0,0 @@ -import discord -import json -import logging -import asyncio -from settings import * -from utilities import * - -######################################### -# # -# # -# Setting up logging # -# # -# # -######################################### -local_logger = logging.getLogger(__name__) -local_logger.setLevel(LOGGING_LEVEL) -local_logger.addHandler(LOGGING_HANDLER) -local_logger.info("Innitalized {} logger".format(__name__)) - - -######################################### -# # -# # -# Making commands # -# # -# # -######################################### - - -class Time(commands.Cog): - """A cog which handles reminder events and commands""" - - def __init__(self, bot): - self.config_entry = None - self.bot = bot - self.tf = {"d": 86400, "h": 3600, "m": 60, "s": 1} - - @commands.command() - async def remind(self, ctx, *args): - """the date format is as such: - d => days - h => hours - m => minutes - s => seconds - Also the order is important. The time parser will stop once it's reached seconds.""" - delay = 0 - done = False - text = "" - for a in args: - if not done: - # parsing the time - if a[-1] in self.tf.keys(): - try: - delay += int(a[:-1]) * self.tf[a[-1]] - if a[-1] == "s": - done = True - - except ValueError as e: - # if seconds isn't precised but that the timestamp is done - done = True - else: - # making the text - text += f" {a}" - - if delay == 0: - await ctx.send(embed=get_embed_err(ERR_NOT_ENOUGH_ARG)) - return - - await asyncio.sleep(delay) - await ctx.author.send(text) - - -def setup(bot): - bot.add_cog(Time(bot)) diff --git a/src/exts/todo.py b/src/exts/todo.py deleted file mode 100644 index ce21103..0000000 --- a/src/exts/todo.py +++ /dev/null @@ -1,218 +0,0 @@ -import logging -import discord -from typing import Union -from settings import * -from utilities import * - -######################################### -# # -# # -# Setting up logging # -# # -# # -######################################### -local_logger = logging.getLogger(__name__) -local_logger.setLevel(LOGGING_LEVEL) -local_logger.addHandler(LOGGING_HANDLER) -local_logger.info("Innitalized {} logger".format(__name__)) - - -class Todo(commands.Cog): - """A suite of command to make a nice todo list.""" - - def __init__(self, bot): - self.bot = bot - self.config_entry = None - - @commands.Cog.listener() - async def on_raw_reaction_add(self, reaction): - if reaction.user_id == self.bot.user.id: - return - first_message = [ - await self.bot.get_channel(reaction.channel_id).fetch_message( - reaction.message_id - ) - ] - - todo = get_todo(reaction.guild_id) - - # checking if channel is todo - # for chan in lambda: [chan for group in todo["groups"].values() for chan in group]: - # if reaction.channel_id == chan.id: - - is_todo = False - for grp in todo["groups"].values(): - for chan in grp: - if reaction.channel_id == chan: - group = grp - is_todo = True - break - - if is_todo: # check if it's the good channel - if len( - message.embeds - ): # Check if it's an embed, I think this will avoid most problems - if reaction.emoji.name == EMOJIS["wastebasket"]: - for chan in grp: - messages = [] - async for message in await self.bot.get_channel(chan).history(): - if ( - message.embeds[0].description - == first_message[0].embeds[0].description - ): - messages.append(message) - - await self.bot.delete_messages(messages) - - elif reaction.emoji.name == EMOJIS["check"]: - await message.remove_reaction(EMOJIS["hourglass"], self.bot.user) - elif reaction.emoji.name == EMOJIS["hourglass"]: - await message.remove_reaction(EMOJIS["check"], self.bot.user) - - @commands.Cog.listener() - async def on_raw_reaction_remove(self, reaction): - if reaction.user_id == self.bot.user.id: - return - - message = await self.bot.get_channel(reaction.channel_id).fetch_message( - reaction.message_id - ) - - # checking if channel is todo - todo = get_todo(reaction.guild_id) - for chan in [chan for group in todo["groups"].values() for chan in group]: - if reaction.channel_id == chan.id: - is_todo = True - break - - if is_todo: # Check if it's a todo-message (check if it's the good channel) - if len( - message.embeds - ): # Check if it's an embed, I think this will avoid most problems - if reaction.user_id != self.bot.user.id: - if reaction.emoji.name == EMOJIS["check"]: - await message.add_reaction(EMOJIS["hourglass"]) - elif reaction.emoji.name == EMOJIS["hourglass"]: - await message.add_reaction(EMOJIS["check"]) - - @commands.group() - @has_auth("manager") - async def todo(self, ctx): - """Commands to manage a todolist.""" - if ctx.invoked_subcommand is None: - await ctx.send(ERR_NOT_ENOUGH_ARG) - - @todo.command() - async def add( - self, ctx, todo_type, assignee: Union[bool, discord.Member], groups, *args - ): # , repost:Union[bool, discord.TextChannel] - """Command to add a todo. Usage : ;;;""" - - todo_dict = get_todo(ctx.guild.id) - - # making sure the type is valid - if todo_type not in todo_dict["todo_types"]: - await ctx.send( - "Can't assign to an unexisting type. To get a list of available types run `::todo listtypes`." - ) - return - - else: - print(todo_dict["todo_types"][todo_type][1]) - # the color value is saved as an hexadecimal value so it is made an int to get the base 10 decimal value - embed_color = int(todo_dict["todo_types"][todo_type], 16) - print(embed_color) - - # building the todo name string - crt_todo = "" - for word in args: - crt_todo += word - - # building the embed - new_embed = discord.Embed(description=crt_todo, color=embed_color) - new_embed.set_footer(todo_type) - - # if repost: - # public_todo = await repost.send(embed=new_embed) - # new_embed.add_field(name="Public repost", value=repost.mention+" : "+ str(public_todo.id), inline=True) - - # sending message and reactions - msg = await ctx.send(embed=new_embed) - await message.add_reaction(EMOJIS["wastebasket"]) - await message.add_reaction(EMOJIS["check"]) - await message.add_reaction(EMOJIS["hourglass"]) - - @todo.command() - async def addtype(self, ctx, todo_type, hex_color): - """Command to add a todo type.""" - command = command.split(";") - - if command[1].startswith("#"): - command[1] = command[1][1:] - if len(command[1]) != 6: - if len(command[1]) != 3: - await ctx.send( - "The color must be in hexadecimal, like this **#ccc** or **#ff0000**" - ) - return - else: - command[1] = command[1] + command[1] - color = "0x" + command[1] - - with open(TODO_TYPES_FILE, "r+") as file: - content = file.readlines() - for line in content: - line = line.split(";") - if line[0] == command[0]: - await ctx.send( - "There is already a type named **" + command[0] + "**" - ) - return - - file.write("\n" + command[0] + ";" + color) - await ctx.send( - 'You added the label "' - + command[0] - + "\", the embed's color for this todo type will be : #" - + command[1] - ) - - @todo.command() - async def removetype(self, ctx, todo_type): - """deletes the type""" - try: - old_conf = get_conf(ctx.guild.id) - print(old_conf["todo_types"]) - # checking whether the type exists in the db - if todo_type not in old_conf["todo_types"]: - await ctx.send("Can't delete an unexisting type.") - return - - old_conf["todo_types"].pop(todo_type) - update_conf(ctx.guild.id, old_conf) - await ctx.send(f"Successfully deleted {todo_type} type.") - - except Exception as e: - local_logger.exception(e) - await ctx.send(ERR_UNEXCPECTED) - - @todo.command() - async def listtypes(self, ctx): - """Lists all available types""" - try: - todo_dict = get_todo(ctx.guild.id) - text = "" - for t in todo_dict["types"]: - text += f"""\n**{t}** - \t*#{todo_dict["types"][t]}*""" - - new_embed = discord.Embed( - title="**Type** - *Color*", description=text, color=0x28A745 - ) - await ctx.send(embed=new_embed) - except Exception as e: - raise e - local_logger.exception(e) - - -def setup(bot): - bot.add_cog(Todo(bot)) diff --git a/src/help.py b/src/help.py deleted file mode 100644 index 2087536..0000000 --- a/src/help.py +++ /dev/null @@ -1,332 +0,0 @@ -import logging -import asyncio -from time import time -import discord -from settings import EMOJIS, HELP_TIME -from utilities import * - -######################################### -# # -# # -# Setting up logging # -# # -# # -######################################### -local_logger = logging.getLogger(__name__) -local_logger.setLevel(LOGGING_LEVEL) -local_logger.addHandler(LOGGING_HANDLER) -local_logger.info("Innitalized {} logger".format(__name__)) - -######################################### -# # -# # -# Making commands # -# # -# # -######################################### - - -class InteractiveHelp(discord.ext.commands.DefaultHelpCommand): - """This Help class offers interaction support through embeds and reactions.""" - - def __init__(self, react_time: int = HELP_TIME, **options): - super().__init__(**options) - self.react_time = react_time - - def help_reaction(self, reaction, user): - if reaction.emoji not in ( - EMOJIS["arrow_backward"], - EMOJIS["arrow_forward"], - EMOJIS["information_source"], - EMOJIS["track_previous"], - EMOJIS["track_next"], - ): - return False - - # making sure the author of the help command is indeed the one reacting - if user == reaction.message.author: - return False - - return True - - def get_help_lang(self): - with ConfigFile(self.get_destination().guild.id) as conf: - lang = conf["lang"] - return lang - - async def set_reactions(self, msg: discord.Message, pages: int): - # adding approriate interactions - if pages > 1: - if pages > 2: - await msg.add_reaction(EMOJIS["track_previous"]) - await msg.add_reaction(EMOJIS["arrow_backward"]) - await msg.add_reaction(EMOJIS["information_source"]) - await msg.add_reaction(EMOJIS["arrow_forward"]) - if pages > 2: - await msg.add_reaction(EMOJIS["track_next"]) - - else: - await msg.add_reaction(EMOJIS["information_source"]) - - async def start_interaction(self, pages: list, msg: discord.Message): - current_page = 0 - start_time = time() - elapsed_time = 0 - try: - while elapsed_time < self.react_time: - print("waiting for a reaction") - reaction, user = await self.context.bot.wait_for( - "reaction_add", - timeout=self.react_time - elapsed_time, - check=self.help_reaction, - ) - print(f"Got reaction {reaction} from {user}") - - # interpret reactions - if reaction.emoji == EMOJIS["arrow_forward"]: - # making sure you're not on the last page - local_logger.debug("Going forward in paging") - if current_page != len(pages) - 1: - current_page += 1 - - elif reaction.emoji == EMOJIS["arrow_backward"]: - local_logger.debug("Going backward in paging") - # making sure you're not on the first page - if current_page != 0: - current_page -= 1 - - elif reaction.emoji == EMOJIS["track_next"]: - local_logger.debug("Going to last page.") - current_page = len(pages) - 1 - - elif reaction.emoji == EMOJIS["track_previous"]: - local_logger.debug("Going to first page") - current_page = 0 - - else: - # the only other allowed reaction is information_source - local_logger.debug( - "Deleting help embed and sending help interface manual." - ) - await self.send_bot_help(self.get_bot_mapping()) - await msg.delete() - break - - await msg.remove_reaction(reaction, user) - await msg.edit(suppress=False, embed=pages[current_page]) - elapsed_time = time() - start_time - except asyncio.exceptions.TimeoutError: - await msg.delete() - - async def send_bot_help(self, mapping): - pages = get_bot_pages(self.context.bot.cogs, self.get_help_lang()) - msg = await self.get_destination().send(embed=pages[0]) - - await self.set_reactions(msg, len(pages)) - await self.start_interaction(pages, msg) - - async def send_cog_help(self, cog): - pages = get_cog_pages(cog, self.get_help_lang()) - msg = await self.get_destination().send(embed=pages[0]) - - await self.set_reactions(msg, len(pages)) - await self.start_interaction(pages, msg) - - async def send_group_help(self, group): - pages = get_group_pages(group, self.get_help_lang()) - msg = await self.get_destination().send(embed=pages[0]) - - await self.set_reactions(msg, len(pages)) - await self.start_interaction(pages, msg) - - async def send_command_help(self, command): - pages = get_command_pages(command, self.get_help_lang()) - msg = await self.get_destination().send(embed=pages[0]) - - await self.set_reactions(msg, len(pages)) - await self.start_interaction(pages, msg) - - -def get_help(command, lang: str): - """this needs heavy refactoring""" - if command.cog: - text = Translator( - command.cog.__module__.split(".")[-1], lang, help_type=True - )._dict - if not command.parents: - if isinstance(command, discord.ext.commands.Group): - return text[command.name][0] # returns the description of the group - elif isinstance(command, discord.ext.commands.Command): - return text[command.name] - - else: - if isinstance(command, discord.ext.commands.Command): - for parent in command.parents: - text = text[parent.name][1] - return text[command.name] - elif isinstance(command, discord.ext.commands.Group): - for parent in command.parents[:-1]: - text = text[parent.name][1] - return text[0] # returns the description of the group - - else: - # the only commands not in a cog are in main.py -> ext group - return Translator("default", lang, help_type=True)[command.name] - - -def get_bot_pages(cog_mapping, lang: str): - """currently doesn't support commands outside of cogs""" - cogs = cog_mapping.values() - cog_names = cog_mapping.keys() - - pages = [] - for cog in cogs: - pages += get_cog_pages(cog, lang, paginate=False) - - # explaining how help works - tr = Translator("help", lang, help_type=True)._dict - description = tr["description"] - header = discord.Embed(title="Help", description=description, color=7506394) - - # listing all available cogs - chars_per_cog = int(5000 / len(cog_names)) - index = discord.Embed( - title="Index", - description="Here's an index of all cogs (sometimes also refered to as extensions) this bot contains. To get more information on a specific one type `::help `. Otherwise you can also browse through the pages.", - color=7506394, - ) - for cog in cog_names: - index.add_field(name=cog, value=tr[cog.lower()][:chars_per_cog], inline=True) - - pages_number = len(pages) - header.set_footer(text=f"Page (1/{pages_number+2})") - index.set_footer(text=f"Page (2/{pages_number+2})") - for embed, crt in zip(pages, range(pages_number)): - embed.set_footer(text=f"Page ({crt+3}/{pages_number+2})") - - pages.insert(0, header) - pages.insert(1, index) - return pages - - -def get_cog_pages( - cog: discord.ext.commands.Cog, lang: str, paginate: bool = True -) -> list: - pages = [] - for command in cog.get_commands(): - if isinstance(command, discord.ext.commands.Group): - pages += get_group_pages(command, lang, paginate=False) - else: - pages += get_command_pages(command, lang, paginate=False) - - description = Translator("help", lang, help_type=True)._dict[ - cog.qualified_name.lower() - ] - header = discord.Embed( - title=cog.qualified_name, description=description, color=7506394 - ) - - # paginating - if paginate: - pages_number = len(pages) - header.set_footer(text=f"Page (1/{pages_number+1})") - for embed, crt in zip(pages, range(pages_number)): - embed.set_footer(text=f"Page ({crt+2}/{pages_number+1})") - - pages.insert(0, header) - return pages - - -def get_group_pages( - group: discord.ext.commands.Group, lang: str, paginate: bool = True -) -> list: - pages = [] - for command in group.commands: - pages += get_command_pages(command, lang, paginate=False) - - description = get_help(group, lang) - header = discord.Embed(title=group.name, description=description, color=7506394) - - # paginating - if paginate: - pages_number = len(pages) - header.set_footer(text=f"Page (1/{pages_number+1})") - for embed, crt in zip(pages, range(pages_number)): - embed.set_footer(text=f"Page ({crt+2}/{pages_number+1})") - - pages.insert(0, header) - return pages - - -def get_command_pages( - command: discord.ext.commands.command, - lang: str, - threshold: int = 150, - paginate: bool = True, -) -> list: - """this returns a list of Embeds that represent help pages - cmd_type is an int that must be 0 (command), 1 (cog), 2 (group) - maybe return a generator instead?""" - name = "" - if command.parents: - for parent in command.parents: - name += f"{parent.name} / " - name += command.name - - description, usage = get_help(command, lang) - - # split command in multiple pages - if ( - count_chars(description, usage, name) > 2048 - threshold - ): # maximum embed description size is 2048 chars - pages = [] - # splitting by line breaks - paragraphs = description.split("\n") - - # splitting by "." - for paragraph in paragraphs: - if count_chars(paragraph) > 2048 - threshold: - sentences = paragraph.split(".") - assert len(sentences) > 0, ValueError( - "No known parsing method for this help string. Should contain at least one dot." - ) - while sentences: - page = "" # the page we're starting to build - crt_count = 2048 # how many chars are left before we need to make a new page - while (crt_count > threshold) and len(sentences) != 0: - # print(sentences) - sentence = sentences.pop(0) - crt_count -= count_chars(sentence) - # /!\ currently not handling the case of sentences larger than 2048 chars - assert crt_count > 0, ValueError( - "This sentence is over 2048 characters long!" - ) # > and not >= because we need to add the dot again - page += sentence + "." - pages.append(page) - - else: - pages.append(paragraph) - else: - pages = [description] - - # building the embeds from the paged descriptions - embeds = [] - pages_number = len(pages) - for page, crt in zip(pages, range(len(pages))): - embed = discord.Embed(title=name, description=page, color=7506394,) - if paginate: - embed.set_footer(text=f"Page ({crt+1}/{pages_number})") - embed.add_field(name="Usage", value=f"`{command.name} {usage}", inline=False) - embeds.append(embed) - - return embeds - - -def count_chars(*args: str) -> int: - count = 0 - for string in args: - assert type(string) == str, TypeError( - f"args should be of type str not {type(string)}" - ) - count += len(string) - return count diff --git a/src/lang/config/help.en b/src/lang/config/help.en deleted file mode 100644 index 52dbef3..0000000 --- a/src/lang/config/help.en +++ /dev/null @@ -1,6 +0,0 @@ -{ - "cfg": ["used to start to configure the way the bot should work on your server. See `cfg ` for more information on a specific command.", "` disabled"], - "init": ["starts the configuration of **all** loaded extensions plus a few miscelleanous ones that can only be accessed through this command. Should ne used upon bot addition to server or when making a full re-configuration of the bot or clearances.", "` starts full configuration"], - "summary": ["returns the bot settings for this server in the form of an embed.", "` returns bot settings"] - -} \ No newline at end of file diff --git a/src/lang/development/help.en b/src/lang/development/help.en deleted file mode 100644 index e09b79c..0000000 --- a/src/lang/development/help.en +++ /dev/null @@ -1,5 +0,0 @@ -{ - "update": ["Lets the owner of the bot update the bot from github's repositery. It also sends a notification to all server owners who use the bot. The message sent in the notification is the description of the release on github.", "[message...]` sends message to all owners of the bot."], - "log": ["Returns the bot's log as an attachement", "` returns the log as an attachement"], - "dev": ["Sends the development server URL to the author of the message", "` resolves an invite"] -} \ No newline at end of file diff --git a/src/lang/development/strings.en b/src/lang/development/strings.en deleted file mode 100644 index 4fb6bb0..0000000 --- a/src/lang/development/strings.en +++ /dev/null @@ -1,3 +0,0 @@ -{ - "dev": "I'm full of joy seeing how interested you seem to be about me! Hopefully it's not for a bug report... In any case, here's the server where I'm developed: " -} \ No newline at end of file diff --git a/src/lang/embedding/help.en b/src/lang/embedding/help.en deleted file mode 100644 index 7d1536d..0000000 --- a/src/lang/embedding/help.en +++ /dev/null @@ -1,3 +0,0 @@ -{ - "embed": ["allows you to post a message as an embed. Your msg will be reposted by the bot as an embed !", "[message...]` embeds *message*"] -} \ No newline at end of file diff --git a/src/lang/embedding/strings.en b/src/lang/embedding/strings.en deleted file mode 100644 index 9e26dfe..0000000 --- a/src/lang/embedding/strings.en +++ /dev/null @@ -1 +0,0 @@ -{} \ No newline at end of file diff --git a/src/lang/essentials/help.en b/src/lang/essentials/help.en deleted file mode 100644 index 2618189..0000000 --- a/src/lang/essentials/help.en +++ /dev/null @@ -1,6 +0,0 @@ -{ - "ping": ["pongs back with current latency!", "` tells you the latency"], - "shutdown": ["shutdown the bot", "` shuts the bot down"], - "clear": ["this command lets one delete messages from a channel. The provided arguments are filters that will be applied to the messages selection process. `nbr` specifies the maximum number of messages that should be deleted. If not given then there is no maximum, **be careful with it**. This will always be respected although less messages may get deleted if `period` doesn't contain enough messages. `period` represents a time frame. The bot will look for all messages within this time frame. It should be constructed like `remind`. All messages which were sent between *now* and `period` will get deleted unless this represents more messages than `nbr`. `members...` is a list of server members. Only messages from these users will get deleted. You do not need to pass any of the arguments to the command and can pass any combination of them to the command. However they **must** be given in order!", "[nbr] [period] [members...]` deletes `nbr` message(s) from `members` in the currenct channel within `period`."], - "status": ["returns statistics about the server and their members", "` embed stats about the server."] -} \ No newline at end of file diff --git a/src/lang/essentials/strings.en b/src/lang/essentials/strings.en deleted file mode 100644 index 46ad4ab..0000000 --- a/src/lang/essentials/strings.en +++ /dev/null @@ -1,17 +0,0 @@ -{ - "welcome1": "Do you want to have a welcome message sent when a new user joins the server?", - "welcome2": "Enter the message you'd like to be sent to the new users. If you want to mention them use `{0}`", - "goodbye1": "Do you want to have a goodbye message sent when an user leaves the server?", - "goodbye2": "Enter the message you'd like to be sent when an user leaves. If you want to mention them use `{0}`", - "start_conf": "**Starting {} message configuration**", - "send_check": "To make sure the message is as you'd like I'm sending it to you.\n**-- Beginning of message --**", - "response": "**--End of message --**\nIs this the message you want to set as the {} message?", - "retry": "Do you want to retry?", - "latency": " Latency of {0:.3f} seconds", - "stats_name": "Server info", - "stats_description": "{} was created on {} and belongs to {}. Since then {} users have joined it.", - "status_str": "**{online} **🟢 online\n**{idle} **🟠 idling\n**{dnd} **🔴 not to disturb\n**{offline} **⚪ offline", - "mstats_name": "Member statistics", - "sstats_name": "Roles" - -} \ No newline at end of file diff --git a/src/lang/help/help.en b/src/lang/help/help.en deleted file mode 100644 index ed2a7d9..0000000 --- a/src/lang/help/help.en +++ /dev/null @@ -1,12 +0,0 @@ -{ - "description": "This is the `help` command interface. With you can learn to use all commands made available to you by this bot. Type `::help ` to get more information on a specific command.\nHere are some basic concepts:\n a command is a process that you ask the bot to do. They are differenciated by their name and to call (=invoke) them you must append `::` before the name of the command.\n**Beware**, for the sake of performance the interactive help sessions get shut down after some time and delete themselves to save space and improve readibility.\n Arguments, also known as parameters are options you can pass to the commands. You specify them by writting them down after the said command.\n\nThere are many kinds of parameters.\n`` is a **required** parameter that must be specified. If you don't you'll get a `NotEnoughArguments` error.\n`[arg]` are **optional** arguments\n`...` means that you can specify multiple arguments of this type. All arguments are separated by a space (` `).", - "time": "Time provides various commands linked to time.", - "role": "Role management utilities", - "slapping": "A warning interface meant to use moderation on server with multiple moderators and lots of members.", - "poll": "This suite of commands provides automatic poll creation. A poll is an embed message sent by the bot to specified channels. Every user can react to the poll to show their opinion regarding the interrogation submitted by the poll. With each reaction, the poll's color will change to give everyone a quick visual feedback of all members' opinion. A poll is generated from a user's message. Currently it only supports messages from a poll channel. However it is planned to improve this to allow one to create a poll using a dedicated command. Same goes for poll editing which is yet unsupported. To palliate to this you can remove your poll if you consider it was malformed.", - "embedding": "This extension allows any user to send a message as an embed. The color of the embed is defined by the user's role color.", - "essentials": "This extension contains some of the most basic managing commands and should almost always be enabled.", - "development": "Allows the developers to update the bot and notify all server owners of the changes. It also facilitates bug fixing by providing an easy way to retrieve the log.", - "config": "Allows the owner of a server to configure the behavior of the bot.", - "defaults": "A suite of commands always activated which handle extension management. This cannot be unloaded as it is part of the core of the bot and is required for live updates." -} \ No newline at end of file diff --git a/src/lang/help/strings.en b/src/lang/help/strings.en deleted file mode 100644 index fda58b4..0000000 --- a/src/lang/help/strings.en +++ /dev/null @@ -1,3 +0,0 @@ -{ - "help_page_title": "Help" -} \ No newline at end of file diff --git a/src/lang/poll/help.en b/src/lang/poll/help.en deleted file mode 100644 index d62aee9..0000000 --- a/src/lang/poll/help.en +++ /dev/null @@ -1,7 +0,0 @@ -{ - "poll": ["a suite of commands that lets one have more control over polls", { - "rm": ["allows one to delete one of their poll by issuing its id", "` deletes poll with *id*"], - "status" : ["returns stats about your running polls. This is also called when one of you poll gets deleted.", "` returns nothing"], - "extended": ["This command creates polls that can have more than the 3 standard reaction but do not support dynamic color. The way to make one is to be write the following command in a poll channel (message discarded otherwise). The message is composed of the description then a line break then, one each following line: an emoji followed by a description each of these lines are separated by a line break", " `"] - }] -} \ No newline at end of file diff --git a/src/lang/poll/strings.en b/src/lang/poll/strings.en deleted file mode 100644 index 986af4c..0000000 --- a/src/lang/poll/strings.en +++ /dev/null @@ -1 +0,0 @@ -{"start_conf": "**Starting poll configuration**", "pursue": "Do you want to activate polls on this server?", "poll_channels": "List all the channels you want to use as poll channels. You must mention those channels like this:", "invalid_chan": "You cannot set this channel as a poll channel. Please try again...", "confirmed": "You are about to make poll channels. Do you want to continue?", "drop": "Aborting addition of poll channels. Do you want to leave the poll configuration interface?", "conf_done": "Poll configuration is done."} \ No newline at end of file diff --git a/src/lang/role/help.en b/src/lang/role/help.en deleted file mode 100644 index 2626f0e..0000000 --- a/src/lang/role/help.en +++ /dev/null @@ -1,7 +0,0 @@ -{ - "role": ["role management utility. Requires a Gestion role", { - "add": ["Give roles to a member.", " [roles...]` gives *roles* to *member*"], - "rm": ["Remove roles from a member.", " [roles...]` removes *roles* from *member*"], - "free": ["Lists all free roles on this server.", "` returns a list of free roles"] - }] -} \ No newline at end of file diff --git a/src/lang/role/strings.en b/src/lang/role/strings.en deleted file mode 100644 index 1a7996b..0000000 --- a/src/lang/role/strings.en +++ /dev/null @@ -1,10 +0,0 @@ -{ - "start_conf": "\n**Starting free roles configuration**", - "pursue": "Free roles can be gotten by everyone using the `role add` command. Do you want to set some?", - "proles": "List all the roles you want to be \"free\".", - "agrees": "You are about to set {} as \"free\" roles. Are you sure?", - "retry": "Do you want to retry?", - "not_free_role": "You're not allowed to give yourself the {} role. Ask a moderator if you think this is wrong.", - "gave": "You gave {} {} role(s)." - -} \ No newline at end of file diff --git a/src/lang/slapping/help.en b/src/lang/slapping/help.en deleted file mode 100644 index b3b2d7c..0000000 --- a/src/lang/slapping/help.en +++ /dev/null @@ -1,8 +0,0 @@ -{ - "slap": ["Meant to give a warning to misbehavioring members. Cumulated slaps will result in warnings, role removal and eventually kick. Beware the slaps are logged throughout history.", " [reason...]` slaps `member` with `reason`"], - "forgive": ["Pardonning a member to reduce his slap count.", " [nbr]` removes `nbr` slaps from `member`"], - "slaps": ["Returns an embed representing the number of slaps of each member. More detailed info can be obtained if member arguments are provided.", "[members...]` returns a list denoting the number of times each slapped member has been slapped. If `members` is precised then detailed information about the slaps of these members will be displayed."], - "spam": ["Can be used by anyone when they believe someone is spamming. One user can only report another once for the same spamming. Rules can be set for automatic moderation. A set amount of reports must be done for the counter-spamming rules to take effect.", "` raises spam counter for `member` by one."], - "abuse": ["Can be used by anyone. This allows users to report unruly behavior from another member. This will send a report card to a set moderation channel for the moderators to review. A explanation for the report must be provided.", " [reason...]` reports `member` because of `reason`"], - "mute": ["This command allows managers to mute a member in a channel for some time. When muted a member can still see and react in the channel but is unable to send any message until the cooldown is over.", "