1
0

myanimebot.py 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424
  1. #!/usr/bin/env python3
  2. # Copyright Penta & lulu (c) 2018/2021 - Under BSD License - Based on feed2discord.py by Eric Eisenhart
  3. # Compatible for Python 3.7.X
  4. # Library import
  5. import asyncio
  6. import logging
  7. import sys
  8. import urllib.request
  9. import signal
  10. from configparser import ConfigParser
  11. from datetime import datetime
  12. from typing import List, Tuple
  13. import aiohttp
  14. import discord
  15. import feedparser
  16. from aiohttp.web_exceptions import HTTPError, HTTPNotModified
  17. from dateutil.parser import parse as parse_datetime
  18. from html2text import HTML2Text
  19. # Our modules
  20. import myanimebot.anilist as anilist
  21. import myanimebot.globals as globals
  22. import myanimebot.utils as utils
  23. import myanimebot.myanimelist as myanimelist
  24. import myanimebot.commands as commands
  25. from myanimebot.discord import send_embed_wrapper, build_embed, in_allowed_role
  26. if not sys.version_info[:2] >= (3, 7):
  27. print("ERROR: Requires python 3.7 or newer.")
  28. exit(1)
  29. def exit_app():
  30. logging.info("Closing all tasks...")
  31. globals.task_feed.cancel()
  32. globals.task_feed_anilist.cancel()
  33. globals.task_thumbnail.cancel()
  34. globals.task_gameplayed.cancel()
  35. globals.logger.critical("Script halted.")
  36. # Closing all ressources
  37. globals.conn.close()
  38. globals.log_cursor.close()
  39. globals.log_conn.close()
  40. # Main function that check the RSS feeds from MyAnimeList
  41. async def background_check_feed(asyncioloop):
  42. globals.logger.info("Starting up background_check_feed")
  43. # We configure the http header
  44. http_headers = { "User-Agent": "MyAnimeBot Discord Bot v" + globals.VERSION, }
  45. await globals.client.wait_until_ready()
  46. globals.logger.debug("Discord client connected, unlocking background_check_feed...")
  47. while not globals.client.is_closed():
  48. try:
  49. db_user = globals.conn.cursor(buffered=True, dictionary=True)
  50. db_user.execute("SELECT mal_user, servers FROM t_users WHERE service=%s", [globals.SERVICE_MAL])
  51. data_user = db_user.fetchone()
  52. except Exception as e:
  53. globals.logger.critical("Database unavailable! (" + str(e) + ")")
  54. quit()
  55. while data_user is not None:
  56. user = utils.User(id=None,
  57. service_id=None,
  58. name=data_user[globals.DB_USER_NAME],
  59. servers=data_user["servers"].split(','))
  60. stop_boucle = 0
  61. feed_type = 1
  62. try:
  63. while stop_boucle == 0 :
  64. try:
  65. async with aiohttp.ClientSession() as httpclient:
  66. if feed_type == 1 :
  67. http_response = await httpclient.request("GET", "https://myanimelist.net/rss.php?type=rm&u=" + user.name, headers=http_headers)
  68. media = "manga"
  69. else :
  70. http_response = await httpclient.request("GET", "https://myanimelist.net/rss.php?type=rw&u=" + user.name, headers=http_headers)
  71. media = "anime"
  72. except Exception as e:
  73. globals.logger.error("Error while loading RSS (" + str(feed_type) + ") of '" + user.name + "': " + str(e))
  74. break
  75. http_data = await http_response.read()
  76. feeds_data = feedparser.parse(http_data)
  77. for feed_data in feeds_data.entries:
  78. pubDateRaw = datetime.strptime(feed_data.published, '%a, %d %b %Y %H:%M:%S %z').astimezone(globals.timezone)
  79. pubDate = pubDateRaw.strftime("%Y-%m-%d %H:%M:%S")
  80. if feed_type == 1:
  81. media_type = utils.MediaType.MANGA
  82. else:
  83. media_type = utils.MediaType.ANIME
  84. feed = myanimelist.build_feed_from_data(feed_data, user, None, pubDateRaw.timestamp(), media_type)
  85. cursor = globals.conn.cursor(buffered=True)
  86. 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])
  87. data = cursor.fetchone()
  88. if data is None:
  89. var = datetime.now(globals.timezone) - pubDateRaw
  90. globals.logger.debug(" - " + feed.media.name + ": " + str(var.total_seconds()))
  91. if var.total_seconds() < globals.secondMax:
  92. globals.logger.info(user.name + ": Item '" + feed.media.name + "' not seen, processing...")
  93. cursor.execute("SELECT thumbnail FROM t_animes WHERE guid=%s AND service=%s LIMIT 1", [feed.media.url, globals.SERVICE_MAL]) # TODO Change that ?
  94. data_img = cursor.fetchone()
  95. if data_img is None:
  96. try:
  97. image = myanimelist.get_thumbnail(feed.media.url)
  98. globals.logger.info("First time seeing this " + media + ", adding thumbnail into database: " + image)
  99. except Exception as e:
  100. globals.logger.warning("Error while getting the thumbnail: " + str(e))
  101. image = ""
  102. 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])
  103. globals.conn.commit()
  104. else: image = data_img[0]
  105. feed.media.image = image
  106. 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])
  107. 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()))
  108. globals.conn.commit()
  109. for server in user.servers:
  110. db_srv = globals.conn.cursor(buffered=True)
  111. db_srv.execute("SELECT channel FROM t_servers WHERE server = %s", [server])
  112. data_channel = db_srv.fetchone()
  113. while data_channel is not None:
  114. for channel in data_channel: await send_embed_wrapper(asyncioloop, channel, globals.client, build_embed(feed))
  115. data_channel = db_srv.fetchone()
  116. if feed_type == 1:
  117. feed_type = 0
  118. await asyncio.sleep(globals.MYANIMELIST_SECONDS_BETWEEN_REQUESTS)
  119. else:
  120. stop_boucle = 1
  121. except Exception as e:
  122. globals.logger.exception("Error when parsing RSS for '" + user.name + "': \n")
  123. await asyncio.sleep(globals.MYANIMELIST_SECONDS_BETWEEN_REQUESTS)
  124. data_user = db_user.fetchone()
  125. async def fetch_activities_anilist():
  126. await anilist.check_new_activities()
  127. @globals.client.event
  128. async def on_ready():
  129. globals.logger.info("Logged in as " + globals.client.user.name + " (" + str(globals.client.user.id) + ")")
  130. globals.logger.info("Starting all tasks...")
  131. globals.task_feed = globals.client.loop.create_task(background_check_feed(globals.client.loop))
  132. globals.task_feed_anilist = globals.client.loop.create_task(anilist.background_check_feed(globals.client.loop))
  133. globals.task_thumbnail = globals.client.loop.create_task(update_thumbnail_catalog(globals.client.loop))
  134. globals.task_gameplayed = globals.client.loop.create_task(change_gameplayed(globals.client.loop))
  135. @globals.client.event
  136. async def on_error(event, *args, **kwargs):
  137. globals.logger.exception("Crap! An unknown Discord error occured...")
  138. @globals.client.event
  139. async def on_message(message):
  140. if message.author == globals.client.user: return
  141. words = message.content.split(" ")
  142. channel = message.channel
  143. author = str('{0.author.mention}'.format(message))
  144. # A user is trying to get help
  145. if words[0] == globals.prefix:
  146. if len(words) > 1:
  147. if words[1] == "ping":
  148. await commands.ping_cmd(message, channel)
  149. elif words[1] == "here":
  150. if in_allowed_role(message.author, message.guild):
  151. cursor = globals.conn.cursor(buffered=True)
  152. cursor.execute("SELECT server, channel FROM t_servers WHERE server=%s", [str(message.guild.id)])
  153. data = cursor.fetchone()
  154. if data is None:
  155. cursor.execute("INSERT INTO t_servers (server, channel) VALUES (%s,%s)", [str(message.guild.id), str(message.channel.id)])
  156. globals.conn.commit()
  157. await message.channel.send("Channel **" + str(message.channel) + "** configured for **" + str(message.guild) + "**.")
  158. else:
  159. if(data[1] == str(message.channel.id)): await message.channel.send("Channel **" + str(message.channel) + "** already in use for this server.")
  160. else:
  161. cursor.execute("UPDATE t_servers SET channel = %s WHERE server = %s", [str(message.channel.id), str(message.guild.id)])
  162. globals.conn.commit()
  163. await message.channel.send("Channel updated to: **" + str(message.channel) + "**.")
  164. cursor.close()
  165. else: await message.channel.send("Only allowed users can use this command!")
  166. elif words[1] == "add":
  167. if in_allowed_role(message.author, message.guild):
  168. await commands.add_user_cmd(words, message)
  169. else: await message.channel.send("Only allowed users can use this command!")
  170. elif words[1] == "delete":
  171. if in_allowed_role(message.author, message.guild):
  172. await commands.delete_user_cmd(words, message)
  173. else: await message.channel.send("Only allowed users can use this command!")
  174. elif words[1] == "stop":
  175. if in_allowed_role(message.author, message.guild):
  176. if (len(words) == 2):
  177. cursor = globals.conn.cursor(buffered=True)
  178. cursor.execute("SELECT server FROM t_servers WHERE server=%s", [str(message.guild.id)])
  179. data = cursor.fetchone()
  180. if data is None: await message.channel.send("The server **" + str(message.guild) + "** is not in our database.")
  181. else:
  182. cursor.execute("DELETE FROM t_servers WHERE server = %s", [message.guild.id])
  183. globals.conn.commit()
  184. await message.channel.send("Server **" + str(message.guild) + "** deleted from our database.")
  185. cursor.close()
  186. else: await message.channel.send("Too many arguments! Only type *stop* if you want to stop this bot on **" + message.guild + "**")
  187. else: await message.channel.send("Only allowed users can use this command!")
  188. elif words[1] == "info":
  189. await commands.info_cmd(message, words)
  190. elif words[1] == "about":
  191. await commands.about_cmd(channel)
  192. elif words[1] == "help":
  193. await commands.help_cmd(channel)
  194. elif words[1] == "top":
  195. if len(words) == 2:
  196. try:
  197. cursor = globals.conn.cursor(buffered=True)
  198. cursor.execute("SELECT * FROM v_Top")
  199. data = cursor.fetchone()
  200. if data is None: await message.channel.send("It seems that there is no statistics... (what happened?!)")
  201. else:
  202. topText = "**__Here is the global statistics of this bot:__**\n\n"
  203. while data is not None:
  204. topText += " - " + str(data[0]) + ": " + str(data[1]) + "\n"
  205. data = cursor.fetchone()
  206. cursor = globals.conn.cursor(buffered=True)
  207. cursor.execute("SELECT * FROM v_TotalFeeds")
  208. data = cursor.fetchone()
  209. topText += "\n***Total user entry***: " + str(data[0])
  210. cursor = globals.conn.cursor(buffered=True)
  211. cursor.execute("SELECT * FROM v_TotalAnimes")
  212. data = cursor.fetchone()
  213. topText += "\n***Total unique manga/anime***: " + str(data[0])
  214. await message.channel.send(topText)
  215. cursor.close()
  216. except Exception as e:
  217. globals.logger.warning("An error occured while displaying the global top: " + str(e))
  218. await message.channel.send("Unable to reply to your request at the moment...")
  219. elif len(words) > 2:
  220. keyword = str(' '.join(words[2:]))
  221. globals.logger.info("Displaying the global top for the keyword: " + keyword)
  222. try:
  223. cursor = globals.conn.cursor(buffered=True)
  224. cursor.callproc('sp_UsersPerKeyword', [str(keyword), '20'])
  225. for result in cursor.stored_results():
  226. data = result.fetchone()
  227. if data is None: await message.channel.send("It seems that there is no statistics for the keyword **" + keyword + "**.")
  228. else:
  229. topKeyText = "**__Here is the statistics for the keyword " + keyword + ":__**\n\n"
  230. while data is not None:
  231. topKeyText += " - " + str(data[0]) + ": " + str(data[1]) + "\n"
  232. data = result.fetchone()
  233. await message.channel.send(topKeyText)
  234. cursor.close()
  235. except Exception as e:
  236. globals.logger.warning("An error occured while displaying the global top for keyword '" + keyword + "': " + str(e))
  237. await message.channel.send("Unable to reply to your request at the moment...")
  238. elif words[1] == "role":
  239. if len(words) > 2:
  240. if message.author.guild_permissions.administrator:
  241. cursor = globals.conn.cursor(buffered=True)
  242. if (words[2] == "everyone") | (words[2] == "@everyone"):
  243. cursor.execute("UPDATE t_servers SET admin_group = NULL WHERE server = %s", [str(message.guild.id)])
  244. globals.conn.commit()
  245. await message.channel.send("Everybody is now allowed to use the bot!")
  246. else:
  247. rolesFound = message.role_mentions
  248. if (len(rolesFound) > 1): await message.channel.send("Please specify only 1 group!")
  249. elif (len(rolesFound) == 0): await message.channel.send("Please specify a correct group.")
  250. else:
  251. cursor.execute("UPDATE t_servers SET admin_group = %s WHERE server = %s", [str(rolesFound[0].id), str(message.guild.id)])
  252. globals.conn.commit()
  253. await message.channel.send("The role **" + str(rolesFound[0].name) + "** is now allowed to use this bot!")
  254. cursor.close()
  255. else: await message.channel.send("Only server's admins can use this command!")
  256. else:
  257. await message.channel.send("You have to specify a role!")
  258. # If mentioned
  259. elif globals.client.user in message.mentions:
  260. await channel.send(":heart:")
  261. # Get a random anime name and change the bot's activity
  262. async def change_gameplayed(asyncioloop):
  263. globals.logger.info("Starting up change_gameplayed")
  264. await globals.client.wait_until_ready()
  265. await asyncio.sleep(1)
  266. while not globals.client.is_closed():
  267. # Get a random anime name from the users' list
  268. cursor = globals.conn.cursor(buffered=True)
  269. cursor.execute("SELECT title FROM t_animes ORDER BY RAND() LIMIT 1")
  270. data = cursor.fetchone()
  271. anime = utils.truncate_end_show(data[0])
  272. # Try to change the bot's activity
  273. try:
  274. if data is not None: await globals.client.change_presence(activity=discord.Activity(type=discord.ActivityType.watching, name=anime))
  275. except Exception as e:
  276. globals.logger.warning("An error occured while changing the displayed anime title: " + str(e))
  277. cursor.close()
  278. # Do it every minute
  279. await asyncio.sleep(60)
  280. async def update_thumbnail_catalog(asyncioloop):
  281. globals.logger.info("Starting up update_thumbnail_catalog")
  282. while not globals.client.is_closed():
  283. await asyncio.sleep(43200)
  284. globals.logger.info("Automatic check of the thumbnail database on going...")
  285. reload = 0
  286. cursor = globals.conn.cursor(buffered=True)
  287. cursor.execute("SELECT guid, title, thumbnail FROM t_animes")
  288. data = cursor.fetchone()
  289. while data is not None:
  290. try:
  291. if (data[2] != "") : urllib.request.urlopen(data[2])
  292. else: reload = 1
  293. except urllib.error.HTTPError as e:
  294. globals.logger.warning("HTTP Error while getting the current thumbnail of '" + str(data[1]) + "': " + str(e))
  295. reload = 1
  296. except Exception as e:
  297. globals.logger.debug("Error while getting the current thumbnail of '" + str(data[1]) + "': " + str(e))
  298. if (reload == 1) :
  299. try:
  300. image = myanimelist.get_thumbnail(data[0])
  301. cursor.execute("UPDATE t_animes SET thumbnail = %s WHERE guid = %s", [image, data[0]])
  302. globals.conn.commit()
  303. globals.logger.info("Updated thumbnail found for \"" + str(data[1]) + "\": %s", image)
  304. except Exception as e:
  305. globals.logger.warning("Error while downloading updated thumbnail for '" + str(data[1]) + "': " + str(e))
  306. await asyncio.sleep(3)
  307. data = cursor.fetchone()
  308. cursor.close()
  309. globals.logger.info("Thumbnail database checked.")
  310. # Starting main function
  311. if __name__ == "__main__":
  312. # Catch SIGINT signal (Ctrl-C)
  313. signal.signal(signal.SIGINT, exit_app)
  314. # Run the app
  315. try:
  316. globals.client.run(globals.token)
  317. except Exception as e:
  318. globals.logger.error("Encountered exception while running the bot: {}".format(e))
  319. exit_app()