discord.py 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315
  1. import asyncio
  2. import urllib.request
  3. from datetime import datetime
  4. import aiohttp
  5. import feedparser
  6. import pytz
  7. from dateutil.parser import parse as parse_datetime
  8. import discord
  9. import discord.ext.commands
  10. # Our modules
  11. import myanimebot.anilist as anilist
  12. import myanimebot.healthcheck as healthcheck
  13. import myanimebot.globals as globals # TODO Rename globals module
  14. import myanimebot.myanimelist as myanimelist
  15. import myanimebot.utils as utils
  16. import myanimebot.commands as commands
  17. class MyAnimeBot(discord.ext.commands.Bot):
  18. def __init__(self, cmd_prefix, **options):
  19. discord.ext.commands.Bot.__init__(self, command_prefix=cmd_prefix, help_command=None, **options)
  20. self.add_commands()
  21. async def on_ready(self):
  22. globals.logger.info("Logged in as {} ({})".format(self.user.name, self.user.id))
  23. globals.logger.info("Starting all tasks...")
  24. if globals.MAL_ENABLED:
  25. globals.task_feed = globals.client.loop.create_task(background_check_feed(globals.client.loop))
  26. if globals.ANI_ENABLED:
  27. globals.task_feed_anilist = globals.client.loop.create_task(anilist.background_check_feed(globals.client.loop))
  28. if globals.HEALTHCHECK_ENABLED:
  29. globals.task_healthcheck = globals.client.loop.create_task(healthcheck.main(globals.client.loop))
  30. globals.task_thumbnail = self.loop.create_task(update_thumbnail_catalog(self.loop))
  31. globals.task_gameplayed = self.loop.create_task(change_gameplayed(self.loop))
  32. async def on_error(self, event, *args, **kwargs):
  33. globals.logger.exception("Crap! An unknown Discord error occured...")
  34. def add_commands(self):
  35. self.add_command(commands.about_cmd)
  36. self.add_command(commands.add_user_cmd)
  37. self.add_command(commands.delete_user_cmd)
  38. self.add_command(commands.here_cmd)
  39. self.add_command(commands.help_cmd)
  40. self.add_command(commands.info_cmd)
  41. self.add_command(commands.ping_cmd)
  42. self.add_command(commands.stop_cmd)
  43. self.add_command(commands.role_cmd)
  44. self.add_command(commands.top_cmd)
  45. async def on_command_error(self, context, error):
  46. if isinstance(error, discord.ext.commands.CheckFailure) or \
  47. isinstance(error, discord.ext.commands.MissingRequiredArgument) or \
  48. isinstance(error, discord.ext.commands.ConversionError):
  49. # A permission check returned False
  50. # or an argument is missing
  51. # or a converter failed
  52. # Should be handled by the command's error handler
  53. pass
  54. elif isinstance(error, discord.ext.commands.CommandNotFound):
  55. globals.logger.debug("Unknown command: {}".format(error))
  56. else:
  57. globals.logger.exception("An exception occured during the processing of a command: {}".format(error))
  58. await context.reply("Error command: {}".format(error)) # TODO debug
  59. async def on_message(self, message : discord.Message):
  60. if message.author == globals.client.user: return
  61. if globals.client.user in message.mentions:
  62. return self.on_mention(message.channel)
  63. return await self.process_commands(message)
  64. async def on_mention(channel):
  65. return await channel.send(":heart:")
  66. def build_embed(feed : utils.Feed):
  67. ''' Build the embed message related to the anime's status '''
  68. # Get service
  69. if feed.service == utils.Service.MAL:
  70. service_name = 'MyAnimeList'
  71. profile_url = "{}{}".format(globals.MAL_PROFILE_URL, feed.user.name)
  72. icon_url = globals.MAL_ICON_URL
  73. elif feed.service == utils.Service.ANILIST:
  74. service_name = 'AniList'
  75. profile_url = "{}{}".format(globals.ANILIST_PROFILE_URL, feed.user.name)
  76. icon_url = globals.ANILIST_ICON_URL
  77. else:
  78. raise NotImplementedError('Unknown service {}'.format(feed.service))
  79. description = utils.build_description_string(feed)
  80. content = "[{}]({})\n```{}```".format(utils.filter_name(feed.media.name), feed.media.url, description)
  81. profile_url_label = "{}'s {}".format(feed.user.name, service_name)
  82. try:
  83. embed = discord.Embed(colour=int(feed.status.value, 16), url=feed.media.url, description=content, timestamp=feed.date_publication.astimezone(pytz.timezone("utc")))
  84. embed.set_thumbnail(url=feed.media.image)
  85. embed.set_author(name=profile_url_label, url=profile_url, icon_url=icon_url)
  86. embed.set_footer(text="MyAnimeBot", icon_url=globals.iconBot)
  87. return embed
  88. except Exception as e:
  89. globals.logger.error("Error when generating the message: " + str(e))
  90. return
  91. async def send_embed_wrapper(asyncioloop, channelid, client, embed):
  92. ''' Send an embed message to a channel '''
  93. channel = client.get_channel(int(channelid))
  94. try:
  95. await channel.send(embed=embed)
  96. globals.logger.info("Message sent in channel: {}".format(channelid))
  97. except Exception as e:
  98. globals.logger.debug("Impossible to send a message on '{}': {}".format(channelid, e))
  99. return
  100. # Main function that check the RSS feeds from MyAnimeList
  101. async def background_check_feed(asyncioloop):
  102. globals.logger.info("Starting up background_check_feed")
  103. # We configure the http header
  104. http_headers = { "User-Agent": "MyAnimeBot Discord Bot v" + globals.VERSION, }
  105. timeout = aiohttp.ClientTimeout(total=5)
  106. await globals.client.wait_until_ready()
  107. globals.logger.debug("Discord client connected, unlocking background_check_feed...")
  108. while not globals.client.is_closed():
  109. try:
  110. db_user = globals.conn.cursor(buffered=True, dictionary=True)
  111. db_user.execute("SELECT mal_user, servers FROM t_users WHERE service=%s", [globals.SERVICE_MAL])
  112. data_user = db_user.fetchone()
  113. except Exception as e:
  114. globals.logger.critical("Database unavailable! (" + str(e) + ")")
  115. quit()
  116. while data_user is not None:
  117. user = utils.User(id=None,
  118. service_id=None,
  119. name=data_user[globals.DB_USER_NAME],
  120. servers=data_user["servers"].split(','))
  121. stop_boucle = 0
  122. feed_type = 1
  123. try:
  124. while stop_boucle == 0 :
  125. try:
  126. async with aiohttp.ClientSession() as httpclient:
  127. if feed_type == 1 :
  128. http_response = await httpclient.request("GET", "https://myanimelist.net/rss.php?type=rm&u=" + user.name, headers=http_headers, timeout=timeout)
  129. media = "manga"
  130. else :
  131. http_response = await httpclient.request("GET", "https://myanimelist.net/rss.php?type=rw&u=" + user.name, headers=http_headers, timeout=timeout)
  132. media = "anime"
  133. http_data = await http_response.read()
  134. except asyncio.TimeoutError:
  135. globals.logger.error("Error while loading RSS of '{}': Timeout".format(user.name))
  136. break
  137. except Exception as e:
  138. globals.logger.exception("Error while loading RSS ({}) of '{}':\n".format(feed_type, user.name))
  139. break
  140. feeds_data = feedparser.parse(http_data)
  141. for feed_data in feeds_data.entries:
  142. pubDateRaw = datetime.strptime(feed_data.published, '%a, %d %b %Y %H:%M:%S %z').astimezone(globals.timezone)
  143. pubDate = pubDateRaw.strftime("%Y-%m-%d %H:%M:%S")
  144. if feed_type == 1:
  145. media_type = utils.MediaType.MANGA
  146. else:
  147. media_type = utils.MediaType.ANIME
  148. feed = myanimelist.build_feed_from_data(feed_data, user, None, pubDateRaw.timestamp(), media_type)
  149. cursor = globals.conn.cursor(buffered=True)
  150. 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])
  151. data = cursor.fetchone()
  152. if data is None:
  153. var = datetime.now(globals.timezone) - pubDateRaw
  154. globals.logger.debug(" - " + feed.media.name + ": " + str(var.total_seconds()))
  155. if var.total_seconds() < globals.secondMax:
  156. globals.logger.info(user.name + ": Item '" + feed.media.name + "' not seen, processing...")
  157. cursor.execute("SELECT thumbnail FROM t_animes WHERE guid=%s AND service=%s LIMIT 1", [feed.media.url, globals.SERVICE_MAL]) # TODO Change that ?
  158. data_img = cursor.fetchone()
  159. if data_img is None:
  160. try:
  161. image = myanimelist.get_thumbnail(feed.media.url)
  162. globals.logger.info("First time seeing this " + media + ", adding thumbnail into database: " + image)
  163. except Exception as e:
  164. globals.logger.warning("Error while getting the thumbnail: " + str(e))
  165. image = ""
  166. 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])
  167. globals.conn.commit()
  168. else: image = data_img[0]
  169. feed.media.image = image
  170. 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])
  171. 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()))
  172. globals.conn.commit()
  173. for server in user.servers:
  174. db_srv = globals.conn.cursor(buffered=True)
  175. db_srv.execute("SELECT channel FROM t_servers WHERE server = %s", [server])
  176. data_channel = db_srv.fetchone()
  177. while data_channel is not None:
  178. for channel in data_channel: await send_embed_wrapper(asyncioloop, channel, globals.client, build_embed(feed))
  179. data_channel = db_srv.fetchone()
  180. if feed_type == 1:
  181. feed_type = 0
  182. await asyncio.sleep(globals.MYANIMELIST_SECONDS_BETWEEN_REQUESTS)
  183. else:
  184. stop_boucle = 1
  185. except Exception as e:
  186. globals.logger.exception("Error when parsing RSS for '{}':\n".format(user.name))
  187. await asyncio.sleep(globals.MYANIMELIST_SECONDS_BETWEEN_REQUESTS)
  188. data_user = db_user.fetchone()
  189. async def fetch_activities_anilist():
  190. await anilist.check_new_activities()
  191. # Get a random anime name and change the bot's activity
  192. async def change_gameplayed(asyncioloop):
  193. globals.logger.info("Starting up change_gameplayed")
  194. await globals.client.wait_until_ready()
  195. await asyncio.sleep(1)
  196. while not globals.client.is_closed():
  197. # Get a random anime name from the users' list
  198. cursor = globals.conn.cursor(buffered=True)
  199. cursor.execute("SELECT title FROM t_animes ORDER BY RAND() LIMIT 1")
  200. data = cursor.fetchone()
  201. anime = utils.truncate_end_show(data[0])
  202. # Try to change the bot's activity
  203. try:
  204. if data is not None: await globals.client.change_presence(activity=discord.Activity(type=discord.ActivityType.watching, name=anime))
  205. except Exception as e:
  206. globals.logger.warning("An error occured while changing the displayed anime title: " + str(e))
  207. cursor.close()
  208. # Do it every minute
  209. await asyncio.sleep(60)
  210. async def update_thumbnail_catalog(asyncioloop):
  211. globals.logger.info("Starting up update_thumbnail_catalog")
  212. while not globals.client.is_closed():
  213. await asyncio.sleep(43200)
  214. globals.logger.info("Automatic check of the thumbnail database on going...")
  215. reload = 0
  216. cursor = globals.conn.cursor(buffered=True)
  217. cursor.execute("SELECT guid, title, thumbnail FROM t_animes")
  218. data = cursor.fetchone()
  219. while data is not None:
  220. try:
  221. if (data[2] != "") : urllib.request.urlopen(data[2])
  222. else: reload = 1
  223. except urllib.error.HTTPError as e:
  224. globals.logger.warning("HTTP Error while getting the current thumbnail of '" + str(data[1]) + "': " + str(e))
  225. reload = 1
  226. except Exception as e:
  227. globals.logger.debug("Error while getting the current thumbnail of '" + str(data[1]) + "': " + str(e))
  228. if (reload == 1) :
  229. try:
  230. image = myanimelist.get_thumbnail(data[0])
  231. cursor.execute("UPDATE t_animes SET thumbnail = %s WHERE guid = %s", [image, data[0]])
  232. globals.conn.commit()
  233. globals.logger.info("Updated thumbnail found for \"" + str(data[1]) + "\": %s", image)
  234. except Exception as e:
  235. globals.logger.warning("Error while downloading updated thumbnail for '" + str(data[1]) + "': " + str(e))
  236. await asyncio.sleep(3)
  237. data = cursor.fetchone()
  238. cursor.close()
  239. globals.logger.info("Thumbnail database checked.")