Initial Template
This commit is contained in:
9
config/emojis.conf
Normal file
9
config/emojis.conf
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
[EMOJIS]
|
||||||
|
|
||||||
|
tick = :✅:
|
||||||
|
clock = :⏱️:
|
||||||
|
warning = :⚠️:
|
||||||
|
config = :⚙️:
|
||||||
|
stats = :📊:
|
||||||
|
utility = :⏱️:
|
||||||
|
cancel = :❌:
|
||||||
@@ -1,10 +1,12 @@
|
|||||||
-- Metadata {{{
|
-- Metadata {{{
|
||||||
CREATE TABLE VersionHistory(
|
CREATE TABLE version_history(
|
||||||
version INTEGER NOT NULL,
|
component TEXT NOT NULL,
|
||||||
time TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP NOT NULL,
|
from_version INTEGER NOT NULL,
|
||||||
author TEXT
|
to_version INTEGER NOT NULL,
|
||||||
|
author TEXT NOT NULL,
|
||||||
|
_timestamp TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||||
);
|
);
|
||||||
INSERT INTO VersionHistory (version, author) VALUES (1, 'Initial Creation');
|
INSERT INTO version_history (component, from_version, to_version, author) VALUES ('ROOT', 0, 1, 'Initial Creation');
|
||||||
|
|
||||||
|
|
||||||
CREATE OR REPLACE FUNCTION update_timestamp_column()
|
CREATE OR REPLACE FUNCTION update_timestamp_column()
|
||||||
@@ -31,76 +33,6 @@ CREATE TABLE bot_config(
|
|||||||
);
|
);
|
||||||
-- }}}
|
-- }}}
|
||||||
|
|
||||||
-- Channel Linker {{{
|
-- TODO: Profile data
|
||||||
|
|
||||||
CREATE TABLE links(
|
|
||||||
linkid SERIAL PRIMARY KEY,
|
|
||||||
name TEXT
|
|
||||||
);
|
|
||||||
|
|
||||||
CREATE TABLE channel_webhooks(
|
|
||||||
channelid BIGINT PRIMARY KEY,
|
|
||||||
webhookid BIGINT NOT NULL,
|
|
||||||
token TEXT NOT NULL
|
|
||||||
);
|
|
||||||
|
|
||||||
CREATE TABLE channel_links(
|
|
||||||
linkid INTEGER NOT NULL REFERENCES links (linkid) ON DELETE CASCADE,
|
|
||||||
channelid BIGINT NOT NULL REFERENCES channel_webhooks (channelid) ON DELETE CASCADE,
|
|
||||||
PRIMARY KEY (linkid, channelid)
|
|
||||||
);
|
|
||||||
|
|
||||||
|
|
||||||
-- }}}
|
|
||||||
|
|
||||||
-- Stream Alerts {{{
|
|
||||||
|
|
||||||
-- DROP TABLE IF EXISTS stream_alerts;
|
|
||||||
-- DROP TABLE IF EXISTS streams;
|
|
||||||
-- DROP TABLE IF EXISTS alert_channels;
|
|
||||||
-- DROP TABLE IF EXISTS streamers;
|
|
||||||
|
|
||||||
CREATE TABLE streamers(
|
|
||||||
userid BIGINT PRIMARY KEY,
|
|
||||||
login_name TEXT NOT NULL,
|
|
||||||
display_name TEXT NOT NULL
|
|
||||||
);
|
|
||||||
|
|
||||||
CREATE TABLE alert_channels(
|
|
||||||
subscriptionid SERIAL PRIMARY KEY,
|
|
||||||
guildid BIGINT NOT NULL,
|
|
||||||
channelid BIGINT NOT NULL,
|
|
||||||
streamerid BIGINT NOT NULL REFERENCES streamers (userid) ON DELETE CASCADE,
|
|
||||||
created_by BIGINT NOT NULL,
|
|
||||||
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
|
||||||
paused BOOLEAN NOT NULL DEFAULT FALSE,
|
|
||||||
end_delete BOOLEAN NOT NULL DEFAULT FALSE,
|
|
||||||
live_message TEXT,
|
|
||||||
end_message TEXT
|
|
||||||
);
|
|
||||||
CREATE INDEX alert_channels_guilds ON alert_channels (guildid);
|
|
||||||
CREATE UNIQUE INDEX alert_channels_channelid_streamerid ON alert_channels (channelid, streamerid);
|
|
||||||
|
|
||||||
CREATE TABLE streams(
|
|
||||||
streamid SERIAL PRIMARY KEY,
|
|
||||||
streamerid BIGINT NOT NULL REFERENCES streamers (userid) ON DELETE CASCADE,
|
|
||||||
start_at TIMESTAMPTZ NOT NULL,
|
|
||||||
twitch_stream_id BIGINT,
|
|
||||||
game_name TEXT,
|
|
||||||
title TEXT,
|
|
||||||
end_at TIMESTAMPTZ
|
|
||||||
);
|
|
||||||
|
|
||||||
CREATE TABLE stream_alerts(
|
|
||||||
alertid SERIAL PRIMARY KEY,
|
|
||||||
streamid INTEGER NOT NULL REFERENCES streams (streamid) ON DELETE CASCADE,
|
|
||||||
subscriptionid INTEGER NOT NULL REFERENCES alert_channels (subscriptionid) ON DELETE CASCADE,
|
|
||||||
sent_at TIMESTAMPTZ NOT NULL,
|
|
||||||
messageid BIGINT NOT NULL,
|
|
||||||
resolved_at TIMESTAMPTZ
|
|
||||||
);
|
|
||||||
|
|
||||||
|
|
||||||
-- }}}
|
|
||||||
|
|
||||||
-- vim: set fdm=marker:
|
-- vim: set fdm=marker:
|
||||||
|
|||||||
@@ -1,8 +1,7 @@
|
|||||||
aiohttp==3.7.4.post0
|
aiohttp
|
||||||
cachetools==4.2.2
|
cachetools
|
||||||
configparser==5.0.2
|
configparser
|
||||||
discord.py [voice]
|
discord.py [voice]
|
||||||
iso8601==0.1.16
|
iso8601
|
||||||
psycopg[pool]
|
psycopg[pool]
|
||||||
pytz==2021.1
|
pytz
|
||||||
twitchAPI
|
|
||||||
|
|||||||
11
src/bot.py
11
src/bot.py
@@ -13,8 +13,6 @@ from meta.monitor import ComponentMonitor, StatusLevel, ComponentStatus
|
|||||||
|
|
||||||
from data import Database
|
from data import Database
|
||||||
|
|
||||||
from constants import DATA_VERSION
|
|
||||||
|
|
||||||
|
|
||||||
for name in conf.config.options('LOGGING_LEVELS', no_defaults=True):
|
for name in conf.config.options('LOGGING_LEVELS', no_defaults=True):
|
||||||
logging.getLogger(name).setLevel(conf.logging_levels[name])
|
logging.getLogger(name).setLevel(conf.logging_levels[name])
|
||||||
@@ -57,15 +55,10 @@ async def main():
|
|||||||
intents.presences = False
|
intents.presences = False
|
||||||
|
|
||||||
async with db.open():
|
async with db.open():
|
||||||
version = await db.version()
|
|
||||||
if version.version != DATA_VERSION:
|
|
||||||
error = f"Data model version is {version}, required version is {DATA_VERSION}! Please migrate."
|
|
||||||
logger.critical(error)
|
|
||||||
raise RuntimeError(error)
|
|
||||||
|
|
||||||
async with aiohttp.ClientSession() as session:
|
async with aiohttp.ClientSession() as session:
|
||||||
async with LionBot(
|
async with LionBot(
|
||||||
command_prefix='!leo!',
|
command_prefix=conf.bot.get('prefix', '!!'),
|
||||||
intents=intents,
|
intents=intents,
|
||||||
appname=appname,
|
appname=appname,
|
||||||
shardname=shardname,
|
shardname=shardname,
|
||||||
@@ -81,7 +74,7 @@ async def main():
|
|||||||
shard_count=sharding.shard_count,
|
shard_count=sharding.shard_count,
|
||||||
help_command=None,
|
help_command=None,
|
||||||
proxy=conf.bot.get('proxy', None),
|
proxy=conf.bot.get('proxy', None),
|
||||||
chunk_guilds_at_startup=False,
|
chunk_guilds_at_startup=True,
|
||||||
) as lionbot:
|
) as lionbot:
|
||||||
ctx_bot.set(lionbot)
|
ctx_bot.set(lionbot)
|
||||||
lionbot.system_monitor.add_component(
|
lionbot.system_monitor.add_component(
|
||||||
|
|||||||
26
src/botdata.py
Normal file
26
src/botdata.py
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
from data import Registry, RowModel, Table
|
||||||
|
from data.columns import String, Timestamp, Integer, Bool
|
||||||
|
|
||||||
|
|
||||||
|
class VersionHistory(RowModel):
|
||||||
|
"""
|
||||||
|
CREATE TABLE version_history(
|
||||||
|
component TEXT NOT NULL,
|
||||||
|
from_version INTEGER NOT NULL,
|
||||||
|
to_version INTEGER NOT NULL,
|
||||||
|
author TEXT NOT NULL,
|
||||||
|
_timestamp TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||||
|
);
|
||||||
|
"""
|
||||||
|
_tablename_ = 'version_history'
|
||||||
|
_cache_ = {}
|
||||||
|
|
||||||
|
component = String()
|
||||||
|
from_version = Integer()
|
||||||
|
to_version = Integer()
|
||||||
|
author = String()
|
||||||
|
_timestamp = Timestamp()
|
||||||
|
|
||||||
|
|
||||||
|
class BotData(Registry):
|
||||||
|
version_history = VersionHistory.table
|
||||||
@@ -1,6 +1,7 @@
|
|||||||
CONFIG_FILE = "config/bot.conf"
|
CONFIG_FILE = "config/bot.conf"
|
||||||
DATA_VERSION = 1
|
|
||||||
|
|
||||||
MAX_COINS = 2147483647 - 1
|
|
||||||
|
|
||||||
HINT_ICON = "https://projects.iamcal.com/emoji-data/img-apple-64/1f4a1.png"
|
HINT_ICON = "https://projects.iamcal.com/emoji-data/img-apple-64/1f4a1.png"
|
||||||
|
|
||||||
|
SCHEMA_VERSIONS = {
|
||||||
|
'ROOT': 1,
|
||||||
|
}
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import logging
|
|||||||
import asyncio
|
import asyncio
|
||||||
from weakref import WeakValueDictionary
|
from weakref import WeakValueDictionary
|
||||||
|
|
||||||
|
from constants import SCHEMA_VERSIONS
|
||||||
import discord
|
import discord
|
||||||
from discord.utils import MISSING
|
from discord.utils import MISSING
|
||||||
from discord.ext.commands import Bot, Cog, HybridCommand, HybridCommandError
|
from discord.ext.commands import Bot, Cog, HybridCommand, HybridCommandError
|
||||||
@@ -13,6 +14,7 @@ from aiohttp import ClientSession
|
|||||||
from data import Database
|
from data import Database
|
||||||
from utils.lib import tabulate
|
from utils.lib import tabulate
|
||||||
from babel.translator import LeoBabel
|
from babel.translator import LeoBabel
|
||||||
|
from botdata import BotData, VersionHistory
|
||||||
|
|
||||||
from .config import Conf
|
from .config import Conf
|
||||||
from .logger import logging_context, log_context, log_action_stack, log_wrap, set_logging_context
|
from .logger import logging_context, log_context, log_action_stack, log_wrap, set_logging_context
|
||||||
@@ -43,6 +45,7 @@ class LionBot(Bot):
|
|||||||
self.appname = appname
|
self.appname = appname
|
||||||
self.shardname = shardname
|
self.shardname = shardname
|
||||||
# self.appdata = appdata
|
# self.appdata = appdata
|
||||||
|
self.data: BotData = db.load_registry(BotData())
|
||||||
self.config = config
|
self.config = config
|
||||||
self.translator = LeoBabel()
|
self.translator = LeoBabel()
|
||||||
|
|
||||||
@@ -53,6 +56,10 @@ class LionBot(Bot):
|
|||||||
self._locks = WeakValueDictionary()
|
self._locks = WeakValueDictionary()
|
||||||
self._running_events = set()
|
self._running_events = set()
|
||||||
|
|
||||||
|
@property
|
||||||
|
def dbconn(self):
|
||||||
|
return self.db
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def core(self):
|
def core(self):
|
||||||
return self.get_cog('CoreCog')
|
return self.get_cog('CoreCog')
|
||||||
@@ -129,6 +136,10 @@ class LionBot(Bot):
|
|||||||
await wrapper()
|
await wrapper()
|
||||||
|
|
||||||
async def start(self, token: str, *, reconnect: bool = True):
|
async def start(self, token: str, *, reconnect: bool = True):
|
||||||
|
await self.data.init()
|
||||||
|
for component, req in SCHEMA_VERSIONS.items():
|
||||||
|
await self.version_check(component, req)
|
||||||
|
|
||||||
with logging_context(action="Login"):
|
with logging_context(action="Login"):
|
||||||
start_task = asyncio.create_task(self.login(token))
|
start_task = asyncio.create_task(self.login(token))
|
||||||
await start_task
|
await start_task
|
||||||
@@ -137,6 +148,24 @@ class LionBot(Bot):
|
|||||||
run_task = asyncio.create_task(self.connect(reconnect=reconnect))
|
run_task = asyncio.create_task(self.connect(reconnect=reconnect))
|
||||||
await run_task
|
await run_task
|
||||||
|
|
||||||
|
async def version_check(self, component: str, req_version: int):
|
||||||
|
# Query the database to confirm that the given component is listed with the given version.
|
||||||
|
# Typically done upon loading a component
|
||||||
|
rows = await VersionHistory.fetch_where(component=component).order_by('_timestamp', ORDER.DESC).limit(1)
|
||||||
|
|
||||||
|
version = rows[0].to_version if rows else 0
|
||||||
|
|
||||||
|
if version != req_version:
|
||||||
|
raise ValueError(f"Component {component} failed version check. Has version '{version}', required version '{req_version}'")
|
||||||
|
else:
|
||||||
|
logger.debug(
|
||||||
|
"Component %s passed version check with version %s",
|
||||||
|
component,
|
||||||
|
version
|
||||||
|
)
|
||||||
|
return True
|
||||||
|
|
||||||
|
|
||||||
def dispatch(self, event_name: str, *args, **kwargs):
|
def dispatch(self, event_name: str, *args, **kwargs):
|
||||||
with logging_context(action=f"Dispatch {event_name}"):
|
with logging_context(action=f"Dispatch {event_name}"):
|
||||||
super().dispatch(event_name, *args, **kwargs)
|
super().dispatch(event_name, *args, **kwargs)
|
||||||
|
|||||||
@@ -1,8 +1,6 @@
|
|||||||
this_package = 'modules'
|
this_package = 'modules'
|
||||||
|
|
||||||
active = [
|
active = [
|
||||||
'.sysadmin',
|
|
||||||
'.voicefix',
|
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +0,0 @@
|
|||||||
|
|
||||||
async def setup(bot):
|
|
||||||
from .exec_cog import Exec
|
|
||||||
|
|
||||||
await bot.add_cog(Exec(bot))
|
|
||||||
@@ -1,336 +0,0 @@
|
|||||||
import io
|
|
||||||
import ast
|
|
||||||
import sys
|
|
||||||
import types
|
|
||||||
import asyncio
|
|
||||||
import traceback
|
|
||||||
import builtins
|
|
||||||
import inspect
|
|
||||||
import logging
|
|
||||||
from io import StringIO
|
|
||||||
|
|
||||||
from typing import Callable, Any, Optional
|
|
||||||
|
|
||||||
from enum import Enum
|
|
||||||
|
|
||||||
import discord
|
|
||||||
from discord.ext import commands
|
|
||||||
from discord.ext.commands.errors import CheckFailure
|
|
||||||
from discord.ui import TextInput, View
|
|
||||||
from discord.ui.button import button
|
|
||||||
import discord.app_commands as appcmd
|
|
||||||
|
|
||||||
from meta.logger import logging_context, log_wrap
|
|
||||||
from meta import conf
|
|
||||||
from meta.context import context, ctx_bot
|
|
||||||
from meta.LionContext import LionContext
|
|
||||||
from meta.LionCog import LionCog
|
|
||||||
from meta.LionBot import LionBot
|
|
||||||
|
|
||||||
from utils.ui import FastModal, input
|
|
||||||
|
|
||||||
from wards import sys_admin
|
|
||||||
|
|
||||||
|
|
||||||
def _(arg): return arg
|
|
||||||
|
|
||||||
def _p(ctx, arg): return arg
|
|
||||||
|
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
|
||||||
|
|
||||||
class ExecModal(FastModal, title="Execute"):
|
|
||||||
code: TextInput = TextInput(
|
|
||||||
label="Code to execute",
|
|
||||||
style=discord.TextStyle.long,
|
|
||||||
required=True
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
class ExecStyle(Enum):
|
|
||||||
EXEC = 'exec'
|
|
||||||
EVAL = 'eval'
|
|
||||||
|
|
||||||
|
|
||||||
class ExecUI(View):
|
|
||||||
def __init__(self, ctx, code=None, style=ExecStyle.EXEC, ephemeral=True) -> None:
|
|
||||||
super().__init__()
|
|
||||||
|
|
||||||
self.ctx: LionContext = ctx
|
|
||||||
self.interaction: Optional[discord.Interaction] = ctx.interaction
|
|
||||||
self.code: Optional[str] = code
|
|
||||||
self.style: ExecStyle = style
|
|
||||||
self.ephemeral: bool = ephemeral
|
|
||||||
|
|
||||||
self._modal: Optional[ExecModal] = None
|
|
||||||
self._msg: Optional[discord.Message] = None
|
|
||||||
|
|
||||||
async def interaction_check(self, interaction: discord.Interaction):
|
|
||||||
"""Only allow the original author to use this View"""
|
|
||||||
if interaction.user.id != self.ctx.author.id:
|
|
||||||
await interaction.response.send_message(
|
|
||||||
("You cannot use this interface!"),
|
|
||||||
ephemeral=True
|
|
||||||
)
|
|
||||||
return False
|
|
||||||
else:
|
|
||||||
return True
|
|
||||||
|
|
||||||
async def run(self):
|
|
||||||
if self.code is None:
|
|
||||||
if (interaction := self.interaction) is not None:
|
|
||||||
self.interaction = None
|
|
||||||
await interaction.response.send_modal(self.get_modal())
|
|
||||||
await self.wait()
|
|
||||||
else:
|
|
||||||
# Complain
|
|
||||||
# TODO: error_reply
|
|
||||||
await self.ctx.reply("Pls give code.")
|
|
||||||
else:
|
|
||||||
await self.interaction.response.defer(thinking=True, ephemeral=self.ephemeral)
|
|
||||||
await self.compile()
|
|
||||||
await self.wait()
|
|
||||||
|
|
||||||
@button(label="Recompile")
|
|
||||||
async def recompile_button(self, interaction, butt):
|
|
||||||
# Interaction response with modal
|
|
||||||
await interaction.response.send_modal(self.get_modal())
|
|
||||||
|
|
||||||
@button(label="Show Source")
|
|
||||||
async def source_button(self, interaction, butt):
|
|
||||||
if len(self.code) > 1900:
|
|
||||||
# Send as file
|
|
||||||
with StringIO(self.code) as fp:
|
|
||||||
fp.seek(0)
|
|
||||||
file = discord.File(fp, filename="source.py")
|
|
||||||
await interaction.response.send_message(file=file, ephemeral=True)
|
|
||||||
else:
|
|
||||||
# Send as message
|
|
||||||
await interaction.response.send_message(
|
|
||||||
content=f"```py\n{self.code}```",
|
|
||||||
ephemeral=True
|
|
||||||
)
|
|
||||||
|
|
||||||
def create_modal(self) -> ExecModal:
|
|
||||||
modal = ExecModal()
|
|
||||||
|
|
||||||
@modal.submit_callback()
|
|
||||||
async def exec_submit(interaction: discord.Interaction):
|
|
||||||
if self.interaction is None:
|
|
||||||
self.interaction = interaction
|
|
||||||
await interaction.response.defer(thinking=True)
|
|
||||||
else:
|
|
||||||
await interaction.response.defer()
|
|
||||||
|
|
||||||
# Set code
|
|
||||||
self.code = modal.code.value
|
|
||||||
|
|
||||||
# Call compile
|
|
||||||
await self.compile()
|
|
||||||
|
|
||||||
return modal
|
|
||||||
|
|
||||||
def get_modal(self):
|
|
||||||
self._modal = self.create_modal()
|
|
||||||
self._modal.code.default = self.code
|
|
||||||
return self._modal
|
|
||||||
|
|
||||||
async def compile(self):
|
|
||||||
# Call _async
|
|
||||||
result = await _async(self.code, style=self.style.value)
|
|
||||||
|
|
||||||
# Display output
|
|
||||||
await self.show_output(result)
|
|
||||||
|
|
||||||
async def show_output(self, output):
|
|
||||||
# Format output
|
|
||||||
# If output message exists and not ephemeral, edit
|
|
||||||
# Otherwise, send message, add buttons
|
|
||||||
if len(output) > 1900:
|
|
||||||
# Send as file
|
|
||||||
with StringIO(output) as fp:
|
|
||||||
fp.seek(0)
|
|
||||||
args = {
|
|
||||||
'content': None,
|
|
||||||
'attachments': [discord.File(fp, filename="output.md")]
|
|
||||||
}
|
|
||||||
else:
|
|
||||||
args = {
|
|
||||||
'content': f"```md\n{output}```",
|
|
||||||
'attachments': []
|
|
||||||
}
|
|
||||||
|
|
||||||
if self._msg is None:
|
|
||||||
if self.interaction is not None:
|
|
||||||
msg = await self.interaction.edit_original_response(**args, view=self)
|
|
||||||
else:
|
|
||||||
# Send new message
|
|
||||||
if args['content'] is None:
|
|
||||||
args['file'] = args.pop('attachments')[0]
|
|
||||||
msg = await self.ctx.reply(**args, ephemeral=self.ephemeral, view=self)
|
|
||||||
|
|
||||||
if not self.ephemeral:
|
|
||||||
self._msg = msg
|
|
||||||
else:
|
|
||||||
if self.interaction is not None:
|
|
||||||
await self.interaction.edit_original_response(**args, view=self)
|
|
||||||
else:
|
|
||||||
# Edit message
|
|
||||||
await self._msg.edit(**args)
|
|
||||||
|
|
||||||
|
|
||||||
def mk_print(fp: io.StringIO) -> Callable[..., None]:
|
|
||||||
def _print(*args, file: Any = fp, **kwargs):
|
|
||||||
return print(*args, file=file, **kwargs)
|
|
||||||
return _print
|
|
||||||
|
|
||||||
|
|
||||||
def mk_status_printer(bot, printer):
|
|
||||||
async def _status(details=False):
|
|
||||||
if details:
|
|
||||||
status = await bot.system_monitor.get_overview()
|
|
||||||
else:
|
|
||||||
status = await bot.system_monitor.get_summary()
|
|
||||||
printer(status)
|
|
||||||
return status
|
|
||||||
return _status
|
|
||||||
|
|
||||||
|
|
||||||
@log_wrap(action="Code Exec")
|
|
||||||
async def _async(to_eval: str, style='exec'):
|
|
||||||
newline = '\n' * ('\n' in to_eval)
|
|
||||||
logger.info(
|
|
||||||
f"Exec code with {style}: {newline}{to_eval}"
|
|
||||||
)
|
|
||||||
|
|
||||||
output = io.StringIO()
|
|
||||||
_print = mk_print(output)
|
|
||||||
|
|
||||||
scope: dict[str, Any] = dict(sys.modules)
|
|
||||||
scope['__builtins__'] = builtins
|
|
||||||
scope.update(builtins.__dict__)
|
|
||||||
scope['ctx'] = ctx = context.get()
|
|
||||||
scope['bot'] = ctx_bot.get()
|
|
||||||
scope['print'] = _print # type: ignore
|
|
||||||
scope['print_status'] = mk_status_printer(scope['bot'], _print)
|
|
||||||
|
|
||||||
try:
|
|
||||||
if ctx and ctx.message:
|
|
||||||
source_str = f"<msg: {ctx.message.id}>"
|
|
||||||
elif ctx and ctx.interaction:
|
|
||||||
source_str = f"<iid: {ctx.interaction.id}>"
|
|
||||||
else:
|
|
||||||
source_str = "Unknown async"
|
|
||||||
|
|
||||||
code = compile(
|
|
||||||
to_eval,
|
|
||||||
source_str,
|
|
||||||
style,
|
|
||||||
ast.PyCF_ALLOW_TOP_LEVEL_AWAIT
|
|
||||||
)
|
|
||||||
func = types.FunctionType(code, scope)
|
|
||||||
|
|
||||||
ret = func()
|
|
||||||
if inspect.iscoroutine(ret):
|
|
||||||
ret = await ret
|
|
||||||
if ret is not None:
|
|
||||||
_print(repr(ret))
|
|
||||||
except Exception:
|
|
||||||
_, exc, tb = sys.exc_info()
|
|
||||||
_print("".join(traceback.format_tb(tb)))
|
|
||||||
_print(f"{type(exc).__name__}: {exc}")
|
|
||||||
|
|
||||||
result = output.getvalue().strip()
|
|
||||||
newline = '\n' * ('\n' in result)
|
|
||||||
logger.info(
|
|
||||||
f"Exec complete, output: {newline}{result}"
|
|
||||||
)
|
|
||||||
return result
|
|
||||||
|
|
||||||
|
|
||||||
class Exec(LionCog):
|
|
||||||
guild_ids = conf.bot.getintlist('admin_guilds')
|
|
||||||
|
|
||||||
def __init__(self, bot: LionBot):
|
|
||||||
self.bot = bot
|
|
||||||
|
|
||||||
async def cog_check(self, ctx: LionContext) -> bool: # type: ignore
|
|
||||||
passed = await sys_admin(ctx.bot, ctx.author.id)
|
|
||||||
if passed:
|
|
||||||
return True
|
|
||||||
else:
|
|
||||||
raise CheckFailure(
|
|
||||||
"You must be a bot owner to do this!"
|
|
||||||
)
|
|
||||||
|
|
||||||
@commands.hybrid_command(
|
|
||||||
name=_('async'),
|
|
||||||
description=_("Execute arbitrary code with Exec")
|
|
||||||
)
|
|
||||||
@appcmd.describe(
|
|
||||||
string="Code to execute.",
|
|
||||||
)
|
|
||||||
async def async_cmd(self, ctx: LionContext,
|
|
||||||
string: Optional[str] = None,
|
|
||||||
):
|
|
||||||
await ExecUI(ctx, string, ExecStyle.EXEC, ephemeral=False).run()
|
|
||||||
|
|
||||||
@commands.hybrid_command(
|
|
||||||
name=_('reload'),
|
|
||||||
description=_("Reload a given LionBot extension. Launches an ExecUI.")
|
|
||||||
)
|
|
||||||
@appcmd.describe(
|
|
||||||
extension=_("Name of the extension to reload. See autocomplete for options."),
|
|
||||||
force=_("Whether to force an extension reload even if it doesn't exist.")
|
|
||||||
)
|
|
||||||
@appcmd.guilds(*guild_ids)
|
|
||||||
async def reload_cmd(self, ctx: LionContext, extension: str, force: Optional[bool] = False):
|
|
||||||
"""
|
|
||||||
This is essentially just a friendly wrapper to reload an extension.
|
|
||||||
It is equivalent to running "await bot.reload_extension(extension)" in eval,
|
|
||||||
with a slightly nicer interface through the autocomplete and error handling.
|
|
||||||
"""
|
|
||||||
exists = (extension in self.bot.extensions)
|
|
||||||
if not (force or exists):
|
|
||||||
embed = discord.Embed(description=f"Unknown extension {extension}", colour=discord.Colour.red())
|
|
||||||
await ctx.reply(embed=embed)
|
|
||||||
else:
|
|
||||||
# Uses an ExecUI to simplify error handling and re-execution
|
|
||||||
if exists:
|
|
||||||
string = f"await bot.reload_extension('{extension}')"
|
|
||||||
else:
|
|
||||||
string = f"await bot.load_extension('{extension}')"
|
|
||||||
await ExecUI(ctx, string, ExecStyle.EVAL).run()
|
|
||||||
|
|
||||||
@reload_cmd.autocomplete('extension')
|
|
||||||
async def reload_extension_acmpl(self, interaction: discord.Interaction, partial: str):
|
|
||||||
keys = set(self.bot.extensions.keys())
|
|
||||||
results = [
|
|
||||||
appcmd.Choice(name=key, value=key)
|
|
||||||
for key in keys
|
|
||||||
if partial.lower() in key.lower()
|
|
||||||
]
|
|
||||||
if not results:
|
|
||||||
results = [
|
|
||||||
appcmd.Choice(name=f"No extensions found matching {partial}", value="None")
|
|
||||||
]
|
|
||||||
return results[:25]
|
|
||||||
|
|
||||||
@commands.hybrid_command(
|
|
||||||
name=_('shutdown'),
|
|
||||||
description=_("Shutdown (or restart) the client.")
|
|
||||||
)
|
|
||||||
@appcmd.guilds(*guild_ids)
|
|
||||||
async def shutdown_cmd(self, ctx: LionContext):
|
|
||||||
"""
|
|
||||||
Shutdown the client.
|
|
||||||
Maybe do something friendly here?
|
|
||||||
"""
|
|
||||||
logger.info("Shutting down on admin request.")
|
|
||||||
await ctx.reply(
|
|
||||||
embed=discord.Embed(
|
|
||||||
description=f"Understood {ctx.author.mention}, cleaning up and shutting down!",
|
|
||||||
colour=discord.Colour.orange()
|
|
||||||
)
|
|
||||||
)
|
|
||||||
await self.bot.close()
|
|
||||||
Submodule src/modules/voicefix deleted from d1297ae986
Reference in New Issue
Block a user