KiTTY, un bot Discord qui est un petit chat :) Il est basé sur une ancienne version du bot Red, sous Python 3.6 et qui a des fonctionnalités bien sympatiques !
25개 이상의 토픽을 선택하실 수 없습니다. Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
This repo is archived. You can view files and clone it, but cannot push or open issues/pull-requests.

1091 lines
39KB

  1. import discord
  2. from discord.ext import commands
  3. from cogs.utils import checks
  4. from cogs.utils.converters import GlobalUser
  5. from __main__ import set_cog
  6. from .utils.dataIO import dataIO
  7. from .utils.chat_formatting import pagify, box
  8. import importlib
  9. import traceback
  10. import logging
  11. import asyncio
  12. import threading
  13. import datetime
  14. import glob
  15. import os
  16. import aiohttp
  17. log = logging.getLogger("red.owner")
  18. class CogNotFoundError(Exception):
  19. pass
  20. class CogLoadError(Exception):
  21. pass
  22. class NoSetupError(CogLoadError):
  23. pass
  24. class CogUnloadError(Exception):
  25. pass
  26. class OwnerUnloadWithoutReloadError(CogUnloadError):
  27. pass
  28. class Owner:
  29. """All owner-only commands that relate to debug bot operations."""
  30. def __init__(self, bot):
  31. self.bot = bot
  32. self.setowner_lock = False
  33. self.disabled_commands = dataIO.load_json("data/red/disabled_commands.json")
  34. self.global_ignores = dataIO.load_json("data/red/global_ignores.json")
  35. self.session = aiohttp.ClientSession(loop=self.bot.loop)
  36. def __unload(self):
  37. self.session.close()
  38. @commands.command()
  39. @checks.is_owner()
  40. async def load(self, *, cog_name: str):
  41. """Loads a cog
  42. Example: load mod"""
  43. module = cog_name.strip()
  44. if "cogs." not in module:
  45. module = "cogs." + module
  46. try:
  47. self._load_cog(module)
  48. except CogNotFoundError:
  49. await self.bot.say("That cog could not be found.")
  50. except CogLoadError as e:
  51. log.exception(e)
  52. traceback.print_exc()
  53. await self.bot.say("There was an issue loading the cog. Check"
  54. " your console or logs for more information.")
  55. except Exception as e:
  56. log.exception(e)
  57. traceback.print_exc()
  58. await self.bot.say('Cog was found and possibly loaded but '
  59. 'something went wrong. Check your console '
  60. 'or logs for more information.')
  61. else:
  62. set_cog(module, True)
  63. await self.disable_commands()
  64. await self.bot.say("The cog has been loaded.")
  65. @commands.group(invoke_without_command=True)
  66. @checks.is_owner()
  67. async def unload(self, *, cog_name: str):
  68. """Unloads a cog
  69. Example: unload mod"""
  70. module = cog_name.strip()
  71. if "cogs." not in module:
  72. module = "cogs." + module
  73. if not self._does_cogfile_exist(module):
  74. await self.bot.say("That cog file doesn't exist. I will not"
  75. " turn off autoloading at start just in case"
  76. " this isn't supposed to happen.")
  77. else:
  78. set_cog(module, False)
  79. try: # No matter what we should try to unload it
  80. self._unload_cog(module)
  81. except OwnerUnloadWithoutReloadError:
  82. await self.bot.say("I cannot allow you to unload the Owner plugin"
  83. " unless you are in the process of reloading.")
  84. except CogUnloadError as e:
  85. log.exception(e)
  86. traceback.print_exc()
  87. await self.bot.say('Unable to safely unload that cog.')
  88. else:
  89. await self.bot.say("The cog has been unloaded.")
  90. @unload.command(name="all")
  91. @checks.is_owner()
  92. async def unload_all(self):
  93. """Unloads all cogs"""
  94. cogs = self._list_cogs()
  95. still_loaded = []
  96. for cog in cogs:
  97. set_cog(cog, False)
  98. try:
  99. self._unload_cog(cog)
  100. except OwnerUnloadWithoutReloadError:
  101. pass
  102. except CogUnloadError as e:
  103. log.exception(e)
  104. traceback.print_exc()
  105. still_loaded.append(cog)
  106. if still_loaded:
  107. still_loaded = ", ".join(still_loaded)
  108. await self.bot.say("I was unable to unload these cogs: "
  109. "{}".format(still_loaded))
  110. else:
  111. await self.bot.say("All cogs are now unloaded.")
  112. @checks.is_owner()
  113. @commands.command(name="reload")
  114. async def _reload(self, *, cog_name: str):
  115. """Reloads a cog
  116. Example: reload audio"""
  117. module = cog_name.strip()
  118. if "cogs." not in module:
  119. module = "cogs." + module
  120. try:
  121. self._unload_cog(module, reloading=True)
  122. except:
  123. pass
  124. try:
  125. self._load_cog(module)
  126. except CogNotFoundError:
  127. await self.bot.say("That cog cannot be found.")
  128. except NoSetupError:
  129. await self.bot.say("That cog does not have a setup function.")
  130. except CogLoadError as e:
  131. log.exception(e)
  132. traceback.print_exc()
  133. await self.bot.say("That cog could not be loaded. Check your"
  134. " console or logs for more information.")
  135. else:
  136. set_cog(module, True)
  137. await self.disable_commands()
  138. await self.bot.say("The cog has been reloaded.")
  139. @commands.command(name="cogs")
  140. @checks.is_owner()
  141. async def _show_cogs(self):
  142. """Shows loaded/unloaded cogs"""
  143. # This function assumes that all cogs are in the cogs folder,
  144. # which is currently true.
  145. # Extracting filename from __module__ Example: cogs.owner
  146. loaded = [c.__module__.split(".")[1] for c in self.bot.cogs.values()]
  147. # What's in the folder but not loaded is unloaded
  148. unloaded = [c.split(".")[1] for c in self._list_cogs()
  149. if c.split(".")[1] not in loaded]
  150. if not unloaded:
  151. unloaded = ["None"]
  152. msg = ("+ Loaded\n"
  153. "{}\n\n"
  154. "- Unloaded\n"
  155. "{}"
  156. "".format(", ".join(sorted(loaded)),
  157. ", ".join(sorted(unloaded)))
  158. )
  159. for page in pagify(msg, [" "], shorten_by=16):
  160. await self.bot.say(box(page.lstrip(" "), lang="diff"))
  161. @commands.command(pass_context=True, hidden=True)
  162. @checks.is_owner()
  163. async def debug(self, ctx, *, code):
  164. """Evaluates code"""
  165. def check(m):
  166. if m.content.strip().lower() == "more":
  167. return True
  168. author = ctx.message.author
  169. channel = ctx.message.channel
  170. code = code.strip('` ')
  171. result = None
  172. global_vars = globals().copy()
  173. global_vars['bot'] = self.bot
  174. global_vars['ctx'] = ctx
  175. global_vars['message'] = ctx.message
  176. global_vars['author'] = ctx.message.author
  177. global_vars['channel'] = ctx.message.channel
  178. global_vars['server'] = ctx.message.server
  179. try:
  180. result = eval(code, global_vars, locals())
  181. except Exception as e:
  182. await self.bot.say(box('{}: {}'.format(type(e).__name__, str(e)),
  183. lang="py"))
  184. return
  185. if asyncio.iscoroutine(result):
  186. result = await result
  187. result = str(result)
  188. if not ctx.message.channel.is_private:
  189. censor = (self.bot.settings.email,
  190. self.bot.settings.password,
  191. self.bot.settings.token)
  192. r = "[EXPUNGED]"
  193. for w in censor:
  194. if w is None or w == "":
  195. continue
  196. result = result.replace(w, r)
  197. result = result.replace(w.lower(), r)
  198. result = result.replace(w.upper(), r)
  199. result = list(pagify(result, shorten_by=16))
  200. for i, page in enumerate(result):
  201. if i != 0 and i % 4 == 0:
  202. last = await self.bot.say("There are still {} messages. "
  203. "Type `more` to continue."
  204. "".format(len(result) - (i+1)))
  205. msg = await self.bot.wait_for_message(author=author,
  206. channel=channel,
  207. check=check,
  208. timeout=10)
  209. if msg is None:
  210. try:
  211. await self.bot.delete_message(last)
  212. except:
  213. pass
  214. finally:
  215. break
  216. await self.bot.say(box(page, lang="py"))
  217. @commands.group(name="set", pass_context=True)
  218. async def _set(self, ctx):
  219. """Changes Red's core settings"""
  220. if ctx.invoked_subcommand is None:
  221. await self.bot.send_cmd_help(ctx)
  222. return
  223. @_set.command(pass_context=True)
  224. async def owner(self, ctx):
  225. """Sets owner"""
  226. if self.bot.settings.no_prompt is True:
  227. await self.bot.say("Console interaction is disabled. Start Red "
  228. "without the `--no-prompt` flag to use this "
  229. "command.")
  230. return
  231. if self.setowner_lock:
  232. await self.bot.say("A set owner command is already pending.")
  233. return
  234. if self.bot.settings.owner is not None:
  235. await self.bot.say(
  236. "The owner is already set. Remember that setting the owner "
  237. "to someone else other than who hosts the bot has security "
  238. "repercussions and is *NOT recommended*. Proceed at your own risk."
  239. )
  240. await asyncio.sleep(3)
  241. await self.bot.say("Confirm in the console that you're the owner.")
  242. self.setowner_lock = True
  243. t = threading.Thread(target=self._wait_for_answer,
  244. args=(ctx.message.author,))
  245. t.start()
  246. @_set.command()
  247. @checks.is_owner()
  248. async def defaultmodrole(self, *, role_name: str):
  249. """Sets the default mod role name
  250. This is used if a server-specific role is not set"""
  251. self.bot.settings.default_mod = role_name
  252. self.bot.settings.save_settings()
  253. await self.bot.say("The default mod role name has been set.")
  254. @_set.command()
  255. @checks.is_owner()
  256. async def defaultadminrole(self, *, role_name: str):
  257. """Sets the default admin role name
  258. This is used if a server-specific role is not set"""
  259. self.bot.settings.default_admin = role_name
  260. self.bot.settings.save_settings()
  261. await self.bot.say("The default admin role name has been set.")
  262. @_set.command(pass_context=True)
  263. @checks.is_owner()
  264. async def prefix(self, ctx, *prefixes):
  265. """Sets Red's global prefixes
  266. Accepts multiple prefixes separated by a space. Enclose in double
  267. quotes if a prefix contains spaces.
  268. Example: set prefix ! $ ? "two words" """
  269. if prefixes == ():
  270. await self.bot.send_cmd_help(ctx)
  271. return
  272. self.bot.settings.prefixes = sorted(prefixes, reverse=True)
  273. self.bot.settings.save_settings()
  274. log.debug("Setting global prefixes to:\n\t{}"
  275. "".format(self.bot.settings.prefixes))
  276. p = "prefixes" if len(prefixes) > 1 else "prefix"
  277. await self.bot.say("Global {} set".format(p))
  278. @_set.command(pass_context=True, no_pm=True)
  279. @checks.serverowner_or_permissions(administrator=True)
  280. async def serverprefix(self, ctx, *prefixes):
  281. """Sets Red's prefixes for this server
  282. Accepts multiple prefixes separated by a space. Enclose in double
  283. quotes if a prefix contains spaces.
  284. Example: set serverprefix ! $ ? "two words"
  285. Issuing this command with no parameters will reset the server
  286. prefixes and the global ones will be used instead."""
  287. server = ctx.message.server
  288. if prefixes == ():
  289. self.bot.settings.set_server_prefixes(server, [])
  290. self.bot.settings.save_settings()
  291. current_p = ", ".join(self.bot.settings.prefixes)
  292. await self.bot.say("Server prefixes reset. Current prefixes: "
  293. "`{}`".format(current_p))
  294. return
  295. prefixes = sorted(prefixes, reverse=True)
  296. self.bot.settings.set_server_prefixes(server, prefixes)
  297. self.bot.settings.save_settings()
  298. log.debug("Setting server's {} prefixes to:\n\t{}"
  299. "".format(server.id, self.bot.settings.prefixes))
  300. p = "Prefixes" if len(prefixes) > 1 else "Prefix"
  301. await self.bot.say("{} set for this server.\n"
  302. "To go back to the global prefixes, do"
  303. " `{}set serverprefix` "
  304. "".format(p, prefixes[0]))
  305. @_set.command(pass_context=True)
  306. @checks.is_owner()
  307. async def name(self, ctx, *, name):
  308. """Sets Red's name"""
  309. name = name.strip()
  310. if name != "":
  311. try:
  312. await self.bot.edit_profile(self.bot.settings.password,
  313. username=name)
  314. except:
  315. await self.bot.say("Failed to change name. Remember that you"
  316. " can only do it up to 2 times an hour."
  317. "Use nicknames if you need frequent "
  318. "changes. {}set nickname"
  319. "".format(ctx.prefix))
  320. else:
  321. await self.bot.say("Done.")
  322. else:
  323. await self.bot.send_cmd_help(ctx)
  324. @_set.command(pass_context=True, no_pm=True)
  325. @checks.is_owner()
  326. async def nickname(self, ctx, *, nickname=""):
  327. """Sets Red's nickname
  328. Leaving this empty will remove it."""
  329. nickname = nickname.strip()
  330. if nickname == "":
  331. nickname = None
  332. try:
  333. await self.bot.change_nickname(ctx.message.server.me, nickname)
  334. await self.bot.say("Done.")
  335. except discord.Forbidden:
  336. await self.bot.say("I cannot do that, I lack the "
  337. "\"Change Nickname\" permission.")
  338. @_set.command(pass_context=True)
  339. @checks.is_owner()
  340. async def game(self, ctx, *, game=None):
  341. """Sets Red's playing status
  342. Leaving this empty will clear it."""
  343. server = ctx.message.server
  344. current_status = server.me.status if server is not None else None
  345. if game:
  346. game = game.strip()
  347. await self.bot.change_presence(game=discord.Game(name=game),
  348. status=current_status)
  349. log.debug('Status set to "{}" by owner'.format(game))
  350. else:
  351. await self.bot.change_presence(game=None, status=current_status)
  352. log.debug('status cleared by owner')
  353. await self.bot.say("Done.")
  354. @_set.command(pass_context=True)
  355. @checks.is_owner()
  356. async def status(self, ctx, *, status=None):
  357. """Sets Red's status
  358. Statuses:
  359. online
  360. idle
  361. dnd
  362. invisible"""
  363. statuses = {
  364. "online" : discord.Status.online,
  365. "idle" : discord.Status.idle,
  366. "dnd" : discord.Status.dnd,
  367. "invisible" : discord.Status.invisible
  368. }
  369. server = ctx.message.server
  370. current_game = server.me.game if server is not None else None
  371. if status is None:
  372. await self.bot.change_presence(status=discord.Status.online,
  373. game=current_game)
  374. await self.bot.say("Status reset.")
  375. else:
  376. status = statuses.get(status.lower(), None)
  377. if status:
  378. await self.bot.change_presence(status=status,
  379. game=current_game)
  380. await self.bot.say("Status changed.")
  381. else:
  382. await self.bot.send_cmd_help(ctx)
  383. @_set.command(pass_context=True)
  384. @checks.is_owner()
  385. async def stream(self, ctx, streamer=None, *, stream_title=None):
  386. """Sets Red's streaming status
  387. Leaving both streamer and stream_title empty will clear it."""
  388. server = ctx.message.server
  389. current_status = server.me.status if server is not None else None
  390. if stream_title:
  391. stream_title = stream_title.strip()
  392. if "twitch.tv/" not in streamer:
  393. streamer = "https://www.twitch.tv/" + streamer
  394. game = discord.Game(type=1, url=streamer, name=stream_title)
  395. await self.bot.change_presence(game=game, status=current_status)
  396. log.debug('Owner has set streaming status and url to "{}" and {}'.format(stream_title, streamer))
  397. elif streamer is not None:
  398. await self.bot.send_cmd_help(ctx)
  399. return
  400. else:
  401. await self.bot.change_presence(game=None, status=current_status)
  402. log.debug('stream cleared by owner')
  403. await self.bot.say("Done.")
  404. @_set.command()
  405. @checks.is_owner()
  406. async def avatar(self, url):
  407. """Sets Red's avatar"""
  408. try:
  409. async with self.session.get(url) as r:
  410. data = await r.read()
  411. await self.bot.edit_profile(self.bot.settings.password, avatar=data)
  412. await self.bot.say("Done.")
  413. log.debug("changed avatar")
  414. except Exception as e:
  415. await self.bot.say("Error, check your console or logs for "
  416. "more information.")
  417. log.exception(e)
  418. traceback.print_exc()
  419. @_set.command(name="token")
  420. @checks.is_owner()
  421. async def _token(self, token):
  422. """Sets Red's login token"""
  423. if len(token) < 50:
  424. await self.bot.say("Invalid token.")
  425. else:
  426. self.bot.settings.token = token
  427. self.bot.settings.save_settings()
  428. await self.bot.say("Token set. Restart me.")
  429. log.debug("Token changed.")
  430. @_set.command(name="adminrole", pass_context=True, no_pm=True)
  431. @checks.serverowner()
  432. async def _server_adminrole(self, ctx, *, role: discord.Role):
  433. """Sets the admin role for this server"""
  434. server = ctx.message.server
  435. if server.id not in self.bot.settings.servers:
  436. await self.bot.say("Remember to set modrole too.")
  437. self.bot.settings.set_server_admin(server, role.name)
  438. await self.bot.say("Admin role set to '{}'".format(role.name))
  439. @_set.command(name="modrole", pass_context=True, no_pm=True)
  440. @checks.serverowner()
  441. async def _server_modrole(self, ctx, *, role: discord.Role):
  442. """Sets the mod role for this server"""
  443. server = ctx.message.server
  444. if server.id not in self.bot.settings.servers:
  445. await self.bot.say("Remember to set adminrole too.")
  446. self.bot.settings.set_server_mod(server, role.name)
  447. await self.bot.say("Mod role set to '{}'".format(role.name))
  448. @commands.group(pass_context=True)
  449. @checks.is_owner()
  450. async def blacklist(self, ctx):
  451. """Blacklist management commands
  452. Blacklisted users will be unable to issue commands"""
  453. if ctx.invoked_subcommand is None:
  454. await self.bot.send_cmd_help(ctx)
  455. @blacklist.command(name="add")
  456. async def _blacklist_add(self, user: GlobalUser):
  457. """Adds user to Red's global blacklist"""
  458. if user.id not in self.global_ignores["blacklist"]:
  459. self.global_ignores["blacklist"].append(user.id)
  460. self.save_global_ignores()
  461. await self.bot.say("User has been blacklisted.")
  462. else:
  463. await self.bot.say("User is already blacklisted.")
  464. @blacklist.command(name="remove")
  465. async def _blacklist_remove(self, user: GlobalUser):
  466. """Removes user from Red's global blacklist"""
  467. if user.id in self.global_ignores["blacklist"]:
  468. self.global_ignores["blacklist"].remove(user.id)
  469. self.save_global_ignores()
  470. await self.bot.say("User has been removed from the blacklist.")
  471. else:
  472. await self.bot.say("User is not blacklisted.")
  473. @blacklist.command(name="list")
  474. async def _blacklist_list(self):
  475. """Lists users on the blacklist"""
  476. blacklist = self._populate_list(self.global_ignores["blacklist"])
  477. if blacklist:
  478. for page in blacklist:
  479. await self.bot.say(box(page))
  480. else:
  481. await self.bot.say("The blacklist is empty.")
  482. @blacklist.command(name="clear")
  483. async def _blacklist_clear(self):
  484. """Clears the global blacklist"""
  485. self.global_ignores["blacklist"] = []
  486. self.save_global_ignores()
  487. await self.bot.say("Blacklist is now empty.")
  488. @commands.group(pass_context=True)
  489. @checks.is_owner()
  490. async def whitelist(self, ctx):
  491. """Whitelist management commands
  492. If the whitelist is not empty, only whitelisted users will
  493. be able to use Red"""
  494. if ctx.invoked_subcommand is None:
  495. await self.bot.send_cmd_help(ctx)
  496. @whitelist.command(name="add")
  497. async def _whitelist_add(self, user: GlobalUser):
  498. """Adds user to Red's global whitelist"""
  499. if user.id not in self.global_ignores["whitelist"]:
  500. if not self.global_ignores["whitelist"]:
  501. msg = "\nNon-whitelisted users will be ignored."
  502. else:
  503. msg = ""
  504. self.global_ignores["whitelist"].append(user.id)
  505. self.save_global_ignores()
  506. await self.bot.say("User has been whitelisted." + msg)
  507. else:
  508. await self.bot.say("User is already whitelisted.")
  509. @whitelist.command(name="remove")
  510. async def _whitelist_remove(self, user: GlobalUser):
  511. """Removes user from Red's global whitelist"""
  512. if user.id in self.global_ignores["whitelist"]:
  513. self.global_ignores["whitelist"].remove(user.id)
  514. self.save_global_ignores()
  515. await self.bot.say("User has been removed from the whitelist.")
  516. else:
  517. await self.bot.say("User is not whitelisted.")
  518. @whitelist.command(name="list")
  519. async def _whitelist_list(self):
  520. """Lists users on the whitelist"""
  521. whitelist = self._populate_list(self.global_ignores["whitelist"])
  522. if whitelist:
  523. for page in whitelist:
  524. await self.bot.say(box(page))
  525. else:
  526. await self.bot.say("The whitelist is empty.")
  527. @whitelist.command(name="clear")
  528. async def _whitelist_clear(self):
  529. """Clears the global whitelist"""
  530. self.global_ignores["whitelist"] = []
  531. self.save_global_ignores()
  532. await self.bot.say("Whitelist is now empty.")
  533. @commands.command()
  534. @checks.is_owner()
  535. async def shutdown(self, silently : bool=False):
  536. """Shuts down Red"""
  537. wave = "\N{WAVING HAND SIGN}"
  538. skin = "\N{EMOJI MODIFIER FITZPATRICK TYPE-3}"
  539. try: # We don't want missing perms to stop our shutdown
  540. if not silently:
  541. await self.bot.say("Shutting down... " + wave + skin)
  542. except:
  543. pass
  544. await self.bot.shutdown()
  545. @commands.command()
  546. @checks.is_owner()
  547. async def restart(self, silently : bool=False):
  548. """Attempts to restart Red
  549. Makes Red quit with exit code 26
  550. The restart is not guaranteed: it must be dealt
  551. with by the process manager in use"""
  552. try:
  553. if not silently:
  554. await self.bot.say("Restarting...")
  555. except:
  556. pass
  557. await self.bot.shutdown(restart=True)
  558. @commands.group(name="command", pass_context=True)
  559. @checks.is_owner()
  560. async def command_disabler(self, ctx):
  561. """Disables/enables commands
  562. With no subcommands returns the disabled commands list"""
  563. if ctx.invoked_subcommand is None:
  564. await self.bot.send_cmd_help(ctx)
  565. if self.disabled_commands:
  566. msg = "Disabled commands:\n```xl\n"
  567. for cmd in self.disabled_commands:
  568. msg += "{}, ".format(cmd)
  569. msg = msg.strip(", ")
  570. await self.bot.whisper("{}```".format(msg))
  571. @command_disabler.command()
  572. async def disable(self, *, command):
  573. """Disables commands/subcommands"""
  574. comm_obj = await self.get_command(command)
  575. if comm_obj is KeyError:
  576. await self.bot.say("That command doesn't seem to exist.")
  577. elif comm_obj is False:
  578. await self.bot.say("You cannot disable owner restricted commands.")
  579. else:
  580. comm_obj.enabled = False
  581. comm_obj.hidden = True
  582. self.disabled_commands.append(command)
  583. self.save_disabled_commands()
  584. await self.bot.say("Command has been disabled.")
  585. @command_disabler.command()
  586. async def enable(self, *, command):
  587. """Enables commands/subcommands"""
  588. if command in self.disabled_commands:
  589. self.disabled_commands.remove(command)
  590. self.save_disabled_commands()
  591. await self.bot.say("Command enabled.")
  592. else:
  593. await self.bot.say("That command is not disabled.")
  594. return
  595. try:
  596. comm_obj = await self.get_command(command)
  597. comm_obj.enabled = True
  598. comm_obj.hidden = False
  599. except: # In case it was in the disabled list but not currently loaded
  600. pass # No point in even checking what returns
  601. async def get_command(self, command):
  602. command = command.split()
  603. try:
  604. comm_obj = self.bot.commands[command[0]]
  605. if len(command) > 1:
  606. command.pop(0)
  607. for cmd in command:
  608. comm_obj = comm_obj.commands[cmd]
  609. except KeyError:
  610. return KeyError
  611. for check in comm_obj.checks:
  612. if hasattr(check, "__name__") and check.__name__ == "is_owner_check":
  613. return False
  614. return comm_obj
  615. async def disable_commands(self): # runs at boot
  616. for cmd in self.disabled_commands:
  617. cmd_obj = await self.get_command(cmd)
  618. try:
  619. cmd_obj.enabled = False
  620. cmd_obj.hidden = True
  621. except:
  622. pass
  623. @commands.command()
  624. @checks.is_owner()
  625. async def join(self):
  626. """Shows Red's invite URL"""
  627. if self.bot.user.bot:
  628. await self.bot.whisper("Invite URL: " + self.bot.oauth_url)
  629. @commands.command(pass_context=True, no_pm=True)
  630. @checks.is_owner()
  631. async def leave(self, ctx):
  632. """Leaves server"""
  633. message = ctx.message
  634. await self.bot.say("Are you sure you want me to leave this server?"
  635. " Type yes to confirm.")
  636. response = await self.bot.wait_for_message(author=message.author)
  637. if response.content.lower().strip() == "yes":
  638. await self.bot.say("Alright. Bye :wave:")
  639. log.debug('Leaving "{}"'.format(message.server.name))
  640. await self.bot.leave_server(message.server)
  641. else:
  642. await self.bot.say("Ok I'll stay here then.")
  643. @commands.command(pass_context=True)
  644. @checks.is_owner()
  645. async def servers(self, ctx):
  646. """Lists and allows to leave servers"""
  647. owner = ctx.message.author
  648. servers = sorted(list(self.bot.servers),
  649. key=lambda s: s.name.lower())
  650. msg = ""
  651. for i, server in enumerate(servers):
  652. msg += "{}: {}\n".format(i, server.name)
  653. msg += "\nTo leave a server just type its number."
  654. for page in pagify(msg, ['\n']):
  655. await self.bot.say(page)
  656. while msg is not None:
  657. msg = await self.bot.wait_for_message(author=owner, timeout=15)
  658. try:
  659. msg = int(msg.content)
  660. await self.leave_confirmation(servers[msg], owner, ctx)
  661. break
  662. except (IndexError, ValueError, AttributeError):
  663. pass
  664. async def leave_confirmation(self, server, owner, ctx):
  665. await self.bot.say("Are you sure you want me "
  666. "to leave {}? (yes/no)".format(server.name))
  667. msg = await self.bot.wait_for_message(author=owner, timeout=15)
  668. if msg is None:
  669. await self.bot.say("I guess not.")
  670. elif msg.content.lower().strip() in ("yes", "y"):
  671. await self.bot.leave_server(server)
  672. if server != ctx.message.server:
  673. await self.bot.say("Done.")
  674. else:
  675. await self.bot.say("Alright then.")
  676. @commands.command(pass_context=True)
  677. @commands.cooldown(1, 60, commands.BucketType.user)
  678. async def contact(self, ctx, *, message : str):
  679. """Sends a message to the owner"""
  680. if self.bot.settings.owner is None:
  681. await self.bot.say("I have no owner set.")
  682. return
  683. server = ctx.message.server
  684. owner = discord.utils.get(self.bot.get_all_members(),
  685. id=self.bot.settings.owner)
  686. author = ctx.message.author
  687. footer = "User ID: " + author.id
  688. if ctx.message.server is None:
  689. source = "through DM"
  690. else:
  691. source = "from {}".format(server)
  692. footer += " | Server ID: " + server.id
  693. if isinstance(author, discord.Member):
  694. colour = author.colour
  695. else:
  696. colour = discord.Colour.red()
  697. description = "Sent by {} {}".format(author, source)
  698. e = discord.Embed(colour=colour, description=message)
  699. if author.avatar_url:
  700. e.set_author(name=description, icon_url=author.avatar_url)
  701. else:
  702. e.set_author(name=description)
  703. e.set_footer(text=footer)
  704. try:
  705. await self.bot.send_message(owner, embed=e)
  706. except discord.InvalidArgument:
  707. await self.bot.say("I cannot send your message, I'm unable to find"
  708. " my owner... *sigh*")
  709. except discord.HTTPException:
  710. await self.bot.say("Your message is too long.")
  711. except:
  712. await self.bot.say("I'm unable to deliver your message. Sorry.")
  713. else:
  714. await self.bot.say("Your message has been sent.")
  715. @commands.command()
  716. async def info(self):
  717. """Shows info about Red"""
  718. author_repo = "https://git.drycat.fr/Geekcat/KiTTY-Discord-Bot/"
  719. red_repo = author_repo + ""
  720. server_url = "https://discord.io/techcordfr"
  721. dpy_repo = "https://github.com/"
  722. python_url = "https://www.python.org/"
  723. since = datetime.datetime(2017, 1, 2, 0, 0)
  724. days_since = (datetime.datetime.utcnow() - since).days
  725. dpy_version = "[{}]({})".format(discord.__version__, dpy_repo)
  726. py_version = "[{}.{}.{}]({})".format(*os.sys.version_info[:3],
  727. python_url)
  728. owner_set = self.bot.settings.owner is not None
  729. owner = self.bot.settings.owner if owner_set else None
  730. if owner:
  731. owner = discord.utils.get(self.bot.get_all_members(), id=owner)
  732. if not owner:
  733. try:
  734. owner = await self.bot.get_user_info(self.bot.settings.owner)
  735. except:
  736. owner = None
  737. if not owner:
  738. owner = "Unknown"
  739. about = (
  740. "This is [KiTTY, an open source cat Discord bot]({})."
  741. "".format(author_repo))
  742. embed = discord.Embed(colour=discord.Colour.red())
  743. embed.add_field(name="Instance owned by", value=str(owner))
  744. embed.add_field(name="Python", value=py_version)
  745. embed.add_field(name="discord.py", value=dpy_version)
  746. embed.add_field(name="About KiTTY", value=about, inline=False)
  747. embed.set_footer(text="Bringing joy since 2017 (over "
  748. "{} days ago!)".format(days_since))
  749. try:
  750. await self.bot.say(embed=embed)
  751. except discord.HTTPException:
  752. await self.bot.say("I need the `Embed links` permission "
  753. "to send this")
  754. @commands.command()
  755. async def uptime(self):
  756. """Shows Red's uptime"""
  757. since = self.bot.uptime.strftime("%Y-%m-%d %H:%M:%S")
  758. passed = self.get_bot_uptime()
  759. await self.bot.say("Been up for: **{}** (since {} UTC)"
  760. "".format(passed, since))
  761. @commands.command()
  762. async def version(self):
  763. """Shows Red's current version"""
  764. response = self.bot.loop.run_in_executor(None, self._get_version)
  765. result = await asyncio.wait_for(response, timeout=10)
  766. try:
  767. await self.bot.say(embed=result)
  768. except discord.HTTPException:
  769. await self.bot.say("I need the `Embed links` permission "
  770. "to send this")
  771. @commands.command(pass_context=True)
  772. @checks.is_owner()
  773. async def traceback(self, ctx, public: bool=False):
  774. """Sends to the owner the last command exception that has occurred
  775. If public (yes is specified), it will be sent to the chat instead"""
  776. if not public:
  777. destination = ctx.message.author
  778. else:
  779. destination = ctx.message.channel
  780. if self.bot._last_exception:
  781. for page in pagify(self.bot._last_exception):
  782. await self.bot.send_message(destination, box(page, lang="py"))
  783. else:
  784. await self.bot.say("No exception has occurred yet.")
  785. def _populate_list(self, _list):
  786. """Used for both whitelist / blacklist
  787. Returns a paginated list"""
  788. users = []
  789. total = len(_list)
  790. for user_id in _list:
  791. user = discord.utils.get(self.bot.get_all_members(), id=user_id)
  792. if user:
  793. users.append("{} ({})".format(user, user.id))
  794. if users:
  795. not_found = total - len(users)
  796. users = ", ".join(users)
  797. if not_found:
  798. users += "\n\n ... and {} users I could not find".format(not_found)
  799. return list(pagify(users, delims=[" ", "\n"]))
  800. return []
  801. def _load_cog(self, cogname):
  802. if not self._does_cogfile_exist(cogname):
  803. raise CogNotFoundError(cogname)
  804. try:
  805. mod_obj = importlib.import_module(cogname)
  806. importlib.reload(mod_obj)
  807. self.bot.load_extension(mod_obj.__name__)
  808. except SyntaxError as e:
  809. raise CogLoadError(*e.args)
  810. except:
  811. raise
  812. def _unload_cog(self, cogname, reloading=False):
  813. if not reloading and cogname == "cogs.owner":
  814. raise OwnerUnloadWithoutReloadError(
  815. "Can't unload the owner plugin :P")
  816. try:
  817. self.bot.unload_extension(cogname)
  818. except:
  819. raise CogUnloadError
  820. def _list_cogs(self):
  821. cogs = [os.path.basename(f) for f in glob.glob("cogs/*.py")]
  822. return ["cogs." + os.path.splitext(f)[0] for f in cogs]
  823. def _does_cogfile_exist(self, module):
  824. if "cogs." not in module:
  825. module = "cogs." + module
  826. if module not in self._list_cogs():
  827. return False
  828. return True
  829. def _wait_for_answer(self, author):
  830. print(author.name + " requested to be set as owner. If this is you, "
  831. "type 'yes'. Otherwise press enter.")
  832. print()
  833. print("*DO NOT* set anyone else as owner. This has security "
  834. "repercussions.")
  835. choice = "None"
  836. while choice.lower() != "yes" and choice == "None":
  837. choice = input("> ")
  838. if choice == "yes":
  839. self.bot.settings.owner = author.id
  840. self.bot.settings.save_settings()
  841. print(author.name + " has been set as owner.")
  842. self.setowner_lock = False
  843. self.owner.hidden = True
  844. else:
  845. print("The set owner request has been ignored.")
  846. self.setowner_lock = False
  847. def _get_version(self):
  848. if not os.path.isdir(".git"):
  849. msg = "This instance of Red hasn't been installed with git."
  850. e = discord.Embed(title=msg,
  851. colour=discord.Colour.red())
  852. return e
  853. commands = " && ".join((
  854. r'git config --get remote.origin.url', # Remote URL
  855. r'git rev-list --count HEAD', # Number of commits
  856. r'git rev-parse --abbrev-ref HEAD', # Branch name
  857. r'git show -s -n 3 HEAD --format="%cr|%s|%H"' # Last 3 commits
  858. ))
  859. result = os.popen(commands).read()
  860. url, ncommits, branch, commits = result.split("\n", 3)
  861. if url.endswith(".git"):
  862. url = url[:-4]
  863. if url.startswith("git@"):
  864. domain, _, resource = url[4:].partition(':')
  865. url = 'https://{}/{}'.format(domain, resource)
  866. repo_name = url.split("/")[-1]
  867. embed = discord.Embed(title="Updates of " + repo_name,
  868. description="Last three updates",
  869. colour=discord.Colour.red(),
  870. url="{}/tree/{}".format(url, branch))
  871. for line in commits.split('\n'):
  872. if not line:
  873. continue
  874. when, commit, chash = line.split("|")
  875. commit_url = url + "/commit/" + chash
  876. content = "[{}]({}) - {} ".format(chash[:6], commit_url, commit)
  877. embed.add_field(name=when, value=content, inline=False)
  878. embed.set_footer(text="Total commits: " + ncommits)
  879. return embed
  880. def get_bot_uptime(self, *, brief=False):
  881. # Courtesy of Danny
  882. now = datetime.datetime.utcnow()
  883. delta = now - self.bot.uptime
  884. hours, remainder = divmod(int(delta.total_seconds()), 3600)
  885. minutes, seconds = divmod(remainder, 60)
  886. days, hours = divmod(hours, 24)
  887. if not brief:
  888. if days:
  889. fmt = '{d} days, {h} hours, {m} minutes, and {s} seconds'
  890. else:
  891. fmt = '{h} hours, {m} minutes, and {s} seconds'
  892. else:
  893. fmt = '{h}h {m}m {s}s'
  894. if days:
  895. fmt = '{d}d ' + fmt
  896. return fmt.format(d=days, h=hours, m=minutes, s=seconds)
  897. def save_global_ignores(self):
  898. dataIO.save_json("data/red/global_ignores.json", self.global_ignores)
  899. def save_disabled_commands(self):
  900. dataIO.save_json("data/red/disabled_commands.json", self.disabled_commands)
  901. def _import_old_data(data):
  902. """Migration from mod.py"""
  903. try:
  904. data["blacklist"] = dataIO.load_json("data/mod/blacklist.json")
  905. except FileNotFoundError:
  906. pass
  907. try:
  908. data["whitelist"] = dataIO.load_json("data/mod/whitelist.json")
  909. except FileNotFoundError:
  910. pass
  911. return data
  912. def check_files():
  913. if not os.path.isfile("data/red/disabled_commands.json"):
  914. print("Creating empty disabled_commands.json...")
  915. dataIO.save_json("data/red/disabled_commands.json", [])
  916. if not os.path.isfile("data/red/global_ignores.json"):
  917. print("Creating empty global_ignores.json...")
  918. data = {"blacklist": [], "whitelist": []}
  919. try:
  920. data = _import_old_data(data)
  921. except Exception as e:
  922. log.error("Failed to migrate blacklist / whitelist data from "
  923. "mod.py: {}".format(e))
  924. dataIO.save_json("data/red/global_ignores.json", data)
  925. def setup(bot):
  926. check_files()
  927. n = Owner(bot)
  928. bot.add_cog(n)