From 92a9912b7a55a4ff44d3d78af96850259e8a914e Mon Sep 17 00:00:00 2001 From: NotaSmartDev Date: Tue, 28 May 2019 20:25:53 +0200 Subject: [PATCH 01/52] fixed some issues & paved the way to the multi-server configuration --- BotEssentials.py | 1 - Config.py | 86 ++++++++++++++++++++++++++++++++++++++++++++++++ checks.py | 7 ++-- executioner.py | 32 ++++++++++++++---- settings.py | 56 +++++++++++++++++-------------- 5 files changed, 148 insertions(+), 34 deletions(-) create mode 100644 Config.py diff --git a/BotEssentials.py b/BotEssentials.py index a157cf5..adfec05 100644 --- a/BotEssentials.py +++ b/BotEssentials.py @@ -82,7 +82,6 @@ async def clear(slef, ctx, nbr:int): except Exception as e: local_logger.exception("Couldn't delete {}".format(msg)) raise e - diff --git a/Config.py b/Config.py new file mode 100644 index 0000000..7c01d1e --- /dev/null +++ b/Config.py @@ -0,0 +1,86 @@ +import logging +import discord +from settings import * +from checks 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 Config(commands.Cog): + """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.bot = bot + + @commands.group() + @is_owner() + async def cfg(self, ctx): + if invoked_subcommand == None: + ctx.send(ERROR_NO_SUBCOMMAND) + + @cfg.command() + async def init(self, ctx): + pass + + @cfg.command() + async def chg(self, ctx, setting): + try: + eval("self.chg_"+setting) + + except Exception as e: + local_logger.exception(ERR_UNEXCPECTED) + + @cfg.command() + async def leave(self, ctx)-> int: + ctx.send("You are about to remove the bot from the server. This will erase all of your configuration from the mainframe and you won't be able to recover the bot without getting another invite. Are you sure you want to continue ? (y/N)") + + async def cfg_poll(self, ctx): + pass + + async def cfg_roll(self, ctx): + pass + + async def cfg_todo(self, ctx): + pass + + async def cfg_welcome(self, ctx): + pass + + async def yes_no_answer(self, ctx): + pass + + @commands.Cog.listener() + #is it possible to add checks to listeners ? (is_owner and in_channel) + async def on_message(self, message): + + #checking that the msg isn't from the bot, in the cfg channel and from the guild owner + #change with a list of IDs that represent the in-progress configuration channels + if self.message.name!="foreman-configuration-channel": return + if message.author==self.bot.user: return + #is it possible to add a check inside a code block ? + if message.author != message.guild.owner: return + + + +def setup(bot): + bot.add_cog(Poll(bot)) \ No newline at end of file diff --git a/checks.py b/checks.py index 12ad117..61b09a0 100644 --- a/checks.py +++ b/checks.py @@ -13,7 +13,7 @@ local_logger = logging.getLogger(__name__) local_logger.setLevel(LOGGING_LEVEL) local_logger.addHandler(LOGGING_HANDLER) -local_logger.info("Innitalized {} logger".format(__name__)) +local_logger.info(f"Innitalized {__name__} logger") ######################################### @@ -27,4 +27,7 @@ def is_runner(): def check_condition(ctx): return ctx.message.author.id ==RUNNER_ID - return commands.check(check_condition) \ No newline at end of file + result = commands.check(check_condition) + if result == False: + ctx.send(ERR_UNSUFFICIENT_PRIVILEGE) + return result \ No newline at end of file diff --git a/executioner.py b/executioner.py index fb7b386..ad60384 100644 --- a/executioner.py +++ b/executioner.py @@ -5,14 +5,11 @@ from settings import * from checks import * import math -import requests as rq -from bs4 import BeautifulSoup import time import random import logging - #INITS THE BOT bot = commands.Bot(command_prefix=PREFIX) @@ -28,7 +25,7 @@ main_logger = logging.getLogger(__name__) main_logger.setLevel(LOGGING_LEVEL) main_logger.addHandler(LOGGING_HANDLER) -main_logger.info("Initalized logger") +main_logger.info(f"Initalized {__name__} logger") #Creating discord.py's logger @@ -52,7 +49,7 @@ @is_runner() async def ext(ctx): if ctx.invoked_subcommand is None: - await ctx.send("NotEnoughArguments:\tYou must provide a subcommand") + await ctx.send(ERR_NOT_ENOUGH_ARG) @ext.command() async def reload(ctx, extension:str): @@ -93,7 +90,29 @@ async def add(ctx, extension:str): raise e await ctx.send("Successfully added and loadded {}".format(extension)) +@ext.command() +async def rm(ctx, extension:str): + try: + bot.unload(extension) + + except Exception as e: + main_logger.exception(e) + await ctx.send("UnexpectedError:\tReport issue to an admin\n{}".format(e)) + raise e + #if the extension was correctly unloaded, removing it from the enblaed extension file + try: + with open(ENABLED_EXTENSIONS_FILE, "aw") as file: + lines = [] + for line in file.readlines(): + if line == extension: + continue + lines.append(line) + file.write(lines) + except Exception as e: + main_logger.exception(e) + await ctx.send("UnexpectedError:\tReport issue to an admin\n{}".format(e)) + raise e @@ -131,5 +150,4 @@ async def add(ctx, extension:str): #running the bot, no matter what finally: - bot.run(TOKEN) - + bot.run(TOKEN) \ No newline at end of file diff --git a/settings.py b/settings.py index 2863a09..8d1567a 100644 --- a/settings.py +++ b/settings.py @@ -9,9 +9,11 @@ PREFIX = "::" -TOKEN = "NTYzODQ4MDcxMjE0MjY4NDI5.XNK7DA.rbTMbw_KwBPOpCaV793UsK6xoCc" +TOKEN = "NTYzODQ4MDcxMjE0MjY4NDI5.XOUOKA.WQ4i2B_Jer-mWptaXZlrsNsMVF0" RUNNER_ID=289426079544901633 +#server with all of the bot's devs. Where all bug reports should be made. +DEV_SRV = 564213369834307600 #emojis dict. May be possible to change incomprehensible unicode to other strings recognized by discord EMOJIS = { @@ -19,40 +21,46 @@ "thumbsdown": "\U0001f44e", "shrug": "\U0001f937", "wastebasket": "\U0001F5D1", - "check": "\U00002705", - "hourglass": "\U000023F3", - "wave": "\U0001F44B" + "check": "\U00002705", + "hourglass": "\U000023F3", + "wave": "\U0001F44B" } -#a set of channel names bound to their ids -CHANNELS = { - "rules": 566569408416186377, - "faq": 566618400307019776 -} - - - - -ADMIN_ROLE = ["Server Admin", "Bot Admin"] -GESTION_ROLES = ["Community Manager", "Server Admin"] -DEV_ROLES = ["Dev"] -for role in ADMIN_ROLE: - GESTION_ROLES.append(role) - DEV_ROLES.append(role) - -PUBLIC_REPOST="Public repost" - +ROLES_FILE = "roles.txt" SLAPPED_LOG_FILE = "slapped.txt" ENABLED_EXTENSIONS_FILE = "enabled_ext.txt" POLL_ALLOWED_CHANNELS_FILE = "poll_channels.txt" TODO_CHANNEL_FILE = "todo_channel.txt" TODO_TYPES_FILE = "todo_types.txt" +WELCOME_MESSAGE_FILE = "welcome_messages.txt" #data used only for Todo -> maybe remove it ? -PUBLIC_REPOST="Public repost" +PUBLIC_REPOST="Public repost" #logging settings LOGGING_HANDLER = logging.FileHandler("forebot.log", "a") LOGGING_FORMATTER = logging.Formatter("[%(asctime)s]:%(name)s:%(message)s") LOGGING_LEVEL = logging.WARNING -LOGGING_HANDLER.setFormatter(LOGGING_FORMATTER) \ No newline at end of file +LOGGING_HANDLER.setFormatter(LOGGING_FORMATTER) + +#generic error messages +ERR_NO_SUBCOMMAND = "You didn't provide any subcommand. See `::help ` for more info on command usage." +ERR_UNEXCPECTED = "An unexcpected error occured. Please report a bug in {} or contact an admin of your server.".format(DEV_SRV) +ERR_NOT_ENOUGH_ARG = "This command requires additional arguments. See `::help ` to get more information on the command's usage" +ERR_UNSUFFICIENT_PRIVILEGE = "You don't have the permission to do this..." + + + +'''#DEPRECATED +CHANNELS = { + "rules": 566569408416186377, + "faq": 566618400307019776 +} + + +#DEPRECATED +ADMIN_ROLE = ["Server Admin", "Bot Admin"] +GESTION_ROLES = ["Community Manager", "Server Admin"] +for role in ADMIN_ROLE: + GESTION_ROLES.append(role) +''' \ No newline at end of file From 8bf8fbaf3d647c2b8fd6ab7aaddf4132a5901727 Mon Sep 17 00:00:00 2001 From: NotaSmartDev Date: Wed, 29 May 2019 15:49:21 +0200 Subject: [PATCH 02/52] minor improvement --- Poll.py | 15 ++++++++++++++- settings.py | 5 +++-- 2 files changed, 17 insertions(+), 3 deletions(-) diff --git a/Poll.py b/Poll.py index dd90b5e..45cff08 100644 --- a/Poll.py +++ b/Poll.py @@ -65,8 +65,16 @@ async def on_raw_reaction_add(self, payload): #checking that user isn't the bot if (payload.user_id != self.bot.user.id) and (payload.channel_id in self.poll_allowed_chans[payload.guild_id]): + #checking wether the reaction should delete the poll + if payload.emoji.name == EMOJIS["no_entry_sign"]: + if payload.user.name == message.embeds[0].title: + return message.delete() + else: + return reaction.remove(user) + + #checking if reaction is allowed - if payload.emoji.name not in [EMOJIS["thumbsdown"],EMOJIS["thumbsup"],EMOJIS["shrug"]]: + 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 @@ -191,6 +199,11 @@ async def rm(self, ctx, msg_id): 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): + pass diff --git a/settings.py b/settings.py index 8d1567a..034aac6 100644 --- a/settings.py +++ b/settings.py @@ -9,7 +9,7 @@ PREFIX = "::" -TOKEN = "NTYzODQ4MDcxMjE0MjY4NDI5.XOUOKA.WQ4i2B_Jer-mWptaXZlrsNsMVF0" +TOKEN = "NTYzODQ4MDcxMjE0MjY4NDI5.XOUOKA.WQ4i2B_Jer-mWptaXZlrsNsMVF" RUNNER_ID=289426079544901633 #server with all of the bot's devs. Where all bug reports should be made. @@ -23,7 +23,8 @@ "wastebasket": "\U0001F5D1", "check": "\U00002705", "hourglass": "\U000023F3", - "wave": "\U0001F44B" + "wave": "\U0001F44B", + "no_entry_sign": "\U0001F6AB" } ROLES_FILE = "roles.txt" From 9a842e8eba6cf344ddf50f8faf935bc1171dc7a9 Mon Sep 17 00:00:00 2001 From: NotaSmartDev Date: Wed, 29 May 2019 16:03:36 +0200 Subject: [PATCH 03/52] changed checks.py to utilities.py --- BotEssentials.py | 4 ++-- Slapping.py | 2 +- Todo.py | 2 +- utilities.py | 33 +++++++++++++++++++++++++++++++++ 4 files changed, 37 insertions(+), 4 deletions(-) create mode 100644 utilities.py diff --git a/BotEssentials.py b/BotEssentials.py index adfec05..12ee304 100644 --- a/BotEssentials.py +++ b/BotEssentials.py @@ -1,7 +1,7 @@ import logging from settings import * import discord -from checks import * +from cheutks import * @@ -12,7 +12,7 @@ # # # # ######################################### -local_logger = logging.getLogger(__name__) +loutal_logger = logging.getLogger(__name__) local_logger.setLevel(LOGGING_LEVEL) local_logger.addHandler(LOGGING_HANDLER) local_logger.info("Innitalized {} logger".format(__name__)) diff --git a/Slapping.py b/Slapping.py index 658d9b2..3f4e26d 100644 --- a/Slapping.py +++ b/Slapping.py @@ -1,7 +1,7 @@ import logging import discord from settings import * -from checks import * +from utilities import * ######################################### # # diff --git a/Todo.py b/Todo.py index d8f6af5..f296228 100644 --- a/Todo.py +++ b/Todo.py @@ -1,7 +1,7 @@ import logging import discord from settings import * -from checks import * +from utilities import * ######################################### # # diff --git a/utilities.py b/utilities.py new file mode 100644 index 0000000..61b09a0 --- /dev/null +++ b/utilities.py @@ -0,0 +1,33 @@ +import logging +from settings import * +from discord.ext import commands + + +######################################### +# # +# # +# Setting up logging # +# # +# # +######################################### +local_logger = logging.getLogger(__name__) +local_logger.setLevel(LOGGING_LEVEL) +local_logger.addHandler(LOGGING_HANDLER) +local_logger.info(f"Innitalized {__name__} logger") + + +######################################### +# # +# # +# Checks # +# # +# # +######################################### + +def is_runner(): + def check_condition(ctx): + return ctx.message.author.id ==RUNNER_ID + result = commands.check(check_condition) + if result == False: + ctx.send(ERR_UNSUFFICIENT_PRIVILEGE) + return result \ No newline at end of file From f20000ae44d1c854dd4a5142c319cd3a869fce8e Mon Sep 17 00:00:00 2001 From: NotaSmartDev Date: Thu, 30 May 2019 02:03:26 +0200 Subject: [PATCH 04/52] added Poll configuration. First step toward #4 --- BotEssentials.py | 4 +- Config.py | 125 ++++++++++++++++++++++++++++++++++++++--------- Embedding.py | 2 +- Poll.py | 2 +- Role.py | 2 +- executioner.py | 4 +- settings.py | 12 +++-- utilities.py | 26 +++++++++- 8 files changed, 144 insertions(+), 33 deletions(-) diff --git a/BotEssentials.py b/BotEssentials.py index 12ee304..8a32ff6 100644 --- a/BotEssentials.py +++ b/BotEssentials.py @@ -1,7 +1,7 @@ import logging from settings import * import discord -from cheutks import * +from utilities import * @@ -12,7 +12,7 @@ # # # # ######################################### -loutal_logger = logging.getLogger(__name__) +local_logger = logging.getLogger(__name__) local_logger.setLevel(LOGGING_LEVEL) local_logger.addHandler(LOGGING_HANDLER) local_logger.info("Innitalized {} logger".format(__name__)) diff --git a/Config.py b/Config.py index 7c01d1e..976124d 100644 --- a/Config.py +++ b/Config.py @@ -1,7 +1,7 @@ import logging import discord from settings import * -from checks import * +from utilities import * ######################################### # # @@ -31,31 +31,124 @@ class Config(commands.Cog): preferences directly from discord.""" def __init__(self, bot): self.bot = bot - + #change to make it cross-server + self.config_channel=None + + #other values can't be added as of now + self.allowed_answers = {1:["yes", "y"], + 0:["no", "n"]} + + + @commands.group() - @is_owner() + @commands.is_owner() async def cfg(self, ctx): - if invoked_subcommand == None: + if ctx.invoked_subcommand == None: ctx.send(ERROR_NO_SUBCOMMAND) @cfg.command() async def init(self, ctx): - pass + #creating new hidden channel only the owner can see + overwrites = { + ctx.guild.default_role: discord.PermissionOverwrite(read_messages=False), + ctx.guild.owner : discord.PermissionOverwrite(read_messages=True) + } + self.config_channel = await ctx.guild.create_text_channel("cli-bot-config") + + #starting all configurations + await ctx.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\n*Starting full configuration...*''') + + try: + await self.cfg_poll(ctx) + local_logger.info("Setup is done") + + except Exception as e: + await ctx.send(ERR_UNEXCPECTED.format(None)) + await ctx.send("Dropping cofiguration and rolling back unconfirmed changes.") + #await self.config_channel.delete(reason="Failed to interactively configure the bot") + local_logger.exception(e) + + + - @cfg.command() async def chg(self, ctx, setting): try: eval("self.chg_"+setting) except Exception as e: - local_logger.exception(ERR_UNEXCPECTED) + local_logger.exception(e) + + + async def is_yn_answer(self, ctx): + if (ctx.channel == self.config_channel) and (ctx.content in (self.allowed_answers[0] or self.allowed_answers[1])): return True + return False + + async def is_answer(self, ctx): + if ctx.channel == self.config_channel: return True + return False @cfg.command() - async def leave(self, ctx)-> int: + async def leave(self, ctx): ctx.send("You are about to remove the bot from the server. This will erase all of your configuration from the mainframe and you won't be able to recover the bot without getting another invite. Are you sure you want to continue ? (y/N)") async def cfg_poll(self, ctx): - pass + try: + await self.config_channel.send("**Starting poll configuration**") + await self.config_channel.send("Do you want to activate polls on this server ? [yes/no]") + #awaiting the user response + response = await self.bot.wait_for("message", check=self.is_yn_answer) + if not response.content[0].lower() =="y": return False + + await self.config_channel.send("List all the channels you want to use as poll channels. Only put the channel mentions in your answer") + response = await self.bot.wait_for("message", check=self.is_answer) + poll_channels = response.channel_mentions + local_logger.info((response.channel_mentions, "which makes", poll_channels)) + poll_channels_str = "" + for chan in response.channel_mentions: + poll_channels_str+= " "+chan.mention + + await self.config_channel.send(f"You are about to make {poll_channels_str} poll channels. Do you want to continue? [y/n]") + + response = await self.bot.wait_for("message", check=self.is_yn_answer) + if not response.content[0].lower() =="y": return False + + #making the data to be saved + with open(POLL_ALLOWED_CHANNELS_FILE, "r") as file: + to_write = [] + for line in file.readlines(): + '''the file is organized like this: + guild_id;poll_chan_1_id;poll_chan_2_id;\n''' + segments = line.split(";") + if not segments[0]==ctx.guild.id: + to_write.append(line) + + guild_chans = f"{ctx.guild.id};" + for chan in response.channel_mentions: + guild_chans+= f"{chan.id};" + guild_chans+="\n" + + to_write.append(guild_chans) + + #writting to the file + write_str = "" + for line in to_write: + write_str+=line + with open(POLL_ALLOWED_CHANNELS_FILE, "w") as file: + file.write(write_str) + + await self.config_channel.send("Poll configuration is done.") + + + + + + local_logger.info(response) + + except Exception as e: + local_logger.exception(e) + + + async def cfg_roll(self, ctx): pass @@ -69,18 +162,6 @@ async def cfg_welcome(self, ctx): async def yes_no_answer(self, ctx): pass - @commands.Cog.listener() - #is it possible to add checks to listeners ? (is_owner and in_channel) - async def on_message(self, message): - - #checking that the msg isn't from the bot, in the cfg channel and from the guild owner - #change with a list of IDs that represent the in-progress configuration channels - if self.message.name!="foreman-configuration-channel": return - if message.author==self.bot.user: return - #is it possible to add a check inside a code block ? - if message.author != message.guild.owner: return - - def setup(bot): - bot.add_cog(Poll(bot)) \ No newline at end of file + bot.add_cog(Config(bot)) \ No newline at end of file diff --git a/Embedding.py b/Embedding.py index 1cd80b5..60ff119 100644 --- a/Embedding.py +++ b/Embedding.py @@ -1,7 +1,7 @@ import logging import discord from settings import * -from checks import * +from utilities import * ######################################### # # diff --git a/Poll.py b/Poll.py index 45cff08..86f74f4 100644 --- a/Poll.py +++ b/Poll.py @@ -2,7 +2,7 @@ import logging import discord from settings import * -from checks import * +from utilities import * ######################################### # # diff --git a/Role.py b/Role.py index c88d165..221cc50 100644 --- a/Role.py +++ b/Role.py @@ -1,7 +1,7 @@ import logging from settings import * import discord -from checks import * +from utilities import * ######################################### # # diff --git a/executioner.py b/executioner.py index ad60384..4181938 100644 --- a/executioner.py +++ b/executioner.py @@ -3,7 +3,7 @@ import discord from discord.utils import find as dfind from settings import * -from checks import * +from utilities import * import math import time import random @@ -135,7 +135,7 @@ async def rm(ctx, extension:str): main_logger.info("Loaded {}".format(ext)) except Exception as e: - main_loggerre.exception(e) + main_logger.exception(e) raise e #if no extension is enabled diff --git a/settings.py b/settings.py index 034aac6..31e25b9 100644 --- a/settings.py +++ b/settings.py @@ -9,12 +9,15 @@ PREFIX = "::" -TOKEN = "NTYzODQ4MDcxMjE0MjY4NDI5.XOUOKA.WQ4i2B_Jer-mWptaXZlrsNsMVF" +TOKEN = "NTYzODQ4MDcxMjE0MjY4NDI5.XO8J4w.7m10-9uaESNdWTExQtK69LNdx7I" RUNNER_ID=289426079544901633 #server with all of the bot's devs. Where all bug reports should be made. DEV_SRV = 564213369834307600 +#github website URL +WEBSITE = "https://github.com/organic-bots/ForeBot" + #emojis dict. May be possible to change incomprehensible unicode to other strings recognized by discord EMOJIS = { "thumbsup": "\U0001f44d", @@ -38,15 +41,18 @@ #data used only for Todo -> maybe remove it ? PUBLIC_REPOST="Public repost" +#TEMP +GESTION_ROLES = ["Community Manager", "Server Admin"] + #logging settings LOGGING_HANDLER = logging.FileHandler("forebot.log", "a") LOGGING_FORMATTER = logging.Formatter("[%(asctime)s]:%(name)s:%(message)s") -LOGGING_LEVEL = logging.WARNING +LOGGING_LEVEL = logging.INFO LOGGING_HANDLER.setFormatter(LOGGING_FORMATTER) #generic error messages ERR_NO_SUBCOMMAND = "You didn't provide any subcommand. See `::help ` for more info on command usage." -ERR_UNEXCPECTED = "An unexcpected error occured. Please report a bug in {} or contact an admin of your server.".format(DEV_SRV) +ERR_UNEXCPECTED = "An unexcpected error occured. Please report a bug in {} or contact an admin of your server." ERR_NOT_ENOUGH_ARG = "This command requires additional arguments. See `::help ` to get more information on the command's usage" ERR_UNSUFFICIENT_PRIVILEGE = "You don't have the permission to do this..." diff --git a/utilities.py b/utilities.py index 61b09a0..d13b46a 100644 --- a/utilities.py +++ b/utilities.py @@ -30,4 +30,28 @@ def check_condition(ctx): result = commands.check(check_condition) if result == False: ctx.send(ERR_UNSUFFICIENT_PRIVILEGE) - return result \ No newline at end of file + return result + +def yes_no_ans(): + async def check_condition(ctx): + return ctx.message.content == ("yes" or "no") + return commands.check(check_condition) + + + +######################################### +# # +# # +# Utility functions # +# # +# # +######################################### + +def load_roles(): + roles_dict = {} + with open(ROLES_FILE, "r") as file: + for line in file.readlines(): + segments = line.split(";") + guild_id = int(segments[0]) + + From fa33ad2efd3c9bcf7e27e259e32dd24d64644963 Mon Sep 17 00:00:00 2001 From: NotaSmartDev Date: Thu, 30 May 2019 02:28:18 +0200 Subject: [PATCH 05/52] fixed minor issues --- Config.py | 13 +++++++++---- settings.py | 2 +- 2 files changed, 10 insertions(+), 5 deletions(-) diff --git a/Config.py b/Config.py index 976124d..693d63b 100644 --- a/Config.py +++ b/Config.py @@ -117,22 +117,27 @@ async def cfg_poll(self, ctx): to_write = [] for line in file.readlines(): '''the file is organized like this: - guild_id;poll_chan_1_id;poll_chan_2_id;\n''' + \nguild_id;poll_chan_1_id;poll_chan_2_id;''' segments = line.split(";") - if not segments[0]==ctx.guild.id: + if not int(segments[0])==ctx.guild.id: to_write.append(line) guild_chans = f"{ctx.guild.id};" - for chan in response.channel_mentions: + for chan in poll_channels: guild_chans+= f"{chan.id};" - guild_chans+="\n" + + #removing the last ";" to prevent Poll from trying to convert it to an int + guild_chans = guild_chans[:-1] + "\n" to_write.append(guild_chans) + local_logger.info(str(guild_chans)) #writting to the file write_str = "" for line in to_write: write_str+=line + + local_logger.info(write_str) with open(POLL_ALLOWED_CHANNELS_FILE, "w") as file: file.write(write_str) diff --git a/settings.py b/settings.py index 31e25b9..a7c8502 100644 --- a/settings.py +++ b/settings.py @@ -9,7 +9,7 @@ PREFIX = "::" -TOKEN = "NTYzODQ4MDcxMjE0MjY4NDI5.XO8J4w.7m10-9uaESNdWTExQtK69LNdx7I" +TOKEN = "your_token" RUNNER_ID=289426079544901633 #server with all of the bot's devs. Where all bug reports should be made. From 8589c25f0cd472815490ded6b8f2e878494a858c Mon Sep 17 00:00:00 2001 From: NotaSmartDev Date: Thu, 30 May 2019 02:42:14 +0200 Subject: [PATCH 06/52] multiple servers can now be configured simultaneously --- Config.py | 26 ++++++++++++-------------- 1 file changed, 12 insertions(+), 14 deletions(-) diff --git a/Config.py b/Config.py index 693d63b..190bc64 100644 --- a/Config.py +++ b/Config.py @@ -32,7 +32,7 @@ class Config(commands.Cog): def __init__(self, bot): self.bot = bot #change to make it cross-server - self.config_channel=None + self.config_channels={} #other values can't be added as of now self.allowed_answers = {1:["yes", "y"], @@ -53,24 +53,22 @@ async def init(self, ctx): ctx.guild.default_role: discord.PermissionOverwrite(read_messages=False), ctx.guild.owner : discord.PermissionOverwrite(read_messages=True) } - self.config_channel = await ctx.guild.create_text_channel("cli-bot-config") + self.config_channels[ctx.guild.id] = await ctx.guild.create_text_channel("cli-bot-config") #starting all configurations await ctx.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\n*Starting full configuration...*''') try: await self.cfg_poll(ctx) - local_logger.info("Setup is done") + local_logger.info(f"Setup for server {ctx.guild.name}({ctx.guild.id}) is done") except Exception as e: await ctx.send(ERR_UNEXCPECTED.format(None)) - await ctx.send("Dropping cofiguration and rolling back unconfirmed changes.") - #await self.config_channel.delete(reason="Failed to interactively configure the bot") + await ctx.send("Dropping configuration and rolling back unconfirmed changes.") + #await self.config_channels[ctx.guild.id].delete(reason="Failed to interactively configure the bot") local_logger.exception(e) - - async def chg(self, ctx, setting): try: eval("self.chg_"+setting) @@ -80,11 +78,11 @@ async def chg(self, ctx, setting): async def is_yn_answer(self, ctx): - if (ctx.channel == self.config_channel) and (ctx.content in (self.allowed_answers[0] or self.allowed_answers[1])): return True + if (ctx.channel == self.config_channels[ctx.guild.id]) and (ctx.content in (self.allowed_answers[0] or self.allowed_answers[1])): return True return False async def is_answer(self, ctx): - if ctx.channel == self.config_channel: return True + if ctx.channel == self.config_channels[ctx.guild.id]: return True return False @cfg.command() @@ -93,13 +91,13 @@ async def leave(self, ctx): async def cfg_poll(self, ctx): try: - await self.config_channel.send("**Starting poll configuration**") - await self.config_channel.send("Do you want to activate polls on this server ? [yes/no]") + await self.config_channels[ctx.guild.id].send("**Starting poll configuration**") + await self.config_channels[ctx.guild.id].send("Do you want to activate polls on this server ? [yes/no]") #awaiting the user response response = await self.bot.wait_for("message", check=self.is_yn_answer) if not response.content[0].lower() =="y": return False - await self.config_channel.send("List all the channels you want to use as poll channels. Only put the channel mentions in your answer") + await self.config_channels[ctx.guild.id].send("List all the channels you want to use as poll channels. Only put the channel mentions in your answer") response = await self.bot.wait_for("message", check=self.is_answer) poll_channels = response.channel_mentions local_logger.info((response.channel_mentions, "which makes", poll_channels)) @@ -107,7 +105,7 @@ async def cfg_poll(self, ctx): for chan in response.channel_mentions: poll_channels_str+= " "+chan.mention - await self.config_channel.send(f"You are about to make {poll_channels_str} poll channels. Do you want to continue? [y/n]") + await self.config_channels[ctx.guild.id].send(f"You are about to make {poll_channels_str} poll channels. Do you want to continue? [y/n]") response = await self.bot.wait_for("message", check=self.is_yn_answer) if not response.content[0].lower() =="y": return False @@ -141,7 +139,7 @@ async def cfg_poll(self, ctx): with open(POLL_ALLOWED_CHANNELS_FILE, "w") as file: file.write(write_str) - await self.config_channel.send("Poll configuration is done.") + await self.config_channels[ctx.guild.id].send("Poll configuration is done.") From 59e4d6d0af832ab446542c5261d8065bc8cf8466 Mon Sep 17 00:00:00 2001 From: NotaSmartDev Date: Thu, 30 May 2019 11:27:00 +0200 Subject: [PATCH 07/52] added a way to unload extensions (#23 ) & prevented the same extension to appear multiple times in enabled file --- checks.py | 33 --------------------------------- executioner.py | 38 +++++++++++++++++++++++++++++++------- 2 files changed, 31 insertions(+), 40 deletions(-) delete mode 100644 checks.py diff --git a/checks.py b/checks.py deleted file mode 100644 index 61b09a0..0000000 --- a/checks.py +++ /dev/null @@ -1,33 +0,0 @@ -import logging -from settings import * -from discord.ext import commands - - -######################################### -# # -# # -# Setting up logging # -# # -# # -######################################### -local_logger = logging.getLogger(__name__) -local_logger.setLevel(LOGGING_LEVEL) -local_logger.addHandler(LOGGING_HANDLER) -local_logger.info(f"Innitalized {__name__} logger") - - -######################################### -# # -# # -# Checks # -# # -# # -######################################### - -def is_runner(): - def check_condition(ctx): - return ctx.message.author.id ==RUNNER_ID - result = commands.check(check_condition) - if result == False: - ctx.send(ERR_UNSUFFICIENT_PRIVILEGE) - return result \ No newline at end of file diff --git a/executioner.py b/executioner.py index 4181938..accb8d0 100644 --- a/executioner.py +++ b/executioner.py @@ -74,9 +74,19 @@ async def add(ctx, extension:str): #if the extension was correctly loaded, adding it to the enabled file try: - #appending new extension to ENABLED_EXTENSIONS_FILE - with open(ENABLED_EXTENSIONS_FILE, "a") as file: - file.write("{}\n".format(extension)) + #fetching laready enabled extensions + with open(ENABLED_EXTENSIONS_FILE, "r") as file: + to_write = "" + for line in file.readlines(): + if line[:-1]==extension: + continue + to_write+=line + + to_write+= f"{extension}\n" + + #writting to file + with open(ENABLED_EXTENSIONS_FILE, "w") as file: + file.write(to_write) except FileNotFoundError as e: #if the file didn't yet exist a new one will be created. This should not happen, only here as a failsafe @@ -90,10 +100,12 @@ async def add(ctx, extension:str): raise e await ctx.send("Successfully added and loadded {}".format(extension)) + + @ext.command() async def rm(ctx, extension:str): try: - bot.unload(extension) + bot.unload_extension(extension) except Exception as e: main_logger.exception(e) @@ -102,18 +114,30 @@ async def rm(ctx, extension:str): #if the extension was correctly unloaded, removing it from the enblaed extension file try: - with open(ENABLED_EXTENSIONS_FILE, "aw") as file: + with open(ENABLED_EXTENSIONS_FILE, "r") as file: lines = [] for line in file.readlines(): - if line == extension: + if line[:-1] == extension: continue lines.append(line) - file.write(lines) + + + #building new file + to_write = "" + for line in lines: + to_write+=line + + with open(ENABLED_EXTENSIONS_FILE, "w") as file: + file.write(to_write) + except Exception as e: main_logger.exception(e) await ctx.send("UnexpectedError:\tReport issue to an admin\n{}".format(e)) raise e + await ctx.send("Successfully removed and unloaded {}".format(extension)) + local_logger.info(f"Disabled and removed {extension}") + ######################################### From 5dca473d076dd729881621b158b43b196f7f55a4 Mon Sep 17 00:00:00 2001 From: NotaSmartDev Date: Thu, 30 May 2019 12:05:16 +0200 Subject: [PATCH 08/52] improved poll configuration interface --- Config.py | 51 +++++++++++++++++++++++++++++++++------------------ Poll.py | 5 ----- utilities.py | 6 ------ 3 files changed, 33 insertions(+), 29 deletions(-) diff --git a/Config.py b/Config.py index 190bc64..2b964c3 100644 --- a/Config.py +++ b/Config.py @@ -85,9 +85,6 @@ async def is_answer(self, ctx): if ctx.channel == self.config_channels[ctx.guild.id]: return True return False - @cfg.command() - async def leave(self, ctx): - ctx.send("You are about to remove the bot from the server. This will erase all of your configuration from the mainframe and you won't be able to recover the bot without getting another invite. Are you sure you want to continue ? (y/N)") async def cfg_poll(self, ctx): try: @@ -97,18 +94,29 @@ async def cfg_poll(self, ctx): response = await self.bot.wait_for("message", check=self.is_yn_answer) if not response.content[0].lower() =="y": return False - await self.config_channels[ctx.guild.id].send("List all the channels you want to use as poll channels. Only put the channel mentions in your answer") - response = await self.bot.wait_for("message", check=self.is_answer) - poll_channels = response.channel_mentions - local_logger.info((response.channel_mentions, "which makes", poll_channels)) - poll_channels_str = "" - for chan in response.channel_mentions: - poll_channels_str+= " "+chan.mention + retry = True + while retry: + await self.config_channels[ctx.guild.id].send("List all the channels you want to use as poll channels.") + response = await self.bot.wait_for("message", check=self.is_answer) + poll_channels = response.channel_mentions + local_logger.info((response.channel_mentions, "which makes", poll_channels)) + poll_channels_str = "" + for chan in response.channel_mentions: + poll_channels_str+= " "+chan.mention + + await self.config_channels[ctx.guild.id].send(f"You are about to make {poll_channels_str} poll channels. Do you want to continue? [y/n]") + + response = await self.bot.wait_for("message", check=self.is_yn_answer) + #wether the asnwer was positive + if not response.content[0].lower() =="y": + #making sure the user really wants to cancel poll configuration + self.config_channels[ctx.guild.id].send("Aborting addition of poll channels. Do you want to leave the poll configuration interface ? [y/n]") + response = await self.bot.wait_for("message", check=self.is_yn_answer) + if response.content[0].lower()=="y": + retry = False + - await self.config_channels[ctx.guild.id].send(f"You are about to make {poll_channels_str} poll channels. Do you want to continue? [y/n]") - response = await self.bot.wait_for("message", check=self.is_yn_answer) - if not response.content[0].lower() =="y": return False #making the data to be saved with open(POLL_ALLOWED_CHANNELS_FILE, "r") as file: @@ -142,19 +150,20 @@ async def cfg_poll(self, ctx): await self.config_channels[ctx.guild.id].send("Poll configuration is done.") + local_logger.info(f"Configuration of poll for server {ctx.guild.name} ({ctx.guild.id}) has been completed.") + except Exception as e: + local_logger.exception(e) - local_logger.info(response) + async def cfg_roll(self, ctx): + pass + - except Exception as e: - local_logger.exception(e) - async def cfg_roll(self, ctx): - pass async def cfg_todo(self, ctx): pass @@ -165,6 +174,12 @@ async def cfg_welcome(self, ctx): async def yes_no_answer(self, ctx): pass + @cfg.command() + async def leave(self, ctx): + ctx.send("You are about to remove the bot from the server. This will erase all of your configuration from the mainframe and you won't be able to recover the bot without getting another invite. Are you sure you want to continue ? (y/N)") + + + def setup(bot): bot.add_cog(Config(bot)) \ No newline at end of file diff --git a/Poll.py b/Poll.py index 86f74f4..5491115 100644 --- a/Poll.py +++ b/Poll.py @@ -168,11 +168,6 @@ async def poll(self, ctx): await ctx.send("NotEnoughArguments:\tYou must provide a subcommand") -# @poll.command() -# async def add(self, ctx, channel, *args): -# pass - - @poll.command() async def rm(self, ctx, msg_id): '''allows one to delete one of their poll by issuing its id''' diff --git a/utilities.py b/utilities.py index d13b46a..9587fe2 100644 --- a/utilities.py +++ b/utilities.py @@ -32,12 +32,6 @@ def check_condition(ctx): ctx.send(ERR_UNSUFFICIENT_PRIVILEGE) return result -def yes_no_ans(): - async def check_condition(ctx): - return ctx.message.content == ("yes" or "no") - return commands.check(check_condition) - - ######################################### # # From 8c57b5815982c0eaccb479e7dbca9a38ae8f7c0b Mon Sep 17 00:00:00 2001 From: NotaSmartDev Date: Thu, 30 May 2019 16:54:26 +0200 Subject: [PATCH 09/52] added basic Roll configuration --- Config.py | 84 ++++++++++++++++++++++++++++++++++++++++++++++++----- settings.py | 14 ++++----- 2 files changed, 81 insertions(+), 17 deletions(-) diff --git a/Config.py b/Config.py index 2b964c3..a38b84b 100644 --- a/Config.py +++ b/Config.py @@ -56,10 +56,17 @@ async def init(self, ctx): self.config_channels[ctx.guild.id] = await ctx.guild.create_text_channel("cli-bot-config") #starting all configurations - await ctx.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\n*Starting full configuration...*''') - + 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\n*Starting full configuration...*''') + await self.config_channels[ctx.guild.id].send("This will overwrite all of your existing configurations. Do you want to continue ? [y/n]") + response = self.bot.wait_for("message", check=self.is_yn_answer) + if response[0].lower() == "n":return False + await self.config_channels[ctx.guild.id].send("**Starting full bot configuration...**") + try: await self.cfg_poll(ctx) + await self.config_channels[ctx.guild.id].send("Role setup is **mendatory** for the bot to work correctly. Otherise no one will be able to use administration commands.") + await self.cfg_roll(ctx) + local_logger.info(f"Setup for server {ctx.guild.name}({ctx.guild.id}) is done") except Exception as e: @@ -96,10 +103,12 @@ async def cfg_poll(self, ctx): retry = True while retry: + #getting the list of channels to be marked polls await self.config_channels[ctx.guild.id].send("List all the channels you want to use as poll channels.") response = await self.bot.wait_for("message", check=self.is_answer) poll_channels = response.channel_mentions - local_logger.info((response.channel_mentions, "which makes", poll_channels)) + + #building string with all the channels that will be marked for polls poll_channels_str = "" for chan in response.channel_mentions: poll_channels_str+= " "+chan.mention @@ -113,11 +122,10 @@ async def cfg_poll(self, ctx): self.config_channels[ctx.guild.id].send("Aborting addition of poll channels. Do you want to leave the poll configuration interface ? [y/n]") response = await self.bot.wait_for("message", check=self.is_yn_answer) if response.content[0].lower()=="y": + local_logger.info(f"Poll configuration has been cancelled for server {ctx.guild.name}") retry = False - - #making the data to be saved with open(POLL_ALLOWED_CHANNELS_FILE, "r") as file: to_write = [] @@ -157,7 +165,69 @@ async def cfg_poll(self, ctx): async def cfg_roll(self, ctx): - pass + try: + #introducing the clearance levels the bot uses + await self.config_channels[ctx.guild.id].send("**Starting role configuration**") + await self.config_channels[ctx.guild.ig].send("This bot uses two level of clearance for its commands.") + await self.config_channels[ctx.guild.ig].send("The 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.") + await self.config_channels[ctx.guild.ig].send("The second level of clearance is **admin**. Anyonw 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: + new_role = [] + #asking the owner which roles he wants to give clearance to + await self.config_channels[ctx.guild.ig].send(f"List all the roles you want to be given the **{role_lvl}** level of clearance.") + response = self.bot.wait_for("message", check=self.is_answer) + roles = response.role_mentions + + #building roll string + roles_str = "" + for role in roles: + roles_str+= f" {role}" + + #asking for confirmation + await self.config_channels[ctx.guild.id].send(f"You are about to give{roles_str} roles the **{role_lvl}** level of clearance. Do you confirm this ? [y/n]") + response = self.bot.wait_for("message", check=self.is_yn_answer) + if repsonse[0].lower() == "n": return False + local_logger.info(f"Server {ctx.guild.name} configured its {role_lvl} roles") + + for role in roles: + new_role.append(role.id) + + #writting configuration to file + with open(ROLES_FILE, "r") as file: + '''the file is written like this: + guild_id;management_role_1|management_role_2;admin_role_1|admin_role_2''' + for line in file.readlines(): + segments = line.split(";") + to_write = "" + if int(segments[0]) == ctx.guild.id: + continue + #if the line isn't defining the server's role settings, marking it for rewrite + to_write += line + + #building the roles lists + for m_role in new_roles[0]: + new_roles[1].append(m_role) + + #adding management roles + guild_line = f"{ctx.guild.id};" + for role in new_roles[0]: + guild_line += f"{role};" + + #adding weak seperator + guild_line+="|" + + #adding admin roles + for role in new_roles[1]: + guild_line += f"{role};" + + + + + except Exception as e: + local_logger.exception(e) + raise e @@ -171,8 +241,6 @@ async def cfg_todo(self, ctx): async def cfg_welcome(self, ctx): pass - async def yes_no_answer(self, ctx): - pass @cfg.command() async def leave(self, ctx): diff --git a/settings.py b/settings.py index a7c8502..5a54ae6 100644 --- a/settings.py +++ b/settings.py @@ -41,8 +41,9 @@ #data used only for Todo -> maybe remove it ? PUBLIC_REPOST="Public repost" -#TEMP -GESTION_ROLES = ["Community Manager", "Server Admin"] +#rolls +ROLES_LEVEL = ["manager", "admin"] + #logging settings LOGGING_HANDLER = logging.FileHandler("forebot.log", "a") @@ -63,11 +64,6 @@ "rules": 566569408416186377, "faq": 566618400307019776 } +''' - -#DEPRECATED -ADMIN_ROLE = ["Server Admin", "Bot Admin"] -GESTION_ROLES = ["Community Manager", "Server Admin"] -for role in ADMIN_ROLE: - GESTION_ROLES.append(role) -''' \ No newline at end of file + For a complete list of all commands enbaled for this clearance see {WEBSITE} \ No newline at end of file From cc28a65dcc6ff62c9cc358d84396c0029118540b Mon Sep 17 00:00:00 2001 From: NotaSmartDev Date: Sat, 1 Jun 2019 00:28:22 +0200 Subject: [PATCH 10/52] added server-specific Slapping, fixing #22 --- Slapping.py | 71 ++++++++++++++++++++++++++++++++++++----------------- 1 file changed, 49 insertions(+), 22 deletions(-) diff --git a/Slapping.py b/Slapping.py index 3f4e26d..bb57eb5 100644 --- a/Slapping.py +++ b/Slapping.py @@ -42,18 +42,30 @@ async def slap(self, ctx, member:discord.Member): with open(SLAPPED_LOG_FILE, "r") as file: content = file.readlines() for line in content: - if line.startswith(str(member.id)): - slap_count = int(line.split(";")[1])+1 - to_write+= "{};{}\n".format(member.id, slap_count) + #finding the right server + if line.startswith(str(ctx.guild.id)): + #looking for the user in the guild slaps log + guild_line = "" + for user in line.split(";")[:1]: + #whether the user if the member + if int(user.split("|")[0]) == member.id: + slap_count = int(user.split("|")[1])+1 + guild_line+=f"{member.id}|{slap_count};" + continue + + #if the user does not match + guild_line+=user + + #if the user wasn't found + if slap_count==0: + guild_line+=f"{member.id}|1;" + + #removing the last ";" and appending a line return + to_write+=guild_line[-1]+"\n" else: to_write += line - #creates a log for the member if he's never been slapped - if slap_count==0: - slap_count = 1 - to_write += "{};{}\n".format(member.id, slap_count) - await ctx.send("{} you've been slapped by {} because of your behavior! This is the {} time. Be careful, if you get slapped too much there *will* be consequences !".format(member.mention, ctx.message.author.mention, slap_count)) @@ -70,22 +82,37 @@ async def pardon(self, ctx, member:discord.Member, nbr=0): #reads the file and prepares logging of slaps with open(SLAPPED_LOG_FILE, "r") as file: - content = file.readlines() - for line in content: - if not line.startswith(str(member.id)): + for line in file.readlines(): + if not line.startswith(str(ctx.guild.id)): to_write+=line + continue + + #looking for the user + guild_line = "" + for user in line.split(";")[:1]: + #whether the user is member + if int(user.split("|")[0]) == member.id: + local_logger.info(f"Found user {member.name} who has ot be pardonnned") + crt_slaps = int(user.split("|")[1]) + #pardonnning the user + if crt_slaps Date: Sat, 1 Jun 2019 00:32:07 +0200 Subject: [PATCH 11/52] cleaned some loosy syntax --- Slapping.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/Slapping.py b/Slapping.py index bb57eb5..7c5f247 100644 --- a/Slapping.py +++ b/Slapping.py @@ -110,9 +110,7 @@ async def pardon(self, ctx, member:discord.Member, nbr=0): guild_line+=user #removing the last ";" and appending a line return - to_write+=guild_line[-1]+"\n" - - + to_write+=guild_line[-1]+"\n" #writting updated file From 7ce262b599e3a6b01004e923f4a8eb7591da2812 Mon Sep 17 00:00:00 2001 From: NotaSmartDev Date: Sat, 1 Jun 2019 00:52:15 +0200 Subject: [PATCH 12/52] added partial role configuration option --- Config.py | 61 ++++++++++++++++++++++++++++++++++++------------------- 1 file changed, 40 insertions(+), 21 deletions(-) diff --git a/Config.py b/Config.py index a38b84b..727638d 100644 --- a/Config.py +++ b/Config.py @@ -174,26 +174,38 @@ async def cfg_roll(self, ctx): new_roles = [] for role_lvl in ROLES_LEVEL: - new_role = [] - #asking the owner which roles he wants to give clearance to - await self.config_channels[ctx.guild.ig].send(f"List all the roles you want to be given the **{role_lvl}** level of clearance.") - response = self.bot.wait_for("message", check=self.is_answer) - roles = response.role_mentions - - #building roll string - roles_str = "" - for role in roles: - roles_str+= f" {role}" + retry = True + while retry: + new_role = [] + #asking the owner which roles he wants to give clearance to + await self.config_channels[ctx.guild.ig].send(f"List all the roles you want to be given the **{role_lvl}** level of clearance.") + response = self.bot.wait_for("message", check=self.is_answer) + roles = response.role_mentions + + #building roll string + roles_str = "" + for role in roles: + roles_str+= f" {role}" + + #asking for confirmation + await self.config_channels[ctx.guild.id].send(f"You are about to give{roles_str} roles the **{role_lvl}** level of clearance. Do you confirm this ? [y/n]") + response = self.bot.wait_for("message", check=self.is_yn_answer) + if repsonse[0].lower() == "n": + await self.config_channels[ctx.guild.id].send(f"Aborting configuration of {role_lvl}. Do you want to retry? [y/n]") + response = self.bot.wait_for("message", check=self.is_yn_answer) + if response[0].lower() == "n": + local_logger.info(f"The configuration for the {role_lvl} clearance has been cancelled for server {ctx.guild.name}") + retry = False + - #asking for confirmation - await self.config_channels[ctx.guild.id].send(f"You are about to give{roles_str} roles the **{role_lvl}** level of clearance. Do you confirm this ? [y/n]") - response = self.bot.wait_for("message", check=self.is_yn_answer) - if repsonse[0].lower() == "n": return 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) + #writting configuration to file with open(ROLES_FILE, "r") as file: '''the file is written like this: @@ -206,21 +218,28 @@ async def cfg_roll(self, ctx): #if the line isn't defining the server's role settings, marking it for rewrite to_write += line - #building the roles lists - for m_role in new_roles[0]: - new_roles[1].append(m_role) + #giving admin roles the manager clearance + for m_role in new_roles[1]: + new_roles[0].append(m_role) #adding management roles guild_line = f"{ctx.guild.id};" for role in new_roles[0]: - guild_line += f"{role};" + guild_line += f"{role}|" - #adding weak seperator - guild_line+="|" + #adding strong separator between clearance levels + guild_line+=";" #adding admin roles for role in new_roles[1]: - guild_line += f"{role};" + guild_line += f"{role}|" + + #addind the modified guild role configuration + to_write+= guild_line[:-1]+"\n" + + #writting to file + with open(ROLES_FILE, "w") as file: + file.write(to_write) From d00ae4c68d3879e28ecd8b795c99a0d88a642141 Mon Sep 17 00:00:00 2001 From: NotaSmartDev Date: Sun, 2 Jun 2019 10:45:29 +0200 Subject: [PATCH 13/52] added file modification time utilities --- utilities.py | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/utilities.py b/utilities.py index 9587fe2..44e2d24 100644 --- a/utilities.py +++ b/utilities.py @@ -1,4 +1,5 @@ import logging +import os from settings import * from discord.ext import commands @@ -41,11 +42,11 @@ def check_condition(ctx): # # ######################################### -def load_roles(): - roles_dict = {} - with open(ROLES_FILE, "r") as file: - for line in file.readlines(): - segments = line.split(";") - guild_id = int(segments[0]) - +def get_m_time(file): + return os.getmtime(file+".conf") +def has_changed(server, last_time): + last_update = get_m_time(file) + if last_update != last_time: + return True + return False \ No newline at end of file From 5477a83fb39110e007bd437cf5f95ac74f0b3ea4 Mon Sep 17 00:00:00 2001 From: NotaSmartDev Date: Sun, 2 Jun 2019 12:52:42 +0200 Subject: [PATCH 14/52] changed Config to use the new configuration structure (see #4 ) --- Config.py | 87 +++++++++++++++------------------------------------- settings.py | 20 +----------- utilities.py | 38 +++++++++++++++++++++-- 3 files changed, 62 insertions(+), 83 deletions(-) diff --git a/Config.py b/Config.py index 727638d..64f104c 100644 --- a/Config.py +++ b/Config.py @@ -46,6 +46,7 @@ async def cfg(self, ctx): if ctx.invoked_subcommand == None: ctx.send(ERROR_NO_SUBCOMMAND) + @cfg.command() async def init(self, ctx): #creating new hidden channel only the owner can see @@ -55,6 +56,11 @@ async def init(self, ctx): } self.config_channels[ctx.guild.id] = await ctx.guild.create_text_channel("cli-bot-config") + #making conf file if it doesn't exist + if not is_init(): + with open(f"{ctx.guild.id}.json", "w") as file: + pass + #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\n*Starting full configuration...*''') await self.config_channels[ctx.guild.id].send("This will overwrite all of your existing configurations. Do you want to continue ? [y/n]") @@ -76,6 +82,8 @@ async def init(self, ctx): local_logger.exception(e) + @cfg.command() + @is_init() async def chg(self, ctx, setting): try: eval("self.chg_"+setting) @@ -110,7 +118,7 @@ async def cfg_poll(self, ctx): #building string with all the channels that will be marked for polls poll_channels_str = "" - for chan in response.channel_mentions: + for chan in poll_channels: poll_channels_str+= " "+chan.mention await self.config_channels[ctx.guild.id].send(f"You are about to make {poll_channels_str} poll channels. Do you want to continue? [y/n]") @@ -126,36 +134,15 @@ async def cfg_poll(self, ctx): retry = False - #making the data to be saved - with open(POLL_ALLOWED_CHANNELS_FILE, "r") as file: - to_write = [] - for line in file.readlines(): - '''the file is organized like this: - \nguild_id;poll_chan_1_id;poll_chan_2_id;''' - segments = line.split(";") - if not int(segments[0])==ctx.guild.id: - to_write.append(line) - guild_chans = f"{ctx.guild.id};" - for chan in poll_channels: - guild_chans+= f"{chan.id};" + old_conf = get_conf(ctx.guild.id) + old_conf["poll_channels"] = poll_channels - #removing the last ";" to prevent Poll from trying to convert it to an int - guild_chans = guild_chans[:-1] + "\n" + if update_conf(ctx.guild.id, old_conf) == False: + await self.config_channels[ctx.guild.id].send(ERR_UNEXCPECTED) - to_write.append(guild_chans) - local_logger.info(str(guild_chans)) - - #writting to the file - write_str = "" - for line in to_write: - write_str+=line - - local_logger.info(write_str) - with open(POLL_ALLOWED_CHANNELS_FILE, "w") as file: - file.write(write_str) - - await self.config_channels[ctx.guild.id].send("Poll configuration is done.") + else: + await self.config_channels[ctx.guild.id].send("Poll configuration is done.") local_logger.info(f"Configuration of poll for server {ctx.guild.name} ({ctx.guild.id}) has been completed.") @@ -206,42 +193,22 @@ async def cfg_roll(self, ctx): #adding to master role list new_roles.append(new_role) - #writting configuration to file - with open(ROLES_FILE, "r") as file: - '''the file is written like this: - guild_id;management_role_1|management_role_2;admin_role_1|admin_role_2''' - for line in file.readlines(): - segments = line.split(";") - to_write = "" - if int(segments[0]) == ctx.guild.id: - continue - #if the line isn't defining the server's role settings, marking it for rewrite - to_write += line #giving admin roles the manager clearance for m_role in new_roles[1]: new_roles[0].append(m_role) - #adding management roles - guild_line = f"{ctx.guild.id};" - for role in new_roles[0]: - guild_line += f"{role}|" - - #adding strong separator between clearance levels - guild_line+=";" + old_conf = get_conf(ctx.guild.id) - #adding admin roles - for role in new_roles[1]: - guild_line += f"{role}|" - - #addind the modified guild role configuration - to_write+= guild_line[:-1]+"\n" - - #writting to file - with open(ROLES_FILE, "w") as file: - file.write(to_write) + #updating the values + old_conf["roles"]["manager"] = new_roles[0] + old_conf["roles"]["admin"] = new_roles[1] + if update_conf(ctx.guild.id, old_conf) == False: + await self.config_channels[ctx.guild.id]send(ERR_UNEXCPECTED) + else: + await self.config_channels[ctx.guild.id]send("Successfully updated role configuration") except Exception as e: @@ -249,16 +216,12 @@ async def cfg_roll(self, ctx): raise e - - - - - async def cfg_todo(self, ctx): pass + async def cfg_welcome(self, ctx): - pass + @cfg.command() diff --git a/settings.py b/settings.py index 5a54ae6..e717a09 100644 --- a/settings.py +++ b/settings.py @@ -30,14 +30,6 @@ "no_entry_sign": "\U0001F6AB" } -ROLES_FILE = "roles.txt" -SLAPPED_LOG_FILE = "slapped.txt" -ENABLED_EXTENSIONS_FILE = "enabled_ext.txt" -POLL_ALLOWED_CHANNELS_FILE = "poll_channels.txt" -TODO_CHANNEL_FILE = "todo_channel.txt" -TODO_TYPES_FILE = "todo_types.txt" -WELCOME_MESSAGE_FILE = "welcome_messages.txt" - #data used only for Todo -> maybe remove it ? PUBLIC_REPOST="Public repost" @@ -56,14 +48,4 @@ ERR_UNEXCPECTED = "An unexcpected error occured. Please report a bug in {} or contact an admin of your server." ERR_NOT_ENOUGH_ARG = "This command requires additional arguments. See `::help ` to get more information on the command's usage" ERR_UNSUFFICIENT_PRIVILEGE = "You don't have the permission to do this..." - - - -'''#DEPRECATED -CHANNELS = { - "rules": 566569408416186377, - "faq": 566618400307019776 -} -''' - - For a complete list of all commands enbaled for this clearance see {WEBSITE} \ No newline at end of file +ERR_NOT_SETUP = "This server hasn't been configured. If you're the owner of the server you can initialise the bot by doing `::cfg init` in any channel. You won't be able to use the bot before that." \ No newline at end of file diff --git a/utilities.py b/utilities.py index 44e2d24..c7b3134 100644 --- a/utilities.py +++ b/utilities.py @@ -33,6 +33,17 @@ def check_condition(ctx): ctx.send(ERR_UNSUFFICIENT_PRIVILEGE) return result +def is_init(): + def check_condition(ctx): + conf_files = os.listdir() + file_name = f"{ctx.guild.id}.json" + return file_name in conf_files + + result = commands.check(check_condition) + if result == False: + ctx.send(ERR_NOT_SETUP) + return result + ######################################### # # @@ -43,10 +54,33 @@ def check_condition(ctx): ######################################### def get_m_time(file): - return os.getmtime(file+".conf") + return os.getmtime(file+"json") def has_changed(server, last_time): last_update = get_m_time(file) if last_update != last_time: return True - return False \ No newline at end of file + return False + +def get_conf(guild_id): + with open(f"{guild_id}.json", "r") as file: + return conf = json.load(file) + +def update_conf(guild_id, conf_dict): + try: + with open(f"{guild_id}.json", "r") as file: + json.dump(file, conf_dict) + return True + + except Exception as e: + local_logger.exception(e) + return False + +def del_conf(guild_id): + try: + os.remove(f"{guild_id}.json") + return True + + except Exception as e: + local_logger.exception(e) + return False \ No newline at end of file From 426d14284adbef361db2df512ee105ced3aa71b9 Mon Sep 17 00:00:00 2001 From: NotaSmartDev Date: Sun, 2 Jun 2019 13:29:22 +0200 Subject: [PATCH 15/52] added welcome/goodbye messages configuration --- Config.py | 94 ++++++++++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 93 insertions(+), 1 deletion(-) diff --git a/Config.py b/Config.py index 64f104c..3214312 100644 --- a/Config.py +++ b/Config.py @@ -221,7 +221,99 @@ async def cfg_todo(self, ctx): async def cfg_welcome(self, ctx): - + try: + await self.config_channels[ctx.guild.id].send("**Starting welcome message configuration**") + retry = True + + await self.config_channels[ctx.guild.id].send("Do you want to have a welcome message sent when a new user joins the server ? [y/n]") + + response = await self.bot.wait_for("message", check=self.is_yn_answer) + if response[0].lower() == "n": + message = False + retry = False + + while retry: + + await self.config_channels[ctx.guild.id].send("Enter the message you'd like to be sent to the new users. If you want to mention them use `{}`") + + message = await self.bot.wait_for("message", check=self.is_answer) + + self.config_channels[ctx.guild.id].send("To make sure the message is as you'd like I'm sending it to you.") + await self.config_channels[ctx.guild.id].send(message.format(ctx.guild.owner.mention)) + + await self.config_channels[ctx.guild.id].send("Is this the message you want to set as the welcome message ? [y/n]") + response = await self.bot.wait_for("message", check=self.is_yn_answer) + + #the user has made a mistake + if response[0].lower() == "n": + await self.config_channels[ctx.guild.id].send("Do you want to retry ? [y/n]") + response = self.bot.wait_for("message", check=self.is_yn_answer) + if response[0].lower == "n": + message = False + retry = False + #otherwise retry + continue + + old_conf = get_conf(ctx.guild.id) + old_conf["messages"]["welcome"]= message + + if update_conf(ctx.guild.id, old_conf) == False: + await self.config_channels[ctx.guild.id]send(ERR_UNEXCPECTED) + + + + except Exception as e: + local_logger.exception(e) + raise e + + + async def cfg_welcome(self, ctx): + try: + await self.config_channels[ctx.guild.id].send("**Starting goodbye message configuration**") + retry = True + + await self.config_channels[ctx.guild.id].send("Do you want to have a goodbye message sent when an user leaves the server ? [y/n]") + + response = await self.bot.wait_for("message", check=self.is_yn_answer) + if response[0].lower() == "n": + message = False + retry = False + + while retry: + + await self.config_channels[ctx.guild.id].send("Enter the message you'd like to be sent. If you want to mention them use `{}`") + + message = await self.bot.wait_for("message", check=self.is_answer) + + self.config_channels[ctx.guild.id].send("To make sure the message is as you'd like I'm sending it to you. Enventual mentions will be directed to you.") + await self.config_channels[ctx.guild.id].send(message.format(ctx.guild.owner.mention)) + + await self.config_channels[ctx.guild.id].send("Is this the message you want to set as the goodbye message ? [y/n]") + response = await self.bot.wait_for("message", check=self.is_yn_answer) + + #the user has made a mistake + if response[0].lower() == "n": + await self.config_channels[ctx.guild.id].send("Do you want to retry ? [y/n]") + response = self.bot.wait_for("message", check=self.is_yn_answer) + if response[0].lower == "n": + message = False + retry = False + #otherwise retry + continue + + old_conf = get_conf(ctx.guild.id) + old_conf["messages"]["goodbye"]= message + + if update_conf(ctx.guild.id, old_conf) == False: + await self.config_channels[ctx.guild.id]send(ERR_UNEXCPECTED) + + + + except Exception as e: + local_logger.exception(e) + raise e + + @cfg.command() From 0fd7e7b1907b0202be90feb01172399dc6a741a2 Mon Sep 17 00:00:00 2001 From: NotaSmartDev Date: Sun, 2 Jun 2019 13:43:54 +0200 Subject: [PATCH 16/52] advertisement configuration --- Config.py | 27 ++++++++++++++++++++++++++- 1 file changed, 26 insertions(+), 1 deletion(-) diff --git a/Config.py b/Config.py index 3214312..db95e03 100644 --- a/Config.py +++ b/Config.py @@ -72,6 +72,14 @@ async def init(self, ctx): await self.cfg_poll(ctx) await self.config_channels[ctx.guild.id].send("Role setup is **mendatory** for the bot to work correctly. Otherise no one will be able to use administration commands.") await self.cfg_roll(ctx) + await self.cfg_welcome(ctx) + await self.cfg_goodbye(ctx) + await self.cfg_todo(ctx) + + #asking for permisison to advertise + await self.config_channels[ctx.guild.id]send("You're almost done ! Just one more thing...") + await self.allow_ad(ctx) + local_logger.info(f"Setup for server {ctx.guild.name}({ctx.guild.id}) is done") @@ -267,7 +275,7 @@ async def cfg_welcome(self, ctx): raise e - async def cfg_welcome(self, ctx): + async def cfg_goodbye(self, ctx): try: await self.config_channels[ctx.guild.id].send("**Starting goodbye message configuration**") retry = True @@ -313,6 +321,23 @@ async def cfg_welcome(self, ctx): local_logger.exception(e) raise e + async def allow_ad(ctx): + try: + await self.config_channels[ctx.guild.id]send("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 (you can always change this later). [y/n]") + repsonse = self.bot.wait_for("message", check=self.is_yn_answer) + if reponse[0].lower()=="n": return False + + await self.config_channels[ctx.guild.id]send("Thank you very much ! In which channel do you want me to post this message ?") + reponse = self.bot.wait_for("message", check=self.is_answer) + + old_conf = get_conf(ctx.guild.id) + old_conf["advertisement"] = reponse.channel_mentions[0].id + + except Exception as e: + local_logger.exception(e) + raise e + + From 7f22e13ba2d687b642fd10b2c91aee69d15560af Mon Sep 17 00:00:00 2001 From: NotaSmartDev Date: Sun, 2 Jun 2019 16:26:54 +0200 Subject: [PATCH 17/52] perfected BotEssentials integreation with the new infrstrcture (fixing #30 and giving a workaround for #29 ) --- BotEssentials.py | 12 ++++++++---- Config.py | 14 ++++++++++++-- 2 files changed, 20 insertions(+), 6 deletions(-) diff --git a/BotEssentials.py b/BotEssentials.py index 8a32ff6..5a0a488 100644 --- a/BotEssentials.py +++ b/BotEssentials.py @@ -34,6 +34,8 @@ class BotEssentials(commands.Cog): """All of the essential methods all of our bots should have""" def __init__(self, bot): self.bot = bot + self.invite_url = "https://discord.gg/mpGM5cg" + @commands.Cog.listener() async def on_ready(self): @@ -42,13 +44,16 @@ async def on_ready(self): @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)) - await member.guild.system_channel.send("Welcome to {} {}! Please make sure to take a look at our {} and before asking a question, at the {}".format(member.guild.name, member.mention, CHANNELS["rules"].mention, CHANNELS["faq"].mention)) + welcome_msg = get_conf(member.guild.id)["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)) - await member.guilg.system_channel.send("Goodbye {0.name} {1}, may your wandering be fun!".format(member, EMOJIS["wave"])) - + goodbye_msg = get_conf(member.guild.id)["messages"]["welcome"] + if goodbye_msg != False: + await member.guild.system_channel.send(goodbye_msg.format(member.mention)) @commands.command() async def ping(self, ctx): @@ -56,7 +61,6 @@ async def ping(self, ctx): latency = self.bot.latency await ctx.send("**Pong !** Latency of {0:.3f} seconds".format(latency)) - #Command that shuts down the bot @commands.command() @is_runner() diff --git a/Config.py b/Config.py index db95e03..e8a73ea 100644 --- a/Config.py +++ b/Config.py @@ -38,6 +38,8 @@ def __init__(self, bot): self.allowed_answers = {1:["yes", "y"], 0:["no", "n"]} + self.ad_msg = "I ({}) have recently been added to this server ! I hope I'll be useful for you. Hopefully you won't find me too many bugs. However if you do I would apreicate 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 maybe, see you there {}".format(self.bot.mention, self.invite_url, EMOJIS["wave"]) + @commands.group() @@ -242,7 +244,7 @@ async def cfg_welcome(self, ctx): while retry: - await self.config_channels[ctx.guild.id].send("Enter the message you'd like to be sent to the new users. If you want to mention them use `{}`") + await self.config_channels[ctx.guild.id].send("Enter the message you'd like to be sent to the new users. If you want to mention them use `{0}`") message = await self.bot.wait_for("message", check=self.is_answer) @@ -289,7 +291,7 @@ async def cfg_goodbye(self, ctx): while retry: - await self.config_channels[ctx.guild.id].send("Enter the message you'd like to be sent. If you want to mention them use `{}`") + await self.config_channels[ctx.guild.id].send("Enter the message you'd like to be sent. If you want to mention them use `{0}`") message = await self.bot.wait_for("message", check=self.is_answer) @@ -333,6 +335,14 @@ async def allow_ad(ctx): old_conf = get_conf(ctx.guild.id) old_conf["advertisement"] = reponse.channel_mentions[0].id + chan = dfind(lambda c: c.id==old_conf["advertisement"], ctx.guild.channels) + chan.send(self.ad_msg) + + #updating conf + update_conf(ctx.guild.id, old_conf) + + + except Exception as e: local_logger.exception(e) raise e From 53f7af2d05879496e00dc88f51ddbcae8310433b Mon Sep 17 00:00:00 2001 From: NotaSmartDev Date: Sun, 2 Jun 2019 16:52:16 +0200 Subject: [PATCH 18/52] Poll: removed uncessary code & wrote new to fit new infrastructure --- Poll.py | 44 +++++++++++++++++--------------------------- 1 file changed, 17 insertions(+), 27 deletions(-) diff --git a/Poll.py b/Poll.py index 5491115..378e3b8 100644 --- a/Poll.py +++ b/Poll.py @@ -32,38 +32,25 @@ class Poll(commands.Cog): def __init__(self, bot): self.bot = bot - #making sure POLL_ALLOWED_CHANNELS_FILE exists - if POLL_ALLOWED_CHANNELS_FILE not in os.listdir(): - local_logger.warning("{} doesn't exist & not configured".format(POLL_ALLOWED_CHANNELS_FILE)) - with open(POLL_ALLOWED_CHANNELS_FILE, "w") as file: - pass - - #making poll_allowed channels according to the message's guild - self.poll_allowed_chans = {} - with open(POLL_ALLOWED_CHANNELS_FILE, "r") as file: - for line in file.readlines(): - clean_line = line.strip("\n") - guild_id = int(clean_line.split(";")[0]) - local_logger.warning("\nChans:{}".format(clean_line.split(";")[1:])) - self.poll_allowed_chans[guild_id] = [int(chan_id) for chan_id in clean_line.split(";")[1:]] - - local_logger.warning(self.poll_allowed_chans) - - + @is_init + async def get_poll_chans(self, guild_id): + with open(f"{guild_id}.json", "r") as file: + return poll_chans = json.load(file)["poll_channels"] @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''' - if not self.poll_allowed_chans[payload.guild_id]: - local_logger.warning("Guild [{0.id}] doesn't have any channel for polls".format(payload.guild_id)) - return #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) + #getting poll_allowed_chans + poll_allowed_chans = get_poll_chans(payload.guild.id) + + #checking that user isn't the bot - if (payload.user_id != self.bot.user.id) and (payload.channel_id in self.poll_allowed_chans[payload.guild_id]): + if (payload.user_id != self.bot.user.id) and (payload.channel_id in poll_allowed_chans): #checking wether the reaction should delete the poll if payload.emoji.name == EMOJIS["no_entry_sign"]: @@ -105,15 +92,15 @@ async def on_raw_reaction_add(self, payload): @commands.Cog.listener() async def on_raw_reaction_remove(self, payload): - if not self.poll_allowed_chans[payload.guild_id]: - local_logger.warning("Guild [{0.id}] doesn't have any channel for polls".format(payload.guild_id)) - return + + #getting poll_allowed_chans + poll_allowed_chans = get_poll_chans(payload.guild.id) #fetching concerned message and the user who added the reaction message = await self.bot.get_channel(payload.channel_id).fetch_message(payload.message_id) #checking that user isn't the bot - if (payload.user_id != self.bot.user.id) and (payload.channel_id in self.poll_allowed_chans[payload.guild_id]): + if (payload.user_id != self.bot.user.id) and (payload.channel_id in poll_allowed_chans): react_for = message.reactions[0].count react_against = message.reactions[2].count @@ -139,7 +126,10 @@ async def balance_poll_color(self, msg, rfor, ragainst): async def on_message(self, message): if message.author==self.bot.user: return - if message.channel.id in self.poll_allowed_chans[message.guild.id] and message.content.startswith(PREFIX)!=True: + #getting poll_allowed_chans + poll_allowed_chans = get_poll_chans(payload.guild.id) + + if message.channel.id in poll_allowed_chans and message.content.startswith(PREFIX)!=True: embed_poll = discord.Embed( title = message.author.name, description = message.content, From 3757493c222c865a3d47b953c400c5904dfd7e60 Mon Sep 17 00:00:00 2001 From: NotaSmartDev Date: Sun, 2 Jun 2019 16:59:50 +0200 Subject: [PATCH 19/52] Role: removed uncessary code & wrote new to fit new infrastructure --- Role.py | 12 +++++++++++- utilities.py | 11 ++++++++++- 2 files changed, 21 insertions(+), 2 deletions(-) diff --git a/Role.py b/Role.py index 221cc50..912ef62 100644 --- a/Role.py +++ b/Role.py @@ -32,13 +32,23 @@ def __init__(self, bot): self.bot = bot @commands.group() - @commands.has_any_role(*GESTION_ROLES) async def role(self, ctx): '''role management utility. Requires a Gestion role''' if ctx.invoked_subcommand is None: local_logger.warning("User didn't provide any subcommand") await ctx.send("NotEnoughArguments:\tYou must provide a subcommand") + allowed_roles = get_roles(ctx.guild.id, "manager") + ok = False + for role in ctx.author.roles: + if role in allowed_roles: + ok = True + + if not ok: + ctx.channel.send(ERR_UNSUFFICIENT_PRIVILEGE) + return False + + @role.command() async def add(self, ctx, member: discord.Member, *roles:discord.Role): '''adds role(s) to ''' diff --git a/utilities.py b/utilities.py index c7b3134..af08d91 100644 --- a/utilities.py +++ b/utilities.py @@ -83,4 +83,13 @@ def del_conf(guild_id): except Exception as e: local_logger.exception(e) - return False \ No newline at end of file + return False + +def get_roles(guild_id, lvl): + try: + with open(f"{guild_id}.json", "r") as file: + return json.load(file)["roles"][lvl] + + except Exception as e: + local_logger.exception(e) + raise e \ No newline at end of file From abadb92e2d290fc0ab05d3fc8e01416f5f7de5ff Mon Sep 17 00:00:00 2001 From: NotaSmartDev Date: Sun, 2 Jun 2019 17:05:31 +0200 Subject: [PATCH 20/52] updated documentation --- README.md | 23 ++++++++++++++++------- 1 file changed, 16 insertions(+), 7 deletions(-) diff --git a/README.md b/README.md index 243a95f..b7e08d5 100644 --- a/README.md +++ b/README.md @@ -14,7 +14,7 @@ After the very first version I got some help of a friend of mine. Together we ma Here is an exhaustive list of all extensions and the commands they provide. This list is kept up to date with the latest updates. -#### Poll: `poll` +#### Poll `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. @@ -22,7 +22,7 @@ This suite of commands provides automatic poll creation. A poll is an embed mess -#### Embedding: +#### Embedding This extension allow nay user to send a message as an embed. The color of the embed is defined by the user's role color. @@ -35,12 +35,12 @@ This extension allow nay user to send a message as an embed. The color of the em This extension contains some of the most basic managing commands and should almost always be enabled. - `ping`: replies with the rounded latency of the message transfer -- `shutdown`: shuts down the bot. Restricted to the user with `RUNNER_ID` -- `clear` `` : deletes the specified `` number of messages in the current channel. chronogically. +- `shutdown`: shuts down the bot. Restricted to the user with `RUNNER_ID`. (**runner**) +- `clear` `` : deletes the specified `` number of messages in the current channel. chronogically. (**manager**) -#### Slapping +#### Slapping (manager) Allows administrators to give quick and light warnings to disrespectful members. By slapping a member he gets notified of his misbehavior and knows who did it. Both the administrator and the user can see his/her slap count. The slap count is also cross-server. @@ -49,9 +49,18 @@ Allows administrators to give quick and light warnings to disrespectful members. -#### Role +#### Role `role` (manager) Allows moderators to add and remove roles to members. - `add` `` ``: adds the specified `` roles from the `` member (roles mustn't be empty). Member can be a mention, a user id or just the string of the name of the member. -- `rm` `` ``: removes the specified `` roles from the `` member (roles mustn't be empty). Member can be a mention, a user id or just the string of the name of the member. \ No newline at end of file +- `rm` `` ``: removes the specified `` roles from the `` member (roles mustn't be empty). Member can be a mention, a user id or just the string of the name of the member. + + + +#### Config (owner) + +Allows the owner of a server to configure the behavior of the bot. + +- `init`: starts full configuration of the bot +- `chg` ``: starts the configuration of the `` extension. \ No newline at end of file From 28d124485ced23faea3f35e1b421996c60da9764 Mon Sep 17 00:00:00 2001 From: NotaSmartDev Date: Mon, 3 Jun 2019 22:42:45 +0200 Subject: [PATCH 21/52] added per-server permission check & small bugfixes --- BotEssentials.py | 8 ++++---- Config.py | 4 ++-- Role.py | 9 +-------- Slapping.py | 46 ++++++++++++---------------------------------- executioner.py | 45 ++++++++++++++------------------------------- settings.py | 5 ++++- utilities.py | 18 ++++++++++++++++-- 7 files changed, 53 insertions(+), 82 deletions(-) diff --git a/BotEssentials.py b/BotEssentials.py index 5a0a488..e850a56 100644 --- a/BotEssentials.py +++ b/BotEssentials.py @@ -2,7 +2,7 @@ from settings import * import discord from utilities import * - +import json ######################################### @@ -76,10 +76,10 @@ async def shutdown(self, ctx): await quit() @commands.command() - @commands.has_any_role(*GESTION_ROLES) - async def clear(slef, ctx, nbr:int): + @has_auth("manager") + async def clear(self, ctx, nbr:int): '''deletes specified number of messages in the current channel''' - async for msg in ctx.channel.history(limit=nbr): + async for msg in ctx.channel.history(limit=nbr+1): local_logger.info("Deleting {}".format(msg)) try: await msg.delete() diff --git a/Config.py b/Config.py index e8a73ea..2e4eb3c 100644 --- a/Config.py +++ b/Config.py @@ -73,7 +73,7 @@ async def init(self, ctx): try: await self.cfg_poll(ctx) await self.config_channels[ctx.guild.id].send("Role setup is **mendatory** for the bot to work correctly. Otherise no one will be able to use administration commands.") - await self.cfg_roll(ctx) + await self.cfg_role(ctx) await self.cfg_welcome(ctx) await self.cfg_goodbye(ctx) await self.cfg_todo(ctx) @@ -161,7 +161,7 @@ async def cfg_poll(self, ctx): local_logger.exception(e) - async def cfg_roll(self, ctx): + async def cfg_role(self, ctx): try: #introducing the clearance levels the bot uses await self.config_channels[ctx.guild.id].send("**Starting role configuration**") diff --git a/Role.py b/Role.py index 912ef62..8043954 100644 --- a/Role.py +++ b/Role.py @@ -38,14 +38,7 @@ async def role(self, ctx): local_logger.warning("User didn't provide any subcommand") await ctx.send("NotEnoughArguments:\tYou must provide a subcommand") - allowed_roles = get_roles(ctx.guild.id, "manager") - ok = False - for role in ctx.author.roles: - if role in allowed_roles: - ok = True - - if not ok: - ctx.channel.send(ERR_UNSUFFICIENT_PRIVILEGE) + if not has_auth(ctx.guild_id.id, ctx.author.roles, "manager"): return False diff --git a/Slapping.py b/Slapping.py index 7c5f247..f52da11 100644 --- a/Slapping.py +++ b/Slapping.py @@ -32,46 +32,24 @@ def __init__(self, bot): self.bot = bot @commands.command() - @commands.has_any_role(*GESTION_ROLES) + @commands.has_any_role(get_roles(ctx.guild.id, "manager")) async def slap(self, ctx, member:discord.Member): '''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''' - to_write = "" - slap_count=0 - - #reads the file and prepares logging of slaps - with open(SLAPPED_LOG_FILE, "r") as file: - content = file.readlines() - for line in content: - #finding the right server - if line.startswith(str(ctx.guild.id)): - #looking for the user in the guild slaps log - guild_line = "" - for user in line.split(";")[:1]: - #whether the user if the member - if int(user.split("|")[0]) == member.id: - slap_count = int(user.split("|")[1])+1 - guild_line+=f"{member.id}|{slap_count};" - continue - #if the user does not match - guild_line+=user + with open(SLAPPING_FILE, "r") as file: + slaps = json.load(file) - #if the user wasn't found - if slap_count==0: - guild_line+=f"{member.id}|1;" + slap_count = slaps[ctx.guild.id][member.id] + if not slap_count: + slap_count = 1 + else: + slap_count+= 1 - #removing the last ";" and appending a line return - to_write+=guild_line[-1]+"\n" + #writting to file + with open(SLAPPING_FILE, "w") as file: + json.dump(file, slaps) - else: - to_write += line - - - await ctx.send("{} you've been slapped by {} because of your behavior! This is the {} time. Be careful, if you get slapped too much there *will* be consequences !".format(member.mention, ctx.message.author.mention, slap_count)) - - #writes out updated data to the file - with open(SLAPPED_LOG_FILE, "w") as file: - file.write(to_write) + await ctx.send("{} you've been slapped by {} because of your behavior! This is the {} time. Be careful, if you get slapped too much there *will* be consequences !".format(member.mention, ctx.message.author.mention, slap_count)) @commands.command() @commands.has_any_role(*GESTION_ROLES) diff --git a/executioner.py b/executioner.py index accb8d0..05fa69e 100644 --- a/executioner.py +++ b/executioner.py @@ -8,6 +8,7 @@ import time import random import logging +import json #INITS THE BOT @@ -74,25 +75,19 @@ async def add(ctx, extension:str): #if the extension was correctly loaded, adding it to the enabled file try: - #fetching laready enabled extensions with open(ENABLED_EXTENSIONS_FILE, "r") as file: - to_write = "" - for line in file.readlines(): - if line[:-1]==extension: - continue - to_write+=line + enabled_exts = json.load(file) + + enabled_exts[extension] = True - to_write+= f"{extension}\n" - - #writting to file with open(ENABLED_EXTENSIONS_FILE, "w") as file: - file.write(to_write) + json.dump(file, enabled_exts) except FileNotFoundError as e: #if the file didn't yet exist a new one will be created. This should not happen, only here as a failsafe main_logger.warning("{} doesn't exist.".format(ENABLED_EXTENSIONS_FILE)) with open(ENABLED_EXTENSIONS_FILE, "w") as file: - file.write("{}\n".format(extension)) + file.write() except Exception as e: #logging any other possible issue @@ -115,20 +110,12 @@ async def rm(ctx, extension:str): #if the extension was correctly unloaded, removing it from the enblaed extension file try: with open(ENABLED_EXTENSIONS_FILE, "r") as file: - lines = [] - for line in file.readlines(): - if line[:-1] == extension: - continue - lines.append(line) - - - #building new file - to_write = "" - for line in lines: - to_write+=line + enabled_exts = json.load(file) + + enabled_exts[extension] = False with open(ENABLED_EXTENSIONS_FILE, "w") as file: - file.write(to_write) + json.dump(file, enabled_exts) except Exception as e: main_logger.exception(e) @@ -153,14 +140,10 @@ async def rm(ctx, extension:str): #trying to load all enabled extensions try: with open(ENABLED_EXTENSIONS_FILE, "r") as file: - for ext in file.readlines(): - try: - bot.load_extension(str(ext[:-1])) - main_logger.info("Loaded {}".format(ext)) - - except Exception as e: - main_logger.exception(e) - raise e + extensions = json.load(file) + for ext in extensions: + if extensions[ext]==True: + bot.load_extension(ext) #if no extension is enabled except FileNotFoundError as e: diff --git a/settings.py b/settings.py index e717a09..4b8e02b 100644 --- a/settings.py +++ b/settings.py @@ -33,7 +33,10 @@ #data used only for Todo -> maybe remove it ? PUBLIC_REPOST="Public repost" -#rolls +#files +ENABLED_EXTENSIONS_FILE = "enabled_exts.json" +SLAPPING_FILE = "slapped.json" +#roles ROLES_LEVEL = ["manager", "admin"] diff --git a/utilities.py b/utilities.py index af08d91..b8b3cc1 100644 --- a/utilities.py +++ b/utilities.py @@ -1,5 +1,6 @@ import logging import os +import json from settings import * from discord.ext import commands @@ -44,6 +45,18 @@ def check_condition(ctx): ctx.send(ERR_NOT_SETUP) return result +def has_auth(clearance): + def predicate(ctx): + allowed_roles = get_roles(ctx.guild.id, clearance) + for role in ctx.author.roles: + print(role.id) + if role.id in allowed_roles: + return True + local_logger.warning(ERR_UNSUFFICIENT_PRIVILEGE) + + return commands.check(predicate) + + ######################################### # # @@ -64,7 +77,7 @@ def has_changed(server, last_time): def get_conf(guild_id): with open(f"{guild_id}.json", "r") as file: - return conf = json.load(file) + return json.load(file) def update_conf(guild_id, conf_dict): try: @@ -92,4 +105,5 @@ def get_roles(guild_id, lvl): except Exception as e: local_logger.exception(e) - raise e \ No newline at end of file + raise e + From 13fc2ed85e7ee3f3590b15a0b355ea2c36254a1f Mon Sep 17 00:00:00 2001 From: NotaSmartDev Date: Mon, 3 Jun 2019 23:32:51 +0200 Subject: [PATCH 22/52] improved Slapping --- Role.py | 1 + Slapping.py | 75 +++++++++++++++++++++----------------------------- executioner.py | 6 ++-- settings.py | 15 ++++++++++ utilities.py | 3 +- 5 files changed, 52 insertions(+), 48 deletions(-) diff --git a/Role.py b/Role.py index 8043954..fc77bc7 100644 --- a/Role.py +++ b/Role.py @@ -32,6 +32,7 @@ def __init__(self, bot): self.bot = bot @commands.group() + @has_auth("admin") async def role(self, ctx): '''role management utility. Requires a Gestion role''' if ctx.invoked_subcommand is None: diff --git a/Slapping.py b/Slapping.py index f52da11..a7957f8 100644 --- a/Slapping.py +++ b/Slapping.py @@ -1,5 +1,6 @@ import logging import discord +import os from settings import * from utilities import * @@ -30,73 +31,59 @@ class Slapping(commands.Cog): """a suite of commands meant to help moderators handle the server""" def __init__(self, bot): self.bot = bot + if SLAPPING_FILE not in os.listdir(): + with open(SLAPPING_FILE, "w") as file: + file.write("") + @commands.command() - @commands.has_any_role(get_roles(ctx.guild.id, "manager")) + @is_init() + @has_auth("manager") async def slap(self, ctx, member:discord.Member): '''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''' - with open(SLAPPING_FILE, "r") as file: slaps = json.load(file) - slap_count = slaps[ctx.guild.id][member.id] - if not slap_count: - slap_count = 1 + #checking wether the user has already been slapped + if str(member.id) not in slaps[str(ctx.guild.id)]: + slaps[str(ctx.guild.id)][str(member.id)] = 1 + else: - slap_count+= 1 + slaps[str(ctx.guild.id)][str(member.id)] +=1 + #writting to file with open(SLAPPING_FILE, "w") as file: - json.dump(file, slaps) + json.dump(slaps, file) - await ctx.send("{} you've been slapped by {} because of your behavior! This is the {} time. Be careful, if you get slapped too much there *will* be consequences !".format(member.mention, ctx.message.author.mention, slap_count)) + await ctx.send("{} you've been slapped by {} because of your behavior! This is the {} time. Be careful, if you get slapped too much there *will* be consequences !".format(member.mention, ctx.message.author.mention, slaps[str(ctx.guild.id)][str(member.id)])) @commands.command() - @commands.has_any_role(*GESTION_ROLES) + @is_init() + @has_auth("manager") async def pardon(self, ctx, member:discord.Member, nbr=0): '''Pardonning a member resets his slaps count.''' - to_write = "" - nbr = int(nbr) - - #reads the file and prepares logging of slaps - with open(SLAPPED_LOG_FILE, "r") as file: - for line in file.readlines(): - if not line.startswith(str(ctx.guild.id)): - to_write+=line - continue - - #looking for the user - guild_line = "" - for user in line.split(";")[:1]: - #whether the user is member - if int(user.split("|")[0]) == member.id: - local_logger.info(f"Found user {member.name} who has ot be pardonnned") - crt_slaps = int(user.split("|")[1]) - #pardonnning the user - if crt_slaps Date: Mon, 3 Jun 2019 23:57:50 +0200 Subject: [PATCH 23/52] adapted Role to the new structure --- BotEssentials.py | 1 - Config.py | 16 ++++++++-------- Role.py | 6 +++--- settings.py | 5 ++++- utilities.py | 4 +++- 5 files changed, 18 insertions(+), 14 deletions(-) diff --git a/BotEssentials.py b/BotEssentials.py index e850a56..de955ef 100644 --- a/BotEssentials.py +++ b/BotEssentials.py @@ -34,7 +34,6 @@ class BotEssentials(commands.Cog): """All of the essential methods all of our bots should have""" def __init__(self, bot): self.bot = bot - self.invite_url = "https://discord.gg/mpGM5cg" @commands.Cog.listener() diff --git a/Config.py b/Config.py index 2e4eb3c..7fbbbc9 100644 --- a/Config.py +++ b/Config.py @@ -38,7 +38,7 @@ def __init__(self, bot): self.allowed_answers = {1:["yes", "y"], 0:["no", "n"]} - self.ad_msg = "I ({}) have recently been added to this server ! I hope I'll be useful for you. Hopefully you won't find me too many bugs. However if you do I would apreicate 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 maybe, see you there {}".format(self.bot.mention, self.invite_url, EMOJIS["wave"]) + self.ad_msg = "I ({}) have recently been added to this server ! I hope I'll be useful for you. Hopefully you won't find me too many bugs. However if you do I would apreicate 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 maybe, see you there {}".format(self.bot.user.mention, DEV_SRV_URL, EMOJIS["wave"]) @@ -79,7 +79,7 @@ async def init(self, ctx): await self.cfg_todo(ctx) #asking for permisison to advertise - await self.config_channels[ctx.guild.id]send("You're almost done ! Just one more thing...") + await self.config_channels[ctx.guild.id].send("You're almost done ! Just one more thing...") await self.allow_ad(ctx) @@ -215,10 +215,10 @@ async def cfg_role(self, ctx): old_conf["roles"]["admin"] = new_roles[1] if update_conf(ctx.guild.id, old_conf) == False: - await self.config_channels[ctx.guild.id]send(ERR_UNEXCPECTED) + await self.config_channels[ctx.guild.id].send(ERR_UNEXCPECTED) else: - await self.config_channels[ctx.guild.id]send("Successfully updated role configuration") + await self.config_channels[ctx.guild.id].send("Successfully updated role configuration") except Exception as e: @@ -268,7 +268,7 @@ async def cfg_welcome(self, ctx): old_conf["messages"]["welcome"]= message if update_conf(ctx.guild.id, old_conf) == False: - await self.config_channels[ctx.guild.id]send(ERR_UNEXCPECTED) + await self.config_channels[ctx.guild.id].send(ERR_UNEXCPECTED) @@ -315,7 +315,7 @@ async def cfg_goodbye(self, ctx): old_conf["messages"]["goodbye"]= message if update_conf(ctx.guild.id, old_conf) == False: - await self.config_channels[ctx.guild.id]send(ERR_UNEXCPECTED) + await self.config_channels[ctx.guild.id].send(ERR_UNEXCPECTED) @@ -325,11 +325,11 @@ async def cfg_goodbye(self, ctx): async def allow_ad(ctx): try: - await self.config_channels[ctx.guild.id]send("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 (you can always change this later). [y/n]") + await self.config_channels[ctx.guild.id].send("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 (you can always change this later). [y/n]") repsonse = self.bot.wait_for("message", check=self.is_yn_answer) if reponse[0].lower()=="n": return False - await self.config_channels[ctx.guild.id]send("Thank you very much ! In which channel do you want me to post this message ?") + await self.config_channels[ctx.guild.id].send("Thank you very much ! In which channel do you want me to post this message ?") reponse = self.bot.wait_for("message", check=self.is_answer) old_conf = get_conf(ctx.guild.id) diff --git a/Role.py b/Role.py index fc77bc7..cc62d41 100644 --- a/Role.py +++ b/Role.py @@ -39,14 +39,14 @@ async def role(self, ctx): local_logger.warning("User didn't provide any subcommand") await ctx.send("NotEnoughArguments:\tYou must provide a subcommand") - if not has_auth(ctx.guild_id.id, ctx.author.roles, "manager"): + if not has_auth(ctx.guild.id, ctx.author.roles, "manager"): return False @role.command() async def add(self, ctx, member: discord.Member, *roles:discord.Role): '''adds role(s) to ''' - if len(role)==0: + if len(roles)==0: local_logger.warning("User didn't provide a role") await ctx.send("NotEnoughArguments:\tYou must provide at least one `role`") @@ -60,7 +60,7 @@ async def add(self, ctx, member: discord.Member, *roles:discord.Role): @role.command() async def rm(self, ctx, member:discord.Member, *roles:discord.Role): '''removes role(s) to ''' - if len(role)==0: + if len(roles)==0: local_logger.warning("User didn't provide a role") await ctx.send("NotEnoughArguments:\tYou must provide at least one `role`") diff --git a/settings.py b/settings.py index c1f1d71..722d4ce 100644 --- a/settings.py +++ b/settings.py @@ -9,7 +9,7 @@ PREFIX = "::" -TOKEN = "your_token" +TOKEN = "NTYzODQ4MDcxMjE0MjY4NDI5.XPP-wA.Vk1crS4mZ124y0Ljr8tYOvWQuzM" RUNNER_ID=289426079544901633 #server with all of the bot's devs. Where all bug reports should be made. @@ -18,6 +18,9 @@ #github website URL WEBSITE = "https://github.com/organic-bots/ForeBot" +#invite to dev server URL +DEV_SRV_URL = "https://discord.gg/mpGM5cg" + #emojis dict. May be possible to change incomprehensible unicode to other strings recognized by discord EMOJIS = { "thumbsup": "\U0001f44d", diff --git a/utilities.py b/utilities.py index 566653a..3ab7a35 100644 --- a/utilities.py +++ b/utilities.py @@ -45,7 +45,7 @@ def check_condition(ctx): ctx.send(ERR_NOT_SETUP) return result -def has_auth(clearance): +def has_auth(clearance, *args): def predicate(ctx): allowed_roles = get_roles(ctx.guild.id, clearance) for role in ctx.author.roles: @@ -57,6 +57,8 @@ def predicate(ctx): return commands.check(predicate) +def is_server_owner(): + pass ######################################### From 1ffe689408d4a430b27bc22cf9bd0fe2de7a8068 Mon Sep 17 00:00:00 2001 From: 0r1g1n <46009999+NotaSmartDev@users.noreply.github.com> Date: Tue, 4 Jun 2019 09:36:57 +0200 Subject: [PATCH 24/52] updated README.md --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index b7e08d5..969ab03 100644 --- a/README.md +++ b/README.md @@ -6,7 +6,7 @@ This bot is a collection of several commands and suite of commands. They are reg ### Development history -[I](https://github.com/NotaSmartDev) (@NotaSmartDev) started building this bot at the end of April 2019 using discord.py API. This bot was first made with the intent to make my discord server more powerful and alive. I had only created it a few days ago but I had realized that I would need additional tools to be able to fulfill all of the plans I had for this server. I had already started making a [bot](https://github.com/organic-bots/LazyFactorian) which serves as an interface to factorio's resources. I thus started building a bot that would enable me to easily manage a scalable server which would contain all of my future bots and would serve as a platform for all my creations. +I ([@NotaSmartDev](https://github.com/NotaSmartDev)) started building this bot at the end of April 2019 using discord.py API. This bot was first made with the intent to make my discord server more powerful and alive. I had only created it a few days ago but I had realized that I would need additional tools to be able to fulfill all of the plans I had for this server. I had already started making a [bot](https://github.com/organic-bots/LazyFactorian) which serves as an interface to factorio's resources. I thus started building a bot that would enable me to easily manage a scalable server which would contain all of my future bots and would serve as a platform for all my creations. After the very first version I got some help of a friend of mine. Together we made the bot evolve so that it could join the ranks of other servers. Indeed I had started to realize that the bot, however simple, may be useful to others. @@ -63,4 +63,4 @@ Allows moderators to add and remove roles to members. Allows the owner of a server to configure the behavior of the bot. - `init`: starts full configuration of the bot -- `chg` ``: starts the configuration of the `` extension. \ No newline at end of file +- `chg` ``: starts the configuration of the `` extension. From 77076ccecd6ce74c27f4675878d45f7e9d51c683 Mon Sep 17 00:00:00 2001 From: NotaSmartDev Date: Wed, 5 Jun 2019 01:24:38 +0200 Subject: [PATCH 25/52] minor bug fixes --- Config.py | 20 ++++++++++---------- Embedding.py | 11 +++++------ Poll.py | 8 ++------ executioner.py | 1 - utilities.py | 11 ++++++++++- 5 files changed, 27 insertions(+), 24 deletions(-) diff --git a/Config.py b/Config.py index 7fbbbc9..362f556 100644 --- a/Config.py +++ b/Config.py @@ -33,12 +33,12 @@ def __init__(self, bot): self.bot = bot #change to make it cross-server self.config_channels={} - + self.bot_member = self.bot.fetch_user(self.bot.user.id) #other values can't be added as of now self.allowed_answers = {1:["yes", "y"], 0:["no", "n"]} - self.ad_msg = "I ({}) have recently been added to this server ! I hope I'll be useful for you. Hopefully you won't find me too many bugs. However if you do I would apreicate 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 maybe, see you there {}".format(self.bot.user.mention, DEV_SRV_URL, EMOJIS["wave"]) + self.ad_msg = "I ({}) have recently been added to this server ! I hope I'll be useful for you. Hopefully you won't find me too many bugs. However if you do I would apreicate 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 maybe, see you there {}".format(self.bot_member, DEV_SRV_URL, EMOJIS["wave"]) @@ -66,7 +66,7 @@ async def init(self, ctx): #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\n*Starting full configuration...*''') await self.config_channels[ctx.guild.id].send("This will overwrite all of your existing configurations. Do you want to continue ? [y/n]") - response = self.bot.wait_for("message", check=self.is_yn_answer) + response = await self.bot.wait_for("message", check=self.is_yn_answer) if response[0].lower() == "n":return False await self.config_channels[ctx.guild.id].send("**Starting full bot configuration...**") @@ -96,7 +96,7 @@ async def init(self, ctx): @is_init() async def chg(self, ctx, setting): try: - eval("self.chg_"+setting) + eval("self.cfg_"+setting) except Exception as e: local_logger.exception(e) @@ -176,7 +176,7 @@ async def cfg_role(self, ctx): new_role = [] #asking the owner which roles he wants to give clearance to await self.config_channels[ctx.guild.ig].send(f"List all the roles you want to be given the **{role_lvl}** level of clearance.") - response = self.bot.wait_for("message", check=self.is_answer) + response = await self.bot.wait_for("message", check=self.is_answer) roles = response.role_mentions #building roll string @@ -186,10 +186,10 @@ async def cfg_role(self, ctx): #asking for confirmation await self.config_channels[ctx.guild.id].send(f"You are about to give{roles_str} roles the **{role_lvl}** level of clearance. Do you confirm this ? [y/n]") - response = self.bot.wait_for("message", check=self.is_yn_answer) + response = await self.bot.wait_for("message", check=self.is_yn_answer) if repsonse[0].lower() == "n": await self.config_channels[ctx.guild.id].send(f"Aborting configuration of {role_lvl}. Do you want to retry? [y/n]") - response = self.bot.wait_for("message", check=self.is_yn_answer) + response = await self.bot.wait_for("message", check=self.is_yn_answer) if response[0].lower() == "n": local_logger.info(f"The configuration for the {role_lvl} clearance has been cancelled for server {ctx.guild.name}") retry = False @@ -257,7 +257,7 @@ async def cfg_welcome(self, ctx): #the user has made a mistake if response[0].lower() == "n": await self.config_channels[ctx.guild.id].send("Do you want to retry ? [y/n]") - response = self.bot.wait_for("message", check=self.is_yn_answer) + response = await self.bot.wait_for("message", check=self.is_yn_answer) if response[0].lower == "n": message = False retry = False @@ -326,11 +326,11 @@ async def cfg_goodbye(self, ctx): async def allow_ad(ctx): try: await self.config_channels[ctx.guild.id].send("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 (you can always change this later). [y/n]") - repsonse = self.bot.wait_for("message", check=self.is_yn_answer) + repsonse = await self.bot.wait_for("message", check=self.is_yn_answer) if reponse[0].lower()=="n": return False await self.config_channels[ctx.guild.id].send("Thank you very much ! In which channel do you want me to post this message ?") - reponse = self.bot.wait_for("message", check=self.is_answer) + reponse = await self.bot.wait_for("message", check=self.is_answer) old_conf = get_conf(ctx.guild.id) old_conf["advertisement"] = reponse.channel_mentions[0].id diff --git a/Embedding.py b/Embedding.py index 60ff119..d6c49ee 100644 --- a/Embedding.py +++ b/Embedding.py @@ -30,12 +30,7 @@ class Embedding(commands.Cog): """A suite of command providing users with embeds manipulation tools.""" def __init__(self, bot): self.bot = bot - #making poll_allowed channels according to the message's guild - self.poll_allowed_chans = {} - with open(POLL_ALLOWED_CHANNELS_FILE) as file: - for line in file.readlines(): - self.poll_allowed_chans[line.split(";")[0]] = [chan_id for chan_id in line.split(";")[1:]] - + #maybe think to block sending an embed in a poll channel @commands.command() async def embed(self, ctx, *args): @@ -45,6 +40,10 @@ async def embed(self, ctx, *args): await ctx.message.delete() return + attachements = ctx.message.attachements + print(attachements) + + msg = "" img_url = None for arg in args: diff --git a/Poll.py b/Poll.py index 378e3b8..92b772b 100644 --- a/Poll.py +++ b/Poll.py @@ -31,11 +31,6 @@ class Poll(commands.Cog): """TODO: 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 - - @is_init - async def get_poll_chans(self, guild_id): - with open(f"{guild_id}.json", "r") as file: - return poll_chans = json.load(file)["poll_channels"] @commands.Cog.listener() async def on_raw_reaction_add(self, payload): @@ -46,6 +41,7 @@ async def on_raw_reaction_add(self, payload): user = self.bot.get_user(payload.user_id) #getting poll_allowed_chans + #@is_init poll_allowed_chans = get_poll_chans(payload.guild.id) @@ -127,7 +123,7 @@ async def on_message(self, message): if message.author==self.bot.user: return #getting poll_allowed_chans - poll_allowed_chans = get_poll_chans(payload.guild.id) + poll_allowed_chans = get_poll_chans(message.guild.id) if message.channel.id in poll_allowed_chans and message.content.startswith(PREFIX)!=True: embed_poll = discord.Embed( diff --git a/executioner.py b/executioner.py index 91cc866..ef4087a 100644 --- a/executioner.py +++ b/executioner.py @@ -1,7 +1,6 @@ #!/usr/bin/env python3 import discord -from discord.utils import find as dfind from settings import * from utilities import * import math diff --git a/utilities.py b/utilities.py index 3ab7a35..d229bd7 100644 --- a/utilities.py +++ b/utilities.py @@ -58,7 +58,13 @@ def predicate(ctx): return commands.check(predicate) def is_server_owner(): - pass + def predicate(ctx): + if ctx.author == ctx.guild.owmer: + return True + ctx.send(ERR_UNSUFFICIENT_PRIVILEGE) + return False + + return commands.check(predicate) ######################################### @@ -110,3 +116,6 @@ def get_roles(guild_id, lvl): local_logger.exception(e) raise e +async def get_poll_chans(guild_id): + with open(f"{guild_id}.json", "r") as file: + return json.load(file)["poll_channels"] From d33283e8872a163d978f0148045431b754f40c28 Mon Sep 17 00:00:00 2001 From: NotaSmartDev Date: Fri, 7 Jun 2019 20:44:51 +0200 Subject: [PATCH 26/52] fixed #40 --- executioner.py | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/executioner.py b/executioner.py index ef4087a..08834d3 100644 --- a/executioner.py +++ b/executioner.py @@ -125,6 +125,18 @@ async def rm(ctx, extension:str): local_logger.info(f"Disabled and removed {extension}") +@ext.command() +async def list(ctx): + try: + ext_list = "" + for e in bot.extensions.keys(): + ext_list+=f"**{e}**, " + ext_list = ext_list[:-2] + await ctx.send(f"The loaded extenions are: {ext_list}") + + except Exception as e: + main_logger.exception(e) + ######################################### # # From a088240049d719d41f394a3adf09d07e1b7888d6 Mon Sep 17 00:00:00 2001 From: NotaSmartDev Date: Fri, 7 Jun 2019 20:52:25 +0200 Subject: [PATCH 27/52] removed invalid token --- settings.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/settings.py b/settings.py index 722d4ce..899e2b3 100644 --- a/settings.py +++ b/settings.py @@ -9,7 +9,7 @@ PREFIX = "::" -TOKEN = "NTYzODQ4MDcxMjE0MjY4NDI5.XPP-wA.Vk1crS4mZ124y0Ljr8tYOvWQuzM" +TOKEN = "your_token" RUNNER_ID=289426079544901633 #server with all of the bot's devs. Where all bug reports should be made. From 12b23693d5b5cee28001ba0edc6ea25a417cc377 Mon Sep 17 00:00:00 2001 From: NotaSmartDev Date: Fri, 7 Jun 2019 21:18:36 +0200 Subject: [PATCH 28/52] =?UTF-8?q?improved=20=1B[3J=1B[H=1B[2J=20performanc?= =?UTF-8?q?e?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- BotEssentials.py | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/BotEssentials.py b/BotEssentials.py index de955ef..8bb9d35 100644 --- a/BotEssentials.py +++ b/BotEssentials.py @@ -78,13 +78,16 @@ async def shutdown(self, ctx): @has_auth("manager") async def clear(self, ctx, nbr:int): '''deletes specified number of messages in the current channel''' + to_del = [] async for msg in ctx.channel.history(limit=nbr+1): local_logger.info("Deleting {}".format(msg)) - try: - await msg.delete() - except Exception as e: - local_logger.exception("Couldn't delete {}".format(msg)) - raise e + to_del.append(msg) + + try: + await self.bot.delete_messages(to_del) + except Exception as e: + local_logger.exception("Couldn't delete at least on of{}".format(to_del)) + raise e From d1053250edc15df136ccbbc7970f4d59215b29f6 Mon Sep 17 00:00:00 2001 From: NotaSmartDev Date: Fri, 7 Jun 2019 21:27:26 +0200 Subject: [PATCH 29/52] fixed #31 --- Embedding.py | 17 ++++++++++------- Feedback.py | 26 ++++++++++++++++++++++++++ executioner.py | 2 +- utilities.py | 2 +- 4 files changed, 38 insertions(+), 9 deletions(-) create mode 100644 Feedback.py diff --git a/Embedding.py b/Embedding.py index d6c49ee..ad74a06 100644 --- a/Embedding.py +++ b/Embedding.py @@ -35,14 +35,18 @@ def __init__(self, bot): @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 !""" - if ctx.channel.name in self.poll_allowed_chans: + if ctx.channel.id in get_poll_chans(ctx.guild.id): local_logger.info("Preventing user from making an embed in a poll channel") await ctx.message.delete() return - attachements = ctx.message.attachements - print(attachements) - + attachments = ctx.message.attachments + files = [] + for a in attachments: + fl_name = a.filename + contents = await a.read() + print(type(a), type(fl_name), type(contents)) + files.append(discord.File(contents, filename=fl_name)) msg = "" img_url = None @@ -52,7 +56,6 @@ async def embed(self, ctx, *args): else: msg += " {}".format(arg) - print(msg) embed_msg = discord.Embed( title = None, description = msg, @@ -60,11 +63,11 @@ async def embed(self, ctx, *args): url = None ) if img_url: - embed_msg.set_image(url=img_url) + await embed_msg.set_image(url=img_url) embed_msg.set_footer(text=ctx.message.author.name, icon_url=ctx.message.author.avatar_url) await ctx.message.delete() - await ctx.message.channel.send(embed=embed_msg) + await ctx.message.channel.send(embed=embed_msg, files=files) def setup(bot): bot.add_cog(Embedding(bot)) \ No newline at end of file diff --git a/Feedback.py b/Feedback.py new file mode 100644 index 0000000..992fff2 --- /dev/null +++ b/Feedback.py @@ -0,0 +1,26 @@ +import discord +import json +import logging +from settings import * +from utilities import * + +class Feedback(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 + + + @commands.command + @commands.is_owner() + async def update(self, message="The bot has been updated. Look at the development server for more information"): #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.''' + for g in self.bot.fetch_guilds(): + #getting the dm with the server owner + dm = g.owner.dm_channel + if dm ==None: + dm = g.owner.create_dm() + + #sending the server owner the updates + dm.send(message) \ No newline at end of file diff --git a/executioner.py b/executioner.py index 08834d3..e250ba9 100644 --- a/executioner.py +++ b/executioner.py @@ -126,7 +126,7 @@ async def rm(ctx, extension:str): @ext.command() -async def list(ctx): +async def ls(ctx): try: ext_list = "" for e in bot.extensions.keys(): diff --git a/utilities.py b/utilities.py index d229bd7..ce309cf 100644 --- a/utilities.py +++ b/utilities.py @@ -116,6 +116,6 @@ def get_roles(guild_id, lvl): local_logger.exception(e) raise e -async def get_poll_chans(guild_id): +def get_poll_chans(guild_id): with open(f"{guild_id}.json", "r") as file: return json.load(file)["poll_channels"] From 0f6c8582b8c046a9a5f47ade78716486aa9182d8 Mon Sep 17 00:00:00 2001 From: NotaSmartDev Date: Fri, 7 Jun 2019 22:23:20 +0200 Subject: [PATCH 30/52] fixed #28 --- Development.py | 64 ++++++++++++++++++++++++++++++++++++++++++++++++++ Embedding.py | 27 +++++++++++---------- Poll.py | 1 + settings.py | 3 ++- 4 files changed, 82 insertions(+), 13 deletions(-) create mode 100644 Development.py diff --git a/Development.py b/Development.py new file mode 100644 index 0000000..bb2bd91 --- /dev/null +++ b/Development.py @@ -0,0 +1,64 @@ +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 # +# # +# # +######################################### + + +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 + + + @commands.command + @commands.is_owner() + async def update(self, ctx, message="The bot has been updated. Look at the development server for more information"): #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.''' + for g in self.bot.fetch_guilds(): + #getting the dm with the server owner + dm = g.owner.dm_channel + if dm ==None: + dm = g.owner.create_dm() + + #sending the server owner the updates + dm.send(message) + + @commands.command() + @commands.is_owner() #-> 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) + + +def setup(bot): + bot.add_cog(Development(bot)) \ No newline at end of file diff --git a/Embedding.py b/Embedding.py index ad74a06..9a1d0c4 100644 --- a/Embedding.py +++ b/Embedding.py @@ -40,21 +40,24 @@ async def embed(self, ctx, *args): await ctx.message.delete() return + #lining attachements attachments = ctx.message.attachments - files = [] - for a in attachments: - fl_name = a.filename - contents = await a.read() - print(type(a), type(fl_name), type(contents)) - files.append(discord.File(contents, filename=fl_name)) - msg = "" + to_link = [] img_url = None - for arg in args: - if arg.startswith("https://"): - img_url = arg + for attachment in attachments: + #whether the attachment is the image + if attachment.height: + img_url = attachment + else: - msg += " {}".format(arg) + to_link.append(attachment.url) + + #building msg + msg = ctx.message.content + msg +="\n" + for attachment in to_link: + msg += f"{attachment}\n" embed_msg = discord.Embed( title = None, @@ -67,7 +70,7 @@ async def embed(self, ctx, *args): embed_msg.set_footer(text=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) + await ctx.message.channel.send(embed=embed_msg) def setup(bot): bot.add_cog(Embedding(bot)) \ No newline at end of file diff --git a/Poll.py b/Poll.py index 92b772b..a1fa47c 100644 --- a/Poll.py +++ b/Poll.py @@ -184,6 +184,7 @@ async def rm(self, ctx, msg_id): @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 diff --git a/settings.py b/settings.py index 899e2b3..ef46cf8 100644 --- a/settings.py +++ b/settings.py @@ -59,7 +59,8 @@ } #logging settings -LOGGING_HANDLER = logging.FileHandler("forebot.log", "a") +LOG_FILE = "forebot.log" +LOGGING_HANDLER = logging.FileHandler(LOG_FILE, "a") LOGGING_FORMATTER = logging.Formatter("[%(asctime)s]:%(name)s:%(message)s") LOGGING_LEVEL = logging.INFO LOGGING_HANDLER.setFormatter(LOGGING_FORMATTER) From fb01abd300a12b86540a1b33dde40ee4a10c01a0 Mon Sep 17 00:00:00 2001 From: NotaSmartDev Date: Sat, 8 Jun 2019 09:23:55 +0200 Subject: [PATCH 31/52] fixed various bugs --- BotEssentials.py | 7 +++++++ Config.py | 29 ++++++++++++++++------------- Embedding.py | 33 +++++++++++---------------------- settings.py | 15 +++++++++++++++ utilities.py | 2 +- 5 files changed, 50 insertions(+), 36 deletions(-) diff --git a/BotEssentials.py b/BotEssentials.py index 8bb9d35..6618a50 100644 --- a/BotEssentials.py +++ b/BotEssentials.py @@ -35,6 +35,13 @@ class BotEssentials(commands.Cog): def __init__(self, bot): self.bot = bot + + @commands.Cog.listener() + async def on_guild_join(self, guild): + with open(f"{guild.id}.json", "w") as file: + file.write(DEFAULT_SERVER_FILE) + local_logger.info(f"Joined server {guild.name}") + @commands.Cog.listener() async def on_ready(self): diff --git a/Config.py b/Config.py index 362f556..971edf3 100644 --- a/Config.py +++ b/Config.py @@ -33,20 +33,20 @@ def __init__(self, bot): self.bot = bot #change to make it cross-server self.config_channels={} - self.bot_member = self.bot.fetch_user(self.bot.user.id) #other values can't be added as of now self.allowed_answers = {1:["yes", "y"], 0:["no", "n"]} - self.ad_msg = "I ({}) have recently been added to this server ! I hope I'll be useful for you. Hopefully you won't find me too many bugs. However if you do I would apreicate 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 maybe, see you there {}".format(self.bot_member, DEV_SRV_URL, EMOJIS["wave"]) + @commands.group() - @commands.is_owner() + @is_server_owner() async def cfg(self, ctx): + self.ad_msg = "I ({}) have recently been added to this server ! I hope I'll be useful for you. Hopefully you won't find me too many bugs. However if you do I would apreicate 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 maybe, see you there {}".format(ctx.me.mention, DEV_SRV_URL, EMOJIS["wave"]) if ctx.invoked_subcommand == None: - ctx.send(ERROR_NO_SUBCOMMAND) + await ctx.send(ERR_NO_SUBCOMMAND) @cfg.command() @@ -67,7 +67,7 @@ async def init(self, ctx): 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\n*Starting full configuration...*''') await self.config_channels[ctx.guild.id].send("This will overwrite all of your existing configurations. Do you want to continue ? [y/n]") response = await self.bot.wait_for("message", check=self.is_yn_answer) - if response[0].lower() == "n":return False + if response.content[0].lower() == "n":return False await self.config_channels[ctx.guild.id].send("**Starting full bot configuration...**") try: @@ -135,14 +135,17 @@ async def cfg_poll(self, ctx): response = await self.bot.wait_for("message", check=self.is_yn_answer) #wether the asnwer was positive + print(response.content) if not response.content[0].lower() =="y": #making sure the user really wants to cancel poll configuration - self.config_channels[ctx.guild.id].send("Aborting addition of poll channels. Do you want to leave the poll configuration interface ? [y/n]") + await self.config_channels[ctx.guild.id].send("Aborting addition of poll channels. Do you want to leave the poll configuration interface ? [y/n]") response = await self.bot.wait_for("message", check=self.is_yn_answer) if response.content[0].lower()=="y": local_logger.info(f"Poll configuration has been cancelled for server {ctx.guild.name}") retry = False + else: retry=False + old_conf = get_conf(ctx.guild.id) @@ -165,9 +168,9 @@ async def cfg_role(self, ctx): try: #introducing the clearance levels the bot uses await self.config_channels[ctx.guild.id].send("**Starting role configuration**") - await self.config_channels[ctx.guild.ig].send("This bot uses two level of clearance for its commands.") - await self.config_channels[ctx.guild.ig].send("The 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.") - await self.config_channels[ctx.guild.ig].send("The second level of clearance is **admin**. Anyonw 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.") + await self.config_channels[ctx.guild.id].send("This bot uses two level of clearance for its commands.") + await self.config_channels[ctx.guild.id].send("The 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.") + await self.config_channels[ctx.guild.id].send("The second level of clearance is **admin**. Anyonw 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: @@ -175,7 +178,7 @@ async def cfg_role(self, ctx): while retry: new_role = [] #asking the owner which roles he wants to give clearance to - await self.config_channels[ctx.guild.ig].send(f"List all the roles you want to be given the **{role_lvl}** level of clearance.") + await self.config_channels[ctx.guild.id].send(f"List all the roles you want to be given the **{role_lvl}** level of clearance.") response = await self.bot.wait_for("message", check=self.is_answer) roles = response.role_mentions @@ -187,7 +190,7 @@ async def cfg_role(self, ctx): #asking for confirmation await self.config_channels[ctx.guild.id].send(f"You are about to give{roles_str} roles the **{role_lvl}** level of clearance. Do you confirm this ? [y/n]") response = await self.bot.wait_for("message", check=self.is_yn_answer) - if repsonse[0].lower() == "n": + if response.content[0].lower() == "n": await self.config_channels[ctx.guild.id].send(f"Aborting configuration of {role_lvl}. Do you want to retry? [y/n]") response = await self.bot.wait_for("message", check=self.is_yn_answer) if response[0].lower() == "n": @@ -304,7 +307,7 @@ async def cfg_goodbye(self, ctx): #the user has made a mistake if response[0].lower() == "n": await self.config_channels[ctx.guild.id].send("Do you want to retry ? [y/n]") - response = self.bot.wait_for("message", check=self.is_yn_answer) + response = await self.bot.wait_for("message", check=self.is_yn_answer) if response[0].lower == "n": message = False retry = False @@ -327,7 +330,7 @@ async def allow_ad(ctx): try: await self.config_channels[ctx.guild.id].send("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 (you can always change this later). [y/n]") repsonse = await self.bot.wait_for("message", check=self.is_yn_answer) - if reponse[0].lower()=="n": return False + if reponse.content[0].lower()=="n": return False await self.config_channels[ctx.guild.id].send("Thank you very much ! In which channel do you want me to post this message ?") reponse = await self.bot.wait_for("message", check=self.is_answer) diff --git a/Embedding.py b/Embedding.py index 9a1d0c4..e8603d9 100644 --- a/Embedding.py +++ b/Embedding.py @@ -1,5 +1,6 @@ import logging import discord +import io from settings import * from utilities import * @@ -41,36 +42,24 @@ async def embed(self, ctx, *args): return #lining attachements - attachments = ctx.message.attachments - - to_link = [] - img_url = None - for attachment in attachments: - #whether the attachment is the image - if attachment.height: - img_url = attachment - - else: - to_link.append(attachment.url) - - #building msg - msg = ctx.message.content - msg +="\n" - for attachment in to_link: - msg += f"{attachment}\n" + 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 = msg, + description = ctx.message.content, colour = ctx.author.color, url = None ) - if img_url: - await embed_msg.set_image(url=img_url) - embed_msg.set_footer(text=ctx.message.author.name, icon_url=ctx.message.author.avatar_url) + print(ctx.message.author.name) + 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) + await ctx.message.channel.send(embed=embed_msg, files=files) def setup(bot): bot.add_cog(Embedding(bot)) \ No newline at end of file diff --git a/settings.py b/settings.py index ef46cf8..0fd853c 100644 --- a/settings.py +++ b/settings.py @@ -58,6 +58,21 @@ } } +DEFAULT_SERVER_FILE = '''{ + "poll_channels": [], + "todo_channel": false, + "roles": { + "manager": [], + "admin": [] + }, + "messages": { + "welcome": false, + "goodbye": false + }, + "advertisement": false + +}''' + #logging settings LOG_FILE = "forebot.log" LOGGING_HANDLER = logging.FileHandler(LOG_FILE, "a") diff --git a/utilities.py b/utilities.py index ce309cf..062ebbf 100644 --- a/utilities.py +++ b/utilities.py @@ -59,7 +59,7 @@ def predicate(ctx): def is_server_owner(): def predicate(ctx): - if ctx.author == ctx.guild.owmer: + if ctx.author == ctx.guild.owner: return True ctx.send(ERR_UNSUFFICIENT_PRIVILEGE) return False From 4ea6837088daa39b00ba20dd6d6f5e12691aa56d Mon Sep 17 00:00:00 2001 From: NotaSmartDev Date: Sat, 8 Jun 2019 13:17:52 +0200 Subject: [PATCH 32/52] fixed #39 & other bugs --- Config.py | 26 +++++++++++++++++--------- Embedding.py | 3 +-- Poll.py | 29 ++++++++++++++++++----------- utilities.py | 18 ++++++++++++++---- 4 files changed, 50 insertions(+), 26 deletions(-) diff --git a/Config.py b/Config.py index 971edf3..3c6950a 100644 --- a/Config.py +++ b/Config.py @@ -61,7 +61,7 @@ async def init(self, ctx): #making conf file if it doesn't exist if not is_init(): with open(f"{ctx.guild.id}.json", "w") as file: - pass + file.write(DEFAULT_SERVER_FILE) #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\n*Starting full configuration...*''') @@ -102,11 +102,11 @@ async def chg(self, ctx, setting): local_logger.exception(e) - async def is_yn_answer(self, ctx): - if (ctx.channel == self.config_channels[ctx.guild.id]) and (ctx.content in (self.allowed_answers[0] or self.allowed_answers[1])): return True + def is_yn_answer(self, ctx): + if (ctx.channel == self.config_channels[ctx.guild.id]) and ((ctx.content in self.allowed_answers[0]) or (ctx.content in self.allowed_answers[1])): return True return False - async def is_answer(self, ctx): + def is_answer(self, ctx): if ctx.channel == self.config_channels[ctx.guild.id]: return True return False @@ -114,7 +114,7 @@ async def is_answer(self, ctx): async def cfg_poll(self, ctx): try: await self.config_channels[ctx.guild.id].send("**Starting poll configuration**") - await self.config_channels[ctx.guild.id].send("Do you want to activate polls on this server ? [yes/no]") + await self.config_channels[ctx.guild.id].send("Do you want to activate polls on this server ? [y/n]") #awaiting the user response response = await self.bot.wait_for("message", check=self.is_yn_answer) if not response.content[0].lower() =="y": return False @@ -122,9 +122,12 @@ async def cfg_poll(self, ctx): retry = True while retry: #getting the list of channels to be marked polls - await self.config_channels[ctx.guild.id].send("List all the channels you want to use as poll channels.") + await self.config_channels[ctx.guild.id].send(f"List all the channels you want to use as poll channels. You must mention those channels like this: {self.config_channels[ctx.guild.id].mention}") response = await self.bot.wait_for("message", check=self.is_answer) poll_channels = response.channel_mentions + if len(poll_channels) == 0: + await self.config_channels[ctx.guild.id].send("You need to add at least one channel.") + continue #building string with all the channels that will be marked for polls poll_channels_str = "" @@ -135,7 +138,7 @@ async def cfg_poll(self, ctx): response = await self.bot.wait_for("message", check=self.is_yn_answer) #wether the asnwer was positive - print(response.content) + print(f"User's response: {response.content}") if not response.content[0].lower() =="y": #making sure the user really wants to cancel poll configuration await self.config_channels[ctx.guild.id].send("Aborting addition of poll channels. Do you want to leave the poll configuration interface ? [y/n]") @@ -146,10 +149,14 @@ async def cfg_poll(self, ctx): else: retry=False + poll_channels_ids = [] + for chan in poll_channels: + poll_channels_ids.append(chan.id) + print(poll_channels_ids) old_conf = get_conf(ctx.guild.id) - old_conf["poll_channels"] = poll_channels + old_conf["poll_channels"] = poll_channels_ids if update_conf(ctx.guild.id, old_conf) == False: await self.config_channels[ctx.guild.id].send(ERR_UNEXCPECTED) @@ -162,6 +169,7 @@ async def cfg_poll(self, ctx): except Exception as e: local_logger.exception(e) + raise e async def cfg_role(self, ctx): @@ -170,7 +178,7 @@ async def cfg_role(self, ctx): await self.config_channels[ctx.guild.id].send("**Starting role configuration**") await self.config_channels[ctx.guild.id].send("This bot uses two level of clearance for its commands.") await self.config_channels[ctx.guild.id].send("The 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.") - await self.config_channels[ctx.guild.id].send("The second level of clearance is **admin**. Anyonw 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.") + await self.config_channels[ctx.guild.id].send("The 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: diff --git a/Embedding.py b/Embedding.py index e8603d9..8b1a289 100644 --- a/Embedding.py +++ b/Embedding.py @@ -51,11 +51,10 @@ async def embed(self, ctx, *args): embed_msg = discord.Embed( title = None, - description = ctx.message.content, + description = ctx.message.content[8:], colour = ctx.author.color, url = None ) - print(ctx.message.author.name) embed_msg.set_author(name=ctx.message.author.name, icon_url=ctx.message.author.avatar_url) await ctx.message.delete() diff --git a/Poll.py b/Poll.py index a1fa47c..e9f3b1d 100644 --- a/Poll.py +++ b/Poll.py @@ -82,7 +82,7 @@ async def on_raw_reaction_add(self, payload): react_for = message.reactions[0].count react_against = message.reactions[2].count #changing color of the embed - await self.balance_poll_color(message, react_for, react_against) + await self.balance_poll_color(message, message.reactions[0].count, message.reactions[2].count) @@ -97,19 +97,26 @@ async def on_raw_reaction_remove(self, payload): #checking that user isn't the bot if (payload.user_id != self.bot.user.id) and (payload.channel_id in poll_allowed_chans): - - react_for = message.reactions[0].count - react_against = message.reactions[2].count #changing color of the embed - await self.balance_poll_color(message, react_for, react_against) + await self.balance_poll_color(message, message.reactions[0].count, message.reactions[2].count) + + + - async def balance_poll_color(self, msg, rfor, ragainst): - #determining their rgb values - r_value = (ragainst/max(rfor, ragainst))*255 - g_value = (rfor/max(rfor, ragainst))*255 - #making the color - color = int((r_value*65536) + (g_value*256)) + + async def 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 diff --git a/utilities.py b/utilities.py index 062ebbf..43c2b90 100644 --- a/utilities.py +++ b/utilities.py @@ -86,12 +86,14 @@ def has_changed(server, last_time): def get_conf(guild_id): with open(f"{guild_id}.json", "r") as file: - return json.load(file) + conf = json.load(file) + print(conf) + return conf def update_conf(guild_id, conf_dict): try: - with open(f"{guild_id}.json", "r") as file: - json.dump(file, conf_dict) + with open(f"{guild_id}.json", "w") as file: + json.dump(conf_dict, file) return True except Exception as e: @@ -118,4 +120,12 @@ def get_roles(guild_id, lvl): def get_poll_chans(guild_id): with open(f"{guild_id}.json", "r") as file: - return json.load(file)["poll_channels"] + fl = json.load(file) + + print(fl) + chans = fl["poll_channels"] + print(chans) + if len(chans)==0: + return None + + return chans From cf1099d9815f48e88ea74331656392a0b5acb4ca Mon Sep 17 00:00:00 2001 From: NotaSmartDev Date: Mon, 10 Jun 2019 18:50:04 +0200 Subject: [PATCH 33/52] fixed #27 --- Todo.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Todo.py b/Todo.py index f296228..8663cf9 100644 --- a/Todo.py +++ b/Todo.py @@ -35,7 +35,7 @@ async def on_raw_reaction_add(self, reaction): repost_field_value = None for field in message.embeds[0].fields: - if field.name == PUBLIC_REPOST: + if field.name == "Public repost": repost_field_value= field.value if repost_field_value!= None: @@ -123,7 +123,7 @@ async def add(self, ctx, *args): new_embed.set_footer(text=command[1]) if repost_channel != None: public_todo = await repost_channel.send(embed=new_embed) - new_embed.add_field(name=PUBLIC_REPOST, value=repost_channel.mention + " : " + str(public_todo.id), inline=True) + new_embed.add_field(name="Public repost", value=repost_channel.mention + " : " + str(public_todo.id), inline=True) message = await channel.send(embed=new_embed) await message.add_reaction(EMOJIS['wastebasket']) From a484f6743a9b53f849723e7a2251457474929baf Mon Sep 17 00:00:00 2001 From: NotaSmartDev Date: Mon, 10 Jun 2019 20:02:21 +0200 Subject: [PATCH 34/52] various bug fixes --- Config.py | 3 +-- Poll.py | 7 +------ Slapping.py | 2 +- settings.py | 2 -- utilities.py | 9 +++++---- 5 files changed, 8 insertions(+), 15 deletions(-) diff --git a/Config.py b/Config.py index 3c6950a..4c8da84 100644 --- a/Config.py +++ b/Config.py @@ -39,8 +39,6 @@ def __init__(self, bot): - - @commands.group() @is_server_owner() async def cfg(self, ctx): @@ -60,6 +58,7 @@ async def init(self, ctx): #making conf file if it doesn't exist if not is_init(): + print("\nThis server wasn't innitalized") with open(f"{ctx.guild.id}.json", "w") as file: file.write(DEFAULT_SERVER_FILE) diff --git a/Poll.py b/Poll.py index e9f3b1d..e5aa776 100644 --- a/Poll.py +++ b/Poll.py @@ -85,7 +85,6 @@ async def on_raw_reaction_add(self, payload): 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): @@ -100,11 +99,6 @@ async def on_raw_reaction_remove(self, payload): #changing color of the embed await self.balance_poll_color(message, message.reactions[0].count, message.reactions[2].count) - - - - - async def poll_color(self, msg, for_count, against_count): r = g = 128 diff = for_count - against_count @@ -126,6 +120,7 @@ async def poll_color(self, msg, for_count, against_count): @commands.Cog.listener() + @is_init() async def on_message(self, message): if message.author==self.bot.user: return diff --git a/Slapping.py b/Slapping.py index a7957f8..fdea35e 100644 --- a/Slapping.py +++ b/Slapping.py @@ -68,7 +68,7 @@ async def pardon(self, ctx, member:discord.Member, nbr=0): #checking wether the user has already been slapped if str(member.id) not in slaps[str(ctx.guild.id)]: - pass + return else: if nbr==0 or slaps[str(ctx.guild.id)][str(member.id)] maybe remove it ? -PUBLIC_REPOST="Public repost" #files ENABLED_EXTENSIONS_FILE = "enabled_exts.json" diff --git a/utilities.py b/utilities.py index 43c2b90..7c7b837 100644 --- a/utilities.py +++ b/utilities.py @@ -35,15 +35,16 @@ def check_condition(ctx): return result def is_init(): + print("Checking initialization !") + print("586661861458575391.json" in os.listdir()) def check_condition(ctx): conf_files = os.listdir() file_name = f"{ctx.guild.id}.json" + print(f"Looking for {file_name} in {conf_files}") + local_logger.info(f"Looking for {file_name} in {conf_files}") return file_name in conf_files - result = commands.check(check_condition) - if result == False: - ctx.send(ERR_NOT_SETUP) - return result + return commands.check(check_condition) def has_auth(clearance, *args): def predicate(ctx): From 2c9fbefb1cdc5f3e903c7f686b3a0686d4c69a7d Mon Sep 17 00:00:00 2001 From: NotaSmartDev Date: Mon, 10 Jun 2019 23:06:29 +0200 Subject: [PATCH 35/52] improved docs & fixed lots of bugs & improved check functions --- CONTRIBUTING.md | 3 +- Config.py | 80 ++++++++++++++++++++++++++----------------------- Poll.py | 5 +++- README.md | 11 ++++++- settings.py | 3 +- utilities.py | 18 ++++++----- 6 files changed, 70 insertions(+), 50 deletions(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 7593a98..8279315 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -11,5 +11,4 @@ Although contributing to the project is allowed and even encouraged it has to fo -This is a *guideline* which means that some exceptions may be made. However it is recommend that you respect it for your contribution to be validated. If you have any other question or think a point is unclear or too limiting feel free to contact an @admin - +This is a *guideline* which means that some exceptions may be made. However it is recommend that you respect it for your contribution to be validated. If you have any other question or think a point is unclear or too limiting feel free to contact an @admin \ No newline at end of file diff --git a/Config.py b/Config.py index 4c8da84..8d5792d 100644 --- a/Config.py +++ b/Config.py @@ -1,5 +1,6 @@ import logging import discord +import asyncio from settings import * from utilities import * @@ -57,8 +58,7 @@ async def init(self, ctx): self.config_channels[ctx.guild.id] = await ctx.guild.create_text_channel("cli-bot-config") #making conf file if it doesn't exist - if not is_init(): - print("\nThis server wasn't innitalized") + if not was_init(ctx): with open(f"{ctx.guild.id}.json", "w") as file: file.write(DEFAULT_SERVER_FILE) @@ -90,6 +90,11 @@ async def init(self, ctx): #await self.config_channels[ctx.guild.id].delete(reason="Failed to interactively configure the bot") 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) + + @cfg.command() @is_init() @@ -137,7 +142,6 @@ async def cfg_poll(self, ctx): response = await self.bot.wait_for("message", check=self.is_yn_answer) #wether the asnwer was positive - print(f"User's response: {response.content}") if not response.content[0].lower() =="y": #making sure the user really wants to cancel poll configuration await self.config_channels[ctx.guild.id].send("Aborting addition of poll channels. Do you want to leave the poll configuration interface ? [y/n]") @@ -152,7 +156,6 @@ async def cfg_poll(self, ctx): for chan in poll_channels: poll_channels_ids.append(chan.id) - print(poll_channels_ids) old_conf = get_conf(ctx.guild.id) old_conf["poll_channels"] = poll_channels_ids @@ -174,10 +177,7 @@ async def cfg_poll(self, ctx): async def cfg_role(self, ctx): try: #introducing the clearance levels the bot uses - await self.config_channels[ctx.guild.id].send("**Starting role configuration**") - await self.config_channels[ctx.guild.id].send("This bot uses two level of clearance for its commands.") - await self.config_channels[ctx.guild.id].send("The 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.") - await self.config_channels[ctx.guild.id].send("The 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.") + await self.config_channels[ctx.guild.id].send("**Starting 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: @@ -192,7 +192,7 @@ async def cfg_role(self, ctx): #building roll string roles_str = "" for role in roles: - roles_str+= f" {role}" + roles_str+= f" {role.mention}" #asking for confirmation await self.config_channels[ctx.guild.id].send(f"You are about to give{roles_str} roles the **{role_lvl}** level of clearance. Do you confirm this ? [y/n]") @@ -200,10 +200,12 @@ async def cfg_role(self, ctx): if response.content[0].lower() == "n": await self.config_channels[ctx.guild.id].send(f"Aborting configuration of {role_lvl}. Do you want to retry? [y/n]") response = await self.bot.wait_for("message", check=self.is_yn_answer) - if response[0].lower() == "n": + if response.content[0].lower() == "n": 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") @@ -248,7 +250,7 @@ async def cfg_welcome(self, ctx): await self.config_channels[ctx.guild.id].send("Do you want to have a welcome message sent when a new user joins the server ? [y/n]") response = await self.bot.wait_for("message", check=self.is_yn_answer) - if response[0].lower() == "n": + if response.content[0].lower() == "n": message = False retry = False @@ -258,28 +260,30 @@ async def cfg_welcome(self, ctx): message = await self.bot.wait_for("message", check=self.is_answer) - self.config_channels[ctx.guild.id].send("To make sure the message is as you'd like I'm sending it to you.") - await self.config_channels[ctx.guild.id].send(message.format(ctx.guild.owner.mention)) + await self.config_channels[ctx.guild.id].send("To make sure the message is as you'd like I'm sending it to you.\n**-- Beginning of message --**") + await self.config_channels[ctx.guild.id].send(message.content.format(ctx.guild.owner.mention)) - await self.config_channels[ctx.guild.id].send("Is this the message you want to set as the welcome message ? [y/n]") + await self.config_channels[ctx.guild.id].send("**--End of message --**\nIs this the message you want to set as the welcome message ? [y/n]") response = await self.bot.wait_for("message", check=self.is_yn_answer) #the user has made a mistake - if response[0].lower() == "n": + if response.content[0].lower() == "n": await self.config_channels[ctx.guild.id].send("Do you want to retry ? [y/n]") response = await self.bot.wait_for("message", check=self.is_yn_answer) - if response[0].lower == "n": + if response.content[0].lower == "n": message = False retry = False #otherwise retry continue - old_conf = get_conf(ctx.guild.id) - old_conf["messages"]["welcome"]= message + else: retry = False - if update_conf(ctx.guild.id, old_conf) == False: - await self.config_channels[ctx.guild.id].send(ERR_UNEXCPECTED) + if message != False: + old_conf = get_conf(ctx.guild.id) + old_conf["messages"]["welcome"]= message.content + if update_conf(ctx.guild.id, old_conf) == False: + await self.config_channels[ctx.guild.id].send(ERR_CANT_SAVE) except Exception as e: @@ -295,7 +299,7 @@ async def cfg_goodbye(self, ctx): await self.config_channels[ctx.guild.id].send("Do you want to have a goodbye message sent when an user leaves the server ? [y/n]") response = await self.bot.wait_for("message", check=self.is_yn_answer) - if response[0].lower() == "n": + if response.content[0].lower() == "n": message = False retry = False @@ -305,27 +309,29 @@ async def cfg_goodbye(self, ctx): message = await self.bot.wait_for("message", check=self.is_answer) - self.config_channels[ctx.guild.id].send("To make sure the message is as you'd like I'm sending it to you. Enventual mentions will be directed to you.") - await self.config_channels[ctx.guild.id].send(message.format(ctx.guild.owner.mention)) + await self.config_channels[ctx.guild.id].send("To make sure the message is as you'd like I'm sending it to you. Enventual mentions will be directed to you.\n**-- Beginning of message --**") + await self.config_channels[ctx.guild.id].send(message.content.format(ctx.guild.owner.mention)) - await self.config_channels[ctx.guild.id].send("Is this the message you want to set as the goodbye message ? [y/n]") + await self.config_channels[ctx.guild.id].send("**--End of message --**\nIs this the message you want to set as the goodbye message ? [y/n]") response = await self.bot.wait_for("message", check=self.is_yn_answer) #the user has made a mistake - if response[0].lower() == "n": + if response.content[0].lower() == "n": await self.config_channels[ctx.guild.id].send("Do you want to retry ? [y/n]") response = await self.bot.wait_for("message", check=self.is_yn_answer) - if response[0].lower == "n": + if response.content[0].lower == "n": message = False retry = False #otherwise retry continue + else: retry = False - old_conf = get_conf(ctx.guild.id) - old_conf["messages"]["goodbye"]= message + if message != False: + old_conf = get_conf(ctx.guild.id) + old_conf["messages"]["goodbye"]= message.content - if update_conf(ctx.guild.id, old_conf) == False: - await self.config_channels[ctx.guild.id].send(ERR_UNEXCPECTED) + if update_conf(ctx.guild.id, old_conf) == False: + await self.config_channels[ctx.guild.id].send(ERR_CANT_SAVE) @@ -333,20 +339,20 @@ async def cfg_goodbye(self, ctx): local_logger.exception(e) raise e - async def allow_ad(ctx): + async def allow_ad(self, ctx): try: await self.config_channels[ctx.guild.id].send("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 (you can always change this later). [y/n]") - repsonse = await self.bot.wait_for("message", check=self.is_yn_answer) - if reponse.content[0].lower()=="n": return False + response = await self.bot.wait_for("message", check=self.is_yn_answer) + if response.content[0].lower()=="n": return False await self.config_channels[ctx.guild.id].send("Thank you very much ! In which channel do you want me to post this message ?") - reponse = await self.bot.wait_for("message", check=self.is_answer) + response = await self.bot.wait_for("message", check=self.is_answer) old_conf = get_conf(ctx.guild.id) - old_conf["advertisement"] = reponse.channel_mentions[0].id + old_conf["advertisement"] = response.channel_mentions[0].id - chan = dfind(lambda c: c.id==old_conf["advertisement"], ctx.guild.channels) - chan.send(self.ad_msg) + chan = discord.utils.find(lambda c: c.id==old_conf["advertisement"], ctx.guild.channels) + await chan.send(self.ad_msg) #updating conf update_conf(ctx.guild.id, old_conf) diff --git a/Poll.py b/Poll.py index e5aa776..c93ef7e 100644 --- a/Poll.py +++ b/Poll.py @@ -120,10 +120,13 @@ async def poll_color(self, msg, for_count, against_count): @commands.Cog.listener() - @is_init() async def on_message(self, message): if message.author==self.bot.user: return + if not was_init(message): + await message.channel.send(ERR_NOT_SETUP) + return + #getting poll_allowed_chans poll_allowed_chans = get_poll_chans(message.guild.id) diff --git a/README.md b/README.md index 969ab03..098aaca 100644 --- a/README.md +++ b/README.md @@ -14,6 +14,14 @@ After the very first version I got some help of a friend of mine. Together we ma Here is an exhaustive list of all extensions and the commands they provide. This list is kept up to date with the latest updates. +#### Defaults `ext` (runner) + +A suite of commands always activated which handle + + + + + #### Poll `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. @@ -58,9 +66,10 @@ Allows moderators to add and remove roles to members. -#### Config (owner) +#### Config `cfg` (owner) Allows the owner of a server to configure the behavior of the bot. - `init`: starts full configuration of the bot - `chg` ``: starts the configuration of the `` extension. + diff --git a/settings.py b/settings.py index 82453d6..38b5e4c 100644 --- a/settings.py +++ b/settings.py @@ -83,4 +83,5 @@ ERR_UNEXCPECTED = "An unexcpected error occured. Please report a bug in {} or contact an admin of your server." ERR_NOT_ENOUGH_ARG = "This command requires additional arguments. See `::help ` to get more information on the command's usage" ERR_UNSUFFICIENT_PRIVILEGE = "You don't have the permission to do this..." -ERR_NOT_SETUP = "This server hasn't been configured. If you're the owner of the server you can initialise the bot by doing `::cfg init` in any channel. You won't be able to use the bot before that." \ No newline at end of file +ERR_NOT_SETUP = "This server hasn't been configured. If you're the owner of the server you can initialise the bot by doing `::cfg init` in any channel. You won't be able to use the bot before that." +ERR_CANT_SAVE = "Couldn't save settings to JSON configuration file." \ No newline at end of file diff --git a/utilities.py b/utilities.py index 7c7b837..2f59c6a 100644 --- a/utilities.py +++ b/utilities.py @@ -31,21 +31,25 @@ def check_condition(ctx): return ctx.message.author.id ==RUNNER_ID result = commands.check(check_condition) if result == False: - ctx.send(ERR_UNSUFFICIENT_PRIVILEGE) + pass + #ctx.send(ERR_UNSUFFICIENT_PRIVILEGE) return result def is_init(): - print("Checking initialization !") - print("586661861458575391.json" in os.listdir()) def check_condition(ctx): conf_files = os.listdir() file_name = f"{ctx.guild.id}.json" - print(f"Looking for {file_name} in {conf_files}") - local_logger.info(f"Looking for {file_name} in {conf_files}") + #ctx.send(ERR_NOT_SETUP) return file_name in conf_files return commands.check(check_condition) +def was_init(ctx): + '''same as the previous function except this one isn't a decorator. Mainly used for listenners''' + if f"{ctx.guild.id}.json" in os.listdir(): + return True + return False + def has_auth(clearance, *args): def predicate(ctx): allowed_roles = get_roles(ctx.guild.id, clearance) @@ -62,7 +66,7 @@ def is_server_owner(): def predicate(ctx): if ctx.author == ctx.guild.owner: return True - ctx.send(ERR_UNSUFFICIENT_PRIVILEGE) + #ctx.send(ERR_UNSUFFICIENT_PRIVILEGE) return False return commands.check(predicate) @@ -123,9 +127,7 @@ def get_poll_chans(guild_id): with open(f"{guild_id}.json", "r") as file: fl = json.load(file) - print(fl) chans = fl["poll_channels"] - print(chans) if len(chans)==0: return None From 8d4f148bb95dc20a53799fcc335b25e2451f3d48 Mon Sep 17 00:00:00 2001 From: NotaSmartDev Date: Mon, 10 Jun 2019 23:42:01 +0200 Subject: [PATCH 36/52] fixed bugs & made configuration safer --- Config.py | 11 ++++++++--- settings.py | 31 +++++++++++++++++++++++++++++-- utilities.py | 3 ++- 3 files changed, 39 insertions(+), 6 deletions(-) diff --git a/Config.py b/Config.py index 8d5792d..6dd88f2 100644 --- a/Config.py +++ b/Config.py @@ -121,7 +121,7 @@ async def cfg_poll(self, ctx): await self.config_channels[ctx.guild.id].send("Do you want to activate polls on this server ? [y/n]") #awaiting the user response response = await self.bot.wait_for("message", check=self.is_yn_answer) - if not response.content[0].lower() =="y": return False + if not response.content[0].lower() == "y": return False retry = True while retry: @@ -129,9 +129,11 @@ async def cfg_poll(self, ctx): await self.config_channels[ctx.guild.id].send(f"List all the channels you want to use as poll channels. You must mention those channels like this: {self.config_channels[ctx.guild.id].mention}") response = await self.bot.wait_for("message", check=self.is_answer) poll_channels = response.channel_mentions - if len(poll_channels) == 0: - await self.config_channels[ctx.guild.id].send("You need to add at least one channel.") + print(poll_channels) + if self.config_channels[ctx.guild.id] in poll_channels: + await self.config_channels[ctx.guild.id].send("You cannot set this channel as a poll one for safety reasons. Please try again...") continue + #building string with all the channels that will be marked for polls poll_channels_str = "" @@ -188,6 +190,9 @@ async def cfg_role(self, ctx): await self.config_channels[ctx.guild.id].send(f"List all the roles you want to be given the **{role_lvl}** level of clearance.") response = await self.bot.wait_for("message", check=self.is_answer) roles = response.role_mentions + if len(roles) == 0: + await self.config_channels[ctx.guild.id].send(f"You need to set at least one role for the {role_lvl} clearance.") + continue #building roll string roles_str = "" diff --git a/settings.py b/settings.py index 38b5e4c..e6a034a 100644 --- a/settings.py +++ b/settings.py @@ -1,4 +1,6 @@ import logging + + ######################################### # # # # @@ -34,6 +36,14 @@ } +######################################### +# # +# # +# Files # +# # +# # +######################################### + #files ENABLED_EXTENSIONS_FILE = "enabled_exts.json" SLAPPING_FILE = "slapped.json" @@ -71,14 +81,31 @@ }''' -#logging settings +######################################### +# # +# # +# Logging # +# # +# # +######################################### + + + LOG_FILE = "forebot.log" LOGGING_HANDLER = logging.FileHandler(LOG_FILE, "a") LOGGING_FORMATTER = logging.Formatter("[%(asctime)s]:%(name)s:%(message)s") LOGGING_LEVEL = logging.INFO LOGGING_HANDLER.setFormatter(LOGGING_FORMATTER) -#generic error messages +######################################### +# # +# # +# Errors # +# # +# # +######################################### + + ERR_NO_SUBCOMMAND = "You didn't provide any subcommand. See `::help ` for more info on command usage." ERR_UNEXCPECTED = "An unexcpected error occured. Please report a bug in {} or contact an admin of your server." ERR_NOT_ENOUGH_ARG = "This command requires additional arguments. See `::help ` to get more information on the command's usage" diff --git a/utilities.py b/utilities.py index 2f59c6a..e322899 100644 --- a/utilities.py +++ b/utilities.py @@ -129,6 +129,7 @@ def get_poll_chans(guild_id): chans = fl["poll_channels"] if len(chans)==0: - return None + #isn't None to prevent Poll listener from crashing + return [] return chans From 20b81cfedff5b400a468143e84d3386cbfa93a19 Mon Sep 17 00:00:00 2001 From: NotaSmartDev Date: Mon, 10 Jun 2019 23:54:20 +0200 Subject: [PATCH 37/52] renamed: Feedback.py -> Development.py --- Feedback.py | 26 -------------------------- 1 file changed, 26 deletions(-) delete mode 100644 Feedback.py diff --git a/Feedback.py b/Feedback.py deleted file mode 100644 index 992fff2..0000000 --- a/Feedback.py +++ /dev/null @@ -1,26 +0,0 @@ -import discord -import json -import logging -from settings import * -from utilities import * - -class Feedback(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 - - - @commands.command - @commands.is_owner() - async def update(self, message="The bot has been updated. Look at the development server for more information"): #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.''' - for g in self.bot.fetch_guilds(): - #getting the dm with the server owner - dm = g.owner.dm_channel - if dm ==None: - dm = g.owner.create_dm() - - #sending the server owner the updates - dm.send(message) \ No newline at end of file From 49745c52af97113ef5793c054a62d0ecbf5c757f Mon Sep 17 00:00:00 2001 From: NotaSmartDev Date: Tue, 11 Jun 2019 18:28:42 +0200 Subject: [PATCH 38/52] fixed typo in Poll & #6 --- BotEssentials.py | 2 +- Poll.py | 21 +++++++++++++++++---- 2 files changed, 18 insertions(+), 5 deletions(-) diff --git a/BotEssentials.py b/BotEssentials.py index 6618a50..6748c81 100644 --- a/BotEssentials.py +++ b/BotEssentials.py @@ -91,7 +91,7 @@ async def clear(self, ctx, nbr:int): to_del.append(msg) try: - await self.bot.delete_messages(to_del) + await ctx.channel.delete_messages(to_del) except Exception as e: local_logger.exception("Couldn't delete at least on of{}".format(to_del)) raise e diff --git a/Poll.py b/Poll.py index c93ef7e..73e2fed 100644 --- a/Poll.py +++ b/Poll.py @@ -1,6 +1,7 @@ import os import logging import discord +import io from settings import * from utilities import * @@ -42,7 +43,7 @@ async def on_raw_reaction_add(self, payload): #getting poll_allowed_chans #@is_init - poll_allowed_chans = get_poll_chans(payload.guild.id) + poll_allowed_chans = get_poll_chans(payload.guild_id) #checking that user isn't the bot @@ -89,7 +90,7 @@ async def on_raw_reaction_add(self, payload): async def on_raw_reaction_remove(self, payload): #getting poll_allowed_chans - poll_allowed_chans = get_poll_chans(payload.guild.id) + poll_allowed_chans = get_poll_chans(payload.guild_id) #fetching concerned message and the user who added the reaction message = await self.bot.get_channel(payload.channel_id).fetch_message(payload.message_id) @@ -99,7 +100,7 @@ async def on_raw_reaction_remove(self, payload): #changing color of the embed await self.balance_poll_color(message, message.reactions[0].count, message.reactions[2].count) - async def poll_color(self, msg, for_count, against_count): + async def balance_poll_color(self, msg, for_count, against_count): r = g = 128 diff = for_count - against_count votes = for_count + against_count @@ -131,18 +132,30 @@ async def on_message(self, message): poll_allowed_chans = get_poll_chans(message.guild.id) if message.channel.id in poll_allowed_chans and message.content.startswith(PREFIX)!=True: + + #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) + + #making embed embed_poll = discord.Embed( title = message.author.name, description = message.content, 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) + 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"]) From 0dda371fa2529ca8c66adadf7fead4f13e877253 Mon Sep 17 00:00:00 2001 From: NotaSmartDev Date: Tue, 11 Jun 2019 23:49:17 +0200 Subject: [PATCH 39/52] documented utilities.py & started to make Todo config-conpliant --- Development.py | 24 ++++++++++++++-------- Poll.py | 2 -- Todo.py | 56 ++++++++++++++++++++++++-------------------------- settings.py | 3 +++ utilities.py | 12 +++++++++-- 5 files changed, 55 insertions(+), 42 deletions(-) diff --git a/Development.py b/Development.py index bb2bd91..999c061 100644 --- a/Development.py +++ b/Development.py @@ -34,19 +34,25 @@ def __init__(self, bot): self.bot = bot - @commands.command + @commands.command() @commands.is_owner() - async def update(self, ctx, message="The bot has been updated. Look at the development server for more information"): #should message be put in settings.py ? + 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.''' - for g in self.bot.fetch_guilds(): - #getting the dm with the server owner - dm = g.owner.dm_channel - if dm ==None: - dm = g.owner.create_dm() + #building message + if len(words)==0: + message = DEFAULT_UPDATE_MESSAGE + else: + message = "" + for w in words: + message+=f" {w}" - #sending the server owner the updates - dm.send(message) + 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() @commands.is_owner() #-> this needs to be changed to is_dev() diff --git a/Poll.py b/Poll.py index 73e2fed..eab073c 100644 --- a/Poll.py +++ b/Poll.py @@ -204,8 +204,6 @@ async def rm(self, ctx, msg_id): 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 - - def setup(bot): diff --git a/Todo.py b/Todo.py index 8663cf9..a623ff2 100644 --- a/Todo.py +++ b/Todo.py @@ -62,7 +62,7 @@ async def on_raw_reaction_remove(self, reaction): await message.add_reaction(EMOJIS['check']) @commands.group() - @commands.has_any_role(*GESTION_ROLES, *ADMIN_ROLE) + @has_auth("manager") async def todo(self, ctx): '''Commands to manage a todolist.''' if ctx.invoked_subcommand is None: @@ -132,7 +132,7 @@ async def add(self, ctx, *args): await ctx.message.delete() @todo.command() - async def addtype(self, ctx, command): + async def addtype(self, ctx, todo_type, hex_color): '''Command to add a todo type.''' command = command.split(";") @@ -158,36 +158,34 @@ async def addtype(self, ctx, command): 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, command): - '''Command to add a remove type.''' - with open(TODO_TYPES_FILE, "r") as file: - lines = file.readlines() - with open(TODO_TYPES_FILE, "w") as file: - deleted=False - for line in lines: - line = line.split(";") - if line[0] != command: - file.write(';'.join(line)) - else: - deleted=True - - if deleted: - await ctx.send('Todo type **'+command+'** deleted') - else: - await ctx.send('There is no type named **'+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): - '''Command to list all the todo types.''' - text = "**Type** - *Color*\n\n" - with open(TODO_TYPES_FILE, "r") as file: - lines = file.readlines() - for line in lines: - line = line.split(';') - text += "**" + line[0] + "** - *#"+line[1][2:] + "*\n" - - new_embed = discord.Embed(description=text, url="", color=0x28a745) - message = await ctx.channel.send(embed=new_embed) + '''Lists all available types''' + conf = get_conf(ctx.guild.id) + text = "" + for t in conf["todo_types"]: + text += f'''\n**{t}** - \t*#{conf["todo_types"][t]}*''' + + new_embed = discord.Embed(title="**Type** - *Color*", description=text, color=0x28a745) + await ctx.send(embed=new_embed) @todo.command() async def channel(self, ctx): diff --git a/settings.py b/settings.py index e6a034a..3c77bce 100644 --- a/settings.py +++ b/settings.py @@ -23,6 +23,9 @@ #invite to dev server URL DEV_SRV_URL = "https://discord.gg/mpGM5cg" +#update message +DEFAULT_UPDATE_MESSAGE = "The bot has been updated. Look at the development server for more information" + #emojis dict. May be possible to change incomprehensible unicode to other strings recognized by discord EMOJIS = { "thumbsup": "\U0001f44d", diff --git a/utilities.py b/utilities.py index e322899..5334e57 100644 --- a/utilities.py +++ b/utilities.py @@ -26,7 +26,7 @@ # # ######################################### -def is_runner(): +def is_runner(): # to be deleted sine it does the same as is_owner() def check_condition(ctx): return ctx.message.author.id ==RUNNER_ID result = commands.check(check_condition) @@ -36,6 +36,7 @@ def check_condition(ctx): return result def is_init(): + '''checks whether the server has been initialized. Meant as a fale-safe for commands requiring configuration.''' def check_condition(ctx): conf_files = os.listdir() file_name = f"{ctx.guild.id}.json" @@ -51,6 +52,7 @@ def was_init(ctx): return False def has_auth(clearance, *args): + '''checks whether the user invoking the command has the specified clearance level of clearance for the server the command is being ran on''' def predicate(ctx): allowed_roles = get_roles(ctx.guild.id, clearance) for role in ctx.author.roles: @@ -63,6 +65,7 @@ def predicate(ctx): return commands.check(predicate) def is_server_owner(): + '''check meant to verify whether its author os the owner of the server where the command is being ran''' def predicate(ctx): if ctx.author == ctx.guild.owner: return True @@ -81,7 +84,7 @@ def predicate(ctx): ######################################### def get_m_time(file): - return os.getmtime(file+"json") + return os.getmtime(file+".json") def has_changed(server, last_time): last_update = get_m_time(file) @@ -90,12 +93,14 @@ def has_changed(server, last_time): return False def get_conf(guild_id): + '''returns the configuration dict of the provided guild_id''' with open(f"{guild_id}.json", "r") as file: conf = json.load(file) print(conf) return conf def update_conf(guild_id, conf_dict): + '''writes the conf_dict to the provided guild_id configuration file''' try: with open(f"{guild_id}.json", "w") as file: json.dump(conf_dict, file) @@ -106,6 +111,7 @@ def update_conf(guild_id, conf_dict): return False def del_conf(guild_id): + '''deletes the configuration entry for the provided guild_id''' try: os.remove(f"{guild_id}.json") return True @@ -115,6 +121,7 @@ def del_conf(guild_id): return False def get_roles(guild_id, lvl): + '''returns the roles with the provided lvl of clearance for the specified guild_id''' try: with open(f"{guild_id}.json", "r") as file: return json.load(file)["roles"][lvl] @@ -124,6 +131,7 @@ def get_roles(guild_id, lvl): raise e def get_poll_chans(guild_id): + '''returns a list of channel ids marked as poll channels for the specified guild_id''' with open(f"{guild_id}.json", "r") as file: fl = json.load(file) From f9262c3fff58c265408c05f64a54b2c31481be4a Mon Sep 17 00:00:00 2001 From: NotaSmartDev Date: Wed, 12 Jun 2019 15:54:47 +0200 Subject: [PATCH 40/52] bug fixes and general improvement --- BotEssentials.py | 2 +- Config.py | 13 +++++----- Role.py | 4 +-- Slapping.py | 44 ++++++++------------------------ settings.py | 11 ++++---- utilities.py | 66 ++++++++++++++++++++++++++++++++++++++---------- 6 files changed, 79 insertions(+), 61 deletions(-) diff --git a/BotEssentials.py b/BotEssentials.py index 6748c81..a48c737 100644 --- a/BotEssentials.py +++ b/BotEssentials.py @@ -57,7 +57,7 @@ async def on_member_join(self, member): @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)) - goodbye_msg = get_conf(member.guild.id)["messages"]["welcome"] + goodbye_msg = get_conf(member.guild.id)["messages"]["goodbye"] if goodbye_msg != False: await member.guild.system_channel.send(goodbye_msg.format(member.mention)) diff --git a/Config.py b/Config.py index 6dd88f2..e649a91 100644 --- a/Config.py +++ b/Config.py @@ -1,6 +1,7 @@ import logging import discord import asyncio +import os from settings import * from utilities import * @@ -59,8 +60,12 @@ async def init(self, ctx): #making conf file if it doesn't exist if not was_init(ctx): - with open(f"{ctx.guild.id}.json", "w") as file: + #making config file + with open(os.path.join(CONFIG_FOLDER, f"{ctx.guild.id}.json"), "w") as file: file.write(DEFAULT_SERVER_FILE) + #making slapping file + with open(os.path.join(SLAPPING_FOLER, f"{ctx.guild.id}.json"), "w") as file: + file.write(DEFAULT_SLAPPED_FILE) #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\n*Starting full configuration...*''') @@ -107,7 +112,7 @@ async def chg(self, ctx, setting): def is_yn_answer(self, ctx): - if (ctx.channel == self.config_channels[ctx.guild.id]) and ((ctx.content in self.allowed_answers[0]) or (ctx.content in self.allowed_answers[1])): return True + if (ctx.channel == self.config_channels[ctx.guild.id]) and ((ctx.content.lower() in self.allowed_answers[0]) or (ctx.content.lower() in self.allowed_answers[1])): return True return False def is_answer(self, ctx): @@ -129,7 +134,6 @@ async def cfg_poll(self, ctx): await self.config_channels[ctx.guild.id].send(f"List all the channels you want to use as poll channels. You must mention those channels like this: {self.config_channels[ctx.guild.id].mention}") response = await self.bot.wait_for("message", check=self.is_answer) poll_channels = response.channel_mentions - print(poll_channels) if self.config_channels[ctx.guild.id] in poll_channels: await self.config_channels[ctx.guild.id].send("You cannot set this channel as a poll one for safety reasons. Please try again...") continue @@ -369,9 +373,6 @@ async def allow_ad(self, ctx): raise e - - - @cfg.command() async def leave(self, ctx): ctx.send("You are about to remove the bot from the server. This will erase all of your configuration from the mainframe and you won't be able to recover the bot without getting another invite. Are you sure you want to continue ? (y/N)") diff --git a/Role.py b/Role.py index cc62d41..9b106bc 100644 --- a/Role.py +++ b/Role.py @@ -45,7 +45,7 @@ async def role(self, ctx): @role.command() async def add(self, ctx, member: discord.Member, *roles:discord.Role): - '''adds role(s) to ''' + '''Gives listed roles''' if len(roles)==0: local_logger.warning("User didn't provide a role") await ctx.send("NotEnoughArguments:\tYou must provide at least one `role`") @@ -59,7 +59,7 @@ async def add(self, ctx, member: discord.Member, *roles:discord.Role): @role.command() async def rm(self, ctx, member:discord.Member, *roles:discord.Role): - '''removes role(s) to ''' + '''Removes 's roles''' if len(roles)==0: local_logger.warning("User didn't provide a role") await ctx.send("NotEnoughArguments:\tYou must provide at least one `role`") diff --git a/Slapping.py b/Slapping.py index fdea35e..b167347 100644 --- a/Slapping.py +++ b/Slapping.py @@ -31,31 +31,19 @@ class Slapping(commands.Cog): """a suite of commands meant to help moderators handle the server""" def __init__(self, bot): self.bot = bot - if SLAPPING_FILE not in os.listdir(): - with open(SLAPPING_FILE, "w") as file: - file.write("") - @commands.command() @is_init() @has_auth("manager") async def slap(self, ctx, member:discord.Member): '''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''' - with open(SLAPPING_FILE, "r") as file: - slaps = json.load(file) - - #checking wether the user has already been slapped - if str(member.id) not in slaps[str(ctx.guild.id)]: - slaps[str(ctx.guild.id)][str(member.id)] = 1 - - else: - slaps[str(ctx.guild.id)][str(member.id)] +=1 - - - #writting to file - with open(SLAPPING_FILE, "w") as file: - json.dump(slaps, file) + #slapping + slaps = get_slaps(ctx.guild.id, member.id) + print(type(slaps)) + slaps += 1 + update_slaps(ctx.guild.od, member.id, slaps) + #warning await ctx.send("{} you've been slapped by {} because of your behavior! This is the {} time. Be careful, if you get slapped too much there *will* be consequences !".format(member.mention, ctx.message.author.mention, slaps[str(ctx.guild.id)][str(member.id)])) @commands.command() @@ -63,26 +51,16 @@ async def slap(self, ctx, member:discord.Member): @has_auth("manager") async def pardon(self, ctx, member:discord.Member, nbr=0): '''Pardonning a member resets his slaps count.''' - with open(SLAPPING_FILE, "r") as file: - slaps = json.load(file) + slaps = get_slaps(ctx.guild.id, member.id) - #checking wether the user has already been slapped - if str(member.id) not in slaps[str(ctx.guild.id)]: - return + if nbr==0 or slaps Date: Wed, 12 Jun 2019 16:29:58 +0200 Subject: [PATCH 41/52] fixed Slapping --- Config.py | 3 ++- Slapping.py | 9 ++++----- settings.py | 2 +- utilities.py | 10 +++++----- 4 files changed, 12 insertions(+), 12 deletions(-) diff --git a/Config.py b/Config.py index e649a91..2ff0630 100644 --- a/Config.py +++ b/Config.py @@ -64,7 +64,7 @@ async def init(self, ctx): with open(os.path.join(CONFIG_FOLDER, f"{ctx.guild.id}.json"), "w") as file: file.write(DEFAULT_SERVER_FILE) #making slapping file - with open(os.path.join(SLAPPING_FOLER, f"{ctx.guild.id}.json"), "w") as file: + with open(os.path.join(SLAPPING_FOLDER, f"{ctx.guild.id}.json"), "w") as file: file.write(DEFAULT_SLAPPED_FILE) #starting all configurations @@ -98,6 +98,7 @@ async def init(self, ctx): 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") diff --git a/Slapping.py b/Slapping.py index b167347..0c5ae28 100644 --- a/Slapping.py +++ b/Slapping.py @@ -39,12 +39,11 @@ async def slap(self, ctx, member:discord.Member): '''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''' #slapping slaps = get_slaps(ctx.guild.id, member.id) - print(type(slaps)) slaps += 1 - update_slaps(ctx.guild.od, member.id, slaps) + update_slaps(ctx.guild.id, member.id, slaps) #warning - await ctx.send("{} you've been slapped by {} because of your behavior! This is the {} time. Be careful, if you get slapped too much there *will* be consequences !".format(member.mention, ctx.message.author.mention, slaps[str(ctx.guild.id)][str(member.id)])) + await ctx.send("{} you've been slapped by {} because of your behavior! This is the {} time. Be careful, if you get slapped too much there *will* be consequences !".format(member.mention, ctx.message.author.mention, slaps)) @commands.command() @is_init() @@ -59,9 +58,9 @@ async def pardon(self, ctx, member:discord.Member, nbr=0): else: slaps -=nbr - update_slaps(ctx.guild.od, member.id, slaps) + update_slaps(ctx.guild.id, member.id, slaps) - await ctx.send("{} you've been pardonned by {}.\t ({} slaps left)".format(member.mention, ctx.author.mention, slaps[str(ctx.guild.id)][str(member.id)])) + await ctx.send("{} you've been pardonned by {}.\t ({} slaps left)".format(member.mention, ctx.author.mention, slaps)) def setup(bot): bot.add_cog(Slapping(bot)) \ No newline at end of file diff --git a/settings.py b/settings.py index 2595042..9502094 100644 --- a/settings.py +++ b/settings.py @@ -49,7 +49,7 @@ #files ENABLED_EXTENSIONS_FILE = "enabled_exts.json" -SLAPPING_FOLER = "slapping" +SLAPPING_FOLDER = "slapping" CONFIG_FOLDER = "servers" #roles ROLES_LEVEL = ["manager", "admin"] diff --git a/utilities.py b/utilities.py index bd9327d..b7314f4 100644 --- a/utilities.py +++ b/utilities.py @@ -149,7 +149,7 @@ def get_poll_chans(guild_id): def get_slaps(guild_id, user_id): '''returns an int of the number of slaps of the user_id in the provided guild_id''' - with open(os.path.join(SLAPPING_FOLER, f"{guild_id}.json"), "r") as file: + with open(os.path.join(SLAPPING_FOLDER, f"{guild_id}.json"), "r") as file: fl = json.load(file) try: @@ -166,14 +166,14 @@ def get_slaps(guild_id, user_id): def update_slaps(guild_id, user_id, slaps): '''changed the number of time the user has been slapped''' - with open(os.path.join(SLAPPING_FOLER, f"{guild_id}.json"), "r") as file: + with open(os.path.join(SLAPPING_FOLDER, f"{guild_id}.json"), "r") as file: fl = json.load(file) try: - fl[f"user_id"] = slaps + fl[f"{user_id}"] = slaps - with open(os.path.join(SLAPPING_FOLER, f"{guild_id}.json"), "w") as file: - file.write(fl) + with open(os.path.join(SLAPPING_FOLDER, f"{guild_id}.json"), "w") as file: + json.dump(fl, file) return True except Exception as e: From 619d3a5ae3799d75e658f5aeda4bbc5272a57d17 Mon Sep 17 00:00:00 2001 From: 0r1g1n <46009999+NotaSmartDev@users.noreply.github.com> Date: Wed, 19 Jun 2019 17:35:52 +0200 Subject: [PATCH 42/52] Added image --- README.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/README.md b/README.md index 098aaca..ef63ebc 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,7 @@ +

+ +

+ # Forebot This program is a Discord bot which is basically a mod to the real time messaging platform. It is built using discord.py API which offers full compatibility to the official Discord API. From 5d59919d5471a159405f73bfe2971f0f0a2b52bf Mon Sep 17 00:00:00 2001 From: NotaSmartDev Date: Fri, 21 Jun 2019 20:31:28 +0200 Subject: [PATCH 43/52] added Todo configuration entry --- Config.py | 116 ++++++++++++++++++++++++++++++++++++++++++++++++--- Todo.py | 92 +++++++++++++--------------------------- settings.py | 16 +++++-- utilities.py | 22 +++++++++- 4 files changed, 175 insertions(+), 71 deletions(-) diff --git a/Config.py b/Config.py index 2ff0630..40f59d6 100644 --- a/Config.py +++ b/Config.py @@ -44,7 +44,7 @@ def __init__(self, bot): @commands.group() @is_server_owner() async def cfg(self, ctx): - self.ad_msg = "I ({}) have recently been added to this server ! I hope I'll be useful for you. Hopefully you won't find me too many bugs. However if you do I would apreicate 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 maybe, see you there {}".format(ctx.me.mention, DEV_SRV_URL, EMOJIS["wave"]) + self.ad_msg = "I ({}) have recently been added to this server ! I hope I'll be useful to you. I hope 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 maybe, see you there {}".format(ctx.me.mention, DEV_SRV_URL, EMOJIS["wave"]) if ctx.invoked_subcommand == None: await ctx.send(ERR_NO_SUBCOMMAND) @@ -248,10 +248,6 @@ async def cfg_role(self, ctx): raise e - async def cfg_todo(self, ctx): - pass - - async def cfg_welcome(self, ctx): try: await self.config_channels[ctx.guild.id].send("**Starting welcome message configuration**") @@ -378,6 +374,116 @@ async def allow_ad(self, ctx): async def leave(self, ctx): ctx.send("You are about to remove the bot from the server. This will erase all of your configuration from the mainframe and you won't be able to recover the bot without getting another invite. Are you sure you want to continue ? (y/N)") + @cfg.command() + async def cfg_todo(self, ctx): + await self.config_channels[ctx.guild.id].send("You are entering the Todo extension configuration. This extension lets you manage a todo list directly from discord. This extension requires more configuration than most. It has thus been subdivided for you. I'm going to ask which parts you want me to configure with you...") + + #asking for group configuration + await self.config_channels[ctx.guild.id].send("Do you want to configure the todo groups ?") + reponse = await self.bot.wait_for("message", check=self.is_yn_answer) + if response.content[0].lower() == "y": self.cfg_todo_grps(ctx) + + #asking for channel configuration + await self.config_channels[ctx.guild.id].send("Do you want to configure the todo channels ?") + reponse = await self.bot.wait_for("message", check=self.is_yn_answer) + if response.content[0].lower() == "y": self.cfg_todo_chan(ctx) + + #asking for type configuration + #await self.config_channels[ctx.guild.id].send("Do you want to configure the todo types ?") + #reponse = await self.bot.wait_for("message", check=self.is_yn_answer) + # if response.content[0].lower() == "y": self.cfg_todo_type(ctx) + + async def cfg_todo_grps(self, ctx): + try: + retry = True + groups = [] + while retry: + await self.config_channels[ctx.guild.id].send("What is the name of the group you want to make ?") + response = await self.bot.wait_for("message", check=self.is_answer) + + await self.config_channels[ctx.guild.id].send("About to create group {}. Do you want to confirm this?") + response = await self.bot.wait_for("message", check=self.is_yn_answer) + if response.content[0].lower() == "n": + continue + + #if the creation was successfull + await self.config_channels[ctx.guild.id].send("Do you want to create another group ? [y/n]") + response = await self.bot.wait_for("message", check=self.is_yn_answer) + if response.content[0].lower() == "n": + retry = False + continue + + #building group string and making the group + grps_str = "" + todo_dict = get_todo(ctx.guild.id) + for grp in groups: + #building the string + grps_str+=f"`{grp}`, " + + #making the group + todo_dict["groups"][grp] = [] + + update_todo(ctx.guild.id, todo_dict) + + await self.config_channels[ctx.guild.id].send(f"You created the following groups: {grps_str}") + + except Exception as e: + local_logger.exception(e) + raise e + + + + async def cfg_todo_chan(self, ctx): + try: + await self.config_channels[ctx.guild.id].send("Each group you've set earlier can be attached to several channels. Each time a new entry is made for a group, the todo will be posted in every channel bound to it.") + todo_dict = get_todo(ctx.guild.id) + for group in todo_dict["groups"]: + retry = True + while retry: + await self.config_channels[ctx.guild.id].send(f"List (like this {self.config_channels[ctx.guild.id].mention})all the channels you want to bind to the {group} group.") + respone = await self.bot.wait_for("message", check=self.is_answer) + + #making group's channels + chans = [] + chans_str = "" + for chan in response.channel_mentions: + chans_str+= f"{chan} " + chans.append(chan) + await self.config_channels[ctx.guild.id].send(f"You are about to make {chans_str} {group} todo channels. Do you want to confirm? [y/n]") + response = await self.bot.wait_for("message", check=self.is_yn_answer) + if response.content[0].lower() == "n": + await self.config_channels[ctx.guild.id].send(f"Do you want to try to configure {group} again ?") + response = await self.bot.wait_for("message", check=self.is_yn_answer) + if response.content[0].lower() == "n":retry = False + continue + todo_dict["groups"][group] = chans + + await self.config_channels[ctx.guild.id].send("You are now done completing the configuration of the todo channels.") + + + + + + + + except Exception as e: + local_logger.exception(e) + raise e + + async def cfg_todo_type(self, ctx): + try: + retry = True + while retry: + await self.config_channels[ctx.guild.id].send("You are starting the todo types configuration. Which types do you want to add ? Write it like this: `my_type_name;ffffff` The part after the ") + + + + except Exception as e: + local_logger.exception(e) + raise e + + + diff --git a/Todo.py b/Todo.py index a623ff2..7b8d45f 100644 --- a/Todo.py +++ b/Todo.py @@ -1,5 +1,6 @@ import logging import discord +from typing import Union from settings import * from utilities import * @@ -69,67 +70,41 @@ async def todo(self, ctx): await ctx.send('Error: See for ``' + PREFIX + 'help todo``') @todo.command() - async def add(self, ctx, *args): + async def add(self, ctx, todo_type, assignee: Union[bool, discord.Member], repost:Union[bool, discord.TextChannel], *args): '''Command to add a todo. Usage : ;;;''' - with open(TODO_CHANNEL_FILE , "r") as file: - channel_id = file.readline() - if channel_id == None or channel_id == "": - await ctx.channel.send("The todos' channel must be selected with the command " + PREFIX + "todo channel") - return - channel = self.bot.get_channel(int(channel_id)) - - command = "" - for arg in args: - command += " {}".format(arg) - command = command.split(";") - todo_type = None - - with open(TODO_TYPES_FILE, "r") as file: - lines = file.readlines() - for line in lines: - line = line.split(';') - if command[1] == line[0]: - todo_type = line - break - - if todo_type != None: - embed_color = int(todo_type[1], 16) - else: - embed_color = 0x28a745 + todo_dict = get_todo(ctx.guild.id) - new_embed = discord.Embed(description=command[0], url="", color=embed_color) - - command[2] = command[2].replace(' ', '') #TODO: Use dfind instead ? - if command[2] != "false": - if command[2].startswith("<@"): - user = ctx.guild.get_member(int(command[2][2:-1])) - else: - user = ctx.guild.get_member_named(command[2]) - - if user != None: - new_embed.add_field(name="Asssigned to", value=user.mention, inline=True) - - if command[3] != "false": - if command[3].startswith("<#"): - repost_channel = ctx.guild.get_channel(int(command[3][2:-1])) - else: - for chan in ctx.guild.channels: - if(chan.name == command[3]): - repost_channel = chan + #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: - repost_channel = None - - new_embed.set_footer(text=command[1]) - if repost_channel != None: - public_todo = await repost_channel.send(embed=new_embed) - new_embed.add_field(name="Public repost", value=repost_channel.mention + " : " + str(public_todo.id), inline=True) - - message = await channel.send(embed=new_embed) + 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']) - await ctx.message.delete() + await message.add_reaction(EMOJIS['hourglass']) + @todo.command() async def addtype(self, ctx, todo_type, hex_color): @@ -187,13 +162,6 @@ async def listtypes(self, ctx): new_embed = discord.Embed(title="**Type** - *Color*", description=text, color=0x28a745) await ctx.send(embed=new_embed) - @todo.command() - async def channel(self, ctx): - '''Command to select the channel where the todos will be''' - with open(TODO_CHANNEL_FILE , "w") as file: - file.write(str(ctx.channel.id)) - await ctx.channel.send('Okay ! This channel wil be used for the todos !') - def setup(bot): bot.add_cog(Todo(bot)) \ No newline at end of file diff --git a/settings.py b/settings.py index 9502094..9b1765c 100644 --- a/settings.py +++ b/settings.py @@ -47,10 +47,12 @@ # # ######################################### -#files +#Files ENABLED_EXTENSIONS_FILE = "enabled_exts.json" SLAPPING_FOLDER = "slapping" CONFIG_FOLDER = "servers" +TODO_FOLDER = "todo" + #roles ROLES_LEVEL = ["manager", "admin"] @@ -83,6 +85,16 @@ }''' +DEFAULT_TODO_FILE = { + "groups": { + "level1": [] + }, + "types": { + "bug": "ffffff" + }, + +} + ######################################### # # # # @@ -91,8 +103,6 @@ # # ######################################### - - LOG_FILE = "forebot.log" LOGGING_HANDLER = logging.FileHandler(LOG_FILE, "a") LOGGING_FORMATTER = logging.Formatter("[%(asctime)s]:%(name)s:%(message)s") diff --git a/utilities.py b/utilities.py index b7314f4..ce210a6 100644 --- a/utilities.py +++ b/utilities.py @@ -96,7 +96,6 @@ def get_conf(guild_id): '''returns the configuration dict of the provided guild_id''' with open(os.path.join(CONFIG_FOLDER,f"{guild_id}.json"), "r") as file: conf = json.load(file) - print(conf) return conf def update_conf(guild_id, conf_dict): @@ -181,3 +180,24 @@ def update_slaps(guild_id, user_id, slaps): local_logger.exception(e) return False +def get_todo(guild_id): + '''returns the todo dict of the specifeid guild_id''' + try: + with open(os.path.join(TODO_FOLDER, f"{guild_id}.json"), "r") as file: + fl = json.load(file) + return conf + except Exception as e: + raise e + local_logger.exception(e) + +def update_todo(guild_id, todo_dict): + '''updates the todo file for the specified guild_id''' + try: + with open(os.path.join(TODO_FOLDER, f"{guild_id}.json"), "w") as file: + json.dump(todo_dict, file) + return True + + except Exception as e: + raise e + local_logger.exception(e) + return False \ No newline at end of file From d0648c7e6f3d6af98f68bd4194eca91e67a96773 Mon Sep 17 00:00:00 2001 From: NotaSmartDev Date: Tue, 25 Jun 2019 20:09:15 +0200 Subject: [PATCH 44/52] updated & improved documentation --- README.md | 72 ++++++++++++++++++++++++++++++++++++++++++------------- 1 file changed, 55 insertions(+), 17 deletions(-) diff --git a/README.md b/README.md index ef63ebc..ed6dbf3 100644 --- a/README.md +++ b/README.md @@ -8,29 +8,53 @@ This program is a Discord bot which is basically a mod to the real time messagin This bot is a collection of several commands and suite of commands. They are regrouped under several extensions which can be loaded, updated and disabled on the fly, without ever having to take the bot off line. This allows one to stay use only the functions he wishes and keep them updated without any penalties. -### Development history +## Development history I ([@NotaSmartDev](https://github.com/NotaSmartDev)) started building this bot at the end of April 2019 using discord.py API. This bot was first made with the intent to make my discord server more powerful and alive. I had only created it a few days ago but I had realized that I would need additional tools to be able to fulfill all of the plans I had for this server. I had already started making a [bot](https://github.com/organic-bots/LazyFactorian) which serves as an interface to factorio's resources. I thus started building a bot that would enable me to easily manage a scalable server which would contain all of my future bots and would serve as a platform for all my creations. After the very first version I got some help of a friend of mine. Together we made the bot evolve so that it could join the ranks of other servers. Indeed I had started to realize that the bot, however simple, may be useful to others. -### Commands +## Commands + +### Getting started Here is an exhaustive list of all extensions and the commands they provide. This list is kept up to date with the latest updates. -#### Defaults `ext` (runner) +Those commands are sometimes regrouped under a **group**. This means that a command belonging to a **group** will only be recognized if the **group**'s name is appended before the command. For example the command `ls` of group `ext` needs to be called like this: `ext ls`. + +To prevent abuse a **clearance** system is included with the bot. This allows servers to limit the use of certain commands to select group of members. One member can possess multiple roles (ie: levels of clearance). The implemented level of clearances are listed & described in the following table in order of magnitude: + +| Clearance | Description | +| ------------- | ------------------------------------------------------------ | +| * | this represents the wildcard and mean everyone can use the command. No matter their roles | +| runner | this role is assigned to only one member: the one with the `RUNNER_ID`. This is defined in the `settings.py` file and should represent the ID of the user responsible for the bot. It is also the only cross-server role. | +| owner | this role is automatically assigned to every server owner. It is however server-specific. It gives this member supremacy over all members in his/her server. | +| administrator | this role gives access to all server commands except the bot configuration ones | +| manager | this role gives access to message management, warnings issues and other server moderation commands | -A suite of commands always activated which handle +### Reference +#### 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. +| Group | Command | Arguments | Description | Clearance | +| ----- | :------: | :-----------: | :----------------------------------------------------------: | --------- | +| `ext` | `add` | `` | loads the specified `` bot extension. If the command fails the bot will continue to run without the extension. | runner | +| `ext` | `rm` | `` | removes the specified `` bot extension. If the command fails the bot will continue to run with the extension. | runner | +| `ext` | `reload` | `` | reloads the specified `` bot extension. If the command fails the extension will stay unloaded | runner | +| `ext` | `ls` | | lists all *active* extensions. Enabled extensions which are not running anymore (ie: if they crashed unexpectedly) are not listed. | runner | -#### Poll `poll` + + +#### 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. -- `rm` ``: if the user is the author of the poll with the `` message, the bot deletes the specified poll. +| Group | Command | Arguments | Description | Clearance | +| ------ | :-----: | :--------: | :----------------------------------------------------------: | --------- | +| `poll` | `rm` | `` | if the user is the author of the poll with the `` message, the bot deletes the specified poll. | * | @@ -39,6 +63,9 @@ This suite of commands provides automatic poll creation. A poll is an embed mess This extension allow nay user to send a message as an embed. The color of the embed is defined by the user's role color. - `embed` ``: Deletes the message sent and transforms into an embed. The message is `` parameter which takes unlimited arguments. +| Group | Command | Arguments | Description | Clearance | +| ----- | :-----: | :-------: | :----------------------------------------------------------: | --------- | +| | `embed` | * | converts all arguments which form the user's message into a new embed one. Markdown is supported, including named links | * | @@ -46,34 +73,45 @@ This extension allow nay user to send a message as an embed. The color of the em This extension contains some of the most basic managing commands and should almost always be enabled. -- `ping`: replies with the rounded latency of the message transfer -- `shutdown`: shuts down the bot. Restricted to the user with `RUNNER_ID`. (**runner**) -- `clear` `` : deletes the specified `` number of messages in the current channel. chronogically. (**manager**) + +| Group | Command | Arguments | Description | Clearance | +| ----- | :--------: | :-------: | :----------------------------------------------------------: | --------- | +| | `ping` | | replies with the rounded latency of the message transfer | * | +| | `shutdown` | | shuts down the bot properly | runner | +| | `clear` | `` | deletes the specified `` number of messages in the current channel; chronologically | manager | -#### Slapping (manager) +#### Slapping -Allows administrators to give quick and light warnings to disrespectful members. By slapping a member he gets notified of his misbehavior and knows who did it. Both the administrator and the user can see his/her slap count. The slap count is also cross-server. +Allows moderators to give quick and light warnings to disrespectful members. By slapping a member he gets notified of his misbehavior and knows who did it. Both the administrator and the user can see his/her slap count. The slap count is also cross-server. -- `slap` ``: slaps the specified `` member one time. -- `pardon` `` `` : slaps the specified `` member `` number of time(s). If `` is unspecified, pardons the member of all his slaps. Member can be a mention, a user id or just the string of the name of the member. +| Group | Command | Arguments | Description | Clearance | +| ----- | :------: | :----------------: | :----------------------------------------------------------: | --------- | +| | `slap` | `` | slaps the specified `` member one time | manager | +| | `pardon` | `` `` | slaps the specified `` member `` number of time(s). If `` is unspecified, pardons the member of all his slaps. Member can be a mention, a user id or just the string of the name of the member. | manager | -#### Role `role` (manager) +#### Role Allows moderators to add and remove roles to members. -- `add` `` ``: adds the specified `` roles from the `` member (roles mustn't be empty). Member can be a mention, a user id or just the string of the name of the member. -- `rm` `` ``: removes the specified `` roles from the `` member (roles mustn't be empty). Member can be a mention, a user id or just the string of the name of the member. +| Group | Command | Arguments | Description | Clearance | +| ------ | :-----: | :------------------: | :----------------------------------------------------------: | --------- | +| `role` | `add` | `` `` | adds the specified `` roles from the `` member (roles mustn't be empty). Member can be a mention, a user id or just the string of the name of the member | manager | +| `role` | `rm` | `` `` | removes the specified `` roles from the `` member (roles mustn't be empty). Member can be a mention, a user id or just the string of the name of the member | manager | -#### Config `cfg` (owner) +#### Config Allows the owner of a server to configure the behavior of the bot. - `init`: starts full configuration of the bot - `chg` ``: starts the configuration of the `` extension. +| Group | Command | Arguments | Description | Clearance | +| ----- | :-----: | :-----------: | :----------------------------------------------------------: | --------- | +| `cfg` | `init` | | starts full configuration of the bot in a new, restricted, channel | owner | +| `cfg` | `chg` | `` | starts the configuration of the `` extension. This is done in a new, restricted, channel | owner | \ No newline at end of file From 7935ed28d2efd04544b7ceeff6487faa639eb1b6 Mon Sep 17 00:00:00 2001 From: NotaSmartDev Date: Tue, 25 Jun 2019 20:14:49 +0200 Subject: [PATCH 45/52] attempting to fix github README display --- README.md | 3 --- 1 file changed, 3 deletions(-) diff --git a/README.md b/README.md index ed6dbf3..0f3ecc0 100644 --- a/README.md +++ b/README.md @@ -108,9 +108,6 @@ Allows moderators to add and remove roles to members. Allows the owner of a server to configure the behavior of the bot. -- `init`: starts full configuration of the bot -- `chg` ``: starts the configuration of the `` extension. - | Group | Command | Arguments | Description | Clearance | | ----- | :-----: | :-----------: | :----------------------------------------------------------: | --------- | | `cfg` | `init` | | starts full configuration of the bot in a new, restricted, channel | owner | From 574b4319fc4869b11b536c8705a9695c359bd56d Mon Sep 17 00:00:00 2001 From: NotaSmartDev Date: Tue, 25 Jun 2019 20:21:15 +0200 Subject: [PATCH 46/52] another attempt to fix the display --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 0f3ecc0..a4e52a0 100644 --- a/README.md +++ b/README.md @@ -39,6 +39,7 @@ To prevent abuse a **clearance** system is included with the bot. This allows se #### 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. + | Group | Command | Arguments | Description | Clearance | | ----- | :------: | :-----------: | :----------------------------------------------------------: | --------- | | `ext` | `add` | `` | loads the specified `` bot extension. If the command fails the bot will continue to run without the extension. | runner | @@ -62,7 +63,6 @@ This suite of commands provides automatic poll creation. A poll is an embed mess This extension allow nay user to send a message as an embed. The color of the embed is defined by the user's role color. -- `embed` ``: Deletes the message sent and transforms into an embed. The message is `` parameter which takes unlimited arguments. | Group | Command | Arguments | Description | Clearance | | ----- | :-----: | :-------: | :----------------------------------------------------------: | --------- | | | `embed` | * | converts all arguments which form the user's message into a new embed one. Markdown is supported, including named links | * | From e3ed28af546ba69f3f9d5b6303c427b27605a2a1 Mon Sep 17 00:00:00 2001 From: NotaSmartDev Date: Tue, 25 Jun 2019 21:26:37 +0200 Subject: [PATCH 47/52] added Development documentation --- README.md | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index a4e52a0..f113ba1 100644 --- a/README.md +++ b/README.md @@ -111,4 +111,15 @@ Allows the owner of a server to configure the behavior of the bot. | Group | Command | Arguments | Description | Clearance | | ----- | :-----: | :-----------: | :----------------------------------------------------------: | --------- | | `cfg` | `init` | | starts full configuration of the bot in a new, restricted, channel | owner | -| `cfg` | `chg` | `` | starts the configuration of the `` extension. This is done in a new, restricted, channel | owner | \ No newline at end of file +| `cfg` | `chg` | `` | starts the configuration of the `` extension. This is done in a new, restricted, channel | owner | + + + +#### 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. + +| Group | Command | Arguments | Description | Clearance | +| ----- | :------: | :-------: | :----------------------------------------------------------: | --------- | +| | `update` | * | sends an update message to all users who own a server of which the bot is a member. The given arguments will be transformed into the message sent to the server owners. A default message is sent if none is provided. This can be modified in `settings.py`. | owner | +| | `log` | | returns the bot's log file | owner | \ No newline at end of file From 40c3ddcb1917426fba341e8be45719642ee3a4ca Mon Sep 17 00:00:00 2001 From: NotaSmartDev Date: Wed, 26 Jun 2019 01:59:04 +0200 Subject: [PATCH 48/52] updated 's clearance requirements --- README.md | 38 +++++++++++++--------- Role.py | 94 ++++++++++++++++++++++++++----------------------------- 2 files changed, 68 insertions(+), 64 deletions(-) diff --git a/README.md b/README.md index f113ba1..fb194ef 100644 --- a/README.md +++ b/README.md @@ -6,32 +6,40 @@ This program is a Discord bot which is basically a mod to the real time messaging platform. It is built using discord.py API which offers full compatibility to the official Discord API. -This bot is a collection of several commands and suite of commands. They are regrouped under several extensions which can be loaded, updated and disabled on the fly, without ever having to take the bot off line. This allows one to stay use only the functions he wishes and keep them updated without any penalties. +​ This bot is a collection of several commands and suite of commands. They are regrouped under several extensions which can be loaded, updated and disabled on the fly, without ever having to take the bot off line. This allows one to stay use only the functions he wishes and keep them updated without any penalties. ## Development history I ([@NotaSmartDev](https://github.com/NotaSmartDev)) started building this bot at the end of April 2019 using discord.py API. This bot was first made with the intent to make my discord server more powerful and alive. I had only created it a few days ago but I had realized that I would need additional tools to be able to fulfill all of the plans I had for this server. I had already started making a [bot](https://github.com/organic-bots/LazyFactorian) which serves as an interface to factorio's resources. I thus started building a bot that would enable me to easily manage a scalable server which would contain all of my future bots and would serve as a platform for all my creations. -After the very first version I got some help of a friend of mine. Together we made the bot evolve so that it could join the ranks of other servers. Indeed I had started to realize that the bot, however simple, may be useful to others. +​ After the very first version I got some help of a friend of mine. Together we made the bot evolve so that it could join the ranks of other servers. Indeed I had started to realize that the bot, however simple, may be useful to others. ## Commands ### Getting started -Here is an exhaustive list of all extensions and the commands they provide. This list is kept up to date with the latest updates. +Here is an exhaustive list of all extensions and the commands they provide. This list is kept up to date with the latest updates. Some commands can only be ran in a server (ie: you can't have roles in a DM). They are also all case sensitive. -Those commands are sometimes regrouped under a **group**. This means that a command belonging to a **group** will only be recognized if the **group**'s name is appended before the command. For example the command `ls` of group `ext` needs to be called like this: `ext ls`. +​ Those commands are sometimes regrouped under a **group**. This means that a command belonging to a **group** will only be recognized if the **group**'s name is appended before the command. For example the command `ls` of group `ext` needs to be called like this: `ext ls`. -To prevent abuse a **clearance** system is included with the bot. This allows servers to limit the use of certain commands to select group of members. One member can possess multiple roles (ie: levels of clearance). The implemented level of clearances are listed & described in the following table in order of magnitude: +​ To prevent abuse a **clearance** system is included with the bot. This allows servers to limit the use of certain commands to select group of members. One member can possess multiple roles (ie: levels of clearance). The implemented level of clearances are listed & described in the following table in order of magnitude: | Clearance | Description | | ------------- | ------------------------------------------------------------ | -| * | this represents the wildcard and mean everyone can use the command. No matter their roles | -| runner | this role is assigned to only one member: the one with the `RUNNER_ID`. This is defined in the `settings.py` file and should represent the ID of the user responsible for the bot. It is also the only cross-server role. | +| * | this represents the wildcard and means everyone can use the command. No matter their roles | +| runner | this role is assigned to only one member: the one with the [`RUNNER_ID`](https://github.com/organic-bots/ForeBot/blob/e3ed28af546ba69f3f9d5b6303c427b27605a2a1/settings.py#L15). This is defined in the `settings.py` file and should represent the ID of the user responsible for the bot. It is also the only cross-server role. | | owner | this role is automatically assigned to every server owner. It is however server-specific. It gives this member supremacy over all members in his/her server. | | administrator | this role gives access to all server commands except the bot configuration ones | | manager | this role gives access to message management, warnings issues and other server moderation commands | +​ Arguments (aka: parameters) are referenced in `<` & `>` in this reference although using those symbols isn't necessary when using the commands. Some arguments are optional. If it's the case they are preceded by a `*`. Otherwise the command's list of required arguments is to be like this: `` ``. This can also be blank when the command doesn't require any argument. + + Sometimes commands require a `*` argument. This means that the argument length is unlimited. It can range from 0 to 2000 characters (the maximum allowed by discord). + +Finally arguments which are not `*` but comprises spaces need to be put in quotes like this: `"this is one argument only"`. Else each "word" will be considered a different argument. If the argument count doesn't exactly match then the command will fail. Also the arguments order matters. + + + ### Reference @@ -86,10 +94,10 @@ This extension contains some of the most basic managing commands and should almo Allows moderators to give quick and light warnings to disrespectful members. By slapping a member he gets notified of his misbehavior and knows who did it. Both the administrator and the user can see his/her slap count. The slap count is also cross-server. -| Group | Command | Arguments | Description | Clearance | -| ----- | :------: | :----------------: | :----------------------------------------------------------: | --------- | -| | `slap` | `` | slaps the specified `` member one time | manager | -| | `pardon` | `` `` | slaps the specified `` member `` number of time(s). If `` is unspecified, pardons the member of all his slaps. Member can be a mention, a user id or just the string of the name of the member. | manager | +| Group | Command | Arguments | Description | Clearance | +| ----- | :------: | :-----------------: | :----------------------------------------------------------: | :-------: | +| | `slap` | `` | slaps the specified `` member one time | manager | +| | `pardon` | `` *`` | slaps the specified `` member `` number of time(s). If `` is unspecified, pardons the member of all his slaps. Member can be a mention, a user id or just the string of the name of the member. | manager | @@ -97,10 +105,10 @@ Allows moderators to give quick and light warnings to disrespectful members. By Allows moderators to add and remove roles to members. -| Group | Command | Arguments | Description | Clearance | -| ------ | :-----: | :------------------: | :----------------------------------------------------------: | --------- | -| `role` | `add` | `` `` | adds the specified `` roles from the `` member (roles mustn't be empty). Member can be a mention, a user id or just the string of the name of the member | manager | -| `role` | `rm` | `` `` | removes the specified `` roles from the `` member (roles mustn't be empty). Member can be a mention, a user id or just the string of the name of the member | manager | +| Group | Command | Arguments | Description | Clearance | +| ------ | :-----: | :------------------: | :----------------------------------------------------------: | :-----------: | +| `role` | `add` | `` `` | adds the specified `` roles from the `` member (roles mustn't be empty). Member can be a mention, a user id or just the string of the name of the member | administrator | +| `role` | `rm` | `` `` | removes the specified `` roles from the `` member (roles mustn't be empty). Member can be a mention, a user id or just the string of the name of the member | administrator | diff --git a/Role.py b/Role.py index 9b106bc..d6cc864 100644 --- a/Role.py +++ b/Role.py @@ -4,11 +4,11 @@ from utilities import * ######################################### -# # -# # -# Setting up logging # -# # -# # +# # +# # +# Setting up logging # +# # +# # ######################################### local_logger = logging.getLogger(__name__) local_logger.setLevel(LOGGING_LEVEL) @@ -17,60 +17,56 @@ ######################################### -# # -# # -# Making commands # -# # -# # +# # +# # +# Making commands # +# # +# # ######################################### class Role(commands.Cog): - """role management utility. Requires a Gestion role""" - def __init__(self, bot): - self.bot = bot + """role management utility. Requires a Gestion role""" + def __init__(self, bot): + self.bot = bot - @commands.group() - @has_auth("admin") - async def role(self, ctx): - '''role management utility. Requires a Gestion role''' - if ctx.invoked_subcommand is None: - local_logger.warning("User didn't provide any subcommand") - await ctx.send("NotEnoughArguments:\tYou must provide a subcommand") + @commands.group() + @has_auth("admin") + async def role(self, ctx): + '''role management utility. Requires a Gestion role''' + if ctx.invoked_subcommand is None: + local_logger.warning("User didn't provide any subcommand") + await ctx.send("NotEnoughArguments:\tYou must provide a subcommand") - if not has_auth(ctx.guild.id, ctx.author.roles, "manager"): - return False + @role.command() + async def add(self, ctx, member: discord.Member, *roles:discord.Role): + '''Gives listed roles''' + if len(roles)==0: + local_logger.warning("User didn't provide a role") + await ctx.send("NotEnoughArguments:\tYou must provide at least one `role`") + else: + try: + await member.add_roles(*roles) + except Exception as e: + local_logger.exception("Couldn't add {} to {}".format(roles, member)) + await ctx.send("An unexpected error occured !\nTraceback:```python\n{}```".format(e)) - @role.command() - async def add(self, ctx, member: discord.Member, *roles:discord.Role): - '''Gives listed roles''' - if len(roles)==0: - local_logger.warning("User didn't provide a role") - await ctx.send("NotEnoughArguments:\tYou must provide at least one `role`") + @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") + await ctx.send("NotEnoughArguments:\tYou must provide at least one `role`") - else: - try: - await member.add_roles(*roles) - except Exception as e: - local_logger.exception("Couldn't add {} to {}".format(roles, member)) - await ctx.send("An unexpected error occured !\nTraceback:```python\n{}```".format(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") - await ctx.send("NotEnoughArguments:\tYou must provide at least one `role`") - - else: - try: - await member.remove_roles(*roles) - except Exception as e: - local_logger.exception("Couldn't remove roles ") - await ctx.send("An unexpected error occured !\nTraceback:```python\n{}```".format(e)) + else: + try: + await member.remove_roles(*roles) + except Exception as e: + local_logger.exception("Couldn't remove roles ") + await ctx.send("An unexpected error occured !\nTraceback:```python\n{}```".format(e)) def setup(bot): - bot.add_cog(Role(bot)) \ No newline at end of file + bot.add_cog(Role(bot)) \ No newline at end of file From 6c3cdcfe3307f9376858e765e6622042d4458ad8 Mon Sep 17 00:00:00 2001 From: NotaSmartDev Date: Wed, 26 Jun 2019 15:09:20 +0200 Subject: [PATCH 49/52] improved Todo --- Config.py | 766 +++++++++++++++++++++++++------------------------ Poll.py | 372 ++++++++++++------------ Todo.py | 79 ++--- executioner.py | 172 +++++------ settings.py | 95 +++--- utilities.py | 275 +++++++++--------- 6 files changed, 899 insertions(+), 860 deletions(-) diff --git a/Config.py b/Config.py index 40f59d6..75d001c 100644 --- a/Config.py +++ b/Config.py @@ -6,11 +6,11 @@ from utilities import * ######################################### -# # -# # -# Setting up logging # -# # -# # +# # +# # +# Setting up logging # +# # +# # ######################################### local_logger = logging.getLogger(__name__) local_logger.setLevel(LOGGING_LEVEL) @@ -19,473 +19,499 @@ ######################################### -# # -# # -# Making commands # -# # -# # +# # +# # +# Making commands # +# # +# # ######################################### class Config(commands.Cog): - """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.bot = bot - #change to make it cross-server - self.config_channels={} - #other values can't be added as of now - self.allowed_answers = {1:["yes", "y"], - 0:["no", "n"]} - - - - @commands.group() - @is_server_owner() - async def cfg(self, ctx): - self.ad_msg = "I ({}) have recently been added to this server ! I hope I'll be useful to you. I hope 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 maybe, see you there {}".format(ctx.me.mention, DEV_SRV_URL, EMOJIS["wave"]) - if ctx.invoked_subcommand == None: - await ctx.send(ERR_NO_SUBCOMMAND) - - - @cfg.command() - async def init(self, ctx): - #creating new hidden channel only the owner can see - overwrites = { - ctx.guild.default_role: discord.PermissionOverwrite(read_messages=False), - ctx.guild.owner : discord.PermissionOverwrite(read_messages=True) - } - self.config_channels[ctx.guild.id] = await ctx.guild.create_text_channel("cli-bot-config") - - #making conf file if it doesn't exist - if not was_init(ctx): - #making config file - with open(os.path.join(CONFIG_FOLDER, f"{ctx.guild.id}.json"), "w") as file: - file.write(DEFAULT_SERVER_FILE) - #making slapping file - with open(os.path.join(SLAPPING_FOLDER, f"{ctx.guild.id}.json"), "w") as file: - file.write(DEFAULT_SLAPPED_FILE) - - #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\n*Starting full configuration...*''') - await self.config_channels[ctx.guild.id].send("This will overwrite all of your existing configurations. Do you want to continue ? [y/n]") - response = await self.bot.wait_for("message", check=self.is_yn_answer) - if response.content[0].lower() == "n":return False - await self.config_channels[ctx.guild.id].send("**Starting full bot configuration...**") - - try: - await self.cfg_poll(ctx) - await self.config_channels[ctx.guild.id].send("Role setup is **mendatory** for the bot to work correctly. Otherise no one will be able to use administration commands.") - await self.cfg_role(ctx) - await self.cfg_welcome(ctx) - await self.cfg_goodbye(ctx) - await self.cfg_todo(ctx) - - #asking for permisison to advertise - await self.config_channels[ctx.guild.id].send("You're almost done ! Just one more thing...") - await self.allow_ad(ctx) - - - local_logger.info(f"Setup for server {ctx.guild.name}({ctx.guild.id}) is done") - - except Exception as e: - await ctx.send(ERR_UNEXCPECTED.format(None)) - await ctx.send("Dropping configuration and rolling back unconfirmed changes.") - #await self.config_channels[ctx.guild.id].delete(reason="Failed to interactively configure the bot") - 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") - - - - @cfg.command() - @is_init() - async def chg(self, ctx, setting): - try: - eval("self.cfg_"+setting) - - except Exception as e: - local_logger.exception(e) - - - def is_yn_answer(self, ctx): - if (ctx.channel == self.config_channels[ctx.guild.id]) and ((ctx.content.lower() in self.allowed_answers[0]) or (ctx.content.lower() in self.allowed_answers[1])): return True - return False - - def is_answer(self, ctx): - if ctx.channel == self.config_channels[ctx.guild.id]: return True - return False - - - async def cfg_poll(self, ctx): - try: - await self.config_channels[ctx.guild.id].send("**Starting poll configuration**") - await self.config_channels[ctx.guild.id].send("Do you want to activate polls on this server ? [y/n]") - #awaiting the user response - response = await self.bot.wait_for("message", check=self.is_yn_answer) - if not response.content[0].lower() == "y": return False - - retry = True - while retry: - #getting the list of channels to be marked polls - await self.config_channels[ctx.guild.id].send(f"List all the channels you want to use as poll channels. You must mention those channels like this: {self.config_channels[ctx.guild.id].mention}") - response = await self.bot.wait_for("message", check=self.is_answer) - poll_channels = response.channel_mentions - if self.config_channels[ctx.guild.id] in poll_channels: - await self.config_channels[ctx.guild.id].send("You cannot set this channel as a poll one for safety reasons. Please try again...") - continue + """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.bot = bot + #change to make it cross-server + self.config_channels={} + #other values can't be added as of now + self.allowed_answers = {1:["yes", "y"], + 0:["no", "n"]} + + + + @commands.group() + @is_server_owner() + async def cfg(self, ctx): + self.ad_msg = "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 ctx.invoked_subcommand == None: + await ctx.send(ERR_NO_SUBCOMMAND) + + + async def make_cfg_chan(self, ctx): + overwrite = { + ctx.guild.default_role: discord.PermissionOverwrite(read_messages=False), + ctx.guild.owner : discord.PermissionOverwrite(read_messages=True) + } + self.config_channels[ctx.guild.id] = await ctx.guild.create_text_channel("cli-bot-config", overwrites=overwrite) + return self.config_channels[ctx.guild.id] + + + @cfg.command() + async def init(self, ctx): + #creating new hidden channel only the owner can see + await self.make_cfg_chan(ctx) + + #making conf file if it doesn't exist + if not was_init(ctx): + #making config file + with open(os.path.join(CONFIG_FOLDER, f"{ctx.guild.id}.json"), "w") as file: + file.write(DEFAULT_SERVER_FILE) + #making slapping file + with open(os.path.join(SLAPPING_FOLDER, f"{ctx.guild.id}.json"), "w") as file: + file.write(DEFAULT_SLAPPED_FILE) + #making todo file + with open(os.path.join(TODO_FOLDER, f"{ctx.guild.id}.json"), "w") as file: + file.write(DEFAULT_TODO_FILE) + + #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("This will overwrite all of your existing configurations. Do you want to continue ? [y/n]") + response = await self.bot.wait_for("message", check=self.is_yn_answer) + if response.content[0].lower() == "n":return False + await self.config_channels[ctx.guild.id].send("**Starting full bot configuration...**") + + try: + await self.cfg_poll(ctx) + await self.config_channels[ctx.guild.id].send("Role setup is **mendatory** for the bot to work correctly. Otherise no one will be able to use administration commands.") + await self.cfg_role(ctx) + await self.cfg_welcome(ctx) + await self.cfg_goodbye(ctx) + await self.cfg_todo(ctx) + + #asking for permisison to advertise + await self.config_channels[ctx.guild.id].send("You're almost done ! Just one more thing...") + await self.allow_ad(ctx) + + + local_logger.info(f"Setup for server {ctx.guild.name}({ctx.guild.id}) is done") + + except Exception as e: + await ctx.send(ERR_UNEXCPECTED.format(None)) + await ctx.send("Dropping configuration and rolling back unconfirmed changes.") + #await self.config_channels[ctx.guild.id].delete(reason="Failed to interactively configure the bot") + 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") + + @cfg.command() + @is_init() + async def chg(self, ctx, setting): + '''doesn't work yet''' + try: + print(f"Starting config of extension {setting}") + await self.make_cfg_chan(ctx) + eval("await self.cfg_"+setting) + + except Exception as e: + 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") + + + def is_yn_answer(self, ctx): + if (ctx.channel == self.config_channels[ctx.guild.id]) and ((ctx.content.lower() in self.allowed_answers[0]) or (ctx.content.lower() in self.allowed_answers[1])): return True + return False + + def is_answer(self, ctx): + if ctx.channel == self.config_channels[ctx.guild.id]: return True + return False + + + async def cfg_poll(self, ctx): + try: + await self.config_channels[ctx.guild.id].send("**Starting poll configuration**") + await self.config_channels[ctx.guild.id].send("Do you want to activate polls on this server ? [y/n]") + #awaiting the user response + response = await self.bot.wait_for("message", check=self.is_yn_answer) + if not response.content[0].lower() == "y": return False + + retry = True + while retry: + #getting the list of channels to be marked polls + await self.config_channels[ctx.guild.id].send(f"List all the channels you want to use as poll channels. You must mention those channels like this: {self.config_channels[ctx.guild.id].mention}") + response = await self.bot.wait_for("message", check=self.is_answer) + poll_channels = response.channel_mentions + if self.config_channels[ctx.guild.id] in poll_channels: + await self.config_channels[ctx.guild.id].send("You cannot set this channel as a poll one for safety reasons. Please try again...") + continue + + + #building string with all the channels that will be marked for polls + poll_channels_str = "" + for chan in poll_channels: + poll_channels_str+= " "+chan.mention + + await self.config_channels[ctx.guild.id].send(f"You are about to make {poll_channels_str} poll channels. Do you want to continue? [y/n]") + + response = await self.bot.wait_for("message", check=self.is_yn_answer) + #wether the asnwer was positive + if not response.content[0].lower() =="y": + #making sure the user really wants to cancel poll configuration + await self.config_channels[ctx.guild.id].send("Aborting addition of poll channels. Do you want to leave the poll configuration interface ? [y/n]") + response = await self.bot.wait_for("message", check=self.is_yn_answer) + if response.content[0].lower()=="y": + local_logger.info(f"Poll configuration has been cancelled for server {ctx.guild.name}") + retry = False - - #building string with all the channels that will be marked for polls - poll_channels_str = "" - for chan in poll_channels: - poll_channels_str+= " "+chan.mention + else: retry=False - await self.config_channels[ctx.guild.id].send(f"You are about to make {poll_channels_str} poll channels. Do you want to continue? [y/n]") + poll_channels_ids = [] + for chan in poll_channels: + poll_channels_ids.append(chan.id) - response = await self.bot.wait_for("message", check=self.is_yn_answer) - #wether the asnwer was positive - if not response.content[0].lower() =="y": - #making sure the user really wants to cancel poll configuration - await self.config_channels[ctx.guild.id].send("Aborting addition of poll channels. Do you want to leave the poll configuration interface ? [y/n]") - response = await self.bot.wait_for("message", check=self.is_yn_answer) - if response.content[0].lower()=="y": - local_logger.info(f"Poll configuration has been cancelled for server {ctx.guild.name}") - retry = False - else: retry=False + old_conf = get_conf(ctx.guild.id) + old_conf["poll_channels"] = poll_channels_ids - poll_channels_ids = [] - for chan in poll_channels: - poll_channels_ids.append(chan.id) + if update_conf(ctx.guild.id, old_conf) == False: + await self.config_channels[ctx.guild.id].send(ERR_UNEXCPECTED) + else: + await self.config_channels[ctx.guild.id].send("Poll configuration is done.") - old_conf = get_conf(ctx.guild.id) - old_conf["poll_channels"] = poll_channels_ids - if update_conf(ctx.guild.id, old_conf) == False: - await self.config_channels[ctx.guild.id].send(ERR_UNEXCPECTED) + local_logger.info(f"Configuration of poll for server {ctx.guild.name} ({ctx.guild.id}) has been completed.") - else: - await self.config_channels[ctx.guild.id].send("Poll configuration is done.") + except Exception as e: + local_logger.exception(e) + raise e - local_logger.info(f"Configuration of poll for server {ctx.guild.name} ({ctx.guild.id}) has been completed.") + async def cfg_role(self, ctx): + try: + #introducing the clearance levels the bot uses + await self.config_channels[ctx.guild.id].send("**Starting 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.") - except Exception as e: - local_logger.exception(e) - raise e + 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 + await self.config_channels[ctx.guild.id].send(f"List all the roles you want to be given the **{role_lvl}** level of clearance.") + response = await self.bot.wait_for("message", check=self.is_answer) + roles = response.role_mentions + if len(roles) == 0: + await self.config_channels[ctx.guild.id].send(f"You need to set at least one role for the {role_lvl} clearance.") + continue + #building roll string + roles_str = "" + for role in roles: + roles_str+= f" {role.mention}" - async def cfg_role(self, ctx): - try: - #introducing the clearance levels the bot uses - await self.config_channels[ctx.guild.id].send("**Starting 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.") + #asking for confirmation + await self.config_channels[ctx.guild.id].send(f"You are about to give{roles_str} roles the **{role_lvl}** level of clearance. Do you confirm this ? [y/n]") + response = await self.bot.wait_for("message", check=self.is_yn_answer) + if response.content[0].lower() == "n": + await self.config_channels[ctx.guild.id].send(f"Aborting configuration of {role_lvl}. Do you want to retry? [y/n]") + response = await self.bot.wait_for("message", check=self.is_yn_answer) + if response.content[0].lower() == "n": + local_logger.info(f"The configuration for the {role_lvl} clearance has been cancelled for server {ctx.guild.name}") + retry = False - 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 - await self.config_channels[ctx.guild.id].send(f"List all the roles you want to be given the **{role_lvl}** level of clearance.") - response = await self.bot.wait_for("message", check=self.is_answer) - roles = response.role_mentions - if len(roles) == 0: - await self.config_channels[ctx.guild.id].send(f"You need to set at least one role for the {role_lvl} clearance.") - continue + else: retry = False - #building roll string - roles_str = "" - for role in roles: - roles_str+= f" {role.mention}" - #asking for confirmation - await self.config_channels[ctx.guild.id].send(f"You are about to give{roles_str} roles the **{role_lvl}** level of clearance. Do you confirm this ? [y/n]") - response = await self.bot.wait_for("message", check=self.is_yn_answer) - if response.content[0].lower() == "n": - await self.config_channels[ctx.guild.id].send(f"Aborting configuration of {role_lvl}. Do you want to retry? [y/n]") - response = await self.bot.wait_for("message", check=self.is_yn_answer) - if response.content[0].lower() == "n": - local_logger.info(f"The configuration for the {role_lvl} clearance has been cancelled for server {ctx.guild.name}") - retry = False + local_logger.info(f"Server {ctx.guild.name} configured its {role_lvl} roles") - else: retry = False + for role in roles: + new_role.append(role.id) + #adding to master role list + new_roles.append(new_role) - local_logger.info(f"Server {ctx.guild.name} configured its {role_lvl} roles") - for role in roles: - new_role.append(role.id) + #giving admin roles the manager clearance + for m_role in new_roles[1]: + new_roles[0].append(m_role) - #adding to master role list - new_roles.append(new_role) + old_conf = get_conf(ctx.guild.id) + #updating the values + old_conf["roles"]["manager"] = new_roles[0] + old_conf["roles"]["admin"] = new_roles[1] - #giving admin roles the manager clearance - for m_role in new_roles[1]: - new_roles[0].append(m_role) + if update_conf(ctx.guild.id, old_conf) == False: + await self.config_channels[ctx.guild.id].send(ERR_UNEXCPECTED) - old_conf = get_conf(ctx.guild.id) + else: + await self.config_channels[ctx.guild.id].send("Successfully updated role configuration") - #updating the values - old_conf["roles"]["manager"] = new_roles[0] - old_conf["roles"]["admin"] = new_roles[1] - if update_conf(ctx.guild.id, old_conf) == False: - await self.config_channels[ctx.guild.id].send(ERR_UNEXCPECTED) + except Exception as e: + local_logger.exception(e) + raise e - else: - await self.config_channels[ctx.guild.id].send("Successfully updated role configuration") + async def cfg_welcome(self, ctx): + try: + await self.config_channels[ctx.guild.id].send("**Starting welcome message configuration**") + retry = True - except Exception as e: - local_logger.exception(e) - raise e + await self.config_channels[ctx.guild.id].send("Do you want to have a welcome message sent when a new user joins the server ? [y/n]") + response = await self.bot.wait_for("message", check=self.is_yn_answer) + if response.content[0].lower() == "n": + message = False + retry = False - async def cfg_welcome(self, ctx): - try: - await self.config_channels[ctx.guild.id].send("**Starting welcome message configuration**") - retry = True + while retry: - await self.config_channels[ctx.guild.id].send("Do you want to have a welcome message sent when a new user joins the server ? [y/n]") + await self.config_channels[ctx.guild.id].send("Enter the message you'd like to be sent to the new users. If you want to mention them use `{0}`") - response = await self.bot.wait_for("message", check=self.is_yn_answer) - if response.content[0].lower() == "n": - message = False - retry = False + message = await self.bot.wait_for("message", check=self.is_answer) - while retry: + await self.config_channels[ctx.guild.id].send("To make sure the message is as you'd like I'm sending it to you.\n**-- Beginning of message --**") + await self.config_channels[ctx.guild.id].send(message.content.format(ctx.guild.owner.mention)) - await self.config_channels[ctx.guild.id].send("Enter the message you'd like to be sent to the new users. If you want to mention them use `{0}`") + await self.config_channels[ctx.guild.id].send("**--End of message --**\nIs this the message you want to set as the welcome message ? [y/n]") + response = await self.bot.wait_for("message", check=self.is_yn_answer) - message = await self.bot.wait_for("message", check=self.is_answer) + #the user has made a mistake + if response.content[0].lower() == "n": + await self.config_channels[ctx.guild.id].send("Do you want to retry ? [y/n]") + response = await self.bot.wait_for("message", check=self.is_yn_answer) + if response.content[0].lower == "n": + message = False + retry = False + #otherwise retry + continue - await self.config_channels[ctx.guild.id].send("To make sure the message is as you'd like I'm sending it to you.\n**-- Beginning of message --**") - await self.config_channels[ctx.guild.id].send(message.content.format(ctx.guild.owner.mention)) + else: retry = False - await self.config_channels[ctx.guild.id].send("**--End of message --**\nIs this the message you want to set as the welcome message ? [y/n]") - response = await self.bot.wait_for("message", check=self.is_yn_answer) + if message != False: + old_conf = get_conf(ctx.guild.id) + old_conf["messages"]["welcome"]= message.content - #the user has made a mistake - if response.content[0].lower() == "n": - await self.config_channels[ctx.guild.id].send("Do you want to retry ? [y/n]") - response = await self.bot.wait_for("message", check=self.is_yn_answer) - if response.content[0].lower == "n": - message = False - retry = False - #otherwise retry - continue + if update_conf(ctx.guild.id, old_conf) == False: + await self.config_channels[ctx.guild.id].send(ERR_CANT_SAVE) - else: retry = False - if message != False: - old_conf = get_conf(ctx.guild.id) - old_conf["messages"]["welcome"]= message.content + except Exception as e: + local_logger.exception(e) + raise e - if update_conf(ctx.guild.id, old_conf) == False: - await self.config_channels[ctx.guild.id].send(ERR_CANT_SAVE) + async def cfg_goodbye(self, ctx): + try: + await self.config_channels[ctx.guild.id].send("**Starting goodbye message configuration**") + retry = True - except Exception as e: - local_logger.exception(e) - raise e + await self.config_channels[ctx.guild.id].send("Do you want to have a goodbye message sent when an user leaves the server ? [y/n]") + response = await self.bot.wait_for("message", check=self.is_yn_answer) + if response.content[0].lower() == "n": + message = False + retry = False - async def cfg_goodbye(self, ctx): - try: - await self.config_channels[ctx.guild.id].send("**Starting goodbye message configuration**") - retry = True + while retry: - await self.config_channels[ctx.guild.id].send("Do you want to have a goodbye message sent when an user leaves the server ? [y/n]") + await self.config_channels[ctx.guild.id].send("Enter the message you'd like to be sent. If you want to mention them use `{0}`") - response = await self.bot.wait_for("message", check=self.is_yn_answer) - if response.content[0].lower() == "n": - message = False - retry = False + message = await self.bot.wait_for("message", check=self.is_answer) - while retry: + await self.config_channels[ctx.guild.id].send("To make sure the message is as you'd like I'm sending it to you. Enventual mentions will be directed to you.\n**-- Beginning of message --**") + await self.config_channels[ctx.guild.id].send(message.content.format(ctx.guild.owner.mention)) - await self.config_channels[ctx.guild.id].send("Enter the message you'd like to be sent. If you want to mention them use `{0}`") + await self.config_channels[ctx.guild.id].send("**--End of message --**\nIs this the message you want to set as the goodbye message ? [y/n]") + response = await self.bot.wait_for("message", check=self.is_yn_answer) - message = await self.bot.wait_for("message", check=self.is_answer) + #the user has made a mistake + if response.content[0].lower() == "n": + await self.config_channels[ctx.guild.id].send("Do you want to retry ? [y/n]") + response = await self.bot.wait_for("message", check=self.is_yn_answer) + if response.content[0].lower == "n": + message = False + retry = False + #otherwise retry + continue + else: retry = False - await self.config_channels[ctx.guild.id].send("To make sure the message is as you'd like I'm sending it to you. Enventual mentions will be directed to you.\n**-- Beginning of message --**") - await self.config_channels[ctx.guild.id].send(message.content.format(ctx.guild.owner.mention)) + if message != False: + old_conf = get_conf(ctx.guild.id) + old_conf["messages"]["goodbye"]= message.content - await self.config_channels[ctx.guild.id].send("**--End of message --**\nIs this the message you want to set as the goodbye message ? [y/n]") - response = await self.bot.wait_for("message", check=self.is_yn_answer) + if update_conf(ctx.guild.id, old_conf) == False: + await self.config_channels[ctx.guild.id].send(ERR_CANT_SAVE) - #the user has made a mistake - if response.content[0].lower() == "n": - await self.config_channels[ctx.guild.id].send("Do you want to retry ? [y/n]") - response = await self.bot.wait_for("message", check=self.is_yn_answer) - if response.content[0].lower == "n": - message = False - retry = False - #otherwise retry - continue - else: retry = False - if message != False: - old_conf = get_conf(ctx.guild.id) - old_conf["messages"]["goodbye"]= message.content - if update_conf(ctx.guild.id, old_conf) == False: - await self.config_channels[ctx.guild.id].send(ERR_CANT_SAVE) + except Exception as e: + local_logger.exception(e) + raise e + async def allow_ad(self, ctx): + try: + await self.config_channels[ctx.guild.id].send("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 (you can always change this later). [y/n]") + response = await self.bot.wait_for("message", check=self.is_yn_answer) + if response.content[0].lower()=="n": return False + await self.config_channels[ctx.guild.id].send("Thank you very much ! In which channel do you want me to post this message ?") + response = await self.bot.wait_for("message", check=self.is_answer) - except Exception as e: - local_logger.exception(e) - raise e + old_conf = get_conf(ctx.guild.id) + old_conf["advertisement"] = response.channel_mentions[0].id - async def allow_ad(self, ctx): - try: - await self.config_channels[ctx.guild.id].send("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 (you can always change this later). [y/n]") - response = await self.bot.wait_for("message", check=self.is_yn_answer) - if response.content[0].lower()=="n": return False + chan = discord.utils.find(lambda c: c.id==old_conf["advertisement"], ctx.guild.channels) + await chan.send(self.ad_msg) - await self.config_channels[ctx.guild.id].send("Thank you very much ! In which channel do you want me to post this message ?") - response = await self.bot.wait_for("message", check=self.is_answer) + #updating conf + update_conf(ctx.guild.id, old_conf) - old_conf = get_conf(ctx.guild.id) - old_conf["advertisement"] = response.channel_mentions[0].id - chan = discord.utils.find(lambda c: c.id==old_conf["advertisement"], ctx.guild.channels) - await chan.send(self.ad_msg) - #updating conf - update_conf(ctx.guild.id, old_conf) + except Exception as e: + local_logger.exception(e) + raise e + @cfg.command() + async def leave(self, ctx): + ctx.send("You are about to remove the bot from the server. This will erase all of your configuration from the mainframe and you won't be able to recover the bot without getting another invite. Are you sure you want to continue ? (y/N)") - except Exception as e: - local_logger.exception(e) - raise e + async def cfg_todo(self, ctx): + local_logger.info("Starting todo configuration") + try: + await self.config_channels[ctx.guild.id].send("**Starting Todo configuration...**\n This extension lets you manage a todo list directly from discord. This extension requires more configuration than most. It has thus been subdivided for you. I'm going to ask which parts you want me to configure with you...") - @cfg.command() - async def leave(self, ctx): - ctx.send("You are about to remove the bot from the server. This will erase all of your configuration from the mainframe and you won't be able to recover the bot without getting another invite. Are you sure you want to continue ? (y/N)") + #asking for group configuration + await self.config_channels[ctx.guild.id].send("Do you want to configure the todo groups ? [y/n]") + response = await self.bot.wait_for("message", check=self.is_yn_answer) + if response.content[0].lower() == "y": await self.cfg_todo_grps(ctx) - @cfg.command() - async def cfg_todo(self, ctx): - await self.config_channels[ctx.guild.id].send("You are entering the Todo extension configuration. This extension lets you manage a todo list directly from discord. This extension requires more configuration than most. It has thus been subdivided for you. I'm going to ask which parts you want me to configure with you...") + #asking for channel configuration + await self.config_channels[ctx.guild.id].send("Do you want to configure the todo channels ? [y/n]") + reponse = await self.bot.wait_for("message", check=self.is_yn_answer) + if response.content[0].lower() == "y": await self.cfg_todo_chan(ctx) - #asking for group configuration - await self.config_channels[ctx.guild.id].send("Do you want to configure the todo groups ?") - reponse = await self.bot.wait_for("message", check=self.is_yn_answer) - if response.content[0].lower() == "y": self.cfg_todo_grps(ctx) + #asking for type configuration + #await self.config_channels[ctx.guild.id].send("Do you want to configure the todo types ?") + #reponse = await self.bot.wait_for("message", check=self.is_yn_answer) + #if response.content[0].lower() == "y": self.cfg_todo_type(ctx) - #asking for channel configuration - await self.config_channels[ctx.guild.id].send("Do you want to configure the todo channels ?") - reponse = await self.bot.wait_for("message", check=self.is_yn_answer) - if response.content[0].lower() == "y": self.cfg_todo_chan(ctx) + except Exception as e: + local_logger.exception(e) + raise e - #asking for type configuration - #await self.config_channels[ctx.guild.id].send("Do you want to configure the todo types ?") - #reponse = await self.bot.wait_for("message", check=self.is_yn_answer) - # if response.content[0].lower() == "y": self.cfg_todo_type(ctx) - async def cfg_todo_grps(self, ctx): - try: - retry = True - groups = [] - while retry: - await self.config_channels[ctx.guild.id].send("What is the name of the group you want to make ?") - response = await self.bot.wait_for("message", check=self.is_answer) + async def cfg_todo_grps(self, ctx): + try: + retry = True + groups = [] + while retry: + await self.config_channels[ctx.guild.id].send("What is the name of the group you want to make ?") + response = await self.bot.wait_for("message", check=self.is_answer) - await self.config_channels[ctx.guild.id].send("About to create group {}. Do you want to confirm this?") - response = await self.bot.wait_for("message", check=self.is_yn_answer) - if response.content[0].lower() == "n": - continue + #building group string + todo_dict = get_todo(ctx.guild.id) + grp_str=f"`{response.content}`" + groups.append(response.content) - #if the creation was successfull - await self.config_channels[ctx.guild.id].send("Do you want to create another group ? [y/n]") - response = await self.bot.wait_for("message", check=self.is_yn_answer) - if response.content[0].lower() == "n": - retry = False - continue - #building group string and making the group - grps_str = "" - todo_dict = get_todo(ctx.guild.id) - for grp in groups: - #building the string - grps_str+=f"`{grp}`, " + await self.config_channels[ctx.guild.id].send(f"About to create group {grp_str}. Do you want to confirm this? [y/n]") + response = await self.bot.wait_for("message", check=self.is_yn_answer) + if response.content[0].lower() == "n": + continue - #making the group - todo_dict["groups"][grp] = [] + #if the creation was successfull + await self.config_channels[ctx.guild.id].send("Do you want to create another group ? [y/n]") + response = await self.bot.wait_for("message", check=self.is_yn_answer) + if response.content[0].lower() == "n": + retry = False + continue - update_todo(ctx.guild.id, todo_dict) - await self.config_channels[ctx.guild.id].send(f"You created the following groups: {grps_str}") - except Exception as e: - local_logger.exception(e) - raise e + #making the group + grps_str = " " + for grp in groups: + todo_dict["groups"][grp] = [] + grps_str += f"{grp}, " + #removing trailing comma and whitespace + grps_str = grps_str[:-2] + update_todo(ctx.guild.id, todo_dict) + await self.config_channels[ctx.guild.id].send(f"You created the following groups: {grps_str}") - async def cfg_todo_chan(self, ctx): - try: - await self.config_channels[ctx.guild.id].send("Each group you've set earlier can be attached to several channels. Each time a new entry is made for a group, the todo will be posted in every channel bound to it.") - todo_dict = get_todo(ctx.guild.id) - for group in todo_dict["groups"]: - retry = True - while retry: - await self.config_channels[ctx.guild.id].send(f"List (like this {self.config_channels[ctx.guild.id].mention})all the channels you want to bind to the {group} group.") - respone = await self.bot.wait_for("message", check=self.is_answer) + except Exception as e: + local_logger.exception(e) + raise e - #making group's channels - chans = [] - chans_str = "" - for chan in response.channel_mentions: - chans_str+= f"{chan} " - chans.append(chan) - await self.config_channels[ctx.guild.id].send(f"You are about to make {chans_str} {group} todo channels. Do you want to confirm? [y/n]") - response = await self.bot.wait_for("message", check=self.is_yn_answer) - if response.content[0].lower() == "n": - await self.config_channels[ctx.guild.id].send(f"Do you want to try to configure {group} again ?") - response = await self.bot.wait_for("message", check=self.is_yn_answer) - if response.content[0].lower() == "n":retry = False - continue - todo_dict["groups"][group] = chans - await self.config_channels[ctx.guild.id].send("You are now done completing the configuration of the todo channels.") + async def cfg_todo_chan(self, ctx): + try: + await self.config_channels[ctx.guild.id].send("Each group you've set earlier can be attached to several channels. Each time a new entry is made for a group, the todo will be posted in every channel bound to it.") + todo_dict = get_todo(ctx.guild.id) + print(todo_dict) + for group in todo_dict["groups"]: + retry = True + while retry: + await self.config_channels[ctx.guild.id].send(f"List (like this {self.config_channels[ctx.guild.id].mention})all the channels you want to bind to the {group} group.") + response = await self.bot.wait_for("message", check=self.is_answer) + #making group's channels + chans = [] + chans_str = "" + for chan in response.channel_mentions: + print(chan, type(chan)) + chans_str+= f"{chan.mention} " + chans.append(chan.id) + + #confirming + await self.config_channels[ctx.guild.id].send(f"You are about to make {chans_str} {group}'s todo channels. Do you want to confirm? [y/n]") + response = await self.bot.wait_for("message", check=self.is_yn_answer) + if response.content[0].lower() == "n": + await self.config_channels[ctx.guild.id].send(f"Do you want to try to configure {group} again ?") + response = await self.bot.wait_for("message", check=self.is_yn_answer) + if response.content[0].lower() == "n":retry = False + continue + else: + retry = False + todo_dict["groups"][group] = chans + update_todo(ctx.guild.id, todo_dict) + await self.config_channels[ctx.guild.id].send("You are now done completing the configuration of the todo channels.") + except Exception as e: + local_logger.exception(e) + raise e - except Exception as e: - local_logger.exception(e) - raise e + async def cfg_todo_type(self, ctx): + try: + retry = True + while retry: + await self.config_channels[ctx.guild.id].send("You are starting the todo types configuration. Which types do you want to add ? Write it like this: `my_type_name;ffffff` The part after the ") - async def cfg_todo_type(self, ctx): - try: - retry = True - while retry: - await self.config_channels[ctx.guild.id].send("You are starting the todo types configuration. Which types do you want to add ? Write it like this: `my_type_name;ffffff` The part after the ") - - - - except Exception as e: - local_logger.exception(e) - raise e - - + except Exception as e: + local_logger.exception(e) + raise e def setup(bot): - bot.add_cog(Config(bot)) \ No newline at end of file + bot.add_cog(Config(bot)) \ No newline at end of file diff --git a/Poll.py b/Poll.py index eab073c..6605d2e 100644 --- a/Poll.py +++ b/Poll.py @@ -6,11 +6,11 @@ from utilities import * ######################################### -# # -# # -# Setting up logging # -# # -# # +# # +# # +# Setting up logging # +# # +# # ######################################### local_logger = logging.getLogger(__name__) local_logger.setLevel(LOGGING_LEVEL) @@ -20,191 +20,191 @@ ######################################### -# # -# # -# Making commands # -# # -# # +# # +# # +# Making commands # +# # +# # ######################################### class Poll(commands.Cog): - """TODO: 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 - - @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''' - - #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) - - #getting poll_allowed_chans - #@is_init - poll_allowed_chans = get_poll_chans(payload.guild_id) - - - #checking that user isn't the bot - if (payload.user_id != self.bot.user.id) and (payload.channel_id in poll_allowed_chans): - - #checking wether the reaction should delete the poll - if payload.emoji.name == EMOJIS["no_entry_sign"]: - if payload.user.name == message.embeds[0].title: - return message.delete() - else: - return reaction.remove(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 - poll_allowed_chans = get_poll_chans(payload.guild_id) - - #fetching concerned message and the user who added the reaction - message = await self.bot.get_channel(payload.channel_id).fetch_message(payload.message_id) - - #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) - - 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.Cog.listener() - async def on_message(self, message): - if message.author==self.bot.user: return - - if not was_init(message): - await message.channel.send(ERR_NOT_SETUP) - return - - #getting poll_allowed_chans - poll_allowed_chans = get_poll_chans(message.guild.id) - - if message.channel.id in poll_allowed_chans and message.content.startswith(PREFIX)!=True: - - #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) - - #making embed - embed_poll = discord.Embed( - title = message.author.name, - description = message.content, - 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") - - - @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") - await ctx.send("NotEnoughArguments:\tYou must provide 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 + """TODO: 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 + + @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''' + + #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) + + #getting poll_allowed_chans + #@is_init + poll_allowed_chans = get_poll_chans(payload.guild_id) + + + #checking that user isn't the bot + if (payload.user_id != self.bot.user.id) and (payload.channel_id in poll_allowed_chans): + + #checking wether the reaction should delete the poll + if payload.emoji.name == EMOJIS["no_entry_sign"]: + if payload.user.name == message.embeds[0].title: + return message.delete() + else: + return reaction.remove(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 + poll_allowed_chans = get_poll_chans(payload.guild_id) + + #fetching concerned message and the user who added the reaction + message = await self.bot.get_channel(payload.channel_id).fetch_message(payload.message_id) + + #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) + + 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.Cog.listener() + async def on_message(self, message): + if message.author==self.bot.user: return + + if not was_init(message): + await message.channel.send(ERR_NOT_SETUP) + return + + #getting poll_allowed_chans + poll_allowed_chans = get_poll_chans(message.guild.id) + + if message.channel.id in poll_allowed_chans and message.content.startswith(PREFIX)!=True: + + #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) + + #making embed + embed_poll = discord.Embed( + title = message.author.name, + description = message.content, + 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") + + + @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") + await ctx.send("NotEnoughArguments:\tYou must provide 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 def setup(bot): - bot.add_cog(Poll(bot)) \ No newline at end of file + bot.add_cog(Poll(bot)) \ No newline at end of file diff --git a/Todo.py b/Todo.py index 7b8d45f..eab6ddd 100644 --- a/Todo.py +++ b/Todo.py @@ -23,39 +23,54 @@ def __init__(self, bot): @commands.Cog.listener() async def on_raw_reaction_add(self, reaction): - if reaction.user_id != self.bot.user.id: - message = await self.bot.get_channel(reaction.channel_id).fetch_message(reaction.message_id) - - with open(TODO_CHANNEL_FILE , "r") as file: - channel_id = file.readline() - - if reaction.channel_id == int(channel_id): #Check if it's a todo-message (check if it's the good channel) - if len(message.embeds) > 0: # Check if it's an embed, I think this will avoid most problems - if reaction.emoji.name == EMOJIS['wastebasket']: - await self.bot.get_channel(reaction.channel_id).delete_messages([message]) - - repost_field_value = None - for field in message.embeds[0].fields: - if field.name == "Public repost": - repost_field_value= field.value - - if repost_field_value!= None: - repost_message = await self.bot.get_channel(int(repost_field_value.split(':')[0][2:-2])).fetch_message(int(repost_field_value.split(':')[1][1:])) - await repost_message.delete() - 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) + 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) - with open(TODO_CHANNEL_FILE , "r") as file: - channel_id = file.readline() + #checking if channel is todo + todo = get_todo(reaction.guild_id) + for chan in lambda: [chan for group in todo["groups"].values() for chan in group]: + if reaction.channel_id == chan.id: + is_todo = True + break - if reaction.channel_id == int(channel_id): # Check if it's a todo-message (check if it's the good channel) - if len(message.embeds) > 0: # Check if it's an embed, I think this will avoid most problems + 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']) @@ -67,10 +82,10 @@ async def on_raw_reaction_remove(self, reaction): async def todo(self, ctx): '''Commands to manage a todolist.''' if ctx.invoked_subcommand is None: - await ctx.send('Error: See for ``' + PREFIX + 'help todo``') + await ctx.send(ERR_NOT_ENOUGH_ARG) @todo.command() - async def add(self, ctx, todo_type, assignee: Union[bool, discord.Member], repost:Union[bool, discord.TextChannel], *args): + async def add(self, ctx, todo_type, assignee: Union[bool, discord.Member], *args): #, repost:Union[bool, discord.TextChannel] '''Command to add a todo. Usage : ;;;''' todo_dict = get_todo(ctx.guild.id) @@ -95,9 +110,9 @@ async def add(self, ctx, todo_type, assignee: Union[bool, discord.Member], repos 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) + #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) diff --git a/executioner.py b/executioner.py index e250ba9..781c552 100644 --- a/executioner.py +++ b/executioner.py @@ -14,11 +14,11 @@ bot = commands.Bot(command_prefix=PREFIX) ######################################### -# # -# # -# Setting up logging # -# # -# # +# # +# # +# Setting up logging # +# # +# # ######################################### #Creating main logger @@ -36,11 +36,11 @@ ######################################### -# # -# # -# Extension Manipulation # -# # -# # +# # +# # +# Extension Manipulation # +# # +# # ######################################### @@ -48,124 +48,124 @@ @bot.group() @is_runner() async def ext(ctx): - if ctx.invoked_subcommand is None: - await ctx.send(ERR_NOT_ENOUGH_ARG) + if ctx.invoked_subcommand is None: + await ctx.send(ERR_NOT_ENOUGH_ARG) @ext.command() async def reload(ctx, extension:str): - try: - bot.reload_extension(extension) - await ctx.send("Successfully reloaded {}".format(extension)) - except Exception as e: - await ctx.send("Couldn't reload extension {} because:```python\n{}```".format(extension, e)) - raise e + try: + bot.reload_extension(extension) + await ctx.send("Successfully reloaded {}".format(extension)) + except Exception as e: + await ctx.send("Couldn't reload extension {} because:```python\n{}```".format(extension, e)) + raise e @ext.command() async def add(ctx, extension:str): - #trying to load the extension. Should only fail if the extension is not installed - try: - bot.load_extension(extension) + #trying to load the extension. Should only fail if the extension is not installed + try: + bot.load_extension(extension) - except Exception as e: - main_logger.exception(e) - await ctx.send("UnexpectedError:\tReport issue to an admin\n{}".format(e)) - raise e + except Exception as e: + main_logger.exception(e) + await ctx.send("UnexpectedError:\tReport issue to an admin\n{}".format(e)) + raise e - #if the extension was correctly loaded, adding it to the enabled file - try: - with open(ENABLED_EXTENSIONS_FILE, "r") as file: - enabled_exts = json.load(file) - - enabled_exts[extension] = True + #if the extension was correctly loaded, adding it to the enabled file + try: + with open(ENABLED_EXTENSIONS_FILE, "r") as file: + enabled_exts = json.load(file) + + enabled_exts[extension] = True - with open(ENABLED_EXTENSIONS_FILE, "w") as file: - json.dump(enabled_exts, file) + with open(ENABLED_EXTENSIONS_FILE, "w") as file: + json.dump(enabled_exts, file) - except FileNotFoundError as e: - #if the file didn't yet exist a new one will be created. This should not happen, only here as a failsafe - main_logger.warning("{} doesn't exist.".format(ENABLED_EXTENSIONS_FILE)) - with open(ENABLED_EXTENSIONS_FILE, "w") as file: - file.write(DEFAULT_EXTENSIONS_JSON) + except FileNotFoundError as e: + #if the file didn't yet exist a new one will be created. This should not happen, only here as a failsafe + main_logger.warning("{} doesn't exist.".format(ENABLED_EXTENSIONS_FILE)) + with open(ENABLED_EXTENSIONS_FILE, "w") as file: + file.write(DEFAULT_EXTENSIONS_JSON) - except Exception as e: - #logging any other possible issue - await ctx.send("UnexpectedError:\tReport issue to an admin") - raise e + except Exception as e: + #logging any other possible issue + await ctx.send("UnexpectedError:\tReport issue to an admin") + raise e - await ctx.send("Successfully added and loadded {}".format(extension)) + await ctx.send("Successfully added and loadded {}".format(extension)) @ext.command() async def rm(ctx, extension:str): - try: - bot.unload_extension(extension) + try: + bot.unload_extension(extension) - except Exception as e: - main_logger.exception(e) - await ctx.send("UnexpectedError:\tReport issue to an admin\n{}".format(e)) - raise e + except Exception as e: + main_logger.exception(e) + await ctx.send("UnexpectedError:\tReport issue to an admin\n{}".format(e)) + raise e - #if the extension was correctly unloaded, removing it from the enblaed extension file - try: - with open(ENABLED_EXTENSIONS_FILE, "r") as file: - enabled_exts = json.load(file) - - enabled_exts[extension] = False + #if the extension was correctly unloaded, removing it from the enblaed extension file + try: + with open(ENABLED_EXTENSIONS_FILE, "r") as file: + enabled_exts = json.load(file) + + enabled_exts[extension] = False - with open(ENABLED_EXTENSIONS_FILE, "w") as file: - json.dump(enabled_exts, file) + with open(ENABLED_EXTENSIONS_FILE, "w") as file: + json.dump(enabled_exts, file) - except Exception as e: - main_logger.exception(e) - await ctx.send("UnexpectedError:\tReport issue to an admin\n{}".format(e)) - raise e + except Exception as e: + main_logger.exception(e) + await ctx.send("UnexpectedError:\tReport issue to an admin\n{}".format(e)) + raise e - await ctx.send("Successfully removed and unloaded {}".format(extension)) - local_logger.info(f"Disabled and removed {extension}") + await ctx.send("Successfully removed and unloaded {}".format(extension)) + local_logger.info(f"Disabled and removed {extension}") @ext.command() async def ls(ctx): - try: - ext_list = "" - for e in bot.extensions.keys(): - ext_list+=f"**{e}**, " - ext_list = ext_list[:-2] - await ctx.send(f"The loaded extenions are: {ext_list}") + try: + ext_list = "" + for e in bot.extensions.keys(): + ext_list+=f"**{e}**, " + ext_list = ext_list[:-2] + await ctx.send(f"The loaded extenions are: {ext_list}") - except Exception as e: - main_logger.exception(e) + except Exception as e: + main_logger.exception(e) ######################################### -# # -# # -# Setup & Execution # -# # -# # +# # +# # +# Setup & Execution # +# # +# # ######################################### #loading enabled extensions and starting #bot #trying to load all enabled extensions try: - with open(ENABLED_EXTENSIONS_FILE, "r") as file: - extensions = json.load(file) - for ext in extensions: - if extensions[ext]==True: - bot.load_extension(ext) + with open(ENABLED_EXTENSIONS_FILE, "r") as file: + extensions = json.load(file) + for ext in extensions: + if extensions[ext]==True: + bot.load_extension(ext) #if no extension is enabled except FileNotFoundError as e: - main_logger.warning("No extension enabled, none loaded. You probably want to configure the bot or add some extensions") - raise e + main_logger.warning("No extension enabled, none loaded. You probably want to configure the bot or add some extensions") + raise e #unexpected error handling except Exception as e: - main_logger.exception(e) - raise e + main_logger.exception(e) + raise e #running the bot, no matter what finally: - bot.run(TOKEN) \ No newline at end of file + bot.run(TOKEN) \ No newline at end of file diff --git a/settings.py b/settings.py index 9b1765c..2c663d9 100644 --- a/settings.py +++ b/settings.py @@ -2,11 +2,11 @@ ######################################### -# # -# # -# Global Variables # -# # -# # +# # +# # +# Global Variables # +# # +# # ######################################### @@ -28,23 +28,23 @@ #emojis dict. May be possible to change incomprehensible unicode to other strings recognized by discord EMOJIS = { - "thumbsup": "\U0001f44d", - "thumbsdown": "\U0001f44e", - "shrug": "\U0001f937", - "wastebasket": "\U0001F5D1", - "check": "\U00002705", - "hourglass": "\U000023F3", - "wave": "\U0001F44B", - "no_entry_sign": "\U0001F6AB" + "thumbsup": "\U0001f44d", + "thumbsdown": "\U0001f44e", + "shrug": "\U0001f937", + "wastebasket": "\U0001F5D1", + "check": "\U00002705", + "hourglass": "\U000023F3", + "wave": "\U0001F44B", + "no_entry_sign": "\U0001F6AB" } ######################################### -# # -# # -# Files # -# # -# # +# # +# # +# Files # +# # +# # ######################################### #Files @@ -58,16 +58,16 @@ #default JSON files DEFAULT_EXTENSIONS_JSON = '''{ - "Slapping": false, - "BotEssentials":true, - "Role":false, - "Embedding":false, - "Config":false, - "Poll":false + "Slapping": false, + "BotEssentials":true, + "Role":false, + "Embedding":false, + "Config":false, + "Poll":false }''' DEFAULT_SLAPPED_FILE = '''{ - "463665420054953995": 0 + "463665420054953995": 0 }''' DEFAULT_SERVER_FILE = '''{ @@ -85,36 +85,35 @@ }''' -DEFAULT_TODO_FILE = { - "groups": { - "level1": [] - }, - "types": { - "bug": "ffffff" - }, - -} +DEFAULT_TODO_FILE = '''{ + "groups": { + "default": [] + }, + "types": { + "default": "000000" + } +}''' ######################################### -# # -# # -# Logging # -# # -# # +# # +# # +# Logging # +# # +# # ######################################### LOG_FILE = "forebot.log" -LOGGING_HANDLER = logging.FileHandler(LOG_FILE, "a") -LOGGING_FORMATTER = logging.Formatter("[%(asctime)s]:%(name)s:%(message)s") -LOGGING_LEVEL = logging.INFO +LOGGING_HANDLER = logging.FileHandler(LOG_FILE, "a") +LOGGING_FORMATTER = logging.Formatter("\n[%(asctime)s][%(name)s]:%(message)s") +LOGGING_LEVEL = logging.INFO LOGGING_HANDLER.setFormatter(LOGGING_FORMATTER) ######################################### -# # -# # -# Errors # -# # -# # +# # +# # +# Errors # +# # +# # ######################################### @@ -122,5 +121,5 @@ ERR_UNEXCPECTED = "An unexcpected error occured. Please report a bug in {} or contact an admin of your server." ERR_NOT_ENOUGH_ARG = "This command requires additional arguments. See `::help ` to get more information on the command's usage" ERR_UNSUFFICIENT_PRIVILEGE = "You don't have the permission to do this..." -ERR_NOT_SETUP = "This server hasn't been configured. If you're the owner of the server you can initialise the bot by doing `::cfg init` in any channel. You won't be able to use the bot before that." +ERR_NOT_SETUP = "This server hasn't been configured. If you're the owner of the server you can initialize the bot by doing `::cfg init` in any channel. You won't be able to use the bot before that." ERR_CANT_SAVE = "Couldn't save settings to JSON configuration file." \ No newline at end of file diff --git a/utilities.py b/utilities.py index ce210a6..b180f49 100644 --- a/utilities.py +++ b/utilities.py @@ -6,11 +6,11 @@ ######################################### -# # -# # -# Setting up logging # -# # -# # +# # +# # +# Setting up logging # +# # +# # ######################################### local_logger = logging.getLogger(__name__) local_logger.setLevel(LOGGING_LEVEL) @@ -19,185 +19,184 @@ ######################################### -# # -# # -# Checks # -# # -# # +# # +# # +# Checks # +# # +# # ######################################### def is_runner(): # to be deleted sine it does the same as is_owner() - def check_condition(ctx): - return ctx.message.author.id ==RUNNER_ID - result = commands.check(check_condition) - if result == False: - pass - #ctx.send(ERR_UNSUFFICIENT_PRIVILEGE) - return result + def check_condition(ctx): + return ctx.message.author.id ==RUNNER_ID + result = commands.check(check_condition) + if result == False: + pass + #ctx.send(ERR_UNSUFFICIENT_PRIVILEGE) + return result def is_init(): - '''checks whether the server has been initialized. Meant as a fale-safe for commands requiring configuration.''' - def check_condition(ctx): - conf_files = os.listdir(CONFIG_FOLDER) - file_name = f"{ctx.guild.id}.json" - #ctx.send(ERR_NOT_SETUP) - return file_name in conf_files + '''checks whether the server has been initialized. Meant as a fale-safe for commands requiring configuration.''' + def check_condition(ctx): + conf_files = os.listdir(CONFIG_FOLDER) + file_name = f"{ctx.guild.id}.json" + #ctx.send(ERR_NOT_SETUP) + return file_name in conf_files - return commands.check(check_condition) + return commands.check(check_condition) def was_init(ctx): - '''same as the previous function except this one isn't a decorator. Mainly used for listenners''' - if f"{ctx.guild.id}.json" in os.listdir(CONFIG_FOLDER): - return True - return False + '''same as the previous function except this one isn't a decorator. Mainly used for listenners''' + if f"{ctx.guild.id}.json" in os.listdir(CONFIG_FOLDER): + return True + return False def has_auth(clearance, *args): - '''checks whether the user invoking the command has the specified clearance level of clearance for the server the command is being ran on''' - def predicate(ctx): - allowed_roles = get_roles(ctx.guild.id, clearance) - for role in ctx.author.roles: - if role.id in allowed_roles: - return True - local_logger.send(ERR_UNSUFFICIENT_PRIVILEGE) - local_logger.warning(ERR_UNSUFFICIENT_PRIVILEGE) - return False - - return commands.check(predicate) + '''checks whether the user invoking the command has the specified clearance level of clearance for the server the command is being ran on''' + def predicate(ctx): + allowed_roles = get_roles(ctx.guild.id, clearance) + for role in ctx.author.roles: + if role.id in allowed_roles: + return True + local_logger.send(ERR_UNSUFFICIENT_PRIVILEGE) + local_logger.warning(ERR_UNSUFFICIENT_PRIVILEGE) + return False + + return commands.check(predicate) def is_server_owner(): - '''check meant to verify whether its author os the owner of the server where the command is being ran''' - def predicate(ctx): - if ctx.author == ctx.guild.owner: - return True - #ctx.send(ERR_UNSUFFICIENT_PRIVILEGE) - return False + '''check meant to verify whether its author os the owner of the server where the command is being ran''' + def predicate(ctx): + if ctx.author == ctx.guild.owner: + return True + #ctx.send(ERR_UNSUFFICIENT_PRIVILEGE) + return False - return commands.check(predicate) + return commands.check(predicate) ######################################### -# # -# # -# Utility functions # -# # -# # +# # +# # +# Utility functions # +# # +# # ######################################### def get_m_time(file): - return os.getmtime(file+".json") + return os.getmtime(file+".json") def has_changed(server, last_time): - last_update = get_m_time(file) - if last_update != last_time: - return True - return False + last_update = get_m_time(file) + if last_update != last_time: + return True + return False def get_conf(guild_id): - '''returns the configuration dict of the provided guild_id''' - with open(os.path.join(CONFIG_FOLDER,f"{guild_id}.json"), "r") as file: - conf = json.load(file) - return conf + '''returns the configuration dict of the provided guild_id''' + with open(os.path.join(CONFIG_FOLDER,f"{guild_id}.json"), "r") as file: + conf = json.load(file) + return conf def update_conf(guild_id, conf_dict): - '''writes the conf_dict to the provided guild_id configuration file''' - try: - with open(os.path.join(CONFIG_FOLDER,f"{guild_id}.json"), "w") as file: - json.dump(conf_dict, file) - return True + '''writes the conf_dict to the provided guild_id configuration file''' + try: + with open(os.path.join(CONFIG_FOLDER,f"{guild_id}.json"), "w") as file: + json.dump(conf_dict, file) + return True - except Exception as e: - local_logger.exception(e) - return False + except Exception as e: + local_logger.exception(e) + return False def del_conf(guild_id): - '''deletes the configuration entry for the provided guild_id''' - try: - os.remove(os.path.join(CONFIG_FOLDER,f"{guild_id}.json")) - return True + '''deletes the configuration entry for the provided guild_id''' + try: + os.remove(os.path.join(CONFIG_FOLDER,f"{guild_id}.json")) + return True - except Exception as e: - local_logger.exception(e) - return False + except Exception as e: + local_logger.exception(e) + return False def get_roles(guild_id, lvl): - '''returns the roles with the provided lvl of clearance for the specified guild_id''' - try: - with open(os.path.join(CONFIG_FOLDER,f"{guild_id}.json"), "r") as file: - return json.load(file)["roles"][lvl] + '''returns the roles with the provided lvl of clearance for the specified guild_id''' + try: + with open(os.path.join(CONFIG_FOLDER,f"{guild_id}.json"), "r") as file: + return json.load(file)["roles"][lvl] - except Exception as e: - local_logger.exception(e) - raise e + except Exception as e: + local_logger.exception(e) + raise e def get_poll_chans(guild_id): - '''returns a list of channel ids marked as poll channels for the specified guild_id''' - try: - with open(os.path.join(CONFIG_FOLDER,f"{guild_id}.json"), "r") as file: - fl = json.load(file) - - chans = fl["poll_channels"] - if len(chans)==0: - #isn't None to prevent Poll listener from crashing - return [] - - return chans - - except Exception as e: - raise e - local_logger.exception(e) + '''returns a list of channel ids marked as poll channels for the specified guild_id''' + try: + with open(os.path.join(CONFIG_FOLDER,f"{guild_id}.json"), "r") as file: + fl = json.load(file) + + chans = fl["poll_channels"] + if len(chans)==0: + #isn't None to prevent Poll listener from crashing + return [] + + return chans + + except Exception as e: + raise e + local_logger.exception(e) def get_slaps(guild_id, user_id): - '''returns an int of the number of slaps of the user_id in the provided guild_id''' - with open(os.path.join(SLAPPING_FOLDER, f"{guild_id}.json"), "r") as file: - fl = json.load(file) + '''returns an int of the number of slaps of the user_id in the provided guild_id''' + with open(os.path.join(SLAPPING_FOLDER, f"{guild_id}.json"), "r") as file: + fl = json.load(file) - try: - slaps = fl[f"{user_id}"] - except KeyError: - slaps = 0 + try: + slaps = fl[f"{user_id}"] + except KeyError: + slaps = 0 - except Exception as e: - raise e - local_logger.exception(e) + except Exception as e: + raise e + local_logger.exception(e) - return slaps + return slaps def update_slaps(guild_id, user_id, slaps): - '''changed the number of time the user has been slapped''' - with open(os.path.join(SLAPPING_FOLDER, f"{guild_id}.json"), "r") as file: - fl = json.load(file) + '''changed the number of time the user has been slapped''' + with open(os.path.join(SLAPPING_FOLDER, f"{guild_id}.json"), "r") as file: + fl = json.load(file) - try: - fl[f"{user_id}"] = slaps + try: + fl[f"{user_id}"] = slaps - with open(os.path.join(SLAPPING_FOLDER, f"{guild_id}.json"), "w") as file: - json.dump(fl, file) + with open(os.path.join(SLAPPING_FOLDER, f"{guild_id}.json"), "w") as file: + json.dump(fl, file) - return True - except Exception as e: - raise e - local_logger.exception(e) - return False + return True + except Exception as e: + raise e + local_logger.exception(e) + return False def get_todo(guild_id): - '''returns the todo dict of the specifeid guild_id''' - try: - with open(os.path.join(TODO_FOLDER, f"{guild_id}.json"), "r") as file: - fl = json.load(file) - return conf - except Exception as e: - raise e - local_logger.exception(e) + '''returns the todo dict of the specifeid guild_id''' + try: + with open(os.path.join(TODO_FOLDER, f"{guild_id}.json"), "r") as file: + return json.load(file) + except Exception as e: + raise e + local_logger.exception(e) def update_todo(guild_id, todo_dict): - '''updates the todo file for the specified guild_id''' - try: - with open(os.path.join(TODO_FOLDER, f"{guild_id}.json"), "w") as file: - json.dump(todo_dict, file) - return True - - except Exception as e: - raise e - local_logger.exception(e) - return False \ No newline at end of file + '''updates the todo file for the specified guild_id''' + try: + with open(os.path.join(TODO_FOLDER, f"{guild_id}.json"), "w") as file: + json.dump(todo_dict, file) + return True + + except Exception as e: + raise e + local_logger.exception(e) + return False \ No newline at end of file From dad6e14eb21a3f6d2d0b14efc5860c14febeb169 Mon Sep 17 00:00:00 2001 From: NotaSmartDev Date: Fri, 28 Jun 2019 15:51:09 +0200 Subject: [PATCH 50/52] changed author username --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index fb194ef..385d0af 100644 --- a/README.md +++ b/README.md @@ -10,7 +10,7 @@ This program is a Discord bot which is basically a mod to the real time messagin ## Development history -I ([@NotaSmartDev](https://github.com/NotaSmartDev)) started building this bot at the end of April 2019 using discord.py API. This bot was first made with the intent to make my discord server more powerful and alive. I had only created it a few days ago but I had realized that I would need additional tools to be able to fulfill all of the plans I had for this server. I had already started making a [bot](https://github.com/organic-bots/LazyFactorian) which serves as an interface to factorio's resources. I thus started building a bot that would enable me to easily manage a scalable server which would contain all of my future bots and would serve as a platform for all my creations. +I ([@s0lst1ce](https://github.com/s0lst1ce)) started building this bot at the end of April 2019 using discord.py API. This bot was first made with the intent to make my discord server more powerful and alive. I had only created it a few days ago but I had realized that I would need additional tools to be able to fulfill all of the plans I had for this server. I had already started making a [bot](https://github.com/organic-bots/LazyFactorian) which serves as an interface to factorio's resources. I thus started building a bot that would enable me to easily manage a scalable server which would contain all of my future bots and would serve as a platform for all my creations. ​ After the very first version I got some help of a friend of mine. Together we made the bot evolve so that it could join the ranks of other servers. Indeed I had started to realize that the bot, however simple, may be useful to others. From 14a99ed36dd3bd38bd2292250cfe2c41e3ff04fd Mon Sep 17 00:00:00 2001 From: s0lst1ce Date: Fri, 28 Jun 2019 22:04:05 +0200 Subject: [PATCH 51/52] added feedback for role assignement --- Config.py | 10 +++++----- Role.py | 5 +++++ Todo.py | 2 +- 3 files changed, 11 insertions(+), 6 deletions(-) diff --git a/Config.py b/Config.py index 75d001c..9280c69 100644 --- a/Config.py +++ b/Config.py @@ -88,7 +88,7 @@ async def init(self, ctx): await self.cfg_role(ctx) await self.cfg_welcome(ctx) await self.cfg_goodbye(ctx) - await self.cfg_todo(ctx) + #await self.cfg_todo(ctx) #asking for permisison to advertise await self.config_channels[ctx.guild.id].send("You're almost done ! Just one more thing...") @@ -198,7 +198,7 @@ async def cfg_poll(self, ctx): async def cfg_role(self, ctx): try: #introducing the clearance levels the bot uses - await self.config_channels[ctx.guild.id].send("**Starting 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.") + await self.config_channels[ctx.guild.id].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: @@ -435,13 +435,11 @@ async def cfg_todo_grps(self, ctx): #if the creation was successfull await self.config_channels[ctx.guild.id].send("Do you want to create another group ? [y/n]") - response = await self.bot.wait_for("message", check=self.is_yn_answer) + response = await self.bot.wait_for("message", check=self.is_yn_answer) if response.content[0].lower() == "n": retry = False continue - - #making the group grps_str = " " for grp in groups: @@ -449,8 +447,10 @@ async def cfg_todo_grps(self, ctx): grps_str += f"{grp}, " #removing trailing comma and whitespace grps_str = grps_str[:-2] + print(grps_str) update_todo(ctx.guild.id, todo_dict) + print(grps_str) await self.config_channels[ctx.guild.id].send(f"You created the following groups: {grps_str}") diff --git a/Role.py b/Role.py index d6cc864..51514a2 100644 --- a/Role.py +++ b/Role.py @@ -49,6 +49,11 @@ async def add(self, ctx, member: discord.Member, *roles:discord.Role): else: try: await member.add_roles(*roles) + roles_str = "" + for role in roles: + roles_str+= f" {role}" + + await ctx.send(f"You gave {member.name} {roles_str} role(s).") except Exception as e: local_logger.exception("Couldn't add {} to {}".format(roles, member)) await ctx.send("An unexpected error occured !\nTraceback:```python\n{}```".format(e)) diff --git a/Todo.py b/Todo.py index eab6ddd..cfbe7c8 100644 --- a/Todo.py +++ b/Todo.py @@ -64,7 +64,7 @@ async def on_raw_reaction_remove(self, reaction): #checking if channel is todo todo = get_todo(reaction.guild_id) - for chan in lambda: [chan for group in todo["groups"].values() for chan in group]: + for chan in [chan for group in todo["groups"].values() for chan in group]: if reaction.channel_id == chan.id: is_todo = True break From 9e62075a5d7f337bdee8d4edadccf773a74856b5 Mon Sep 17 00:00:00 2001 From: s0lst1ce Date: Fri, 28 Jun 2019 22:15:55 +0200 Subject: [PATCH 52/52] updated todo documentation --- README.md | 17 ++++++++++++++++- 1 file changed, 16 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 385d0af..f4e6984 100644 --- a/README.md +++ b/README.md @@ -130,4 +130,19 @@ Allows the developers to update the bot and notify all server owners of the chan | Group | Command | Arguments | Description | Clearance | | ----- | :------: | :-------: | :----------------------------------------------------------: | --------- | | | `update` | * | sends an update message to all users who own a server of which the bot is a member. The given arguments will be transformed into the message sent to the server owners. A default message is sent if none is provided. This can be modified in `settings.py`. | owner | -| | `log` | | returns the bot's log file | owner | \ No newline at end of file +| | `log` | | returns the bot's log file | owner | + + + +#### Todo *In development* + +Allows moderators to make a to-do list in one or more channels. It's also possible to make types for the to-do's, to assign a member to a to-do and to make a copy of the to-do in a public or in a other channel. If the to-do is deleted the replica will also be deleted. For all the command where arguments are split with : `;` you must respect those. + +| Group | Command | Arguments | Description | Clearance | +| ------ | :----------: | :----------------------------------------------------------: | :----------------------------------------------------------: | :-------: | +| `todo` | `add` | `` `;` `` | adds the to-do in the selected channel (see `todo channel` command) . A color will be used for the embeds if the to-do type exist. The member can be mention or just wrote normally, he will be mention in both case. The channel can be a mention or can be wrote manually, he will be write as mentioned is both case. | * | +| `todo` | `removetype` | `` | removes the type | * | +| `todo` | `listtypes` | `` | list created types | * | +| `todo` | `addtype` | `` `` | adds a type for the to-dos | * | +| `todo` | `channel` | | select the channel for the future to-do's | * | +