commands.py 18 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414
  1. import discord
  2. import urllib
  3. import datetime
  4. from typing import List, Tuple
  5. import myanimebot.utils as utils
  6. import myanimebot.globals as globals
  7. import myanimebot.anilist as anilist
  8. import myanimebot.database as database
  9. def build_info_cmd_message(users, server, channels, role, filters : List[utils.Service]) -> str:
  10. ''' Build the corresponding message for the info command '''
  11. registered_channel = globals.client.get_channel(int(channels[0]["channel"]))
  12. # Store users
  13. mal_users = []
  14. anilist_users = []
  15. for user in users:
  16. # If user is part of the server, add it to the message
  17. if str(server.id) in user['servers'].split(','):
  18. try:
  19. user_service = utils.Service.from_str(user["service"])
  20. if user_service == utils.Service.MAL:
  21. mal_users.append(user[globals.DB_USER_NAME])
  22. elif user_service == utils.Service.ANILIST:
  23. anilist_users.append(user[globals.DB_USER_NAME])
  24. except NotImplementedError:
  25. pass # Nothing to do here
  26. if not mal_users and not anilist_users:
  27. return "No users registered on this server. Try to add one."
  28. else:
  29. message = 'Registered user(s) on **{}**\n\n'.format(server)
  30. if mal_users: # If not empty
  31. # Don't print if there is filters and MAL is not in them
  32. if not filters or (filters and utils.Service.MAL in filters):
  33. message += '**MyAnimeList** users:\n'
  34. message += '```{}```\n'.format(', '.join(mal_users))
  35. if anilist_users: # If not empty
  36. # Don't print if there is filters and MAL is not in them
  37. if not filters or (filters and utils.Service.ANILIST in filters):
  38. message += '**AniList** users:\n'
  39. message += '```{}```\n'.format(', '.join(anilist_users))
  40. message += 'Assigned channel : **{}**'.format(registered_channel)
  41. if role is not None:
  42. message += '\nAllowed role: **{}**'.format(role)
  43. return message
  44. def get_service_filters_list(filters : str) -> List[utils.Service]:
  45. ''' Creates and returns a service filter list from a comma-separated string '''
  46. filters_list = []
  47. for filter in filters.split(','):
  48. try:
  49. filters_list.append(utils.Service.from_str(filter))
  50. except NotImplementedError:
  51. pass # Ignore incorrect filter
  52. return filters_list
  53. def in_allowed_role(user : discord.Member, server : int) -> bool :
  54. ''' Check if a user has the permissions to configure the bot on a specific server '''
  55. targetRole = utils.get_allowed_role(server.id)
  56. globals.logger.debug ("Role target: " + str(targetRole))
  57. if user.guild_permissions.administrator:
  58. globals.logger.debug (str(user) + " is server admin on " + str(server) + "!")
  59. return True
  60. elif (targetRole is None):
  61. globals.logger.debug ("No group specified for " + str(server))
  62. return True
  63. else:
  64. for role in user.roles:
  65. if str(role.id) == str(targetRole):
  66. globals.logger.debug ("Permissions validated for " + str(user))
  67. return True
  68. return False
  69. def check_user_name_validity(user_name: str, service : utils.Service) -> Tuple[bool, str]:
  70. """ Check if user_name exists on a specific service.
  71. Returns:
  72. - bool: True if user_name exists
  73. - str: Error string if the user does not exist
  74. """
  75. if service == utils.Service.MAL:
  76. try:
  77. # Ping user profile to check validity
  78. urllib.request.urlopen('{}{}'.format(globals.MAL_PROFILE_URL, user_name))
  79. except urllib.error.HTTPError as e:
  80. if (e.code == 404): # URL profile not found
  81. return False, "User **{}** doesn't exist on MyAnimeList!".format(user_name)
  82. else:
  83. globals.logger.warning("HTTP Code {} while trying to add user '{}' and checking its validity.".format(e.code, user_name))
  84. return False, "An error occured when we checked this username on MyAnimeList, maybe the website is down?"
  85. elif service == utils.Service.ANILIST:
  86. is_user_valid = anilist.check_username_validity(user_name)
  87. if is_user_valid == False:
  88. globals.logger.warning("No results returned while trying to add user '{}' and checking its validity.".format(user_name))
  89. return False, "User **{}** doesn't exist on AniList!".format(user_name)
  90. return True, None
  91. async def add_user_cmd(words, message):
  92. ''' Processes the command "add" and add a user to fetch the data for '''
  93. # Check if command is valid
  94. if len(words) != 4:
  95. if (len(words) < 4):
  96. return await message.channel.send("Usage: {} add **{}**/**{}** **username**".format(globals.prefix, globals.SERVICE_MAL, globals.SERVICE_ANILIST))
  97. return await message.channel.send("Too many arguments! You have to specify only one username.")
  98. # Verify that the user is allowed
  99. if in_allowed_role(message.author, message.guild) is False:
  100. return await message.channel.send("Only allowed users can use this command!")
  101. try:
  102. service = utils.Service.from_str(words[2])
  103. except NotImplementedError:
  104. return await message.channel.send('Incorrect service. Use **"{}"** or **"{}"** for example'.format(globals.SERVICE_MAL, globals.SERVICE_ANILIST))
  105. user = words[3]
  106. server_id = str(message.guild.id)
  107. if(len(user) > 14):
  108. return await message.channel.send("Username too long!")
  109. try:
  110. # Check user validity
  111. is_valid, error_string = check_user_name_validity(user, service)
  112. if is_valid == False:
  113. return await message.channel.send(error_string)
  114. # Get user's servers
  115. user_servers = utils.get_user_servers(user, service)
  116. # User not present in database
  117. if user_servers is None:
  118. utils.insert_user_into_db(user, service, server_id)
  119. return await message.channel.send("**{}** added to the database for the server **{}**.".format(user, str(message.guild)))
  120. else: # User present in database
  121. is_server_present = server_id in user_servers.split(',')
  122. if is_server_present == True: # The user already has registered this server
  123. return await message.channel.send("User **{}** is already registered in our database for this server!".format(user))
  124. else:
  125. new_servers = '{},{}'.format(user_servers, server_id)
  126. utils.update_user_servers_db(user, service, new_servers)
  127. return await message.channel.send("**{}** added to the database for the server **{}**.".format(user, str(message.guild)))
  128. except Exception as e:
  129. globals.logger.warning("Error while adding user '{}' on server '{}': {}".format(user, message.guild, str(e)))
  130. return await message.channel.send("An unknown error occured while addind this user, the error has been logged.")
  131. async def delete_user_cmd(words, message):
  132. ''' Processes the command "delete" and remove a registered user '''
  133. # Check if command is valid
  134. if len(words) != 4:
  135. if (len(words) < 4):
  136. return await message.channel.send("Usage: {} delete **{}**/**{}** **username**".format(globals.prefix, globals.SERVICE_MAL, globals.SERVICE_ANILIST))
  137. return await message.channel.send("Too many arguments! You have to specify only one username.")
  138. # Verify that the user is allowed
  139. if in_allowed_role(message.author, message.guild) is False:
  140. return await message.channel.send("Only allowed users can use this command!")
  141. try:
  142. service = utils.Service.from_str(words[2])
  143. except NotImplementedError:
  144. return await message.channel.send('Incorrect service. Use **"{}"** or **"{}"** for example'.format(globals.SERVICE_MAL, globals.SERVICE_ANILIST))
  145. user = words[3]
  146. server_id = str(message.guild.id)
  147. user_servers = utils.get_user_servers(user, service)
  148. # If user is not present in the database
  149. if user_servers is None:
  150. return await message.channel.send("The user **" + user + "** is not in our database for this server!")
  151. # Else if present, update the servers for this user
  152. srv_string = utils.remove_server_from_servers(server_id, user_servers)
  153. if srv_string is None: # Server not present in the user's servers
  154. return await message.channel.send("The user **" + user + "** is not in our database for this server!")
  155. if srv_string == "":
  156. utils.delete_user_from_db(user, service)
  157. else:
  158. utils.update_user_servers_db(user, service, srv_string)
  159. return await message.channel.send("**" + user + "** deleted from the database for this server.")
  160. async def info_cmd(message, words):
  161. ''' Processes the command "info" and sends a message '''
  162. # Get filters if available
  163. filters = []
  164. if (len(words) >= 3): # If filters are specified
  165. filters = get_service_filters_list(words[2])
  166. server = message.guild
  167. if utils.is_server_in_db(server.id) == False:
  168. await message.channel.send("The server **{}** is not in our database.".format(server))
  169. else:
  170. users = utils.get_users()
  171. channels = utils.get_channels(server.id)
  172. role = utils.get_allowed_role(server.id)
  173. if channels is None:
  174. await message.channel.send("No channel assigned for this bot on this server.")
  175. else:
  176. await message.channel.send(build_info_cmd_message(users, server, channels, utils.get_role_name(role, server), filters))
  177. async def ping_cmd(message, channel):
  178. ''' Responds to ping command '''
  179. messageTimestamp = message.created_at
  180. currentTimestamp = datetime.datetime.utcnow()
  181. delta = round((currentTimestamp - messageTimestamp).total_seconds() * 1000)
  182. await message.reply("pong ({}ms)".format(delta))
  183. async def about_cmd(channel):
  184. ''' Responds to about command with a brief description of this bot '''
  185. embed = discord.Embed(title="***MyAnimeBot Commands***", colour=0xEED000)
  186. embed.title = "MyAnimeBot version {} by Penta & lulu".format(globals.VERSION)
  187. embed.colour = 0xEED000
  188. embed.description = """MyAnimeBot checks MyAnimeList and Anilist profiles for specified users, and send a message for every new activities found.
  189. More help with the **{} help** command.
  190. Check our GitHub page for more informations: https://github.com/Penta/MyAnimeBot
  191. """.format(globals.prefix)
  192. embed.set_thumbnail(url=globals.iconBot)
  193. await channel.send(embed=embed)
  194. async def help_cmd(channel):
  195. ''' Responds to help command '''
  196. embed = discord.Embed(title="***MyAnimeBot Commands***", colour=0xEED000)
  197. embed.add_field(name="`here`", value="Register this channel. The bot will send new activities on registered channels.")
  198. embed.add_field(name="`stop`", value="Un-register this channel. The bot will now stop sending new activities for this channel.")
  199. embed.add_field(name="`info [mal|ani]`", value="Get the registered users for this server. Users can be filtered by specifying a service.")
  200. embed.add_field(name="`add {mal|ani} <user>`", value="Register a user for a specific service.\nEx: `add mal MyUser`")
  201. embed.add_field(name="`delete {mal|ani} <user>`", value="Remove a user for a specific service.\nEx: `delete ani MyUser`")
  202. embed.add_field(name="`role <@discord_role>`", value="Specify a role that is able to manage the bot.\nEx: `role @Moderator`, `role @everyone`")
  203. embed.add_field(name="`top`", value="Show statistics for this server.")
  204. embed.add_field(name="`ping`", value="Ping the bot.")
  205. embed.add_field(name="`about`", value="Get some information about this bot")
  206. await channel.send(embed=embed)
  207. async def here_cmd(author, server, channel):
  208. ''' Processes the command "here" and registers a channel to send new found feeds '''
  209. # Verify that the user is allowed
  210. if in_allowed_role(author, server) is False:
  211. return await channel.send("Only allowed users can use this command!")
  212. if utils.is_server_in_db(server.id):
  213. # Server in DB, so we need to update the channel
  214. # Check if channel already registered
  215. channels = utils.get_channels(server.id)
  216. channels_id = [channel["channel"] for channel in channels]
  217. globals.logger.debug("Channels {} and channel id {}".format(channels_id, channel.id))
  218. if (str(channel.id) in channels_id):
  219. await channel.send("Channel **{}** already in use for this server.".format(channel))
  220. else:
  221. cursor = database.create_cursor()
  222. cursor.execute("UPDATE t_servers SET channel = {} WHERE server = '{}'".format(channel.id, server.id))
  223. globals.conn.commit()
  224. await channel.send("Channel updated to: **{}**.".format(channel))
  225. cursor.close()
  226. else:
  227. # No server found in DB, so register it
  228. cursor = database.create_cursor()
  229. cursor.execute("INSERT INTO t_servers (server, channel) VALUES ('{}',{})".format(server.id, channel.id))
  230. globals.conn.commit() # TODO Move to corresponding file
  231. await channel.send("Channel **{}** configured for **{}**.".format(channel, server))
  232. async def stop_cmd(author, server, channel):
  233. ''' Processes the command "stop" and unregisters a channel '''
  234. # Verify that the user is allowed
  235. if in_allowed_role(author, server) is False:
  236. return await channel.send("Only allowed users can use this command!")
  237. if utils.is_server_in_db(server.id):
  238. # Remove server from DB
  239. cursor = database.create_cursor()
  240. cursor.execute("DELETE FROM t_servers WHERE server = {}".format(server.id))
  241. globals.conn.commit()
  242. await channel.send("Server **{}** is now unregistered from our database.".format(server))
  243. else:
  244. await channel.send("Server **{}** was already not registered.".format(server))
  245. async def role_cmd(words, message, author, server, channel):
  246. ''' Processes the command "role" and registers a role to be able to use the bot's commands '''
  247. if len(words) <= 2:
  248. return await channel.send("A role must be specified.")
  249. if not author.guild_permissions.administrator:
  250. return await channel.send("Only server's admins can use this command.")
  251. role_str = words[2]
  252. if (role_str == "everyone") or (role_str == "@everyone"):
  253. cursor = database.create_cursor()
  254. cursor.execute("UPDATE t_servers SET admin_group = NULL WHERE server = %s", [str(server.id)])
  255. globals.conn.commit()
  256. cursor.close()
  257. await channel.send("Everyone is now allowed to use the bot.")
  258. else: # A role is found
  259. rolesFound = message.role_mentions
  260. if (len(rolesFound) == 0):
  261. return await channel.send("Please specify a correct role.")
  262. elif (len(rolesFound) > 1):
  263. return await channel.send("Please specify only 1 role.")
  264. else:
  265. roleFound = rolesFound[0]
  266. # Update db with newly added role
  267. cursor = database.create_cursor()
  268. cursor.execute("UPDATE t_servers SET admin_group = %s WHERE server = %s", [str(roleFound.id), str(server.id)])
  269. globals.conn.commit()
  270. cursor.close()
  271. await channel.send("The role **{}** is now allowed to use this bot!".format(roleFound.name))
  272. async def top_cmd(words, channel):
  273. ''' Processes the command "top" and returns statistics on registered feeds '''
  274. # TODO Redo this function
  275. if len(words) == 2:
  276. try:
  277. cursor = database.create_cursor()
  278. cursor.execute("SELECT * FROM v_Top")
  279. data = cursor.fetchone()
  280. if data is None: await message.channel.send("It seems that there is no statistics... (what happened?!)")
  281. else:
  282. topText = "**__Here is the global statistics of this bot:__**\n\n"
  283. while data is not None:
  284. topText += " - " + str(data[0]) + ": " + str(data[1]) + "\n"
  285. data = cursor.fetchone()
  286. cursor = database.create_cursor()
  287. cursor.execute("SELECT * FROM v_TotalFeeds")
  288. data = cursor.fetchone()
  289. topText += "\n***Total user entry***: " + str(data[0])
  290. cursor = database.create_cursor()
  291. cursor.execute("SELECT * FROM v_TotalAnimes")
  292. data = cursor.fetchone()
  293. topText += "\n***Total unique manga/anime***: " + str(data[0])
  294. await channel.send(topText)
  295. cursor.close()
  296. except Exception as e:
  297. globals.logger.warning("An error occured while displaying the global top: " + str(e))
  298. await channel.send("Unable to reply to your request at the moment...")
  299. elif len(words) > 2:
  300. keyword = str(' '.join(words[2:]))
  301. globals.logger.info("Displaying the global top for the keyword: " + keyword)
  302. try:
  303. cursor = database.create_cursor()
  304. cursor.callproc('sp_UsersPerKeyword', [str(keyword), '20'])
  305. for result in cursor.stored_results():
  306. data = result.fetchone()
  307. if data is None: await message.channel.send("It seems that there is no statistics for the keyword **" + keyword + "**.")
  308. else:
  309. topKeyText = "**__Here is the statistics for the keyword " + keyword + ":__**\n\n"
  310. while data is not None:
  311. topKeyText += " - " + str(data[0]) + ": " + str(data[1]) + "\n"
  312. data = result.fetchone()
  313. await channel.send(topKeyText)
  314. cursor.close()
  315. except Exception as e:
  316. globals.logger.warning("An error occured while displaying the global top for keyword '" + keyword + "': " + str(e))
  317. await channel.send("Unable to reply to your request at the moment...")
  318. async def on_mention(channel):
  319. return await channel.send(":heart:")