1
0

discord.py 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318
  1. import asyncio
  2. import urllib.request
  3. from configparser import ConfigParser
  4. from datetime import datetime
  5. from typing import List, Tuple
  6. import aiohttp
  7. import feedparser
  8. import pytz
  9. from dateutil.parser import parse as parse_datetime
  10. import discord
  11. # Our modules
  12. import myanimebot.anilist as anilist
  13. import myanimebot.healthcheck as healthcheck
  14. import myanimebot.commands as commands
  15. import myanimebot.globals as globals # TODO Rename globals module
  16. import myanimebot.myanimelist as myanimelist
  17. import myanimebot.utils as utils
  18. class MyAnimeBot(discord.Client):
  19. async def on_ready(self):
  20. globals.logger.info("Logged in as " + globals.client.user.name + " (" + str(globals.client.user.id) + ")")
  21. globals.logger.info("Starting all tasks...")
  22. if globals.MAL_ENABLED:
  23. globals.task_feed = globals.client.loop.create_task(background_check_feed(globals.client.loop))
  24. if globals.ANI_ENABLED:
  25. globals.task_feed_anilist = globals.client.loop.create_task(anilist.background_check_feed(globals.client.loop))
  26. if globals.HEALTHCHECK_ENABLED:
  27. globals.task_healthcheck = globals.client.loop.create_task(healthcheck.main(globals.client.loop))
  28. globals.task_thumbnail = globals.client.loop.create_task(update_thumbnail_catalog(globals.client.loop))
  29. globals.task_gameplayed = globals.client.loop.create_task(change_gameplayed(globals.client.loop))
  30. async def on_error(self, event, *args, **kwargs):
  31. globals.logger.exception("Crap! An unknown Discord error occured...")
  32. async def on_message(self, message):
  33. if message.author == globals.client.user: return
  34. words = message.content.strip().split()
  35. channel = message.channel
  36. author = str('{0.author.mention}'.format(message))
  37. # Check input validity
  38. if len(words) == 0:
  39. return
  40. # A user is trying to get help
  41. if words[0] == globals.prefix:
  42. if len(words) > 1:
  43. if words[1] == "ping":
  44. await commands.ping_cmd(message, channel)
  45. elif words[1] == "here":
  46. await commands.here_cmd(message.author, message.guild, channel)
  47. elif words[1] == "add":
  48. await commands.add_user_cmd(words, message)
  49. elif words[1] == "delete":
  50. await commands.delete_user_cmd(words, message)
  51. elif words[1] == "stop":
  52. await commands.stop_cmd(message.author, message.guild, channel)
  53. elif words[1] == "info":
  54. await commands.info_cmd(message, words)
  55. elif words[1] == "about":
  56. await commands.about_cmd(channel)
  57. elif words[1] == "help":
  58. await commands.help_cmd(channel)
  59. elif words[1] == "top":
  60. await commands.top_cmd(words, channel)
  61. elif words[1] == "role":
  62. await commands.role_cmd(words, message, message.author, message.guild, channel)
  63. # If mentioned
  64. elif globals.client.user in message.mentions:
  65. await commands.on_mention(channel)
  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.")