You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

318 lines
15KB

  1. # -*- coding: utf-8 -*-
  2. # MIT License
  3. #
  4. # Copyright (c) 2017-2018 Roxanne Gibson
  5. #
  6. # Permission is hereby granted, free of charge, to any person obtaining a copy
  7. # of this software and associated documentation files (the "Software"), to deal
  8. # in the Software without restriction, including without limitation the rights
  9. # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
  10. # copies of the Software, and to permit persons to whom the Software is
  11. # furnished to do so, subject to the following conditions:
  12. #
  13. # The above copyright notice and this permission notice shall be included in all
  14. # copies or substantial portions of the Software.
  15. #
  16. # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
  17. # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
  18. # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
  19. # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
  20. # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
  21. # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
  22. # SOFTWARE.
  23. import random
  24. import discord
  25. from discord.ext import commands
  26. import roxbot
  27. from roxbot.db import *
  28. class CCCommands(db.Entity):
  29. # Sadly no way to add a custom constraint to this while we either use pony or sqlite/
  30. name = Required(str)
  31. output = Required(Json)
  32. type = Required(int, py_check=lambda val: 0 <= val <= 2)
  33. guild_id = Required(int, size=64)
  34. composite_key(name, guild_id)
  35. class CustomCommands(commands.Cog):
  36. """The Custom Commands cog allows moderators to add custom commands for their Discord server to Roxbot. Allowing custom outputs predefined by the moderators.
  37. For example, we can set a command to require a prefix and call it "roxbot" and configure an output. Then if a user does `;roxbot` roxbot will output the configured output.
  38. """
  39. ERROR_AT_MENTION = "Custom Commands cannot mention people/roles/everyone."
  40. ERROR_COMMAND_NULL = "That Custom Command doesn't exist."
  41. ERROR_COMMAND_EXISTS = "Custom Command already exists."
  42. ERROR_COMMAND_EXISTS_INTERNAL = "This is already the name of a built in command."
  43. ERROR_EMBED_VALUE = "Not enough options given to generate embed."
  44. ERROR_INCORRECT_TYPE = "Incorrect type given."
  45. ERROR_OUTPUT_TOO_LONG = "Failed to set output. Given output was too long."
  46. ERROR_PREFIX_SPACE = "Custom commands with a prefix can only be one word with no spaces."
  47. OUTPUT_ADD = "{} has been added with the output: '{}'"
  48. OUTPUT_EDIT = "Edit made. {} now outputs {}"
  49. OUTPUT_REMOVE = "Removed {} custom command"
  50. def __init__(self, bot_client):
  51. self.bot = bot_client
  52. self.embed_fields = ("title", "description", "colour", "color", "footer", "image", "thumbnail", "url")
  53. @staticmethod
  54. def _get_output(command):
  55. # Check for a list as the output. If so, randomly select a item from the list.
  56. return random.choice(command)
  57. @staticmethod
  58. def _cc_to_embed(command_output):
  59. # discord.Embed.Empty is used by discord.py to denote when a field is empty. Hence why it is the fallback here
  60. title = command_output.get("title", discord.Embed.Empty)
  61. desc = command_output.get("description", discord.Embed.Empty)
  62. # Check for both possible colour fields. Then strip possible # and convert to hex for embed
  63. colour = command_output.get("colour", discord.Embed.Empty) or command_output.get("color", discord.Embed.Empty)
  64. if isinstance(colour, str):
  65. colour = discord.Colour(int(colour.strip("#"), 16))
  66. url = command_output.get("url", discord.Embed.Empty)
  67. footer = command_output.get("footer", discord.Embed.Empty)
  68. image = command_output.get("image", discord.Embed.Empty)
  69. thumbnail = command_output.get("thumbnail", discord.Embed.Empty)
  70. embed = discord.Embed(title=title, description=desc, colour=colour, url=url)
  71. if footer:
  72. embed.set_footer(text=footer)
  73. if image:
  74. embed.set_image(url=image)
  75. if thumbnail:
  76. embed.set_thumbnail(url=thumbnail)
  77. return embed
  78. def _embed_parse_options(self, options):
  79. # Create an dict from a list, taking each two items as a key value pair
  80. output = {item: options[index + 1] for index, item in enumerate(options) if index % 2 == 0}
  81. for key in output.copy().keys():
  82. if key not in self.embed_fields:
  83. output.pop(key)
  84. # Check for errors in inputs that would stop embed from being posted.
  85. title = output.get("title", "")
  86. footer = output.get("footer", "")
  87. if len(title) > 256 or len(footer) > 256:
  88. raise ValueError("Title or Footer must be smaller than 256 characters.")
  89. # We only need one so purge the inferior spelling
  90. if "colour" in output and "color" in output:
  91. output.pop("color")
  92. return output
  93. @commands.Cog.listener()
  94. async def on_message(self, message):
  95. """
  96. """
  97. # Emulate discord.py's feature of not running commands invoked by the bot (expects not to be used for self-botting)
  98. if message.author == self.bot.user:
  99. return
  100. # Limit custom commands to guilds only.
  101. if not isinstance(message.channel, discord.TextChannel):
  102. return
  103. # Emulate Roxbot's blacklist system
  104. if self.bot.blacklisted(message.author):
  105. raise commands.CheckFailure
  106. msg = message.content.lower()
  107. channel = message.channel
  108. with db_session:
  109. if msg.startswith(self.bot.command_prefix):
  110. command_name = msg.split(self.bot.command_prefix)[1]
  111. command = CCCommands.get(name=command_name, guild_id=message.guild.id)
  112. try:
  113. if command.type == 1:
  114. output = self._get_output(command.output)
  115. return await channel.send(output)
  116. elif command.type == 2:
  117. embed = self._cc_to_embed(command.output)
  118. return await channel.send(embed=embed)
  119. except AttributeError:
  120. pass
  121. else:
  122. try:
  123. command = CCCommands.get(name=msg, guild_id=message.guild.id, type=0)
  124. if command:
  125. output = self._get_output(command.output)
  126. return await channel.send(output)
  127. except:
  128. pass
  129. @commands.guild_only()
  130. @commands.group(aliases=["cc"])
  131. async def custom(self, ctx):
  132. """
  133. A group of commands to manage custom commands for your server.
  134. Requires the Manage Messages permission.
  135. """
  136. if ctx.invoked_subcommand is None:
  137. raise commands.CommandNotFound("Subcommand '{}' does not exist.".format(ctx.subcommand_passed))
  138. @commands.has_permissions(manage_messages=True)
  139. @custom.command()
  140. async def add(self, ctx, command_type, command, *output):
  141. """Adds a custom command to the list of custom commands.
  142. Options:
  143. - `type` - There are three types of custom commands.
  144. - `no_prefix`/`0` - These are custom commands that will trigger without a prefix. Example: a command named `test` will trigger when a user says `test` in chat.
  145. - `prefix`/`1` - These are custom commands that will trigger with a prefix. Example: a command named `test` will trigger when a user says `;test` in chat.
  146. - `embed`/`2` - These are prefix commands that will output a rich embed. [You can find out more about rich embeds from Discord's API documentation.](https://discordapp.com/developers/docs/resources/channel#embed-object) Embed types currently support these fields: `title, description, colour, color, url, footer, image, thumbnail`
  147. - `name` - The name of the command. No commands can have the same name.
  148. - `output` - The output of the command. The way you input this is determined by the type.
  149. `no_prefix` and `prefix` types support single outputs and also listing multiple outputs. When the latter is chosen, the output will be a random choice of the multiple outputs.
  150. Examples:
  151. # Add a no_prefix command called "test" with a URL output.
  152. ;cc add no_prefix test "https://www.youtube.com/watch?v=vJZp6awlL58"
  153. # Add a prefix command called test2 with a randomised output between "the person above me is annoying" and "the person above me is cool :sunglasses:"
  154. ;cc add prefix test2 "the person above me is annoying" "the person above me is cool :sunglasses:
  155. # Add an embed command called test3 with a title of "Title" and a description that is a markdown hyperlink to a youtube video, and the colour #deadbf
  156. ;cc add embed test3 title "Title" description "[Click here for a rad video](https://www.youtube.com/watch?v=dQw4w9WgXcQ)" colour #deadbf
  157. Note: With custom commands, it is important to remember that "" is used to pass any text with spaces as one argument. If the output you want requires the use of these characters, surround your output with three speech quotes at either side instead.
  158. """
  159. command = command.lower()
  160. if command_type in ("0", "no_prefix", "no prefix"):
  161. command_type = 0
  162. elif command_type in ("1", "prefix"):
  163. command_type = 1
  164. elif command_type in ("2", "embed"):
  165. command_type = 2
  166. if len(output) < 2:
  167. raise roxbot.UserError(self.ERROR_EMBED_VALUE)
  168. try:
  169. output = self._embed_parse_options(output)
  170. except ValueError:
  171. raise roxbot.UserError(self.ERROR_OUTPUT_TOO_LONG)
  172. else:
  173. raise roxbot.UserError(self.ERROR_INCORRECT_TYPE)
  174. with db_session:
  175. if ctx.message.mentions or ctx.message.mention_everyone or ctx.message.role_mentions:
  176. raise roxbot.UserError(self.ERROR_AT_MENTION)
  177. elif len(output) > 1800:
  178. raise roxbot.UserError(self.ERROR_OUTPUT_TOO_LONG)
  179. elif command in self.bot.all_commands.keys() and command_type == 1:
  180. raise roxbot.UserError(self.ERROR_COMMAND_EXISTS_INTERNAL)
  181. elif select(c for c in CCCommands if c.name == command and c.guild_id == ctx.guild.id).exists():
  182. raise roxbot.UserError(self.ERROR_COMMAND_EXISTS)
  183. elif len(command.split(" ")) > 1 and command_type == "1":
  184. raise roxbot.UserError(self.ERROR_PREFIX_SPACE)
  185. CCCommands(name=command, guild_id=ctx.guild.id, output=output, type=command_type)
  186. return await ctx.send(self.OUTPUT_ADD.format(command, output if len(output) > 1 else output[0]))
  187. @commands.has_permissions(manage_messages=True)
  188. @custom.command()
  189. async def edit(self, ctx, command, *edit):
  190. """Edits an existing custom command.
  191. Example:
  192. # edit a command called test to output "new output"
  193. ;cc edit test "new output"
  194. For more examples of how to setup a custom command, look at the help for the ;custom add command.
  195. You cannot change the type of a command. If you want to change the type, remove the command and re-add it.
  196. """
  197. if ctx.message.mentions or ctx.message.mention_everyone or ctx.message.role_mentions:
  198. raise roxbot.UserError(self.ERROR_AT_MENTION)
  199. if not edit:
  200. raise commands.BadArgument("Missing required argument: edit")
  201. with db_session:
  202. query = CCCommands.get(name=command.lower(), guild_id=ctx.guild.id)
  203. if query:
  204. if query.type == 2:
  205. if len(edit) < 2:
  206. raise roxbot.UserError(self.ERROR_EMBED_VALUE)
  207. try:
  208. edit = self._embed_parse_options(edit)
  209. query.output = edit
  210. return await ctx.send(self.OUTPUT_EDIT.format(command, edit))
  211. except ValueError:
  212. raise roxbot.UserError(self.ERROR_OUTPUT_TOO_LONG)
  213. else:
  214. query.output = edit
  215. return await ctx.send(self.OUTPUT_EDIT.format(command, edit if len(edit) > 1 else edit[0]))
  216. else:
  217. raise roxbot.UserError(self.ERROR_COMMAND_NULL)
  218. @commands.has_permissions(manage_messages=True)
  219. @custom.command()
  220. async def remove(self, ctx, command):
  221. """Removes a custom command.
  222. Example:
  223. # Remove custom command called "test"
  224. ;cc remove test
  225. """
  226. command = command.lower()
  227. with db_session:
  228. c = CCCommands.get(name=command, guild_id=ctx.guild.id)
  229. if c:
  230. c.delete()
  231. return await ctx.send(self.OUTPUT_REMOVE.format(command))
  232. else:
  233. raise roxbot.UserError(self.ERROR_COMMAND_NULL)
  234. @custom.command()
  235. async def list(self, ctx, debug="0"):
  236. """Lists all custom commands for this guild."""
  237. if debug != "0" and debug != "1":
  238. debug = "0"
  239. with db_session:
  240. no_prefix_commands = select(c for c in CCCommands if c.type == 0 and c.guild_id == ctx.guild.id)[:]
  241. prefix_commands = select(c for c in CCCommands if c.type == 1 and c.guild_id == ctx.guild.id)[:]
  242. embed_commands = select(c for c in CCCommands if c.type == 2 and c.guild_id == ctx.guild.id)[:]
  243. def add_commands(commands, paginator):
  244. if not commands:
  245. paginator.add_line("There are no commands setup.")
  246. else:
  247. for command in commands:
  248. output = command.name
  249. if debug == "1":
  250. output += " = '{}'".format(command.output if command.type == 2 else command.output[0])
  251. paginator.add_line("- " + output)
  252. paginator = commands.Paginator(prefix="```md")
  253. paginator.add_line("__Here is the list of Custom Commands...__")
  254. paginator.add_line()
  255. paginator.add_line("__Prefix Commands (Non Embeds):__")
  256. add_commands(prefix_commands, paginator)
  257. paginator.add_line()
  258. paginator.add_line("__Prefix Commands (Embeds):__")
  259. add_commands(embed_commands, paginator)
  260. paginator.add_line()
  261. paginator.add_line("__Commands that don't require prefix:__")
  262. add_commands(no_prefix_commands, paginator)
  263. for page in paginator.pages:
  264. await ctx.send(page)
  265. def setup(bot_client):
  266. bot_client.add_cog(CustomCommands(bot_client))