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 !
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.
This repo is archived. You can view files and clone it, but cannot push or open issues/pull-requests.

1729 lines
66KB

  1. import discord
  2. from discord.ext import commands
  3. from .utils.dataIO import dataIO
  4. from .utils import checks
  5. from __main__ import send_cmd_help, settings
  6. from datetime import datetime
  7. from collections import deque, defaultdict, OrderedDict
  8. from cogs.utils.chat_formatting import escape_mass_mentions, box, pagify
  9. import os
  10. import re
  11. import logging
  12. import asyncio
  13. ACTIONS_REPR = {
  14. "BAN" : ("Ban", "\N{HAMMER}"),
  15. "KICK" : ("Kick", "\N{WOMANS BOOTS}"),
  16. "CMUTE" : ("Channel mute", "\N{SPEAKER WITH CANCELLATION STROKE}"),
  17. "SMUTE" : ("Server mute", "\N{SPEAKER WITH CANCELLATION STROKE}"),
  18. "SOFTBAN" : ("Softban", "\N{DASH SYMBOL} \N{HAMMER}"),
  19. "HACKBAN" : ("Preemptive ban", "\N{BUST IN SILHOUETTE} \N{HAMMER}"),
  20. "UNBAN" : ("Unban", "\N{DOVE OF PEACE}")
  21. }
  22. ACTIONS_CASES = {
  23. "BAN" : True,
  24. "KICK" : True,
  25. "CMUTE" : False,
  26. "SMUTE" : True,
  27. "SOFTBAN" : True,
  28. "HACKBAN" : True,
  29. "UNBAN" : True
  30. }
  31. default_settings = {
  32. "ban_mention_spam" : False,
  33. "delete_repeats" : False,
  34. "mod-log" : None,
  35. "respect_hierarchy" : False
  36. }
  37. for act, enabled in ACTIONS_CASES.items():
  38. act = act.lower() + '_cases'
  39. default_settings[act] = enabled
  40. class ModError(Exception):
  41. pass
  42. class UnauthorizedCaseEdit(ModError):
  43. pass
  44. class CaseMessageNotFound(ModError):
  45. pass
  46. class NoModLogChannel(ModError):
  47. pass
  48. class NoModLogAccess(ModError):
  49. pass
  50. class TempCache:
  51. """
  52. This is how we avoid events such as ban and unban
  53. from triggering twice in the mod-log.
  54. Kinda hacky but functioning
  55. """
  56. def __init__(self, bot):
  57. self.bot = bot
  58. self._cache = []
  59. def add(self, user, server, action, seconds=1):
  60. tmp = (user.id, server.id, action)
  61. self._cache.append(tmp)
  62. async def delete_value():
  63. await asyncio.sleep(seconds)
  64. self._cache.remove(tmp)
  65. self.bot.loop.create_task(delete_value())
  66. def check(self, user, server, action):
  67. return (user.id, server.id, action) in self._cache
  68. class Mod:
  69. """Moderation tools."""
  70. def __init__(self, bot):
  71. self.bot = bot
  72. self.ignore_list = dataIO.load_json("data/mod/ignorelist.json")
  73. self.filter = dataIO.load_json("data/mod/filter.json")
  74. self.past_names = dataIO.load_json("data/mod/past_names.json")
  75. self.past_nicknames = dataIO.load_json("data/mod/past_nicknames.json")
  76. settings = dataIO.load_json("data/mod/settings.json")
  77. self.settings = defaultdict(lambda: default_settings.copy(), settings)
  78. self.cache = OrderedDict()
  79. self.cases = dataIO.load_json("data/mod/modlog.json")
  80. self.last_case = defaultdict(dict)
  81. self.temp_cache = TempCache(bot)
  82. perms_cache = dataIO.load_json("data/mod/perms_cache.json")
  83. self._perms_cache = defaultdict(dict, perms_cache)
  84. @commands.group(pass_context=True, no_pm=True)
  85. @checks.serverowner_or_permissions(administrator=True)
  86. async def modset(self, ctx):
  87. """Manages server administration settings."""
  88. if ctx.invoked_subcommand is None:
  89. server = ctx.message.server
  90. await send_cmd_help(ctx)
  91. roles = settings.get_server(server).copy()
  92. _settings = {**self.settings[server.id], **roles}
  93. if "respect_hierarchy" not in _settings:
  94. _settings["respect_hierarchy"] = default_settings["respect_hierarchy"]
  95. if "delete_delay" not in _settings:
  96. _settings["delete_delay"] = "Disabled"
  97. msg = ("Admin role: {ADMIN_ROLE}\n"
  98. "Mod role: {MOD_ROLE}\n"
  99. "Mod-log: {mod-log}\n"
  100. "Delete repeats: {delete_repeats}\n"
  101. "Ban mention spam: {ban_mention_spam}\n"
  102. "Delete delay: {delete_delay}\n"
  103. "Respects hierarchy: {respect_hierarchy}"
  104. "".format(**_settings))
  105. await self.bot.say(box(msg))
  106. @modset.command(name="adminrole", pass_context=True, no_pm=True, hidden=True)
  107. async def _modset_adminrole(self, ctx):
  108. """Use [p]set adminrole instead"""
  109. await self.bot.say("This command has been renamed "
  110. "`{}set adminrole`".format(ctx.prefix))
  111. @modset.command(name="modrole", pass_context=True, no_pm=True, hidden=True)
  112. async def _modset_modrole(self, ctx):
  113. """Use [p]set modrole instead"""
  114. await self.bot.say("This command has been renamed "
  115. "`{}set modrole`".format(ctx.prefix))
  116. @modset.command(pass_context=True, no_pm=True)
  117. async def modlog(self, ctx, channel : discord.Channel=None):
  118. """Sets a channel as mod log
  119. Leaving the channel parameter empty will deactivate it"""
  120. server = ctx.message.server
  121. if channel:
  122. self.settings[server.id]["mod-log"] = channel.id
  123. await self.bot.say("Mod events will be sent to {}"
  124. "".format(channel.mention))
  125. else:
  126. if self.settings[server.id]["mod-log"] is None:
  127. await send_cmd_help(ctx)
  128. return
  129. self.settings[server.id]["mod-log"] = None
  130. await self.bot.say("Mod log deactivated.")
  131. dataIO.save_json("data/mod/settings.json", self.settings)
  132. @modset.command(pass_context=True, no_pm=True)
  133. async def banmentionspam(self, ctx, max_mentions : int=False):
  134. """Enables auto ban for messages mentioning X different people
  135. Accepted values: 5 or superior"""
  136. server = ctx.message.server
  137. if max_mentions:
  138. if max_mentions < 5:
  139. max_mentions = 5
  140. self.settings[server.id]["ban_mention_spam"] = max_mentions
  141. await self.bot.say("Autoban for mention spam enabled. "
  142. "Anyone mentioning {} or more different people "
  143. "in a single message will be autobanned."
  144. "".format(max_mentions))
  145. else:
  146. if self.settings[server.id]["ban_mention_spam"] is False:
  147. await send_cmd_help(ctx)
  148. return
  149. self.settings[server.id]["ban_mention_spam"] = False
  150. await self.bot.say("Autoban for mention spam disabled.")
  151. dataIO.save_json("data/mod/settings.json", self.settings)
  152. @modset.command(pass_context=True, no_pm=True)
  153. async def deleterepeats(self, ctx):
  154. """Enables auto deletion of repeated messages"""
  155. server = ctx.message.server
  156. if not self.settings[server.id]["delete_repeats"]:
  157. self.settings[server.id]["delete_repeats"] = True
  158. await self.bot.say("Messages repeated up to 3 times will "
  159. "be deleted.")
  160. else:
  161. self.settings[server.id]["delete_repeats"] = False
  162. await self.bot.say("Repeated messages will be ignored.")
  163. dataIO.save_json("data/mod/settings.json", self.settings)
  164. @modset.command(pass_context=True, no_pm=True)
  165. async def resetcases(self, ctx):
  166. """Resets modlog's cases"""
  167. server = ctx.message.server
  168. self.cases[server.id] = {}
  169. dataIO.save_json("data/mod/modlog.json", self.cases)
  170. await self.bot.say("Cases have been reset.")
  171. @modset.command(pass_context=True, no_pm=True)
  172. async def deletedelay(self, ctx, time: int=None):
  173. """Sets the delay until the bot removes the command message.
  174. Must be between -1 and 60.
  175. A delay of -1 means the bot will not remove the message."""
  176. server = ctx.message.server
  177. if time is not None:
  178. time = min(max(time, -1), 60) # Enforces the time limits
  179. self.settings[server.id]["delete_delay"] = time
  180. if time == -1:
  181. await self.bot.say("Command deleting disabled.")
  182. else:
  183. await self.bot.say("Delete delay set to {}"
  184. " seconds.".format(time))
  185. dataIO.save_json("data/mod/settings.json", self.settings)
  186. else:
  187. try:
  188. delay = self.settings[server.id]["delete_delay"]
  189. except KeyError:
  190. await self.bot.say("Delete delay not yet set up on this"
  191. " server.")
  192. else:
  193. if delay != -1:
  194. await self.bot.say("Bot will delete command messages after"
  195. " {} seconds. Set this value to -1 to"
  196. " stop deleting messages".format(delay))
  197. else:
  198. await self.bot.say("I will not delete command messages.")
  199. @modset.command(pass_context=True, no_pm=True, name='cases')
  200. async def set_cases(self, ctx, action: str = None, enabled: bool = None):
  201. """Enables or disables case creation for each type of mod action
  202. Enabled can be 'on' or 'off'"""
  203. server = ctx.message.server
  204. if action == enabled: # No args given
  205. await self.bot.send_cmd_help(ctx)
  206. msg = "Current settings:\n```py\n"
  207. maxlen = max(map(lambda x: len(x[0]), ACTIONS_REPR.values()))
  208. for action, name in ACTIONS_REPR.items():
  209. action = action.lower() + '_cases'
  210. value = self.settings[server.id].get(action,
  211. default_settings[action])
  212. value = 'enabled' if value else 'disabled'
  213. msg += '%s : %s\n' % (name[0].ljust(maxlen), value)
  214. msg += '```'
  215. await self.bot.say(msg)
  216. elif action.upper() not in ACTIONS_CASES:
  217. msg = "That's not a valid action. Valid actions are: \n"
  218. msg += ', '.join(sorted(map(str.lower, ACTIONS_CASES)))
  219. await self.bot.say(msg)
  220. elif enabled == None:
  221. action = action.lower() + '_cases'
  222. value = self.settings[server.id].get(action,
  223. default_settings[action])
  224. await self.bot.say('Case creation for %s is currently %s' %
  225. (action, 'enabled' if value else 'disabled'))
  226. else:
  227. name = ACTIONS_REPR[action.upper()][0]
  228. action = action.lower() + '_cases'
  229. value = self.settings[server.id].get(action,
  230. default_settings[action])
  231. if value != enabled:
  232. self.settings[server.id][action] = enabled
  233. dataIO.save_json("data/mod/settings.json", self.settings)
  234. msg = ('Case creation for %s actions %s %s.' %
  235. (name.lower(),
  236. 'was already' if enabled == value else 'is now',
  237. 'enabled' if enabled else 'disabled')
  238. )
  239. await self.bot.say(msg)
  240. @modset.command(pass_context=True, no_pm=True)
  241. @checks.serverowner_or_permissions()
  242. async def hierarchy(self, ctx):
  243. """Toggles role hierarchy check for mods / admins"""
  244. server = ctx.message.server
  245. toggled = self.settings[server.id].get("respect_hierarchy",
  246. default_settings["respect_hierarchy"])
  247. if not toggled:
  248. self.settings[server.id]["respect_hierarchy"] = True
  249. await self.bot.say("Role hierarchy will be checked when "
  250. "moderation commands are issued.")
  251. else:
  252. self.settings[server.id]["respect_hierarchy"] = False
  253. await self.bot.say("Role hierarchy will be ignored when "
  254. "moderation commands are issued.")
  255. dataIO.save_json("data/mod/settings.json", self.settings)
  256. @commands.command(no_pm=True, pass_context=True)
  257. @checks.admin_or_permissions(kick_members=True)
  258. async def kick(self, ctx, user: discord.Member, *, reason: str = None):
  259. """Kicks user."""
  260. author = ctx.message.author
  261. server = author.server
  262. if author == user:
  263. await self.bot.say("I cannot let you do that. Self-harm is "
  264. "bad \N{PENSIVE FACE}")
  265. return
  266. elif not self.is_allowed_by_hierarchy(server, author, user):
  267. await self.bot.say("I cannot let you do that. You are "
  268. "not higher than the user in the role "
  269. "hierarchy.")
  270. return
  271. try:
  272. await self.bot.kick(user)
  273. logger.info("{}({}) kicked {}({})".format(
  274. author.name, author.id, user.name, user.id))
  275. await self.new_case(server,
  276. action="KICK",
  277. mod=author,
  278. user=user,
  279. reason=reason)
  280. await self.bot.say("Done. That felt good.")
  281. except discord.errors.Forbidden:
  282. await self.bot.say("I'm not allowed to do that.")
  283. except Exception as e:
  284. print(e)
  285. @commands.command(no_pm=True, pass_context=True)
  286. @checks.admin_or_permissions(ban_members=True)
  287. async def ban(self, ctx, user: discord.Member, days: str = None, *, reason: str = None):
  288. """Bans user and deletes last X days worth of messages.
  289. If days is not a number, it's treated as the first word of the reason.
  290. Minimum 0 days, maximum 7. Defaults to 0."""
  291. author = ctx.message.author
  292. server = author.server
  293. if author == user:
  294. await self.bot.say("I cannot let you do that. Self-harm is "
  295. "bad \N{PENSIVE FACE}")
  296. return
  297. elif not self.is_allowed_by_hierarchy(server, author, user):
  298. await self.bot.say("I cannot let you do that. You are "
  299. "not higher than the user in the role "
  300. "hierarchy.")
  301. return
  302. if days:
  303. if days.isdigit():
  304. days = int(days)
  305. else:
  306. if reason:
  307. reason = days + ' ' + reason
  308. else:
  309. reason = days
  310. days = 0
  311. else:
  312. days = 0
  313. if days < 0 or days > 7:
  314. await self.bot.say("Invalid days. Must be between 0 and 7.")
  315. return
  316. try:
  317. self.temp_cache.add(user, server, "BAN")
  318. await self.bot.ban(user, days)
  319. logger.info("{}({}) banned {}({}), deleting {} days worth of messages".format(
  320. author.name, author.id, user.name, user.id, str(days)))
  321. await self.new_case(server,
  322. action="BAN",
  323. mod=author,
  324. user=user,
  325. reason=reason)
  326. await self.bot.say("Done. It was about time.")
  327. except discord.errors.Forbidden:
  328. await self.bot.say("I'm not allowed to do that.")
  329. except Exception as e:
  330. print(e)
  331. @commands.command(no_pm=True, pass_context=True)
  332. @checks.admin_or_permissions(ban_members=True)
  333. async def hackban(self, ctx, user_id: int, *, reason: str = None):
  334. """Preemptively bans user from the server
  335. A user ID needs to be provided
  336. If the user is present in the server a normal ban will be
  337. issued instead"""
  338. user_id = str(user_id)
  339. author = ctx.message.author
  340. server = author.server
  341. ban_list = await self.bot.get_bans(server)
  342. is_banned = discord.utils.get(ban_list, id=user_id)
  343. if is_banned:
  344. await self.bot.say("User is already banned.")
  345. return
  346. user = server.get_member(user_id)
  347. if user is not None:
  348. await ctx.invoke(self.ban, user=user, reason=reason)
  349. return
  350. try:
  351. await self.bot.http.ban(user_id, server.id, 0)
  352. except discord.NotFound:
  353. await self.bot.say("User not found. Have you provided the "
  354. "correct user ID?")
  355. except discord.Forbidden:
  356. await self.bot.say("I lack the permissions to do this.")
  357. else:
  358. logger.info("{}({}) hackbanned {}"
  359. "".format(author.name, author.id, user_id))
  360. user = await self.bot.get_user_info(user_id)
  361. await self.new_case(server,
  362. action="HACKBAN",
  363. mod=author,
  364. user=user,
  365. reason=reason)
  366. await self.bot.say("Done. The user will not be able to join this "
  367. "server.")
  368. @commands.command(no_pm=True, pass_context=True)
  369. @checks.admin_or_permissions(ban_members=True)
  370. async def softban(self, ctx, user: discord.Member, *, reason: str = None):
  371. """Kicks the user, deleting 1 day worth of messages."""
  372. server = ctx.message.server
  373. channel = ctx.message.channel
  374. can_ban = channel.permissions_for(server.me).ban_members
  375. author = ctx.message.author
  376. if author == user:
  377. await self.bot.say("I cannot let you do that. Self-harm is "
  378. "bad \N{PENSIVE FACE}")
  379. return
  380. elif not self.is_allowed_by_hierarchy(server, author, user):
  381. await self.bot.say("I cannot let you do that. You are "
  382. "not higher than the user in the role "
  383. "hierarchy.")
  384. return
  385. try:
  386. invite = await self.bot.create_invite(server, max_age=3600*24)
  387. invite = "\nInvite: " + invite
  388. except:
  389. invite = ""
  390. if can_ban:
  391. try:
  392. try: # We don't want blocked DMs preventing us from banning
  393. msg = await self.bot.send_message(user, "You have been banned and "
  394. "then unbanned as a quick way to delete your messages.\n"
  395. "You can now join the server again.{}".format(invite))
  396. except:
  397. pass
  398. self.temp_cache.add(user, server, "BAN")
  399. await self.bot.ban(user, 1)
  400. logger.info("{}({}) softbanned {}({}), deleting 1 day worth "
  401. "of messages".format(author.name, author.id, user.name,
  402. user.id))
  403. await self.new_case(server,
  404. action="SOFTBAN",
  405. mod=author,
  406. user=user,
  407. reason=reason)
  408. self.temp_cache.add(user, server, "UNBAN")
  409. await self.bot.unban(server, user)
  410. await self.bot.say("Done. Enough chaos.")
  411. except discord.errors.Forbidden:
  412. await self.bot.say("My role is not high enough to softban that user.")
  413. await self.bot.delete_message(msg)
  414. except Exception as e:
  415. print(e)
  416. else:
  417. await self.bot.say("I'm not allowed to do that.")
  418. @commands.command(no_pm=True, pass_context=True)
  419. @checks.admin_or_permissions(manage_nicknames=True)
  420. async def rename(self, ctx, user : discord.Member, *, nickname=""):
  421. """Changes user's nickname
  422. Leaving the nickname empty will remove it."""
  423. nickname = nickname.strip()
  424. if nickname == "":
  425. nickname = None
  426. try:
  427. await self.bot.change_nickname(user, nickname)
  428. await self.bot.say("Done.")
  429. except discord.Forbidden:
  430. await self.bot.say("I cannot do that, I lack the "
  431. "\"Manage Nicknames\" permission.")
  432. @commands.group(pass_context=True, no_pm=True, invoke_without_command=True)
  433. @checks.mod_or_permissions(administrator=True)
  434. async def mute(self, ctx, user : discord.Member, *, reason: str = None):
  435. """Mutes user in the channel/server
  436. Defaults to channel"""
  437. if ctx.invoked_subcommand is None:
  438. await ctx.invoke(self.channel_mute, user=user, reason=reason)
  439. @checks.mod_or_permissions(administrator=True)
  440. @mute.command(name="channel", pass_context=True, no_pm=True)
  441. async def channel_mute(self, ctx, user : discord.Member, *, reason: str = None):
  442. """Mutes user in the current channel"""
  443. author = ctx.message.author
  444. channel = ctx.message.channel
  445. server = ctx.message.server
  446. overwrites = channel.overwrites_for(user)
  447. if overwrites.send_messages is False:
  448. await self.bot.say("That user can't send messages in this "
  449. "channel.")
  450. return
  451. elif not self.is_allowed_by_hierarchy(server, author, user):
  452. await self.bot.say("I cannot let you do that. You are "
  453. "not higher than the user in the role "
  454. "hierarchy.")
  455. return
  456. self._perms_cache[user.id][channel.id] = overwrites.send_messages
  457. overwrites.send_messages = False
  458. try:
  459. await self.bot.edit_channel_permissions(channel, user, overwrites)
  460. except discord.Forbidden:
  461. await self.bot.say("Failed to mute user. I need the manage roles "
  462. "permission and the user I'm muting must be "
  463. "lower than myself in the role hierarchy.")
  464. else:
  465. dataIO.save_json("data/mod/perms_cache.json", self._perms_cache)
  466. await self.new_case(server,
  467. action="CMUTE",
  468. channel=channel,
  469. mod=author,
  470. user=user,
  471. reason=reason)
  472. await self.bot.say("User has been muted in this channel.")
  473. @checks.mod_or_permissions(administrator=True)
  474. @mute.command(name="server", pass_context=True, no_pm=True)
  475. async def server_mute(self, ctx, user : discord.Member, *, reason: str = None):
  476. """Mutes user in the server"""
  477. author = ctx.message.author
  478. server = ctx.message.server
  479. if not self.is_allowed_by_hierarchy(server, author, user):
  480. await self.bot.say("I cannot let you do that. You are "
  481. "not higher than the user in the role "
  482. "hierarchy.")
  483. return
  484. register = {}
  485. for channel in server.channels:
  486. if channel.type != discord.ChannelType.text:
  487. continue
  488. overwrites = channel.overwrites_for(user)
  489. if overwrites.send_messages is False:
  490. continue
  491. register[channel.id] = overwrites.send_messages
  492. overwrites.send_messages = False
  493. try:
  494. await self.bot.edit_channel_permissions(channel, user,
  495. overwrites)
  496. except discord.Forbidden:
  497. await self.bot.say("Failed to mute user. I need the manage roles "
  498. "permission and the user I'm muting must be "
  499. "lower than myself in the role hierarchy.")
  500. return
  501. else:
  502. await asyncio.sleep(0.1)
  503. if not register:
  504. await self.bot.say("That user is already muted in all channels.")
  505. return
  506. self._perms_cache[user.id] = register
  507. dataIO.save_json("data/mod/perms_cache.json", self._perms_cache)
  508. await self.new_case(server,
  509. action="SMUTE",
  510. mod=author,
  511. user=user,
  512. reason=reason)
  513. await self.bot.say("User has been muted in this server.")
  514. @commands.group(pass_context=True, no_pm=True, invoke_without_command=True)
  515. @checks.mod_or_permissions(administrator=True)
  516. async def unmute(self, ctx, user : discord.Member):
  517. """Unmutes user in the channel/server
  518. Defaults to channel"""
  519. if ctx.invoked_subcommand is None:
  520. await ctx.invoke(self.channel_unmute, user=user)
  521. @checks.mod_or_permissions(administrator=True)
  522. @unmute.command(name="channel", pass_context=True, no_pm=True)
  523. async def channel_unmute(self, ctx, user : discord.Member):
  524. """Unmutes user in the current channel"""
  525. channel = ctx.message.channel
  526. author = ctx.message.author
  527. server = ctx.message.server
  528. overwrites = channel.overwrites_for(user)
  529. if overwrites.send_messages:
  530. await self.bot.say("That user doesn't seem to be muted "
  531. "in this channel.")
  532. return
  533. elif not self.is_allowed_by_hierarchy(server, author, user):
  534. await self.bot.say("I cannot let you do that. You are "
  535. "not higher than the user in the role "
  536. "hierarchy.")
  537. return
  538. if user.id in self._perms_cache:
  539. old_value = self._perms_cache[user.id].get(channel.id)
  540. else:
  541. old_value = None
  542. overwrites.send_messages = old_value
  543. is_empty = self.are_overwrites_empty(overwrites)
  544. try:
  545. if not is_empty:
  546. await self.bot.edit_channel_permissions(channel, user,
  547. overwrites)
  548. else:
  549. await self.bot.delete_channel_permissions(channel, user)
  550. except discord.Forbidden:
  551. await self.bot.say("Failed to unmute user. I need the manage roles"
  552. " permission and the user I'm unmuting must be "
  553. "lower than myself in the role hierarchy.")
  554. else:
  555. try:
  556. del self._perms_cache[user.id][channel.id]
  557. except KeyError:
  558. pass
  559. if user.id in self._perms_cache and not self._perms_cache[user.id]:
  560. del self._perms_cache[user.id] # clear
  561. dataIO.save_json("data/mod/perms_cache.json", self._perms_cache)
  562. await self.bot.say("User has been unmuted in this channel.")
  563. @checks.mod_or_permissions(administrator=True)
  564. @unmute.command(name="server", pass_context=True, no_pm=True)
  565. async def server_unmute(self, ctx, user : discord.Member):
  566. """Unmutes user in the server"""
  567. server = ctx.message.server
  568. author = ctx.message.author
  569. if user.id not in self._perms_cache:
  570. await self.bot.say("That user doesn't seem to have been muted with {0}mute commands. "
  571. "Unmute them in the channels you want with `{0}unmute <user>`"
  572. "".format(ctx.prefix))
  573. return
  574. elif not self.is_allowed_by_hierarchy(server, author, user):
  575. await self.bot.say("I cannot let you do that. You are "
  576. "not higher than the user in the role "
  577. "hierarchy.")
  578. return
  579. for channel in server.channels:
  580. if channel.type != discord.ChannelType.text:
  581. continue
  582. if channel.id not in self._perms_cache[user.id]:
  583. continue
  584. value = self._perms_cache[user.id].get(channel.id)
  585. overwrites = channel.overwrites_for(user)
  586. if overwrites.send_messages is False:
  587. overwrites.send_messages = value
  588. is_empty = self.are_overwrites_empty(overwrites)
  589. try:
  590. if not is_empty:
  591. await self.bot.edit_channel_permissions(channel, user,
  592. overwrites)
  593. else:
  594. await self.bot.delete_channel_permissions(channel, user)
  595. except discord.Forbidden:
  596. await self.bot.say("Failed to unmute user. I need the manage roles"
  597. " permission and the user I'm unmuting must be "
  598. "lower than myself in the role hierarchy.")
  599. return
  600. else:
  601. del self._perms_cache[user.id][channel.id]
  602. await asyncio.sleep(0.1)
  603. if user.id in self._perms_cache and not self._perms_cache[user.id]:
  604. del self._perms_cache[user.id] # clear
  605. dataIO.save_json("data/mod/perms_cache.json", self._perms_cache)
  606. await self.bot.say("User has been unmuted in this server.")
  607. @commands.group(pass_context=True)
  608. @checks.mod_or_permissions(manage_messages=True)
  609. async def cleanup(self, ctx):
  610. """Deletes messages."""
  611. if ctx.invoked_subcommand is None:
  612. await send_cmd_help(ctx)
  613. @cleanup.command(pass_context=True, no_pm=True)
  614. async def text(self, ctx, text: str, number: int):
  615. """Deletes last X messages matching the specified text.
  616. Example:
  617. clear text \"test\" 5
  618. Remember to use double quotes."""
  619. channel = ctx.message.channel
  620. author = ctx.message.author
  621. server = author.server
  622. is_bot = self.bot.user.bot
  623. has_permissions = channel.permissions_for(server.me).manage_messages
  624. def check(m):
  625. if text in m.content:
  626. return True
  627. elif m == ctx.message:
  628. return True
  629. else:
  630. return False
  631. to_delete = [ctx.message]
  632. if not has_permissions:
  633. await self.bot.say("I'm not allowed to delete messages.")
  634. return
  635. tries_left = 5
  636. tmp = ctx.message
  637. while tries_left and len(to_delete) - 1 < number:
  638. async for message in self.bot.logs_from(channel, limit=100,
  639. before=tmp):
  640. if len(to_delete) - 1 < number and check(message):
  641. to_delete.append(message)
  642. tmp = message
  643. tries_left -= 1
  644. logger.info("{}({}) deleted {} messages "
  645. " containing '{}' in channel {}".format(author.name,
  646. author.id, len(to_delete), text, channel.id))
  647. if is_bot:
  648. await self.mass_purge(to_delete)
  649. else:
  650. await self.slow_deletion(to_delete)
  651. @cleanup.command(pass_context=True, no_pm=True)
  652. async def user(self, ctx, user: discord.Member, number: int):
  653. """Deletes last X messages from specified user.
  654. Examples:
  655. clear user @\u200bTwentysix 2
  656. clear user Red 6"""
  657. channel = ctx.message.channel
  658. author = ctx.message.author
  659. server = author.server
  660. is_bot = self.bot.user.bot
  661. has_permissions = channel.permissions_for(server.me).manage_messages
  662. self_delete = user == self.bot.user
  663. def check(m):
  664. if m.author == user:
  665. return True
  666. elif m == ctx.message:
  667. return True
  668. else:
  669. return False
  670. to_delete = [ctx.message]
  671. if not has_permissions and not self_delete:
  672. await self.bot.say("I'm not allowed to delete messages.")
  673. return
  674. tries_left = 5
  675. tmp = ctx.message
  676. while tries_left and len(to_delete) - 1 < number:
  677. async for message in self.bot.logs_from(channel, limit=100,
  678. before=tmp):
  679. if len(to_delete) - 1 < number and check(message):
  680. to_delete.append(message)
  681. tmp = message
  682. tries_left -= 1
  683. logger.info("{}({}) deleted {} messages "
  684. " made by {}({}) in channel {}"
  685. "".format(author.name, author.id, len(to_delete),
  686. user.name, user.id, channel.name))
  687. if is_bot and not self_delete:
  688. # For whatever reason the purge endpoint requires manage_messages
  689. await self.mass_purge(to_delete)
  690. else:
  691. await self.slow_deletion(to_delete)
  692. @cleanup.command(pass_context=True, no_pm=True)
  693. async def after(self, ctx, message_id : int):
  694. """Deletes all messages after specified message
  695. To get a message id, enable developer mode in Discord's
  696. settings, 'appearance' tab. Then right click a message
  697. and copy its id.
  698. This command only works on bots running as bot accounts.
  699. """
  700. channel = ctx.message.channel
  701. author = ctx.message.author
  702. server = channel.server
  703. is_bot = self.bot.user.bot
  704. has_permissions = channel.permissions_for(server.me).manage_messages
  705. if not is_bot:
  706. await self.bot.say("This command can only be used on bots with "
  707. "bot accounts.")
  708. return
  709. to_delete = []
  710. after = await self.bot.get_message(channel, message_id)
  711. if not has_permissions:
  712. await self.bot.say("I'm not allowed to delete messages.")
  713. return
  714. elif not after:
  715. await self.bot.say("Message not found.")
  716. return
  717. async for message in self.bot.logs_from(channel, limit=2000,
  718. after=after):
  719. to_delete.append(message)
  720. logger.info("{}({}) deleted {} messages in channel {}"
  721. "".format(author.name, author.id,
  722. len(to_delete), channel.name))
  723. await self.mass_purge(to_delete)
  724. @cleanup.command(pass_context=True, no_pm=True)
  725. async def messages(self, ctx, number: int):
  726. """Deletes last X messages.
  727. Example:
  728. clear messages 26"""
  729. channel = ctx.message.channel
  730. author = ctx.message.author
  731. server = author.server
  732. is_bot = self.bot.user.bot
  733. has_permissions = channel.permissions_for(server.me).manage_messages
  734. to_delete = []
  735. if not has_permissions:
  736. await self.bot.say("I'm not allowed to delete messages.")
  737. return
  738. async for message in self.bot.logs_from(channel, limit=number+1):
  739. to_delete.append(message)
  740. logger.info("{}({}) deleted {} messages in channel {}"
  741. "".format(author.name, author.id,
  742. number, channel.name))
  743. if is_bot:
  744. await self.mass_purge(to_delete)
  745. else:
  746. await self.slow_deletion(to_delete)
  747. @cleanup.command(pass_context=True, no_pm=True, name='bot')
  748. async def clear_bot(self, ctx, number: int):
  749. """Cleans up command messages and messages from the bot"""
  750. channel = ctx.message.channel
  751. author = ctx.message.author
  752. server = channel.server
  753. is_bot = self.bot.user.bot
  754. has_permissions = channel.permissions_for(server.me).manage_messages
  755. prefixes = self.bot.command_prefix
  756. if isinstance(prefixes, str):
  757. prefixes = [prefixes]
  758. elif callable(prefixes):
  759. if asyncio.iscoroutine(prefixes):
  760. await self.bot.say('Coroutine prefixes not yet implemented.')
  761. return
  762. prefixes = prefixes(self.bot, ctx.message)
  763. # In case some idiot sets a null prefix
  764. if '' in prefixes:
  765. prefixes.pop('')
  766. def check(m):
  767. if m.author.id == self.bot.user.id:
  768. return True
  769. elif m == ctx.message:
  770. return True
  771. p = discord.utils.find(m.content.startswith, prefixes)
  772. if p and len(p) > 0:
  773. return m.content[len(p):].startswith(tuple(self.bot.commands))
  774. return False
  775. to_delete = [ctx.message]
  776. if not has_permissions:
  777. await self.bot.say("I'm not allowed to delete messages.")
  778. return
  779. tries_left = 5
  780. tmp = ctx.message
  781. while tries_left and len(to_delete) - 1 < number:
  782. async for message in self.bot.logs_from(channel, limit=100,
  783. before=tmp):
  784. if len(to_delete) - 1 < number and check(message):
  785. to_delete.append(message)
  786. tmp = message
  787. tries_left -= 1
  788. logger.info("{}({}) deleted {} "
  789. " command messages in channel {}"
  790. "".format(author.name, author.id, len(to_delete),
  791. channel.name))
  792. if is_bot:
  793. await self.mass_purge(to_delete)
  794. else:
  795. await self.slow_deletion(to_delete)
  796. @cleanup.command(pass_context=True, name='self')
  797. async def clear_self(self, ctx, number: int, match_pattern: str = None):
  798. """Cleans up messages owned by the bot.
  799. By default, all messages are cleaned. If a third argument is specified,
  800. it is used for pattern matching: If it begins with r( and ends with ),
  801. then it is interpreted as a regex, and messages that match it are
  802. deleted. Otherwise, it is used in a simple substring test.
  803. Some helpful regex flags to include in your pattern:
  804. Dots match newlines: (?s); Ignore case: (?i); Both: (?si)
  805. """
  806. channel = ctx.message.channel
  807. author = ctx.message.author
  808. is_bot = self.bot.user.bot
  809. # You can always delete your own messages, this is needed to purge
  810. can_mass_purge = False
  811. if type(author) is discord.Member:
  812. me = channel.server.me
  813. can_mass_purge = channel.permissions_for(me).manage_messages
  814. use_re = (match_pattern and match_pattern.startswith('r(') and
  815. match_pattern.endswith(')'))
  816. if use_re:
  817. match_pattern = match_pattern[1:] # strip 'r'
  818. match_re = re.compile(match_pattern)
  819. def content_match(c):
  820. return bool(match_re.match(c))
  821. elif match_pattern:
  822. def content_match(c):
  823. return match_pattern in c
  824. else:
  825. def content_match(_):
  826. return True
  827. def check(m):
  828. if m.author.id != self.bot.user.id:
  829. return False
  830. elif content_match(m.content):
  831. return True
  832. return False
  833. to_delete = []
  834. # Selfbot convenience, delete trigger message
  835. if author == self.bot.user:
  836. to_delete.append(ctx.message)
  837. number += 1
  838. tries_left = 5
  839. tmp = ctx.message
  840. while tries_left and len(to_delete) < number:
  841. async for message in self.bot.logs_from(channel, limit=100,
  842. before=tmp):
  843. if len(to_delete) < number and check(message):
  844. to_delete.append(message)
  845. tmp = message
  846. tries_left -= 1
  847. if channel.name:
  848. channel_name = 'channel ' + channel.name
  849. else:
  850. channel_name = str(channel)
  851. logger.info("{}({}) deleted {} messages "
  852. "sent by the bot in {}"
  853. "".format(author.name, author.id, len(to_delete),
  854. channel_name))
  855. if is_bot and can_mass_purge:
  856. await self.mass_purge(to_delete)
  857. else:
  858. await self.slow_deletion(to_delete)
  859. @commands.command(pass_context=True)
  860. @checks.mod_or_permissions(manage_messages=True)
  861. async def reason(self, ctx, case, *, reason : str=""):
  862. """Lets you specify a reason for mod-log's cases
  863. Defaults to last case assigned to yourself, if available."""
  864. author = ctx.message.author
  865. server = author.server
  866. try:
  867. case = int(case)
  868. if not reason:
  869. await send_cmd_help(ctx)
  870. return
  871. except:
  872. if reason:
  873. reason = "{} {}".format(case, reason)
  874. else:
  875. reason = case
  876. case = self.last_case[server.id].get(author.id)
  877. if case is None:
  878. await send_cmd_help(ctx)
  879. return
  880. try:
  881. await self.update_case(server, case=case, mod=author,
  882. reason=reason)
  883. except UnauthorizedCaseEdit:
  884. await self.bot.say("That case is not yours.")
  885. except KeyError:
  886. await self.bot.say("That case doesn't exist.")
  887. except NoModLogChannel:
  888. await self.bot.say("There's no mod-log channel set.")
  889. except CaseMessageNotFound:
  890. await self.bot.say("I couldn't find the case's message.")
  891. except NoModLogAccess:
  892. await self.bot.say("I'm not allowed to access the mod-log "
  893. "channel (or its message history)")
  894. else:
  895. await self.bot.say("Case #{} updated.".format(case))
  896. @commands.group(pass_context=True, no_pm=True)
  897. @checks.admin_or_permissions(manage_channels=True)
  898. async def ignore(self, ctx):
  899. """Adds servers/channels to ignorelist"""
  900. if ctx.invoked_subcommand is None:
  901. await send_cmd_help(ctx)
  902. await self.bot.say(self.count_ignored())
  903. @ignore.command(name="channel", pass_context=True)
  904. async def ignore_channel(self, ctx, channel: discord.Channel=None):
  905. """Ignores channel
  906. Defaults to current one"""
  907. current_ch = ctx.message.channel
  908. if not channel:
  909. if current_ch.id not in self.ignore_list["CHANNELS"]:
  910. self.ignore_list["CHANNELS"].append(current_ch.id)
  911. dataIO.save_json("data/mod/ignorelist.json", self.ignore_list)
  912. await self.bot.say("Channel added to ignore list.")
  913. else:
  914. await self.bot.say("Channel already in ignore list.")
  915. else:
  916. if channel.id not in self.ignore_list["CHANNELS"]:
  917. self.ignore_list["CHANNELS"].append(channel.id)
  918. dataIO.save_json("data/mod/ignorelist.json", self.ignore_list)
  919. await self.bot.say("Channel added to ignore list.")
  920. else:
  921. await self.bot.say("Channel already in ignore list.")
  922. @ignore.command(name="server", pass_context=True)
  923. async def ignore_server(self, ctx):
  924. """Ignores current server"""
  925. server = ctx.message.server
  926. if server.id not in self.ignore_list["SERVERS"]:
  927. self.ignore_list["SERVERS"].append(server.id)
  928. dataIO.save_json("data/mod/ignorelist.json", self.ignore_list)
  929. await self.bot.say("This server has been added to the ignore list.")
  930. else:
  931. await self.bot.say("This server is already being ignored.")
  932. @commands.group(pass_context=True, no_pm=True)
  933. @checks.admin_or_permissions(manage_channels=True)
  934. async def unignore(self, ctx):
  935. """Removes servers/channels from ignorelist"""
  936. if ctx.invoked_subcommand is None:
  937. await send_cmd_help(ctx)
  938. await self.bot.say(self.count_ignored())
  939. @unignore.command(name="channel", pass_context=True)
  940. async def unignore_channel(self, ctx, channel: discord.Channel=None):
  941. """Removes channel from ignore list
  942. Defaults to current one"""
  943. current_ch = ctx.message.channel
  944. if not channel:
  945. if current_ch.id in self.ignore_list["CHANNELS"]:
  946. self.ignore_list["CHANNELS"].remove(current_ch.id)
  947. dataIO.save_json("data/mod/ignorelist.json", self.ignore_list)
  948. await self.bot.say("This channel has been removed from the ignore list.")
  949. else:
  950. await self.bot.say("This channel is not in the ignore list.")
  951. else:
  952. if channel.id in self.ignore_list["CHANNELS"]:
  953. self.ignore_list["CHANNELS"].remove(channel.id)
  954. dataIO.save_json("data/mod/ignorelist.json", self.ignore_list)
  955. await self.bot.say("Channel removed from ignore list.")
  956. else:
  957. await self.bot.say("That channel is not in the ignore list.")
  958. @unignore.command(name="server", pass_context=True)
  959. async def unignore_server(self, ctx):
  960. """Removes current server from ignore list"""
  961. server = ctx.message.server
  962. if server.id in self.ignore_list["SERVERS"]:
  963. self.ignore_list["SERVERS"].remove(server.id)
  964. dataIO.save_json("data/mod/ignorelist.json", self.ignore_list)
  965. await self.bot.say("This server has been removed from the ignore list.")
  966. else:
  967. await self.bot.say("This server is not in the ignore list.")
  968. def count_ignored(self):
  969. msg = "```Currently ignoring:\n"
  970. msg += str(len(self.ignore_list["CHANNELS"])) + " channels\n"
  971. msg += str(len(self.ignore_list["SERVERS"])) + " servers\n```\n"
  972. return msg
  973. @commands.group(name="filter", pass_context=True, no_pm=True)
  974. @checks.mod_or_permissions(manage_messages=True)
  975. async def _filter(self, ctx):
  976. """Adds/removes words from filter
  977. Use double quotes to add/remove sentences
  978. Using this command with no subcommands will send
  979. the list of the server's filtered words."""
  980. if ctx.invoked_subcommand is None:
  981. await send_cmd_help(ctx)
  982. server = ctx.message.server
  983. author = ctx.message.author
  984. if server.id in self.filter:
  985. if self.filter[server.id]:
  986. words = ", ".join(self.filter[server.id])
  987. words = "Filtered in this server:\n\n" + words
  988. try:
  989. for page in pagify(words, delims=[" ", "\n"], shorten_by=8):
  990. await self.bot.send_message(author, page)
  991. except discord.Forbidden:
  992. await self.bot.say("I can't send direct messages to you.")
  993. @_filter.command(name="add", pass_context=True)
  994. async def filter_add(self, ctx, *words: str):
  995. """Adds words to the filter
  996. Use double quotes to add sentences
  997. Examples:
  998. filter add word1 word2 word3
  999. filter add \"This is a sentence\""""
  1000. if words == ():
  1001. await send_cmd_help(ctx)
  1002. return
  1003. server = ctx.message.server
  1004. added = 0
  1005. if server.id not in self.filter.keys():
  1006. self.filter[server.id] = []
  1007. for w in words:
  1008. if w.lower() not in self.filter[server.id] and w != "":
  1009. self.filter[server.id].append(w.lower())
  1010. added += 1
  1011. if added:
  1012. dataIO.save_json("data/mod/filter.json", self.filter)
  1013. await self.bot.say("Words added to filter.")
  1014. else:
  1015. await self.bot.say("Words already in the filter.")
  1016. @_filter.command(name="remove", pass_context=True)
  1017. async def filter_remove(self, ctx, *words: str):
  1018. """Remove words from the filter
  1019. Use double quotes to remove sentences
  1020. Examples:
  1021. filter remove word1 word2 word3
  1022. filter remove \"This is a sentence\""""
  1023. if words == ():
  1024. await send_cmd_help(ctx)
  1025. return
  1026. server = ctx.message.server
  1027. removed = 0
  1028. if server.id not in self.filter.keys():
  1029. await self.bot.say("There are no filtered words in this server.")
  1030. return
  1031. for w in words:
  1032. if w.lower() in self.filter[server.id]:
  1033. self.filter[server.id].remove(w.lower())
  1034. removed += 1
  1035. if removed:
  1036. dataIO.save_json("data/mod/filter.json", self.filter)
  1037. await self.bot.say("Words removed from filter.")
  1038. else:
  1039. await self.bot.say("Those words weren't in the filter.")
  1040. @commands.group(no_pm=True, pass_context=True)
  1041. @checks.admin_or_permissions(manage_roles=True)
  1042. async def editrole(self, ctx):
  1043. """Edits roles settings"""
  1044. if ctx.invoked_subcommand is None:
  1045. await send_cmd_help(ctx)
  1046. @editrole.command(aliases=["color"], pass_context=True)
  1047. async def colour(self, ctx, role: discord.Role, value: discord.Colour):
  1048. """Edits a role's colour
  1049. Use double quotes if the role contains spaces.
  1050. Colour must be in hexadecimal format.
  1051. \"http://www.w3schools.com/colors/colors_picker.asp\"
  1052. Examples:
  1053. !editrole colour \"The Transistor\" #ff0000
  1054. !editrole colour Test #ff9900"""
  1055. author = ctx.message.author
  1056. try:
  1057. await self.bot.edit_role(ctx.message.server, role, color=value)
  1058. logger.info("{}({}) changed the colour of role '{}'".format(
  1059. author.name, author.id, role.name))
  1060. await self.bot.say("Done.")
  1061. except discord.Forbidden:
  1062. await self.bot.say("I need permissions to manage roles first.")
  1063. except Exception as e:
  1064. print(e)
  1065. await self.bot.say("Something went wrong.")
  1066. @editrole.command(name="name", pass_context=True)
  1067. @checks.admin_or_permissions(administrator=True)
  1068. async def edit_role_name(self, ctx, role: discord.Role, name: str):
  1069. """Edits a role's name
  1070. Use double quotes if the role or the name contain spaces.
  1071. Examples:
  1072. !editrole name \"The Transistor\" Test"""
  1073. if name == "":
  1074. await self.bot.say("Name cannot be empty.")
  1075. return
  1076. try:
  1077. author = ctx.message.author
  1078. old_name = role.name # probably not necessary?
  1079. await self.bot.edit_role(ctx.message.server, role, name=name)
  1080. logger.info("{}({}) changed the name of role '{}' to '{}'".format(
  1081. author.name, author.id, old_name, name))
  1082. await self.bot.say("Done.")
  1083. except discord.Forbidden:
  1084. await self.bot.say("I need permissions to manage roles first.")
  1085. except Exception as e:
  1086. print(e)
  1087. await self.bot.say("Something went wrong.")
  1088. @commands.command()
  1089. async def names(self, user : discord.Member):
  1090. """Show previous names/nicknames of a user"""
  1091. server = user.server
  1092. names = self.past_names[user.id] if user.id in self.past_names else None
  1093. try:
  1094. nicks = self.past_nicknames[server.id][user.id]
  1095. nicks = [escape_mass_mentions(nick) for nick in nicks]
  1096. except:
  1097. nicks = None
  1098. msg = ""
  1099. if names:
  1100. names = [escape_mass_mentions(name) for name in names]
  1101. msg += "**Past 20 names**:\n"
  1102. msg += ", ".join(names)
  1103. if nicks:
  1104. if msg:
  1105. msg += "\n\n"
  1106. msg += "**Past 20 nicknames**:\n"
  1107. msg += ", ".join(nicks)
  1108. if msg:
  1109. await self.bot.say(msg)
  1110. else:
  1111. await self.bot.say("That user doesn't have any recorded name or "
  1112. "nickname change.")
  1113. async def mass_purge(self, messages):
  1114. while messages:
  1115. if len(messages) > 1:
  1116. await self.bot.delete_messages(messages[:100])
  1117. messages = messages[100:]
  1118. else:
  1119. await self.bot.delete_message(messages[0])
  1120. messages = []
  1121. await asyncio.sleep(1.5)
  1122. async def slow_deletion(self, messages):
  1123. for message in messages:
  1124. try:
  1125. await self.bot.delete_message(message)
  1126. except:
  1127. pass
  1128. def is_admin_or_superior(self, obj):
  1129. if isinstance(obj, discord.Message):
  1130. user = obj.author
  1131. elif isinstance(obj, discord.Member):
  1132. user = obj
  1133. elif isinstance(obj, discord.Role):
  1134. pass
  1135. else:
  1136. raise TypeError('Only messages, members or roles may be passed')
  1137. server = obj.server
  1138. admin_role = settings.get_server_admin(server)
  1139. if isinstance(obj, discord.Role):
  1140. return obj.name == admin_role
  1141. if user.id == settings.owner:
  1142. return True
  1143. elif discord.utils.get(user.roles, name=admin_role):
  1144. return True
  1145. else:
  1146. return False
  1147. def is_mod_or_superior(self, obj):
  1148. if isinstance(obj, discord.Message):
  1149. user = obj.author
  1150. elif isinstance(obj, discord.Member):
  1151. user = obj
  1152. elif isinstance(obj, discord.Role):
  1153. pass
  1154. else:
  1155. raise TypeError('Only messages, members or roles may be passed')
  1156. server = obj.server
  1157. admin_role = settings.get_server_admin(server)
  1158. mod_role = settings.get_server_mod(server)
  1159. if isinstance(obj, discord.Role):
  1160. return obj.name in [admin_role, mod_role]
  1161. if user.id == settings.owner:
  1162. return True
  1163. elif discord.utils.get(user.roles, name=admin_role):
  1164. return True
  1165. elif discord.utils.get(user.roles, name=mod_role):
  1166. return True
  1167. else:
  1168. return False
  1169. def is_allowed_by_hierarchy(self, server, mod, user):
  1170. toggled = self.settings[server.id].get("respect_hierarchy",
  1171. default_settings["respect_hierarchy"])
  1172. is_special = mod == server.owner or mod.id == self.bot.settings.owner
  1173. if not toggled:
  1174. return True
  1175. else:
  1176. return mod.top_role.position > user.top_role.position or is_special
  1177. async def new_case(self, server, *, action, mod=None, user, reason=None, until=None, channel=None, force_create=False):
  1178. action_type = action.lower() + "_cases"
  1179. enabled_case = self.settings.get(server.id, {}).get(action_type, default_settings.get(action_type))
  1180. if not force_create and not enabled_case:
  1181. return False
  1182. mod_channel = server.get_channel(self.settings[server.id]["mod-log"])
  1183. if mod_channel is None:
  1184. return None
  1185. if server.id not in self.cases:
  1186. self.cases[server.id] = {}
  1187. case_n = len(self.cases[server.id]) + 1
  1188. case = {
  1189. "case" : case_n,
  1190. "created" : datetime.utcnow().timestamp(),
  1191. "modified" : None,
  1192. "action" : action,
  1193. "channel" : channel.id if channel else None,
  1194. "user" : str(user),
  1195. "user_id" : user.id,
  1196. "reason" : reason,
  1197. "moderator" : str(mod) if mod is not None else None,
  1198. "moderator_id" : mod.id if mod is not None else None,
  1199. "amended_by" : None,
  1200. "amended_id" : None,
  1201. "message" : None,
  1202. "until" : until.timestamp() if until else None,
  1203. }
  1204. case_msg = self.format_case_msg(case)
  1205. try:
  1206. msg = await self.bot.send_message(mod_channel, case_msg)
  1207. case["message"] = msg.id
  1208. except:
  1209. pass
  1210. self.cases[server.id][str(case_n)] = case
  1211. if mod:
  1212. self.last_case[server.id][mod.id] = case_n
  1213. dataIO.save_json("data/mod/modlog.json", self.cases)
  1214. return case_n
  1215. async def update_case(self, server, *, case, mod=None, reason=None,
  1216. until=False):
  1217. channel = server.get_channel(self.settings[server.id]["mod-log"])
  1218. if channel is None:
  1219. raise NoModLogChannel()
  1220. case = str(case)
  1221. case = self.cases[server.id][case]
  1222. if case["moderator_id"] is not None:
  1223. if case["moderator_id"] != mod.id:
  1224. if self.is_admin_or_superior(mod):
  1225. case["amended_by"] = str(mod)
  1226. case["amended_id"] = mod.id
  1227. else:
  1228. raise UnauthorizedCaseEdit()
  1229. else:
  1230. case["moderator"] = str(mod)
  1231. case["moderator_id"] = mod.id
  1232. if case["reason"]: # Existing reason
  1233. case["modified"] = datetime.utcnow().timestamp()
  1234. case["reason"] = reason
  1235. if until is not False:
  1236. case["until"] = until
  1237. case_msg = self.format_case_msg(case)
  1238. dataIO.save_json("data/mod/modlog.json", self.cases)
  1239. if case["message"] is None: # The case's message was never sent
  1240. raise CaseMessageNotFound()
  1241. try:
  1242. msg = await self.bot.get_message(channel, case["message"])
  1243. except discord.NotFound:
  1244. raise CaseMessageNotFound()
  1245. except discord.Forbidden:
  1246. raise NoModLogAccess()
  1247. else:
  1248. await self.bot.edit_message(msg, case_msg)
  1249. def format_case_msg(self, case):
  1250. tmp = case.copy()
  1251. if case["reason"] is None:
  1252. tmp["reason"] = "Type [p]reason %i <reason> to add it" % tmp["case"]
  1253. if case["moderator"] is None:
  1254. tmp["moderator"] = "Unknown"
  1255. tmp["moderator_id"] = "Nobody has claimed responsibility yet"
  1256. if case["action"] in ACTIONS_REPR:
  1257. tmp["action"] = ' '.join(ACTIONS_REPR[tmp["action"]])
  1258. channel = case.get("channel")
  1259. if channel:
  1260. channel = self.bot.get_channel(channel)
  1261. tmp["action"] += ' in ' + channel.mention
  1262. case_msg = (
  1263. "**Case #{case}** | {action}\n"
  1264. "**User:** {user} ({user_id})\n"
  1265. "**Moderator:** {moderator} ({moderator_id})\n"
  1266. ).format(**tmp)
  1267. created = case.get('created')
  1268. until = case.get('until')
  1269. if created and until:
  1270. start = datetime.fromtimestamp(created)
  1271. end = datetime.fromtimestamp(until)
  1272. end_fmt = end.strftime('%Y-%m-%d %H:%M:%S UTC')
  1273. duration = end - start
  1274. dur_fmt = strfdelta(duration)
  1275. case_msg += ("**Until:** {}\n"
  1276. "**Duration:** {}\n").format(end_fmt, dur_fmt)
  1277. amended = case.get('amended_by')
  1278. if amended:
  1279. amended_id = case.get('amended_id')
  1280. case_msg += "**Amended by:** %s (%s)\n" % (amended, amended_id)
  1281. modified = case.get('modified')
  1282. if modified:
  1283. modified = datetime.fromtimestamp(modified)
  1284. modified_fmt = modified.strftime('%Y-%m-%d %H:%M:%S UTC')
  1285. case_msg += "**Last modified:** %s\n" % modified_fmt
  1286. case_msg += "**Reason:** %s\n" % tmp["reason"]
  1287. return case_msg
  1288. async def check_filter(self, message):
  1289. server = message.server
  1290. if server.id in self.filter.keys():
  1291. for w in self.filter[server.id]:
  1292. if w in message.content.lower():
  1293. try:
  1294. await self.bot.delete_message(message)
  1295. logger.info("Message deleted in server {}."
  1296. "Filtered: {}"
  1297. "".format(server.id, w))
  1298. return True
  1299. except:
  1300. pass
  1301. return False
  1302. async def check_duplicates(self, message):
  1303. server = message.server
  1304. author = message.author
  1305. if server.id not in self.settings:
  1306. return False
  1307. if self.settings[server.id]["delete_repeats"]:
  1308. if not message.content:
  1309. return False
  1310. if author.id not in self.cache:
  1311. self.cache[author.id] = deque(maxlen=3)
  1312. self.cache.move_to_end(author.id)
  1313. while len(self.cache) > 100000:
  1314. self.cache.popitem(last=False) # the oldest gets discarded
  1315. self.cache[author.id].append(message.content)
  1316. msgs = self.cache[author.id]
  1317. if len(msgs) == 3 and msgs[0] == msgs[1] == msgs[2]:
  1318. try:
  1319. await self.bot.delete_message(message)
  1320. return True
  1321. except:
  1322. pass
  1323. return False
  1324. async def check_mention_spam(self, message):
  1325. server = message.server
  1326. author = message.author
  1327. if server.id not in self.settings:
  1328. return False
  1329. if self.settings[server.id]["ban_mention_spam"]:
  1330. max_mentions = self.settings[server.id]["ban_mention_spam"]
  1331. mentions = set(message.mentions)
  1332. if len(mentions) >= max_mentions:
  1333. try:
  1334. self.temp_cache.add(author, server, "BAN")
  1335. await self.bot.ban(author, 1)
  1336. except:
  1337. logger.info("Failed to ban member for mention spam in "
  1338. "server {}".format(server.id))
  1339. else:
  1340. await self.new_case(server,
  1341. action="BAN",
  1342. mod=server.me,
  1343. user=author,
  1344. reason="Mention spam (Autoban)")
  1345. return True
  1346. return False
  1347. async def on_command(self, command, ctx):
  1348. """Currently used for:
  1349. * delete delay"""
  1350. server = ctx.message.server
  1351. message = ctx.message
  1352. try:
  1353. delay = self.settings[server.id]["delete_delay"]
  1354. except KeyError:
  1355. # We have no delay set
  1356. return
  1357. except AttributeError:
  1358. # DM
  1359. return
  1360. if delay == -1:
  1361. return
  1362. async def _delete_helper(bot, message):
  1363. try:
  1364. await bot.delete_message(message)
  1365. logger.debug("Deleted command msg {}".format(message.id))
  1366. except:
  1367. pass # We don't really care if it fails or not
  1368. await asyncio.sleep(delay)
  1369. await _delete_helper(self.bot, message)
  1370. async def on_message(self, message):
  1371. author = message.author
  1372. if message.server is None or self.bot.user == author:
  1373. return
  1374. valid_user = isinstance(author, discord.Member) and not author.bot
  1375. # Bots and mods or superior are ignored from the filter
  1376. if not valid_user or self.is_mod_or_superior(message):
  1377. return
  1378. deleted = await self.check_filter(message)
  1379. if not deleted:
  1380. deleted = await self.check_duplicates(message)
  1381. if not deleted:
  1382. deleted = await self.check_mention_spam(message)
  1383. async def on_message_edit(self, _, message):
  1384. author = message.author
  1385. if message.server is None or self.bot.user == author:
  1386. return
  1387. valid_user = isinstance(author, discord.Member) and not author.bot
  1388. if not valid_user or self.is_mod_or_superior(message):
  1389. return
  1390. await self.check_filter(message)
  1391. async def on_member_ban(self, member):
  1392. server = member.server
  1393. if not self.temp_cache.check(member, server, "BAN"):
  1394. await self.new_case(server,
  1395. user=member,
  1396. action="BAN")
  1397. async def on_member_unban(self, server, user):
  1398. if not self.temp_cache.check(user, server, "UNBAN"):
  1399. await self.new_case(server,
  1400. user=user,
  1401. action="UNBAN")
  1402. async def check_names(self, before, after):
  1403. if before.name != after.name:
  1404. if before.id not in self.past_names:
  1405. self.past_names[before.id] = [after.name]
  1406. else:
  1407. if after.name not in self.past_names[before.id]:
  1408. names = deque(self.past_names[before.id], maxlen=20)
  1409. names.append(after.name)
  1410. self.past_names[before.id] = list(names)
  1411. dataIO.save_json("data/mod/past_names.json", self.past_names)
  1412. if before.nick != after.nick and after.nick is not None:
  1413. server = before.server
  1414. if server.id not in self.past_nicknames:
  1415. self.past_nicknames[server.id] = {}
  1416. if before.id in self.past_nicknames[server.id]:
  1417. nicks = deque(self.past_nicknames[server.id][before.id],
  1418. maxlen=20)
  1419. else:
  1420. nicks = []
  1421. if after.nick not in nicks:
  1422. nicks.append(after.nick)
  1423. self.past_nicknames[server.id][before.id] = list(nicks)
  1424. dataIO.save_json("data/mod/past_nicknames.json",
  1425. self.past_nicknames)
  1426. def are_overwrites_empty(self, overwrites):
  1427. """There is currently no cleaner way to check if a
  1428. PermissionOverwrite object is empty"""
  1429. original = [p for p in iter(overwrites)]
  1430. empty = [p for p in iter(discord.PermissionOverwrite())]
  1431. return original == empty
  1432. def strfdelta(delta):
  1433. s = []
  1434. if delta.days:
  1435. ds = '%i day' % delta.days
  1436. if delta.days > 1:
  1437. ds += 's'
  1438. s.append(ds)
  1439. hrs, rem = divmod(delta.seconds, 60*60)
  1440. if hrs:
  1441. hs = '%i hr' % hrs
  1442. if hrs > 1:
  1443. hs += 's'
  1444. s.append(hs)
  1445. mins, secs = divmod(rem, 60)
  1446. if mins:
  1447. s.append('%i min' % mins)
  1448. if secs:
  1449. s.append('%i sec' % secs)
  1450. return ' '.join(s)
  1451. def check_folders():
  1452. folders = ("data", "data/mod/")
  1453. for folder in folders:
  1454. if not os.path.exists(folder):
  1455. print("Creating " + folder + " folder...")
  1456. os.makedirs(folder)
  1457. def check_files():
  1458. ignore_list = {"SERVERS": [], "CHANNELS": []}
  1459. files = {
  1460. "ignorelist.json" : ignore_list,
  1461. "filter.json" : {},
  1462. "past_names.json" : {},
  1463. "past_nicknames.json" : {},
  1464. "settings.json" : {},
  1465. "modlog.json" : {},
  1466. "perms_cache.json" : {}
  1467. }
  1468. for filename, value in files.items():
  1469. if not os.path.isfile("data/mod/{}".format(filename)):
  1470. print("Creating empty {}".format(filename))
  1471. dataIO.save_json("data/mod/{}".format(filename), value)
  1472. def setup(bot):
  1473. global logger
  1474. check_folders()
  1475. check_files()
  1476. logger = logging.getLogger("mod")
  1477. # Prevents the logger from being loaded again in case of module reload
  1478. if logger.level == 0:
  1479. logger.setLevel(logging.INFO)
  1480. handler = logging.FileHandler(
  1481. filename='data/mod/mod.log', encoding='utf-8', mode='a')
  1482. handler.setFormatter(
  1483. logging.Formatter('%(asctime)s %(message)s', datefmt="[%d/%m/%Y %H:%M]"))
  1484. logger.addHandler(handler)
  1485. n = Mod(bot)
  1486. bot.add_listener(n.check_names, "on_member_update")
  1487. bot.add_cog(n)