diff --git a/BotEssentials.py b/BotEssentials.py index a157cf5..a48c737 100644 --- a/BotEssentials.py +++ b/BotEssentials.py @@ -1,8 +1,8 @@ import logging from settings import * import discord -from checks import * - +from utilities import * +import json ######################################### @@ -34,6 +34,14 @@ class BotEssentials(commands.Cog): """All of the essential methods all of our bots should have""" 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): @@ -42,13 +50,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"]["goodbye"] + if goodbye_msg != False: + await member.guild.system_channel.send(goodbye_msg.format(member.mention)) @commands.command() async def ping(self, ctx): @@ -56,7 +67,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() @@ -72,17 +82,19 @@ 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): + 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 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/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 new file mode 100644 index 0000000..9280c69 --- /dev/null +++ b/Config.py @@ -0,0 +1,517 @@ +import logging +import discord +import asyncio +import os +from settings import * +from utilities import * + +######################################### +# # +# # +# Setting up logging # +# # +# # +######################################### +local_logger = logging.getLogger(__name__) +local_logger.setLevel(LOGGING_LEVEL) +local_logger.addHandler(LOGGING_HANDLER) +local_logger.info("Innitalized {} logger".format(__name__)) + + +######################################### +# # +# # +# Making commands # +# # +# # +######################################### + + + +class 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. 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 + + else: retry=False + + poll_channels_ids = [] + for chan in poll_channels: + poll_channels_ids.append(chan.id) + + + 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) + + 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.") + + except Exception as e: + local_logger.exception(e) + raise e + + + async def cfg_role(self, ctx): + try: + #introducing the clearance levels the bot uses + 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: + 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}" + + #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 + + else: retry = False + + + local_logger.info(f"Server {ctx.guild.name} configured its {role_lvl} roles") + + for role in roles: + new_role.append(role.id) + + #adding to master role list + new_roles.append(new_role) + + + #giving admin roles the manager clearance + for m_role in new_roles[1]: + new_roles[0].append(m_role) + + old_conf = get_conf(ctx.guild.id) + + #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: + local_logger.exception(e) + raise e + + + 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.content[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 `{0}`") + + message = await self.bot.wait_for("message", check=self.is_answer) + + 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("**--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.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"]["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: + local_logger.exception(e) + raise e + + + async def cfg_goodbye(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.content[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 `{0}`") + + message = await self.bot.wait_for("message", check=self.is_answer) + + 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("**--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.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) + + 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)") + + + 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...") + + #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) + + #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 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) + + except Exception as e: + local_logger.exception(e) + raise e + + + 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) + + #building group string + todo_dict = get_todo(ctx.guild.id) + grp_str=f"`{response.content}`" + groups.append(response.content) + + + 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 + + #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 + + #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] + 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}") + + 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) + 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 + + 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 + + +def setup(bot): + bot.add_cog(Config(bot)) \ No newline at end of file diff --git a/Development.py b/Development.py new file mode 100644 index 0000000..999c061 --- /dev/null +++ b/Development.py @@ -0,0 +1,70 @@ +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, *words): #should message be put in settings.py ? + '''Lets the owner of the bot update the bot from github's repositery. It also sends a notification to all server owners who use the bot. The message sent in the notification is the description of the release on github. + NB: as of now the bot only sends a generic notification & doesn't update the bot.''' + #building message + if len(words)==0: + message = DEFAULT_UPDATE_MESSAGE + else: + message = "" + for w in words: + message+=f" {w}" + + owners = [] + for g in self.bot.guilds: + if g.owner not in owners: owners.append(g.owner) + + for o in owners: + await o.send(message) + + @commands.command() + @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 1cd80b5..8b1a289 100644 --- a/Embedding.py +++ b/Embedding.py @@ -1,7 +1,8 @@ import logging import discord +import io from settings import * -from checks import * +from utilities import * ######################################### # # @@ -30,42 +31,34 @@ 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): """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 - msg = "" - img_url = None - for arg in args: - if arg.startswith("https://"): - img_url = arg - else: - msg += " {}".format(arg) + #lining attachements + files = [] + for attachment in ctx.message.attachments: + content = await attachment.read() + io_content = io.BytesIO(content) + file = discord.File(io_content, filename=attachment.filename) + files.append(file) - print(msg) embed_msg = discord.Embed( title = None, - description = msg, + description = ctx.message.content[8:], colour = ctx.author.color, url = None ) - if img_url: - embed_msg.set_image(url=img_url) - embed_msg.set_footer(text=ctx.message.author.name, icon_url=ctx.message.author.avatar_url) + 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/Poll.py b/Poll.py index dd90b5e..6605d2e 100644 --- a/Poll.py +++ b/Poll.py @@ -1,15 +1,16 @@ import os import logging import discord +import io from settings import * -from checks import * +from utilities import * ######################################### -# # -# # -# Setting up logging # -# # -# # +# # +# # +# Setting up logging # +# # +# # ######################################### local_logger = logging.getLogger(__name__) local_logger.setLevel(LOGGING_LEVEL) @@ -19,181 +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 - - #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) - - - - @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) - - #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 if reaction is allowed - if 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, react_for, react_against) - - - - @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 - - #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]): - - 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) - - - 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)) - #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 message.channel.id in self.poll_allowed_chans[message.guild.id] and message.content.startswith(PREFIX)!=True: - embed_poll = discord.Embed( - title = message.author.name, - description = message.content, - colour = discord.Color(16776960), - url = None - ) - embed_poll.set_thumbnail(url=message.author.avatar_url) - #embed_poll.set_footer(text=message.author.name, icon_url=message.author.avatar_url) - - try: - await message.delete() - sent_msg = await message.channel.send(embed=embed_poll) - 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 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''' - 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 - - + """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/README.md b/README.md index ac9c8f7..f4e6984 100644 --- a/README.md +++ b/README.md @@ -1,32 +1,79 @@ +

+ +

+ # 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. -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 ([@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. + +## 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. 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`. + +​ 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 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. + + + -### 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. +### Reference -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. +#### Defaults -### Commands +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. -Here is an exhaustive list of all extensions and the commands they provide. This list is kept up to date with the latest 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. | * | -#### 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. -- `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 | * | @@ -34,34 +81,68 @@ 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. + +| 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 -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 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 | 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 | + + + +#### Config + +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 | -#### Todo + + +#### 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 | + + + +#### 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. -- `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` `addtype` `;`: adds a type for the to-dos. -- `todo` `removetype` ``: Remove the type. -- `todo` `listypes` ``: List created types. -- `todo` `channel`: Select the channel for the future to-do's \ No newline at end of file +| 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 | * | + diff --git a/Role.py b/Role.py index c88d165..51514a2 100644 --- a/Role.py +++ b/Role.py @@ -1,14 +1,14 @@ import logging from settings import * import discord -from checks import * +from utilities import * ######################################### -# # -# # -# Setting up logging # -# # -# # +# # +# # +# Setting up logging # +# # +# # ######################################### local_logger = logging.getLogger(__name__) local_logger.setLevel(LOGGING_LEVEL) @@ -17,56 +17,61 @@ ######################################### -# # -# # -# 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() - @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") + @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") - @role.command() - async def add(self, ctx, member: discord.Member, *roles:discord.Role): - '''adds role(s) to ''' - if len(role)==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 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)) + else: + try: + await member.add_roles(*roles) + roles_str = "" + for role in roles: + roles_str+= f" {role}" - @role.command() - async def rm(self, ctx, member:discord.Member, *roles:discord.Role): - '''removes role(s) to ''' - if len(role)==0: - local_logger.warning("User didn't provide a role") - await ctx.send("NotEnoughArguments:\tYou must provide at least one `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)) - 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)) + @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)) 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 diff --git a/Slapping.py b/Slapping.py index 658d9b2..0c5ae28 100644 --- a/Slapping.py +++ b/Slapping.py @@ -1,7 +1,8 @@ import logging import discord +import os from settings import * -from checks import * +from utilities import * ######################################### # # @@ -30,70 +31,36 @@ class Slapping(commands.Cog): """a suite of commands meant to help moderators handle the server""" def __init__(self, bot): self.bot = bot - + @commands.command() - @commands.has_any_role(*GESTION_ROLES) + @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''' - 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: - if line.startswith(str(member.id)): - slap_count = int(line.split(";")[1])+1 - to_write+= "{};{}\n".format(member.id, slap_count) - - 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) + #slapping + slaps = get_slaps(ctx.guild.id, member.id) + slaps += 1 + update_slaps(ctx.guild.id, member.id, slaps) - - 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) + #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)) @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: - content = file.readlines() - for line in content: - if not line.startswith(str(member.id)): - to_write+=line - - #if iterating over the user, removing the right number of slaps - else: - crt_slaps = int(line.split(";")[1]) - print(crt_slaps) - if crt_slaps 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 [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']) @@ -62,77 +78,51 @@ 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: - await ctx.send('Error: See for ``' + PREFIX + 'help todo``') + await ctx.send(ERR_NOT_ENOUGH_ARG) @todo.command() - async def add(self, ctx, *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 : ;;;''' - 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, command): + async def addtype(self, ctx, todo_type, hex_color): '''Command to add a todo type.''' command = command.split(";") @@ -158,43 +148,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 - @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" + old_conf["todo_types"].pop(todo_type) + update_conf(ctx.guild.id, old_conf) + await ctx.send(f"Successfully deleted {todo_type} type.") - new_embed = discord.Embed(description=text, url="", color=0x28a745) - message = await ctx.channel.send(embed=new_embed) + except Exception as e: + local_logger.exception(e) + await ctx.send(ERR_UNEXCPECTED) @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 !') + async def listtypes(self, ctx): + '''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) def setup(bot): bot.add_cog(Todo(bot)) diff --git a/checks.py b/checks.py deleted file mode 100644 index 12ad117..0000000 --- a/checks.py +++ /dev/null @@ -1,30 +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("Innitalized {} logger".format(__name__)) - - -######################################### -# # -# # -# Checks # -# # -# # -######################################### - -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 diff --git a/executioner.py b/executioner.py index d5ba5c7..c63662e 100644 --- a/executioner.py +++ b/executioner.py @@ -1,34 +1,31 @@ #!/usr/bin/env python3 import discord -from discord.utils import find as dfind from settings import * -from checks import * +from utilities import * import math -import requests as rq -from bs4 import BeautifulSoup import time import random import logging - +import json #INITS THE BOT bot = commands.Bot(command_prefix=PREFIX) ######################################### -# # -# # -# Setting up logging # -# # -# # +# # +# # +# Setting up logging # +# # +# # ######################################### #Creating main logger 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 @@ -39,11 +36,11 @@ ######################################### -# # -# # -# Extension Manipulation # -# # -# # +# # +# # +# Extension Manipulation # +# # +# # ######################################### @@ -51,85 +48,125 @@ @bot.group() @is_runner() async def ext(ctx): - if ctx.invoked_subcommand is None: - await ctx.send("NotEnoughArguments:\tYou must provide a subcommand") + 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 + + #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) + + 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 + + await ctx.send("Successfully added and loadded {}".format(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 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)) +@ext.command() +async def rm(ctx, extension:str): + 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 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)) + #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 - except Exception as e: - #logging any other possible issue - await ctx.send("UnexpectedError:\tReport issue to an admin") - raise e + with open(ENABLED_EXTENSIONS_FILE, "w") as file: + json.dump(enabled_exts, file) - await ctx.send("Successfully added and loadded {}".format(extension)) + 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}") +@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}") + + 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: - 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 + 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) - + bot.run(TOKEN) \ No newline at end of file diff --git a/settings.py b/settings.py index c625eb3..2c663d9 100644 --- a/settings.py +++ b/settings.py @@ -1,10 +1,12 @@ import logging + + ######################################### -# # -# # -# Global Variables # -# # -# # +# # +# # +# Global Variables # +# # +# # ######################################### @@ -12,47 +14,112 @@ TOKEN = "your_token" 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" + +#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", - "thumbsdown": "\U0001f44e", - "shrug": "\U0001f937", - "wastebasket": "\U0001F5D1", + "thumbsup": "\U0001f44d", + "thumbsdown": "\U0001f44e", + "shrug": "\U0001f937", + "wastebasket": "\U0001F5D1", "check": "\U00002705", "hourglass": "\U000023F3", - "wave": "\U0001F44B" + "wave": "\U0001F44B", + "no_entry_sign": "\U0001F6AB" } -#a set of channel names bound to their ids -CHANNELS = { - "rules": 566569408416186377, - "faq": 566618400307019776 -} +######################################### +# # +# # +# Files # +# # +# # +######################################### + +#Files +ENABLED_EXTENSIONS_FILE = "enabled_exts.json" +SLAPPING_FOLDER = "slapping" +CONFIG_FOLDER = "servers" +TODO_FOLDER = "todo" +#roles +ROLES_LEVEL = ["manager", "admin"] +#default JSON files +DEFAULT_EXTENSIONS_JSON = '''{ + "Slapping": false, + "BotEssentials":true, + "Role":false, + "Embedding":false, + "Config":false, + "Poll":false +}''' -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) +DEFAULT_SLAPPED_FILE = '''{ + "463665420054953995": 0 +}''' -PUBLIC_REPOST="Public repost" +DEFAULT_SERVER_FILE = '''{ + "poll_channels": [], + "todo_channel": false, + "roles": { + "manager": [], + "admin": [] + }, + "messages": { + "welcome": false, + "goodbye": false + }, + "advertisement": false -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" +}''' + +DEFAULT_TODO_FILE = '''{ + "groups": { + "default": [] + }, + "types": { + "default": "000000" + } +}''' + +######################################### +# # +# # +# Logging # +# # +# # +######################################### + +LOG_FILE = "forebot.log" +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 # +# # +# # +######################################### -#data used only for Todo -> maybe remove it ? -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 +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" +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 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 new file mode 100644 index 0000000..b180f49 --- /dev/null +++ b/utilities.py @@ -0,0 +1,202 @@ +import logging +import os +import json +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(): # 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 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 + + 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 + +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) + +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 + + return commands.check(predicate) + + +######################################### +# # +# # +# Utility functions # +# # +# # +######################################### + +def get_m_time(file): + 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 + +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 + +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 + + 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 + + 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] + + 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) + +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) + + try: + slaps = fl[f"{user_id}"] + except KeyError: + slaps = 0 + + except Exception as e: + raise e + local_logger.exception(e) + + 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) + + try: + fl[f"{user_id}"] = slaps + + 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 + +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: + 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