ソースを参照

WIP: Now using discordpy's Bot class

Lucas Villeneuve 5 年 前
コミット
936643a1a3

+ 3 - 2
myanimebot.py

@@ -20,9 +20,10 @@ from aiohttp.web_exceptions import HTTPError, HTTPNotModified
 from dateutil.parser import parse as parse_datetime
 from html2text import HTML2Text
 
+
 # Our modules
-import myanimebot.anilist as anilist
 import myanimebot.globals as globals
+import myanimebot.anilist as anilist
 import myanimebot.utils as utils
 import myanimebot.myanimelist as myanimelist
 import myanimebot.commands as commands
@@ -61,7 +62,7 @@ if __name__ == "__main__":
 	
 	# Run the app
 	try:
-		globals.client = MyAnimeBot()
+		globals.client = MyAnimeBot(cmd_prefix=globals.prefix)
 		globals.client.run(globals.token)
 	except Exception as e:
 		globals.logger.error("Encountered exception while running the bot: {}".format(e))

+ 1 - 2
myanimebot/__init__.py

@@ -2,5 +2,4 @@ from .utils import *
 from .anilist import *
 from .globals import *
 from .myanimelist import *
-from .discord import *
-from .commands import *
+from .discord import *

+ 0 - 414
myanimebot/commands.py

@@ -1,414 +0,0 @@
-import discord
-import urllib
-import datetime
-
-from typing import List, Tuple
-
-import myanimebot.utils as utils
-import myanimebot.globals as globals
-import myanimebot.anilist as anilist
-
-
-def build_info_cmd_message(users, server, channels, role, filters : List[utils.Service]) -> str:
-    ''' Build the corresponding message for the info command '''
-
-    registered_channel = globals.client.get_channel(int(channels[0]["channel"]))
-
-    # Store users
-    mal_users = []
-    anilist_users = []
-    for user in users:
-        # If user is part of the server, add it to the message
-        if str(server.id) in user['servers'].split(','):
-            try:
-                user_service = utils.Service.from_str(user["service"])
-                if user_service == utils.Service.MAL:
-                    mal_users.append(user[globals.DB_USER_NAME])
-                elif user_service == utils.Service.ANILIST:
-                    anilist_users.append(user[globals.DB_USER_NAME])
-            except NotImplementedError:
-                pass # Nothing to do here
-
-    if not mal_users and not anilist_users:
-        return "No users registered on this server. Try to add one."
-    else:
-        message =  'Registered user(s) on **{}**\n\n'.format(server)
-        if mal_users: # If not empty
-            # Don't print if there is filters and MAL is not in them
-            if not filters or (filters and utils.Service.MAL in filters): 
-                message += '**MyAnimeList** users:\n'
-                message += '```{}```\n'.format(', '.join(mal_users))
-        if anilist_users: # If not empty
-            # Don't print if there is filters and MAL is not in them
-            if not filters or (filters and utils.Service.ANILIST in filters):
-                message += '**AniList** users:\n'
-                message += '```{}```\n'.format(', '.join(anilist_users))
-        message += 'Assigned channel : **{}**'.format(registered_channel)
-        if role is not None:
-            message += '\nAllowed role: **{}**'.format(role)
-    return message
-
-
-def get_service_filters_list(filters : str) -> List[utils.Service]:
-    ''' Creates and returns a service filter list from a comma-separated string '''
-
-    filters_list = []
-    for filter in filters.split(','):
-        try:
-            filters_list.append(utils.Service.from_str(filter))
-        except NotImplementedError:
-            pass # Ignore incorrect filter
-    return filters_list
-
-
-def in_allowed_role(user : discord.Member, server : int) -> bool :
-    ''' Check if a user has the permissions to configure the bot on a specific server '''
-
-    targetRole = utils.get_allowed_role(server.id)
-    globals.logger.debug ("Role target: " + str(targetRole))
-
-    if user.guild_permissions.administrator:
-        globals.logger.debug (str(user) + " is server admin on " + str(server) + "!")
-        return True
-    elif (targetRole is None):
-        globals.logger.debug ("No group specified for " + str(server))
-        return True
-    else:
-        for role in user.roles:
-            if str(role.id) == str(targetRole):
-                globals.logger.debug ("Permissions validated for " + str(user))
-                return True
-
-    return False
-
-
-def check_user_name_validity(user_name: str, service : utils.Service) -> Tuple[bool, str]:
-    """ Check if user_name exists on a specific service.
-        
-        Returns:
-            - bool: 	True if user_name exists
-            - str:		Error string if the user does not exist
-    """
-
-    if service == utils.Service.MAL:
-        try:
-            # Ping user profile to check validity
-            urllib.request.urlopen('{}{}'.format(globals.MAL_PROFILE_URL, user_name))
-        except urllib.error.HTTPError as e:
-            if (e.code == 404): # URL profile not found
-                return False, "User **{}** doesn't exist on MyAnimeList!".format(user_name)
-            else:
-                globals.logger.warning("HTTP Code {} while trying to add user '{}' and checking its validity.".format(e.code, user_name))
-                return False, "An error occured when we checked this username on MyAnimeList, maybe the website is down?"
-    elif service == utils.Service.ANILIST:
-        is_user_valid = anilist.check_username_validity(user_name)
-        if is_user_valid == False:
-            globals.logger.warning("No results returned while trying to add user '{}' and checking its validity.".format(user_name))
-            return False, "User **{}** doesn't exist on AniList!".format(user_name)
-    return True, None
-
-
-async def add_user_cmd(words, message):
-    ''' Processes the command "add" and add a user to fetch the data for '''
-
-    # Check if command is valid
-    if len(words) != 4:
-        if (len(words) < 4):
-            return await message.channel.send("Usage: {} add **{}**/**{}** **username**".format(globals.prefix, globals.SERVICE_MAL, globals.SERVICE_ANILIST))
-        return await message.channel.send("Too many arguments! You have to specify only one username.")
-
-    # Verify that the user is allowed
-    if in_allowed_role(message.author, message.guild) is False:
-        return await message.channel.send("Only allowed users can use this command!")
-
-    try:
-        service = utils.Service.from_str(words[2])
-    except NotImplementedError:
-        return await message.channel.send('Incorrect service. Use **"{}"** or **"{}"** for example'.format(globals.SERVICE_MAL, globals.SERVICE_ANILIST))
-    user = words[3]
-    server_id = str(message.guild.id)
-
-    if(len(user) > 14):
-        return await message.channel.send("Username too long!")
-
-    try:
-        # Check user validity
-        is_valid, error_string = check_user_name_validity(user, service)
-        if is_valid == False:
-            return await message.channel.send(error_string)
-
-        # Get user's servers
-        user_servers = utils.get_user_servers(user, service)
-        # User not present in database
-        if user_servers is None: 
-            utils.insert_user_into_db(user, service, server_id)
-            return await message.channel.send("**{}** added to the database for the server **{}**.".format(user, str(message.guild)))
-        else: # User present in database
-
-            is_server_present = server_id in user_servers.split(',')
-            if is_server_present == True: # The user already has registered this server
-                return await message.channel.send("User **{}** is already registered in our database for this server!".format(user))
-            else:
-                new_servers = '{},{}'.format(user_servers, server_id)
-                utils.update_user_servers_db(user, service, new_servers)					
-                return await message.channel.send("**{}** added to the database for the server **{}**.".format(user, str(message.guild)))
-    except Exception as e:
-        globals.logger.warning("Error while adding user '{}' on server '{}': {}".format(user, message.guild, str(e)))
-        return await message.channel.send("An unknown error occured while addind this user, the error has been logged.")
-
-
-async def delete_user_cmd(words, message):
-    ''' Processes the command "delete" and remove a registered user '''
-
-    # Check if command is valid
-    if len(words) != 4:
-        if (len(words) < 4):
-            return await message.channel.send("Usage: {} delete **{}**/**{}** **username**".format(globals.prefix, globals.SERVICE_MAL, globals.SERVICE_ANILIST))
-        return await message.channel.send("Too many arguments! You have to specify only one username.")
-
-    # Verify that the user is allowed
-    if in_allowed_role(message.author, message.guild) is False:
-        return await message.channel.send("Only allowed users can use this command!")
-
-    try:
-        service = utils.Service.from_str(words[2])
-    except NotImplementedError:
-        return await message.channel.send('Incorrect service. Use **"{}"** or **"{}"** for example'.format(globals.SERVICE_MAL, globals.SERVICE_ANILIST))
-    user = words[3]
-    server_id = str(message.guild.id)
-    
-    user_servers = utils.get_user_servers(user, service)
-    # If user is not present in the database
-    if user_servers is None:
-        return await message.channel.send("The user **" + user + "** is not in our database for this server!")
-
-    # Else if present, update the servers for this user
-    srv_string = utils.remove_server_from_servers(server_id, user_servers)
-    
-    if srv_string is None: # Server not present in the user's servers
-        return await message.channel.send("The user **" + user + "** is not in our database for this server!")
-
-    if srv_string == "":
-        utils.delete_user_from_db(user, service)
-    else:
-        utils.update_user_servers_db(user, service, srv_string)
-
-    return await message.channel.send("**" + user + "** deleted from the database for this server.")
-
-
-async def info_cmd(message, words):
-    ''' Processes the command "info" and sends a message '''
-
-    # Get filters if available
-    filters = []
-    if (len(words) >= 3): # If filters are specified
-        filters = get_service_filters_list(words[2])
-
-    server = message.guild
-    if utils.is_server_in_db(server.id) == False:
-         await message.channel.send("The server **{}** is not in our database.".format(server))
-    else:
-        users = utils.get_users()
-        channels = utils.get_channels(server.id)
-        role = utils.get_allowed_role(server.id)
-        if channels is None:
-            await message.channel.send("No channel assigned for this bot on this server.")
-        else:
-            await message.channel.send(build_info_cmd_message(users, server, channels, utils.get_role_name(role, server), filters))
-
-
-async def ping_cmd(message, channel):
-    ''' Responds to ping command '''
-    messageTimestamp = message.created_at
-    currentTimestamp = datetime.datetime.utcnow()
-    delta = round((currentTimestamp - messageTimestamp).total_seconds() * 1000)
-
-    await message.reply("pong ({}ms)".format(delta))
-
-
-async def about_cmd(channel):
-    ''' Responds to about command with a brief description of this bot '''
-
-    embed = discord.Embed(title="***MyAnimeBot Commands***", colour=0xEED000)
-    embed.title = "MyAnimeBot version {} by Penta & lulu".format(globals.VERSION)
-    embed.colour = 0xEED000
-    embed.description = """MyAnimeBot checks MyAnimeList and Anilist profiles for specified users, and send a message for every new activities found.
-        More help with the **{} help** command.
-        
-        Check our GitHub page for more informations: https://github.com/Penta/MyAnimeBot
-        """.format(globals.prefix)
-    embed.set_thumbnail(url=globals.iconBot)
-
-    await channel.send(embed=embed)
-
-
-async def help_cmd(channel):
-    ''' Responds to help command '''
-
-    embed = discord.Embed(title="***MyAnimeBot Commands***", colour=0xEED000)
-    embed.add_field(name="`here`", value="Register this channel. The bot will send new activities on registered channels.")
-    embed.add_field(name="`stop`", value="Un-register this channel. The bot will now stop sending new activities for this channel.")
-    embed.add_field(name="`info [mal|ani]`", value="Get the registered users for this server. Users can be filtered by specifying a service.")
-    embed.add_field(name="`add {mal|ani} <user>`", value="Register a user for a specific service.\nEx: `add mal MyUser`")
-    embed.add_field(name="`delete {mal|ani} <user>`", value="Remove a user for a specific service.\nEx: `delete ani MyUser`")
-    embed.add_field(name="`role <@discord_role>`", value="Specify a role that is able to manage the bot.\nEx: `role @Moderator`, `role @everyone`")
-    embed.add_field(name="`top`", value="Show statistics for this server.")
-    embed.add_field(name="`ping`", value="Ping the bot.")
-    embed.add_field(name="`about`", value="Get some information about this bot")
-    await channel.send(embed=embed)
-
-
-async def here_cmd(author, server, channel):
-    ''' Processes the command "here" and registers a channel to send new found feeds '''
-
-    # Verify that the user is allowed
-    if in_allowed_role(author, server) is False:
-        return await channel.send("Only allowed users can use this command!")
-    
-    if utils.is_server_in_db(server.id):
-        # Server in DB, so we need to update the channel
-
-        # Check if channel already registered
-        channels = utils.get_channels(server.id)
-        channels_id = [channel["channel"] for channel in channels]
-        globals.logger.debug("Channels {} and channel id {}".format(channels_id, channel.id))
-        if (str(channel.id) in channels_id):
-            await channel.send("Channel **{}** already in use for this server.".format(channel))
-        else:
-            cursor = globals.conn.cursor(buffered=True)
-            cursor.execute("UPDATE t_servers SET channel = {} WHERE server = {}".format(channel.id, server.id))
-            globals.conn.commit()
-            
-            await channel.send("Channel updated to: **{}**.".format(channel))
-            
-        cursor.close()
-    else:
-        # No server found in DB, so register it
-        cursor = globals.conn.cursor(buffered=True)
-        cursor.execute("INSERT INTO t_servers (server, channel) VALUES ({},{})".format(server.id, channel.id))
-        globals.conn.commit() # TODO Move to corresponding file
-        
-        await channel.send("Channel **{}** configured for **{}**.".format(channel, server))
-
-
-async def stop_cmd(author, server, channel):
-    ''' Processes the command "stop" and unregisters a channel '''
-
-    # Verify that the user is allowed
-    if in_allowed_role(author, server) is False:
-        return await channel.send("Only allowed users can use this command!")
-
-    if utils.is_server_in_db(server.id):
-        # Remove server from DB
-        cursor = globals.conn.cursor(buffered=True)
-        cursor.execute("DELETE FROM t_servers WHERE server = {}".format(server.id))
-        globals.conn.commit()
-
-        await channel.send("Server **{}** is now unregistered from our database.".format(server))
-    else:
-        await channel.send("Server **{}** was already not registered.".format(server))
-
-
-async def role_cmd(words, message, author, server, channel):
-    ''' Processes the command "role" and registers a role to be able to use the bot's commands '''
-
-    if len(words) <= 2:
-        return await channel.send("A role must be specified.")
-
-    if not author.guild_permissions.administrator:
-        return await channel.send("Only server's admins can use this command.")
-
-
-    role_str = words[2]
-    if (role_str == "everyone") or (role_str == "@everyone"):
-        cursor = globals.conn.cursor(buffered=True)
-        cursor.execute("UPDATE t_servers SET admin_group = NULL WHERE server = %s", [str(server.id)])
-        globals.conn.commit()
-        cursor.close()
-
-        await channel.send("Everyone is now allowed to use the bot.")
-    else: # A role is found
-        rolesFound = message.role_mentions
-
-        if (len(rolesFound) == 0):
-            return await channel.send("Please specify a correct role.")
-        elif (len(rolesFound) > 1):
-            return await channel.send("Please specify only 1 role.")
-        else:
-            roleFound = rolesFound[0]
-            # Update db with newly added role
-            cursor = globals.conn.cursor(buffered=True)
-            cursor.execute("UPDATE t_servers SET admin_group = %s WHERE server = %s", [str(roleFound.id), str(server.id)])
-            globals.conn.commit()
-            cursor.close()
-
-            await channel.send("The role **{}** is now allowed to use this bot!".format(roleFound.name))
-
-
-async def top_cmd(words, channel):
-    ''' Processes the command "top" and returns statistics on registered feeds '''
-
-    # TODO Redo this function
-
-    if len(words) == 2:
-        try:
-            cursor = globals.conn.cursor(buffered=True)
-            cursor.execute("SELECT * FROM v_Top")
-            data = cursor.fetchone()
-            
-            if data is None: await message.channel.send("It seems that there is no statistics... (what happened?!)")
-            else:
-                topText = "**__Here is the global statistics of this bot:__**\n\n"
-                
-                while data is not None:
-                    topText += " - " + str(data[0]) + ": " + str(data[1]) + "\n"
-                        
-                    data = cursor.fetchone()
-                    
-                cursor = globals.conn.cursor(buffered=True)
-                cursor.execute("SELECT * FROM v_TotalFeeds")
-                data = cursor.fetchone()
-                
-                topText += "\n***Total user entry***: " + str(data[0])
-                
-                cursor = globals.conn.cursor(buffered=True)
-                cursor.execute("SELECT * FROM v_TotalAnimes")
-                data = cursor.fetchone()
-                
-                topText += "\n***Total unique manga/anime***: " + str(data[0])
-                
-                await channel.send(topText)
-            
-            cursor.close()
-        except Exception as e:
-            globals.logger.warning("An error occured while displaying the global top: " + str(e))
-            await channel.send("Unable to reply to your request at the moment...")
-    elif len(words) > 2:
-        keyword = str(' '.join(words[2:]))
-        globals.logger.info("Displaying the global top for the keyword: " + keyword)
-        
-        try:
-            cursor = globals.conn.cursor(buffered=True)
-            cursor.callproc('sp_UsersPerKeyword', [str(keyword), '20'])
-            for result in cursor.stored_results():
-                data = result.fetchone()
-                
-                if data is None: await message.channel.send("It seems that there is no statistics for the keyword **" + keyword + "**.")
-                else:
-                    topKeyText = "**__Here is the statistics for the keyword " + keyword + ":__**\n\n"
-                    
-                    while data is not None:
-                        topKeyText += " - " + str(data[0]) + ": " + str(data[1]) + "\n"
-                            
-                        data = result.fetchone()
-                        
-                    await channel.send(topKeyText)
-                
-            cursor.close()
-        except Exception as e:
-            globals.logger.warning("An error occured while displaying the global top for keyword '" + keyword + "': " + str(e))
-            await channel.send("Unable to reply to your request at the moment...")
-
-
-async def on_mention(channel):
-    return await channel.send(":heart:")

+ 10 - 0
myanimebot/commands/__init__.py

@@ -0,0 +1,10 @@
+from myanimebot.commands.about import about_cmd
+from myanimebot.commands.add_user import add_user_cmd
+from myanimebot.commands.delete_user import delete_user_cmd
+from myanimebot.commands.help import help_cmd
+from myanimebot.commands.here import here_cmd
+from myanimebot.commands.info import info_cmd
+from myanimebot.commands.ping import ping_cmd
+from myanimebot.commands.role import role_cmd
+from myanimebot.commands.stop import stop_cmd
+from myanimebot.commands.top import top_cmd

+ 21 - 0
myanimebot/commands/about.py

@@ -0,0 +1,21 @@
+import discord
+import discord.ext.commands as discord_cmds
+
+import myanimebot.globals as globals
+
+
+@discord_cmds.command(name="about")
+async def about_cmd(ctx):
+    ''' Responds to about command with a brief description of this bot '''
+
+    embed = discord.Embed(title="***MyAnimeBot Commands***", colour=0xEED000)
+    embed.title = "MyAnimeBot version {} by Penta & lulu".format(globals.VERSION)
+    embed.colour = 0xEED000
+    embed.description = """MyAnimeBot checks MyAnimeList and Anilist profiles for specified users, and send a message for every new activities found.
+        More help with the **{} help** command.
+        
+        Check our GitHub page for more informations: https://github.com/Penta/MyAnimeBot
+        """.format(globals.prefix)
+    embed.set_thumbnail(url=globals.iconBot)
+
+    await ctx.send(embed=embed)

+ 88 - 0
myanimebot/commands/add_user.py

@@ -0,0 +1,88 @@
+import urllib
+import discord.ext.commands as discord_cmds
+
+from typing import Tuple
+
+import myanimebot.utils as utils
+import myanimebot.globals as globals
+import myanimebot.anilist as anilist
+import myanimebot.commands.permissions as permissions
+import myanimebot.commands.converters as converters
+
+
+def check_user_name_validity(user_name: str, service : utils.Service) -> Tuple[bool, str]:
+    """ Check if user_name exists on a specific service.
+        
+        Returns:
+            - bool: 	True if user_name exists
+            - str:		Error string if the user does not exist
+    """
+
+    if(len(user_name) > globals.USERNAME_MAX_LENGTH):
+        return False, "Username too long!"
+
+    if service == utils.Service.MAL:
+        try:
+            # Ping user profile to check validity
+            urllib.request.urlopen('{}{}'.format(globals.MAL_PROFILE_URL, user_name))
+        except urllib.error.HTTPError as e:
+            if (e.code == 404): # URL profile not found
+                return False, "User **{}** doesn't exist on MyAnimeList!".format(user_name)
+            else:
+                globals.logger.warning("HTTP Code {} while trying to add user '{}' and checking its validity.".format(e.code, user_name))
+                return False, "An error occured when we checked this username on MyAnimeList, maybe the website is down?"
+    elif service == utils.Service.ANILIST:
+        is_user_valid = anilist.check_username_validity(user_name)
+        if is_user_valid == False:
+            globals.logger.warning("No results returned while trying to add user '{}' and checking its validity.".format(user_name))
+            return False, "User **{}** doesn't exist on AniList!".format(user_name)
+    return True, None
+
+
+@discord_cmds.command(name="add")
+@discord_cmds.check(permissions.in_allowed_role)
+async def add_user_cmd(ctx, service : converters.to_service, user : str):
+    ''' Processes the command "add" and add a user to fetch the data for '''
+
+    if user is None:
+        return await ctx.send("Usage: {} add **{}**/**{}** **username**".format(globals.prefix, globals.SERVICE_MAL, globals.SERVICE_ANILIST))
+    elif service is None:
+        return await ctx.send('Incorrect service. Use **"{}"** or **"{}"** for example'.format(globals.SERVICE_MAL, globals.SERVICE_ANILIST))
+
+    server = ctx.guild
+    server_id = str(server.id)
+
+    try:
+        # Check user validity
+        is_valid, error_string = check_user_name_validity(user, service)
+        if is_valid == False:
+            return await ctx.send(error_string)
+
+        # Get user's servers
+        user_servers = utils.get_user_servers(user, service)
+        # User not present in database
+        if user_servers is None: 
+            utils.insert_user_into_db(user, service, server_id)
+            return await ctx.send("**{}** added to the database for the server **{}**.".format(user, server))
+        else: # User present in database
+
+            is_server_present = server_id in user_servers.split(',')
+            if is_server_present == True: # The user already has registered this server
+                return await ctx.send("User **{}** is already registered in our database for this server!".format(user))
+            else:
+                new_servers = '{},{}'.format(user_servers, server_id)
+                utils.update_user_servers_db(user, service, new_servers)					
+                return await ctx.send("**{}** added to the database for the server **{}**.".format(user, server))
+    except Exception as e:
+        globals.logger.warning("Error while adding user '{}' on server '{}': {}".format(user, server, str(e)))
+        return await ctx.send("An unknown error occured while addind this user, the error has been logged.")
+
+
+@add_user_cmd.error
+async def add_user_cmd_error(ctx, error):
+    ''' Processes errors from add cmd '''
+    
+    if isinstance(error, discord_cmds.MissingRequiredArgument):
+        return await ctx.send("Usage: {} add **{}**/**{}** **username**".format(globals.prefix, globals.SERVICE_MAL, globals.SERVICE_ANILIST))
+    elif isinstance(error, discord_cmds.ConversionError):
+        return await ctx.send('Incorrect service {}. Use **"{}"** or **"{}"** for example'.format(error.original, globals.SERVICE_MAL, globals.SERVICE_ANILIST))

+ 9 - 0
myanimebot/commands/converters.py

@@ -0,0 +1,9 @@
+import discord.ext.commands as discord_cmds
+
+import myanimebot.utils as utils
+
+def to_service(service : str) -> utils.Service:
+    try:
+        return utils.Service.from_str(service)
+    except NotImplementedError:
+        raise discord_cmds.ConversionError(to_service, service)

+ 43 - 0
myanimebot/commands/delete_user.py

@@ -0,0 +1,43 @@
+import discord.ext.commands as discord_cmds
+
+import myanimebot.utils as utils
+import myanimebot.globals as globals
+import myanimebot.commands.permissions as permissions
+import myanimebot.commands.converters as converters
+
+
+@discord_cmds.command(name="delete")
+@discord_cmds.check(permissions.in_allowed_role)
+async def delete_user_cmd(ctx, service : converters.to_service, user : str):
+    ''' Processes the command "delete" and remove a registered user '''
+
+    server = ctx.guild
+    server_id = str(server.id)
+    
+    user_servers = utils.get_user_servers(user, service)
+    # If user is not present in the database
+    if user_servers is None:
+        return await ctx.send("The user **" + user + "** is not in our database for this server!")
+
+    # Else if present, update the servers for this user
+    srv_string = utils.remove_server_from_servers(server_id, user_servers)
+    
+    if srv_string is None: # Server not present in the user's servers
+        return await ctx.send("The user **" + user + "** is not in our database for this server!")
+
+    if srv_string == "":
+        utils.delete_user_from_db(user, service)
+    else:
+        utils.update_user_servers_db(user, service, srv_string)
+
+    return await ctx.send("**{}** deleted from the database for this server.".format(user))
+
+
+@delete_user_cmd.error
+async def delete_user_cmd_error(ctx, error):
+    ''' Processes errors from delete cmd '''
+
+    if isinstance(error, discord_cmds.MissingRequiredArgument):
+        return await ctx.send("Usage: {} add **{}**/**{}** **username**".format(globals.prefix, globals.SERVICE_MAL, globals.SERVICE_ANILIST))
+    elif isinstance(error, discord_cmds.ConversionError):
+        return await ctx.send('Incorrect service {}. Use **"{}"** or **"{}"** for example'.format(error.original, globals.SERVICE_MAL, globals.SERVICE_ANILIST))

+ 19 - 0
myanimebot/commands/help.py

@@ -0,0 +1,19 @@
+import discord
+import discord.ext.commands as discord_cmds
+
+
+@discord_cmds.command(name="help")
+async def help_cmd(ctx):
+    ''' Responds to help command '''
+
+    embed = discord.Embed(title="***MyAnimeBot Commands***", colour=0xEED000)
+    embed.add_field(name="`here`", value="Register this channel. The bot will send new activities on registered channels.")
+    embed.add_field(name="`stop`", value="Un-register this channel. The bot will now stop sending new activities for this channel.")
+    embed.add_field(name="`info [mal|ani]`", value="Get the registered users for this server. Users can be filtered by specifying a service.")
+    embed.add_field(name="`add {mal|ani} <user>`", value="Register a user for a specific service.\nEx: `add mal MyUser`")
+    embed.add_field(name="`delete {mal|ani} <user>`", value="Remove a user for a specific service.\nEx: `delete ani MyUser`")
+    embed.add_field(name="`role <@discord_role>`", value="Specify a role that is able to manage the bot.\nEx: `role @Moderator`, `role @everyone`")
+    embed.add_field(name="`top`", value="Show statistics for this server.")
+    embed.add_field(name="`ping`", value="Ping the bot.")
+    embed.add_field(name="`about`", value="Get some information about this bot")
+    await ctx.send(embed=embed)

+ 39 - 0
myanimebot/commands/here.py

@@ -0,0 +1,39 @@
+import discord.ext.commands as discord_cmds
+
+
+import myanimebot.utils as utils
+import myanimebot.globals as globals
+import myanimebot.commands.permissions as permissions
+
+
+@discord_cmds.command(name="here")
+@discord_cmds.check(permissions.in_allowed_role)
+async def here_cmd(ctx):
+    ''' Processes the command "here" and registers a channel to send new found feeds '''
+
+    server = ctx.guild
+    channel = ctx.channel
+    
+    if utils.is_server_in_db(server.id):
+        # Server in DB, so we need to update the channel
+
+        # Check if channel already registered
+        channels = utils.get_channels(server.id)
+        channels_id = [channel["channel"] for channel in channels]
+        globals.logger.debug("Channels {} and channel id {}".format(channels_id, channel.id))
+        if (str(channel.id) in channels_id):
+            await ctx.send("Channel **{}** already in use for this server.".format(channel))
+        else:
+            cursor = globals.conn.cursor(buffered=True)
+            cursor.execute("UPDATE t_servers SET channel = {} WHERE server = {}".format(channel.id, server.id))
+            globals.conn.commit()
+            cursor.close()
+            
+            await ctx.send("Channel updated to: **{}**.".format(channel))
+    else:
+        # No server found in DB, so register it
+        cursor = globals.conn.cursor(buffered=True)
+        cursor.execute("INSERT INTO t_servers (server, channel) VALUES ({},{})".format(server.id, channel.id))
+        globals.conn.commit() # TODO Move to corresponding file
+        
+        await ctx.send("Channel **{}** configured for **{}**.".format(channel, server))

+ 88 - 0
myanimebot/commands/info.py

@@ -0,0 +1,88 @@
+import discord.ext.commands as discord_cmds
+
+from typing import List, Optional
+
+import myanimebot.utils as utils
+import myanimebot.globals as globals
+import myanimebot.commands.converters as converters
+
+
+def build_info_cmd_message(users, server, channels, role, filters : List[utils.Service]) -> str:
+    ''' Build the corresponding message for the info command '''
+
+    registered_channel = globals.client.get_channel(int(channels[0]["channel"]))
+
+    # Store users
+    mal_users = []
+    anilist_users = []
+    for user in users:
+        # If user is part of the server, add it to the message
+        if str(server.id) in user['servers'].split(','):
+            try:
+                user_service = utils.Service.from_str(user["service"])
+                if user_service == utils.Service.MAL:
+                    mal_users.append(user[globals.DB_USER_NAME])
+                elif user_service == utils.Service.ANILIST:
+                    anilist_users.append(user[globals.DB_USER_NAME])
+            except NotImplementedError:
+                pass # Nothing to do here
+
+    if not mal_users and not anilist_users:
+        return "No users registered on this server. Try to add one."
+    else:
+        message =  'Registered user(s) on **{}**\n\n'.format(server)
+        if mal_users: # If not empty
+            # Don't print if there is filters and MAL is not in them
+            if not filters or (filters and utils.Service.MAL in filters): 
+                message += '**MyAnimeList** users:\n'
+                message += '```{}```\n'.format(', '.join(mal_users))
+        if anilist_users: # If not empty
+            # Don't print if there is filters and MAL is not in them
+            if not filters or (filters and utils.Service.ANILIST in filters):
+                message += '**AniList** users:\n'
+                message += '```{}```\n'.format(', '.join(anilist_users))
+        message += 'Assigned channel : **{}**'.format(registered_channel)
+        if role is not None:
+            message += '\nAllowed role: **{}**'.format(role)
+    return message
+
+
+def get_service_filters_list(filters : str) -> List[utils.Service]:
+    ''' Creates and returns a service filter list from a comma-separated string '''
+
+    if filters is None: return []
+
+    filters_list = []
+    for filter in filters.split(','):
+        try:
+            filters_list.append(utils.Service.from_str(filter))
+        except NotImplementedError:
+            pass # Ignore incorrect filter
+    return filters_list
+
+
+@discord_cmds.command(name="info")
+async def info_cmd(ctx, filters : Optional[get_service_filters_list]):
+    ''' Processes the command "info" and sends a message '''
+
+    server = ctx.guild
+    if utils.is_server_in_db(server.id) == False:
+         await ctx.send("The server **{}** is not in our database.".format(server))
+    else:
+        users = utils.get_users()
+        channels = utils.get_channels(server.id)
+        role = utils.get_allowed_role(server.id)
+        if channels is None:
+            await ctx.send("No channel assigned for this bot on this server.")
+        else:
+            await ctx.send(build_info_cmd_message(users, server, channels, utils.get_role_name(role, server), filters))
+
+
+@info_cmd.error
+async def info_cmd_error(ctx, error):
+    ''' Processes errors from info cmd '''
+
+    # Should not happen
+    if isinstance(error, discord_cmds.ConversionError):
+        globals.logger.error('[info_cmd] An error occured when trying to convert {} to List[Service] filters: {}'.format(error.original, error))
+

+ 38 - 0
myanimebot/commands/permissions.py

@@ -0,0 +1,38 @@
+import discord
+import myanimebot.utils as utils
+import myanimebot.globals as globals
+
+
+async def in_allowed_role(ctx) -> bool :
+    ''' Checks if a user has the permissions to configure the bot on a specific server '''
+
+    user = ctx.author
+    server = ctx.guild
+
+    targetRole = utils.get_allowed_role(server.id)
+    globals.logger.debug ("Role target: " + str(targetRole))
+
+    if user.guild_permissions.administrator:
+        globals.logger.debug (str(user) + " is server admin on " + str(server) + "!")
+        return True
+    elif (targetRole is None):
+        globals.logger.debug ("No role specified for " + str(server))
+        return True
+    else:
+        for role in user.roles:
+            if str(role.id) == str(targetRole):
+                globals.logger.debug ("Permissions validated for " + str(user))
+                return True
+
+    await ctx.send("Only allowed users can use this command!")
+    return False
+
+
+async def is_administrator(ctx) -> bool :
+    ''' Checks if a user is a server's adminitrator '''
+
+    if ctx.author.guild_permissions.administrator:
+        return True
+    
+    await ctx.send("Only server's admins can use this command.")
+    return False

+ 14 - 0
myanimebot/commands/ping.py

@@ -0,0 +1,14 @@
+import datetime
+
+import discord.ext.commands as discord_cmds
+
+
+@discord_cmds.command(name="ping")
+async def ping_cmd(ctx):
+    ''' Responds to ping command '''
+    messageTimestamp = ctx.message.created_at
+    currentTimestamp = datetime.datetime.utcnow()
+    delta = round((currentTimestamp - messageTimestamp).total_seconds() * 1000)
+
+    print("Sending pong")
+    await ctx.reply("pong ({}ms)".format(delta))

+ 47 - 0
myanimebot/commands/role.py

@@ -0,0 +1,47 @@
+import discord.ext.commands as discord_cmds
+
+
+import myanimebot.globals as globals
+import myanimebot.commands.permissions as permissions
+
+
+@discord_cmds.command(name="role")
+@discord_cmds.check(permissions.is_administrator)
+async def role_cmd(ctx, role : str):
+    ''' Processes the command "role" and registers a role to be able to use the bot's commands '''
+
+    server = ctx.guild
+    message = ctx.message
+    
+
+    if (role == "everyone") or (role == "@everyone"):
+        cursor = globals.conn.cursor(buffered=True)
+        cursor.execute("UPDATE t_servers SET admin_group = NULL WHERE server = %s", [str(server.id)])
+        globals.conn.commit()
+        cursor.close()
+
+        await ctx.send("Everyone is now allowed to use the bot.")
+    else: # A role is found
+        rolesFound = message.role_mentions
+
+        if (len(rolesFound) == 0):
+            return await ctx.send("Please specify a correct role.")
+        elif (len(rolesFound) > 1):
+            return await ctx.send("Please specify only 1 role.")
+        else:
+            roleFound = rolesFound[0]
+            # Update db with newly added role
+            cursor = globals.conn.cursor(buffered=True)
+            cursor.execute("UPDATE t_servers SET admin_group = %s WHERE server = %s", [str(roleFound.id), str(server.id)])
+            globals.conn.commit()
+            cursor.close()
+
+            await ctx.send("The role **{}** is now allowed to use this bot!".format(roleFound.name))
+
+
+@role_cmd.error
+async def role_cmd_error(ctx, error):
+    ''' Processes errors from role cmd '''
+
+    if isinstance(error, discord_cmds.MissingRequiredArgument):
+        return await ctx.send("Usage: {} add **{}**/**{}** **username**".format(globals.prefix, globals.SERVICE_MAL, globals.SERVICE_ANILIST))

+ 24 - 0
myanimebot/commands/stop.py

@@ -0,0 +1,24 @@
+import discord.ext.commands as discord_cmds
+
+
+import myanimebot.utils as utils
+import myanimebot.globals as globals
+import myanimebot.commands.permissions as permissions
+
+
+@discord_cmds.command(name="stop")
+@discord_cmds.check(permissions.in_allowed_role)
+async def stop_cmd(ctx):
+    ''' Processes the command "stop" and unregisters a channel '''
+
+    server = ctx.guild
+
+    if utils.is_server_in_db(server.id):
+        # Remove server from DB
+        cursor = globals.conn.cursor(buffered=True)
+        cursor.execute("DELETE FROM t_servers WHERE server = {}".format(server.id))
+        globals.conn.commit()
+
+        await ctx.send("Server **{}** is now unregistered from our database.".format(server))
+    else:
+        await ctx.send("Server **{}** was already not registered.".format(server))

+ 67 - 0
myanimebot/commands/top.py

@@ -0,0 +1,67 @@
+import discord.ext.commands as discord_cmds
+import myanimebot.globals as globals
+
+
+@discord_cmds.command(name="top")
+async def top_cmd(ctx, *, keyword):
+    ''' Processes the command "top" and returns statistics on registered feeds '''
+
+    # TODO Redo this function
+
+    if keyword is None:
+        try:
+            cursor = globals.conn.cursor(buffered=True)
+            cursor.execute("SELECT * FROM v_Top")
+            data = cursor.fetchone()
+            
+            if data is None: await ctx.send("It seems that there is no statistics... (what happened?!)")
+            else:
+                topText = "**__Here is the global statistics of this bot:__**\n\n"
+                
+                while data is not None:
+                    topText += " - " + str(data[0]) + ": " + str(data[1]) + "\n"
+                        
+                    data = cursor.fetchone()
+                    
+                cursor = globals.conn.cursor(buffered=True)
+                cursor.execute("SELECT * FROM v_TotalFeeds")
+                data = cursor.fetchone()
+                
+                topText += "\n***Total user entry***: " + str(data[0])
+                
+                cursor = globals.conn.cursor(buffered=True)
+                cursor.execute("SELECT * FROM v_TotalAnimes")
+                data = cursor.fetchone()
+                
+                topText += "\n***Total unique manga/anime***: " + str(data[0])
+                
+                await ctx.send(topText)
+            
+            cursor.close()
+        except Exception as e:
+            globals.logger.warning("An error occured while displaying the global top: " + str(e))
+            await ctx.send("Unable to reply to your request at the moment...")
+    else:
+        globals.logger.info("Displaying the global top for the keyword: " + keyword)
+        
+        try:
+            cursor = globals.conn.cursor(buffered=True)
+            cursor.callproc('sp_UsersPerKeyword', [str(keyword), '20'])
+            for result in cursor.stored_results():
+                data = result.fetchone()
+                
+                if data is None: await ctx.send("It seems that there is no statistics for the keyword **" + keyword + "**.")
+                else:
+                    topKeyText = "**__Here is the statistics for the keyword " + keyword + ":__**\n\n"
+                    
+                    while data is not None:
+                        topKeyText += " - " + str(data[0]) + ": " + str(data[1]) + "\n"
+                            
+                        data = result.fetchone()
+                        
+                    await ctx.send(topKeyText)
+                
+            cursor.close()
+        except Exception as e:
+            globals.logger.warning("An error occured while displaying the global top for keyword '" + keyword + "': " + str(e))
+            await ctx.send("Unable to reply to your request at the moment...")

+ 48 - 51
myanimebot/discord.py

@@ -1,8 +1,6 @@
 import asyncio
 import urllib.request
-from configparser import ConfigParser
 from datetime import datetime
-from typing import List, Tuple
 
 import aiohttp
 import feedparser
@@ -10,20 +8,26 @@ import pytz
 from dateutil.parser import parse as parse_datetime
 
 import discord
+import discord.ext.commands
 
 
 # Our modules
 import myanimebot.anilist as anilist
 import myanimebot.healthcheck as healthcheck
-import myanimebot.commands as commands
 import myanimebot.globals as globals  # TODO Rename globals module
 import myanimebot.myanimelist as myanimelist
 import myanimebot.utils as utils
+import myanimebot.commands as commands
+
 
+class MyAnimeBot(discord.ext.commands.Bot):
+    def __init__(self, cmd_prefix, **options):
+        discord.ext.commands.Bot.__init__(self, command_prefix=cmd_prefix, help_command=None, **options)
+
+        self.add_commands()
 
-class MyAnimeBot(discord.Client):
     async def on_ready(self):
-        globals.logger.info("Logged in as " + globals.client.user.name + " (" + str(globals.client.user.id) + ")")
+        globals.logger.info("Logged in as {} ({})".format(self.user.name, self.user.id))
 
         globals.logger.info("Starting all tasks...")
 
@@ -32,63 +36,56 @@ class MyAnimeBot(discord.Client):
 
         if globals.ANI_ENABLED:
             globals.task_feed_anilist = globals.client.loop.create_task(anilist.background_check_feed(globals.client.loop))
-		
+
         if globals.HEALTHCHECK_ENABLED:
             globals.task_healthcheck = globals.client.loop.create_task(healthcheck.main(globals.client.loop))
 
-        globals.task_thumbnail = globals.client.loop.create_task(update_thumbnail_catalog(globals.client.loop))
-        globals.task_gameplayed = globals.client.loop.create_task(change_gameplayed(globals.client.loop))
+        globals.task_thumbnail = self.loop.create_task(update_thumbnail_catalog(self.loop))
+        globals.task_gameplayed = self.loop.create_task(change_gameplayed(self.loop))
 
     async def on_error(self, event, *args, **kwargs):
         globals.logger.exception("Crap! An unknown Discord error occured...")
 
-    async def on_message(self, message):
-        if message.author == globals.client.user: return
-
-        words = message.content.strip().split()
-        channel = message.channel
-        author = str('{0.author.mention}'.format(message))
-
-        # Check input validity
-        if len(words) == 0:
-            return
-
-        # A user is trying to get help
-        if words[0] == globals.prefix:
-            if len(words) > 1:
-                if words[1] == "ping":
-                    await commands.ping_cmd(message, channel)
-
-                elif words[1] == "here":
-                    await commands.here_cmd(message.author, message.guild, channel)
-
-                elif words[1] == "add":
-                    await commands.add_user_cmd(words, message)
-
-                elif words[1] == "delete":
-                    await commands.delete_user_cmd(words, message)
-
-                elif words[1] == "stop":
-                    await commands.stop_cmd(message.author, message.guild, channel)
-
-                elif words[1] == "info":
-                    await commands.info_cmd(message, words)
+    def add_commands(self):
+        self.add_command(commands.about_cmd)
+        self.add_command(commands.add_user_cmd)
+        self.add_command(commands.delete_user_cmd)
+        self.add_command(commands.here_cmd)
+        self.add_command(commands.help_cmd)
+        self.add_command(commands.info_cmd)
+        self.add_command(commands.ping_cmd)
+        self.add_command(commands.stop_cmd)
+        self.add_command(commands.role_cmd)
+        self.add_command(commands.top_cmd)
+
+    async def on_command_error(self, context, error):
+        if isinstance(error, discord.ext.commands.CheckFailure) or \
+            isinstance(error, discord.ext.commands.MissingRequiredArgument) or \
+            isinstance(error, discord.ext.commands.ConversionError):
+            # A permission check returned False
+            # or an argument is missing
+            # or a converter failed
+            
+            # Should be handled by the command's error handler
+            pass
+        elif isinstance(error, discord.ext.commands.CommandNotFound):
+            globals.logger.debug("Unknown command: {}".format(error))
+        else:
+            globals.logger.exception("An exception occured during the processing of a command: {}".format(error))
+            await context.reply("Error command: {}".format(error)) # TODO debug
 
-                elif words[1] == "about":
-                    await commands.about_cmd(channel)
+    async def on_message(self, message : discord.Message):
 
-                elif words[1] == "help":
-                    await commands.help_cmd(channel)
+        if message.author == globals.client.user: return
 
-                elif words[1] == "top":
-                    await commands.top_cmd(words, channel)
-                
-                elif words[1] == "role":
-                    await commands.role_cmd(words, message, message.author, message.guild, channel)
+        if globals.client.user in message.mentions:
+            return self.on_mention(message.channel)
+        
+        return await self.process_commands(message)
+    
+    async def on_mention(channel):
+        return await channel.send(":heart:")
 
-        # If mentioned
-        elif globals.client.user in message.mentions:
-            await commands.on_mention(channel)
 
 
 def build_embed(feed : utils.Feed):

+ 2 - 1
myanimebot/globals.py

@@ -53,7 +53,7 @@ logPath=CONFIG.get("logPath", "myanimebot.log")
 timezone=pytz.timezone(CONFIG.get("timezone", "utc"))
 secondMax=CONFIG.getint("secondMax", 7200)
 token=CONFIG.get("token")
-prefix=CONFIG.get("prefix", "!mab")
+prefix=CONFIG.get("prefix", "!mab ").strip('"')
 MYANIMELIST_SECONDS_BETWEEN_REQUESTS=CONFIG.getint("myanimelist_seconds_between_requests", 2)
 iconBot=CONFIG.get("iconBot", "http://myanimebot.pentou.eu/rsc/bot_avatar.jpg")
 ANILIST_SECONDS_BETWEEN_FETCHES=CONFIG.getint("anilist_seconds_between_fetches", 60)
@@ -70,6 +70,7 @@ ANI_ENABLED=CONFIG.getboolean("ani_enabled", True)
 HEALTHCHECK_ENABLED=CONFIG.getboolean("healthcheck_enabled", False)
 HEALTHCHECK_PORT=CONFIG.getint("healthcheck_port", 15200)
 HEALTHCHECK_IP=CONFIG.get("healthcheck_ip", "0.0.0.0")
+USERNAME_MAX_LENGTH=14
 
 # Log configuration
 log_format='%(asctime)-13s : %(name)-15s : %(levelname)-8s : %(message)s'

+ 12 - 19
tests/test_commands.py

@@ -7,24 +7,23 @@ import discord.ext.test as dpytest
 
 
 @pytest.fixture
-def client(event_loop):
-    ''' Create our mock client to be used for testing purposes '''
+def bot(request, event_loop):
+    ''' Create our mock bot to be used for testing purposes '''
 
     intents = discord.Intents.default()
     intents.members = True
 
-    c = MyAnimeBot(loop=event_loop, intents=intents)
-    dpytest.configure(c)
+    b = MyAnimeBot(globals.prefix, loop=event_loop, intents=intents)
+    globals.client = b
+    dpytest.configure(b)
 
-    return c
+    return b
 
 
 @pytest.mark.asyncio
-async def test_about_cmd(client):
-    guild = client.guilds[0]
-    channel = guild.text_channels[0]
+async def test_about_cmd(bot):
 
-    await commands.about_cmd(channel)
+    await dpytest.message("{}about".format(globals.prefix))
 
     embed = discord.Embed(title="***MyAnimeBot Commands***", colour=0xEED000)
     embed.title = "MyAnimeBot version {} by Penta & lulu".format(globals.VERSION)
@@ -41,11 +40,9 @@ async def test_about_cmd(client):
 
 
 @pytest.mark.asyncio
-async def test_help_cmd(client):
-    guild = client.guilds[0]
-    channel = guild.text_channels[0]
+async def test_help_cmd(bot):
 
-    await commands.help_cmd(channel)
+    await dpytest.message("{}help".format(globals.prefix))
 
     embed = discord.Embed(title="***MyAnimeBot Commands***", colour=0xEED000)
     embed.add_field(name="`here`", value="Register this channel. The bot will send new activities on registered channels.")
@@ -64,14 +61,10 @@ async def test_help_cmd(client):
 
 
 @pytest.mark.asyncio
-async def test_ping_cmd(client):
-    guild = client.guilds[0]
-    channel = guild.text_channels[0]
+async def test_ping_cmd(bot):
 
-    message = await channel.send("Test Message")
-    await dpytest.empty_queue()
+    await dpytest.message("{}ping".format(globals.prefix))
 
-    await commands.ping_cmd(message, channel)
     dpytest.verify_message(text="pong", contains=True)
 
     await dpytest.empty_queue()