1
0

discord.py 14 KB

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