PageRenderTime 52ms CodeModel.GetById 26ms RepoModel.GetById 0ms app.codeStats 0ms

/pcbot/builtin.py

https://gitlab.com/xZwop/PCBOT
Python | 434 lines | 391 code | 30 blank | 13 comment | 16 complexity | cd897dee13f3024c1d44aeae996d1af0 MD5 | raw file
  1. """ Plugin for built-in commands.
  2. This script works just like any of the plugins in plugins/
  3. """
  4. import random
  5. import logging
  6. from datetime import datetime, timedelta
  7. import importlib
  8. import discord
  9. import asyncio
  10. from pcbot import utils, Config, Annotate, config
  11. import plugins
  12. lambdas = Config("lambdas", data={})
  13. lambda_config = Config("lambda-config", data=dict(imports=[], blacklist=[]))
  14. code_globals = {}
  15. @plugins.command(name="help", aliases="commands")
  16. def help_(client: discord.Client, message: discord.Message, command: str.lower=None, *args):
  17. """ Display commands or their usage and description. """
  18. # Display the specific command
  19. if command:
  20. if command.startswith(config.command_prefix):
  21. command = command[1:]
  22. for plugin in plugins.all_values():
  23. cmd = plugins.get_command(plugin, command)
  24. if not cmd:
  25. continue
  26. # Get the specific command with arguments and send the help
  27. cmd = plugins.get_sub_command(cmd, args)
  28. yield from client.say(message, utils.format_help(cmd))
  29. break
  30. # Display every command
  31. else:
  32. commands = []
  33. for plugin in plugins.all_values():
  34. if getattr(plugin, "__commands", False): # Massive pile of shit that works (so sorry)
  35. commands.extend(
  36. cmd.name_prefix.split()[0] for cmd in plugin.__commands
  37. if not cmd.hidden and
  38. (not getattr(getattr(cmd, "function"), "__owner__", False) or
  39. utils.is_owner(message.author))
  40. )
  41. commands = ", ".join(sorted(commands))
  42. m = "**Commands**:```{0}```Use `{1}help <command>`, `{1}<command> {2}` or " \
  43. "`{1}<command> {3}` for command specific help.".format(
  44. commands, config.command_prefix, *config.help_arg)
  45. yield from client.say(message, m)
  46. @plugins.command(hidden=True)
  47. def setowner(client: discord.Client, message: discord.Message):
  48. """ Set the bot owner. Only works in private messages. """
  49. if not message.channel.is_private:
  50. return
  51. assert not utils.owner_cfg.data, "An owner is already set."
  52. owner_code = str(random.randint(100, 999))
  53. logging.critical("Owner code for assignment: {}".format(owner_code))
  54. yield from client.say(message,
  55. "A code has been printed in the console for you to repeat within 60 seconds.")
  56. user_code = yield from client.wait_for_message(timeout=60, channel=message.channel, content=owner_code)
  57. assert user_code, "You failed to send the desired code."
  58. if user_code:
  59. yield from client.say(message, "You have been assigned bot owner.")
  60. utils.owner_cfg.data = message.author.id
  61. utils.owner_cfg.save()
  62. @plugins.command()
  63. @utils.owner
  64. def stop(client: discord.Client, message: discord.Message):
  65. """ Stops the bot. """
  66. yield from client.say(message, ":boom: :gun:")
  67. yield from plugins.save_plugins()
  68. yield from client.logout()
  69. @plugins.command()
  70. @utils.owner
  71. def game(client: discord.Client, message: discord.Message, name: Annotate.Content=None):
  72. """ Stop playing or set game to `name`. """
  73. yield from client.change_status(discord.Game(name=name, type=0))
  74. if name:
  75. yield from client.say(message, "*Set the game to* **{}**.".format(name))
  76. else:
  77. yield from client.say(message, "*No longer playing.*")
  78. @game.command()
  79. @utils.owner
  80. def stream(client: discord.Client, message: discord.Message, url: str, title: Annotate.Content):
  81. """ Start streaming a game. """
  82. yield from client.change_status(discord.Game(name=title, url=url, type=1))
  83. yield from client.say(message, "Started streaming **{}**.".format(title))
  84. @plugins.command()
  85. @utils.owner
  86. def do(client: discord.Client, message: discord.Message, python_code: Annotate.Code):
  87. """ Execute python code. Coroutines do not work, although you can run `say(msg, c=message.channel)`
  88. to send a message, optionally to a channel. Eg: `say("Hello!")`. """
  89. def say(msg, m=message):
  90. asyncio.async(client.say(m, msg))
  91. code_globals.update(dict(say=say, message=message, client=client))
  92. try:
  93. exec(python_code, code_globals)
  94. except Exception as e:
  95. say("```" + utils.format_exception(e) + "```")
  96. @plugins.command(name="eval")
  97. @utils.owner
  98. def eval_(client: discord.Client, message: discord.Message, python_code: Annotate.Code):
  99. """ Evaluate a python expression. Can be any python code on one line that returns something. """
  100. code_globals.update(dict(message=message, client=client))
  101. try:
  102. result = eval(python_code, code_globals)
  103. except Exception as e:
  104. result = utils.format_exception(e)
  105. yield from client.say(message, "**Result:** \n```{}\n```".format(result))
  106. @plugins.command(name="plugin", hidden=True, aliases="pl")
  107. def plugin_(client: discord.Client, message: discord.Message):
  108. """ Manage plugins.
  109. **Owner command unless no argument is specified.** """
  110. yield from client.say(message,
  111. "**Plugins:** ```{}```".format(", ".join(plugins.all_keys())))
  112. @plugin_.command(aliases="r")
  113. @utils.owner
  114. def reload(client: discord.Client, message: discord.Message, name: str.lower=None):
  115. """ Reloads all plugins or the specified plugin. """
  116. if name:
  117. assert plugins.get_plugin(name), "`{}` is not a plugin".format(name)
  118. # The plugin entered is valid so we reload it
  119. yield from plugins.save_plugin(name)
  120. plugins.reload_plugin(name)
  121. yield from client.say(message, "Reloaded plugin `{}`.".format(name))
  122. else:
  123. # Reload all plugins
  124. yield from plugins.save_plugins()
  125. for plugin_name in plugins.all_keys():
  126. plugins.reload_plugin(plugin_name)
  127. yield from client.say(message, "All plugins reloaded.")
  128. @plugin_.command(error="You need to specify the name of the plugin to load.")
  129. @utils.owner
  130. def load(client: discord.Client, message: discord.Message, name: str.lower):
  131. """ Loads a plugin. """
  132. assert not plugins.get_plugin(name), "Plugin `{}` is already loaded.".format(name)
  133. # The plugin isn't loaded so we'll try to load it
  134. assert plugins.load_plugin(name), "Plugin `{}` could not be loaded.".format(name)
  135. # The plugin was loaded successfully
  136. yield from client.say(message, "Plugin `{}` loaded.".format(name))
  137. @plugin_.command(error="You need to specify the name of the plugin to unload.")
  138. @utils.owner
  139. def unload(client: discord.Client, message: discord.Message, name: str.lower):
  140. """ Unloads a plugin. """
  141. assert plugins.get_plugin(name), "`{}` is not a loaded plugin.".format(name)
  142. # The plugin is loaded so we unload it
  143. yield from plugins.save_plugin(name)
  144. plugins.unload_plugin(name)
  145. yield from client.say(message, "Plugin `{}` unloaded.".format(name))
  146. @plugins.command(name="lambda", hidden=True)
  147. def lambda_(client: discord.Client, message: discord.Message):
  148. """ Create commands. See `{pre}help do` for information on how the code works.
  149. **In addition**, there's the `arg(i, default=0)` function for getting arguments in positions,
  150. where the default argument is what to return when the argument does not exist.
  151. **Owner command unless no argument is specified.**"""
  152. yield from client.say(message,
  153. "**Lambdas:** ```\n" "{}```".format(", ".join(sorted(lambdas.data.keys()))))
  154. @lambda_.command(aliases="a")
  155. @utils.owner
  156. def add(client: discord.Client, message: discord.Message, trigger: str.lower, python_code: Annotate.Code):
  157. """ Add a command that runs the specified python code. """
  158. lambdas.data[trigger] = python_code
  159. lambdas.save()
  160. yield from client.say(message, "Command `{}` set.".format(trigger))
  161. @lambda_.command(aliases="r")
  162. @utils.owner
  163. def remove(client: discord.Client, message: discord.Message, trigger: str.lower):
  164. """ Remove a command. """
  165. assert trigger in lambdas.data, "Command `{}` does not exist.".format(trigger)
  166. # The command specified exists and we remove it
  167. del lambdas.data[trigger]
  168. lambdas.save()
  169. yield from client.say(message, "Command `{}` removed.".format(trigger))
  170. @lambda_.command()
  171. @utils.owner
  172. def enable(client: discord.Client, message: discord.Message, trigger: str.lower):
  173. """ Enable a command. """
  174. # If the specified trigger is in the blacklist, we remove it
  175. if trigger in lambda_config.data["blacklist"]:
  176. lambda_config.data["blacklist"].remove(trigger)
  177. lambda_config.save()
  178. yield from client.say(message, "Command `{}` enabled.".format(trigger))
  179. else:
  180. assert trigger in lambdas.data, "Command `{}` does not exist.".format(trigger)
  181. # The command exists so surely it must be disabled
  182. yield from client.say(message, "Command `{}` is already enabled.".format(trigger))
  183. @lambda_.command()
  184. @utils.owner
  185. def disable(client: discord.Client, message: discord.Message, trigger: str.lower):
  186. """ Disable a command. """
  187. # If the specified trigger is not in the blacklist, we add it
  188. if trigger not in lambda_config.data["blacklist"]:
  189. lambda_config.data["blacklist"].append(trigger)
  190. lambda_config.save()
  191. yield from client.say(message, "Command `{}` disabled.".format(trigger))
  192. else:
  193. assert trigger in lambdas.data, "Command `{}` does not exist.".format(trigger)
  194. # The command exists so surely it must be disabled
  195. yield from client.say(message, "Command `{}` is already disabled.".format(trigger))
  196. def import_module(module: str, attr: str=None):
  197. """ Remotely import a module or attribute from module into code_globals. """
  198. try:
  199. imported = importlib.import_module(module)
  200. except ImportError:
  201. e = "Unable to import module {}.".format(module)
  202. logging.error(e)
  203. raise ImportError(e)
  204. else:
  205. if attr:
  206. if hasattr(imported, attr):
  207. code_globals[attr] = getattr(imported, attr)
  208. else:
  209. e = "Module {} has no attribute {}.".format(module, attr)
  210. logging.error(e)
  211. raise KeyError(e)
  212. else:
  213. code_globals[module] = imported
  214. @lambda_.command(name="import")
  215. @utils.owner
  216. def import_(client: discord.Client, message: discord.Message, module: str, attr: str=None):
  217. """ Import the specified module. Specifying `attr` will act like `from attr import module`. """
  218. try:
  219. import_module(module, attr)
  220. except ImportError:
  221. yield from client.say(message, "Unable to import `{}`.".format(module))
  222. except KeyError:
  223. yield from client.say(message, "Unable to import `{}` from `{}`.".format(attr, module))
  224. else:
  225. # There were no errors when importing, so we add the name to our startup imports
  226. lambda_config.data["imports"].append((module, attr))
  227. lambda_config.save()
  228. yield from client.say(message, "Imported and setup `{}` for import.".format(attr or module))
  229. @lambda_.command()
  230. def source(client: discord.Client, message: discord.Message, trigger: str.lower):
  231. """ Disable source of a command """
  232. assert trigger in lambdas.data, "Command `{}` does not exist.".format(trigger)
  233. # The command exists so we display the source
  234. yield from client.say(message, "Source for `{}`:\n{}".format(trigger, lambdas.data[trigger]))
  235. @plugins.command()
  236. def ping(client: discord.Client, message: discord.Message):
  237. """ Tracks the time spent parsing the command and sending a message. """
  238. # Track the time it took to receive a message and send it.
  239. start_time = datetime.now()
  240. first_message = yield from client.say(message, "Pong!")
  241. stop_time = datetime.now()
  242. # Edit our message with the tracked time (in ms)
  243. time_elapsed = (stop_time - start_time).microseconds / 1000
  244. yield from client.edit_message(first_message,
  245. "Pong! `{elapsed:.4f}ms`".format(elapsed=time_elapsed))
  246. @asyncio.coroutine
  247. def get_changelog(num: int):
  248. """ Get the latest commit messages from PCBOT. """
  249. since = datetime.utcnow() - timedelta(days=7)
  250. commits = yield from utils.download_json("https://api.github.com/repos/{}commits".format(config.github_repo),
  251. since=since.strftime("%Y-%m-%dT00:00:00"))
  252. changelog = []
  253. # Go through every commit and add "- " in front of the first line and " " for all other lines
  254. # Also add dates after each commit
  255. for commit in commits[:num]:
  256. commit_message = commit["commit"]["message"]
  257. commit_date = commit["commit"]["committer"]["date"]
  258. formatted_commit = []
  259. for i, line in enumerate(commit_message.split("\n")):
  260. if not line == "":
  261. line = ("- " if i == 0 else " ") + line
  262. formatted_commit.append(line)
  263. # Add the date as well as the
  264. changelog.append("\n".join(formatted_commit) + "\n " + commit_date.replace("T", " ").replace("Z", ""))
  265. # Return formatted changelog
  266. return "```\n{}```".format("\n\n".join(changelog))
  267. @plugins.command(name=config.client_name.lower())
  268. def bot_info(client: discord.Client, message: discord.Message):
  269. """ Display basic information and changelog. """
  270. # Grab the latest commit
  271. # changelog = yield from get_changelog(1)
  272. yield from client.say(message, "**{ver}** - **{name}**\n"
  273. "__Github repo:__ <{repo}>\n"
  274. "__Owner (host):__ `{host}`\n"
  275. "__Up since:__ `{up}`\n"
  276. "__Messages since up date:__ `{mes}`\n"
  277. "__Servers connected to:__ `{servers}`".format(
  278. ver=config.version, name=client.user.name,
  279. up=client.time_started.strftime("%d-%m-%Y %H:%M:%S"), mes=len(client.messages),
  280. host=getattr(utils.get_member(client, utils.owner_cfg.data), "name", None) or "Not in this server.",
  281. servers=len(client.servers),
  282. repo="https://github.com/{}".format(config.github_repo),
  283. # changelog=changelog
  284. ))
  285. @bot_info.command(name="changelog")
  286. def changelog_(client: discord.Client, message: discord.Message, num: utils.int_range(f=1)=3):
  287. """ Get `num` requests from the changelog. Defaults to 3. """
  288. changelog = yield from get_changelog(num)
  289. yield from client.say(message, changelog)
  290. def init():
  291. """ Import any imports for lambdas. """
  292. # Add essential globals for "do", "eval" and "lambda" commands
  293. code_globals.update(dict(
  294. utils=utils,
  295. datetime=datetime,
  296. random=random,
  297. asyncio=asyncio,
  298. plugins=plugins
  299. ))
  300. # Import modules for "do", "eval" and "lambda" commands
  301. for module, attr in lambda_config.data["imports"]:
  302. # Remove any already imported modules
  303. if (attr or module) in code_globals:
  304. lambda_config.data["imports"].remove([module, attr])
  305. lambda_config.save()
  306. continue
  307. import_module(module, attr)
  308. @plugins.event()
  309. def on_message(client: discord.Client, message: discord.Message):
  310. """ Perform lambda commands. """
  311. args = utils.split(message.content)
  312. # Check if the command is a lambda command and is not disabled (in the blacklist)
  313. if args[0] in lambdas.data and args[0] not in lambda_config.data["blacklist"]:
  314. def say(msg, m=message):
  315. asyncio.async(client.say(m, msg))
  316. def arg(i, default=0):
  317. if len(args) > i:
  318. return args[i]
  319. else:
  320. return default
  321. code_globals.update(dict(arg=arg, say=say, args=args, message=message, client=client))
  322. # Execute the command
  323. try:
  324. exec(lambdas.data[args[0]], code_globals)
  325. except Exception as e:
  326. if utils.is_owner(message.author):
  327. say("```" + utils.format_exception(e) + "```")
  328. else:
  329. logging.warn("An exception occurred when parsing lambda command:"
  330. "\n{}".format(utils.format_exception(e)))
  331. return True
  332. # Initialize the plugin's modules
  333. init()