mirror of
https://github.com/Cog-Creators/Red-DiscordBot.git
synced 2025-11-06 03:08:55 -05:00
* Readd work due to redoing branch * [modlog] Move to core and start work on separating it from cogs * More work on modlog separation * [Core] Finish logic for modlog, do docstrings, async getters * [Core] Add stuff to dunder all * [Docs] Add mod log docs * [Core] Move away from dunder str for Case class * [Docs] don't need to doc special members in modlog docs * More on mod log to implement commands * More work on Mod * [Mod] compatibility with async getters * [Tests] start tests for mod * [Tests] attempted fix * [Tests] mod tests passing now! * [ModLog] update for i18n * modlog.pot -> messages.pot * [Mod] i18n * fix getting admin/mod roles * Fix doc building * [Mod/Modlog] redo imports * [Tests] fix imports in mod tests * [Mod] fix logger problem * [Mod] cleanup errors * A couple of bug fixes Async getters, some old `config.set` syntax * Filter ignores private channels * Fix softban Was still relying on default channels * Actually ignore private channels * Add check for ignored channels * Fix logic for ignore check * Send confirm messages before making case * Pass in guild when setting modlog * Thanks autocomplete * Maintain all data for case * Properly ignore softbans in events * [Mod] bugfixes * [Mod] more changes * [ModLog] timestamp change * [Mod] split filter and cleanup to their own cogs + regen messages.pot * [Cleanup] change logic * [Cleanup] increase limit for channel.history * [Mod] await getter in modset banmentionspam * [Mod] attempt duplicate modlog message fix * [Mod] get_user -> get_user_info * [Modlog] change reason command so the case author can edit their cases (#806) * [Modlog] make reason command guild only * [Modlog] clarify the reason command's help * [Mod] package path changes + numpy style docstrings for modlog * [Mod] change ban and unban events to need view audit log perms to find/create a case * [Modlog] refactoring * [Filter] add autoban feature * [Mod] update case types + event changes * [Mod/Modlog] fix tests, fix permissions things * [Docs] fix up modlog docs * Regenerate messages.pot
226 lines
6.9 KiB
Python
226 lines
6.9 KiB
Python
import asyncio
|
|
import os
|
|
from collections import Counter
|
|
from enum import Enum
|
|
from importlib.machinery import ModuleSpec
|
|
from pathlib import Path
|
|
|
|
import discord
|
|
from discord.ext.commands.bot import BotBase
|
|
from discord.ext.commands import GroupMixin
|
|
|
|
from .cog_manager import CogManager
|
|
from . import Config, i18n, RedContext
|
|
|
|
|
|
class RedBase(BotBase):
|
|
"""Mixin for the main bot class.
|
|
|
|
This exists because `Red` inherits from `discord.AutoShardedClient`, which
|
|
is something other bot classes (namely selfbots) may not want to have as
|
|
a parent class.
|
|
|
|
Selfbots should inherit from this mixin along with `discord.Client`.
|
|
"""
|
|
def __init__(self, cli_flags, bot_dir: Path=Path.cwd(), **kwargs):
|
|
self._shutdown_mode = ExitCodes.CRITICAL
|
|
self.db = Config.get_core_conf(force_registration=True)
|
|
self._co_owners = cli_flags.co_owner
|
|
|
|
self.db.register_global(
|
|
token=None,
|
|
prefix=[],
|
|
packages=[],
|
|
owner=None,
|
|
whitelist=[],
|
|
blacklist=[],
|
|
enable_sentry=None,
|
|
locale='en'
|
|
)
|
|
|
|
self.db.register_guild(
|
|
prefix=[],
|
|
whitelist=[],
|
|
blacklist=[],
|
|
admin_role=None,
|
|
mod_role=None
|
|
)
|
|
|
|
async def prefix_manager(bot, message):
|
|
if not cli_flags.prefix:
|
|
global_prefix = await bot.db.prefix()
|
|
else:
|
|
global_prefix = cli_flags.prefix
|
|
if message.guild is None:
|
|
return global_prefix
|
|
server_prefix = await bot.db.guild(message.guild).prefix()
|
|
return server_prefix if server_prefix else global_prefix
|
|
|
|
if "command_prefix" not in kwargs:
|
|
kwargs["command_prefix"] = prefix_manager
|
|
|
|
if cli_flags.owner and "owner_id" not in kwargs:
|
|
kwargs["owner_id"] = cli_flags.owner
|
|
|
|
if "owner_id" not in kwargs:
|
|
loop = asyncio.get_event_loop()
|
|
loop.run_until_complete(self._dict_abuse(kwargs))
|
|
|
|
self.counter = Counter()
|
|
self.uptime = None
|
|
|
|
self.main_dir = bot_dir
|
|
|
|
self.cog_mgr = CogManager(paths=(str(self.main_dir / 'cogs'),))
|
|
|
|
super().__init__(**kwargs)
|
|
|
|
async def _dict_abuse(self, indict):
|
|
"""
|
|
Please blame <@269933075037814786> for this.
|
|
|
|
:param indict:
|
|
:return:
|
|
"""
|
|
|
|
indict['owner_id'] = await self.db.owner()
|
|
i18n.set_locale(await self.db.locale())
|
|
|
|
async def is_owner(self, user):
|
|
if user.id in self._co_owners:
|
|
return True
|
|
return await super().is_owner(user)
|
|
|
|
async def is_admin(self, member: discord.Member):
|
|
"""Checks if a member is an admin of their guild."""
|
|
admin_role = await self.db.guild(member.guild).admin_role()
|
|
return (not admin_role or
|
|
any(role.id == admin_role for role in member.roles))
|
|
|
|
async def is_mod(self, member: discord.Member):
|
|
"""Checks if a member is a mod or admin of their guild."""
|
|
mod_role = await self.db.guild(member.guild).mod_role()
|
|
admin_role = await self.db.guild(member.guild).admin_role()
|
|
return (not (admin_role or mod_role) or
|
|
any(role.id in (mod_role, admin_role) for role in member.roles))
|
|
|
|
async def send_cmd_help(self, ctx):
|
|
if ctx.invoked_subcommand:
|
|
pages = await self.formatter.format_help_for(ctx, ctx.invoked_subcommand)
|
|
for page in pages:
|
|
await ctx.send(page)
|
|
else:
|
|
pages = await self.formatter.format_help_for(ctx, ctx.command)
|
|
for page in pages:
|
|
await ctx.send(page)
|
|
|
|
async def get_context(self, message, *, cls=RedContext):
|
|
return await super().get_context(message, cls=cls)
|
|
|
|
def list_packages(self):
|
|
"""Lists packages present in the cogs the folder"""
|
|
return os.listdir("cogs")
|
|
|
|
async def save_packages_status(self, packages):
|
|
await self.db.packages.set(packages)
|
|
|
|
async def add_loaded_package(self, pkg_name: str):
|
|
curr_pkgs = await self.db.packages()
|
|
if pkg_name not in curr_pkgs:
|
|
curr_pkgs.append(pkg_name)
|
|
await self.save_packages_status(curr_pkgs)
|
|
|
|
async def remove_loaded_package(self, pkg_name: str):
|
|
curr_pkgs = await self.db.packages()
|
|
if pkg_name in curr_pkgs:
|
|
await self.save_packages_status([p for p in curr_pkgs if p != pkg_name])
|
|
|
|
def load_extension(self, spec: ModuleSpec):
|
|
name = spec.name.split('.')[-1]
|
|
if name in self.extensions:
|
|
return
|
|
|
|
lib = spec.loader.load_module()
|
|
if not hasattr(lib, 'setup'):
|
|
del lib
|
|
raise discord.ClientException('extension does not have a setup function')
|
|
|
|
lib.setup(self)
|
|
self.extensions[name] = lib
|
|
|
|
def unload_extension(self, name):
|
|
lib = self.extensions.get(name)
|
|
if lib is None:
|
|
return
|
|
|
|
lib_name = lib.__name__ # Thank you
|
|
|
|
# find all references to the module
|
|
|
|
# remove the cogs registered from the module
|
|
for cogname, cog in self.cogs.copy().items():
|
|
if cog.__module__.startswith(lib_name):
|
|
self.remove_cog(cogname)
|
|
|
|
# first remove all the commands from the module
|
|
for cmd in self.all_commands.copy().values():
|
|
if cmd.module.startswith(lib_name):
|
|
if isinstance(cmd, GroupMixin):
|
|
cmd.recursively_remove_all_commands()
|
|
self.remove_command(cmd.name)
|
|
|
|
# then remove all the listeners from the module
|
|
for event_list in self.extra_events.copy().values():
|
|
remove = []
|
|
for index, event in enumerate(event_list):
|
|
if event.__module__.startswith(lib_name):
|
|
remove.append(index)
|
|
|
|
for index in reversed(remove):
|
|
del event_list[index]
|
|
|
|
try:
|
|
func = getattr(lib, 'teardown')
|
|
except AttributeError:
|
|
pass
|
|
else:
|
|
try:
|
|
func(self)
|
|
except:
|
|
pass
|
|
finally:
|
|
# finally remove the import..
|
|
del lib
|
|
del self.extensions[name]
|
|
# del sys.modules[name]
|
|
|
|
|
|
class Red(RedBase, discord.AutoShardedClient):
|
|
"""
|
|
You're welcome Caleb.
|
|
"""
|
|
async def shutdown(self, *, restart: bool=False):
|
|
"""Gracefully quit Red.
|
|
|
|
The program will exit with code :code:`0` by default.
|
|
|
|
Parameters
|
|
----------
|
|
restart : bool
|
|
If :code:`True`, the program will exit with code :code:`26`. If the
|
|
launcher sees this, it will attempt to restart the bot.
|
|
|
|
"""
|
|
if not restart:
|
|
self._shutdown_mode = ExitCodes.SHUTDOWN
|
|
else:
|
|
self._shutdown_mode = ExitCodes.RESTART
|
|
|
|
await self.logout()
|
|
|
|
|
|
class ExitCodes(Enum):
|
|
CRITICAL = 1
|
|
SHUTDOWN = 0
|
|
RESTART = 26
|