Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add moderation tools #23

Open
wants to merge 20 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -127,3 +127,6 @@ dmypy.json

# Pyre type checker
.pyre/

# db
data/moderation_log.db
3 changes: 3 additions & 0 deletions LICENSE
Original file line number Diff line number Diff line change
Expand Up @@ -19,3 +19,6 @@ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

This notice applies to all files except for cogs/admin.py and cogs/utils/context.py, which are under the
MPL 2.0, which can be found at http://mozilla.org/MPL/2.0/.
50 changes: 38 additions & 12 deletions bot.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@
from pretty_help import PrettyHelp

from dotenv import load_dotenv

from cogs.utils import context
from logger import getLogger, set_global_logging_level
from curation_validator import get_launch_commands_bluebot, validate_curation, CurationType

Expand All @@ -28,10 +30,16 @@
PENDING_FIXES_CHANNEL = int(os.getenv('PENDING_FIXES_CHANNEL'))
NOTIFY_ME_CHANNEL = int(os.getenv('NOTIFY_ME_CHANNEL'))
GOD_USER = int(os.getenv('GOD_USER'))
NOTIFICATION_SQUAD_ID = int(os.getenv('NOTIFICATION_SQUAD_ID'))
BOT_GUY = int(os.getenv('BOT_GUY'))

bot = commands.Bot(command_prefix="-", help_command=PrettyHelp(color=discord.Color.red()))
NOTIFICATION_SQUAD_ID = int(os.getenv('NOTIFICATION_SQUAD_ID'))
TIMEOUT_ID = int(os.getenv('TIMEOUT_ID'))

intents = discord.Intents.default()
intents.members = True
bot = commands.Bot(command_prefix="-",
help_command=PrettyHelp(color=discord.Color.red()),
case_insensitive=False,
intents=intents)
COOL_CRAB = "<:cool_crab:587188729362513930>"
EXTREME_EMOJI_ID = 778145279714918400

Expand All @@ -43,12 +51,16 @@ async def on_ready():

@bot.event
async def on_message(message: discord.Message):
await bot.process_commands(message)
await process_commands(message)
await forward_ping(message)
await notify_me(message)
await check_curation_in_message(message, dry_run=False)


async def process_commands(message):
ctx = await bot.get_context(message, cls=context.Context)
await bot.invoke(ctx)

@bot.event
async def on_command_error(ctx: discord.ext.commands.Context, error: Exception):
if isinstance(error, commands.MaxConcurrencyReached):
Expand All @@ -58,6 +70,16 @@ async def on_command_error(ctx: discord.ext.commands.Context, error: Exception):
await ctx.channel.send("Insufficient permissions.")
return
elif isinstance(error, commands.CommandNotFound):
await ctx.channel.send(f"Command {ctx.invoked_with} not found.")
return
elif isinstance(error, commands.UserInputError):
await ctx.send("Invalid input.")
return
elif isinstance(error, commands.NoPrivateMessage):
try:
await ctx.author.send('This command cannot be used in direct messages.')
except discord.Forbidden:
pass
return
else:
reply_channel: discord.TextChannel = bot.get_channel(BOT_TESTING_CHANNEL)
Expand All @@ -75,14 +97,15 @@ async def forward_ping(message: discord.Message):


async def notify_me(message: discord.Message):
notification_squad = message.guild.get_role(NOTIFICATION_SQUAD_ID)
if message.channel is bot.get_channel(NOTIFY_ME_CHANNEL):
if "unnotify me" in message.content.lower():
l.debug(f"Removed role from {message.author.id}")
await message.author.remove_roles(notification_squad)
elif "notify me" in message.content.lower():
l.debug(f"Gave role to {message.author.id}")
await message.author.add_roles(notification_squad)
if message.guild is not None:
notification_squad = message.guild.get_role(NOTIFICATION_SQUAD_ID)
if message.channel is bot.get_channel(NOTIFY_ME_CHANNEL):
if "unnotify me" in message.content.lower():
l.debug(f"Removed role from {message.author.id}")
await message.author.remove_roles(notification_squad)
elif "notify me" in message.content.lower():
l.debug(f"Gave role to {message.author.id}")
await message.author.add_roles(notification_squad)


async def check_curation_in_message(message: discord.Message, dry_run: bool = True):
Expand Down Expand Up @@ -216,5 +239,8 @@ async def predicate(ctx):
bot.load_extension('cogs.curation')
bot.load_extension('cogs.info')
bot.load_extension('cogs.utilities')
bot.load_extension('cogs.moderation')
bot.load_extension('cogs.admin')

l.info(f"starting the bot...")
bot.run(TOKEN)
142 changes: 142 additions & 0 deletions cogs/admin.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,142 @@
from discord.ext import commands
import asyncio
import importlib
import os
import re
import sys
import subprocess

from discord.utils import get

from bot import BOT_GUY, l

"""This code is subject to the terms of the Mozilla Public
License, v. 2.0. If a copy of the MPL was not distributed with this
file, You can obtain one at http://mozilla.org/MPL/2.0/."""


class Admin(commands.Cog):
"""Admin-only commands that make the bot dynamic."""

def __init__(self, bot):
self.bot = bot
self._last_result = None
self.sessions = set()

async def run_process(self, command):
try:
process = await asyncio.create_subprocess_shell(command, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
result = await process.communicate()
except NotImplementedError:
process = subprocess.Popen(command, shell=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
result = await self.bot.loop.run_in_executor(None, process.communicate)

return [output.decode() for output in result]

async def cog_check(self, ctx: commands.Context):
return ctx.author.id == BOT_GUY or get(ctx.author.roles, name='Administrator')

@commands.command(hidden=True)
async def load(self, ctx, *, module):
"""Loads a module."""
try:
self.bot.load_extension(module)
except commands.ExtensionError as e:
await ctx.send(f'{e.__class__.__name__}: {e}')
else:
await ctx.send('\N{OK HAND SIGN}')

@commands.command(hidden=True)
async def unload(self, ctx, *, module):
"""Unloads a module."""
try:
self.bot.unload_extension(module)
except commands.ExtensionError as e:
await ctx.send(f'{e.__class__.__name__}: {e}')
else:
await ctx.send('\N{OK HAND SIGN}')

@commands.group(name='reload', hidden=True, invoke_without_command=True)
async def _reload(self, ctx, *, module):
l.debug("reload command issued")
"""Reloads a module."""
try:
self.bot.reload_extension(module)
except commands.ExtensionError as e:
await ctx.send(f'{e.__class__.__name__}: {e}')
else:
await ctx.send('\N{OK HAND SIGN}')

_GIT_PULL_REGEX = re.compile(r'\s*(?P<filename>.+?)\s*\|\s*[0-9]+\s*[+-]+')

def find_modules_from_git(self, output):
files = self._GIT_PULL_REGEX.findall(output)
ret = []
for file in files:
root, ext = os.path.splitext(file)
if ext != '.py':
continue

if root.startswith('cogs/'):
# A submodule is a directory inside the main cog directory for
# my purposes
ret.append((root.count('/') - 1, root.replace('/', '.')))

# For reload order, the submodules should be reloaded first
ret.sort(reverse=True)
return ret

def reload_or_load_extension(self, module):
try:
self.bot.reload_extension(module)
except commands.ExtensionNotLoaded:
self.bot.load_extension(module)

@_reload.command(name='all', hidden=True)
async def _reload_all(self, ctx):
"""Reloads all modules, while pulling from git."""

async with ctx.typing():
stdout, stderr = await self.run_process('git pull')

# progress and stuff is redirected to stderr in git pull
# however, things like "fast forward" and files
# along with the text "already up-to-date" are in stdout

if stdout.startswith('Already up to date.'):
return await ctx.send(stdout)

modules = self.find_modules_from_git(stdout)
mods_text = '\n'.join(f'{index}. `{module}`' for index, (_, module) in enumerate(modules, start=1))
prompt_text = f'This will update the following modules, are you sure?\n{mods_text}'
confirm = await ctx.prompt(prompt_text, reacquire=False)
if not confirm:
return await ctx.send('Aborting.')

statuses = []
for is_submodule, module in modules:
if is_submodule:
try:
actual_module = sys.modules[module]
except KeyError:
statuses.append((ctx.tick(None), module))
else:
try:
importlib.reload(actual_module)
except Exception as e:
statuses.append((ctx.tick(False), module))
else:
statuses.append((ctx.tick(True), module))
else:
try:
self.reload_or_load_extension(module)
except commands.ExtensionError:
statuses.append((ctx.tick(False), module))
else:
statuses.append((ctx.tick(True), module))

await ctx.send('\n'.join(f'{status}: `{module}`' for status, module in statuses))


def setup(bot):
bot.add_cog(Admin(bot))
Loading