浏览代码

Created a client class + adding dpytest for cmds

Lucas Villeneuve 5 年之前
父节点
当前提交
7af6779582
共有 6 个文件被更改,包括 462 次插入368 次删除
  1. 2 361
      myanimebot.py
  2. 6 3
      myanimebot/commands.py
  3. 374 3
      myanimebot/discord.py
  4. 1 1
      myanimebot/globals.py
  5. 2 0
      requirements.txt
  6. 77 0
      tests/test_commands.py

+ 2 - 361
myanimebot.py

@@ -26,7 +26,7 @@ import myanimebot.globals as globals
 import myanimebot.utils as utils
 import myanimebot.myanimelist as myanimelist
 import myanimebot.commands as commands
-from myanimebot.discord import send_embed_wrapper, build_embed, in_allowed_role
+from myanimebot.discord import send_embed_wrapper, build_embed, in_allowed_role, MyAnimeBot
 
 
 if not sys.version_info[:2] >= (3, 7):
@@ -49,366 +49,6 @@ def exit_app():
 	globals.log_conn.close()
 
 
-# Main function that check the RSS feeds from MyAnimeList
-async def background_check_feed(asyncioloop):
-	globals.logger.info("Starting up background_check_feed")
-	
-	# We configure the http header
-	http_headers = { "User-Agent": "MyAnimeBot Discord Bot v" + globals.VERSION, }
-	
-	await globals.client.wait_until_ready()
-	
-	globals.logger.debug("Discord client connected, unlocking background_check_feed...")
-	
-	while not globals.client.is_closed():
-		try:
-			db_user = globals.conn.cursor(buffered=True, dictionary=True)
-			db_user.execute("SELECT mal_user, servers FROM t_users WHERE service=%s", [globals.SERVICE_MAL])
-			data_user = db_user.fetchone()
-		except Exception as e:
-			globals.logger.critical("Database unavailable! (" + str(e) + ")")
-			quit()
-
-		while data_user is not None:
-			user = utils.User(id=None,
-								service_id=None,
-								name=data_user[globals.DB_USER_NAME],
-								servers=data_user["servers"].split(','))
-			stop_boucle = 0
-			feed_type = 1
-
-			try:
-				while stop_boucle == 0 :
-					try:
-						async with aiohttp.ClientSession() as httpclient:
-							if feed_type == 1 :
-								http_response = await httpclient.request("GET", "https://myanimelist.net/rss.php?type=rm&u=" + user.name, headers=http_headers)
-								media = "manga"
-							else : 
-								http_response = await httpclient.request("GET", "https://myanimelist.net/rss.php?type=rw&u=" + user.name, headers=http_headers)
-								media = "anime"
-					except Exception as e:
-						globals.logger.error("Error while loading RSS (" + str(feed_type) + ") of '" + user.name + "': " + str(e))
-						break
-
-					http_data = await http_response.read()
-					feeds_data = feedparser.parse(http_data)
-					
-					for feed_data in feeds_data.entries:
-						pubDateRaw = datetime.strptime(feed_data.published, '%a, %d %b %Y %H:%M:%S %z').astimezone(globals.timezone)
-						pubDate = pubDateRaw.strftime("%Y-%m-%d %H:%M:%S")
-						if feed_type == 1:
-							media_type = utils.MediaType.MANGA
-						else:
-							media_type = utils.MediaType.ANIME
-
-						feed = myanimelist.build_feed_from_data(feed_data, user, None, pubDateRaw.timestamp(), media_type)
-						
-						cursor = globals.conn.cursor(buffered=True)
-						cursor.execute("SELECT published, title, url, type FROM t_feeds WHERE published=%s AND title=%s AND user=%s AND type=%s AND obsolete=0 AND service=%s", [pubDate, feed.media.name, user.name, feed.get_status_str(), globals.SERVICE_MAL])
-						data = cursor.fetchone()
-
-						if data is None:
-							var = datetime.now(globals.timezone) - pubDateRaw
-							
-							globals.logger.debug(" - " + feed.media.name + ": " + str(var.total_seconds()))
-						
-							if var.total_seconds() < globals.secondMax:
-								globals.logger.info(user.name + ": Item '" + feed.media.name + "' not seen, processing...")
-								
-								cursor.execute("SELECT thumbnail FROM t_animes WHERE guid=%s AND service=%s LIMIT 1", [feed.media.url, globals.SERVICE_MAL]) # TODO Change that ?
-								data_img = cursor.fetchone()
-								
-								if data_img is None:
-									try:
-										image = myanimelist.get_thumbnail(feed.media.url)
-										
-										globals.logger.info("First time seeing this " + media + ", adding thumbnail into database: " + image)
-									except Exception as e:
-										globals.logger.warning("Error while getting the thumbnail: " + str(e))
-										image = ""
-										
-									cursor.execute("INSERT INTO t_animes (guid, service, title, thumbnail, found, discoverer, media) VALUES (%s, %s, %s, %s, NOW(), %s, %s)", [feed.media.url, globals.SERVICE_MAL, feed.media.name, image, user.name, media])
-									globals.conn.commit()
-								else: image = data_img[0]
-								feed.media.image = image
-
-								cursor.execute("UPDATE t_feeds SET obsolete=1 WHERE published=%s AND title=%s AND user=%s AND service=%s", [pubDate, feed.media.name, user.name, globals.SERVICE_MAL])
-								cursor.execute("INSERT INTO t_feeds (published, title, service, url, user, found, type) VALUES (%s, %s, %s, %s, %s, NOW(), %s)", (pubDate, feed.media.name, globals.SERVICE_MAL, feed.media.url, user.name, feed.get_status_str()))
-								globals.conn.commit()
-								
-								for server in user.servers:
-									db_srv = globals.conn.cursor(buffered=True)
-									db_srv.execute("SELECT channel FROM t_servers WHERE server = %s", [server])
-									data_channel = db_srv.fetchone()
-									
-									while data_channel is not None:
-										for channel in data_channel: await send_embed_wrapper(asyncioloop, channel, globals.client, build_embed(feed))
-										
-										data_channel = db_srv.fetchone()
-					if feed_type == 1:
-						feed_type = 0
-						await asyncio.sleep(globals.MYANIMELIST_SECONDS_BETWEEN_REQUESTS)
-					else:
-						stop_boucle = 1
-					
-			except Exception as e:
-				globals.logger.exception("Error when parsing RSS for '" + user.name + "': \n")
-			
-			await asyncio.sleep(globals.MYANIMELIST_SECONDS_BETWEEN_REQUESTS)
-
-			data_user = db_user.fetchone()
-
-
-async def fetch_activities_anilist():
-	await anilist.check_new_activities()
-
-
-@globals.client.event
-async def on_ready():
-	globals.logger.info("Logged in as " + globals.client.user.name + " (" + str(globals.client.user.id) + ")")
-
-	globals.logger.info("Starting all tasks...")
-
-	globals.task_feed = globals.client.loop.create_task(background_check_feed(globals.client.loop))
-	globals.task_feed_anilist = globals.client.loop.create_task(anilist.background_check_feed(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.client.event
-async def on_error(event, *args, **kwargs):
-	globals.logger.exception("Crap! An unknown Discord error occured...")
-
-
-@globals.client.event
-async def on_message(message):
-	if message.author == globals.client.user: return
-
-	words = message.content.split(" ")
-	channel = message.channel
-	author = str('{0.author.mention}'.format(message))
-
-	# 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":
-				if in_allowed_role(message.author, message.guild):
-					cursor = globals.conn.cursor(buffered=True)
-					cursor.execute("SELECT server, channel FROM t_servers WHERE server=%s", [str(message.guild.id)])
-					data = cursor.fetchone()
-					
-					if data is None:
-						cursor.execute("INSERT INTO t_servers (server, channel) VALUES (%s,%s)", [str(message.guild.id), str(message.channel.id)])
-						globals.conn.commit()
-						
-						await message.channel.send("Channel **" + str(message.channel) + "** configured for **" + str(message.guild) + "**.")
-					else:
-						if(data[1] == str(message.channel.id)): await message.channel.send("Channel **" + str(message.channel) + "** already in use for this server.")
-						else:
-							cursor.execute("UPDATE t_servers SET channel = %s WHERE server = %s", [str(message.channel.id), str(message.guild.id)])
-							globals.conn.commit()
-							
-							await message.channel.send("Channel updated to: **" + str(message.channel) + "**.")
-							
-					cursor.close()
-				else: await message.channel.send("Only allowed users can use this command!")
-				
-			elif words[1] == "add":
-				if in_allowed_role(message.author, message.guild):
-					await commands.add_user_cmd(words, message)
-				else: await message.channel.send("Only allowed users can use this command!")
-				
-			elif words[1] == "delete":
-				if in_allowed_role(message.author, message.guild):
-					await commands.delete_user_cmd(words, message)
-				else: await message.channel.send("Only allowed users can use this command!")
-				
-			elif words[1] == "stop":
-				if in_allowed_role(message.author, message.guild):
-					if (len(words) == 2):
-						cursor = globals.conn.cursor(buffered=True)
-						cursor.execute("SELECT server FROM t_servers WHERE server=%s", [str(message.guild.id)])
-						data = cursor.fetchone()
-					
-						if data is None: await message.channel.send("The server **" + str(message.guild) + "** is not in our database.")
-						else:
-							cursor.execute("DELETE FROM t_servers WHERE server = %s", [message.guild.id])
-							globals.conn.commit()
-							await message.channel.send("Server **" + str(message.guild) + "** deleted from our database.")
-						
-						cursor.close()
-					else: await message.channel.send("Too many arguments! Only type *stop* if you want to stop this bot on **" + message.guild + "**")
-				else: await message.channel.send("Only allowed users can use this command!")
-				
-			elif words[1] == "info":
-				await commands.info_cmd(message, words)
-
-			elif words[1] == "about":
-				await commands.about_cmd(channel)
-			
-			elif words[1] == "help":
-				await commands.help_cmd(channel)
-			
-			elif words[1] == "top":
-				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 message.channel.send(topText)
-						
-						cursor.close()
-					except Exception as e:
-						globals.logger.warning("An error occured while displaying the global top: " + str(e))
-						await message.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 message.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 message.channel.send("Unable to reply to your request at the moment...")
-			
-			elif words[1] == "role":
-				if len(words) > 2:
-					if message.author.guild_permissions.administrator:
-						cursor = globals.conn.cursor(buffered=True)
-
-						if (words[2] == "everyone") | (words[2] == "@everyone"):
-							cursor.execute("UPDATE t_servers SET admin_group = NULL WHERE server = %s", [str(message.guild.id)])
-							globals.conn.commit()
-
-							await message.channel.send("Everybody is now allowed to use the bot!")
-						else:
-							rolesFound = message.role_mentions
-
-							if (len(rolesFound) > 1): await message.channel.send("Please specify only 1 group!")
-							elif (len(rolesFound) == 0): await message.channel.send("Please specify a correct group.")
-							else: 
-								cursor.execute("UPDATE t_servers SET admin_group = %s WHERE server = %s", [str(rolesFound[0].id), str(message.guild.id)])
-								globals.conn.commit()
-
-								await message.channel.send("The role **" + str(rolesFound[0].name) + "** is now allowed to use this bot!")
-
-						cursor.close()
-					else: await message.channel.send("Only server's admins can use this command!")
-				else:
-					await message.channel.send("You have to specify a role!")
-
-	# If mentioned
-	elif globals.client.user in message.mentions:
-		await channel.send(":heart:")
-
-# Get a random anime name and change the bot's activity
-async def change_gameplayed(asyncioloop):
-	globals.logger.info("Starting up change_gameplayed")
-	
-	await globals.client.wait_until_ready()
-	await asyncio.sleep(1)
-
-	while not globals.client.is_closed():
-		# Get a random anime name from the users' list
-		cursor = globals.conn.cursor(buffered=True)
-		cursor.execute("SELECT title FROM t_animes ORDER BY RAND() LIMIT 1")
-		data = cursor.fetchone()
-		anime = utils.truncate_end_show(data[0])
-		
-		# Try to change the bot's activity
-		try:
-			if data is not None: await globals.client.change_presence(activity=discord.Activity(type=discord.ActivityType.watching, name=anime))
-		except Exception as e:
-			globals.logger.warning("An error occured while changing the displayed anime title: " + str(e))
-			
-		cursor.close()
-		# Do it every minute
-		await asyncio.sleep(60)
-
-async def update_thumbnail_catalog(asyncioloop):
-	globals.logger.info("Starting up update_thumbnail_catalog")
-	
-	while not globals.client.is_closed():
-		await asyncio.sleep(43200)
-		
-		globals.logger.info("Automatic check of the thumbnail database on going...")
-		reload = 0
-		
-		cursor = globals.conn.cursor(buffered=True)
-		cursor.execute("SELECT guid, title, thumbnail FROM t_animes")
-		data = cursor.fetchone()
-
-		while data is not None:
-			try:
-				if (data[2] != "") : urllib.request.urlopen(data[2])
-				else: reload = 1
-			except urllib.error.HTTPError as e:
-				globals.logger.warning("HTTP Error while getting the current thumbnail of '" + str(data[1]) + "': " + str(e))
-				reload = 1
-			except Exception as e:
-				globals.logger.debug("Error while getting the current thumbnail of '" + str(data[1]) + "': " + str(e))
-			
-			if (reload == 1) :
-				try:
-					image = myanimelist.get_thumbnail(data[0])
-						
-					cursor.execute("UPDATE t_animes SET thumbnail = %s WHERE guid = %s", [image, data[0]])
-					globals.conn.commit()
-						
-					globals.logger.info("Updated thumbnail found for \"" + str(data[1]) + "\": %s", image)
-				except Exception as e:
-					globals.logger.warning("Error while downloading updated thumbnail for '" + str(data[1]) + "': " + str(e))
-
-			await asyncio.sleep(3)
-			data = cursor.fetchone()
-
-		cursor.close()
-
-		globals.logger.info("Thumbnail database checked.")
-
-
 # Starting main function	
 if __name__ == "__main__":
 	
@@ -417,6 +57,7 @@ if __name__ == "__main__":
 	
 	# Run the app
 	try:
+		globals.client = MyAnimeBot()
 		globals.client.run(globals.token)
 	except Exception as e:
 		globals.logger.error("Encountered exception while running the bot: {}".format(e))

+ 6 - 3
myanimebot/commands.py

@@ -195,14 +195,17 @@ async def ping_cmd(message, channel):
 async def about_cmd(channel):
     ''' Responds to about command with a brief description of this bot '''
 
-    title = "MyAnimeBot version {} by Penta & lulu".format(globals.VERSION)
-    description = """MyAnimeBot checks MyAnimeList and Anilist profiles for specified users, and send a message for every new activities found.
+    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=discord.Embed(colour=0x777777, title=title, description=description).set_thumbnail(url=globals.iconBot))
+    await channel.send(embed=embed)
 
 
 async def help_cmd(channel):

+ 374 - 3
myanimebot/discord.py

@@ -1,10 +1,199 @@
-import discord
+import asyncio
+import urllib.request
+from configparser import ConfigParser
+from datetime import datetime
+from typing import List, Tuple
+
+import aiohttp
+import feedparser
 import pytz
+from dateutil.parser import parse as parse_datetime
 
-import myanimebot.globals as globals # TODO Rename globals module
+import discord
+# Our modules
+import myanimebot.anilist as anilist
+import myanimebot.commands as commands
+import myanimebot.globals as globals  # TODO Rename globals module
+import myanimebot.myanimelist as myanimelist
 import myanimebot.utils as utils
 
 
+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("Starting all tasks...")
+
+		globals.task_feed = globals.client.loop.create_task(background_check_feed(globals.client.loop))
+		globals.task_feed_anilist = globals.client.loop.create_task(anilist.background_check_feed(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))
+
+	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.split(" ")
+		channel = message.channel
+		author = str('{0.author.mention}'.format(message))
+
+		# 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":
+					if in_allowed_role(message.author, message.guild):
+						cursor = globals.conn.cursor(buffered=True)
+						cursor.execute("SELECT server, channel FROM t_servers WHERE server=%s", [str(message.guild.id)])
+						data = cursor.fetchone()
+						
+						if data is None:
+							cursor.execute("INSERT INTO t_servers (server, channel) VALUES (%s,%s)", [str(message.guild.id), str(message.channel.id)])
+							globals.conn.commit()
+							
+							await message.channel.send("Channel **" + str(message.channel) + "** configured for **" + str(message.guild) + "**.")
+						else:
+							if(data[1] == str(message.channel.id)): await message.channel.send("Channel **" + str(message.channel) + "** already in use for this server.")
+							else:
+								cursor.execute("UPDATE t_servers SET channel = %s WHERE server = %s", [str(message.channel.id), str(message.guild.id)])
+								globals.conn.commit()
+								
+								await message.channel.send("Channel updated to: **" + str(message.channel) + "**.")
+								
+						cursor.close()
+					else: await message.channel.send("Only allowed users can use this command!")
+					
+				elif words[1] == "add":
+					if in_allowed_role(message.author, message.guild):
+						await commands.add_user_cmd(words, message)
+					else: await message.channel.send("Only allowed users can use this command!")
+					
+				elif words[1] == "delete":
+					if in_allowed_role(message.author, message.guild):
+						await commands.delete_user_cmd(words, message)
+					else: await message.channel.send("Only allowed users can use this command!")
+					
+				elif words[1] == "stop":
+					if in_allowed_role(message.author, message.guild):
+						if (len(words) == 2):
+							cursor = globals.conn.cursor(buffered=True)
+							cursor.execute("SELECT server FROM t_servers WHERE server=%s", [str(message.guild.id)])
+							data = cursor.fetchone()
+						
+							if data is None: await message.channel.send("The server **" + str(message.guild) + "** is not in our database.")
+							else:
+								cursor.execute("DELETE FROM t_servers WHERE server = %s", [message.guild.id])
+								globals.conn.commit()
+								await message.channel.send("Server **" + str(message.guild) + "** deleted from our database.")
+							
+							cursor.close()
+						else: await message.channel.send("Too many arguments! Only type *stop* if you want to stop this bot on **" + message.guild + "**")
+					else: await message.channel.send("Only allowed users can use this command!")
+					
+				elif words[1] == "info":
+					await commands.info_cmd(message, words)
+
+				elif words[1] == "about":
+					await commands.about_cmd(channel)
+				
+				elif words[1] == "help":
+					await commands.help_cmd(channel)
+				
+				elif words[1] == "top":
+					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 message.channel.send(topText)
+							
+							cursor.close()
+						except Exception as e:
+							globals.logger.warning("An error occured while displaying the global top: " + str(e))
+							await message.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 message.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 message.channel.send("Unable to reply to your request at the moment...")
+				
+				elif words[1] == "role":
+					if len(words) > 2:
+						if message.author.guild_permissions.administrator:
+							cursor = globals.conn.cursor(buffered=True)
+
+							if (words[2] == "everyone") | (words[2] == "@everyone"):
+								cursor.execute("UPDATE t_servers SET admin_group = NULL WHERE server = %s", [str(message.guild.id)])
+								globals.conn.commit()
+
+								await message.channel.send("Everybody is now allowed to use the bot!")
+							else:
+								rolesFound = message.role_mentions
+
+								if (len(rolesFound) > 1): await message.channel.send("Please specify only 1 group!")
+								elif (len(rolesFound) == 0): await message.channel.send("Please specify a correct group.")
+								else: 
+									cursor.execute("UPDATE t_servers SET admin_group = %s WHERE server = %s", [str(rolesFound[0].id), str(message.guild.id)])
+									globals.conn.commit()
+
+									await message.channel.send("The role **" + str(rolesFound[0].name) + "** is now allowed to use this bot!")
+
+							cursor.close()
+						else: await message.channel.send("Only server's admins can use this command!")
+					else:
+						await message.channel.send("You have to specify a role!")
+
+		# If mentioned
+		elif globals.client.user in message.mentions:
+			await channel.send(":heart:")
+
+
 def build_embed(feed : utils.Feed):
     ''' Build the embed message related to the anime's status '''
 
@@ -65,4 +254,186 @@ def in_allowed_role(user : discord.Member, server : int) -> bool :
 				globals.logger.debug ("Permissions validated for " + str(user))
 				return True
 
-	return False
+	return False
+
+
+
+	# Main function that check the RSS feeds from MyAnimeList
+async def background_check_feed(asyncioloop):
+	globals.logger.info("Starting up background_check_feed")
+	
+	# We configure the http header
+	http_headers = { "User-Agent": "MyAnimeBot Discord Bot v" + globals.VERSION, }
+	
+	await globals.client.wait_until_ready()
+	
+	globals.logger.debug("Discord client connected, unlocking background_check_feed...")
+	
+	while not globals.client.is_closed():
+		try:
+			db_user = globals.conn.cursor(buffered=True, dictionary=True)
+			db_user.execute("SELECT mal_user, servers FROM t_users WHERE service=%s", [globals.SERVICE_MAL])
+			data_user = db_user.fetchone()
+		except Exception as e:
+			globals.logger.critical("Database unavailable! (" + str(e) + ")")
+			quit()
+
+		while data_user is not None:
+			user = utils.User(id=None,
+								service_id=None,
+								name=data_user[globals.DB_USER_NAME],
+								servers=data_user["servers"].split(','))
+			stop_boucle = 0
+			feed_type = 1
+
+			try:
+				while stop_boucle == 0 :
+					try:
+						async with aiohttp.ClientSession() as httpclient:
+							if feed_type == 1 :
+								http_response = await httpclient.request("GET", "https://myanimelist.net/rss.php?type=rm&u=" + user.name, headers=http_headers)
+								media = "manga"
+							else : 
+								http_response = await httpclient.request("GET", "https://myanimelist.net/rss.php?type=rw&u=" + user.name, headers=http_headers)
+								media = "anime"
+					except Exception as e:
+						globals.logger.error("Error while loading RSS (" + str(feed_type) + ") of '" + user.name + "': " + str(e))
+						break
+
+					http_data = await http_response.read()
+					feeds_data = feedparser.parse(http_data)
+					
+					for feed_data in feeds_data.entries:
+						pubDateRaw = datetime.strptime(feed_data.published, '%a, %d %b %Y %H:%M:%S %z').astimezone(globals.timezone)
+						pubDate = pubDateRaw.strftime("%Y-%m-%d %H:%M:%S")
+						if feed_type == 1:
+							media_type = utils.MediaType.MANGA
+						else:
+							media_type = utils.MediaType.ANIME
+
+						feed = myanimelist.build_feed_from_data(feed_data, user, None, pubDateRaw.timestamp(), media_type)
+						
+						cursor = globals.conn.cursor(buffered=True)
+						cursor.execute("SELECT published, title, url, type FROM t_feeds WHERE published=%s AND title=%s AND user=%s AND type=%s AND obsolete=0 AND service=%s", [pubDate, feed.media.name, user.name, feed.get_status_str(), globals.SERVICE_MAL])
+						data = cursor.fetchone()
+
+						if data is None:
+							var = datetime.now(globals.timezone) - pubDateRaw
+							
+							globals.logger.debug(" - " + feed.media.name + ": " + str(var.total_seconds()))
+						
+							if var.total_seconds() < globals.secondMax:
+								globals.logger.info(user.name + ": Item '" + feed.media.name + "' not seen, processing...")
+								
+								cursor.execute("SELECT thumbnail FROM t_animes WHERE guid=%s AND service=%s LIMIT 1", [feed.media.url, globals.SERVICE_MAL]) # TODO Change that ?
+								data_img = cursor.fetchone()
+								
+								if data_img is None:
+									try:
+										image = myanimelist.get_thumbnail(feed.media.url)
+										
+										globals.logger.info("First time seeing this " + media + ", adding thumbnail into database: " + image)
+									except Exception as e:
+										globals.logger.warning("Error while getting the thumbnail: " + str(e))
+										image = ""
+										
+									cursor.execute("INSERT INTO t_animes (guid, service, title, thumbnail, found, discoverer, media) VALUES (%s, %s, %s, %s, NOW(), %s, %s)", [feed.media.url, globals.SERVICE_MAL, feed.media.name, image, user.name, media])
+									globals.conn.commit()
+								else: image = data_img[0]
+								feed.media.image = image
+
+								cursor.execute("UPDATE t_feeds SET obsolete=1 WHERE published=%s AND title=%s AND user=%s AND service=%s", [pubDate, feed.media.name, user.name, globals.SERVICE_MAL])
+								cursor.execute("INSERT INTO t_feeds (published, title, service, url, user, found, type) VALUES (%s, %s, %s, %s, %s, NOW(), %s)", (pubDate, feed.media.name, globals.SERVICE_MAL, feed.media.url, user.name, feed.get_status_str()))
+								globals.conn.commit()
+								
+								for server in user.servers:
+									db_srv = globals.conn.cursor(buffered=True)
+									db_srv.execute("SELECT channel FROM t_servers WHERE server = %s", [server])
+									data_channel = db_srv.fetchone()
+									
+									while data_channel is not None:
+										for channel in data_channel: await send_embed_wrapper(asyncioloop, channel, globals.client, build_embed(feed))
+										
+										data_channel = db_srv.fetchone()
+					if feed_type == 1:
+						feed_type = 0
+						await asyncio.sleep(globals.MYANIMELIST_SECONDS_BETWEEN_REQUESTS)
+					else:
+						stop_boucle = 1
+					
+			except Exception as e:
+				globals.logger.exception("Error when parsing RSS for '" + user.name + "': \n")
+			
+			await asyncio.sleep(globals.MYANIMELIST_SECONDS_BETWEEN_REQUESTS)
+
+			data_user = db_user.fetchone()
+
+
+async def fetch_activities_anilist():
+	await anilist.check_new_activities()
+
+
+# Get a random anime name and change the bot's activity
+async def change_gameplayed(asyncioloop):
+	globals.logger.info("Starting up change_gameplayed")
+	
+	await globals.client.wait_until_ready()
+	await asyncio.sleep(1)
+
+	while not globals.client.is_closed():
+		# Get a random anime name from the users' list
+		cursor = globals.conn.cursor(buffered=True)
+		cursor.execute("SELECT title FROM t_animes ORDER BY RAND() LIMIT 1")
+		data = cursor.fetchone()
+		anime = utils.truncate_end_show(data[0])
+		
+		# Try to change the bot's activity
+		try:
+			if data is not None: await globals.client.change_presence(activity=discord.Activity(type=discord.ActivityType.watching, name=anime))
+		except Exception as e:
+			globals.logger.warning("An error occured while changing the displayed anime title: " + str(e))
+			
+		cursor.close()
+		# Do it every minute
+		await asyncio.sleep(60)
+
+async def update_thumbnail_catalog(asyncioloop):
+	globals.logger.info("Starting up update_thumbnail_catalog")
+	
+	while not globals.client.is_closed():
+		await asyncio.sleep(43200)
+		
+		globals.logger.info("Automatic check of the thumbnail database on going...")
+		reload = 0
+		
+		cursor = globals.conn.cursor(buffered=True)
+		cursor.execute("SELECT guid, title, thumbnail FROM t_animes")
+		data = cursor.fetchone()
+
+		while data is not None:
+			try:
+				if (data[2] != "") : urllib.request.urlopen(data[2])
+				else: reload = 1
+			except urllib.error.HTTPError as e:
+				globals.logger.warning("HTTP Error while getting the current thumbnail of '" + str(data[1]) + "': " + str(e))
+				reload = 1
+			except Exception as e:
+				globals.logger.debug("Error while getting the current thumbnail of '" + str(data[1]) + "': " + str(e))
+			
+			if (reload == 1) :
+				try:
+					image = myanimelist.get_thumbnail(data[0])
+						
+					cursor.execute("UPDATE t_animes SET thumbnail = %s WHERE guid = %s", [image, data[0]])
+					globals.conn.commit()
+						
+					globals.logger.info("Updated thumbnail found for \"" + str(data[1]) + "\": %s", image)
+				except Exception as e:
+					globals.logger.warning("Error while downloading updated thumbnail for '" + str(data[1]) + "': " + str(e))
+
+			await asyncio.sleep(3)
+			data = cursor.fetchone()
+
+		cursor.close()
+
+		globals.logger.info("Thumbnail database checked.")

+ 1 - 1
myanimebot/globals.py

@@ -123,7 +123,7 @@ except Exception as e:
 
 
 # Initialization of the Discord client
-client = discord.Client()
+client = None
 
 task_feed       	= None
 task_feed_anilist	= None

+ 2 - 0
requirements.txt

@@ -11,6 +11,7 @@ cffi==1.14.4
 chardet==3.0.4
 discord==1.0.1
 discord.py==1.5.1
+dpytest==0.0.22
 feedparser==6.0.2
 html2text==2020.1.16
 idna==2.10
@@ -29,6 +30,7 @@ pycparser==2.20
 PyNaCl==1.4.0
 pyparsing==2.4.7
 pytest==6.2.1
+pytest-asyncio==0.14.0
 python-dateutil==2.8.1
 pytz==2020.5
 requests==2.25.1

+ 77 - 0
tests/test_commands.py

@@ -0,0 +1,77 @@
+import pytest
+import discord
+import myanimebot.commands as commands
+import myanimebot.globals as globals
+from myanimebot.discord import MyAnimeBot
+import discord.ext.test as dpytest
+
+
+@pytest.fixture
+def client(event_loop):
+    ''' Create our mock client to be used for testing purposes '''
+
+    intents = discord.Intents.default()
+    intents.members = True
+
+    c = MyAnimeBot(loop=event_loop, intents=intents)
+    dpytest.configure(c)
+
+    return c
+
+
+@pytest.mark.asyncio
+async def test_about_cmd(client):
+    guild = client.guilds[0]
+    channel = guild.text_channels[0]
+
+    await commands.about_cmd(channel)
+
+    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)
+
+    dpytest.verify_embed(embed=embed)
+
+    await dpytest.empty_queue()
+
+
+@pytest.mark.asyncio
+async def test_help_cmd(client):
+    guild = client.guilds[0]
+    channel = guild.text_channels[0]
+
+    await commands.help_cmd(channel)
+
+    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")
+
+    dpytest.verify_embed(embed=embed)
+
+    await dpytest.empty_queue()
+
+
+@pytest.mark.asyncio
+async def test_ping_cmd(client):
+    guild = client.guilds[0]
+    channel = guild.text_channels[0]
+
+    message = await channel.send("Test Message")
+    await dpytest.empty_queue()
+
+    await commands.ping_cmd(message, channel)
+    dpytest.verify_message(text="pong", contains=True)
+
+    await dpytest.empty_queue()