Twentysix 27bdce7f32 [Core] CLI flags, env vars login, revamped setup, in. selfbot support, etc (#513)
Startup flags: can be started and configured without ever doing the interactive setup
Changed default settings format so that all unset values are None
Removed new cogs prompt
Removed `LOGIN_TYPE` from settings.json. It now defaults to token and fallbacks to email/password
Smarter initial setup: only asks for the settings that are actually missing
For the first installation all default cogs are loaded
Startup flag that allows settings to be memory-only
Initial selfbot support
Only reset login credentials (on confirmation) instead of deleting the whole file in case of login failure
Revamped main screen
Made sure that nothing blows up when you run Red on Windows without `chcp 65001`
Possibility of setting credentials in the environment variables `RED_TOKEN` / `RED_EMAIL` `RED_PASSWORD`. They will take priority over the configuration stored on disk.
2016-12-11 00:16:20 +01:00

1393 lines
54 KiB
Python

import discord
from discord.ext import commands
from .utils.dataIO import dataIO
from .utils import checks
from __main__ import send_cmd_help, settings
from collections import deque, defaultdict
from cogs.utils.chat_formatting import escape_mass_mentions, box
import os
import re
import logging
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:
"""Moderation tools."""
def __init__(self, bot):
self.bot = bot
self.whitelist_list = dataIO.load_json("data/mod/whitelist.json")
self.blacklist_list = dataIO.load_json("data/mod/blacklist.json")
self.ignore_list = dataIO.load_json("data/mod/ignorelist.json")
self.filter = dataIO.load_json("data/mod/filter.json")
self.past_names = dataIO.load_json("data/mod/past_names.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.last_case = defaultdict(dict)
self._tmp_banned_cache = []
perms_cache = dataIO.load_json("data/mod/perms_cache.json")
self._perms_cache = defaultdict(dict, perms_cache)
@commands.group(pass_context=True, no_pm=True)
@checks.serverowner_or_permissions(administrator=True)
async def modset(self, ctx):
"""Manages server administration settings."""
if ctx.invoked_subcommand is None:
server = ctx.message.server
await send_cmd_help(ctx)
roles = settings.get_server(server).copy()
_settings = {**self.settings[server.id], **roles}
if "delete_delay" not in _settings:
_settings["delete_delay"] = -1
msg = ("Admin role: {ADMIN_ROLE}\n"
"Mod role: {MOD_ROLE}\n"
"Mod-log: {mod-log}\n"
"Delete repeats: {delete_repeats}\n"
"Ban mention spam: {ban_mention_spam}\n"
"Delete delay: {delete_delay}\n"
"".format(**_settings))
await self.bot.say(box(msg))
@modset.command(name="adminrole", pass_context=True, no_pm=True)
async def _modset_adminrole(self, ctx, role_name: str):
"""Sets the admin role for this server, case insensitive."""
server = ctx.message.server
if server.id not in settings.servers:
await self.bot.say("Remember to set modrole too.")
settings.set_server_admin(server, role_name)
await self.bot.say("Admin role set to '{}'".format(role_name))
@modset.command(name="modrole", pass_context=True, no_pm=True)
async def _modset_modrole(self, ctx, role_name: str):
"""Sets the mod role for this server, case insensitive."""
server = ctx.message.server
if server.id not in settings.servers:
await self.bot.say("Remember to set adminrole too.")
settings.set_server_mod(server, 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.")
@modset.command(pass_context=True, no_pm=True)
async def deletedelay(self, ctx, time: int=None):
"""Sets the delay until the bot removes the command message.
Must be between -1 and 60.
A delay of -1 means the bot will not remove the message."""
server = ctx.message.server
if time is not None:
time = min(max(time, -1), 60) # Enforces the time limits
self.settings[server.id]["delete_delay"] = time
if time == -1:
await self.bot.say("Command deleting disabled.")
else:
await self.bot.say("Delete delay set to {}"
" seconds.".format(time))
dataIO.save_json("data/mod/settings.json", self.settings)
else:
try:
delay = self.settings[server.id]["delete_delay"]
except KeyError:
await self.bot.say("Delete delay not yet set up on this"
" server.")
else:
if delay != -1:
await self.bot.say("Bot will delete command messages after"
" {} seconds. Set this value to -1 to"
" stop deleting messages".format(delay))
else:
await self.bot.say("I will not delete command messages.")
@commands.command(no_pm=True, pass_context=True)
@checks.admin_or_permissions(kick_members=True)
async def kick(self, ctx, user: discord.Member):
"""Kicks user."""
author = ctx.message.author
server = author.server
try:
await self.bot.kick(user)
logger.info("{}({}) kicked {}({})".format(
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.")
except discord.errors.Forbidden:
await self.bot.say("I'm not allowed to do that.")
except Exception as e:
print(e)
@commands.command(no_pm=True, pass_context=True)
@checks.admin_or_permissions(ban_members=True)
async def ban(self, ctx, user: discord.Member, days: int=0):
"""Bans user and deletes last X days worth of messages.
Minimum 0 days, maximum 7. Defaults to 0."""
author = ctx.message.author
server = author.server
if days < 0 or days > 7:
await self.bot.say("Invalid days. Must be between 0 and 7.")
return
try:
self._tmp_banned_cache.append(user)
await self.bot.ban(user, days)
logger.info("{}({}) banned {}({}), deleting {} days worth of messages".format(
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.")
except discord.errors.Forbidden:
await self.bot.say("I'm not allowed to do that.")
except Exception as e:
print(e)
finally:
await asyncio.sleep(1)
self._tmp_banned_cache.remove(user)
@commands.command(no_pm=True, pass_context=True)
@checks.admin_or_permissions(ban_members=True)
async def softban(self, ctx, user: discord.Member):
"""Kicks the user, deleting 1 day worth of messages."""
server = ctx.message.server
channel = ctx.message.channel
can_ban = channel.permissions_for(server.me).ban_members
author = ctx.message.author
try:
invite = await self.bot.create_invite(server, max_age=3600*24)
invite = "\nInvite: " + invite
except:
invite = ""
if can_ban:
try:
try: # We don't want blocked DMs preventing us from banning
msg = await self.bot.send_message(user, "You have been banned and "
"then unbanned as a quick way to delete your messages.\n"
"You can now join the server again.{}".format(invite))
except:
pass
self._tmp_banned_cache.append(user)
await self.bot.ban(user, 1)
logger.info("{}({}) softbanned {}({}), deleting 1 day worth "
"of messages".format(author.name, author.id, user.name,
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.say("Done. Enough chaos.")
except discord.errors.Forbidden:
await self.bot.say("My role is not high enough to softban that user.")
await self.bot.delete_message(msg)
except Exception as e:
print(e)
finally:
await asyncio.sleep(1)
self._tmp_banned_cache.remove(user)
else:
await self.bot.say("I'm not allowed to do that.")
@commands.command(no_pm=True, pass_context=True)
@checks.admin_or_permissions(manage_nicknames=True)
async def rename(self, ctx, user : discord.Member, *, nickname=""):
"""Changes user's nickname
Leaving the nickname empty will remove it."""
nickname = nickname.strip()
if nickname == "":
nickname = None
try:
await self.bot.change_nickname(user, nickname)
await self.bot.say("Done.")
except discord.Forbidden:
await self.bot.say("I cannot do that, I lack the "
"\"Manage Nicknames\" permission.")
@commands.group(pass_context=True, no_pm=True, invoke_without_command=True)
@checks.mod_or_permissions(administrator=True)
async def mute(self, ctx, user : discord.Member):
"""Mutes user in the channel/server
Defaults to channel"""
if ctx.invoked_subcommand is None:
await ctx.invoke(self.channel_mute, user=user)
@mute.command(name="channel", pass_context=True, no_pm=True)
async def channel_mute(self, ctx, user : discord.Member):
"""Mutes user in the current channel"""
channel = ctx.message.channel
overwrites = channel.overwrites_for(user)
if overwrites.send_messages is False:
await self.bot.say("That user can't send messages in this "
"channel.")
return
self._perms_cache[user.id][channel.id] = overwrites.send_messages
overwrites.send_messages = False
try:
await self.bot.edit_channel_permissions(channel, user, overwrites)
except discord.Forbidden:
await self.bot.say("Failed to mute user. I need the manage roles "
"permission and the user I'm muting must be "
"lower than myself in the role hierarchy.")
else:
dataIO.save_json("data/mod/perms_cache.json", self._perms_cache)
await self.bot.say("User has been muted in this channel.")
@mute.command(name="server", pass_context=True, no_pm=True)
async def server_mute(self, ctx, user : discord.Member):
"""Mutes user in the server"""
server = ctx.message.server
register = {}
for channel in server.channels:
if channel.type != discord.ChannelType.text:
continue
overwrites = channel.overwrites_for(user)
if overwrites.send_messages is False:
continue
register[channel.id] = overwrites.send_messages
overwrites.send_messages = False
try:
await self.bot.edit_channel_permissions(channel, user,
overwrites)
except discord.Forbidden:
await self.bot.say("Failed to mute user. I need the manage roles "
"permission and the user I'm muting must be "
"lower than myself in the role hierarchy.")
return
else:
await asyncio.sleep(0.1)
if not register:
await self.bot.say("That user is already muted in all channels.")
return
self._perms_cache[user.id] = register
dataIO.save_json("data/mod/perms_cache.json", self._perms_cache)
await self.bot.say("User has been muted in this server.")
@commands.group(pass_context=True, no_pm=True, invoke_without_command=True)
@checks.mod_or_permissions(administrator=True)
async def unmute(self, ctx, user : discord.Member):
"""Unmutes user in the channel/server
Defaults to channel"""
if ctx.invoked_subcommand is None:
await ctx.invoke(self.channel_unmute, user=user)
@unmute.command(name="channel", pass_context=True, no_pm=True)
async def channel_unmute(self, ctx, user : discord.Member):
"""Unmutes user in the current channel"""
channel = ctx.message.channel
overwrites = channel.overwrites_for(user)
if overwrites.send_messages:
await self.bot.say("That user doesn't seem to be muted "
"in this channel.")
return
if user.id in self._perms_cache:
old_value = self._perms_cache[user.id].get(channel.id, None)
else:
old_value = None
overwrites.send_messages = old_value
is_empty = self.are_overwrites_empty(overwrites)
try:
if not is_empty:
await self.bot.edit_channel_permissions(channel, user,
overwrites)
else:
await self.bot.delete_channel_permissions(channel, user)
except discord.Forbidden:
await self.bot.say("Failed to unmute user. I need the manage roles"
" permission and the user I'm unmuting must be "
"lower than myself in the role hierarchy.")
else:
try:
del self._perms_cache[user.id][channel.id]
except KeyError:
pass
if user.id in self._perms_cache and not self._perms_cache[user.id]:
del self._perms_cache[user.id] #cleanup
dataIO.save_json("data/mod/perms_cache.json", self._perms_cache)
await self.bot.say("User has been unmuted in this channel.")
@unmute.command(name="server", pass_context=True, no_pm=True)
async def server_unmute(self, ctx, user : discord.Member):
"""Unmutes user in the server"""
server = ctx.message.server
if user.id not in self._perms_cache:
await self.bot.say("That user doesn't seem to have been muted with {0}mute commands. "
"Unmute them in the channels you want with `{0}unmute <user>`"
"".format(ctx.prefix))
return
for channel in server.channels:
if channel.type != discord.ChannelType.text:
continue
if channel.id not in self._perms_cache[user.id]:
continue
value = self._perms_cache[user.id].get(channel.id)
overwrites = channel.overwrites_for(user)
if overwrites.send_messages is False:
overwrites.send_messages = value
is_empty = self.are_overwrites_empty(overwrites)
try:
if not is_empty:
await self.bot.edit_channel_permissions(channel, user,
overwrites)
else:
await self.bot.delete_channel_permissions(channel, user)
except discord.Forbidden:
await self.bot.say("Failed to unmute user. I need the manage roles"
" permission and the user I'm unmuting must be "
"lower than myself in the role hierarchy.")
return
else:
del self._perms_cache[user.id][channel.id]
await asyncio.sleep(0.1)
if user.id in self._perms_cache and not self._perms_cache[user.id]:
del self._perms_cache[user.id] #cleanup
dataIO.save_json("data/mod/perms_cache.json", self._perms_cache)
await self.bot.say("User has been unmuted in this server.")
@commands.group(pass_context=True)
@checks.mod_or_permissions(manage_messages=True)
async def cleanup(self, ctx):
"""Deletes messages."""
if ctx.invoked_subcommand is None:
await send_cmd_help(ctx)
@cleanup.command(pass_context=True, no_pm=True)
async def text(self, ctx, text: str, number: int):
"""Deletes last X messages matching the specified text.
Example:
cleanup text \"test\" 5
Remember to use double quotes."""
channel = ctx.message.channel
author = ctx.message.author
server = author.server
is_bot = self.bot.user.bot
has_permissions = channel.permissions_for(server.me).manage_messages
def check(m):
if text in m.content:
return True
elif m == ctx.message:
return True
else:
return False
to_delete = [ctx.message]
if not has_permissions:
await self.bot.say("I'm not allowed to delete messages.")
return
tries_left = 5
tmp = ctx.message
while tries_left and len(to_delete) - 1 < number:
async for message in self.bot.logs_from(channel, limit=100,
before=tmp):
if len(to_delete) - 1 < number and check(message):
to_delete.append(message)
tmp = message
tries_left -= 1
logger.info("{}({}) deleted {} messages "
" containing '{}' in channel {}".format(author.name,
author.id, len(to_delete), text, channel.id))
if is_bot:
await self.mass_purge(to_delete)
else:
await self.slow_deletion(to_delete)
@cleanup.command(pass_context=True, no_pm=True)
async def user(self, ctx, user: discord.Member, number: int):
"""Deletes last X messages from specified user.
Examples:
cleanup user @\u200bTwentysix 2
cleanup user Red 6"""
channel = ctx.message.channel
author = ctx.message.author
server = author.server
is_bot = self.bot.user.bot
has_permissions = channel.permissions_for(server.me).manage_messages
def check(m):
if m.author == user:
return True
elif m == ctx.message:
return True
else:
return False
to_delete = [ctx.message]
if not has_permissions:
await self.bot.say("I'm not allowed to delete messages.")
return
tries_left = 5
tmp = ctx.message
while tries_left and len(to_delete) - 1 < number:
async for message in self.bot.logs_from(channel, limit=100,
before=tmp):
if len(to_delete) - 1 < number and check(message):
to_delete.append(message)
tmp = message
tries_left -= 1
logger.info("{}({}) deleted {} messages "
" made by {}({}) in channel {}"
"".format(author.name, author.id, len(to_delete),
user.name, user.id, channel.name))
if is_bot:
await self.mass_purge(to_delete)
else:
await self.slow_deletion(to_delete)
@cleanup.command(pass_context=True, no_pm=True)
async def after(self, ctx, message_id : int):
"""Deletes all messages after specified message
To get a message id, enable developer mode in Discord's
settings, 'appearance' tab. Then right click a message
and copy its id.
This command only works on bots running as bot accounts.
"""
channel = ctx.message.channel
author = ctx.message.author
server = channel.server
is_bot = self.bot.user.bot
has_permissions = channel.permissions_for(server.me).manage_messages
if not is_bot:
await self.bot.say("This command can only be used on bots with "
"bot accounts.")
return
to_delete = []
after = await self.bot.get_message(channel, message_id)
if not has_permissions:
await self.bot.say("I'm not allowed to delete messages.")
return
elif not after:
await self.bot.say("Message not found.")
return
async for message in self.bot.logs_from(channel, limit=2000,
after=after):
to_delete.append(message)
logger.info("{}({}) deleted {} messages in channel {}"
"".format(author.name, author.id,
len(to_delete), channel.name))
await self.mass_purge(to_delete)
@cleanup.command(pass_context=True, no_pm=True)
async def messages(self, ctx, number: int):
"""Deletes last X messages.
Example:
cleanup messages 26"""
channel = ctx.message.channel
author = ctx.message.author
server = author.server
is_bot = self.bot.user.bot
has_permissions = channel.permissions_for(server.me).manage_messages
to_delete = []
if not has_permissions:
await self.bot.say("I'm not allowed to delete messages.")
return
async for message in self.bot.logs_from(channel, limit=number+1):
to_delete.append(message)
logger.info("{}({}) deleted {} messages in channel {}"
"".format(author.name, author.id,
number, channel.name))
if is_bot:
await self.mass_purge(to_delete)
else:
await self.slow_deletion(to_delete)
@cleanup.command(pass_context=True, no_pm=True, name='bot')
async def cleanup_bot(self, ctx, number: int):
"""Cleans up command messages and messages from the bot"""
channel = ctx.message.channel
author = ctx.message.author
server = channel.server
is_bot = self.bot.user.bot
has_permissions = channel.permissions_for(server.me).manage_messages
prefixes = self.bot.command_prefix
if isinstance(prefixes, str):
prefixes = [prefixes]
elif callable(prefixes):
if asyncio.iscoroutine(prefixes):
await self.bot.say('Coroutine prefixes not yet implemented.')
return
prefixes = prefixes(self.bot, ctx.message)
# In case some idiot sets a null prefix
if '' in prefixes:
prefixes.pop('')
def check(m):
if m.author.id == self.bot.user.id:
return True
elif m == ctx.message:
return True
p = discord.utils.find(m.content.startswith, prefixes)
if p and len(p) > 0:
return m.content[len(p):].startswith(tuple(self.bot.commands))
return False
to_delete = [ctx.message]
if not has_permissions:
await self.bot.say("I'm not allowed to delete messages.")
return
tries_left = 5
tmp = ctx.message
while tries_left and len(to_delete) - 1 < number:
async for message in self.bot.logs_from(channel, limit=100,
before=tmp):
if len(to_delete) - 1 < number and check(message):
to_delete.append(message)
tmp = message
tries_left -= 1
logger.info("{}({}) deleted {} "
" command messages in channel {}"
"".format(author.name, author.id, len(to_delete),
channel.name))
if is_bot:
await self.mass_purge(to_delete)
else:
await self.slow_deletion(to_delete)
@cleanup.command(pass_context=True, name='self')
async def cleanup_self(self, ctx, number: int, match_pattern: str = None):
"""Cleans up messages owned by the bot.
By default, all messages are cleaned. If a third argument is specified,
it is used for pattern matching: If it begins with r( and ends with ),
then it is interpreted as a regex, and messages that match it are
deleted. Otherwise, it is used in a simple substring test.
Some helpful regex flags to include in your pattern:
Dots match newlines: (?s); Ignore case: (?i); Both: (?si)
"""
channel = ctx.message.channel
author = ctx.message.author
is_bot = self.bot.user.bot
# You can always delete your own messages, this is needed to purge
can_mass_purge = False
if type(author) is discord.Member:
me = channel.server.me
can_mass_purge = channel.permissions_for(me).manage_messages
use_re = (match_pattern and match_pattern.startswith('r(') and
match_pattern.endswith(')'))
if use_re:
match_pattern = match_pattern[1:] # strip 'r'
match_re = re.compile(match_pattern)
def content_match(c):
return bool(match_re.match(c))
elif match_pattern:
def content_match(c):
return match_pattern in c
else:
def content_match(_):
return True
def check(m):
if m.author.id != self.bot.user.id:
return False
elif content_match(m.content):
return True
return False
to_delete = []
# Selfbot convenience, delete trigger message
if author == self.bot.user:
to_delete.append(ctx.message)
number += 1
tries_left = 5
tmp = ctx.message
while tries_left and len(to_delete) < number:
async for message in self.bot.logs_from(channel, limit=100,
before=tmp):
if len(to_delete) < number and check(message):
to_delete.append(message)
tmp = message
tries_left -= 1
if channel.name:
channel_name = 'channel ' + channel.name
else:
channel_name = str(channel)
logger.info("{}({}) deleted {} messages "
"sent by the bot in {}"
"".format(author.name, author.id, len(to_delete),
channel_name))
if is_bot and can_mass_purge:
await self.mass_purge(to_delete)
else:
await self.slow_deletion(to_delete)
@commands.command(pass_context=True)
@checks.mod_or_permissions(manage_messages=True)
async def reason(self, ctx, case, *, reason : str=""):
"""Lets you specify a reason for mod-log's cases
Defaults to last case assigned to yourself, if available."""
author = ctx.message.author
server = author.server
try:
case = int(case)
if not reason:
await send_cmd_help(ctx)
return
except:
if reason:
reason = "{} {}".format(case, reason)
else:
reason = case
case = self.last_case[server.id].get(author.id, None)
if case is None:
await send_cmd_help(ctx)
return
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.".format(case))
@commands.group(pass_context=True)
@checks.is_owner()
async def blacklist(self, ctx):
"""Bans user from using the bot"""
if ctx.invoked_subcommand is None:
await send_cmd_help(ctx)
@blacklist.command(name="add")
async def _blacklist_add(self, user: discord.Member):
"""Adds user to bot's blacklist"""
if user.id not in self.blacklist_list:
self.blacklist_list.append(user.id)
dataIO.save_json("data/mod/blacklist.json", self.blacklist_list)
await self.bot.say("User has been added to blacklist.")
else:
await self.bot.say("User is already blacklisted.")
@blacklist.command(name="remove")
async def _blacklist_remove(self, user: discord.Member):
"""Removes user from bot's blacklist"""
if user.id in self.blacklist_list:
self.blacklist_list.remove(user.id)
dataIO.save_json("data/mod/blacklist.json", self.blacklist_list)
await self.bot.say("User has been removed from blacklist.")
else:
await self.bot.say("User is not in blacklist.")
@blacklist.command(name="clear")
async def _blacklist_clear(self):
"""Clears the blacklist"""
self.blacklist_list = []
dataIO.save_json("data/mod/blacklist.json", self.blacklist_list)
await self.bot.say("Blacklist is now empty.")
@commands.group(pass_context=True)
@checks.is_owner()
async def whitelist(self, ctx):
"""Users who will be able to use the bot"""
if ctx.invoked_subcommand is None:
await send_cmd_help(ctx)
@whitelist.command(name="add")
async def _whitelist_add(self, user: discord.Member):
"""Adds user to bot's whitelist"""
if user.id not in self.whitelist_list:
if not self.whitelist_list:
msg = "\nAll users not in whitelist will be ignored (owner, admins and mods excluded)"
else:
msg = ""
self.whitelist_list.append(user.id)
dataIO.save_json("data/mod/whitelist.json", self.whitelist_list)
await self.bot.say("User has been added to whitelist." + msg)
else:
await self.bot.say("User is already whitelisted.")
@whitelist.command(name="remove")
async def _whitelist_remove(self, user: discord.Member):
"""Removes user from bot's whitelist"""
if user.id in self.whitelist_list:
self.whitelist_list.remove(user.id)
dataIO.save_json("data/mod/whitelist.json", self.whitelist_list)
await self.bot.say("User has been removed from whitelist.")
else:
await self.bot.say("User is not in whitelist.")
@whitelist.command(name="clear")
async def _whitelist_clear(self):
"""Clears the whitelist"""
self.whitelist_list = []
dataIO.save_json("data/mod/whitelist.json", self.whitelist_list)
await self.bot.say("Whitelist is now empty.")
@commands.group(pass_context=True, no_pm=True)
@checks.admin_or_permissions(manage_channels=True)
async def ignore(self, ctx):
"""Adds servers/channels to ignorelist"""
if ctx.invoked_subcommand is None:
await send_cmd_help(ctx)
await self.bot.say(self.count_ignored())
@ignore.command(name="channel", pass_context=True)
async def ignore_channel(self, ctx, channel: discord.Channel=None):
"""Ignores channel
Defaults to current one"""
current_ch = ctx.message.channel
if not channel:
if current_ch.id not in self.ignore_list["CHANNELS"]:
self.ignore_list["CHANNELS"].append(current_ch.id)
dataIO.save_json("data/mod/ignorelist.json", self.ignore_list)
await self.bot.say("Channel added to ignore list.")
else:
await self.bot.say("Channel already in ignore list.")
else:
if channel.id not in self.ignore_list["CHANNELS"]:
self.ignore_list["CHANNELS"].append(channel.id)
dataIO.save_json("data/mod/ignorelist.json", self.ignore_list)
await self.bot.say("Channel added to ignore list.")
else:
await self.bot.say("Channel already in ignore list.")
@ignore.command(name="server", pass_context=True)
async def ignore_server(self, ctx):
"""Ignores current server"""
server = ctx.message.server
if server.id not in self.ignore_list["SERVERS"]:
self.ignore_list["SERVERS"].append(server.id)
dataIO.save_json("data/mod/ignorelist.json", self.ignore_list)
await self.bot.say("This server has been added to the ignore list.")
else:
await self.bot.say("This server is already being ignored.")
@commands.group(pass_context=True, no_pm=True)
@checks.admin_or_permissions(manage_channels=True)
async def unignore(self, ctx):
"""Removes servers/channels from ignorelist"""
if ctx.invoked_subcommand is None:
await send_cmd_help(ctx)
await self.bot.say(self.count_ignored())
@unignore.command(name="channel", pass_context=True)
async def unignore_channel(self, ctx, channel: discord.Channel=None):
"""Removes channel from ignore list
Defaults to current one"""
current_ch = ctx.message.channel
if not channel:
if current_ch.id in self.ignore_list["CHANNELS"]:
self.ignore_list["CHANNELS"].remove(current_ch.id)
dataIO.save_json("data/mod/ignorelist.json", self.ignore_list)
await self.bot.say("This channel has been removed from the ignore list.")
else:
await self.bot.say("This channel is not in the ignore list.")
else:
if channel.id in self.ignore_list["CHANNELS"]:
self.ignore_list["CHANNELS"].remove(channel.id)
dataIO.save_json("data/mod/ignorelist.json", self.ignore_list)
await self.bot.say("Channel removed from ignore list.")
else:
await self.bot.say("That channel is not in the ignore list.")
@unignore.command(name="server", pass_context=True)
async def unignore_server(self, ctx):
"""Removes current server from ignore list"""
server = ctx.message.server
if server.id in self.ignore_list["SERVERS"]:
self.ignore_list["SERVERS"].remove(server.id)
dataIO.save_json("data/mod/ignorelist.json", self.ignore_list)
await self.bot.say("This server has been removed from the ignore list.")
else:
await self.bot.say("This server is not in the ignore list.")
def count_ignored(self):
msg = "```Currently ignoring:\n"
msg += str(len(self.ignore_list["CHANNELS"])) + " channels\n"
msg += str(len(self.ignore_list["SERVERS"])) + " servers\n```\n"
return msg
@commands.group(name="filter", pass_context=True, no_pm=True)
@checks.mod_or_permissions(manage_messages=True)
async def _filter(self, ctx):
"""Adds/removes words from filter
Use double quotes to add/remove sentences
Using this command with no subcommands will send
the list of the server's filtered words."""
if ctx.invoked_subcommand is None:
await send_cmd_help(ctx)
server = ctx.message.server
author = ctx.message.author
msg = ""
if server.id in self.filter.keys():
if self.filter[server.id] != []:
word_list = self.filter[server.id]
for w in word_list:
msg += '"' + w + '" '
await self.bot.send_message(author, "Words filtered in this server: " + msg)
@_filter.command(name="add", pass_context=True)
async def filter_add(self, ctx, *words: str):
"""Adds words to the filter
Use double quotes to add sentences
Examples:
filter add word1 word2 word3
filter add \"This is a sentence\""""
if words == ():
await send_cmd_help(ctx)
return
server = ctx.message.server
added = 0
if server.id not in self.filter.keys():
self.filter[server.id] = []
for w in words:
if w.lower() not in self.filter[server.id] and w != "":
self.filter[server.id].append(w.lower())
added += 1
if added:
dataIO.save_json("data/mod/filter.json", self.filter)
await self.bot.say("Words added to filter.")
else:
await self.bot.say("Words already in the filter.")
@_filter.command(name="remove", pass_context=True)
async def filter_remove(self, ctx, *words: str):
"""Remove words from the filter
Use double quotes to remove sentences
Examples:
filter remove word1 word2 word3
filter remove \"This is a sentence\""""
if words == ():
await send_cmd_help(ctx)
return
server = ctx.message.server
removed = 0
if server.id not in self.filter.keys():
await self.bot.say("There are no filtered words in this server.")
return
for w in words:
if w.lower() in self.filter[server.id]:
self.filter[server.id].remove(w.lower())
removed += 1
if removed:
dataIO.save_json("data/mod/filter.json", self.filter)
await self.bot.say("Words removed from filter.")
else:
await self.bot.say("Those words weren't in the filter.")
@commands.group(no_pm=True, pass_context=True)
@checks.admin_or_permissions(manage_roles=True)
async def editrole(self, ctx):
"""Edits roles settings"""
if ctx.invoked_subcommand is None:
await send_cmd_help(ctx)
@editrole.command(aliases=["color"], pass_context=True)
async def colour(self, ctx, role: discord.Role, value: discord.Colour):
"""Edits a role's colour
Use double quotes if the role contains spaces.
Colour must be in hexadecimal format.
\"http://www.w3schools.com/colors/colors_picker.asp\"
Examples:
!editrole colour \"The Transistor\" #ff0000
!editrole colour Test #ff9900"""
author = ctx.message.author
try:
await self.bot.edit_role(ctx.message.server, role, color=value)
logger.info("{}({}) changed the colour of role '{}'".format(
author.name, author.id, role.name))
await self.bot.say("Done.")
except discord.Forbidden:
await self.bot.say("I need permissions to manage roles first.")
except Exception as e:
print(e)
await self.bot.say("Something went wrong.")
@editrole.command(name="name", pass_context=True)
@checks.admin_or_permissions(administrator=True)
async def edit_role_name(self, ctx, role: discord.Role, name: str):
"""Edits a role's name
Use double quotes if the role or the name contain spaces.
Examples:
!editrole name \"The Transistor\" Test"""
if name == "":
await self.bot.say("Name cannot be empty.")
return
try:
author = ctx.message.author
old_name = role.name # probably not necessary?
await self.bot.edit_role(ctx.message.server, role, name=name)
logger.info("{}({}) changed the name of role '{}' to '{}'".format(
author.name, author.id, old_name, name))
await self.bot.say("Done.")
except discord.Forbidden:
await self.bot.say("I need permissions to manage roles first.")
except Exception as e:
print(e)
await self.bot.say("Something went wrong.")
@commands.command()
async def names(self, user : discord.Member):
"""Show previous names/nicknames of a user"""
server = user.server
names = self.past_names[user.id] if user.id in self.past_names else None
try:
nicks = self.past_nicknames[server.id][user.id]
nicks = [escape_mass_mentions(nick) for nick in nicks]
except:
nicks = None
msg = ""
if names:
names = [escape_mass_mentions(name) for name in names]
msg += "**Past 20 names**:\n"
msg += ", ".join(names)
if nicks:
if msg:
msg += "\n\n"
msg += "**Past 20 nicknames**:\n"
msg += ", ".join(nicks)
if msg:
await self.bot.say(msg)
else:
await self.bot.say("That user doesn't have any recorded name or "
"nickname change.")
async def mass_purge(self, messages):
while messages:
if len(messages) > 1:
await self.bot.delete_messages(messages[:100])
messages = messages[100:]
else:
await self.bot.delete_message(messages[0])
messages = []
await asyncio.sleep(1.5)
async def slow_deletion(self, messages):
for message in messages:
try:
await self.bot.delete_message(message)
except:
pass
def is_mod_or_superior(self, message):
user = message.author
server = message.server
admin_role = settings.get_server_admin(server)
mod_role = settings.get_server_mod(server)
if user.id == settings.owner:
return True
elif discord.utils.get(user.roles, name=admin_role):
return True
elif discord.utils.get(user.roles, name=mod_role):
return True
else:
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 responsibility 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
if mod:
self.last_case[server.id][mod.id] = case_n
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)
else:
raise CaseMessageNotFound()
async def check_filter(self, message):
server = message.server
if server.id in self.filter.keys():
for w in self.filter[server.id]:
if w in message.content.lower():
try:
await self.bot.delete_message(message)
logger.info("Message deleted in server {}."
"Filtered: {}"
"".format(server.id, w))
return True
except:
pass
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:
if any([m.attachments for m in msgs]):
return False
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_command(self, command, ctx):
"""Currently used for:
* delete delay"""
server = ctx.message.server
message = ctx.message
try:
delay = self.settings[server.id]["delete_delay"]
except KeyError:
# We have no delay set
return
except AttributeError:
# DM
return
if delay == -1:
return
async def _delete_helper(bot, message):
try:
await bot.delete_message(message)
logger.debug("Deleted command msg {}".format(message.id))
except discord.errors.Forbidden:
# Do not have delete permissions
logger.debug("Wanted to delete mid {} but no"
" permissions".format(message.id))
await asyncio.sleep(delay)
await _delete_helper(self.bot, message)
async def on_message(self, message):
if message.channel.is_private or self.bot.user == message.author \
or not isinstance(message.author, discord.Member):
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):
if before.name != after.name:
if before.id not in self.past_names:
self.past_names[before.id] = [after.name]
else:
if after.name not in self.past_names[before.id]:
names = deque(self.past_names[before.id], maxlen=20)
names.append(after.name)
self.past_names[before.id] = list(names)
dataIO.save_json("data/mod/past_names.json", self.past_names)
if before.nick != after.nick and after.nick is not None:
server = before.server
if server.id not in self.past_nicknames:
self.past_nicknames[server.id] = {}
if before.id in self.past_nicknames[server.id]:
nicks = deque(self.past_nicknames[server.id][before.id],
maxlen=20)
else:
nicks = []
if after.nick not in nicks:
nicks.append(after.nick)
self.past_nicknames[server.id][before.id] = list(nicks)
dataIO.save_json("data/mod/past_nicknames.json",
self.past_nicknames)
def are_overwrites_empty(self, overwrites):
"""There is currently no cleaner way to check if a
PermissionOverwrite object is empty"""
original = [p for p in iter(overwrites)]
empty = [p for p in iter(discord.PermissionOverwrite())]
return original == empty
def check_folders():
folders = ("data", "data/mod/")
for folder in folders:
if not os.path.exists(folder):
print("Creating " + folder + " folder...")
os.makedirs(folder)
def check_files():
ignore_list = {"SERVERS": [], "CHANNELS": []}
files = {
"blacklist.json" : [],
"whitelist.json" : [],
"ignorelist.json" : ignore_list,
"filter.json" : {},
"past_names.json" : {},
"past_nicknames.json" : {},
"settings.json" : {},
"modlog.json" : {},
"perms_cache.json" : {}
}
for filename, value in files.items():
if not os.path.isfile("data/mod/{}".format(filename)):
print("Creating empty {}".format(filename))
dataIO.save_json("data/mod/{}".format(filename), value)
def setup(bot):
global logger
check_folders()
check_files()
logger = logging.getLogger("red.mod")
# Prevents the logger from being loaded again in case of module reload
if logger.level == 0:
logger.setLevel(logging.INFO)
handler = logging.FileHandler(
filename='data/mod/mod.log', encoding='utf-8', mode='a')
handler.setFormatter(
logging.Formatter('%(asctime)s %(message)s', datefmt="[%d/%m/%Y %H:%M]"))
logger.addHandler(handler)
n = Mod(bot)
bot.add_listener(n.check_names, "on_member_update")
bot.add_cog(n)