[Mod] Added mod-log and various antispam features

Modlog with settable channel
Toggeable autoban for X number of different mentions in a single message
Toggeable deletion of repeated messages
This commit is contained in:
Twentysix 2016-10-14 22:29:55 +02:00
parent 55c87abf88
commit 0f6e788214

View File

@ -3,12 +3,34 @@ from discord.ext import commands
from .utils.dataIO import dataIO from .utils.dataIO import dataIO
from .utils import checks from .utils import checks
from __main__ import send_cmd_help, settings from __main__ import send_cmd_help, settings
from collections import deque from collections import deque, defaultdict
from cogs.utils.chat_formatting import escape_mass_mentions from cogs.utils.chat_formatting import escape_mass_mentions, box
import os import os
import logging import logging
import asyncio import asyncio
default_settings = {
"ban_mention_spam" : False,
"delete_repeats" : False,
"mod-log" : None
}
class ModError(Exception):
pass
class UnauthorizedCaseEdit(ModError):
pass
class CaseMessageNotFound(ModError):
pass
class NoModLogChannel(ModError):
pass
class Mod: class Mod:
"""Moderation tools.""" """Moderation tools."""
@ -21,18 +43,28 @@ class Mod:
self.filter = dataIO.load_json("data/mod/filter.json") self.filter = dataIO.load_json("data/mod/filter.json")
self.past_names = dataIO.load_json("data/mod/past_names.json") self.past_names = dataIO.load_json("data/mod/past_names.json")
self.past_nicknames = dataIO.load_json("data/mod/past_nicknames.json") self.past_nicknames = dataIO.load_json("data/mod/past_nicknames.json")
settings = dataIO.load_json("data/mod/settings.json")
self.settings = defaultdict(lambda: default_settings.copy(), settings)
self.cache = defaultdict(lambda: deque(maxlen=3))
self.cases = dataIO.load_json("data/mod/modlog.json")
self._tmp_banned_cache = []
@commands.group(pass_context=True, no_pm=True) @commands.group(pass_context=True, no_pm=True)
@checks.serverowner_or_permissions(administrator=True) @checks.serverowner_or_permissions(administrator=True)
async def modset(self, ctx): async def modset(self, ctx):
"""Manages server administration settings.""" """Manages server administration settings."""
if ctx.invoked_subcommand is None: if ctx.invoked_subcommand is None:
server = ctx.message.server
await send_cmd_help(ctx) await send_cmd_help(ctx)
msg = "```" roles = settings.get_server(server).copy()
for k, v in settings.get_server(ctx.message.server).items(): _settings = {**self.settings[server.id], **roles}
msg += str(k) + ": " + str(v) + "\n" msg = ("Admin role: {ADMIN_ROLE}\n"
msg += "```" "Mod role: {MOD_ROLE}\n"
await self.bot.say(msg) "Mod-log: {mod-log}\n"
"Delete repeats: {delete_repeats}\n"
"Ban mention spam: {ban_mention_spam}\n"
"".format(**_settings))
await self.bot.say(box(msg))
@modset.command(name="adminrole", pass_context=True, no_pm=True) @modset.command(name="adminrole", pass_context=True, no_pm=True)
async def _modset_adminrole(self, ctx, role_name: str): async def _modset_adminrole(self, ctx, role_name: str):
@ -52,15 +84,81 @@ class Mod:
settings.set_server_mod(server, role_name) settings.set_server_mod(server, role_name)
await self.bot.say("Mod role set to '{}'".format(role_name)) await self.bot.say("Mod role set to '{}'".format(role_name))
@modset.command(pass_context=True, no_pm=True)
async def modlog(self, ctx, channel : discord.Channel=None):
"""Sets a channel as mod log
Leaving the channel parameter empty will deactivate it"""
server = ctx.message.server
if channel:
self.settings[server.id]["mod-log"] = channel.id
await self.bot.say("Mod events will be sent to {}"
"".format(channel.mention))
else:
if self.settings[server.id]["mod-log"] is None:
await send_cmd_help(ctx)
return
self.settings[server.id]["mod-log"] = None
await self.bot.say("Mod log deactivated.")
dataIO.save_json("data/mod/settings.json", self.settings)
@modset.command(pass_context=True, no_pm=True)
async def banmentionspam(self, ctx, max_mentions : int=False):
"""Enables auto ban for messages mentioning X different people
Accepted values: 5 or superior"""
server = ctx.message.server
if max_mentions:
if max_mentions < 5:
max_mentions = 5
self.settings[server.id]["ban_mention_spam"] = max_mentions
await self.bot.say("Autoban for mention spam enabled. "
"Anyone mentioning {} or more different people "
"in a single message will be autobanned."
"".format(max_mentions))
else:
if self.settings[server.id]["ban_mention_spam"] is False:
await send_cmd_help(ctx)
return
self.settings[server.id]["ban_mention_spam"] = False
await self.bot.say("Autoban for mention spam disabled.")
dataIO.save_json("data/mod/settings.json", self.settings)
@modset.command(pass_context=True, no_pm=True)
async def deleterepeats(self, ctx):
"""Enables auto deletion of repeated messages"""
server = ctx.message.server
if not self.settings[server.id]["delete_repeats"]:
self.settings[server.id]["delete_repeats"] = True
await self.bot.say("Messages repeated up to 3 times will"
"be deleted.")
else:
self.settings[server.id]["delete_repeats"] = False
await self.bot.say("Repeated messages will be ignored.")
dataIO.save_json("data/mod/settings.json", self.settings)
@modset.command(pass_context=True, no_pm=True)
async def resetcases(self, ctx):
"""Resets modlog's cases"""
server = ctx.message.server
self.cases[server.id] = {}
dataIO.save_json("data/mod/modlog.json", self.cases)
await self.bot.say("Cases have been reset.")
@commands.command(no_pm=True, pass_context=True) @commands.command(no_pm=True, pass_context=True)
@checks.admin_or_permissions(kick_members=True) @checks.admin_or_permissions(kick_members=True)
async def kick(self, ctx, user: discord.Member): async def kick(self, ctx, user: discord.Member):
"""Kicks user.""" """Kicks user."""
author = ctx.message.author author = ctx.message.author
server = author.server
try: try:
await self.bot.kick(user) await self.bot.kick(user)
logger.info("{}({}) kicked {}({})".format( logger.info("{}({}) kicked {}({})".format(
author.name, author.id, user.name, user.id)) author.name, author.id, user.name, user.id))
await self.new_case(server,
action="Kick \N{WOMANS BOOTS}",
mod=author,
user=user)
await self.bot.say("Done. That felt good.") await self.bot.say("Done. That felt good.")
except discord.errors.Forbidden: except discord.errors.Forbidden:
await self.bot.say("I'm not allowed to do that.") await self.bot.say("I'm not allowed to do that.")
@ -74,18 +172,27 @@ class Mod:
Minimum 0 days, maximum 7. Defaults to 0.""" Minimum 0 days, maximum 7. Defaults to 0."""
author = ctx.message.author author = ctx.message.author
server = author.server
if days < 0 or days > 7: if days < 0 or days > 7:
await self.bot.say("Invalid days. Must be between 0 and 7.") await self.bot.say("Invalid days. Must be between 0 and 7.")
return return
try: try:
self._tmp_banned_cache.append(user)
await self.bot.ban(user, days) await self.bot.ban(user, days)
logger.info("{}({}) banned {}({}), deleting {} days worth of messages".format( logger.info("{}({}) banned {}({}), deleting {} days worth of messages".format(
author.name, author.id, user.name, user.id, str(days))) author.name, author.id, user.name, user.id, str(days)))
await self.new_case(server,
action="Ban \N{HAMMER}",
mod=author,
user=user)
await self.bot.say("Done. It was about time.") await self.bot.say("Done. It was about time.")
except discord.errors.Forbidden: except discord.errors.Forbidden:
await self.bot.say("I'm not allowed to do that.") await self.bot.say("I'm not allowed to do that.")
except Exception as e: except Exception as e:
print(e) print(e)
finally:
await asyncio.sleep(1)
self._tmp_banned_cache.remove(user)
@commands.command(no_pm=True, pass_context=True) @commands.command(no_pm=True, pass_context=True)
@checks.admin_or_permissions(ban_members=True) @checks.admin_or_permissions(ban_members=True)
@ -108,10 +215,15 @@ class Mod:
"You can now join the server again.{}".format(invite)) "You can now join the server again.{}".format(invite))
except: except:
pass pass
self._tmp_banned_cache.append(user)
await self.bot.ban(user, 1) await self.bot.ban(user, 1)
logger.info("{}({}) softbanned {}({}), deleting 1 day worth " logger.info("{}({}) softbanned {}({}), deleting 1 day worth "
"of messages".format(author.name, author.id, user.name, "of messages".format(author.name, author.id, user.name,
user.id)) user.id))
await self.new_case(server,
action="Softban \N{DASH SYMBOL} \N{HAMMER}",
mod=author,
user=user)
await self.bot.unban(server, user) await self.bot.unban(server, user)
await self.bot.say("Done. Enough chaos.") await self.bot.say("Done. Enough chaos.")
except discord.errors.Forbidden: except discord.errors.Forbidden:
@ -119,6 +231,9 @@ class Mod:
await self.bot.delete_message(msg) await self.bot.delete_message(msg)
except Exception as e: except Exception as e:
print(e) print(e)
finally:
await asyncio.sleep(1)
self._tmp_banned_cache.remove(user)
else: else:
await self.bot.say("I'm not allowed to do that.") await self.bot.say("I'm not allowed to do that.")
@ -317,6 +432,27 @@ class Mod:
else: else:
await self.slow_deletion(to_delete) await self.slow_deletion(to_delete)
@commands.command(pass_context=True)
@checks.mod_or_permissions(manage_messages=True)
async def reason(self, ctx, case : int, *, reason : str):
author = ctx.message.author
server = author.server
case = str(case)
try:
await self.update_case(server, case=case, mod=author,
reason=reason)
except UnauthorizedCaseEdit:
await self.bot.say("That case is not yours.")
except KeyError:
await self.bot.say("That case doesn't exist.")
except NoModLogChannel:
await self.bot.say("There's no mod-log channel set.")
except CaseMessageNotFound:
await self.bot.say("Couldn't find the case's message.")
else:
await self.bot.say("Case updated.")
@commands.group(pass_context=True) @commands.group(pass_context=True)
@checks.is_owner() @checks.is_owner()
async def blacklist(self, ctx): async def blacklist(self, ctx):
@ -642,15 +778,7 @@ class Mod:
pass pass
await asyncio.sleep(1.5) await asyncio.sleep(1.5)
async def _delete_message(self, message): def is_mod_or_superior(self, message):
try:
await self.bot.delete_message(message)
except discord.errors.NotFound:
pass
except:
raise
def immune_from_filter(self, message):
user = message.author user = message.author
server = message.server server = message.server
admin_role = settings.get_server_admin(server) admin_role = settings.get_server_admin(server)
@ -665,26 +793,157 @@ class Mod:
else: else:
return False return False
async def new_case(self, server, *, action, mod=None, user, reason=None):
channel = server.get_channel(self.settings[server.id]["mod-log"])
if channel is None:
return
if server.id in self.cases:
case_n = len(self.cases[server.id]) + 1
else:
case_n = 1
case = {"case" : case_n,
"action" : action,
"user" : user.name,
"user_id" : user.id,
"reason" : reason,
"moderator" : mod.name if mod is not None else None,
"moderator_id" : mod.id if mod is not None else None}
if server.id not in self.cases:
self.cases[server.id] = {}
tmp = case.copy()
if case["reason"] is None:
tmp["reason"] = "Type [p]reason {} <reason> to add it".format(case_n)
if case["moderator"] is None:
tmp["moderator"] = "Unknown"
tmp["moderator_id"] = "Nobody has claimed responsability yet"
case_msg = ("**Case #{case}** | {action}\n"
"**User:** {user} ({user_id})\n"
"**Moderator:** {moderator} ({moderator_id})\n"
"**Reason:** {reason}"
"".format(**tmp))
try:
msg = await self.bot.send_message(channel, case_msg)
except:
msg = None
case["message"] = msg.id if msg is not None else None
self.cases[server.id][str(case_n)] = case
dataIO.save_json("data/mod/modlog.json", self.cases)
async def update_case(self, server, *, case, mod, reason):
channel = server.get_channel(self.settings[server.id]["mod-log"])
if channel is None:
raise NoModLogChannel()
case = str(case)
case = self.cases[server.id][case]
if case["moderator_id"] is not None:
if case["moderator_id"] != mod.id:
raise UnauthorizedCaseEdit()
case["reason"] = reason
case["moderator"] = mod.name
case["moderator_id"] = mod.id
case_msg = ("**Case #{case}** | {action}\n"
"**User:** {user} ({user_id})\n"
"**Moderator:** {moderator} ({moderator_id})\n"
"**Reason:** {reason}"
"".format(**case))
dataIO.save_json("data/mod/modlog.json", self.cases)
msg = await self.bot.get_message(channel, case["message"])
if msg:
await self.bot.edit_message(msg, case_msg.format(**case))
else:
raise CaseMessageNotFound()
async def check_filter(self, message): async def check_filter(self, message):
if message.channel.is_private:
return
server = message.server server = message.server
can_delete = message.channel.permissions_for(server.me).manage_messages
if (message.author.id == self.bot.user.id or
self.immune_from_filter(message) or not can_delete): # Owner, admins and mods are immune to the filter
return
if server.id in self.filter.keys(): if server.id in self.filter.keys():
for w in self.filter[server.id]: for w in self.filter[server.id]:
if w in message.content.lower(): if w in message.content.lower():
# Something else in discord.py is throwing a 404 error
# after deletion
try: try:
await self._delete_message(message) await self.bot.delete_message(message)
logger.info("Message deleted in server {}."
"Filtered: {}"
"".format(server.id, w))
return True
except: except:
pass pass
print("Message deleted. Filtered: " + w) return False
async def check_duplicates(self, message):
server = message.server
author = message.author
if server.id not in self.settings:
return False
if self.settings[server.id]["delete_repeats"]:
self.cache[author].append(message)
msgs = self.cache[author]
if len(msgs) == 3 and \
msgs[0].content == msgs[1].content == msgs[2].content:
try:
await self.bot.delete_message(message)
return True
except:
pass
return False
async def check_mention_spam(self, message):
server = message.server
author = message.author
if server.id not in self.settings:
return False
if self.settings[server.id]["ban_mention_spam"]:
max_mentions = self.settings[server.id]["ban_mention_spam"]
mentions = set(message.mentions)
if len(mentions) >= max_mentions:
try:
self._tmp_banned_cache.append(author)
await self.bot.ban(author, 1)
except:
logger.info("Failed to ban member for mention spam in "
"server {}".format(server.id))
else:
await self.new_case(server,
action="Ban \N{HAMMER}",
mod=server.me,
user=author,
reason="Mention spam (Autoban)")
return True
finally:
await asyncio.sleep(1)
self._tmp_banned_cache.remove(author)
return False
async def on_message(self, message):
if message.channel.is_private or self.bot.user == message.author:
return
elif self.is_mod_or_superior(message):
return
deleted = await self.check_filter(message)
if not deleted:
deleted = await self.check_duplicates(message)
if not deleted:
deleted = await self.check_mention_spam(message)
async def on_member_ban(self, member):
if member not in self._tmp_banned_cache:
server = member.server
await self.new_case(server,
user=member,
action="Ban \N{HAMMER}")
async def check_names(self, before, after): async def check_names(self, before, after):
if before.name != after.name: if before.name != after.name:
@ -748,6 +1007,14 @@ def check_files():
print("Creating empty past_nicknames.json...") print("Creating empty past_nicknames.json...")
dataIO.save_json("data/mod/past_nicknames.json", {}) dataIO.save_json("data/mod/past_nicknames.json", {})
if not os.path.isfile("data/mod/settings.json"):
print("Creating empty settings.json...")
dataIO.save_json("data/mod/settings.json", {})
if not os.path.isfile("data/mod/modlog.json"):
print("Creating empty modlog.json...")
dataIO.save_json("data/mod/modlog.json", {})
def setup(bot): def setup(bot):
global logger global logger
@ -763,6 +1030,5 @@ def setup(bot):
logging.Formatter('%(asctime)s %(message)s', datefmt="[%d/%m/%Y %H:%M]")) logging.Formatter('%(asctime)s %(message)s', datefmt="[%d/%m/%Y %H:%M]"))
logger.addHandler(handler) logger.addHandler(handler)
n = Mod(bot) n = Mod(bot)
bot.add_listener(n.check_filter, "on_message")
bot.add_listener(n.check_names, "on_member_update") bot.add_listener(n.check_names, "on_member_update")
bot.add_cog(n) bot.add_cog(n)