@@ -12,6 +12,8 @@ ALSO_READ = config/emojis.conf, config/secrets.conf, config/gui.conf
|
|||||||
asset_path = assets
|
asset_path = assets
|
||||||
|
|
||||||
support_guild =
|
support_guild =
|
||||||
|
invite_bot =
|
||||||
|
|
||||||
|
|
||||||
[ENDPOINTS]
|
[ENDPOINTS]
|
||||||
guild_log =
|
guild_log =
|
||||||
@@ -50,3 +52,8 @@ domains = base, wards, schedule, shop, moderation, economy, user_config, config,
|
|||||||
[TEXT_TRACKER]
|
[TEXT_TRACKER]
|
||||||
batchsize = 1
|
batchsize = 1
|
||||||
batchtime = 600
|
batchtime = 600
|
||||||
|
|
||||||
|
[TOPGG]
|
||||||
|
enabled = false
|
||||||
|
route = /dbl
|
||||||
|
port = 5000
|
||||||
|
|||||||
@@ -4,3 +4,6 @@ token =
|
|||||||
[DATA]
|
[DATA]
|
||||||
args = dbname=lion_data
|
args = dbname=lion_data
|
||||||
appid = StudyLion
|
appid = StudyLion
|
||||||
|
|
||||||
|
[TOPGG]
|
||||||
|
auth =
|
||||||
|
|||||||
7
data/migration/v13-v14/migration.sql
Normal file
7
data/migration/v13-v14/migration.sql
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
BEGIN;
|
||||||
|
|
||||||
|
ALTER TABLE bot_config ADD COLUMN sponsor_prompt TEXT;
|
||||||
|
ALTER TABLE bot_config ADD COLUMN sponsor_message TEXT;
|
||||||
|
|
||||||
|
INSERT INTO VersionHistory (version, author) VALUES (14, 'v13-v14 migration');
|
||||||
|
COMMIT;
|
||||||
@@ -4,7 +4,7 @@ CREATE TABLE VersionHistory(
|
|||||||
time TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP NOT NULL,
|
time TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP NOT NULL,
|
||||||
author TEXT
|
author TEXT
|
||||||
);
|
);
|
||||||
INSERT INTO VersionHistory (version, author) VALUES (13, 'Initial Creation');
|
INSERT INTO VersionHistory (version, author) VALUES (14, 'Initial Creation');
|
||||||
|
|
||||||
|
|
||||||
CREATE OR REPLACE FUNCTION update_timestamp_column()
|
CREATE OR REPLACE FUNCTION update_timestamp_column()
|
||||||
@@ -17,17 +17,6 @@ $$ language 'plpgsql';
|
|||||||
-- }}}
|
-- }}}
|
||||||
|
|
||||||
-- App metadata {{{
|
-- App metadata {{{
|
||||||
CREATE TABLE AppData(
|
|
||||||
appid TEXT PRIMARY KEY,
|
|
||||||
last_study_badge_scan TIMESTAMP
|
|
||||||
);
|
|
||||||
|
|
||||||
CREATE TABLE AppConfig(
|
|
||||||
appid TEXT,
|
|
||||||
key TEXT,
|
|
||||||
value TEXT,
|
|
||||||
PRIMARY KEY(appid, key)
|
|
||||||
);
|
|
||||||
|
|
||||||
CREATE TABLE global_user_blacklist(
|
CREATE TABLE global_user_blacklist(
|
||||||
userid BIGINT PRIMARY KEY,
|
userid BIGINT PRIMARY KEY,
|
||||||
@@ -50,6 +39,8 @@ CREATE TABLE app_config(
|
|||||||
|
|
||||||
CREATE TABLE bot_config(
|
CREATE TABLE bot_config(
|
||||||
appname TEXT PRIMARY KEY REFERENCES app_config(appname) ON DELETE CASCADE,
|
appname TEXT PRIMARY KEY REFERENCES app_config(appname) ON DELETE CASCADE,
|
||||||
|
sponsor_prompt TEXT,
|
||||||
|
sponsor_message TEXT,
|
||||||
default_skin TEXT
|
default_skin TEXT
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|||||||
@@ -41,7 +41,7 @@ class BabelCog(LionCog):
|
|||||||
self.bot.core.user_config.register_model_setting(LocaleSettings.UserLocale)
|
self.bot.core.user_config.register_model_setting(LocaleSettings.UserLocale)
|
||||||
|
|
||||||
configcog = self.bot.get_cog('ConfigCog')
|
configcog = self.bot.get_cog('ConfigCog')
|
||||||
self.crossload_group(self.configure_group, configcog.configure_group)
|
self.crossload_group(self.configure_group, configcog.config_group)
|
||||||
|
|
||||||
userconfigcog = self.bot.get_cog('UserConfigCog')
|
userconfigcog = self.bot.get_cog('UserConfigCog')
|
||||||
self.crossload_group(self.userconfig_group, userconfigcog.userconfig_group)
|
self.crossload_group(self.userconfig_group, userconfigcog.userconfig_group)
|
||||||
@@ -114,8 +114,6 @@ class BabelCog(LionCog):
|
|||||||
language=LocaleSettings.GuildLocale._display_name,
|
language=LocaleSettings.GuildLocale._display_name,
|
||||||
force_language=LocaleSettings.ForceLocale._display_name
|
force_language=LocaleSettings.ForceLocale._display_name
|
||||||
)
|
)
|
||||||
@appcmds.guild_only() # Can be removed when attached as a subcommand
|
|
||||||
@appcmds.default_permissions(manage_guild=True)
|
|
||||||
@low_management_ward
|
@low_management_ward
|
||||||
async def cmd_configure_language(self, ctx: LionContext,
|
async def cmd_configure_language(self, ctx: LionContext,
|
||||||
language: Optional[str] = None,
|
language: Optional[str] = None,
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ from settings.groups import SettingGroup
|
|||||||
from meta.errors import UserInputError
|
from meta.errors import UserInputError
|
||||||
from meta.context import ctx_bot
|
from meta.context import ctx_bot
|
||||||
from core.data import CoreData
|
from core.data import CoreData
|
||||||
|
from wards import low_management_iward
|
||||||
|
|
||||||
from .translator import ctx_translator
|
from .translator import ctx_translator
|
||||||
from . import babel
|
from . import babel
|
||||||
@@ -104,9 +105,10 @@ class LocaleSettings(SettingGroup):
|
|||||||
"""
|
"""
|
||||||
Guild configuration for whether to force usage of the guild locale.
|
Guild configuration for whether to force usage of the guild locale.
|
||||||
|
|
||||||
Exposed via `/configure language` command and standard configuration interface.
|
Exposed via `/config language` command and standard configuration interface.
|
||||||
"""
|
"""
|
||||||
setting_id = 'force_locale'
|
setting_id = 'force_locale'
|
||||||
|
_write_ward = low_management_iward
|
||||||
|
|
||||||
_display_name = _p('guildset:force_locale', 'force_language')
|
_display_name = _p('guildset:force_locale', 'force_language')
|
||||||
_desc = _p('guildset:force_locale|desc',
|
_desc = _p('guildset:force_locale|desc',
|
||||||
@@ -144,15 +146,16 @@ class LocaleSettings(SettingGroup):
|
|||||||
def set_str(self):
|
def set_str(self):
|
||||||
bot = ctx_bot.get()
|
bot = ctx_bot.get()
|
||||||
if bot:
|
if bot:
|
||||||
return bot.core.mention_cmd('configure language')
|
return bot.core.mention_cmd('config language')
|
||||||
|
|
||||||
class GuildLocale(ModelData, LocaleSetting):
|
class GuildLocale(ModelData, LocaleSetting):
|
||||||
"""
|
"""
|
||||||
Guild-configured locale.
|
Guild-configured locale.
|
||||||
|
|
||||||
Exposed via `/configure language` command, and standard configuration interface.
|
Exposed via `/config language` command, and standard configuration interface.
|
||||||
"""
|
"""
|
||||||
setting_id = 'guild_locale'
|
setting_id = 'guild_locale'
|
||||||
|
_write_ward = low_management_iward
|
||||||
|
|
||||||
_display_name = _p('guildset:locale', 'language')
|
_display_name = _p('guildset:locale', 'language')
|
||||||
_desc = _p('guildset:locale|desc', "Your preferred language for interacting with me.")
|
_desc = _p('guildset:locale|desc', "Your preferred language for interacting with me.")
|
||||||
@@ -180,4 +183,4 @@ class LocaleSettings(SettingGroup):
|
|||||||
def set_str(self):
|
def set_str(self):
|
||||||
bot = ctx_bot.get()
|
bot = ctx_bot.get()
|
||||||
if bot:
|
if bot:
|
||||||
return bot.core.mention_cmd('configure language')
|
return bot.core.mention_cmd('config language')
|
||||||
|
|||||||
@@ -29,6 +29,7 @@ class LocaleSettingUI(ConfigUI):
|
|||||||
async def force_button(self, press: discord.Interaction, pressed: Button):
|
async def force_button(self, press: discord.Interaction, pressed: Button):
|
||||||
await press.response.defer()
|
await press.response.defer()
|
||||||
setting = next(inst for inst in self.instances if inst.setting_id == LocaleSettings.ForceLocale.setting_id)
|
setting = next(inst for inst in self.instances if inst.setting_id == LocaleSettings.ForceLocale.setting_id)
|
||||||
|
await setting.interaction_check(self.guildid, press)
|
||||||
setting.value = not setting.value
|
setting.value = not setting.value
|
||||||
await setting.write()
|
await setting.write()
|
||||||
|
|
||||||
@@ -80,7 +81,7 @@ class LocaleSettingUI(ConfigUI):
|
|||||||
class LocaleDashboard(DashboardSection):
|
class LocaleDashboard(DashboardSection):
|
||||||
section_name = _p(
|
section_name = _p(
|
||||||
'dash:locale|title',
|
'dash:locale|title',
|
||||||
"Server Language Configuration ({commands[configure language]})"
|
"Server Language Configuration ({commands[config language]})"
|
||||||
)
|
)
|
||||||
_option_name = _p(
|
_option_name = _p(
|
||||||
"dash:locale|dropdown|placeholder",
|
"dash:locale|dropdown|placeholder",
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
CONFIG_FILE = "config/bot.conf"
|
CONFIG_FILE = "config/bot.conf"
|
||||||
DATA_VERSION = 13
|
DATA_VERSION = 14
|
||||||
|
|
||||||
MAX_COINS = 2147483647 - 1
|
MAX_COINS = 2147483647 - 1
|
||||||
|
|
||||||
|
|||||||
@@ -25,12 +25,35 @@ class ConfigCog(LionCog):
|
|||||||
...
|
...
|
||||||
|
|
||||||
@cmds.hybrid_group(
|
@cmds.hybrid_group(
|
||||||
name=_p('group:configure', "configure"),
|
name=_p('group:config', "config"),
|
||||||
description=_p('group:configure|desc', "View and adjust my configuration options."),
|
description=_p('group:config|desc', "View and adjust moderation-level configuration."),
|
||||||
)
|
)
|
||||||
@appcmds.guild_only
|
@appcmds.guild_only
|
||||||
@appcmds.default_permissions(manage_guild=True)
|
@appcmds.default_permissions(manage_guild=True)
|
||||||
async def configure_group(self, ctx: LionContext):
|
async def config_group(self, ctx: LionContext):
|
||||||
|
"""
|
||||||
|
Bare command group, has no function.
|
||||||
|
"""
|
||||||
|
return
|
||||||
|
|
||||||
|
@cmds.hybrid_group(
|
||||||
|
name=_p('group:admin', "admin"),
|
||||||
|
description=_p('group:admin|desc', "Administrative commands."),
|
||||||
|
)
|
||||||
|
@appcmds.guild_only
|
||||||
|
@appcmds.default_permissions(administrator=True)
|
||||||
|
async def admin_group(self, ctx: LionContext):
|
||||||
|
"""
|
||||||
|
Bare command group, has no function.
|
||||||
|
"""
|
||||||
|
return
|
||||||
|
|
||||||
|
@admin_group.group(
|
||||||
|
name=_p('group:admin_config', "config"),
|
||||||
|
description=_p('group:admin_config|desc', "View and adjust admin-level configuration."),
|
||||||
|
)
|
||||||
|
@appcmds.guild_only
|
||||||
|
async def admin_config_group(self, ctx: LionContext):
|
||||||
"""
|
"""
|
||||||
Bare command group, has no function.
|
Bare command group, has no function.
|
||||||
"""
|
"""
|
||||||
|
|||||||
@@ -47,6 +47,8 @@ class CoreData(Registry, name="core"):
|
|||||||
------
|
------
|
||||||
CREATE TABLE bot_config(
|
CREATE TABLE bot_config(
|
||||||
appname TEXT PRIMARY KEY REFERENCES app_config(appname) ON DELETE CASCADE,
|
appname TEXT PRIMARY KEY REFERENCES app_config(appname) ON DELETE CASCADE,
|
||||||
|
sponsor_prompt TEXT,
|
||||||
|
sponsor_message TEXT,
|
||||||
default_skin TEXT
|
default_skin TEXT
|
||||||
);
|
);
|
||||||
"""
|
"""
|
||||||
@@ -54,6 +56,8 @@ class CoreData(Registry, name="core"):
|
|||||||
|
|
||||||
appname = String(primary=True)
|
appname = String(primary=True)
|
||||||
default_skin = String()
|
default_skin = String()
|
||||||
|
sponsor_prompt = String()
|
||||||
|
sponsor_message = String()
|
||||||
|
|
||||||
class Shard(RowModel):
|
class Shard(RowModel):
|
||||||
"""
|
"""
|
||||||
|
|||||||
@@ -18,6 +18,8 @@ active = [
|
|||||||
'.moderation',
|
'.moderation',
|
||||||
'.video_channels',
|
'.video_channels',
|
||||||
'.meta',
|
'.meta',
|
||||||
|
'.sponsors',
|
||||||
|
'.topgg',
|
||||||
'.test',
|
'.test',
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|||||||
@@ -29,14 +29,14 @@ class GuildConfigCog(LionCog):
|
|||||||
configcog = self.bot.get_cog('ConfigCog')
|
configcog = self.bot.get_cog('ConfigCog')
|
||||||
if configcog is None:
|
if configcog is None:
|
||||||
raise ValueError("Cannot load GuildConfigCog without ConfigCog")
|
raise ValueError("Cannot load GuildConfigCog without ConfigCog")
|
||||||
self.crossload_group(self.configure_group, configcog.configure_group)
|
self.crossload_group(self.configure_group, configcog.config_group)
|
||||||
|
|
||||||
@cmds.hybrid_command(
|
@cmds.hybrid_command(
|
||||||
name="dashboard",
|
name="dashboard",
|
||||||
description="At-a-glance view of the server's configuration."
|
description="At-a-glance view of the server's configuration."
|
||||||
)
|
)
|
||||||
@appcmds.guild_only
|
@appcmds.guild_only
|
||||||
@appcmds.default_permissions(manage_guild=True)
|
@low_management_ward
|
||||||
async def dashboard_cmd(self, ctx: LionContext):
|
async def dashboard_cmd(self, ctx: LionContext):
|
||||||
if not ctx.guild or not ctx.interaction:
|
if not ctx.guild or not ctx.interaction:
|
||||||
return
|
return
|
||||||
@@ -64,8 +64,6 @@ class GuildConfigCog(LionCog):
|
|||||||
timezone=GeneralSettings.Timezone._desc,
|
timezone=GeneralSettings.Timezone._desc,
|
||||||
event_log=GeneralSettings.EventLog._desc,
|
event_log=GeneralSettings.EventLog._desc,
|
||||||
)
|
)
|
||||||
@appcmds.guild_only()
|
|
||||||
@appcmds.default_permissions(manage_guild=True)
|
|
||||||
@low_management_ward
|
@low_management_ward
|
||||||
async def cmd_configure_general(self, ctx: LionContext,
|
async def cmd_configure_general(self, ctx: LionContext,
|
||||||
timezone: Optional[str] = None,
|
timezone: Optional[str] = None,
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ from meta.context import ctx_bot
|
|||||||
from meta.errors import UserInputError
|
from meta.errors import UserInputError
|
||||||
from core.data import CoreData
|
from core.data import CoreData
|
||||||
from babel.translator import ctx_translator
|
from babel.translator import ctx_translator
|
||||||
|
from wards import low_management_iward
|
||||||
|
|
||||||
from . import babel
|
from . import babel
|
||||||
|
|
||||||
@@ -20,13 +21,14 @@ class GeneralSettings(SettingGroup):
|
|||||||
"""
|
"""
|
||||||
Guild timezone configuration.
|
Guild timezone configuration.
|
||||||
|
|
||||||
Exposed via `/configure general timezone:`, and the standard interface.
|
Exposed via `/config general timezone:`, and the standard interface.
|
||||||
The `timezone` setting acts as the default timezone for all members,
|
The `timezone` setting acts as the default timezone for all members,
|
||||||
and the timezone used to display guild-wide statistics.
|
and the timezone used to display guild-wide statistics.
|
||||||
"""
|
"""
|
||||||
setting_id = 'timezone'
|
setting_id = 'timezone'
|
||||||
_event = 'guildset_timezone'
|
_event = 'guildset_timezone'
|
||||||
_set_cmd = 'configure general'
|
_set_cmd = 'config general'
|
||||||
|
_write_ward = low_management_iward
|
||||||
|
|
||||||
_display_name = _p('guildset:timezone', "timezone")
|
_display_name = _p('guildset:timezone', "timezone")
|
||||||
_desc = _p(
|
_desc = _p(
|
||||||
@@ -58,7 +60,8 @@ class GeneralSettings(SettingGroup):
|
|||||||
"""
|
"""
|
||||||
setting_id = 'eventlog'
|
setting_id = 'eventlog'
|
||||||
_event = 'guildset_eventlog'
|
_event = 'guildset_eventlog'
|
||||||
_set_cmd = 'configure general'
|
_set_cmd = 'config general'
|
||||||
|
_write_ward = low_management_iward
|
||||||
|
|
||||||
_display_name = _p('guildset:eventlog', "event_log")
|
_display_name = _p('guildset:eventlog', "event_log")
|
||||||
_desc = _p(
|
_desc = _p(
|
||||||
|
|||||||
@@ -41,6 +41,7 @@ class GeneralSettingUI(ConfigUI):
|
|||||||
await selection.response.defer(thinking=True, ephemeral=True)
|
await selection.response.defer(thinking=True, ephemeral=True)
|
||||||
|
|
||||||
setting = self.get_instance(GeneralSettings.EventLog)
|
setting = self.get_instance(GeneralSettings.EventLog)
|
||||||
|
await setting.interaction_check(setting.parent_id, selection)
|
||||||
|
|
||||||
value = selected.values[0].resolve() if selected.values else None
|
value = selected.values[0].resolve() if selected.values else None
|
||||||
setting = await setting.from_value(self.guildid, value)
|
setting = await setting.from_value(self.guildid, value)
|
||||||
@@ -95,7 +96,7 @@ class GeneralSettingUI(ConfigUI):
|
|||||||
class GeneralDashboard(DashboardSection):
|
class GeneralDashboard(DashboardSection):
|
||||||
section_name = _p(
|
section_name = _p(
|
||||||
"dash:general|title",
|
"dash:general|title",
|
||||||
"General Configuration ({commands[configure general]})"
|
"General Configuration ({commands[config general]})"
|
||||||
)
|
)
|
||||||
_option_name = _p(
|
_option_name = _p(
|
||||||
"dash:general|option|name",
|
"dash:general|option|name",
|
||||||
|
|||||||
@@ -64,7 +64,7 @@ class Economy(LionCog):
|
|||||||
"Attempting to load the EconomyCog before ConfigCog! Failed to crossload configuration group."
|
"Attempting to load the EconomyCog before ConfigCog! Failed to crossload configuration group."
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
self.crossload_group(self.configure_group, configcog.configure_group)
|
self.crossload_group(self.configure_group, configcog.config_group)
|
||||||
|
|
||||||
# ----- Economy Bonus registration -----
|
# ----- Economy Bonus registration -----
|
||||||
def register_economy_bonus(self, bonus_coro, name=None):
|
def register_economy_bonus(self, bonus_coro, name=None):
|
||||||
@@ -903,7 +903,6 @@ class Economy(LionCog):
|
|||||||
appcmds.Choice(name=EconomySettings.AllowTransfers._outputs[False], value=0),
|
appcmds.Choice(name=EconomySettings.AllowTransfers._outputs[False], value=0),
|
||||||
]
|
]
|
||||||
)
|
)
|
||||||
@appcmds.default_permissions(manage_guild=True)
|
|
||||||
@moderator_ward
|
@moderator_ward
|
||||||
async def configure_economy(self, ctx: LionContext,
|
async def configure_economy(self, ctx: LionContext,
|
||||||
allow_transfers: Optional[appcmds.Choice[int]] = None,
|
allow_transfers: Optional[appcmds.Choice[int]] = None,
|
||||||
|
|||||||
@@ -17,6 +17,7 @@ from meta.logger import log_wrap
|
|||||||
from core.data import CoreData
|
from core.data import CoreData
|
||||||
from core.setting_types import CoinSetting
|
from core.setting_types import CoinSetting
|
||||||
from babel.translator import ctx_translator
|
from babel.translator import ctx_translator
|
||||||
|
from wards import low_management_iward
|
||||||
|
|
||||||
from . import babel, logger
|
from . import babel, logger
|
||||||
from .data import EconomyData
|
from .data import EconomyData
|
||||||
@@ -32,6 +33,7 @@ class EconomySettings(SettingGroup):
|
|||||||
"""
|
"""
|
||||||
class CoinsPerXP(ModelData, CoinSetting):
|
class CoinsPerXP(ModelData, CoinSetting):
|
||||||
setting_id = 'coins_per_xp'
|
setting_id = 'coins_per_xp'
|
||||||
|
_write_ward = low_management_iward
|
||||||
|
|
||||||
_display_name = _p('guildset:coins_per_xp', "coins_per_100xp")
|
_display_name = _p('guildset:coins_per_xp', "coins_per_100xp")
|
||||||
_desc = _p(
|
_desc = _p(
|
||||||
@@ -63,10 +65,11 @@ class EconomySettings(SettingGroup):
|
|||||||
@property
|
@property
|
||||||
def set_str(self):
|
def set_str(self):
|
||||||
bot = ctx_bot.get()
|
bot = ctx_bot.get()
|
||||||
return bot.core.mention_cmd('configure economy') if bot else None
|
return bot.core.mention_cmd('config economy') if bot else None
|
||||||
|
|
||||||
class AllowTransfers(ModelData, BoolSetting):
|
class AllowTransfers(ModelData, BoolSetting):
|
||||||
setting_id = 'allow_transfers'
|
setting_id = 'allow_transfers'
|
||||||
|
_write_ward = low_management_iward
|
||||||
|
|
||||||
_display_name = _p('guildset:allow_transfers', "allow_transfers")
|
_display_name = _p('guildset:allow_transfers', "allow_transfers")
|
||||||
_desc = _p(
|
_desc = _p(
|
||||||
@@ -91,7 +94,7 @@ class EconomySettings(SettingGroup):
|
|||||||
@property
|
@property
|
||||||
def set_str(self):
|
def set_str(self):
|
||||||
bot = ctx_bot.get()
|
bot = ctx_bot.get()
|
||||||
return bot.core.mention_cmd('configure economy') if bot else None
|
return bot.core.mention_cmd('config economy') if bot else None
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def update_message(self):
|
def update_message(self):
|
||||||
@@ -115,6 +118,7 @@ class EconomySettings(SettingGroup):
|
|||||||
|
|
||||||
class StartingFunds(ModelData, CoinSetting):
|
class StartingFunds(ModelData, CoinSetting):
|
||||||
setting_id = 'starting_funds'
|
setting_id = 'starting_funds'
|
||||||
|
_write_ward = low_management_iward
|
||||||
|
|
||||||
_display_name = _p('guildset:starting_funds', "starting_funds")
|
_display_name = _p('guildset:starting_funds', "starting_funds")
|
||||||
_desc = _p(
|
_desc = _p(
|
||||||
|
|||||||
@@ -64,7 +64,7 @@ class EconomyConfigUI(ConfigUI):
|
|||||||
class EconomyDashboard(DashboardSection):
|
class EconomyDashboard(DashboardSection):
|
||||||
section_name = _p(
|
section_name = _p(
|
||||||
'dash:economy|title',
|
'dash:economy|title',
|
||||||
"Economy Configuration ({commands[configure economy]})"
|
"Economy Configuration ({commands[config economy]})"
|
||||||
)
|
)
|
||||||
_option_name = _p(
|
_option_name = _p(
|
||||||
"dash:economy|dropdown|placeholder",
|
"dash:economy|dropdown|placeholder",
|
||||||
|
|||||||
@@ -1,15 +1,23 @@
|
|||||||
|
from io import StringIO
|
||||||
from typing import Optional
|
from typing import Optional
|
||||||
import asyncio
|
import asyncio
|
||||||
|
|
||||||
import discord
|
import discord
|
||||||
from discord.ext import commands as cmds
|
from discord.ext import commands as cmds
|
||||||
|
from discord.enums import AppCommandOptionType
|
||||||
from discord import app_commands as appcmds
|
from discord import app_commands as appcmds
|
||||||
|
from psycopg import sql
|
||||||
|
from data.queries import NULLS, ORDER
|
||||||
|
|
||||||
from meta import LionCog, LionBot, LionContext
|
from meta import LionCog, LionBot, LionContext
|
||||||
from meta.logger import log_wrap
|
from meta.logger import log_wrap
|
||||||
from meta.sharding import THIS_SHARD
|
from meta.sharding import THIS_SHARD
|
||||||
|
from meta.errors import UserInputError, SafeCancellation
|
||||||
from babel.translator import ctx_locale
|
from babel.translator import ctx_locale
|
||||||
from utils.lib import utc_now
|
from utils.lib import utc_now, parse_time_static, write_records
|
||||||
|
from utils.ui import ChoicedEnum, Transformed
|
||||||
|
from utils.ratelimits import Bucket, BucketFull, BucketOverFull
|
||||||
|
from data import RawExpr, NULL
|
||||||
|
|
||||||
from wards import low_management_ward, equippable_role, high_management_ward
|
from wards import low_management_ward, equippable_role, high_management_ward
|
||||||
|
|
||||||
@@ -21,6 +29,24 @@ from .settingui import MemberAdminUI
|
|||||||
_p = babel._p
|
_p = babel._p
|
||||||
|
|
||||||
|
|
||||||
|
class DownloadableData(ChoicedEnum):
|
||||||
|
VOICE_LEADERBOARD = _p('cmd:admin_data|param:data_type|choice:voice_leaderboard', "Voice Leaderboard")
|
||||||
|
MSG_LEADERBOARD = _p('cmd:admin_data|param:data_type|choice:msg_leaderboard', "Message Leaderboard")
|
||||||
|
XP_LEADERBOARD = _p('cmd:admin_data|param:data_type|choice:xp_leaderboard', "XP Leaderboard")
|
||||||
|
ROLEMENU_EQUIP = _p('cmd:admin_data|param:data_type|choice:rolemenu_equip', "Rolemenu Roles Equipped")
|
||||||
|
TRANSACTIONS = _p('cmd:admin_data|param:data_type|choice:transactions', "Economy Transactions (Incomplete)")
|
||||||
|
BALANCES = _p('cmd:admin_data|param:data_type|choice:balances', "Economy Balances")
|
||||||
|
VOICE_SESSIONS = _p('cmd:admin_data|param:data_type|choice:voice_sessions', "Voice Sessions")
|
||||||
|
|
||||||
|
@property
|
||||||
|
def choice_name(self):
|
||||||
|
return self.value
|
||||||
|
|
||||||
|
|
||||||
|
@property
|
||||||
|
def choice_value(self):
|
||||||
|
return self.name
|
||||||
|
|
||||||
class MemberAdminCog(LionCog):
|
class MemberAdminCog(LionCog):
|
||||||
def __init__(self, bot: LionBot):
|
def __init__(self, bot: LionBot):
|
||||||
self.bot = bot
|
self.bot = bot
|
||||||
@@ -31,6 +57,9 @@ class MemberAdminCog(LionCog):
|
|||||||
# Set of (guildid, userid) that are currently being added
|
# Set of (guildid, userid) that are currently being added
|
||||||
self._adding_roles = set()
|
self._adding_roles = set()
|
||||||
|
|
||||||
|
# Map of guildid -> Bucket
|
||||||
|
self._data_request_buckets: dict[int, Bucket] = {}
|
||||||
|
|
||||||
# ----- Initialisation -----
|
# ----- Initialisation -----
|
||||||
async def cog_load(self):
|
async def cog_load(self):
|
||||||
await self.data.init()
|
await self.data.init()
|
||||||
@@ -46,7 +75,8 @@ class MemberAdminCog(LionCog):
|
|||||||
"Configuration command cannot be crossloaded."
|
"Configuration command cannot be crossloaded."
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
self.crossload_group(self.configure_group, configcog.configure_group)
|
self.crossload_group(self.configure_group, configcog.config_group)
|
||||||
|
self.crossload_group(self.admin_group, configcog.admin_group)
|
||||||
|
|
||||||
# ----- Cog API -----
|
# ----- Cog API -----
|
||||||
async def absent_remove_role(self, guildid, userid, roleid):
|
async def absent_remove_role(self, guildid, userid, roleid):
|
||||||
@@ -55,6 +85,12 @@ class MemberAdminCog(LionCog):
|
|||||||
"""
|
"""
|
||||||
return await self.data.past_roles.delete_where(guildid=guildid, userid=userid, roleid=roleid)
|
return await self.data.past_roles.delete_where(guildid=guildid, userid=userid, roleid=roleid)
|
||||||
|
|
||||||
|
def data_bucket_req(self, guildid: int):
|
||||||
|
bucket = self._data_request_buckets.get(guildid, None)
|
||||||
|
if bucket is None:
|
||||||
|
bucket = self._data_request_buckets[guildid] = Bucket(10, 10)
|
||||||
|
bucket.request()
|
||||||
|
|
||||||
# ----- Event Handlers -----
|
# ----- Event Handlers -----
|
||||||
@LionCog.listener('on_member_join')
|
@LionCog.listener('on_member_join')
|
||||||
@log_wrap(action="Greetings")
|
@log_wrap(action="Greetings")
|
||||||
@@ -320,7 +356,15 @@ class MemberAdminCog(LionCog):
|
|||||||
)
|
)
|
||||||
|
|
||||||
# ----- Cog Commands -----
|
# ----- Cog Commands -----
|
||||||
@cmds.hybrid_command(
|
@LionCog.placeholder_group
|
||||||
|
@cmds.hybrid_group('admin', with_app_command=False)
|
||||||
|
async def admin_group(self, ctx: LionContext):
|
||||||
|
"""
|
||||||
|
Substitute configure command group.
|
||||||
|
"""
|
||||||
|
pass
|
||||||
|
|
||||||
|
@admin_group.command(
|
||||||
name=_p('cmd:resetmember', "resetmember"),
|
name=_p('cmd:resetmember', "resetmember"),
|
||||||
description=_p(
|
description=_p(
|
||||||
'cmd:resetmember|desc',
|
'cmd:resetmember|desc',
|
||||||
@@ -342,7 +386,6 @@ class MemberAdminCog(LionCog):
|
|||||||
),
|
),
|
||||||
)
|
)
|
||||||
@high_management_ward
|
@high_management_ward
|
||||||
@appcmds.default_permissions(administrator=True)
|
|
||||||
async def cmd_resetmember(self, ctx: LionContext,
|
async def cmd_resetmember(self, ctx: LionContext,
|
||||||
target: discord.User,
|
target: discord.User,
|
||||||
saved_roles: Optional[bool] = False,
|
saved_roles: Optional[bool] = False,
|
||||||
@@ -378,6 +421,214 @@ class MemberAdminCog(LionCog):
|
|||||||
ephemeral=True
|
ephemeral=True
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@admin_group.command(
|
||||||
|
name=_p('cmd:admin_data', "data"),
|
||||||
|
description=_p(
|
||||||
|
'cmd:admin_data|desc',
|
||||||
|
"Download various raw data for external analysis and backup."
|
||||||
|
)
|
||||||
|
)
|
||||||
|
@appcmds.rename(
|
||||||
|
data_type=_p('cmd:admin_data|param:data_type', "type"),
|
||||||
|
target=_p('cmd:admin_data|param:target', "target"),
|
||||||
|
start=_p('cmd:admin_data|param:start', "after"),
|
||||||
|
end=_p('cmd:admin_data|param:end', "before"),
|
||||||
|
limit=_p('cmd:admin_data|param:limit', "limit"),
|
||||||
|
)
|
||||||
|
@appcmds.describe(
|
||||||
|
data_type=_p(
|
||||||
|
'cmd:admin_data|param:data_type|desc',
|
||||||
|
"Select the type of data you want to download"
|
||||||
|
),
|
||||||
|
target=_p(
|
||||||
|
'cmd:admin_data|param:target|desc',
|
||||||
|
"Filter the data by selecting a user or role"
|
||||||
|
),
|
||||||
|
start=_p(
|
||||||
|
'cmd:admin_data|param:start|desc',
|
||||||
|
"Retrieve records created after this date and time in server timezone (YYYY-MM-DD HH:MM)"
|
||||||
|
),
|
||||||
|
end=_p(
|
||||||
|
'cmd:admin_data|param:end|desc',
|
||||||
|
"Retrieve records created before this date and time in server timezone (YYYY-MM-DD HH:MM)"
|
||||||
|
),
|
||||||
|
limit=_p(
|
||||||
|
'cmd:admin_data|param:limit|desc',
|
||||||
|
"Maximum number of records to retrieve."
|
||||||
|
)
|
||||||
|
)
|
||||||
|
@high_management_ward
|
||||||
|
async def cmd_data(self, ctx: LionContext,
|
||||||
|
data_type: Transformed[DownloadableData, AppCommandOptionType.string],
|
||||||
|
target: Optional[discord.User | discord.Member | discord.Role] = None,
|
||||||
|
start: Optional[str] = None,
|
||||||
|
end: Optional[str] = None,
|
||||||
|
limit: appcmds.Range[int, 1, 100000] = 1000,
|
||||||
|
):
|
||||||
|
if not ctx.guild:
|
||||||
|
return
|
||||||
|
if not ctx.interaction:
|
||||||
|
return
|
||||||
|
|
||||||
|
t = self.bot.translator.t
|
||||||
|
|
||||||
|
# Parse arguments
|
||||||
|
|
||||||
|
userids: Optional[list[int]] = None
|
||||||
|
if target is None:
|
||||||
|
# All guild members
|
||||||
|
userids = None
|
||||||
|
elif isinstance(target, discord.Role):
|
||||||
|
# Members of the given role
|
||||||
|
userids = [member.id for member in target.members]
|
||||||
|
else:
|
||||||
|
# target is a user or member
|
||||||
|
userids = [target.id]
|
||||||
|
|
||||||
|
if start:
|
||||||
|
start_time = await parse_time_static(start, ctx.lguild.timezone)
|
||||||
|
else:
|
||||||
|
start_time = ctx.guild.created_at
|
||||||
|
|
||||||
|
if end:
|
||||||
|
end_time = await parse_time_static(end, ctx.lguild.timezone)
|
||||||
|
else:
|
||||||
|
end_time = utc_now()
|
||||||
|
|
||||||
|
# Form query
|
||||||
|
if data_type is DownloadableData.VOICE_LEADERBOARD:
|
||||||
|
query = self.bot.core.data.Member.table.select_where()
|
||||||
|
query.select(
|
||||||
|
'guildid',
|
||||||
|
'userid',
|
||||||
|
total_time=RawExpr(
|
||||||
|
sql.SQL("study_time_between(guildid, userid, %s, %s)"),
|
||||||
|
(start_time, end_time)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
query.order_by('total_time', ORDER.DESC, NULLS.LAST)
|
||||||
|
elif data_type is DownloadableData.MSG_LEADERBOARD:
|
||||||
|
from tracking.text.data import TextTrackerData as Data
|
||||||
|
|
||||||
|
query = Data.TextSessions.table.select_where()
|
||||||
|
query.select(
|
||||||
|
'guildid',
|
||||||
|
'userid',
|
||||||
|
total_messages="SUM(messages)"
|
||||||
|
)
|
||||||
|
query.where(
|
||||||
|
Data.TextSessions.start_time >= start_time,
|
||||||
|
Data.TextSessions.start_time < end_time,
|
||||||
|
)
|
||||||
|
query.group_by('guildid', 'userid')
|
||||||
|
query.order_by('total_messages', ORDER.DESC, NULLS.LAST)
|
||||||
|
elif data_type is DownloadableData.XP_LEADERBOARD:
|
||||||
|
from modules.statistics.data import StatsData as Data
|
||||||
|
|
||||||
|
query = Data.MemberExp.table.select_where()
|
||||||
|
query.select(
|
||||||
|
'guildid',
|
||||||
|
'userid',
|
||||||
|
total_xp="SUM(amount)"
|
||||||
|
)
|
||||||
|
query.where(
|
||||||
|
Data.MemberExp.earned_at >= start_time,
|
||||||
|
Data.MemberExp.earned_at < end_time,
|
||||||
|
)
|
||||||
|
query.group_by('guildid', 'userid')
|
||||||
|
query.order_by('total_xp', ORDER.DESC, NULLS.LAST)
|
||||||
|
elif data_type is DownloadableData.ROLEMENU_EQUIP:
|
||||||
|
from modules.rolemenus.data import RoleMenuData as Data
|
||||||
|
|
||||||
|
query = Data.RoleMenuHistory.table.select_where().leftjoin('role_menus', using=('menuid',))
|
||||||
|
query.select(
|
||||||
|
guildid=Data.RoleMenu.guildid,
|
||||||
|
userid=Data.RoleMenuHistory.userid,
|
||||||
|
menuid=Data.RoleMenu.menuid,
|
||||||
|
menu_messageid=Data.RoleMenu.messageid,
|
||||||
|
menu_name=Data.RoleMenu.name,
|
||||||
|
equipid=Data.RoleMenuHistory.equipid,
|
||||||
|
roleid=Data.RoleMenuHistory.roleid,
|
||||||
|
obtained_at=Data.RoleMenuHistory.obtained_at,
|
||||||
|
expires_at=Data.RoleMenuHistory.expires_at,
|
||||||
|
removed_at=Data.RoleMenuHistory.removed_at,
|
||||||
|
transactionid=Data.RoleMenuHistory.transactionid,
|
||||||
|
)
|
||||||
|
query.where(
|
||||||
|
Data.RoleMenuHistory.obtained_at >= start_time,
|
||||||
|
Data.RoleMenuHistory.obtained_at < end_time,
|
||||||
|
)
|
||||||
|
query.order_by(Data.RoleMenuHistory.obtained_at, ORDER.DESC)
|
||||||
|
elif data_type is DownloadableData.TRANSACTIONS:
|
||||||
|
raise SafeCancellation("Transaction data is not yet available")
|
||||||
|
elif data_type is DownloadableData.BALANCES:
|
||||||
|
raise SafeCancellation("Member balance data is not yet available")
|
||||||
|
elif data_type is DownloadableData.VOICE_SESSIONS:
|
||||||
|
raise SafeCancellation("Raw voice session data is not yet available")
|
||||||
|
else:
|
||||||
|
raise ValueError(f"Unknown data type requested {data_type}")
|
||||||
|
|
||||||
|
query.where(guildid=ctx.guild.id)
|
||||||
|
if userids:
|
||||||
|
query.where(userid=userids)
|
||||||
|
query.limit(limit)
|
||||||
|
query.with_no_adapter()
|
||||||
|
|
||||||
|
# Request bucket
|
||||||
|
try:
|
||||||
|
self.data_bucket_req(ctx.guild.id)
|
||||||
|
except BucketOverFull:
|
||||||
|
# Don't do anything, even respond to the interaction
|
||||||
|
raise SafeCancellation()
|
||||||
|
except BucketFull:
|
||||||
|
raise SafeCancellation(t(_p(
|
||||||
|
'cmd:admin_data|error:ratelimited',
|
||||||
|
"Too many requests! Please wait a few minutes before using this command again."
|
||||||
|
)))
|
||||||
|
|
||||||
|
# Run query
|
||||||
|
await ctx.interaction.response.defer(thinking=True)
|
||||||
|
results = await query
|
||||||
|
|
||||||
|
if results:
|
||||||
|
with StringIO() as stream:
|
||||||
|
write_records(results, stream)
|
||||||
|
stream.seek(0)
|
||||||
|
file = discord.File(stream, filename='data.csv')
|
||||||
|
await ctx.reply(file=file)
|
||||||
|
else:
|
||||||
|
await ctx.error_reply(
|
||||||
|
t(_p(
|
||||||
|
'cmd:admin_data|error:no_results',
|
||||||
|
"Your query had no results! Try relaxing your filters."
|
||||||
|
))
|
||||||
|
)
|
||||||
|
|
||||||
|
@cmd_data.autocomplete('start')
|
||||||
|
@cmd_data.autocomplete('end')
|
||||||
|
async def cmd_data_acmpl_time(self, interaction: discord.Interaction, partial: str):
|
||||||
|
if not interaction.guild:
|
||||||
|
return []
|
||||||
|
|
||||||
|
lguild = await self.bot.core.lions.fetch_guild(interaction.guild.id)
|
||||||
|
timezone = lguild.timezone
|
||||||
|
|
||||||
|
t = self.bot.translator.t
|
||||||
|
try:
|
||||||
|
timestamp = await parse_time_static(partial, timezone)
|
||||||
|
choice = appcmds.Choice(
|
||||||
|
name=timestamp.strftime('%Y-%m-%d %H:%M'),
|
||||||
|
value=partial
|
||||||
|
)
|
||||||
|
except UserInputError:
|
||||||
|
choice = appcmds.Choice(
|
||||||
|
name=t(_p(
|
||||||
|
'cmd:admin_data|acmpl:time|error:parse',
|
||||||
|
"Cannot parse \"{partial}\" as a time. Try the format YYYY-MM-DD HH:MM"
|
||||||
|
)).format(partial=partial)[:100],
|
||||||
|
value=partial
|
||||||
|
)
|
||||||
|
return [choice]
|
||||||
|
|
||||||
# ----- Config Commands -----
|
# ----- Config Commands -----
|
||||||
@LionCog.placeholder_group
|
@LionCog.placeholder_group
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ from settings import ListData, ModelData
|
|||||||
from settings.groups import SettingGroup
|
from settings.groups import SettingGroup
|
||||||
from settings.setting_types import BoolSetting, ChannelSetting, RoleListSetting
|
from settings.setting_types import BoolSetting, ChannelSetting, RoleListSetting
|
||||||
from utils.lib import recurse_map, replace_multiple, tabulate
|
from utils.lib import recurse_map, replace_multiple, tabulate
|
||||||
|
from wards import low_management_iward, high_management_iward
|
||||||
|
|
||||||
from . import babel
|
from . import babel
|
||||||
from .data import MemberAdminData
|
from .data import MemberAdminData
|
||||||
@@ -36,6 +37,7 @@ _greeting_subkey_desc = {
|
|||||||
class MemberAdminSettings(SettingGroup):
|
class MemberAdminSettings(SettingGroup):
|
||||||
class GreetingChannel(ModelData, ChannelSetting):
|
class GreetingChannel(ModelData, ChannelSetting):
|
||||||
setting_id = 'greeting_channel'
|
setting_id = 'greeting_channel'
|
||||||
|
_write_ward = low_management_iward
|
||||||
|
|
||||||
_display_name = _p('guildset:greeting_channel', "welcome_channel")
|
_display_name = _p('guildset:greeting_channel', "welcome_channel")
|
||||||
_desc = _p(
|
_desc = _p(
|
||||||
@@ -87,6 +89,7 @@ class MemberAdminSettings(SettingGroup):
|
|||||||
|
|
||||||
class GreetingMessage(ModelData, MessageSetting):
|
class GreetingMessage(ModelData, MessageSetting):
|
||||||
setting_id = 'greeting_message'
|
setting_id = 'greeting_message'
|
||||||
|
_write_ward = low_management_iward
|
||||||
|
|
||||||
_display_name = _p(
|
_display_name = _p(
|
||||||
'guildset:greeting_message', "welcome_message"
|
'guildset:greeting_message', "welcome_message"
|
||||||
@@ -209,6 +212,7 @@ class MemberAdminSettings(SettingGroup):
|
|||||||
|
|
||||||
class ReturningMessage(ModelData, MessageSetting):
|
class ReturningMessage(ModelData, MessageSetting):
|
||||||
setting_id = 'returning_message'
|
setting_id = 'returning_message'
|
||||||
|
_write_ward = low_management_iward
|
||||||
|
|
||||||
_display_name = _p(
|
_display_name = _p(
|
||||||
'guildset:returning_message', "returning_message"
|
'guildset:returning_message', "returning_message"
|
||||||
@@ -335,6 +339,7 @@ class MemberAdminSettings(SettingGroup):
|
|||||||
|
|
||||||
class Autoroles(ListData, RoleListSetting):
|
class Autoroles(ListData, RoleListSetting):
|
||||||
setting_id = 'autoroles'
|
setting_id = 'autoroles'
|
||||||
|
_write_ward = high_management_iward
|
||||||
|
|
||||||
_display_name = _p(
|
_display_name = _p(
|
||||||
'guildset:autoroles', "autoroles"
|
'guildset:autoroles', "autoroles"
|
||||||
@@ -357,6 +362,7 @@ class MemberAdminSettings(SettingGroup):
|
|||||||
|
|
||||||
class BotAutoroles(ListData, RoleListSetting):
|
class BotAutoroles(ListData, RoleListSetting):
|
||||||
setting_id = 'bot_autoroles'
|
setting_id = 'bot_autoroles'
|
||||||
|
_write_ward = high_management_iward
|
||||||
|
|
||||||
_display_name = _p(
|
_display_name = _p(
|
||||||
'guildset:bot_autoroles', "bot_autoroles"
|
'guildset:bot_autoroles', "bot_autoroles"
|
||||||
@@ -379,6 +385,7 @@ class MemberAdminSettings(SettingGroup):
|
|||||||
class RolePersistence(ModelData, BoolSetting):
|
class RolePersistence(ModelData, BoolSetting):
|
||||||
setting_id = 'role_persistence'
|
setting_id = 'role_persistence'
|
||||||
_event = 'guildset_role_persistence'
|
_event = 'guildset_role_persistence'
|
||||||
|
_write_ward = low_management_iward
|
||||||
|
|
||||||
_display_name = _p('guildset:role_persistence', "role_persistence")
|
_display_name = _p('guildset:role_persistence', "role_persistence")
|
||||||
_desc = _p(
|
_desc = _p(
|
||||||
|
|||||||
@@ -45,6 +45,7 @@ class MemberAdminUI(ConfigUI):
|
|||||||
"""
|
"""
|
||||||
await selection.response.defer(thinking=True, ephemeral=True)
|
await selection.response.defer(thinking=True, ephemeral=True)
|
||||||
setting = self.get_instance(Settings.GreetingChannel)
|
setting = self.get_instance(Settings.GreetingChannel)
|
||||||
|
await setting.interaction_check(setting.parent_id, selection)
|
||||||
setting.value = selected.values[0] if selected.values else None
|
setting.value = selected.values[0] if selected.values else None
|
||||||
await setting.write()
|
await setting.write()
|
||||||
await selection.delete_original_response()
|
await selection.delete_original_response()
|
||||||
@@ -73,6 +74,7 @@ class MemberAdminUI(ConfigUI):
|
|||||||
await equippable_role(self.bot, role, selection.user)
|
await equippable_role(self.bot, role, selection.user)
|
||||||
|
|
||||||
setting = self.get_instance(Settings.Autoroles)
|
setting = self.get_instance(Settings.Autoroles)
|
||||||
|
await setting.interaction_check(setting.parent_id, selection)
|
||||||
setting.value = selected.values
|
setting.value = selected.values
|
||||||
await setting.write()
|
await setting.write()
|
||||||
# Instance hooks will update the menu
|
# Instance hooks will update the menu
|
||||||
@@ -102,6 +104,7 @@ class MemberAdminUI(ConfigUI):
|
|||||||
await equippable_role(self.bot, role, selection.user)
|
await equippable_role(self.bot, role, selection.user)
|
||||||
|
|
||||||
setting = self.get_instance(Settings.BotAutoroles)
|
setting = self.get_instance(Settings.BotAutoroles)
|
||||||
|
await setting.interaction_check(setting.parent_id, selection)
|
||||||
setting.value = selected.values
|
setting.value = selected.values
|
||||||
await setting.write()
|
await setting.write()
|
||||||
# Instance hooks will update the menu
|
# Instance hooks will update the menu
|
||||||
@@ -131,6 +134,7 @@ class MemberAdminUI(ConfigUI):
|
|||||||
await press.response.defer(thinking=True, ephemeral=True)
|
await press.response.defer(thinking=True, ephemeral=True)
|
||||||
t = self.bot.translator.t
|
t = self.bot.translator.t
|
||||||
setting = self.get_instance(Settings.GreetingMessage)
|
setting = self.get_instance(Settings.GreetingMessage)
|
||||||
|
await setting.interaction_check(setting.parent_id, press)
|
||||||
|
|
||||||
value = setting.value
|
value = setting.value
|
||||||
if value is None:
|
if value is None:
|
||||||
@@ -173,6 +177,7 @@ class MemberAdminUI(ConfigUI):
|
|||||||
await press.response.defer(thinking=True, ephemeral=True)
|
await press.response.defer(thinking=True, ephemeral=True)
|
||||||
t = self.bot.translator.t
|
t = self.bot.translator.t
|
||||||
setting = self.get_instance(Settings.ReturningMessage)
|
setting = self.get_instance(Settings.ReturningMessage)
|
||||||
|
await setting.interaction_check(setting.parent_id, press)
|
||||||
greeting = self.get_instance(Settings.GreetingMessage)
|
greeting = self.get_instance(Settings.GreetingMessage)
|
||||||
|
|
||||||
value = setting.value
|
value = setting.value
|
||||||
@@ -254,7 +259,7 @@ class MemberAdminUI(ConfigUI):
|
|||||||
class MemberAdminDashboard(DashboardSection):
|
class MemberAdminDashboard(DashboardSection):
|
||||||
section_name = _p(
|
section_name = _p(
|
||||||
"dash:member_admin|title",
|
"dash:member_admin|title",
|
||||||
"Greetings and Initial Roles ({commands[configure welcome]})"
|
"Greetings and Initial Roles ({commands[config welcome]})"
|
||||||
)
|
)
|
||||||
_option_name = _p(
|
_option_name = _p(
|
||||||
"dash:member_admin|dropdown|placeholder",
|
"dash:member_admin|dropdown|placeholder",
|
||||||
@@ -278,7 +283,7 @@ class MemberAdminDashboard(DashboardSection):
|
|||||||
page.add_field(
|
page.add_field(
|
||||||
name=t(_p(
|
name=t(_p(
|
||||||
'dash:member_admin|section:greeting_messages|name',
|
'dash:member_admin|section:greeting_messages|name',
|
||||||
"Greeting Messages ({commands[configure welcome]})"
|
"Greeting Messages ({commands[admin config welcome]})"
|
||||||
)).format(commands=self.bot.core.mention_cache),
|
)).format(commands=self.bot.core.mention_cache),
|
||||||
value=table,
|
value=table,
|
||||||
inline=False
|
inline=False
|
||||||
@@ -289,7 +294,7 @@ class MemberAdminDashboard(DashboardSection):
|
|||||||
page.add_field(
|
page.add_field(
|
||||||
name=t(_p(
|
name=t(_p(
|
||||||
'dash:member_admin|section:initial_roles|name',
|
'dash:member_admin|section:initial_roles|name',
|
||||||
"Initial Roles ({commands[configure welcome]})"
|
"Initial Roles ({commands[admin config welcome]})"
|
||||||
)).format(commands=self.bot.core.mention_cache),
|
)).format(commands=self.bot.core.mention_cache),
|
||||||
value=table,
|
value=table,
|
||||||
inline=False
|
inline=False
|
||||||
|
|||||||
@@ -1,19 +1,36 @@
|
|||||||
from typing import Optional
|
from typing import Optional
|
||||||
|
import gc
|
||||||
|
import sys
|
||||||
import asyncio
|
import asyncio
|
||||||
|
import logging
|
||||||
|
|
||||||
import discord
|
import discord
|
||||||
from discord.ext import commands as cmds
|
from discord.ext import commands as cmds
|
||||||
from discord import app_commands as appcmds
|
from discord import app_commands as appcmds
|
||||||
|
from data.queries import ORDER
|
||||||
|
from utils.lib import tabulate
|
||||||
|
|
||||||
from wards import low_management
|
from wards import low_management
|
||||||
from meta import LionBot, LionCog, LionContext
|
from meta import LionBot, LionCog, LionContext
|
||||||
|
from data import Table
|
||||||
from utils.ui import AButton, AsComponents
|
from utils.ui import AButton, AsComponents
|
||||||
|
from utils.lib import utc_now
|
||||||
|
|
||||||
from . import babel
|
from . import babel
|
||||||
from .helpui import HelpUI
|
from .helpui import HelpUI
|
||||||
|
|
||||||
_p = babel._p
|
_p = babel._p
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
created = utc_now()
|
||||||
|
guide_link = "https://discord.studylions.com/tutorial"
|
||||||
|
|
||||||
|
animation_link = (
|
||||||
|
"https://media.discordapp.net/attachments/879412267731542047/926837189814419486/ezgif.com-resize.gif"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
class MetaCog(LionCog):
|
class MetaCog(LionCog):
|
||||||
def __init__(self, bot: LionBot):
|
def __init__(self, bot: LionBot):
|
||||||
@@ -27,6 +44,8 @@ class MetaCog(LionCog):
|
|||||||
)
|
)
|
||||||
)
|
)
|
||||||
async def help_cmd(self, ctx: LionContext):
|
async def help_cmd(self, ctx: LionContext):
|
||||||
|
if not ctx.interaction:
|
||||||
|
return
|
||||||
await ctx.interaction.response.defer(thinking=True, ephemeral=True)
|
await ctx.interaction.response.defer(thinking=True, ephemeral=True)
|
||||||
ui = HelpUI(
|
ui = HelpUI(
|
||||||
ctx.bot,
|
ctx.bot,
|
||||||
@@ -35,3 +54,342 @@ class MetaCog(LionCog):
|
|||||||
show_admin=await low_management(ctx.bot, ctx.author, ctx.guild),
|
show_admin=await low_management(ctx.bot, ctx.author, ctx.guild),
|
||||||
)
|
)
|
||||||
await ui.run(ctx.interaction)
|
await ui.run(ctx.interaction)
|
||||||
|
|
||||||
|
@LionCog.listener('on_guild_join')
|
||||||
|
async def post_join_message(self, guild: discord.Guild):
|
||||||
|
logger.debug(f"Sending join message to <gid: {guild.id}>")
|
||||||
|
# Send join message
|
||||||
|
t = self.bot.translator.t
|
||||||
|
message = t(_p(
|
||||||
|
'new_guild_join_message|desc',
|
||||||
|
"Thank you for inviting me to your community!\n"
|
||||||
|
"Get started by typing {help_cmd} to see my commands,"
|
||||||
|
" and {dash_cmd} to view and set up my configuration options!\n\n"
|
||||||
|
"If you need any help configuring me,"
|
||||||
|
" or would like to suggest a feature,"
|
||||||
|
" report a bug, and stay updated,"
|
||||||
|
" make sure to join our main support server by [clicking here]({support})."
|
||||||
|
)).format(
|
||||||
|
dash_cmd=self.bot.core.mention_cmd('dashboard'),
|
||||||
|
help_cmd=self.bot.core.mention_cmd('help'),
|
||||||
|
support=self.bot.config.bot.support_guild,
|
||||||
|
)
|
||||||
|
try:
|
||||||
|
await guild.me.edit(nick="Leo")
|
||||||
|
except discord.HTTPException:
|
||||||
|
pass
|
||||||
|
if (channel := guild.system_channel) and channel.permissions_for(guild.me).embed_links:
|
||||||
|
embed = discord.Embed(
|
||||||
|
description=message,
|
||||||
|
colour=discord.Colour.orange(),
|
||||||
|
)
|
||||||
|
embed.set_author(
|
||||||
|
name=t(_p(
|
||||||
|
'new_guild_join_message|name',
|
||||||
|
"Hello everyone! My name is Leo, the LionBot!"
|
||||||
|
)),
|
||||||
|
icon_url="https://cdn.discordapp.com/emojis/933610591459872868.webp"
|
||||||
|
)
|
||||||
|
embed.set_image(url=animation_link)
|
||||||
|
|
||||||
|
try:
|
||||||
|
await channel.send(embed=embed)
|
||||||
|
except discord.HTTPException:
|
||||||
|
logger.warning(
|
||||||
|
f"Could not send join message to <gid: {guild.id}>",
|
||||||
|
exc_info=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
@cmds.hybrid_command(
|
||||||
|
name=_p('cmd:invite', "invite"),
|
||||||
|
description=_p(
|
||||||
|
'cmd:invite|desc',
|
||||||
|
"Invite LionBot to your own server."
|
||||||
|
)
|
||||||
|
)
|
||||||
|
async def invite_cmd(self, ctx: LionContext):
|
||||||
|
t = self.bot.translator.t
|
||||||
|
|
||||||
|
embed = discord.Embed(
|
||||||
|
colour=discord.Colour.orange(),
|
||||||
|
description=t(_p(
|
||||||
|
'cmd:invite|embed|desc',
|
||||||
|
"[Click here]({invite_link}) to add me to your server."
|
||||||
|
)).format(
|
||||||
|
invite_link=self.bot.config.bot.invite_bot,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
embed.add_field(
|
||||||
|
name=t(_p(
|
||||||
|
'cmd:invite|embed|field:tips|name',
|
||||||
|
"Setup Tips"
|
||||||
|
)),
|
||||||
|
value=t(_p(
|
||||||
|
'cmd:invite|embed|field:tips|value',
|
||||||
|
"Remember to check out {help_cmd} for the important command list,"
|
||||||
|
" including the admin page which displays the hidden admin-level"
|
||||||
|
" configuration commands like {dashboard}!\n"
|
||||||
|
"Also, if you have any issues or questions,"
|
||||||
|
" you can join our [support server]({support_link}) to talk to our friendly"
|
||||||
|
" support team!"
|
||||||
|
)).format(
|
||||||
|
help_cmd=self.bot.core.mention_cmd('help'),
|
||||||
|
dashboard=self.bot.core.mention_cmd('dashboard'),
|
||||||
|
support_link=self.bot.config.bot.support_guild,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
await ctx.reply(embed=embed, ephemeral=True)
|
||||||
|
|
||||||
|
@cmds.hybrid_command(
|
||||||
|
name=_p('cmd:support', "support"),
|
||||||
|
description=_p(
|
||||||
|
'cmd:support|desc',
|
||||||
|
"Have an issue or a question? Speak to my friendly support team here."
|
||||||
|
)
|
||||||
|
)
|
||||||
|
async def support_cmd(self, ctx: LionContext):
|
||||||
|
t = self.bot.translator.t
|
||||||
|
await ctx.reply(
|
||||||
|
t(_p(
|
||||||
|
'cmd:support|response',
|
||||||
|
"Speak to my friendly support team by joining this server and making a ticket"
|
||||||
|
" in the support channel!\n"
|
||||||
|
"{support_link}"
|
||||||
|
)).format(support_link=self.bot.config.bot.support_guild),
|
||||||
|
ephemeral=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
@cmds.hybrid_command(
|
||||||
|
name=_p('cmd:nerd', "nerd"),
|
||||||
|
description=_p(
|
||||||
|
'cmd:nerd|desc',
|
||||||
|
"View hidden details and statistics about me ('nerd statistics')",
|
||||||
|
)
|
||||||
|
)
|
||||||
|
async def nerd_cmd(self, ctx: LionContext):
|
||||||
|
t = self.bot.translator.t
|
||||||
|
|
||||||
|
if ctx.interaction:
|
||||||
|
await ctx.interaction.response.defer(thinking=True)
|
||||||
|
|
||||||
|
embed = discord.Embed(
|
||||||
|
colour=discord.Colour.orange(),
|
||||||
|
title=t(_p(
|
||||||
|
'cmd:nerd|title',
|
||||||
|
"Nerd Statistics"
|
||||||
|
)),
|
||||||
|
)
|
||||||
|
if ctx.guild:
|
||||||
|
embed.set_footer(
|
||||||
|
text=f"Your guildid: {ctx.guild.id}"
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
embed.set_footer(
|
||||||
|
text="Sent from direct message"
|
||||||
|
)
|
||||||
|
|
||||||
|
# Bot Stats
|
||||||
|
bot_stats_lines = []
|
||||||
|
|
||||||
|
# Currently {n} people active in {m} rooms of {n} guilds
|
||||||
|
query = await Table('voice_sessions_ongoing').bind(self.bot.db).select_one_where(
|
||||||
|
).select(
|
||||||
|
total_users='COUNT(userid)',
|
||||||
|
total_rooms='COUNT(channelid)',
|
||||||
|
total_guilds='COUNT(guildid)',
|
||||||
|
)
|
||||||
|
bot_stats_lines.append((
|
||||||
|
t(_p('cmd:nerd|field:currently|name', "Currently")),
|
||||||
|
t(_p(
|
||||||
|
'cmd:nerd|field:currently|value',
|
||||||
|
"`{people}` people active in `{rooms}` rooms of `{guilds}` guilds."
|
||||||
|
)).format(
|
||||||
|
people=query['total_users'],
|
||||||
|
rooms=query['total_rooms'],
|
||||||
|
guilds=query['total_guilds']
|
||||||
|
)
|
||||||
|
))
|
||||||
|
|
||||||
|
# Recorded {h} voice hours from {n} people across {n} sessions
|
||||||
|
query = await Table('voice_sessions').bind(self.bot.db).select_one_where(
|
||||||
|
).select(
|
||||||
|
total_hours='SUM(duration) / 3600',
|
||||||
|
total_users='COUNT(userid)',
|
||||||
|
total_sessions='COUNT(*)',
|
||||||
|
)
|
||||||
|
bot_stats_lines.append((
|
||||||
|
t(_p('cmd:nerd|field:recorded|name', "Recorded")),
|
||||||
|
t(_p(
|
||||||
|
'cmd:nerd|field:recorded|value',
|
||||||
|
"`{hours}` voice hours from `{users}` people across `{sessions}` sessions."
|
||||||
|
)).format(
|
||||||
|
hours=query['total_hours'],
|
||||||
|
users=query['total_users'],
|
||||||
|
sessions=query['total_sessions'],
|
||||||
|
)
|
||||||
|
))
|
||||||
|
|
||||||
|
# Registered {n} users and {m} guilds
|
||||||
|
query1 = await Table('user_config').bind(self.bot.db).select_one_where(
|
||||||
|
).select(total_users='COUNT(*)')
|
||||||
|
query2 = await Table('guild_config').bind(self.bot.db).select_one_where(
|
||||||
|
).select(total_guilds='COUNT(*)')
|
||||||
|
bot_stats_lines.append((
|
||||||
|
t(_p('cmd:nerd|field:registered|name', "Registered")),
|
||||||
|
t(_p(
|
||||||
|
'cmd:nerd|field:registered|value',
|
||||||
|
"`{users}` users and `{guilds}` guilds."
|
||||||
|
)).format(
|
||||||
|
users=query1['total_users'],
|
||||||
|
guilds=query2['total_guilds'],
|
||||||
|
)
|
||||||
|
))
|
||||||
|
|
||||||
|
# {n} tasks completed out of {m}
|
||||||
|
query = await Table('tasklist').bind(self.bot.db).select_one_where(
|
||||||
|
).select(
|
||||||
|
total_tasks='COUNT(*)',
|
||||||
|
total_completed='COUNT(*) filter (WHERE completed_at IS NOT NULL)',
|
||||||
|
)
|
||||||
|
bot_stats_lines.append((
|
||||||
|
t(_p('cmd:nerd|field:tasks|name', "Tasks")),
|
||||||
|
t(_p(
|
||||||
|
'cmd:nerd|field:tasks|value',
|
||||||
|
"`{tasks}` tasks completed out of `{total}`."
|
||||||
|
)).format(
|
||||||
|
tasks=query['total_completed'], total=query['total_tasks']
|
||||||
|
)
|
||||||
|
))
|
||||||
|
|
||||||
|
# {m} timers running across {n} guilds
|
||||||
|
query = await Table('timers').bind(self.bot.db).select_one_where(
|
||||||
|
).select(
|
||||||
|
total_timers='COUNT(*)',
|
||||||
|
guilds='COUNT(guildid)'
|
||||||
|
)
|
||||||
|
bot_stats_lines.append((
|
||||||
|
t(_p('cmd:nerd|field:timers|name', "Timers")),
|
||||||
|
t(_p(
|
||||||
|
'cmd:nerd|field:timers|value',
|
||||||
|
"`{timers}` timers running across `{guilds}` guilds."
|
||||||
|
)).format(
|
||||||
|
timers=query['total_timers'],
|
||||||
|
guilds=query['guilds'],
|
||||||
|
)
|
||||||
|
))
|
||||||
|
|
||||||
|
bot_stats_section = '\n'.join(tabulate(*bot_stats_lines))
|
||||||
|
embed.add_field(
|
||||||
|
name=t(_p('cmd:nerd|section:bot_stats|name', "Bot Stats")),
|
||||||
|
value=bot_stats_section,
|
||||||
|
inline=False,
|
||||||
|
)
|
||||||
|
|
||||||
|
# ----- Process -----
|
||||||
|
process_lines = []
|
||||||
|
|
||||||
|
# Shard {n} of {n}
|
||||||
|
process_lines.append((
|
||||||
|
t(_p('cmd:nerd|field:shard|name', "Shard")),
|
||||||
|
t(_p(
|
||||||
|
'cmd:nerd|field:shard|value',
|
||||||
|
"`{shard_number}` of `{shard_count}`"
|
||||||
|
)).format(shard_number=self.bot.shard_id, shard_count=self.bot.shard_count)
|
||||||
|
))
|
||||||
|
|
||||||
|
# Guilds
|
||||||
|
process_lines.append((
|
||||||
|
t(_p('cmd:nerd|field:guilds|name', "Guilds")),
|
||||||
|
t(_p(
|
||||||
|
'cmd:nerd|field:guilds|value',
|
||||||
|
"`{guilds}` guilds with `{count}` total members."
|
||||||
|
)).format(
|
||||||
|
guilds=len(self.bot.guilds),
|
||||||
|
count=sum(guild.member_count or 0 for guild in self.bot.guilds)
|
||||||
|
)
|
||||||
|
))
|
||||||
|
|
||||||
|
# Version
|
||||||
|
version = await self.bot.db.version()
|
||||||
|
process_lines.append((
|
||||||
|
t(_p('cmd:nerd|field:version|name', "Leo Version")),
|
||||||
|
t(_p(
|
||||||
|
'cmd:nerd|field:version|value',
|
||||||
|
"`v{version}`, last updated {timestamp} from `{reason}`."
|
||||||
|
)).format(
|
||||||
|
version=version.version,
|
||||||
|
timestamp=discord.utils.format_dt(version.time, 'D'),
|
||||||
|
reason=version.author,
|
||||||
|
)
|
||||||
|
))
|
||||||
|
|
||||||
|
# Py version
|
||||||
|
py_version = sys.version.split()[0]
|
||||||
|
dpy_version = discord.__version__
|
||||||
|
process_lines.append((
|
||||||
|
t(_p('cmd:nerd|field:py_version|name', "Py Version")),
|
||||||
|
t(_p(
|
||||||
|
'cmd:nerd|field:py_version|value',
|
||||||
|
"`{py_version}` running discord.py `{dpy_version}`"
|
||||||
|
)).format(
|
||||||
|
py_version=py_version, dpy_version=dpy_version,
|
||||||
|
)
|
||||||
|
))
|
||||||
|
|
||||||
|
process_section = '\n'.join(tabulate(*process_lines))
|
||||||
|
embed.add_field(
|
||||||
|
name=t(_p('cmd:nerd|section:process_section|name', "Process")),
|
||||||
|
value=process_section,
|
||||||
|
inline=False,
|
||||||
|
)
|
||||||
|
|
||||||
|
# ----- Shard Statistics -----
|
||||||
|
shard_lines = []
|
||||||
|
|
||||||
|
# Handling `n` events
|
||||||
|
shard_lines.append((
|
||||||
|
t(_p('cmd:nerd|field:handling|name', "Handling")),
|
||||||
|
t(_p(
|
||||||
|
'cmd:nerd|field:handling|name',
|
||||||
|
"`{events}` active commands and events."
|
||||||
|
)).format(
|
||||||
|
events=len(self.bot._running_events)
|
||||||
|
),
|
||||||
|
))
|
||||||
|
|
||||||
|
# Working on n background tasks
|
||||||
|
shard_lines.append((
|
||||||
|
t(_p('cmd:nerd|field:working|name', "Working On")),
|
||||||
|
t(_p(
|
||||||
|
'cmd:nerd|field:working|value',
|
||||||
|
"`{tasks}` background tasks."
|
||||||
|
)).format(tasks=len(asyncio.all_tasks()))
|
||||||
|
))
|
||||||
|
|
||||||
|
# Count objects in memory
|
||||||
|
shard_lines.append((
|
||||||
|
t(_p('cmd:nerd|field:objects|name', "Objects")),
|
||||||
|
t(_p(
|
||||||
|
'cmd:nerd|field:objects|value',
|
||||||
|
"`{objects}` loaded in memory."
|
||||||
|
)).format(objects=gc.get_count())
|
||||||
|
))
|
||||||
|
|
||||||
|
# Uptime
|
||||||
|
uptime = int((utc_now() - created).total_seconds())
|
||||||
|
uptimestr = (
|
||||||
|
f"`{uptime // (24 * 3600)}` days, `{uptime // 3600 % 24:02}:{uptime // 60 % 60:02}:{uptime % 60:02}`"
|
||||||
|
)
|
||||||
|
shard_lines.append((
|
||||||
|
t(_p('cmd:nerd|field:uptime|name', "Uptime")),
|
||||||
|
uptimestr,
|
||||||
|
))
|
||||||
|
|
||||||
|
shard_section = '\n'.join(tabulate(*shard_lines))
|
||||||
|
embed.add_field(
|
||||||
|
name=t(_p('cmd:nerd|section:shard_section|name', "Shard Statistics")),
|
||||||
|
value=shard_section,
|
||||||
|
inline=False,
|
||||||
|
)
|
||||||
|
|
||||||
|
await ctx.reply(embed=embed)
|
||||||
|
|||||||
@@ -74,8 +74,8 @@ admin_extra = _p(
|
|||||||
Use {cmd_dashboard} to see an overview of the server configuration, \
|
Use {cmd_dashboard} to see an overview of the server configuration, \
|
||||||
and quickly jump to the feature configuration panels to modify settings.
|
and quickly jump to the feature configuration panels to modify settings.
|
||||||
|
|
||||||
Configuration panels are also accessible directly through the `/configure` commands \
|
Most settings may also be directly set through the `/config` and `/admin config` commands, \
|
||||||
and most features may be configured through these commands.
|
depending on whether the settings require moderator (manage server) or admin level permissions, respectively.
|
||||||
|
|
||||||
Other relevant commands for guild configuration below:
|
Other relevant commands for guild configuration below:
|
||||||
`/editshop`: Add/Edit/Remove colour roles from the {coin} shop.
|
`/editshop`: Add/Edit/Remove colour roles from the {coin} shop.
|
||||||
|
|||||||
@@ -5,22 +5,27 @@ import asyncio
|
|||||||
import discord
|
import discord
|
||||||
from discord.ext import commands as cmds
|
from discord.ext import commands as cmds
|
||||||
from discord import app_commands as appcmds
|
from discord import app_commands as appcmds
|
||||||
|
from discord.ui.text_input import TextInput, TextStyle
|
||||||
|
|
||||||
from meta import LionCog, LionBot, LionContext
|
from meta import LionCog, LionBot, LionContext
|
||||||
|
from meta.errors import SafeCancellation, UserInputError
|
||||||
from meta.logger import log_wrap
|
from meta.logger import log_wrap
|
||||||
from meta.sharding import THIS_SHARD
|
from meta.sharding import THIS_SHARD
|
||||||
from core.data import CoreData
|
from core.data import CoreData
|
||||||
from utils.lib import utc_now
|
from utils.lib import utc_now, parse_ranges, parse_time_static
|
||||||
|
from utils.ui import input
|
||||||
|
|
||||||
from wards import low_management_ward, high_management_ward, equippable_role
|
from wards import low_management_ward, high_management_ward, equippable_role, moderator_ward
|
||||||
|
|
||||||
from . import babel, logger
|
from . import babel, logger
|
||||||
from .data import ModerationData, TicketType, TicketState
|
from .data import ModerationData, TicketType, TicketState
|
||||||
from .settings import ModerationSettings
|
from .settings import ModerationSettings
|
||||||
from .settingui import ModerationSettingUI
|
from .settingui import ModerationSettingUI
|
||||||
from .ticket import Ticket
|
from .ticket import Ticket
|
||||||
|
from .tickets import NoteTicket, WarnTicket
|
||||||
|
from .ticketui import TicketListUI, TicketFilter
|
||||||
|
|
||||||
_p = babel._p
|
_p, _np = babel._p, babel._np
|
||||||
|
|
||||||
|
|
||||||
class ModerationCog(LionCog):
|
class ModerationCog(LionCog):
|
||||||
@@ -51,7 +56,7 @@ class ModerationCog(LionCog):
|
|||||||
"Moderation configuration will not crossload."
|
"Moderation configuration will not crossload."
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
self.crossload_group(self.configure_group, configcog.configure_group)
|
self.crossload_group(self.configure_group, configcog.admin_config_group)
|
||||||
|
|
||||||
if self.bot.is_ready():
|
if self.bot.is_ready():
|
||||||
await self.initialise()
|
await self.initialise()
|
||||||
@@ -125,6 +130,447 @@ class ModerationCog(LionCog):
|
|||||||
...
|
...
|
||||||
|
|
||||||
# ----- Commands -----
|
# ----- Commands -----
|
||||||
|
# modnote command
|
||||||
|
@cmds.hybrid_command(
|
||||||
|
name=_p('cmd:modnote', "modnote"),
|
||||||
|
description=_p(
|
||||||
|
'cmd:modnote|desc',
|
||||||
|
"Add a note to the target member's moderation record."
|
||||||
|
)
|
||||||
|
)
|
||||||
|
@appcmds.rename(
|
||||||
|
target=_p('cmd:modnote|param:target', "target"),
|
||||||
|
note=_p('cmd:modnote|param:note', "note"),
|
||||||
|
)
|
||||||
|
@appcmds.describe(
|
||||||
|
target=_p(
|
||||||
|
'cmd:modnote|param:target|desc',
|
||||||
|
"Target member or user to add a note to."
|
||||||
|
),
|
||||||
|
note=_p(
|
||||||
|
'cmd:modnote|param:note|desc',
|
||||||
|
"Contents of the note."
|
||||||
|
),
|
||||||
|
)
|
||||||
|
@appcmds.default_permissions(manage_guild=True)
|
||||||
|
@appcmds.guild_only
|
||||||
|
@moderator_ward
|
||||||
|
async def cmd_modnote(self, ctx: LionContext,
|
||||||
|
target: discord.Member | discord.User,
|
||||||
|
note: Optional[appcmds.Range[str, 1, 1024]] = None,
|
||||||
|
):
|
||||||
|
"""
|
||||||
|
Create a NoteTicket on the given target.
|
||||||
|
|
||||||
|
If `note` is not given, prompts for the note content via modal.
|
||||||
|
"""
|
||||||
|
if not ctx.guild:
|
||||||
|
return
|
||||||
|
if not ctx.interaction:
|
||||||
|
return
|
||||||
|
t = self.bot.translator.t
|
||||||
|
|
||||||
|
if note is None:
|
||||||
|
# Prompt for note via modal
|
||||||
|
modal_title = t(_p(
|
||||||
|
'cmd:modnote|modal:enter_note|title',
|
||||||
|
"Moderation Note"
|
||||||
|
))
|
||||||
|
input_field = TextInput(
|
||||||
|
label=t(_p(
|
||||||
|
'cmd:modnote|modal:enter_note|field|label',
|
||||||
|
"Note Content",
|
||||||
|
)),
|
||||||
|
style=TextStyle.long,
|
||||||
|
min_length=1,
|
||||||
|
max_length=1024,
|
||||||
|
)
|
||||||
|
try:
|
||||||
|
interaction, note = await input(
|
||||||
|
ctx.interaction, modal_title,
|
||||||
|
field=input_field,
|
||||||
|
timeout=300
|
||||||
|
)
|
||||||
|
except asyncio.TimeoutError:
|
||||||
|
# Moderator did not fill in the modal in time
|
||||||
|
# Just leave quietly
|
||||||
|
raise SafeCancellation
|
||||||
|
else:
|
||||||
|
interaction = ctx.interaction
|
||||||
|
|
||||||
|
await interaction.response.defer(thinking=True, ephemeral=True)
|
||||||
|
|
||||||
|
# Create NoteTicket
|
||||||
|
ticket = await NoteTicket.create(
|
||||||
|
bot=self.bot,
|
||||||
|
guildid=ctx.guild.id, userid=target.id,
|
||||||
|
moderatorid=ctx.author.id, content=note, expiry=None
|
||||||
|
)
|
||||||
|
|
||||||
|
# Write confirmation with ticket number and link to ticket if relevant
|
||||||
|
embed = discord.Embed(
|
||||||
|
colour=discord.Colour.orange(),
|
||||||
|
description=t(_p(
|
||||||
|
'cmd:modnote|embed:success|desc',
|
||||||
|
"Moderation note created as [Ticket #{ticket}]({jump_link})"
|
||||||
|
)).format(
|
||||||
|
ticket=ticket.data.guild_ticketid,
|
||||||
|
jump_link=ticket.jump_url or ctx.message.jump_url
|
||||||
|
)
|
||||||
|
)
|
||||||
|
await interaction.edit_original_response(embed=embed)
|
||||||
|
|
||||||
|
# Warning Ticket Command
|
||||||
|
@cmds.hybrid_command(
|
||||||
|
name=_p('cmd:warning', "warning"),
|
||||||
|
description=_p(
|
||||||
|
'cmd:warning|desc',
|
||||||
|
"Warn a member for a misdemeanour, and add it to their moderation record."
|
||||||
|
)
|
||||||
|
)
|
||||||
|
@appcmds.rename(
|
||||||
|
target=_p('cmd:warning|param:target', "target"),
|
||||||
|
reason=_p('cmd:warning|param:reason', "reason"),
|
||||||
|
)
|
||||||
|
@appcmds.describe(
|
||||||
|
target=_p(
|
||||||
|
'cmd:warning|param:target|desc',
|
||||||
|
"Target member to warn."
|
||||||
|
),
|
||||||
|
reason=_p(
|
||||||
|
'cmd:warning|param:reason|desc',
|
||||||
|
"The reason why you are warning this member."
|
||||||
|
),
|
||||||
|
)
|
||||||
|
@appcmds.default_permissions(manage_guild=True)
|
||||||
|
@appcmds.guild_only
|
||||||
|
@moderator_ward
|
||||||
|
async def cmd_warning(self, ctx: LionContext,
|
||||||
|
target: discord.Member,
|
||||||
|
reason: Optional[appcmds.Range[str, 0, 1024]] = None,
|
||||||
|
):
|
||||||
|
if not ctx.guild:
|
||||||
|
return
|
||||||
|
if not ctx.interaction:
|
||||||
|
return
|
||||||
|
t = self.bot.translator.t
|
||||||
|
|
||||||
|
# Prompt for warning reason if not given
|
||||||
|
if reason is None:
|
||||||
|
modal_title = t(_p(
|
||||||
|
'cmd:warning|modal:reason|title',
|
||||||
|
"Moderation Warning"
|
||||||
|
))
|
||||||
|
input_field = TextInput(
|
||||||
|
label=t(_p(
|
||||||
|
'cmd:warning|modal:reason|field|label',
|
||||||
|
"Reason for the warning (visible to user)."
|
||||||
|
)),
|
||||||
|
style=TextStyle.long,
|
||||||
|
min_length=0,
|
||||||
|
max_length=1024,
|
||||||
|
)
|
||||||
|
try:
|
||||||
|
interaction, note = await input(
|
||||||
|
ctx.interaction, modal_title,
|
||||||
|
field=input_field,
|
||||||
|
timeout=300,
|
||||||
|
)
|
||||||
|
except asyncio.TimeoutError:
|
||||||
|
raise SafeCancellation
|
||||||
|
else:
|
||||||
|
interaction = ctx.interaction
|
||||||
|
|
||||||
|
await interaction.response.defer(thinking=True, ephemeral=False)
|
||||||
|
|
||||||
|
# Create WarnTicket
|
||||||
|
ticket = await WarnTicket.create(
|
||||||
|
bot=self.bot,
|
||||||
|
guildid=ctx.guild.id, userid=target.id,
|
||||||
|
moderatorid=ctx.author.id, content=reason
|
||||||
|
)
|
||||||
|
|
||||||
|
# Post to user or moderation notify channel
|
||||||
|
alert_embed = discord.Embed(
|
||||||
|
colour=discord.Colour.dark_red(),
|
||||||
|
title=t(_p(
|
||||||
|
'cmd:warning|embed:user_alert|title',
|
||||||
|
"You have received a warning!"
|
||||||
|
)),
|
||||||
|
description=reason,
|
||||||
|
)
|
||||||
|
alert_embed.add_field(
|
||||||
|
name=t(_p(
|
||||||
|
'cmd:warning|embed:user_alert|field:note|name',
|
||||||
|
"Note"
|
||||||
|
)),
|
||||||
|
value=t(_p(
|
||||||
|
'cmd:warning|embed:user_alert|field:note|value',
|
||||||
|
"*Warnings appear in your moderation history."
|
||||||
|
" Continuing failure to comply with server rules and moderator"
|
||||||
|
" directions may result in more severe action."
|
||||||
|
))
|
||||||
|
)
|
||||||
|
alert_embed.set_footer(
|
||||||
|
icon_url=ctx.guild.icon,
|
||||||
|
text=ctx.guild.name,
|
||||||
|
)
|
||||||
|
alert = await self.send_alert(target, embed=alert_embed)
|
||||||
|
|
||||||
|
# Ack the ticket creation, including alert status and warning count
|
||||||
|
|
||||||
|
warning_count = await ticket.count_warnings_for(
|
||||||
|
self.bot, ctx.guild.id, target.id
|
||||||
|
)
|
||||||
|
count_line = t(_np(
|
||||||
|
'cmd:warning|embed:success|line:count',
|
||||||
|
"This their first warning.",
|
||||||
|
"They have recieved **`{count}`** warnings.",
|
||||||
|
warning_count
|
||||||
|
)).format(count=warning_count)
|
||||||
|
|
||||||
|
embed = discord.Embed(
|
||||||
|
colour=discord.Colour.orange(),
|
||||||
|
description=t(_p(
|
||||||
|
'cmd:warning|embed:success|desc',
|
||||||
|
"[Ticket #{ticket}]({jump_link}) {user} has been warned."
|
||||||
|
)).format(
|
||||||
|
ticket=ticket.data.guild_ticketid,
|
||||||
|
jump_link=ticket.jump_url or ctx.message.jump_url,
|
||||||
|
user=target.mention,
|
||||||
|
) + '\n' + count_line
|
||||||
|
)
|
||||||
|
if alert is None:
|
||||||
|
embed.add_field(
|
||||||
|
name=t(_p(
|
||||||
|
'cmd:warning|embed:success|field:no_alert|name',
|
||||||
|
"Note"
|
||||||
|
)),
|
||||||
|
value=t(_p(
|
||||||
|
'cmd:warning|embed:success|field:no_alert|value',
|
||||||
|
"*Could not deliver warning to the target.*"
|
||||||
|
))
|
||||||
|
)
|
||||||
|
await interaction.edit_original_response(embed=embed)
|
||||||
|
|
||||||
|
# Pardon user command
|
||||||
|
@cmds.hybrid_command(
|
||||||
|
name=_p('cmd:pardon', "pardon"),
|
||||||
|
description=_p(
|
||||||
|
'cmd:pardon|desc',
|
||||||
|
"Pardon moderation tickets to mark them as no longer in effect."
|
||||||
|
)
|
||||||
|
)
|
||||||
|
@appcmds.rename(
|
||||||
|
ticketids=_p(
|
||||||
|
'cmd:pardon|param:ticketids',
|
||||||
|
"tickets"
|
||||||
|
),
|
||||||
|
reason=_p(
|
||||||
|
'cmd:pardon|param:reason',
|
||||||
|
"reason"
|
||||||
|
)
|
||||||
|
)
|
||||||
|
@appcmds.describe(
|
||||||
|
ticketids=_p(
|
||||||
|
'cmd:pardon|param:ticketids|desc',
|
||||||
|
"Comma separated list of ticket numbers to pardon."
|
||||||
|
),
|
||||||
|
reason=_p(
|
||||||
|
'cmd:pardon|param:reason',
|
||||||
|
"Why these tickets are being pardoned."
|
||||||
|
)
|
||||||
|
)
|
||||||
|
@appcmds.default_permissions(manage_guild=True)
|
||||||
|
@appcmds.guild_only
|
||||||
|
@moderator_ward
|
||||||
|
async def cmd_pardon(self, ctx: LionContext,
|
||||||
|
ticketids: str,
|
||||||
|
reason: Optional[appcmds.Range[str, 0, 1024]] = None,
|
||||||
|
):
|
||||||
|
if not ctx.guild:
|
||||||
|
return
|
||||||
|
if not ctx.interaction:
|
||||||
|
return
|
||||||
|
t = self.bot.translator.t
|
||||||
|
|
||||||
|
# Prompt for pardon reason if not given
|
||||||
|
# Note we can't parse first since we need to do first response with the modal
|
||||||
|
if reason is None:
|
||||||
|
modal_title = t(_p(
|
||||||
|
'cmd:pardon|modal:reason|title',
|
||||||
|
"Pardon Tickets"
|
||||||
|
))
|
||||||
|
input_field = TextInput(
|
||||||
|
label=t(_p(
|
||||||
|
'cmd:pardon|modal:reason|field|label',
|
||||||
|
"Why are you pardoning these tickets?"
|
||||||
|
)),
|
||||||
|
style=TextStyle.long,
|
||||||
|
min_length=0,
|
||||||
|
max_length=1024,
|
||||||
|
)
|
||||||
|
try:
|
||||||
|
interaction, reason = await input(
|
||||||
|
ctx.interaction, modal_title, field=input_field, timeout=300,
|
||||||
|
)
|
||||||
|
except asyncio.TimeoutError:
|
||||||
|
raise SafeCancellation
|
||||||
|
else:
|
||||||
|
interaction = ctx.interaction
|
||||||
|
|
||||||
|
await interaction.response.defer(thinking=True)
|
||||||
|
|
||||||
|
# Parse provided ticketids
|
||||||
|
try:
|
||||||
|
parsed_ids = parse_ranges(ticketids)
|
||||||
|
errored = False
|
||||||
|
except ValueError:
|
||||||
|
errored = True
|
||||||
|
parsed_ids = []
|
||||||
|
|
||||||
|
if errored or not parsed_ids:
|
||||||
|
raise UserInputError(t(_p(
|
||||||
|
'cmd:pardon|error:parse_ticketids',
|
||||||
|
"Could not parse provided tickets as a list of ticket ids!"
|
||||||
|
" Please enter tickets as a comma separated list of ticket numbers,"
|
||||||
|
" for example `1, 2, 3`."
|
||||||
|
)))
|
||||||
|
|
||||||
|
# Now find these tickets
|
||||||
|
tickets = await Ticket.fetch_tickets(
|
||||||
|
bot=self.bot,
|
||||||
|
guildid=ctx.guild.id,
|
||||||
|
guild_ticketid=parsed_ids,
|
||||||
|
)
|
||||||
|
if not tickets:
|
||||||
|
raise UserInputError(t(_p(
|
||||||
|
'cmd:pardon|error:no_matching',
|
||||||
|
"No matching moderation tickets found to pardon!"
|
||||||
|
)))
|
||||||
|
|
||||||
|
# Pardon each ticket
|
||||||
|
for ticket in tickets:
|
||||||
|
await ticket.pardon(
|
||||||
|
modid=ctx.author.id,
|
||||||
|
reason=reason
|
||||||
|
)
|
||||||
|
|
||||||
|
# Now ack the pardon
|
||||||
|
count = len(tickets)
|
||||||
|
ticketstr = ', '.join(
|
||||||
|
f"[#{ticket.data.guild_ticketid}]({ticket.jump_url})" for ticket in tickets
|
||||||
|
)
|
||||||
|
|
||||||
|
embed = discord.Embed(
|
||||||
|
colour=discord.Colour.brand_green(),
|
||||||
|
description=t(_np(
|
||||||
|
'cmd:pardon|embed:success|title',
|
||||||
|
"Ticket {ticketstr} has been pardoned.",
|
||||||
|
"The following tickets have been pardoned:\n{ticketstr}",
|
||||||
|
count
|
||||||
|
)).format(ticketstr=ticketstr)
|
||||||
|
)
|
||||||
|
await interaction.edit_original_response(embed=embed)
|
||||||
|
|
||||||
|
# View tickets
|
||||||
|
@cmds.hybrid_command(
|
||||||
|
name=_p('cmd:tickets', "tickets"),
|
||||||
|
description=_p(
|
||||||
|
'cmd:tickets|desc',
|
||||||
|
"View moderation tickets in this server."
|
||||||
|
)
|
||||||
|
)
|
||||||
|
@appcmds.rename(
|
||||||
|
target_user=_p('cmd:tickets|param:target', "target"),
|
||||||
|
ticket_type=_p('cmd:tickets|param:type', "type"),
|
||||||
|
ticket_state=_p('cmd:tickets|param:state', "ticket_state"),
|
||||||
|
include_pardoned=_p('cmd:tickets|param:pardoned', "include_pardoned"),
|
||||||
|
acting_moderator=_p('cmd:tickets|param:moderator', "acting_moderator"),
|
||||||
|
after=_p('cmd:tickets|param:after', "after"),
|
||||||
|
before=_p('cmd:tickets|param:before', "before"),
|
||||||
|
)
|
||||||
|
@appcmds.describe(
|
||||||
|
target_user=_p(
|
||||||
|
'cmd:tickets|param:target|desc',
|
||||||
|
"Filter by tickets acting on a given user."
|
||||||
|
),
|
||||||
|
ticket_type=_p(
|
||||||
|
'cmd:tickets|param:type|desc',
|
||||||
|
"Filter by ticket type."
|
||||||
|
),
|
||||||
|
ticket_state=_p(
|
||||||
|
'cmd:tickets|param:state|desc',
|
||||||
|
"Filter by ticket state."
|
||||||
|
),
|
||||||
|
include_pardoned=_p(
|
||||||
|
'cmd:tickets|param:pardoned|desc',
|
||||||
|
"Whether to only show active tickets, or also include pardoned."
|
||||||
|
),
|
||||||
|
acting_moderator=_p(
|
||||||
|
'cmd:tickets|param:moderator|desc',
|
||||||
|
"Filter by moderator responsible for the ticket."
|
||||||
|
),
|
||||||
|
after=_p(
|
||||||
|
'cmd:tickets|param:after|desc',
|
||||||
|
"Only show tickets after this date (YYY-MM-DD HH:MM)"
|
||||||
|
),
|
||||||
|
before=_p(
|
||||||
|
'cmd:tickets|param:before|desc',
|
||||||
|
"Only show tickets before this date (YYY-MM-DD HH:MM)"
|
||||||
|
),
|
||||||
|
)
|
||||||
|
@appcmds.choices(
|
||||||
|
ticket_type=[
|
||||||
|
appcmds.Choice(name=typ.name, value=typ.name)
|
||||||
|
for typ in (TicketType.NOTE, TicketType.WARNING, TicketType.STUDY_BAN)
|
||||||
|
],
|
||||||
|
ticket_state=[
|
||||||
|
appcmds.Choice(name=state.name, value=state.name)
|
||||||
|
for state in (
|
||||||
|
TicketState.OPEN, TicketState.EXPIRING, TicketState.EXPIRED, TicketState.PARDONED,
|
||||||
|
)
|
||||||
|
]
|
||||||
|
)
|
||||||
|
@appcmds.default_permissions(manage_guild=True)
|
||||||
|
@appcmds.guild_only
|
||||||
|
@moderator_ward
|
||||||
|
async def tickets_cmd(self, ctx: LionContext,
|
||||||
|
target_user: Optional[discord.User] = None,
|
||||||
|
ticket_type: Optional[appcmds.Choice[str]] = None,
|
||||||
|
ticket_state: Optional[appcmds.Choice[str]] = None,
|
||||||
|
include_pardoned: Optional[bool] = None,
|
||||||
|
acting_moderator: Optional[discord.User] = None,
|
||||||
|
after: Optional[str] = None,
|
||||||
|
before: Optional[str] = None,
|
||||||
|
):
|
||||||
|
if not ctx.guild:
|
||||||
|
return
|
||||||
|
if not ctx.interaction:
|
||||||
|
return
|
||||||
|
|
||||||
|
filters = TicketFilter(self.bot)
|
||||||
|
if target_user is not None:
|
||||||
|
filters.targetids = [target_user.id]
|
||||||
|
if ticket_type is not None:
|
||||||
|
filters.types = [TicketType[ticket_type.value]]
|
||||||
|
if ticket_state is not None:
|
||||||
|
filters.states = [TicketState[ticket_state.value]]
|
||||||
|
elif include_pardoned:
|
||||||
|
filters.states = None
|
||||||
|
else:
|
||||||
|
filters.states = [TicketState.OPEN, TicketState.EXPIRING]
|
||||||
|
if acting_moderator is not None:
|
||||||
|
filters.moderatorids = [acting_moderator.id]
|
||||||
|
if after is not None:
|
||||||
|
filters.after = await parse_time_static(after, ctx.lguild.timezone)
|
||||||
|
if before is not None:
|
||||||
|
filters.before = await parse_time_static(before, ctx.lguild.timezone)
|
||||||
|
|
||||||
|
|
||||||
|
ticketsui = TicketListUI(self.bot, ctx.guild, ctx.author.id, filters=filters)
|
||||||
|
await ticketsui.run(ctx.interaction)
|
||||||
|
await ticketsui.wait()
|
||||||
|
|
||||||
# ----- Configuration -----
|
# ----- Configuration -----
|
||||||
@LionCog.placeholder_group
|
@LionCog.placeholder_group
|
||||||
@@ -140,12 +586,13 @@ class ModerationCog(LionCog):
|
|||||||
)
|
)
|
||||||
)
|
)
|
||||||
@appcmds.rename(
|
@appcmds.rename(
|
||||||
|
adminrole=ModerationSettings.AdminRole._display_name,
|
||||||
modrole=ModerationSettings.ModRole._display_name,
|
modrole=ModerationSettings.ModRole._display_name,
|
||||||
ticket_log=ModerationSettings.TicketLog._display_name,
|
ticket_log=ModerationSettings.TicketLog._display_name,
|
||||||
alert_channel=ModerationSettings.AlertChannel._display_name,
|
alert_channel=ModerationSettings.AlertChannel._display_name,
|
||||||
)
|
)
|
||||||
@appcmds.describe(
|
@appcmds.describe(
|
||||||
modrole=ModerationSettings.ModRole._desc,
|
adminrole=ModerationSettings.AdminRole._desc,
|
||||||
ticket_log=ModerationSettings.TicketLog._desc,
|
ticket_log=ModerationSettings.TicketLog._desc,
|
||||||
alert_channel=ModerationSettings.AlertChannel._desc,
|
alert_channel=ModerationSettings.AlertChannel._desc,
|
||||||
)
|
)
|
||||||
@@ -154,6 +601,7 @@ class ModerationCog(LionCog):
|
|||||||
modrole: Optional[discord.Role] = None,
|
modrole: Optional[discord.Role] = None,
|
||||||
ticket_log: Optional[discord.TextChannel] = None,
|
ticket_log: Optional[discord.TextChannel] = None,
|
||||||
alert_channel: Optional[discord.TextChannel] = None,
|
alert_channel: Optional[discord.TextChannel] = None,
|
||||||
|
adminrole: Optional[discord.Role] = None,
|
||||||
):
|
):
|
||||||
if not ctx.guild:
|
if not ctx.guild:
|
||||||
return
|
return
|
||||||
@@ -169,6 +617,12 @@ class ModerationCog(LionCog):
|
|||||||
instance = setting(ctx.guild.id, modrole.id)
|
instance = setting(ctx.guild.id, modrole.id)
|
||||||
modified.append(instance)
|
modified.append(instance)
|
||||||
|
|
||||||
|
if adminrole is not None:
|
||||||
|
setting = self.settings.AdminRole
|
||||||
|
await setting._check_value(ctx.guild.id, adminrole)
|
||||||
|
instance = setting(ctx.guild.id, adminrole.id)
|
||||||
|
modified.append(instance)
|
||||||
|
|
||||||
if ticket_log is not None:
|
if ticket_log is not None:
|
||||||
setting = self.settings.TicketLog
|
setting = self.settings.TicketLog
|
||||||
await setting._check_value(ctx.guild.id, ticket_log)
|
await setting._check_value(ctx.guild.id, ticket_log)
|
||||||
|
|||||||
@@ -105,6 +105,6 @@ class ModerationData(Registry):
|
|||||||
file_data = String()
|
file_data = String()
|
||||||
expiry = Timestamp()
|
expiry = Timestamp()
|
||||||
pardoned_by = Integer()
|
pardoned_by = Integer()
|
||||||
pardoned_at = Integer()
|
pardoned_at = Timestamp()
|
||||||
pardoned_reason = String()
|
pardoned_reason = String()
|
||||||
created_at = Timestamp()
|
created_at = Timestamp()
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ from settings.setting_types import (
|
|||||||
|
|
||||||
from core.data import CoreData
|
from core.data import CoreData
|
||||||
from babel.translator import ctx_translator
|
from babel.translator import ctx_translator
|
||||||
|
from wards import low_management_iward, high_management_iward
|
||||||
|
|
||||||
from . import babel
|
from . import babel
|
||||||
|
|
||||||
@@ -16,6 +17,7 @@ class ModerationSettings(SettingGroup):
|
|||||||
class TicketLog(ModelData, ChannelSetting):
|
class TicketLog(ModelData, ChannelSetting):
|
||||||
setting_id = "ticket_log"
|
setting_id = "ticket_log"
|
||||||
_event = 'guildset_ticket_log'
|
_event = 'guildset_ticket_log'
|
||||||
|
_write_ward = low_management_iward
|
||||||
|
|
||||||
_display_name = _p('guildset:ticket_log', "ticket_log")
|
_display_name = _p('guildset:ticket_log', "ticket_log")
|
||||||
_desc = _p(
|
_desc = _p(
|
||||||
@@ -66,6 +68,7 @@ class ModerationSettings(SettingGroup):
|
|||||||
class AlertChannel(ModelData, ChannelSetting):
|
class AlertChannel(ModelData, ChannelSetting):
|
||||||
setting_id = "alert_channel"
|
setting_id = "alert_channel"
|
||||||
_event = 'guildset_alert_channel'
|
_event = 'guildset_alert_channel'
|
||||||
|
_write_ward = low_management_iward
|
||||||
|
|
||||||
_display_name = _p('guildset:alert_channel', "alert_channel")
|
_display_name = _p('guildset:alert_channel', "alert_channel")
|
||||||
_desc = _p(
|
_desc = _p(
|
||||||
@@ -119,18 +122,23 @@ class ModerationSettings(SettingGroup):
|
|||||||
class ModRole(ModelData, RoleSetting):
|
class ModRole(ModelData, RoleSetting):
|
||||||
setting_id = "mod_role"
|
setting_id = "mod_role"
|
||||||
_event = 'guildset_mod_role'
|
_event = 'guildset_mod_role'
|
||||||
|
_write_ward = high_management_iward
|
||||||
|
|
||||||
_display_name = _p('guildset:mod_role', "mod_role")
|
_display_name = _p('guildset:mod_role', "mod_role")
|
||||||
_desc = _p(
|
_desc = _p(
|
||||||
'guildset:mod_role|desc',
|
'guildset:mod_role|desc',
|
||||||
"Guild role permitted to view configuration and perform moderation tasks."
|
"Server role permitted to perform moderation and minor bot configuration."
|
||||||
)
|
)
|
||||||
_long_desc = _p(
|
_long_desc = _p(
|
||||||
'guildset:mod_role|long_desc',
|
'guildset:mod_role|long_desc',
|
||||||
"Members with the set role will be able to access my configuration panels, "
|
"Members with the moderator role are considered moderators,"
|
||||||
"and perform some moderation tasks, such as setting up pomodoro timers. "
|
" and are permitted to use moderator commands,"
|
||||||
"Moderators cannot reconfigure most bot configuration, "
|
" such as viewing and pardoning moderation tickets,"
|
||||||
"or perform operations they do not already have permission for in Discord."
|
" creating moderation notes,"
|
||||||
|
" and performing minor reconfiguration through the `/config` command.\n"
|
||||||
|
"Moderators are never permitted to perform actions (such as giving roles)"
|
||||||
|
" that they do not already have the Discord permissions for.\n"
|
||||||
|
"Members with the 'Manage Guild' permission are always considered moderators."
|
||||||
)
|
)
|
||||||
_accepts = _p(
|
_accepts = _p(
|
||||||
'guildset:mod_role|accepts',
|
'guildset:mod_role|accepts',
|
||||||
@@ -148,12 +156,14 @@ class ModerationSettings(SettingGroup):
|
|||||||
if value:
|
if value:
|
||||||
resp = t(_p(
|
resp = t(_p(
|
||||||
'guildset:mod_role|set_response:set',
|
'guildset:mod_role|set_response:set',
|
||||||
"Members with the {role} will be considered moderators."
|
"Members with {role} will be considered moderators."
|
||||||
|
" You may need to grant them access to view moderation commands"
|
||||||
|
" via the server integration settings."
|
||||||
)).format(role=value.mention)
|
)).format(role=value.mention)
|
||||||
else:
|
else:
|
||||||
resp = t(_p(
|
resp = t(_p(
|
||||||
'guildset:mod_role|set_response:unset',
|
'guildset:mod_role|set_response:unset',
|
||||||
"No members will be given moderation privileges."
|
"Only members with the 'Manage Guild' permission will be considered moderators."
|
||||||
))
|
))
|
||||||
return resp
|
return resp
|
||||||
|
|
||||||
@@ -167,3 +177,47 @@ class ModerationSettings(SettingGroup):
|
|||||||
'guildset:mod_role|formatted:unset',
|
'guildset:mod_role|formatted:unset',
|
||||||
"Not Set."
|
"Not Set."
|
||||||
))
|
))
|
||||||
|
|
||||||
|
class AdminRole(ModelData, RoleSetting):
|
||||||
|
setting_id = "admin_role"
|
||||||
|
_event = 'guildset_admin_role'
|
||||||
|
_write_ward = high_management_iward
|
||||||
|
|
||||||
|
_display_name = _p('guildset:admin_role', "admin_role")
|
||||||
|
_desc = _p(
|
||||||
|
'guildset:admin_role|desc',
|
||||||
|
"Server role allowing access to all administrator level functionality in Leo."
|
||||||
|
)
|
||||||
|
_long_desc = _p(
|
||||||
|
'guildset:admin_role|long_desc',
|
||||||
|
"Members with this role are considered to be server administrators, "
|
||||||
|
"allowing them to use all of my interfaces and commands, "
|
||||||
|
"except for managing roles that are above them in the role hierachy. "
|
||||||
|
"This setting allows giving members administrator-level permissions "
|
||||||
|
"over my systems, without actually giving the members admin server permissions. "
|
||||||
|
"Note that the role will also need to be given permission to see the commands "
|
||||||
|
"through the Discord server integrations interface."
|
||||||
|
)
|
||||||
|
_accepts = _p(
|
||||||
|
'guildset:admin_role|accepts',
|
||||||
|
"Admin role name or id."
|
||||||
|
)
|
||||||
|
|
||||||
|
_model = CoreData.Guild
|
||||||
|
_column = CoreData.Guild.admin_role.name
|
||||||
|
|
||||||
|
@property
|
||||||
|
def update_message(self) -> str:
|
||||||
|
t = ctx_translator.get().t
|
||||||
|
value = self.value
|
||||||
|
if value:
|
||||||
|
resp = t(_p(
|
||||||
|
'guildset:admin_role|set_response:set',
|
||||||
|
"Members with {role} will now be considered admins, and have access to my full interface."
|
||||||
|
)).format(role=value.mention)
|
||||||
|
else:
|
||||||
|
resp = t(_p(
|
||||||
|
'guildset:admin_role|set_response:unset',
|
||||||
|
"The admin role has been unset. Only members with administrator permissions will be considered admins."
|
||||||
|
))
|
||||||
|
return resp
|
||||||
|
|||||||
@@ -18,9 +18,10 @@ _p = babel._p
|
|||||||
|
|
||||||
class ModerationSettingUI(ConfigUI):
|
class ModerationSettingUI(ConfigUI):
|
||||||
setting_classes = (
|
setting_classes = (
|
||||||
|
ModerationSettings.ModRole,
|
||||||
|
ModerationSettings.AdminRole,
|
||||||
ModerationSettings.TicketLog,
|
ModerationSettings.TicketLog,
|
||||||
ModerationSettings.AlertChannel,
|
ModerationSettings.AlertChannel,
|
||||||
ModerationSettings.ModRole,
|
|
||||||
)
|
)
|
||||||
|
|
||||||
def __init__(self, bot: LionBot, guildid: int, channelid, **kwargs):
|
def __init__(self, bot: LionBot, guildid: int, channelid, **kwargs):
|
||||||
@@ -41,6 +42,7 @@ class ModerationSettingUI(ConfigUI):
|
|||||||
await selection.response.defer(thinking=True, ephemeral=True)
|
await selection.response.defer(thinking=True, ephemeral=True)
|
||||||
|
|
||||||
setting = self.get_instance(ModerationSettings.TicketLog)
|
setting = self.get_instance(ModerationSettings.TicketLog)
|
||||||
|
await setting.interaction_check(setting.parent_id, selection)
|
||||||
setting.value = selected.values[0] if selected.values else None
|
setting.value = selected.values[0] if selected.values else None
|
||||||
await setting.write()
|
await setting.write()
|
||||||
await selection.delete_original_response()
|
await selection.delete_original_response()
|
||||||
@@ -66,6 +68,7 @@ class ModerationSettingUI(ConfigUI):
|
|||||||
await selection.response.defer(thinking=True, ephemeral=True)
|
await selection.response.defer(thinking=True, ephemeral=True)
|
||||||
|
|
||||||
setting = self.get_instance(ModerationSettings.AlertChannel)
|
setting = self.get_instance(ModerationSettings.AlertChannel)
|
||||||
|
await setting.interaction_check(setting.parent_id, selection)
|
||||||
setting.value = selected.values[0] if selected.values else None
|
setting.value = selected.values[0] if selected.values else None
|
||||||
await setting.write()
|
await setting.write()
|
||||||
await selection.delete_original_response()
|
await selection.delete_original_response()
|
||||||
@@ -91,6 +94,7 @@ class ModerationSettingUI(ConfigUI):
|
|||||||
await selection.response.defer(thinking=True, ephemeral=True)
|
await selection.response.defer(thinking=True, ephemeral=True)
|
||||||
|
|
||||||
setting = self.get_instance(ModerationSettings.ModRole)
|
setting = self.get_instance(ModerationSettings.ModRole)
|
||||||
|
await setting.interaction_check(setting.parent_id, selection)
|
||||||
setting.value = selected.values[0] if selected.values else None
|
setting.value = selected.values[0] if selected.values else None
|
||||||
await setting.write()
|
await setting.write()
|
||||||
await selection.delete_original_response()
|
await selection.delete_original_response()
|
||||||
@@ -103,6 +107,32 @@ class ModerationSettingUI(ConfigUI):
|
|||||||
"Select Moderator Role"
|
"Select Moderator Role"
|
||||||
))
|
))
|
||||||
|
|
||||||
|
# Admin Role Selector
|
||||||
|
@select(
|
||||||
|
cls=RoleSelect,
|
||||||
|
placeholder="ADMINROLE_MENU_PLACEHOLDER",
|
||||||
|
min_values=0, max_values=1
|
||||||
|
)
|
||||||
|
async def adminrole_menu(self, selection: discord.Interaction, selected: RoleSelect):
|
||||||
|
"""
|
||||||
|
Single role selector for the `admin_role` setting.
|
||||||
|
"""
|
||||||
|
await selection.response.defer(thinking=True, ephemeral=True)
|
||||||
|
|
||||||
|
setting = self.get_instance(ModerationSettings.AdminRole)
|
||||||
|
await setting.interaction_check(setting.parent_id, selection)
|
||||||
|
setting.value = selected.values[0] if selected.values else None
|
||||||
|
await setting.write()
|
||||||
|
await selection.delete_original_response()
|
||||||
|
|
||||||
|
async def adminrole_menu_refresh(self):
|
||||||
|
menu = self.adminrole_menu
|
||||||
|
t = self.bot.translator.t
|
||||||
|
menu.placeholder = t(_p(
|
||||||
|
'ui:moderation_config|menu:adminrole|placeholder',
|
||||||
|
"Select Admin Role"
|
||||||
|
))
|
||||||
|
|
||||||
# ----- UI Flow -----
|
# ----- UI Flow -----
|
||||||
async def make_message(self) -> MessageArgs:
|
async def make_message(self) -> MessageArgs:
|
||||||
t = self.bot.translator.t
|
t = self.bot.translator.t
|
||||||
@@ -133,13 +163,15 @@ class ModerationSettingUI(ConfigUI):
|
|||||||
self.ticket_log_menu_refresh(),
|
self.ticket_log_menu_refresh(),
|
||||||
self.alert_channel_menu_refresh(),
|
self.alert_channel_menu_refresh(),
|
||||||
self.modrole_menu_refresh(),
|
self.modrole_menu_refresh(),
|
||||||
|
self.adminrole_menu_refresh(),
|
||||||
)
|
)
|
||||||
await asyncio.gather(*component_refresh)
|
await asyncio.gather(*component_refresh)
|
||||||
|
|
||||||
self.set_layout(
|
self.set_layout(
|
||||||
|
(self.adminrole_menu,),
|
||||||
|
(self.modrole_menu,),
|
||||||
(self.ticket_log_menu,),
|
(self.ticket_log_menu,),
|
||||||
(self.alert_channel_menu,),
|
(self.alert_channel_menu,),
|
||||||
(self.modrole_menu,),
|
|
||||||
(self.edit_button, self.reset_button, self.close_button,)
|
(self.edit_button, self.reset_button, self.close_button,)
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -147,7 +179,7 @@ class ModerationSettingUI(ConfigUI):
|
|||||||
class ModerationDashboard(DashboardSection):
|
class ModerationDashboard(DashboardSection):
|
||||||
section_name = _p(
|
section_name = _p(
|
||||||
"dash:moderation|title",
|
"dash:moderation|title",
|
||||||
"Moderation Settings ({commands[configure moderation]})"
|
"Moderation Settings ({commands[admin config moderation]})"
|
||||||
)
|
)
|
||||||
_option_name = _p(
|
_option_name = _p(
|
||||||
"dash:moderation|dropdown|placeholder",
|
"dash:moderation|dropdown|placeholder",
|
||||||
|
|||||||
@@ -99,11 +99,11 @@ class Ticket:
|
|||||||
return tickets
|
return tickets
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def guild(self):
|
def guild(self) -> Optional[discord.Guild]:
|
||||||
return self.bot.get_guild(self.data.guildid)
|
return self.bot.get_guild(self.data.guildid)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def target(self):
|
def target(self) -> Optional[discord.Member]:
|
||||||
guild = self.guild
|
guild = self.guild
|
||||||
if guild:
|
if guild:
|
||||||
return guild.get_member(self.data.targetid)
|
return guild.get_member(self.data.targetid)
|
||||||
@@ -111,7 +111,7 @@ class Ticket:
|
|||||||
return None
|
return None
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def type(self):
|
def type(self) -> TicketType:
|
||||||
return self.data.ticket_type
|
return self.data.ticket_type
|
||||||
|
|
||||||
@property
|
@property
|
||||||
@@ -227,10 +227,10 @@ class Ticket:
|
|||||||
name=t(_p('ticket|field:pardoned|name', "Pardoned")),
|
name=t(_p('ticket|field:pardoned|name', "Pardoned")),
|
||||||
value=t(_p(
|
value=t(_p(
|
||||||
'ticket|field:pardoned|value',
|
'ticket|field:pardoned|value',
|
||||||
"Pardoned by <&{moderator}> at {timestamp}.\n{reason}"
|
"Pardoned by <@{moderator}> at {timestamp}.\n{reason}"
|
||||||
)).format(
|
)).format(
|
||||||
moderator=data.pardoned_by,
|
moderator=data.pardoned_by,
|
||||||
timestamp=discord.utils.format_dt(timestamp),
|
timestamp=discord.utils.format_dt(data.pardoned_at) if data.pardoned_at else 'Unknown',
|
||||||
reason=data.pardoned_reason or ''
|
reason=data.pardoned_reason or ''
|
||||||
),
|
),
|
||||||
inline=False
|
inline=False
|
||||||
@@ -297,9 +297,6 @@ class Ticket:
|
|||||||
self.expiring.cancel_tasks(self.data.ticketid)
|
self.expiring.cancel_tasks(self.data.ticketid)
|
||||||
await self.post()
|
await self.post()
|
||||||
|
|
||||||
async def _revert(self):
|
|
||||||
raise NotImplementedError
|
|
||||||
|
|
||||||
async def _expire(self):
|
async def _expire(self):
|
||||||
"""
|
"""
|
||||||
Actual expiry method.
|
Actual expiry method.
|
||||||
@@ -321,11 +318,16 @@ class Ticket:
|
|||||||
await self.post()
|
await self.post()
|
||||||
# TODO: Post an extra note to the modlog about the expiry.
|
# TODO: Post an extra note to the modlog about the expiry.
|
||||||
|
|
||||||
async def revert(self):
|
async def revert(self, reason: Optional[str] = None, **kwargs):
|
||||||
"""
|
"""
|
||||||
Revert this ticket.
|
Revert this ticket.
|
||||||
|
|
||||||
|
By default this is a no-op.
|
||||||
|
Ticket types should override to implement any required revert logic.
|
||||||
|
|
||||||
|
The optional `reason` paramter is intended for any auditable actions.
|
||||||
"""
|
"""
|
||||||
raise NotImplementedError
|
return
|
||||||
|
|
||||||
async def expire(self):
|
async def expire(self):
|
||||||
"""
|
"""
|
||||||
@@ -336,5 +338,31 @@ class Ticket:
|
|||||||
"""
|
"""
|
||||||
await self._expire()
|
await self._expire()
|
||||||
|
|
||||||
async def pardon(self):
|
async def pardon(self, modid: int, reason: str):
|
||||||
raise NotImplementedError
|
"""
|
||||||
|
Pardon a ticket.
|
||||||
|
|
||||||
|
Specifically, set the state of the ticket to `PARDONED`,
|
||||||
|
with the given moderator and reason,
|
||||||
|
and revert the ticket if applicable.
|
||||||
|
|
||||||
|
If the ticket is already pardoned, this is a no-op.
|
||||||
|
"""
|
||||||
|
if self.data.ticket_state != TicketState.PARDONED:
|
||||||
|
# Cancel expiry if it was scheduled
|
||||||
|
self.expiring.cancel_tasks(self.data.ticketid)
|
||||||
|
|
||||||
|
# Revert the ticket if it is currently active
|
||||||
|
if self.data.ticket_state in (TicketState.OPEN, TicketState.EXPIRING):
|
||||||
|
await self.revert(reason=f"Pardoned by {modid}")
|
||||||
|
|
||||||
|
# Set pardoned state
|
||||||
|
await self.data.update(
|
||||||
|
ticket_state=TicketState.PARDONED,
|
||||||
|
pardoned_at=utc_now(),
|
||||||
|
pardoned_by=modid,
|
||||||
|
pardoned_reason=reason
|
||||||
|
)
|
||||||
|
|
||||||
|
# Update ticket log message
|
||||||
|
await self.post()
|
||||||
|
|||||||
2
src/modules/moderation/tickets/__init__.py
Normal file
2
src/modules/moderation/tickets/__init__.py
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
from .note import NoteTicket
|
||||||
|
from .warning import WarnTicket
|
||||||
48
src/modules/moderation/tickets/note.py
Normal file
48
src/modules/moderation/tickets/note.py
Normal file
@@ -0,0 +1,48 @@
|
|||||||
|
from typing import TYPE_CHECKING
|
||||||
|
import datetime as dt
|
||||||
|
|
||||||
|
import discord
|
||||||
|
from meta import LionBot
|
||||||
|
from utils.lib import utc_now
|
||||||
|
|
||||||
|
from ..ticket import Ticket, ticket_factory
|
||||||
|
from ..data import TicketType, TicketState, ModerationData
|
||||||
|
from .. import logger, babel
|
||||||
|
|
||||||
|
if TYPE_CHECKING:
|
||||||
|
from ..cog import ModerationCog
|
||||||
|
|
||||||
|
_p = babel._p
|
||||||
|
|
||||||
|
|
||||||
|
@ticket_factory(TicketType.NOTE)
|
||||||
|
class NoteTicket(Ticket):
|
||||||
|
__slots__ = ()
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
async def create(
|
||||||
|
cls, bot: LionBot, guildid: int, userid: int,
|
||||||
|
moderatorid: int, content: str, expiry=None,
|
||||||
|
**kwargs
|
||||||
|
):
|
||||||
|
modcog: 'ModerationCog' = bot.get_cog('ModerationCog')
|
||||||
|
ticket_data = await modcog.data.Ticket.create(
|
||||||
|
guildid=guildid,
|
||||||
|
targetid=userid,
|
||||||
|
ticket_type=TicketType.NOTE,
|
||||||
|
ticket_state=TicketState.OPEN,
|
||||||
|
moderator_id=moderatorid,
|
||||||
|
content=content,
|
||||||
|
expiry=expiry,
|
||||||
|
created_at=utc_now().replace(tzinfo=None),
|
||||||
|
**kwargs
|
||||||
|
)
|
||||||
|
|
||||||
|
lguild = await bot.core.lions.fetch_guild(guildid)
|
||||||
|
new_ticket = cls(lguild, ticket_data)
|
||||||
|
await new_ticket.post()
|
||||||
|
|
||||||
|
if expiry:
|
||||||
|
cls.expiring.schedule_task(ticket_data.ticketid, expiry.timestamp())
|
||||||
|
|
||||||
|
return new_ticket
|
||||||
66
src/modules/moderation/tickets/warning.py
Normal file
66
src/modules/moderation/tickets/warning.py
Normal file
@@ -0,0 +1,66 @@
|
|||||||
|
from typing import TYPE_CHECKING, Optional
|
||||||
|
import datetime as dt
|
||||||
|
|
||||||
|
import discord
|
||||||
|
from meta import LionBot
|
||||||
|
from utils.lib import utc_now
|
||||||
|
|
||||||
|
from ..ticket import Ticket, ticket_factory
|
||||||
|
from ..data import TicketType, TicketState, ModerationData
|
||||||
|
from .. import logger, babel
|
||||||
|
|
||||||
|
if TYPE_CHECKING:
|
||||||
|
from ..cog import ModerationCog
|
||||||
|
|
||||||
|
_p = babel._p
|
||||||
|
|
||||||
|
|
||||||
|
@ticket_factory(TicketType.WARNING)
|
||||||
|
class WarnTicket(Ticket):
|
||||||
|
__slots__ = ()
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
async def create(
|
||||||
|
cls, bot: LionBot, guildid: int, userid: int,
|
||||||
|
moderatorid: int, content: Optional[str], expiry=None,
|
||||||
|
**kwargs
|
||||||
|
):
|
||||||
|
modcog: 'ModerationCog' = bot.get_cog('ModerationCog')
|
||||||
|
ticket_data = await modcog.data.Ticket.create(
|
||||||
|
guildid=guildid,
|
||||||
|
targetid=userid,
|
||||||
|
ticket_type=TicketType.WARNING,
|
||||||
|
ticket_state=TicketState.OPEN,
|
||||||
|
moderator_id=moderatorid,
|
||||||
|
content=content,
|
||||||
|
expiry=expiry,
|
||||||
|
created_at=utc_now().replace(tzinfo=None),
|
||||||
|
**kwargs
|
||||||
|
)
|
||||||
|
|
||||||
|
lguild = await bot.core.lions.fetch_guild(guildid)
|
||||||
|
new_ticket = cls(lguild, ticket_data)
|
||||||
|
await new_ticket.post()
|
||||||
|
|
||||||
|
if expiry:
|
||||||
|
cls.expiring.schedule_task(ticket_data.ticketid, expiry.timestamp())
|
||||||
|
|
||||||
|
return new_ticket
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
async def count_warnings_for(
|
||||||
|
cls, bot: LionBot, guildid: int, userid: int, **kwargs
|
||||||
|
):
|
||||||
|
modcog: 'ModerationCog' = bot.get_cog('ModerationCog')
|
||||||
|
Ticket = modcog.data.Ticket
|
||||||
|
record = await Ticket.table.select_one_where(
|
||||||
|
(Ticket.ticket_state != TicketState.PARDONED),
|
||||||
|
guildid=guildid,
|
||||||
|
targetid=userid,
|
||||||
|
ticket_type=TicketType.WARNING,
|
||||||
|
**kwargs
|
||||||
|
).select(ticket_count='COUNT(*)').with_no_adapter()
|
||||||
|
return (record[0]['ticket_count'] or 0) if record else 0
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
656
src/modules/moderation/ticketui.py
Normal file
656
src/modules/moderation/ticketui.py
Normal file
@@ -0,0 +1,656 @@
|
|||||||
|
from itertools import chain
|
||||||
|
from typing import Optional
|
||||||
|
from dataclasses import dataclass
|
||||||
|
import asyncio
|
||||||
|
import datetime as dt
|
||||||
|
|
||||||
|
import discord
|
||||||
|
from discord.ui.select import select, Select, SelectOption, UserSelect
|
||||||
|
from discord.ui.button import button, Button, ButtonStyle
|
||||||
|
from discord.ui.text_input import TextInput, TextStyle
|
||||||
|
|
||||||
|
from meta import LionBot, conf
|
||||||
|
from meta.errors import ResponseTimedOut, SafeCancellation, UserInputError
|
||||||
|
from data import ORDER, Condition
|
||||||
|
|
||||||
|
from utils.ui import MessageUI, input
|
||||||
|
from utils.lib import MessageArgs, tabulate, utc_now
|
||||||
|
|
||||||
|
from . import babel, logger
|
||||||
|
from .ticket import Ticket
|
||||||
|
from .data import ModerationData, TicketType, TicketState
|
||||||
|
|
||||||
|
_p = babel._p
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class TicketFilter:
|
||||||
|
bot: LionBot
|
||||||
|
|
||||||
|
after: Optional[dt.datetime] = None
|
||||||
|
before: Optional[dt.datetime] = None
|
||||||
|
targetids: Optional[list[int]] = None
|
||||||
|
moderatorids: Optional[list[int]] = None
|
||||||
|
types: Optional[list[TicketType]] = None
|
||||||
|
states: Optional[list[TicketState]] = None
|
||||||
|
|
||||||
|
def conditions(self) -> list[Condition]:
|
||||||
|
conditions = []
|
||||||
|
Ticket = ModerationData.Ticket
|
||||||
|
|
||||||
|
if self.after is not None:
|
||||||
|
conditions.append(Ticket.created_at >= self.after)
|
||||||
|
if self.before is not None:
|
||||||
|
conditions.append(Ticket.created_at < self.before)
|
||||||
|
if self.targetids is not None:
|
||||||
|
conditions.append(Ticket.targetid == self.targetids)
|
||||||
|
if self.moderatorids is not None:
|
||||||
|
conditions.append(Ticket.moderator_id == self.moderatorids)
|
||||||
|
if self.types is not None:
|
||||||
|
conditions.append(Ticket.ticket_type == self.types)
|
||||||
|
if self.states is not None:
|
||||||
|
conditions.append(Ticket.ticket_state == self.states)
|
||||||
|
|
||||||
|
return conditions
|
||||||
|
|
||||||
|
def formatted(self) -> str:
|
||||||
|
t = self.bot.translator.t
|
||||||
|
lines = []
|
||||||
|
|
||||||
|
if self.after is not None:
|
||||||
|
name = t(_p(
|
||||||
|
'ticketfilter|field:after|name',
|
||||||
|
"Created After"
|
||||||
|
))
|
||||||
|
value = discord.utils.format_dt(self.after, 'd')
|
||||||
|
lines.append((name, value))
|
||||||
|
|
||||||
|
if self.before is not None:
|
||||||
|
name = t(_p(
|
||||||
|
'ticketfilter|field:before|name',
|
||||||
|
"Created Before"
|
||||||
|
))
|
||||||
|
value = discord.utils.format_dt(self.before, 'd')
|
||||||
|
lines.append((name, value))
|
||||||
|
|
||||||
|
if self.targetids is not None:
|
||||||
|
name = t(_p(
|
||||||
|
'ticketfilter|field:targetids|name',
|
||||||
|
"Targets"
|
||||||
|
))
|
||||||
|
value = ', '.join(f"<@{uid}>" for uid in self.targetids) or 'None'
|
||||||
|
lines.append((name, value))
|
||||||
|
|
||||||
|
if self.moderatorids is not None:
|
||||||
|
name = t(_p(
|
||||||
|
'ticketfilter|field:moderatorids|name',
|
||||||
|
"Moderators"
|
||||||
|
))
|
||||||
|
value = ', '.join(f"<@{uid}>" for uid in self.moderatorids) or 'None'
|
||||||
|
lines.append((name, value))
|
||||||
|
|
||||||
|
if self.types is not None:
|
||||||
|
name = t(_p(
|
||||||
|
'ticketfilter|field:types|name',
|
||||||
|
"Ticket Types"
|
||||||
|
))
|
||||||
|
value = ', '.join(typ.name for typ in self.types) or 'None'
|
||||||
|
lines.append((name, value))
|
||||||
|
|
||||||
|
if self.states is not None:
|
||||||
|
name = t(_p(
|
||||||
|
'ticketfilter|field:states|name',
|
||||||
|
"Ticket States"
|
||||||
|
))
|
||||||
|
value = ', '.join(state.name for state in self.states) or 'None'
|
||||||
|
lines.append((name, value))
|
||||||
|
|
||||||
|
if lines:
|
||||||
|
table = tabulate(*lines)
|
||||||
|
filterstr = '\n'.join(table)
|
||||||
|
else:
|
||||||
|
filterstr = ''
|
||||||
|
|
||||||
|
return filterstr
|
||||||
|
|
||||||
|
|
||||||
|
class TicketListUI(MessageUI):
|
||||||
|
block_len = 10
|
||||||
|
|
||||||
|
def _init_children(self):
|
||||||
|
# HACK to stop ViewWeights complaining that this UI has too many children
|
||||||
|
# Children will be correctly initialised after parent init.
|
||||||
|
return []
|
||||||
|
|
||||||
|
def __init__(self, bot: LionBot, guild: discord.Guild, callerid: int, filters=None, **kwargs):
|
||||||
|
super().__init__(callerid=callerid, **kwargs)
|
||||||
|
self._children = super()._init_children()
|
||||||
|
|
||||||
|
self.bot = bot
|
||||||
|
self.data: ModerationData = bot.db.registries[ModerationData.__name__]
|
||||||
|
self.guild = guild
|
||||||
|
self.filters = filters or TicketFilter(bot)
|
||||||
|
|
||||||
|
# Paging state
|
||||||
|
self._pagen = 0
|
||||||
|
self.blocks = [[]]
|
||||||
|
|
||||||
|
# UI State
|
||||||
|
self.show_filters = False
|
||||||
|
self.show_tickets = False
|
||||||
|
|
||||||
|
self.child_ticket: Optional[TicketUI] = None
|
||||||
|
|
||||||
|
@property
|
||||||
|
def page_count(self):
|
||||||
|
return len(self.blocks)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def pagen(self):
|
||||||
|
self._pagen = self._pagen % self.page_count
|
||||||
|
return self._pagen
|
||||||
|
|
||||||
|
@pagen.setter
|
||||||
|
def pagen(self, value):
|
||||||
|
self._pagen = value % self.page_count
|
||||||
|
|
||||||
|
@property
|
||||||
|
def current_page(self):
|
||||||
|
return self.blocks[self.pagen]
|
||||||
|
|
||||||
|
# ----- API -----
|
||||||
|
|
||||||
|
# ----- UI Components -----
|
||||||
|
# Edit Filters
|
||||||
|
@button(
|
||||||
|
label="EDIT_FILTER_BUTTON_PLACEHOLDER",
|
||||||
|
style=ButtonStyle.blurple
|
||||||
|
)
|
||||||
|
async def edit_filter_button(self, press: discord.Interaction, pressed: Button):
|
||||||
|
await press.response.defer(thinking=True, ephemeral=True)
|
||||||
|
self.show_filters = True
|
||||||
|
self.show_tickets = False
|
||||||
|
await self.refresh(thinking=press)
|
||||||
|
|
||||||
|
async def edit_filter_button_refresh(self):
|
||||||
|
button = self.edit_filter_button
|
||||||
|
t = self.bot.translator.t
|
||||||
|
button.label = t(_p(
|
||||||
|
'ui:tickets|button:edit_filter|label',
|
||||||
|
"Edit Filters"
|
||||||
|
))
|
||||||
|
button.style = ButtonStyle.grey if not self.show_filters else ButtonStyle.blurple
|
||||||
|
|
||||||
|
# Select Ticket
|
||||||
|
@button(
|
||||||
|
label="SELECT_TICKET_BUTTON_PLACEHOLDER",
|
||||||
|
style=ButtonStyle.blurple
|
||||||
|
)
|
||||||
|
async def select_ticket_button(self, press: discord.Interaction, pressed: Button):
|
||||||
|
await press.response.defer(thinking=True, ephemeral=True)
|
||||||
|
self.show_tickets = True
|
||||||
|
self.show_filters = False
|
||||||
|
await self.refresh(thinking=press)
|
||||||
|
|
||||||
|
async def select_ticket_button_refresh(self):
|
||||||
|
button = self.select_ticket_button
|
||||||
|
t = self.bot.translator.t
|
||||||
|
button.label = t(_p(
|
||||||
|
'ui:tickets|button:select_ticket|label',
|
||||||
|
"Select Ticket"
|
||||||
|
))
|
||||||
|
button.style = ButtonStyle.grey if not self.show_tickets else ButtonStyle.blurple
|
||||||
|
|
||||||
|
# Pardon All
|
||||||
|
@button(
|
||||||
|
label="PARDON_BUTTON_PLACEHOLDER",
|
||||||
|
style=ButtonStyle.red
|
||||||
|
)
|
||||||
|
async def pardon_button(self, press: discord.Interaction, pressed: Button):
|
||||||
|
t = self.bot.translator.t
|
||||||
|
|
||||||
|
tickets = list(chain(*self.blocks))
|
||||||
|
if not tickets:
|
||||||
|
raise UserInputError(t(_p(
|
||||||
|
'ui:tickets|button:pardon|error:no_tickets',
|
||||||
|
"Not tickets matching the given criterial! Nothing to pardon."
|
||||||
|
)))
|
||||||
|
|
||||||
|
# Request reason via modal
|
||||||
|
modal_title = t(_p(
|
||||||
|
'ui:tickets|button:pardon|modal:reason|title',
|
||||||
|
"Pardon Tickets"
|
||||||
|
))
|
||||||
|
input_field = TextInput(
|
||||||
|
label=t(_p(
|
||||||
|
'ui:tickets|button:pardon|modal:reason|field|label',
|
||||||
|
"Why are you pardoning these tickets?"
|
||||||
|
)),
|
||||||
|
style=TextStyle.long,
|
||||||
|
min_length=0,
|
||||||
|
max_length=1024,
|
||||||
|
)
|
||||||
|
try:
|
||||||
|
interaction, reason = await input(
|
||||||
|
press, modal_title, field=input_field, timeout=300,
|
||||||
|
)
|
||||||
|
except asyncio.TimeoutError:
|
||||||
|
raise ResponseTimedOut
|
||||||
|
|
||||||
|
await interaction.response.defer(thinking=True, ephemeral=True)
|
||||||
|
|
||||||
|
# Run pardon
|
||||||
|
for ticket in tickets:
|
||||||
|
await ticket.pardon(modid=press.user.id, reason=reason)
|
||||||
|
|
||||||
|
await self.refresh(thinking=interaction)
|
||||||
|
|
||||||
|
async def pardon_button_refresh(self):
|
||||||
|
button = self.pardon_button
|
||||||
|
t = self.bot.translator.t
|
||||||
|
button.label = t(_p(
|
||||||
|
'ui:tickets|button:pardon|label',
|
||||||
|
"Pardon All"
|
||||||
|
))
|
||||||
|
button.disabled = not bool(self.current_page)
|
||||||
|
|
||||||
|
# Filter Ticket Type
|
||||||
|
@select(
|
||||||
|
cls=Select,
|
||||||
|
placeholder="FILTER_TYPE_MENU_PLACEHOLDER",
|
||||||
|
min_values=1, max_values=3,
|
||||||
|
)
|
||||||
|
async def filter_type_menu(self, selection: discord.Interaction, selected: Select):
|
||||||
|
await selection.response.defer(thinking=True, ephemeral=True)
|
||||||
|
self.filters.types = [TicketType[value] for value in selected.values] or None
|
||||||
|
self.pagen = 0
|
||||||
|
await self.refresh(thinking=selection)
|
||||||
|
|
||||||
|
async def filter_type_menu_refresh(self):
|
||||||
|
menu = self.filter_type_menu
|
||||||
|
t = self.bot.translator.t
|
||||||
|
menu.placeholder = t(_p(
|
||||||
|
'ui:tickets|menu:filter_type|placeholder',
|
||||||
|
"Select Ticket Types"
|
||||||
|
))
|
||||||
|
|
||||||
|
options = []
|
||||||
|
descmap = {
|
||||||
|
TicketType.NOTE: ('Notes',),
|
||||||
|
TicketType.WARNING: ('Warnings',),
|
||||||
|
TicketType.STUDY_BAN: ('Video Blacklists',),
|
||||||
|
}
|
||||||
|
filtered = self.filters.types
|
||||||
|
for typ, (name,) in descmap.items():
|
||||||
|
option = SelectOption(
|
||||||
|
label=name,
|
||||||
|
value=typ.name,
|
||||||
|
default=(filtered is None or typ in filtered)
|
||||||
|
)
|
||||||
|
options.append(option)
|
||||||
|
menu.options = options
|
||||||
|
|
||||||
|
# Filter Ticket State
|
||||||
|
@select(
|
||||||
|
cls=Select,
|
||||||
|
placeholder="FILTER_STATE_MENU_PLACEHOLDER",
|
||||||
|
min_values=1, max_values=4
|
||||||
|
)
|
||||||
|
async def filter_state_menu(self, selection: discord.Interaction, selected: Select):
|
||||||
|
await selection.response.defer(thinking=True, ephemeral=True)
|
||||||
|
self.filters.states = [TicketState[value] for value in selected.values] or None
|
||||||
|
self.pagen = 0
|
||||||
|
await self.refresh(thinking=selection)
|
||||||
|
|
||||||
|
async def filter_state_menu_refresh(self):
|
||||||
|
menu = self.filter_state_menu
|
||||||
|
t = self.bot.translator.t
|
||||||
|
menu.placeholder = t(_p(
|
||||||
|
'ui:tickets|menu:filter_state|placeholder',
|
||||||
|
"Select Ticket States"
|
||||||
|
))
|
||||||
|
|
||||||
|
options = []
|
||||||
|
descmap = {
|
||||||
|
TicketState.OPEN: ('OPEN', ),
|
||||||
|
TicketState.EXPIRING: ('EXPIRING', ),
|
||||||
|
TicketState.EXPIRED: ('EXPIRED', ),
|
||||||
|
TicketState.PARDONED: ('PARDONED', ),
|
||||||
|
}
|
||||||
|
filtered = self.filters.states
|
||||||
|
for state, (name,) in descmap.items():
|
||||||
|
option = SelectOption(
|
||||||
|
label=name,
|
||||||
|
value=state.name,
|
||||||
|
default=(filtered is None or state in filtered)
|
||||||
|
)
|
||||||
|
options.append(option)
|
||||||
|
menu.options = options
|
||||||
|
|
||||||
|
# Filter Ticket Target
|
||||||
|
@select(
|
||||||
|
cls=UserSelect,
|
||||||
|
placeholder="FILTER_TARGET_MENU_PLACEHOLDER",
|
||||||
|
min_values=0, max_values=10
|
||||||
|
)
|
||||||
|
async def filter_target_menu(self, selection: discord.Interaction, selected: UserSelect):
|
||||||
|
await selection.response.defer(thinking=True, ephemeral=True)
|
||||||
|
self.filters.targetids = [user.id for user in selected.values] or None
|
||||||
|
self.pagen = 0
|
||||||
|
await self.refresh(thinking=selection)
|
||||||
|
|
||||||
|
async def filter_target_menu_refresh(self):
|
||||||
|
menu = self.filter_target_menu
|
||||||
|
t = self.bot.translator.t
|
||||||
|
menu.placeholder = t(_p(
|
||||||
|
'ui:tickets|menu:filter_target|placeholder',
|
||||||
|
"Select Ticket Targets"
|
||||||
|
))
|
||||||
|
|
||||||
|
# Select Ticket
|
||||||
|
@select(
|
||||||
|
cls=Select,
|
||||||
|
placeholder="TICKETS_MENU_PLACEHOLDER",
|
||||||
|
min_values=1, max_values=1
|
||||||
|
)
|
||||||
|
async def tickets_menu(self, selection: discord.Interaction, selected: Select):
|
||||||
|
await selection.response.defer(thinking=True, ephemeral=True)
|
||||||
|
if selected.values:
|
||||||
|
ticketid = int(selected.values[0])
|
||||||
|
ticket = await Ticket.fetch_ticket(self.bot, ticketid)
|
||||||
|
ticketui = TicketUI(self.bot, ticket, self._callerid)
|
||||||
|
if self.child_ticket:
|
||||||
|
await self.child_ticket.quit()
|
||||||
|
self.child_ticket = ticketui
|
||||||
|
await ticketui.run(selection)
|
||||||
|
|
||||||
|
async def tickets_menu_refresh(self):
|
||||||
|
menu = self.tickets_menu
|
||||||
|
t = self.bot.translator.t
|
||||||
|
menu.placeholder = t(_p(
|
||||||
|
'ui:tickets|menu:tickets|placeholder',
|
||||||
|
"Select Ticket"
|
||||||
|
))
|
||||||
|
options = []
|
||||||
|
for ticket in self.current_page:
|
||||||
|
option = SelectOption(
|
||||||
|
label=f"Ticket #{ticket.data.guild_ticketid}",
|
||||||
|
value=str(ticket.data.ticketid)
|
||||||
|
)
|
||||||
|
options.append(option)
|
||||||
|
menu.options = options
|
||||||
|
|
||||||
|
# Backwards
|
||||||
|
@button(emoji=conf.emojis.backward, style=ButtonStyle.grey)
|
||||||
|
async def prev_button(self, press: discord.Interaction, pressed: Button):
|
||||||
|
await press.response.defer(thinking=True, ephemeral=True)
|
||||||
|
self.pagen -= 1
|
||||||
|
await self.refresh(thinking=press)
|
||||||
|
|
||||||
|
# Jump to page
|
||||||
|
@button(label="JUMP_PLACEHOLDER", style=ButtonStyle.blurple)
|
||||||
|
async def jump_button(self, press: discord.Interaction, pressed: Button):
|
||||||
|
"""
|
||||||
|
Jump-to-page button.
|
||||||
|
Loads a page-switch dialogue.
|
||||||
|
"""
|
||||||
|
t = self.bot.translator.t
|
||||||
|
try:
|
||||||
|
interaction, value = await input(
|
||||||
|
press,
|
||||||
|
title=t(_p(
|
||||||
|
'ui:tickets|button:jump|input:title',
|
||||||
|
"Jump to page"
|
||||||
|
)),
|
||||||
|
question=t(_p(
|
||||||
|
'ui:tickets|button:jump|input:question',
|
||||||
|
"Page number to jump to"
|
||||||
|
))
|
||||||
|
)
|
||||||
|
value = value.strip()
|
||||||
|
except asyncio.TimeoutError:
|
||||||
|
return
|
||||||
|
|
||||||
|
if not value.lstrip('- ').isdigit():
|
||||||
|
error_embed = discord.Embed(
|
||||||
|
title=t(_p(
|
||||||
|
'ui:tickets|button:jump|error:invalid_page',
|
||||||
|
"Invalid page number, please try again!"
|
||||||
|
)),
|
||||||
|
colour=discord.Colour.brand_red()
|
||||||
|
)
|
||||||
|
await interaction.response.send_message(embed=error_embed, ephemeral=True)
|
||||||
|
else:
|
||||||
|
await interaction.response.defer(thinking=True)
|
||||||
|
pagen = int(value.lstrip('- '))
|
||||||
|
if value.startswith('-'):
|
||||||
|
pagen = -1 * pagen
|
||||||
|
elif pagen > 0:
|
||||||
|
pagen = pagen - 1
|
||||||
|
self.pagen = pagen
|
||||||
|
await self.refresh(thinking=interaction)
|
||||||
|
|
||||||
|
async def jump_button_refresh(self):
|
||||||
|
component = self.jump_button
|
||||||
|
component.label = f"{self.pagen + 1}/{self.page_count}"
|
||||||
|
component.disabled = (self.page_count <= 1)
|
||||||
|
|
||||||
|
# Forward
|
||||||
|
@button(emoji=conf.emojis.forward, style=ButtonStyle.grey)
|
||||||
|
async def next_button(self, press: discord.Interaction, pressed: Button):
|
||||||
|
await press.response.defer(thinking=True)
|
||||||
|
self.pagen += 1
|
||||||
|
await self.refresh(thinking=press)
|
||||||
|
|
||||||
|
# Quit
|
||||||
|
@button(emoji=conf.emojis.cancel, style=ButtonStyle.red)
|
||||||
|
async def quit_button(self, press: discord.Interaction, pressed: Button):
|
||||||
|
"""
|
||||||
|
Quit the UI.
|
||||||
|
"""
|
||||||
|
await press.response.defer()
|
||||||
|
if self.child_ticket:
|
||||||
|
await self.child_ticket.quit()
|
||||||
|
await self.quit()
|
||||||
|
|
||||||
|
# ----- UI Flow -----
|
||||||
|
def _format_ticket(self, ticket) -> str:
|
||||||
|
"""
|
||||||
|
Format a ticket into a single embed line.
|
||||||
|
"""
|
||||||
|
components = (
|
||||||
|
"[#{ticketid}]({link})",
|
||||||
|
"{created}",
|
||||||
|
"`{type}[{state}]`",
|
||||||
|
"<@{targetid}>",
|
||||||
|
"{content}",
|
||||||
|
)
|
||||||
|
|
||||||
|
formatstr = ' | '.join(components)
|
||||||
|
|
||||||
|
data = ticket.data
|
||||||
|
if not data.content:
|
||||||
|
content = 'No Content'
|
||||||
|
elif len(data.content) > 100:
|
||||||
|
content = data.content[:97] + '...'
|
||||||
|
else:
|
||||||
|
content = data.content
|
||||||
|
|
||||||
|
ticketstr = formatstr.format(
|
||||||
|
ticketid=data.guild_ticketid,
|
||||||
|
link=ticket.jump_url or 'https://lionbot.org',
|
||||||
|
created=discord.utils.format_dt(data.created_at, 'd'),
|
||||||
|
type=data.ticket_type.name,
|
||||||
|
state=data.ticket_state.name,
|
||||||
|
targetid=data.targetid,
|
||||||
|
content=content,
|
||||||
|
)
|
||||||
|
if data.ticket_state is TicketState.PARDONED:
|
||||||
|
ticketstr = f"~~{ticketstr}~~"
|
||||||
|
return ticketstr
|
||||||
|
|
||||||
|
async def make_message(self) -> MessageArgs:
|
||||||
|
t = self.bot.translator.t
|
||||||
|
embed = discord.Embed(
|
||||||
|
title=t(_p(
|
||||||
|
'ui:tickets|embed|title',
|
||||||
|
"Moderation Tickets in {guild}"
|
||||||
|
)).format(guild=self.guild.name),
|
||||||
|
timestamp=utc_now()
|
||||||
|
)
|
||||||
|
tickets = self.current_page
|
||||||
|
if tickets:
|
||||||
|
desc = '\n'.join(self._format_ticket(ticket) for ticket in tickets)
|
||||||
|
else:
|
||||||
|
desc = t(_p(
|
||||||
|
'ui:tickets|embed|desc:no_tickets',
|
||||||
|
"No tickets matching the given criteria!"
|
||||||
|
))
|
||||||
|
embed.description = desc
|
||||||
|
|
||||||
|
filterstr = self.filters.formatted()
|
||||||
|
if filterstr:
|
||||||
|
embed.add_field(
|
||||||
|
name=t(_p(
|
||||||
|
'ui:tickets|embed|field:filters|name',
|
||||||
|
"Filters"
|
||||||
|
)),
|
||||||
|
value=filterstr,
|
||||||
|
inline=False
|
||||||
|
)
|
||||||
|
|
||||||
|
return MessageArgs(embed=embed)
|
||||||
|
|
||||||
|
async def refresh_layout(self):
|
||||||
|
to_refresh = (
|
||||||
|
self.edit_filter_button_refresh(),
|
||||||
|
self.select_ticket_button_refresh(),
|
||||||
|
self.pardon_button_refresh(),
|
||||||
|
self.tickets_menu_refresh(),
|
||||||
|
self.filter_type_menu_refresh(),
|
||||||
|
self.filter_state_menu_refresh(),
|
||||||
|
self.filter_target_menu_refresh(),
|
||||||
|
self.jump_button_refresh(),
|
||||||
|
)
|
||||||
|
await asyncio.gather(*to_refresh)
|
||||||
|
|
||||||
|
action_line = (
|
||||||
|
self.edit_filter_button,
|
||||||
|
self.select_ticket_button,
|
||||||
|
self.pardon_button,
|
||||||
|
)
|
||||||
|
|
||||||
|
if self.page_count > 1:
|
||||||
|
page_line = (
|
||||||
|
self.prev_button,
|
||||||
|
self.jump_button,
|
||||||
|
self.quit_button,
|
||||||
|
self.next_button,
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
page_line = ()
|
||||||
|
action_line = (*action_line, self.quit_button)
|
||||||
|
|
||||||
|
if self.show_filters:
|
||||||
|
menus = (
|
||||||
|
(self.filter_type_menu,),
|
||||||
|
(self.filter_state_menu,),
|
||||||
|
(self.filter_target_menu,),
|
||||||
|
)
|
||||||
|
elif self.show_tickets and self.current_page:
|
||||||
|
menus = ((self.tickets_menu,),)
|
||||||
|
else:
|
||||||
|
menus = ()
|
||||||
|
|
||||||
|
self.set_layout(
|
||||||
|
action_line,
|
||||||
|
*menus,
|
||||||
|
page_line,
|
||||||
|
)
|
||||||
|
|
||||||
|
async def reload(self):
|
||||||
|
tickets = await Ticket.fetch_tickets(
|
||||||
|
self.bot,
|
||||||
|
*self.filters.conditions(),
|
||||||
|
guildid=self.guild.id,
|
||||||
|
)
|
||||||
|
blocks = [
|
||||||
|
tickets[i:i+self.block_len]
|
||||||
|
for i in range(0, len(tickets), self.block_len)
|
||||||
|
]
|
||||||
|
self.blocks = blocks or [[]]
|
||||||
|
|
||||||
|
|
||||||
|
class TicketUI(MessageUI):
|
||||||
|
def __init__(self, bot: LionBot, ticket: Ticket, callerid: int, **kwargs):
|
||||||
|
super().__init__(callerid=callerid, **kwargs)
|
||||||
|
|
||||||
|
self.bot = bot
|
||||||
|
self.ticket = ticket
|
||||||
|
|
||||||
|
# ----- API -----
|
||||||
|
|
||||||
|
# ----- UI Components -----
|
||||||
|
# Pardon Ticket
|
||||||
|
@button(
|
||||||
|
label="PARDON_BUTTON_PLACEHOLDER",
|
||||||
|
style=ButtonStyle.red
|
||||||
|
)
|
||||||
|
async def pardon_button(self, press: discord.Interaction, pressed: Button):
|
||||||
|
t = self.bot.translator.t
|
||||||
|
|
||||||
|
modal_title = t(_p(
|
||||||
|
'ui:ticket|button:pardon|modal:reason|title',
|
||||||
|
"Pardon Moderation Ticket"
|
||||||
|
))
|
||||||
|
input_field = TextInput(
|
||||||
|
label=t(_p(
|
||||||
|
'ui:ticket|button:pardon|modal:reason|field|label',
|
||||||
|
"Why are you pardoning this ticket?"
|
||||||
|
)),
|
||||||
|
style=TextStyle.long,
|
||||||
|
min_length=0,
|
||||||
|
max_length=1024,
|
||||||
|
)
|
||||||
|
try:
|
||||||
|
interaction, reason = await input(
|
||||||
|
press, modal_title, field=input_field, timeout=300,
|
||||||
|
)
|
||||||
|
except asyncio.TimeoutError:
|
||||||
|
raise ResponseTimedOut
|
||||||
|
|
||||||
|
await interaction.response.defer(thinking=True, ephemeral=True)
|
||||||
|
|
||||||
|
await self.ticket.pardon(modid=press.user.id, reason=reason)
|
||||||
|
await self.refresh(thinking=interaction)
|
||||||
|
|
||||||
|
|
||||||
|
async def pardon_button_refresh(self):
|
||||||
|
button = self.pardon_button
|
||||||
|
t = self.bot.translator.t
|
||||||
|
button.label = t(_p(
|
||||||
|
'ui:ticket|button:pardon|label',
|
||||||
|
"Pardon"
|
||||||
|
))
|
||||||
|
button.disabled = (self.ticket.data.ticket_state is TicketState.PARDONED)
|
||||||
|
|
||||||
|
# Quit
|
||||||
|
@button(emoji=conf.emojis.cancel, style=ButtonStyle.red)
|
||||||
|
async def quit_button(self, press: discord.Interaction, pressed: Button):
|
||||||
|
"""
|
||||||
|
Quit the UI.
|
||||||
|
"""
|
||||||
|
await press.response.defer()
|
||||||
|
await self.quit()
|
||||||
|
|
||||||
|
# ----- UI Flow -----
|
||||||
|
async def make_message(self) -> MessageArgs:
|
||||||
|
return await self.ticket.make_message()
|
||||||
|
|
||||||
|
async def refresh_layout(self):
|
||||||
|
await self.pardon_button_refresh()
|
||||||
|
self.set_layout(
|
||||||
|
(self.pardon_button, self.quit_button,)
|
||||||
|
)
|
||||||
|
|
||||||
|
async def reload(self):
|
||||||
|
await self.ticket.data.refresh()
|
||||||
@@ -1,7 +0,0 @@
|
|||||||
# flake8: noqa
|
|
||||||
from .module import module
|
|
||||||
|
|
||||||
from . import help
|
|
||||||
from . import links
|
|
||||||
from . import nerd
|
|
||||||
from . import join_message
|
|
||||||
@@ -1,237 +0,0 @@
|
|||||||
import discord
|
|
||||||
from cmdClient.checks import is_owner
|
|
||||||
|
|
||||||
from utils.lib import prop_tabulate
|
|
||||||
from utils import interactive, ctx_addons # noqa
|
|
||||||
from wards import is_guild_admin
|
|
||||||
|
|
||||||
from .module import module
|
|
||||||
from .lib import guide_link
|
|
||||||
|
|
||||||
|
|
||||||
new_emoji = " 🆕"
|
|
||||||
new_commands = {'botconfig', 'sponsors'}
|
|
||||||
|
|
||||||
# Set the command groups to appear in the help
|
|
||||||
group_hints = {
|
|
||||||
'Pomodoro': "*Stay in sync with your friends using our timers!*",
|
|
||||||
'Productivity': "*Use these to help you stay focused and productive!*",
|
|
||||||
'Statistics': "*StudyLion leaderboards and study statistics.*",
|
|
||||||
'Economy': "*Buy, sell, and trade with your hard-earned coins!*",
|
|
||||||
'Personal Settings': "*Tell me about yourself!*",
|
|
||||||
'Guild Admin': "*Dangerous administration commands!*",
|
|
||||||
'Guild Configuration': "*Control how I behave in your server.*",
|
|
||||||
'Meta': "*Information about me!*",
|
|
||||||
'Support Us': "*Support the team and keep the project alive by using LionGems!*"
|
|
||||||
}
|
|
||||||
|
|
||||||
standard_group_order = (
|
|
||||||
('Pomodoro', 'Productivity', 'Support Us', 'Statistics', 'Economy', 'Personal Settings', 'Meta'),
|
|
||||||
)
|
|
||||||
|
|
||||||
mod_group_order = (
|
|
||||||
('Moderation', 'Meta'),
|
|
||||||
('Pomodoro', 'Productivity', 'Support Us', 'Statistics', 'Economy', 'Personal Settings')
|
|
||||||
)
|
|
||||||
|
|
||||||
admin_group_order = (
|
|
||||||
('Guild Admin', 'Guild Configuration', 'Moderation', 'Meta'),
|
|
||||||
('Pomodoro', 'Productivity', 'Support Us', 'Statistics', 'Economy', 'Personal Settings')
|
|
||||||
)
|
|
||||||
|
|
||||||
bot_admin_group_order = (
|
|
||||||
('Bot Admin', 'Guild Admin', 'Guild Configuration', 'Moderation', 'Meta'),
|
|
||||||
('Pomodoro', 'Productivity', 'Support Us', 'Statistics', 'Economy', 'Personal Settings')
|
|
||||||
)
|
|
||||||
|
|
||||||
# Help embed format
|
|
||||||
# TODO: Add config fields for this
|
|
||||||
title = "StudyLion Command List"
|
|
||||||
header = """
|
|
||||||
[StudyLion](https://bot.studylions.com/) is a fully featured study assistant \
|
|
||||||
that tracks your study time and offers productivity tools \
|
|
||||||
such as to-do lists, task reminders, private study rooms, group accountability sessions, and much much more.\n
|
|
||||||
Use `{{ctx.best_prefix}}help <command>` (e.g. `{{ctx.best_prefix}}help send`) to learn how to use each command, \
|
|
||||||
or [click here]({guide_link}) for a comprehensive tutorial.
|
|
||||||
""".format(guide_link=guide_link)
|
|
||||||
|
|
||||||
|
|
||||||
@module.cmd("help",
|
|
||||||
group="Meta",
|
|
||||||
desc="StudyLion command list.",
|
|
||||||
aliases=('man', 'ls', 'list'))
|
|
||||||
async def cmd_help(ctx):
|
|
||||||
"""
|
|
||||||
Usage``:
|
|
||||||
{prefix}help [cmdname]
|
|
||||||
Description:
|
|
||||||
When used with no arguments, displays a list of commands with brief descriptions.
|
|
||||||
Otherwise, shows documentation for the provided command.
|
|
||||||
Examples:
|
|
||||||
{prefix}help
|
|
||||||
{prefix}help top
|
|
||||||
{prefix}help timezone
|
|
||||||
"""
|
|
||||||
if ctx.arg_str:
|
|
||||||
# Attempt to fetch the command
|
|
||||||
command = ctx.client.cmd_names.get(ctx.arg_str.strip(), None)
|
|
||||||
if command is None:
|
|
||||||
return await ctx.error_reply(
|
|
||||||
("Command `{}` not found!\n"
|
|
||||||
"Write `{}help` to see a list of commands.").format(ctx.args, ctx.best_prefix)
|
|
||||||
)
|
|
||||||
|
|
||||||
smart_help = getattr(command, 'smart_help', None)
|
|
||||||
if smart_help is not None:
|
|
||||||
return await smart_help(ctx)
|
|
||||||
|
|
||||||
help_fields = command.long_help.copy()
|
|
||||||
help_map = {field_name: i for i, (field_name, _) in enumerate(help_fields)}
|
|
||||||
|
|
||||||
if not help_map:
|
|
||||||
return await ctx.reply("No documentation has been written for this command yet!")
|
|
||||||
|
|
||||||
field_pages = [[]]
|
|
||||||
page_fields = field_pages[0]
|
|
||||||
for name, pos in help_map.items():
|
|
||||||
if name.endswith("``"):
|
|
||||||
# Handle codeline help fields
|
|
||||||
page_fields.append((
|
|
||||||
name.strip("`"),
|
|
||||||
"`{}`".format('`\n`'.join(help_fields[pos][1].splitlines()))
|
|
||||||
))
|
|
||||||
elif name.endswith(":"):
|
|
||||||
# Handle property/value help fields
|
|
||||||
lines = help_fields[pos][1].splitlines()
|
|
||||||
|
|
||||||
names = []
|
|
||||||
values = []
|
|
||||||
for line in lines:
|
|
||||||
split = line.split(":", 1)
|
|
||||||
names.append(split[0] if len(split) > 1 else "")
|
|
||||||
values.append(split[-1])
|
|
||||||
|
|
||||||
page_fields.append((
|
|
||||||
name.strip(':'),
|
|
||||||
prop_tabulate(names, values)
|
|
||||||
))
|
|
||||||
elif name == "Related":
|
|
||||||
# Handle the related field
|
|
||||||
names = [cmd_name.strip() for cmd_name in help_fields[pos][1].split(',')]
|
|
||||||
names.sort(key=len)
|
|
||||||
values = [
|
|
||||||
(getattr(ctx.client.cmd_names.get(cmd_name, None), 'desc', '') or '').format(ctx=ctx)
|
|
||||||
for cmd_name in names
|
|
||||||
]
|
|
||||||
page_fields.append((
|
|
||||||
name,
|
|
||||||
prop_tabulate(names, values)
|
|
||||||
))
|
|
||||||
elif name == "PAGEBREAK":
|
|
||||||
page_fields = []
|
|
||||||
field_pages.append(page_fields)
|
|
||||||
else:
|
|
||||||
page_fields.append((name, help_fields[pos][1]))
|
|
||||||
|
|
||||||
# Build the aliases
|
|
||||||
aliases = getattr(command, 'aliases', [])
|
|
||||||
alias_str = "(Aliases `{}`.)".format("`, `".join(aliases)) if aliases else ""
|
|
||||||
|
|
||||||
# Build the embeds
|
|
||||||
pages = []
|
|
||||||
for i, page_fields in enumerate(field_pages):
|
|
||||||
embed = discord.Embed(
|
|
||||||
title="`{}` command documentation. {}".format(
|
|
||||||
command.name,
|
|
||||||
alias_str
|
|
||||||
),
|
|
||||||
colour=discord.Colour(0x9b59b6)
|
|
||||||
)
|
|
||||||
for fieldname, fieldvalue in page_fields:
|
|
||||||
embed.add_field(
|
|
||||||
name=fieldname,
|
|
||||||
value=fieldvalue.format(ctx=ctx, prefix=ctx.best_prefix),
|
|
||||||
inline=False
|
|
||||||
)
|
|
||||||
|
|
||||||
embed.set_footer(
|
|
||||||
text="{}\n[optional] and <required> denote optional and required arguments, respectively.".format(
|
|
||||||
"Page {} of {}".format(i + 1, len(field_pages)) if len(field_pages) > 1 else '',
|
|
||||||
)
|
|
||||||
)
|
|
||||||
pages.append(embed)
|
|
||||||
|
|
||||||
# Post the embed
|
|
||||||
await ctx.pager(pages)
|
|
||||||
else:
|
|
||||||
# Build the command groups
|
|
||||||
cmd_groups = {}
|
|
||||||
for command in ctx.client.cmds:
|
|
||||||
# Get the command group
|
|
||||||
group = getattr(command, 'group', "Misc")
|
|
||||||
cmd_group = cmd_groups.get(group, [])
|
|
||||||
if not cmd_group:
|
|
||||||
cmd_groups[group] = cmd_group
|
|
||||||
|
|
||||||
# Add the command name and description to the group
|
|
||||||
cmd_group.append(
|
|
||||||
(command.name, (getattr(command, 'desc', '') + (new_emoji if command.name in new_commands else '')))
|
|
||||||
)
|
|
||||||
|
|
||||||
# Add any required aliases
|
|
||||||
for alias, desc in getattr(command, 'help_aliases', {}).items():
|
|
||||||
cmd_group.append((alias, desc))
|
|
||||||
|
|
||||||
# Turn the command groups into strings
|
|
||||||
stringy_cmd_groups = {}
|
|
||||||
for group_name, cmd_group in cmd_groups.items():
|
|
||||||
cmd_group.sort(key=lambda tup: len(tup[0]))
|
|
||||||
if ctx.alias == 'ls':
|
|
||||||
stringy_cmd_groups[group_name] = ', '.join(
|
|
||||||
f"`{name}`" for name, _ in cmd_group
|
|
||||||
)
|
|
||||||
else:
|
|
||||||
stringy_cmd_groups[group_name] = prop_tabulate(*zip(*cmd_group))
|
|
||||||
|
|
||||||
# Now put everything into a bunch of embeds
|
|
||||||
if await is_owner.run(ctx):
|
|
||||||
group_order = bot_admin_group_order
|
|
||||||
elif ctx.guild:
|
|
||||||
if is_guild_admin(ctx.author):
|
|
||||||
group_order = admin_group_order
|
|
||||||
elif ctx.guild_settings.mod_role.value in ctx.author.roles:
|
|
||||||
group_order = mod_group_order
|
|
||||||
else:
|
|
||||||
group_order = standard_group_order
|
|
||||||
else:
|
|
||||||
group_order = admin_group_order
|
|
||||||
|
|
||||||
help_embeds = []
|
|
||||||
for page_groups in group_order:
|
|
||||||
embed = discord.Embed(
|
|
||||||
description=header.format(ctx=ctx),
|
|
||||||
colour=discord.Colour(0x9b59b6),
|
|
||||||
title=title
|
|
||||||
)
|
|
||||||
for group in page_groups:
|
|
||||||
group_hint = group_hints.get(group, '').format(ctx=ctx)
|
|
||||||
group_str = stringy_cmd_groups.get(group, None)
|
|
||||||
if group_str:
|
|
||||||
embed.add_field(
|
|
||||||
name=group,
|
|
||||||
value="{}\n{}".format(group_hint, group_str).format(ctx=ctx),
|
|
||||||
inline=False
|
|
||||||
)
|
|
||||||
help_embeds.append(embed)
|
|
||||||
|
|
||||||
# Add the page numbers
|
|
||||||
for i, embed in enumerate(help_embeds):
|
|
||||||
embed.set_footer(text="Page {}/{}".format(i+1, len(help_embeds)))
|
|
||||||
|
|
||||||
# Send the embeds
|
|
||||||
if help_embeds:
|
|
||||||
await ctx.pager(help_embeds)
|
|
||||||
else:
|
|
||||||
await ctx.reply(
|
|
||||||
embed=discord.Embed(description=header, colour=discord.Colour(0x9b59b6))
|
|
||||||
)
|
|
||||||
@@ -1,50 +0,0 @@
|
|||||||
import discord
|
|
||||||
|
|
||||||
from cmdClient import cmdClient
|
|
||||||
|
|
||||||
from meta import client, conf
|
|
||||||
from .lib import guide_link, animation_link
|
|
||||||
|
|
||||||
|
|
||||||
message = """
|
|
||||||
Thank you for inviting me to your community.
|
|
||||||
Get started by typing `{prefix}help` to see my commands, and `{prefix}config info` \
|
|
||||||
to read about my configuration options!
|
|
||||||
|
|
||||||
To learn how to configure me and use all of my features, \
|
|
||||||
make sure to [click here]({guide_link}) to read our full setup guide.
|
|
||||||
|
|
||||||
Remember, if you need any help configuring me, \
|
|
||||||
want to suggest a feature, report a bug and stay updated, \
|
|
||||||
make sure to join our main support and study server by [clicking here]({support_link}).
|
|
||||||
|
|
||||||
Best of luck with your studies!
|
|
||||||
|
|
||||||
""".format(
|
|
||||||
guide_link=guide_link,
|
|
||||||
support_link=conf.bot.get('support_link'),
|
|
||||||
prefix=client.prefix
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
@client.add_after_event('guild_join', priority=0)
|
|
||||||
async def post_join_message(client: cmdClient, guild: discord.Guild):
|
|
||||||
try:
|
|
||||||
await guild.me.edit(nick="Leo")
|
|
||||||
except discord.HTTPException:
|
|
||||||
pass
|
|
||||||
if (channel := guild.system_channel) and channel.permissions_for(guild.me).embed_links:
|
|
||||||
embed = discord.Embed(
|
|
||||||
description=message
|
|
||||||
)
|
|
||||||
embed.set_author(
|
|
||||||
name="Hello everyone! My name is Leo, the StudyLion!",
|
|
||||||
icon_url="https://cdn.discordapp.com/emojis/933610591459872868.webp"
|
|
||||||
)
|
|
||||||
embed.set_image(url=animation_link)
|
|
||||||
try:
|
|
||||||
await channel.send(embed=embed)
|
|
||||||
except discord.HTTPException:
|
|
||||||
# Something went wrong sending the hi message
|
|
||||||
# Not much we can do about this
|
|
||||||
pass
|
|
||||||
@@ -1,5 +0,0 @@
|
|||||||
guide_link = "https://discord.studylions.com/tutorial"
|
|
||||||
|
|
||||||
animation_link = (
|
|
||||||
"https://media.discordapp.net/attachments/879412267731542047/926837189814419486/ezgif.com-resize.gif"
|
|
||||||
)
|
|
||||||
@@ -1,57 +0,0 @@
|
|||||||
import discord
|
|
||||||
|
|
||||||
from meta import conf
|
|
||||||
|
|
||||||
from LionContext import LionContext as Context
|
|
||||||
|
|
||||||
from .module import module
|
|
||||||
from .lib import guide_link
|
|
||||||
|
|
||||||
|
|
||||||
@module.cmd(
|
|
||||||
"support",
|
|
||||||
group="Meta",
|
|
||||||
desc=f"Have a question? Join my [support server]({conf.bot.get('support_link')})"
|
|
||||||
)
|
|
||||||
async def cmd_support(ctx: Context):
|
|
||||||
"""
|
|
||||||
Usage``:
|
|
||||||
{prefix}support
|
|
||||||
Description:
|
|
||||||
Replies with an invite link to my support server.
|
|
||||||
"""
|
|
||||||
await ctx.reply(
|
|
||||||
f"Click here to join my support server: {conf.bot.get('support_link')}"
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
@module.cmd(
|
|
||||||
"invite",
|
|
||||||
group="Meta",
|
|
||||||
desc=f"[Invite me]({conf.bot.get('invite_link')}) to your server so I can help your members stay productive!"
|
|
||||||
)
|
|
||||||
async def cmd_invite(ctx: Context):
|
|
||||||
"""
|
|
||||||
Usage``:
|
|
||||||
{prefix}invite
|
|
||||||
Description:
|
|
||||||
Replies with my invite link so you can add me to your server.
|
|
||||||
"""
|
|
||||||
embed = discord.Embed(
|
|
||||||
colour=discord.Colour.orange(),
|
|
||||||
description=f"Click here to add me to your server: {conf.bot.get('invite_link')}"
|
|
||||||
)
|
|
||||||
embed.add_field(
|
|
||||||
name="Setup tips",
|
|
||||||
value=(
|
|
||||||
"Remember to check out `{prefix}help` for the full command list, "
|
|
||||||
"and `{prefix}config info` for the configuration options.\n"
|
|
||||||
"[Click here]({guide}) for our comprehensive setup tutorial, and if you still have questions you can "
|
|
||||||
"join our support server [here]({support}) to talk to our friendly support team!"
|
|
||||||
).format(
|
|
||||||
prefix=ctx.best_prefix,
|
|
||||||
support=conf.bot.get('support_link'),
|
|
||||||
guide=guide_link
|
|
||||||
)
|
|
||||||
)
|
|
||||||
await ctx.reply(embed=embed)
|
|
||||||
@@ -1,3 +0,0 @@
|
|||||||
from LionModule import LionModule
|
|
||||||
|
|
||||||
module = LionModule("Meta")
|
|
||||||
@@ -1,144 +0,0 @@
|
|||||||
import datetime
|
|
||||||
import asyncio
|
|
||||||
import discord
|
|
||||||
import psutil
|
|
||||||
import sys
|
|
||||||
import gc
|
|
||||||
|
|
||||||
from data import NOTNULL
|
|
||||||
from data.queries import select_where
|
|
||||||
from utils.lib import prop_tabulate, utc_now
|
|
||||||
|
|
||||||
from LionContext import LionContext as Context
|
|
||||||
|
|
||||||
from .module import module
|
|
||||||
|
|
||||||
|
|
||||||
process = psutil.Process()
|
|
||||||
process.cpu_percent()
|
|
||||||
|
|
||||||
|
|
||||||
@module.cmd(
|
|
||||||
"nerd",
|
|
||||||
group="Meta",
|
|
||||||
desc="Information and statistics about me!"
|
|
||||||
)
|
|
||||||
async def cmd_nerd(ctx: Context):
|
|
||||||
"""
|
|
||||||
Usage``:
|
|
||||||
{prefix}nerd
|
|
||||||
Description:
|
|
||||||
View nerdy information and statistics about me!
|
|
||||||
"""
|
|
||||||
# Create embed
|
|
||||||
embed = discord.Embed(
|
|
||||||
colour=discord.Colour.orange(),
|
|
||||||
title="Nerd Panel",
|
|
||||||
description=(
|
|
||||||
"Hi! I'm [StudyLion]({studylion}), a study management bot owned by "
|
|
||||||
"[Ari Horesh]({ari}) and developed by [Conatum#5317]({cona}), with [contributors]({github})."
|
|
||||||
).format(
|
|
||||||
studylion="http://studylions.com/",
|
|
||||||
ari="https://arihoresh.com/",
|
|
||||||
cona="https://github.com/Intery",
|
|
||||||
github="https://github.com/StudyLions/StudyLion"
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
# ----- Study stats -----
|
|
||||||
# Current studying statistics
|
|
||||||
current_students, current_channels, current_guilds= (
|
|
||||||
ctx.client.data.current_sessions.select_one_where(
|
|
||||||
select_columns=(
|
|
||||||
"COUNT(*) AS studying_count",
|
|
||||||
"COUNT(DISTINCT(channelid)) AS channel_count",
|
|
||||||
"COUNT(DISTINCT(guildid)) AS guild_count"
|
|
||||||
)
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
# Past studying statistics
|
|
||||||
past_sessions, past_students, past_duration, past_guilds = ctx.client.data.session_history.select_one_where(
|
|
||||||
select_columns=(
|
|
||||||
"COUNT(*) AS session_count",
|
|
||||||
"COUNT(DISTINCT(userid)) AS user_count",
|
|
||||||
"SUM(duration) / 3600 AS total_hours",
|
|
||||||
"COUNT(DISTINCT(guildid)) AS guild_count"
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
# Tasklist statistics
|
|
||||||
tasks = ctx.client.data.tasklist.select_one_where(
|
|
||||||
select_columns=(
|
|
||||||
'COUNT(*)'
|
|
||||||
)
|
|
||||||
)[0]
|
|
||||||
|
|
||||||
tasks_completed = ctx.client.data.tasklist.select_one_where(
|
|
||||||
completed_at=NOTNULL,
|
|
||||||
select_columns=(
|
|
||||||
'COUNT(*)'
|
|
||||||
)
|
|
||||||
)[0]
|
|
||||||
|
|
||||||
# Timers
|
|
||||||
timer_count, timer_guilds = ctx.client.data.timers.select_one_where(
|
|
||||||
select_columns=("COUNT(*)", "COUNT(DISTINCT(guildid))")
|
|
||||||
)
|
|
||||||
|
|
||||||
study_fields = {
|
|
||||||
"Currently": f"`{current_students}` people working in `{current_channels}` rooms of `{current_guilds}` guilds",
|
|
||||||
"Recorded": f"`{past_duration}` hours from `{past_students}` people across `{past_sessions}` sessions",
|
|
||||||
"Tasks": f"`{tasks_completed}` out of `{tasks}` tasks completed",
|
|
||||||
"Timers": f"`{timer_count}` timers running in `{timer_guilds}` communities"
|
|
||||||
}
|
|
||||||
study_table = prop_tabulate(*zip(*study_fields.items()))
|
|
||||||
|
|
||||||
# ----- Shard statistics -----
|
|
||||||
shard_number = ctx.client.shard_id
|
|
||||||
shard_count = ctx.client.shard_count
|
|
||||||
guilds = len(ctx.client.guilds)
|
|
||||||
member_count = sum(guild.member_count for guild in ctx.client.guilds)
|
|
||||||
commands = len(ctx.client.cmds)
|
|
||||||
aliases = len(ctx.client.cmd_names)
|
|
||||||
dpy_version = discord.__version__
|
|
||||||
py_version = sys.version.split()[0]
|
|
||||||
data_version, data_time, _ = select_where(
|
|
||||||
"VersionHistory",
|
|
||||||
_extra="ORDER BY time DESC LIMIT 1"
|
|
||||||
)[0]
|
|
||||||
data_timestamp = int(data_time.replace(tzinfo=datetime.timezone.utc).timestamp())
|
|
||||||
|
|
||||||
shard_fields = {
|
|
||||||
"Shard": f"`{shard_number}` of `{shard_count}`",
|
|
||||||
"Guilds": f"`{guilds}` servers with `{member_count}` members (on this shard)",
|
|
||||||
"Commands": f"`{commands}` commands with `{aliases}` keywords",
|
|
||||||
"Version": f"`v{data_version}`, last updated <t:{data_timestamp}:F>",
|
|
||||||
"Py version": f"`{py_version}` running discord.py `{dpy_version}`"
|
|
||||||
}
|
|
||||||
shard_table = prop_tabulate(*zip(*shard_fields.items()))
|
|
||||||
|
|
||||||
|
|
||||||
# ----- Execution statistics -----
|
|
||||||
running_commands = len(ctx.client.active_contexts)
|
|
||||||
tasks = len(asyncio.all_tasks())
|
|
||||||
objects = len(gc.get_objects())
|
|
||||||
cpu_percent = process.cpu_percent()
|
|
||||||
mem_percent = int(process.memory_percent())
|
|
||||||
uptime = int(utc_now().timestamp() - process.create_time())
|
|
||||||
|
|
||||||
execution_fields = {
|
|
||||||
"Running": f"`{running_commands}` commands",
|
|
||||||
"Waiting for": f"`{tasks}` tasks to complete",
|
|
||||||
"Objects": f"`{objects}` loaded in memory",
|
|
||||||
"Usage": f"`{cpu_percent}%` CPU, `{mem_percent}%` MEM",
|
|
||||||
"Uptime": f"`{uptime // (24 * 3600)}` days, `{uptime // 3600 % 24:02}:{uptime // 60 % 60:02}:{uptime % 60:02}`"
|
|
||||||
}
|
|
||||||
execution_table = prop_tabulate(*zip(*execution_fields.items()))
|
|
||||||
|
|
||||||
# ----- Combine and output -----
|
|
||||||
embed.add_field(name="Study Stats", value=study_table, inline=False)
|
|
||||||
embed.add_field(name=f"Shard Info", value=shard_table, inline=False)
|
|
||||||
embed.add_field(name=f"Process Stats", value=execution_table, inline=False)
|
|
||||||
|
|
||||||
await ctx.reply(embed=embed)
|
|
||||||
@@ -1,9 +0,0 @@
|
|||||||
from .module import module
|
|
||||||
|
|
||||||
from . import data
|
|
||||||
from . import admin
|
|
||||||
|
|
||||||
from . import tickets
|
|
||||||
from . import video
|
|
||||||
|
|
||||||
from . import commands
|
|
||||||
@@ -1,109 +0,0 @@
|
|||||||
import discord
|
|
||||||
|
|
||||||
from settings import GuildSettings, GuildSetting
|
|
||||||
from wards import guild_admin
|
|
||||||
|
|
||||||
import settings
|
|
||||||
|
|
||||||
from .data import studyban_durations
|
|
||||||
|
|
||||||
|
|
||||||
@GuildSettings.attach_setting
|
|
||||||
class mod_log(settings.Channel, GuildSetting):
|
|
||||||
category = "Moderation"
|
|
||||||
|
|
||||||
attr_name = 'mod_log'
|
|
||||||
_data_column = 'mod_log_channel'
|
|
||||||
|
|
||||||
display_name = "mod_log"
|
|
||||||
desc = "Moderation event logging channel."
|
|
||||||
|
|
||||||
long_desc = (
|
|
||||||
"Channel to post moderation tickets.\n"
|
|
||||||
"These are produced when a manual or automatic moderation action is performed on a member. "
|
|
||||||
"This channel acts as a more context rich moderation history source than the audit log."
|
|
||||||
)
|
|
||||||
|
|
||||||
_chan_type = discord.ChannelType.text
|
|
||||||
|
|
||||||
@property
|
|
||||||
def success_response(self):
|
|
||||||
if self.value:
|
|
||||||
return "Moderation tickets will be posted to {}.".format(self.formatted)
|
|
||||||
else:
|
|
||||||
return "The moderation log has been unset."
|
|
||||||
|
|
||||||
|
|
||||||
@GuildSettings.attach_setting
|
|
||||||
class studyban_role(settings.Role, GuildSetting):
|
|
||||||
category = "Moderation"
|
|
||||||
|
|
||||||
attr_name = 'studyban_role'
|
|
||||||
_data_column = 'studyban_role'
|
|
||||||
|
|
||||||
display_name = "studyban_role"
|
|
||||||
desc = "The role given to members to prevent them from using server study features."
|
|
||||||
|
|
||||||
long_desc = (
|
|
||||||
"This role is to be given to members to prevent them from using the server's study features.\n"
|
|
||||||
"Typically, this role should act as a 'partial mute', and prevent the user from joining study voice channels, "
|
|
||||||
"or participating in study text channels.\n"
|
|
||||||
"It will be given automatically after study related offences, "
|
|
||||||
"such as not enabling video in the video-only channels."
|
|
||||||
)
|
|
||||||
|
|
||||||
@property
|
|
||||||
def success_response(self):
|
|
||||||
if self.value:
|
|
||||||
return "The study ban role is now {}.".format(self.formatted)
|
|
||||||
|
|
||||||
|
|
||||||
@GuildSettings.attach_setting
|
|
||||||
class studyban_durations(settings.SettingList, settings.ListData, settings.Setting):
|
|
||||||
category = "Moderation"
|
|
||||||
|
|
||||||
attr_name = 'studyban_durations'
|
|
||||||
|
|
||||||
_table_interface = studyban_durations
|
|
||||||
_id_column = 'guildid'
|
|
||||||
_data_column = 'duration'
|
|
||||||
_order_column = "rowid"
|
|
||||||
|
|
||||||
_default = [
|
|
||||||
5 * 60,
|
|
||||||
60 * 60,
|
|
||||||
6 * 60 * 60,
|
|
||||||
24 * 60 * 60,
|
|
||||||
168 * 60 * 60,
|
|
||||||
720 * 60 * 60
|
|
||||||
]
|
|
||||||
|
|
||||||
_setting = settings.Duration
|
|
||||||
|
|
||||||
write_ward = guild_admin
|
|
||||||
display_name = "studyban_durations"
|
|
||||||
desc = "Sequence of durations for automatic study bans."
|
|
||||||
|
|
||||||
long_desc = (
|
|
||||||
"This sequence describes how long a member will be automatically study-banned for "
|
|
||||||
"after committing a study-related offence (such as not enabling their video in video only channels).\n"
|
|
||||||
"If the sequence is `1d, 7d, 30d`, for example, the member will be study-banned "
|
|
||||||
"for `1d` on their first offence, `7d` on their second offence, and `30d` on their third. "
|
|
||||||
"On their fourth offence, they will not be unbanned.\n"
|
|
||||||
"This does not count pardoned offences."
|
|
||||||
)
|
|
||||||
accepts = (
|
|
||||||
"Comma separated list of durations in days/hours/minutes/seconds, for example `12h, 1d, 7d, 30d`."
|
|
||||||
)
|
|
||||||
|
|
||||||
# Flat cache, no need to expire objects
|
|
||||||
_cache = {}
|
|
||||||
|
|
||||||
@property
|
|
||||||
def success_response(self):
|
|
||||||
if self.value:
|
|
||||||
return "The automatic study ban durations are now {}.".format(self.formatted)
|
|
||||||
else:
|
|
||||||
return "Automatic study bans will never be reverted."
|
|
||||||
|
|
||||||
|
|
||||||
@@ -1,448 +0,0 @@
|
|||||||
"""
|
|
||||||
Shared commands for the moderation module.
|
|
||||||
"""
|
|
||||||
import asyncio
|
|
||||||
from collections import defaultdict
|
|
||||||
import discord
|
|
||||||
|
|
||||||
from cmdClient.lib import ResponseTimedOut
|
|
||||||
from wards import guild_moderator
|
|
||||||
|
|
||||||
from .module import module
|
|
||||||
from .tickets import Ticket, TicketType, TicketState
|
|
||||||
|
|
||||||
|
|
||||||
type_accepts = {
|
|
||||||
'note': TicketType.NOTE,
|
|
||||||
'notes': TicketType.NOTE,
|
|
||||||
'studyban': TicketType.STUDY_BAN,
|
|
||||||
'studybans': TicketType.STUDY_BAN,
|
|
||||||
'warn': TicketType.WARNING,
|
|
||||||
'warns': TicketType.WARNING,
|
|
||||||
'warning': TicketType.WARNING,
|
|
||||||
'warnings': TicketType.WARNING,
|
|
||||||
}
|
|
||||||
|
|
||||||
type_formatted = {
|
|
||||||
TicketType.NOTE: 'NOTE',
|
|
||||||
TicketType.STUDY_BAN: 'STUDYBAN',
|
|
||||||
TicketType.WARNING: 'WARNING',
|
|
||||||
}
|
|
||||||
|
|
||||||
type_summary_formatted = {
|
|
||||||
TicketType.NOTE: 'note',
|
|
||||||
TicketType.STUDY_BAN: 'studyban',
|
|
||||||
TicketType.WARNING: 'warning',
|
|
||||||
}
|
|
||||||
|
|
||||||
state_formatted = {
|
|
||||||
TicketState.OPEN: 'ACTIVE',
|
|
||||||
TicketState.EXPIRING: 'TEMP',
|
|
||||||
TicketState.EXPIRED: 'EXPIRED',
|
|
||||||
TicketState.PARDONED: 'PARDONED'
|
|
||||||
}
|
|
||||||
|
|
||||||
state_summary_formatted = {
|
|
||||||
TicketState.OPEN: 'Active',
|
|
||||||
TicketState.EXPIRING: 'Temporary',
|
|
||||||
TicketState.EXPIRED: 'Expired',
|
|
||||||
TicketState.REVERTED: 'Manually Reverted',
|
|
||||||
TicketState.PARDONED: 'Pardoned'
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
@module.cmd(
|
|
||||||
"tickets",
|
|
||||||
group="Moderation",
|
|
||||||
desc="View and filter the server moderation tickets.",
|
|
||||||
flags=('active', 'type=')
|
|
||||||
)
|
|
||||||
@guild_moderator()
|
|
||||||
async def cmd_tickets(ctx, flags):
|
|
||||||
"""
|
|
||||||
Usage``:
|
|
||||||
{prefix}tickets [@user] [--type <type>] [--active]
|
|
||||||
Description:
|
|
||||||
Display and optionally filter the moderation event history in this guild.
|
|
||||||
Flags::
|
|
||||||
type: Filter by ticket type. See **Ticket Types** below.
|
|
||||||
active: Only show in-effect tickets (i.e. hide expired and pardoned ones).
|
|
||||||
Ticket Types::
|
|
||||||
note: Moderation notes.
|
|
||||||
warn: Moderation warnings, both manual and automatic.
|
|
||||||
studyban: Bans from using study features from abusing the study system.
|
|
||||||
blacklist: Complete blacklisting from using my commands.
|
|
||||||
Ticket States::
|
|
||||||
Active: Active tickets that will not automatically expire.
|
|
||||||
Temporary: Active tickets that will automatically expire after a set duration.
|
|
||||||
Expired: Tickets that have automatically expired.
|
|
||||||
Reverted: Tickets with actions that have been reverted.
|
|
||||||
Pardoned: Tickets that have been pardoned and no longer apply to the user.
|
|
||||||
Examples:
|
|
||||||
{prefix}tickets {ctx.guild.owner.mention} --type warn --active
|
|
||||||
"""
|
|
||||||
# Parse filter fields
|
|
||||||
# First the user
|
|
||||||
if ctx.args:
|
|
||||||
userstr = ctx.args.strip('<@!&> ')
|
|
||||||
if not userstr.isdigit():
|
|
||||||
return await ctx.error_reply(
|
|
||||||
"**Usage:** `{prefix}tickets [@user] [--type <type>] [--active]`.\n"
|
|
||||||
"Please provide the `user` as a mention or id!".format(prefix=ctx.best_prefix)
|
|
||||||
)
|
|
||||||
filter_userid = int(userstr)
|
|
||||||
else:
|
|
||||||
filter_userid = None
|
|
||||||
|
|
||||||
if flags['type']:
|
|
||||||
typestr = flags['type'].lower()
|
|
||||||
if typestr not in type_accepts:
|
|
||||||
return await ctx.error_reply(
|
|
||||||
"Please see `{prefix}help tickets` for the valid ticket types!".format(prefix=ctx.best_prefix)
|
|
||||||
)
|
|
||||||
filter_type = type_accepts[typestr]
|
|
||||||
else:
|
|
||||||
filter_type = None
|
|
||||||
|
|
||||||
filter_active = flags['active']
|
|
||||||
|
|
||||||
# Build the filter arguments
|
|
||||||
filters = {'guildid': ctx.guild.id}
|
|
||||||
if filter_userid:
|
|
||||||
filters['targetid'] = filter_userid
|
|
||||||
if filter_type:
|
|
||||||
filters['ticket_type'] = filter_type
|
|
||||||
if filter_active:
|
|
||||||
filters['ticket_state'] = [TicketState.OPEN, TicketState.EXPIRING]
|
|
||||||
|
|
||||||
# Fetch the tickets with these filters
|
|
||||||
tickets = Ticket.fetch_tickets(**filters)
|
|
||||||
|
|
||||||
if not tickets:
|
|
||||||
if filters:
|
|
||||||
return await ctx.embed_reply("There are no tickets with these criteria!")
|
|
||||||
else:
|
|
||||||
return await ctx.embed_reply("There are no moderation tickets in this server!")
|
|
||||||
|
|
||||||
tickets = sorted(tickets, key=lambda ticket: ticket.data.guild_ticketid, reverse=True)
|
|
||||||
ticket_map = {ticket.data.guild_ticketid: ticket for ticket in tickets}
|
|
||||||
|
|
||||||
# Build the format string based on the filters
|
|
||||||
components = []
|
|
||||||
# Ticket id with link to message in mod log
|
|
||||||
components.append("[#{ticket.data.guild_ticketid}]({ticket.link})")
|
|
||||||
# Ticket creation date
|
|
||||||
components.append("<t:{timestamp:.0f}:d>")
|
|
||||||
# Ticket type, with current state
|
|
||||||
if filter_type is None:
|
|
||||||
if not filter_active:
|
|
||||||
components.append("`{ticket_type}{ticket_state}`")
|
|
||||||
else:
|
|
||||||
components.append("`{ticket_type}`")
|
|
||||||
elif not filter_active:
|
|
||||||
components.append("`{ticket_real_state}`")
|
|
||||||
if not filter_userid:
|
|
||||||
# Ticket user
|
|
||||||
components.append("<@{ticket.data.targetid}>")
|
|
||||||
if filter_userid or (filter_active and filter_type):
|
|
||||||
# Truncated ticket content
|
|
||||||
components.append("{content}")
|
|
||||||
|
|
||||||
format_str = ' | '.join(components)
|
|
||||||
|
|
||||||
# Break tickets into blocks
|
|
||||||
blocks = [tickets[i:i+10] for i in range(0, len(tickets), 10)]
|
|
||||||
|
|
||||||
# Build pages of tickets
|
|
||||||
ticket_pages = []
|
|
||||||
for block in blocks:
|
|
||||||
ticket_page = []
|
|
||||||
|
|
||||||
type_len = max(len(type_formatted[ticket.type]) for ticket in block)
|
|
||||||
state_len = max(len(state_formatted[ticket.state]) for ticket in block)
|
|
||||||
for ticket in block:
|
|
||||||
# First truncate content if required
|
|
||||||
content = ticket.data.content
|
|
||||||
if len(content) > 40:
|
|
||||||
content = content[:37] + '...'
|
|
||||||
|
|
||||||
# Build ticket line
|
|
||||||
line = format_str.format(
|
|
||||||
ticket=ticket,
|
|
||||||
timestamp=ticket.data.created_at.timestamp(),
|
|
||||||
ticket_type=type_formatted[ticket.type],
|
|
||||||
type_len=type_len,
|
|
||||||
ticket_state=" [{}]".format(state_formatted[ticket.state]) if ticket.state != TicketState.OPEN else '',
|
|
||||||
ticket_real_state=state_formatted[ticket.state],
|
|
||||||
state_len=state_len,
|
|
||||||
content=content
|
|
||||||
)
|
|
||||||
if ticket.state == TicketState.PARDONED:
|
|
||||||
line = "~~{}~~".format(line)
|
|
||||||
|
|
||||||
# Add to current page
|
|
||||||
ticket_page.append(line)
|
|
||||||
# Combine lines and add page to pages
|
|
||||||
ticket_pages.append('\n'.join(ticket_page))
|
|
||||||
|
|
||||||
# Build active ticket type summary
|
|
||||||
freq = defaultdict(int)
|
|
||||||
for ticket in tickets:
|
|
||||||
if ticket.state != TicketState.PARDONED:
|
|
||||||
freq[ticket.type] += 1
|
|
||||||
summary_pairs = [
|
|
||||||
(num, type_summary_formatted[ttype] + ('s' if num > 1 else ''))
|
|
||||||
for ttype, num in freq.items()
|
|
||||||
]
|
|
||||||
summary_pairs.sort(key=lambda pair: pair[0])
|
|
||||||
# num_len = max(len(str(num)) for num in freq.values())
|
|
||||||
# type_summary = '\n'.join(
|
|
||||||
# "**`{:<{}}`** {}".format(pair[0], num_len, pair[1])
|
|
||||||
# for pair in summary_pairs
|
|
||||||
# )
|
|
||||||
|
|
||||||
# # Build status summary
|
|
||||||
# freq = defaultdict(int)
|
|
||||||
# for ticket in tickets:
|
|
||||||
# freq[ticket.state] += 1
|
|
||||||
# num_len = max(len(str(num)) for num in freq.values())
|
|
||||||
# status_summary = '\n'.join(
|
|
||||||
# "**`{:<{}}`** {}".format(freq[state], num_len, state_str)
|
|
||||||
# for state, state_str in state_summary_formatted.items()
|
|
||||||
# if state in freq
|
|
||||||
# )
|
|
||||||
|
|
||||||
summary_strings = [
|
|
||||||
"**`{}`** {}".format(*pair) for pair in summary_pairs
|
|
||||||
]
|
|
||||||
if len(summary_strings) > 2:
|
|
||||||
summary = ', '.join(summary_strings[:-1]) + ', and ' + summary_strings[-1]
|
|
||||||
elif len(summary_strings) == 2:
|
|
||||||
summary = ' and '.join(summary_strings)
|
|
||||||
else:
|
|
||||||
summary = ''.join(summary_strings)
|
|
||||||
if summary:
|
|
||||||
summary += '.'
|
|
||||||
|
|
||||||
# Build embed info
|
|
||||||
title = "{}{}{}".format(
|
|
||||||
"Active " if filter_active else '',
|
|
||||||
"{} tickets ".format(type_formatted[filter_type]) if filter_type else "Tickets ",
|
|
||||||
(" for {}".format(ctx.guild.get_member(filter_userid) or filter_userid)
|
|
||||||
if filter_userid else " in {}".format(ctx.guild.name))
|
|
||||||
)
|
|
||||||
footer = "Click a ticket id to jump to it, or type the number to show the full ticket."
|
|
||||||
page_count = len(blocks)
|
|
||||||
if page_count > 1:
|
|
||||||
footer += "\nPage {{page_num}}/{}".format(page_count)
|
|
||||||
|
|
||||||
# Create embeds
|
|
||||||
embeds = [
|
|
||||||
discord.Embed(
|
|
||||||
title=title,
|
|
||||||
description="{}\n{}".format(summary, page),
|
|
||||||
colour=discord.Colour.orange(),
|
|
||||||
).set_footer(text=footer.format(page_num=i+1))
|
|
||||||
for i, page in enumerate(ticket_pages)
|
|
||||||
]
|
|
||||||
|
|
||||||
# Run output with cancellation and listener
|
|
||||||
out_msg = await ctx.pager(embeds, add_cancel=True)
|
|
||||||
old_task = _displays.pop((ctx.ch.id, ctx.author.id), None)
|
|
||||||
if old_task:
|
|
||||||
old_task.cancel()
|
|
||||||
_displays[(ctx.ch.id, ctx.author.id)] = display_task = asyncio.create_task(_ticket_display(ctx, ticket_map))
|
|
||||||
ctx.tasks.append(display_task)
|
|
||||||
await ctx.cancellable(out_msg, add_reaction=False)
|
|
||||||
|
|
||||||
|
|
||||||
_displays = {} # (channelid, userid) -> Task
|
|
||||||
async def _ticket_display(ctx, ticket_map):
|
|
||||||
"""
|
|
||||||
Display tickets when the ticket number is entered.
|
|
||||||
"""
|
|
||||||
current_ticket_msg = None
|
|
||||||
|
|
||||||
try:
|
|
||||||
while True:
|
|
||||||
# Wait for a number
|
|
||||||
try:
|
|
||||||
result = await ctx.client.wait_for(
|
|
||||||
"message",
|
|
||||||
check=lambda msg: (msg.author == ctx.author
|
|
||||||
and msg.channel == ctx.ch
|
|
||||||
and msg.content.isdigit()
|
|
||||||
and int(msg.content) in ticket_map),
|
|
||||||
timeout=60
|
|
||||||
)
|
|
||||||
except asyncio.TimeoutError:
|
|
||||||
return
|
|
||||||
|
|
||||||
# Delete the response
|
|
||||||
try:
|
|
||||||
await result.delete()
|
|
||||||
except discord.HTTPException:
|
|
||||||
pass
|
|
||||||
|
|
||||||
# Display the ticket
|
|
||||||
embed = ticket_map[int(result.content)].msg_args['embed']
|
|
||||||
if current_ticket_msg:
|
|
||||||
try:
|
|
||||||
await current_ticket_msg.edit(embed=embed)
|
|
||||||
except discord.HTTPException:
|
|
||||||
current_ticket_msg = None
|
|
||||||
|
|
||||||
if not current_ticket_msg:
|
|
||||||
try:
|
|
||||||
current_ticket_msg = await ctx.reply(embed=embed)
|
|
||||||
except discord.HTTPException:
|
|
||||||
return
|
|
||||||
asyncio.create_task(ctx.offer_delete(current_ticket_msg))
|
|
||||||
except asyncio.CancelledError:
|
|
||||||
if current_ticket_msg:
|
|
||||||
try:
|
|
||||||
await current_ticket_msg.delete()
|
|
||||||
except discord.HTTPException:
|
|
||||||
pass
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@module.cmd(
|
|
||||||
"pardon",
|
|
||||||
group="Moderation",
|
|
||||||
desc="Pardon a ticket, or clear a member's moderation history.",
|
|
||||||
flags=('type=',)
|
|
||||||
)
|
|
||||||
@guild_moderator()
|
|
||||||
async def cmd_pardon(ctx, flags):
|
|
||||||
"""
|
|
||||||
Usage``:
|
|
||||||
{prefix}pardon ticketid, ticketid, ticketid
|
|
||||||
{prefix}pardon @user [--type <type>]
|
|
||||||
Description:
|
|
||||||
Marks the given tickets as no longer applicable.
|
|
||||||
These tickets will not be considered when calculating automod actions such as automatic study bans.
|
|
||||||
|
|
||||||
This may be used to mark warns or other tickets as no longer in-effect.
|
|
||||||
If the ticket is active when it is pardoned, it will be reverted, and any expiry cancelled.
|
|
||||||
|
|
||||||
Use the `{prefix}tickets` command to view the relevant tickets.
|
|
||||||
Flags::
|
|
||||||
type: Filter by ticket type. See **Ticket Types** in `{prefix}help tickets`.
|
|
||||||
Examples:
|
|
||||||
{prefix}pardon 21
|
|
||||||
{prefix}pardon {ctx.guild.owner.mention} --type warn
|
|
||||||
"""
|
|
||||||
usage = "**Usage**: `{prefix}pardon ticketid` or `{prefix}pardon @user`.".format(prefix=ctx.best_prefix)
|
|
||||||
if not ctx.args:
|
|
||||||
return await ctx.error_reply(
|
|
||||||
usage
|
|
||||||
)
|
|
||||||
|
|
||||||
# Parse provided tickets or filters
|
|
||||||
targetid = None
|
|
||||||
ticketids = []
|
|
||||||
args = {'guildid': ctx.guild.id}
|
|
||||||
if ',' in ctx.args:
|
|
||||||
# Assume provided numbers are ticketids.
|
|
||||||
items = [item.strip() for item in ctx.args.split(',')]
|
|
||||||
if not all(item.isdigit() for item in items):
|
|
||||||
return await ctx.error_reply(usage)
|
|
||||||
ticketids = [int(item) for item in items]
|
|
||||||
args['guild_ticketid'] = ticketids
|
|
||||||
else:
|
|
||||||
# Guess whether the provided numbers were ticketids or not
|
|
||||||
idstr = ctx.args.strip('<@!&> ')
|
|
||||||
if not idstr.isdigit():
|
|
||||||
return await ctx.error_reply(usage)
|
|
||||||
|
|
||||||
maybe_id = int(idstr)
|
|
||||||
if maybe_id > 4194304: # Testing whether it is greater than the minimum snowflake id
|
|
||||||
# Assume userid
|
|
||||||
targetid = maybe_id
|
|
||||||
args['targetid'] = maybe_id
|
|
||||||
|
|
||||||
# Add the type filter if provided
|
|
||||||
if flags['type']:
|
|
||||||
typestr = flags['type'].lower()
|
|
||||||
if typestr not in type_accepts:
|
|
||||||
return await ctx.error_reply(
|
|
||||||
"Please see `{prefix}help tickets` for the valid ticket types!".format(prefix=ctx.best_prefix)
|
|
||||||
)
|
|
||||||
args['ticket_type'] = type_accepts[typestr]
|
|
||||||
else:
|
|
||||||
# Assume guild ticketid
|
|
||||||
ticketids = [maybe_id]
|
|
||||||
args['guild_ticketid'] = maybe_id
|
|
||||||
|
|
||||||
# Fetch the matching tickets
|
|
||||||
tickets = Ticket.fetch_tickets(**args)
|
|
||||||
|
|
||||||
# Check whether we have the right selection of tickets
|
|
||||||
if targetid and not tickets:
|
|
||||||
return await ctx.error_reply(
|
|
||||||
"<@{}> has no matching tickets to pardon!"
|
|
||||||
)
|
|
||||||
if ticketids and len(ticketids) != len(tickets):
|
|
||||||
# Not all of the ticketids were valid
|
|
||||||
difference = list(set(ticketids).difference(ticket.ticketid for ticket in tickets))
|
|
||||||
if len(difference) == 1:
|
|
||||||
return await ctx.error_reply(
|
|
||||||
"Couldn't find ticket `{}`!".format(difference[0])
|
|
||||||
)
|
|
||||||
else:
|
|
||||||
return await ctx.error_reply(
|
|
||||||
"Couldn't find any of the following tickets:\n`{}`".format(
|
|
||||||
'`, `'.join(difference)
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
# Check whether there are any tickets left to pardon
|
|
||||||
to_pardon = [ticket for ticket in tickets if ticket.state != TicketState.PARDONED]
|
|
||||||
if not to_pardon:
|
|
||||||
if ticketids and len(tickets) == 1:
|
|
||||||
ticket = tickets[0]
|
|
||||||
return await ctx.error_reply(
|
|
||||||
"[Ticket #{}]({}) is already pardoned!".format(ticket.data.guild_ticketid, ticket.link)
|
|
||||||
)
|
|
||||||
else:
|
|
||||||
return await ctx.error_reply(
|
|
||||||
"All of these tickets are already pardoned!"
|
|
||||||
)
|
|
||||||
|
|
||||||
# We now know what tickets we want to pardon
|
|
||||||
# Request the pardon reason
|
|
||||||
try:
|
|
||||||
reason = await ctx.input("Please provide a reason for the pardon.")
|
|
||||||
except ResponseTimedOut:
|
|
||||||
raise ResponseTimedOut("Prompt timed out, no tickets were pardoned.")
|
|
||||||
|
|
||||||
# Pardon the tickets
|
|
||||||
for ticket in to_pardon:
|
|
||||||
await ticket.pardon(ctx.author, reason)
|
|
||||||
|
|
||||||
# Finally, ack the pardon
|
|
||||||
if targetid:
|
|
||||||
await ctx.embed_reply(
|
|
||||||
"The active {}s for <@{}> have been cleared.".format(
|
|
||||||
type_summary_formatted[args['ticket_type']] if flags['type'] else 'ticket',
|
|
||||||
targetid
|
|
||||||
)
|
|
||||||
)
|
|
||||||
elif len(to_pardon) == 1:
|
|
||||||
ticket = to_pardon[0]
|
|
||||||
await ctx.embed_reply(
|
|
||||||
"[Ticket #{}]({}) was pardoned.".format(
|
|
||||||
ticket.data.guild_ticketid,
|
|
||||||
ticket.link
|
|
||||||
)
|
|
||||||
)
|
|
||||||
else:
|
|
||||||
await ctx.embed_reply(
|
|
||||||
"The following tickets were pardoned.\n{}".format(
|
|
||||||
", ".join(
|
|
||||||
"[#{}]({})".format(ticket.data.guild_ticketid, ticket.link)
|
|
||||||
for ticket in to_pardon
|
|
||||||
)
|
|
||||||
)
|
|
||||||
)
|
|
||||||
@@ -1,19 +0,0 @@
|
|||||||
from data import Table, RowTable
|
|
||||||
|
|
||||||
|
|
||||||
studyban_durations = Table('studyban_durations')
|
|
||||||
|
|
||||||
ticket_info = RowTable(
|
|
||||||
'ticket_info',
|
|
||||||
('ticketid', 'guild_ticketid',
|
|
||||||
'guildid', 'targetid', 'ticket_type', 'ticket_state', 'moderator_id', 'auto',
|
|
||||||
'log_msg_id', 'created_at',
|
|
||||||
'content', 'context', 'addendum', 'duration',
|
|
||||||
'file_name', 'file_data',
|
|
||||||
'expiry',
|
|
||||||
'pardoned_by', 'pardoned_at', 'pardoned_reason'),
|
|
||||||
'ticketid',
|
|
||||||
cache_size=20000
|
|
||||||
)
|
|
||||||
|
|
||||||
tickets = Table('tickets')
|
|
||||||
@@ -1,4 +0,0 @@
|
|||||||
from cmdClient import Module
|
|
||||||
|
|
||||||
|
|
||||||
module = Module("Moderation")
|
|
||||||
@@ -1,486 +0,0 @@
|
|||||||
import asyncio
|
|
||||||
import logging
|
|
||||||
import traceback
|
|
||||||
import datetime
|
|
||||||
|
|
||||||
import discord
|
|
||||||
|
|
||||||
from meta import client
|
|
||||||
from data.conditions import THIS_SHARD
|
|
||||||
from settings import GuildSettings
|
|
||||||
from utils.lib import FieldEnum, strfdelta, utc_now
|
|
||||||
|
|
||||||
from .. import data
|
|
||||||
from ..module import module
|
|
||||||
|
|
||||||
|
|
||||||
class TicketType(FieldEnum):
|
|
||||||
"""
|
|
||||||
The possible ticket types.
|
|
||||||
"""
|
|
||||||
NOTE = 'NOTE', 'Note'
|
|
||||||
WARNING = 'WARNING', 'Warning'
|
|
||||||
STUDY_BAN = 'STUDY_BAN', 'Study Ban'
|
|
||||||
MESAGE_CENSOR = 'MESSAGE_CENSOR', 'Message Censor'
|
|
||||||
INVITE_CENSOR = 'INVITE_CENSOR', 'Invite Censor'
|
|
||||||
|
|
||||||
|
|
||||||
class TicketState(FieldEnum):
|
|
||||||
"""
|
|
||||||
The possible ticket states.
|
|
||||||
"""
|
|
||||||
OPEN = 'OPEN', "Active"
|
|
||||||
EXPIRING = 'EXPIRING', "Active"
|
|
||||||
EXPIRED = 'EXPIRED', "Expired"
|
|
||||||
PARDONED = 'PARDONED', "Pardoned"
|
|
||||||
REVERTED = 'REVERTED', "Reverted"
|
|
||||||
|
|
||||||
|
|
||||||
class Ticket:
|
|
||||||
"""
|
|
||||||
Abstract base class representing a Ticketed moderation action.
|
|
||||||
"""
|
|
||||||
# Type of event the class represents
|
|
||||||
_ticket_type = None # type: TicketType
|
|
||||||
|
|
||||||
_ticket_types = {} # Map: TicketType -> Ticket subclass
|
|
||||||
|
|
||||||
_expiry_tasks = {} # Map: ticketid -> expiry Task
|
|
||||||
|
|
||||||
def __init__(self, ticketid, *args, **kwargs):
|
|
||||||
self.ticketid = ticketid
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
async def create(cls, *args, **kwargs):
|
|
||||||
"""
|
|
||||||
Method used to create a new ticket of the current type.
|
|
||||||
Should add a row to the ticket table, post the ticket, and return the Ticket.
|
|
||||||
"""
|
|
||||||
raise NotImplementedError
|
|
||||||
|
|
||||||
@property
|
|
||||||
def data(self):
|
|
||||||
"""
|
|
||||||
Ticket row.
|
|
||||||
This will usually be a row of `ticket_info`.
|
|
||||||
"""
|
|
||||||
return data.ticket_info.fetch(self.ticketid)
|
|
||||||
|
|
||||||
@property
|
|
||||||
def guild(self):
|
|
||||||
return client.get_guild(self.data.guildid)
|
|
||||||
|
|
||||||
@property
|
|
||||||
def target(self):
|
|
||||||
guild = self.guild
|
|
||||||
return guild.get_member(self.data.targetid) if guild else None
|
|
||||||
|
|
||||||
@property
|
|
||||||
def msg_args(self):
|
|
||||||
"""
|
|
||||||
Ticket message posted in the moderation log.
|
|
||||||
"""
|
|
||||||
args = {}
|
|
||||||
|
|
||||||
# Build embed
|
|
||||||
info = self.data
|
|
||||||
member = self.target
|
|
||||||
name = str(member) if member else str(info.targetid)
|
|
||||||
|
|
||||||
if info.auto:
|
|
||||||
title_fmt = "Ticket #{} | {} | {}[Auto] | {}"
|
|
||||||
else:
|
|
||||||
title_fmt = "Ticket #{} | {} | {} | {}"
|
|
||||||
title = title_fmt.format(
|
|
||||||
info.guild_ticketid,
|
|
||||||
TicketState(info.ticket_state).desc,
|
|
||||||
TicketType(info.ticket_type).desc,
|
|
||||||
name
|
|
||||||
)
|
|
||||||
|
|
||||||
embed = discord.Embed(
|
|
||||||
title=title,
|
|
||||||
description=info.content,
|
|
||||||
timestamp=info.created_at
|
|
||||||
)
|
|
||||||
embed.add_field(
|
|
||||||
name="Target",
|
|
||||||
value="<@{}>".format(info.targetid)
|
|
||||||
)
|
|
||||||
|
|
||||||
if not info.auto:
|
|
||||||
embed.add_field(
|
|
||||||
name="Moderator",
|
|
||||||
value="<@{}>".format(info.moderator_id)
|
|
||||||
)
|
|
||||||
|
|
||||||
# if info.duration:
|
|
||||||
# value = "`{}` {}".format(
|
|
||||||
# strfdelta(datetime.timedelta(seconds=info.duration)),
|
|
||||||
# "(Expiry <t:{:.0f}>)".format(info.expiry.timestamp()) if info.expiry else ""
|
|
||||||
# )
|
|
||||||
# embed.add_field(
|
|
||||||
# name="Duration",
|
|
||||||
# value=value
|
|
||||||
# )
|
|
||||||
if info.expiry:
|
|
||||||
if info.ticket_state == TicketState.EXPIRING:
|
|
||||||
embed.add_field(
|
|
||||||
name="Expires at",
|
|
||||||
value="<t:{:.0f}>\n(Duration: `{}`)".format(
|
|
||||||
info.expiry.timestamp(),
|
|
||||||
strfdelta(datetime.timedelta(seconds=info.duration))
|
|
||||||
)
|
|
||||||
)
|
|
||||||
elif info.ticket_state == TicketState.EXPIRED:
|
|
||||||
embed.add_field(
|
|
||||||
name="Expired",
|
|
||||||
value="<t:{:.0f}>".format(
|
|
||||||
info.expiry.timestamp(),
|
|
||||||
)
|
|
||||||
)
|
|
||||||
else:
|
|
||||||
embed.add_field(
|
|
||||||
name="Expiry",
|
|
||||||
value="<t:{:.0f}>".format(
|
|
||||||
info.expiry.timestamp()
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
if info.context:
|
|
||||||
embed.add_field(
|
|
||||||
name="Context",
|
|
||||||
value=info.context,
|
|
||||||
inline=False
|
|
||||||
)
|
|
||||||
|
|
||||||
if info.addendum:
|
|
||||||
embed.add_field(
|
|
||||||
name="Notes",
|
|
||||||
value=info.addendum,
|
|
||||||
inline=False
|
|
||||||
)
|
|
||||||
|
|
||||||
if self.state == TicketState.PARDONED:
|
|
||||||
embed.add_field(
|
|
||||||
name="Pardoned",
|
|
||||||
value=(
|
|
||||||
"Pardoned by <@{}> at <t:{:.0f}>.\n{}"
|
|
||||||
).format(
|
|
||||||
info.pardoned_by,
|
|
||||||
info.pardoned_at.timestamp(),
|
|
||||||
info.pardoned_reason or ""
|
|
||||||
),
|
|
||||||
inline=False
|
|
||||||
)
|
|
||||||
|
|
||||||
embed.set_footer(text="ID: {}".format(info.targetid))
|
|
||||||
|
|
||||||
args['embed'] = embed
|
|
||||||
|
|
||||||
# Add file
|
|
||||||
if info.file_name:
|
|
||||||
args['file'] = discord.File(info.file_data, info.file_name)
|
|
||||||
|
|
||||||
return args
|
|
||||||
|
|
||||||
@property
|
|
||||||
def link(self):
|
|
||||||
"""
|
|
||||||
The link to the ticket in the moderation log.
|
|
||||||
"""
|
|
||||||
info = self.data
|
|
||||||
modlog = GuildSettings(info.guildid).mod_log.data
|
|
||||||
|
|
||||||
return 'https://discord.com/channels/{}/{}/{}'.format(
|
|
||||||
info.guildid,
|
|
||||||
modlog,
|
|
||||||
info.log_msg_id
|
|
||||||
)
|
|
||||||
|
|
||||||
@property
|
|
||||||
def state(self):
|
|
||||||
return TicketState(self.data.ticket_state)
|
|
||||||
|
|
||||||
@property
|
|
||||||
def type(self):
|
|
||||||
return TicketType(self.data.ticket_type)
|
|
||||||
|
|
||||||
async def update(self, **kwargs):
|
|
||||||
"""
|
|
||||||
Update ticket fields.
|
|
||||||
"""
|
|
||||||
fields = (
|
|
||||||
'targetid', 'moderator_id', 'auto', 'log_msg_id',
|
|
||||||
'content', 'expiry', 'ticket_state',
|
|
||||||
'context', 'addendum', 'duration', 'file_name', 'file_data',
|
|
||||||
'pardoned_by', 'pardoned_at', 'pardoned_reason',
|
|
||||||
)
|
|
||||||
params = {field: kwargs[field] for field in fields if field in kwargs}
|
|
||||||
if params:
|
|
||||||
data.ticket_info.update_where(params, ticketid=self.ticketid)
|
|
||||||
|
|
||||||
await self.update_expiry()
|
|
||||||
await self.post()
|
|
||||||
|
|
||||||
async def post(self):
|
|
||||||
"""
|
|
||||||
Post or update the ticket in the moderation log.
|
|
||||||
Also updates the saved message id.
|
|
||||||
"""
|
|
||||||
info = self.data
|
|
||||||
modlog = GuildSettings(info.guildid).mod_log.value
|
|
||||||
if not modlog:
|
|
||||||
return
|
|
||||||
|
|
||||||
resend = True
|
|
||||||
try:
|
|
||||||
if info.log_msg_id:
|
|
||||||
# Try to fetch the message
|
|
||||||
message = await modlog.fetch_message(info.log_msg_id)
|
|
||||||
if message:
|
|
||||||
if message.author.id == client.user.id:
|
|
||||||
# TODO: Handle file edit
|
|
||||||
await message.edit(embed=self.msg_args['embed'])
|
|
||||||
resend = False
|
|
||||||
else:
|
|
||||||
try:
|
|
||||||
await message.delete()
|
|
||||||
except discord.HTTPException:
|
|
||||||
pass
|
|
||||||
|
|
||||||
if resend:
|
|
||||||
message = await modlog.send(**self.msg_args)
|
|
||||||
self.data.log_msg_id = message.id
|
|
||||||
except discord.HTTPException:
|
|
||||||
client.log(
|
|
||||||
"Cannot post ticket (tid: {}) due to discord exception or issue.".format(self.ticketid)
|
|
||||||
)
|
|
||||||
except Exception:
|
|
||||||
# This should never happen in normal operation
|
|
||||||
client.log(
|
|
||||||
"Error while posting ticket (tid:{})! "
|
|
||||||
"Exception traceback follows.\n{}".format(
|
|
||||||
self.ticketid,
|
|
||||||
traceback.format_exc()
|
|
||||||
),
|
|
||||||
context="TICKETS",
|
|
||||||
level=logging.ERROR
|
|
||||||
)
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def load_expiring(cls):
|
|
||||||
"""
|
|
||||||
Load and schedule all expiring tickets.
|
|
||||||
"""
|
|
||||||
# TODO: Consider changing this to a flat timestamp system, to avoid storing lots of coroutines.
|
|
||||||
# TODO: Consider only scheduling the expiries in the next day, and updating this once per day.
|
|
||||||
# TODO: Only fetch tickets from guilds we are in.
|
|
||||||
|
|
||||||
# Cancel existing expiry tasks
|
|
||||||
for task in cls._expiry_tasks.values():
|
|
||||||
if not task.done():
|
|
||||||
task.cancel()
|
|
||||||
|
|
||||||
# Get all expiring tickets
|
|
||||||
expiring_rows = data.tickets.select_where(
|
|
||||||
ticket_state=TicketState.EXPIRING,
|
|
||||||
guildid=THIS_SHARD
|
|
||||||
)
|
|
||||||
|
|
||||||
# Create new expiry tasks
|
|
||||||
now = utc_now()
|
|
||||||
cls._expiry_tasks = {
|
|
||||||
row['ticketid']: asyncio.create_task(
|
|
||||||
cls._schedule_expiry_for(
|
|
||||||
row['ticketid'],
|
|
||||||
(row['expiry'] - now).total_seconds()
|
|
||||||
)
|
|
||||||
) for row in expiring_rows
|
|
||||||
}
|
|
||||||
|
|
||||||
# Log
|
|
||||||
client.log(
|
|
||||||
"Loaded {} expiring tickets.".format(len(cls._expiry_tasks)),
|
|
||||||
context="TICKET_LOADER",
|
|
||||||
)
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
async def _schedule_expiry_for(cls, ticketid, delay):
|
|
||||||
"""
|
|
||||||
Schedule expiry for a given ticketid
|
|
||||||
"""
|
|
||||||
try:
|
|
||||||
await asyncio.sleep(delay)
|
|
||||||
ticket = Ticket.fetch(ticketid)
|
|
||||||
if ticket:
|
|
||||||
await asyncio.shield(ticket._expire())
|
|
||||||
except asyncio.CancelledError:
|
|
||||||
return
|
|
||||||
|
|
||||||
def update_expiry(self):
|
|
||||||
# Cancel any existing expiry task
|
|
||||||
task = self._expiry_tasks.pop(self.ticketid, None)
|
|
||||||
if task and not task.done():
|
|
||||||
task.cancel()
|
|
||||||
|
|
||||||
# Schedule a new expiry task, if applicable
|
|
||||||
if self.data.ticket_state == TicketState.EXPIRING:
|
|
||||||
self._expiry_tasks[self.ticketid] = asyncio.create_task(
|
|
||||||
self._schedule_expiry_for(
|
|
||||||
self.ticketid,
|
|
||||||
(self.data.expiry - utc_now()).total_seconds()
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
async def cancel_expiry(self):
|
|
||||||
"""
|
|
||||||
Cancel ticket expiry.
|
|
||||||
|
|
||||||
In particular, may be used if another ticket overrides `self`.
|
|
||||||
Sets the ticket state to `OPEN`, so that it no longer expires.
|
|
||||||
"""
|
|
||||||
if self.state == TicketState.EXPIRING:
|
|
||||||
# Update the ticket state
|
|
||||||
self.data.ticket_state = TicketState.OPEN
|
|
||||||
|
|
||||||
# Remove from expiry tsks
|
|
||||||
self.update_expiry()
|
|
||||||
|
|
||||||
# Repost
|
|
||||||
await self.post()
|
|
||||||
|
|
||||||
async def _revert(self, reason=None):
|
|
||||||
"""
|
|
||||||
Method used to revert the ticket action, e.g. unban or remove mute role.
|
|
||||||
Generally called by `pardon` and `_expire`.
|
|
||||||
|
|
||||||
May be overriden by the Ticket type, if they implement any revert logic.
|
|
||||||
Is a no-op by default.
|
|
||||||
"""
|
|
||||||
return
|
|
||||||
|
|
||||||
async def _expire(self):
|
|
||||||
"""
|
|
||||||
Method to automatically expire a ticket.
|
|
||||||
|
|
||||||
May be overriden by the Ticket type for more complex expiry logic.
|
|
||||||
Must set `data.ticket_state` to `EXPIRED` if applicable.
|
|
||||||
"""
|
|
||||||
if self.state == TicketState.EXPIRING:
|
|
||||||
client.log(
|
|
||||||
"Automatically expiring ticket (tid:{}).".format(self.ticketid),
|
|
||||||
context="TICKETS"
|
|
||||||
)
|
|
||||||
try:
|
|
||||||
await self._revert(reason="Automatic Expiry")
|
|
||||||
except Exception:
|
|
||||||
# This should never happen in normal operation
|
|
||||||
client.log(
|
|
||||||
"Error while expiring ticket (tid:{})! "
|
|
||||||
"Exception traceback follows.\n{}".format(
|
|
||||||
self.ticketid,
|
|
||||||
traceback.format_exc()
|
|
||||||
),
|
|
||||||
context="TICKETS",
|
|
||||||
level=logging.ERROR
|
|
||||||
)
|
|
||||||
|
|
||||||
# Update state
|
|
||||||
self.data.ticket_state = TicketState.EXPIRED
|
|
||||||
|
|
||||||
# Update log message
|
|
||||||
await self.post()
|
|
||||||
|
|
||||||
# Post a note to the modlog
|
|
||||||
modlog = GuildSettings(self.data.guildid).mod_log.value
|
|
||||||
if modlog:
|
|
||||||
try:
|
|
||||||
await modlog.send(
|
|
||||||
embed=discord.Embed(
|
|
||||||
colour=discord.Colour.orange(),
|
|
||||||
description="[Ticket #{}]({}) expired!".format(self.data.guild_ticketid, self.link)
|
|
||||||
)
|
|
||||||
)
|
|
||||||
except discord.HTTPException:
|
|
||||||
pass
|
|
||||||
|
|
||||||
async def pardon(self, moderator, reason, timestamp=None):
|
|
||||||
"""
|
|
||||||
Pardon process for the ticket.
|
|
||||||
|
|
||||||
May be overidden by the Ticket type for more complex pardon logic.
|
|
||||||
Must set `data.ticket_state` to `PARDONED` if applicable.
|
|
||||||
"""
|
|
||||||
if self.state != TicketState.PARDONED:
|
|
||||||
if self.state in (TicketState.OPEN, TicketState.EXPIRING):
|
|
||||||
try:
|
|
||||||
await self._revert(reason="Pardoned by {}".format(moderator.id))
|
|
||||||
except Exception:
|
|
||||||
# This should never happen in normal operation
|
|
||||||
client.log(
|
|
||||||
"Error while pardoning ticket (tid:{})! "
|
|
||||||
"Exception traceback follows.\n{}".format(
|
|
||||||
self.ticketid,
|
|
||||||
traceback.format_exc()
|
|
||||||
),
|
|
||||||
context="TICKETS",
|
|
||||||
level=logging.ERROR
|
|
||||||
)
|
|
||||||
|
|
||||||
# Update state
|
|
||||||
with self.data.batch_update():
|
|
||||||
self.data.ticket_state = TicketState.PARDONED
|
|
||||||
self.data.pardoned_at = utc_now()
|
|
||||||
self.data.pardoned_by = moderator.id
|
|
||||||
self.data.pardoned_reason = reason
|
|
||||||
|
|
||||||
# Update (i.e. remove) expiry
|
|
||||||
self.update_expiry()
|
|
||||||
|
|
||||||
# Update log message
|
|
||||||
await self.post()
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def fetch_tickets(cls, *ticketids, **kwargs):
|
|
||||||
"""
|
|
||||||
Fetch tickets matching the given criteria (passed transparently to `select_where`).
|
|
||||||
Positional arguments are treated as `ticketids`, which are not supported in keyword arguments.
|
|
||||||
"""
|
|
||||||
if ticketids:
|
|
||||||
kwargs['ticketid'] = ticketids
|
|
||||||
|
|
||||||
# Set the ticket type to the class type if not specified
|
|
||||||
if cls._ticket_type and 'ticket_type' not in kwargs:
|
|
||||||
kwargs['ticket_type'] = cls._ticket_type
|
|
||||||
|
|
||||||
# This is actually mainly for caching, since we don't pass the data to the initialiser
|
|
||||||
rows = data.ticket_info.fetch_rows_where(
|
|
||||||
**kwargs
|
|
||||||
)
|
|
||||||
|
|
||||||
return [
|
|
||||||
cls._ticket_types[TicketType(row.ticket_type)](row.ticketid)
|
|
||||||
for row in rows
|
|
||||||
]
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def fetch(cls, ticketid):
|
|
||||||
"""
|
|
||||||
Return the Ticket with the given id, if found, or `None` otherwise.
|
|
||||||
"""
|
|
||||||
tickets = cls.fetch_tickets(ticketid)
|
|
||||||
return tickets[0] if tickets else None
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def register_ticket_type(cls, ticket_cls):
|
|
||||||
"""
|
|
||||||
Decorator to register a new Ticket subclass as a ticket type.
|
|
||||||
"""
|
|
||||||
cls._ticket_types[ticket_cls._ticket_type] = ticket_cls
|
|
||||||
return ticket_cls
|
|
||||||
|
|
||||||
|
|
||||||
@module.launch_task
|
|
||||||
async def load_expiring_tickets(client):
|
|
||||||
Ticket.load_expiring()
|
|
||||||
@@ -1,4 +0,0 @@
|
|||||||
from .Ticket import Ticket, TicketType, TicketState
|
|
||||||
from .studybans import StudyBanTicket
|
|
||||||
from .notes import NoteTicket
|
|
||||||
from .warns import WarnTicket
|
|
||||||
@@ -1,112 +0,0 @@
|
|||||||
"""
|
|
||||||
Note ticket implementation.
|
|
||||||
|
|
||||||
Guild moderators can add a note about a user, visible in their moderation history.
|
|
||||||
Notes appear in the moderation log and the user's ticket history, like any other ticket.
|
|
||||||
|
|
||||||
This module implements the Note TicketType and the `note` moderation command.
|
|
||||||
"""
|
|
||||||
from cmdClient.lib import ResponseTimedOut
|
|
||||||
|
|
||||||
from wards import guild_moderator
|
|
||||||
|
|
||||||
from ..module import module
|
|
||||||
from ..data import tickets
|
|
||||||
|
|
||||||
from .Ticket import Ticket, TicketType, TicketState
|
|
||||||
|
|
||||||
|
|
||||||
@Ticket.register_ticket_type
|
|
||||||
class NoteTicket(Ticket):
|
|
||||||
_ticket_type = TicketType.NOTE
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
async def create(cls, guildid, targetid, moderatorid, content, **kwargs):
|
|
||||||
"""
|
|
||||||
Create a new Note on a target.
|
|
||||||
|
|
||||||
`kwargs` are passed transparently to the table insert method.
|
|
||||||
"""
|
|
||||||
ticket_row = tickets.insert(
|
|
||||||
guildid=guildid,
|
|
||||||
targetid=targetid,
|
|
||||||
ticket_type=cls._ticket_type,
|
|
||||||
ticket_state=TicketState.OPEN,
|
|
||||||
moderator_id=moderatorid,
|
|
||||||
auto=False,
|
|
||||||
content=content,
|
|
||||||
**kwargs
|
|
||||||
)
|
|
||||||
|
|
||||||
# Create the note ticket
|
|
||||||
ticket = cls(ticket_row['ticketid'])
|
|
||||||
|
|
||||||
# Post the ticket and return
|
|
||||||
await ticket.post()
|
|
||||||
return ticket
|
|
||||||
|
|
||||||
|
|
||||||
@module.cmd(
|
|
||||||
"note",
|
|
||||||
group="Moderation",
|
|
||||||
desc="Add a Note to a member's record."
|
|
||||||
)
|
|
||||||
@guild_moderator()
|
|
||||||
async def cmd_note(ctx):
|
|
||||||
"""
|
|
||||||
Usage``:
|
|
||||||
{prefix}note @target
|
|
||||||
{prefix}note @target <content>
|
|
||||||
Description:
|
|
||||||
Add a note to the target's moderation record.
|
|
||||||
The note will appear in the moderation log and in the `tickets` command.
|
|
||||||
|
|
||||||
The `target` must be specificed by mention or user id.
|
|
||||||
If the `content` is not given, it will be prompted for.
|
|
||||||
Example:
|
|
||||||
{prefix}note {ctx.author.mention} Seen reading the `note` documentation.
|
|
||||||
"""
|
|
||||||
if not ctx.args:
|
|
||||||
return await ctx.error_reply(
|
|
||||||
"**Usage:** `{}note @target <content>`.".format(ctx.best_prefix)
|
|
||||||
)
|
|
||||||
|
|
||||||
# Extract the target. We don't require them to be in the server
|
|
||||||
splits = ctx.args.split(maxsplit=1)
|
|
||||||
target_str = splits[0].strip('<@!&> ')
|
|
||||||
if not target_str.isdigit():
|
|
||||||
return await ctx.error_reply(
|
|
||||||
"**Usage:** `{}note @target <content>`.\n"
|
|
||||||
"`target` must be provided by mention or userid.".format(ctx.best_prefix)
|
|
||||||
)
|
|
||||||
targetid = int(target_str)
|
|
||||||
|
|
||||||
# Extract or prompt for the content
|
|
||||||
if len(splits) != 2:
|
|
||||||
try:
|
|
||||||
content = await ctx.input("What note would you like to add?", timeout=300)
|
|
||||||
except ResponseTimedOut:
|
|
||||||
raise ResponseTimedOut("Prompt timed out, no note was created.")
|
|
||||||
else:
|
|
||||||
content = splits[1].strip()
|
|
||||||
|
|
||||||
# Create the note ticket
|
|
||||||
ticket = await NoteTicket.create(
|
|
||||||
ctx.guild.id,
|
|
||||||
targetid,
|
|
||||||
ctx.author.id,
|
|
||||||
content
|
|
||||||
)
|
|
||||||
|
|
||||||
if ticket.data.log_msg_id:
|
|
||||||
await ctx.embed_reply(
|
|
||||||
"Note on <@{}> created as [Ticket #{}]({}).".format(
|
|
||||||
targetid,
|
|
||||||
ticket.data.guild_ticketid,
|
|
||||||
ticket.link
|
|
||||||
)
|
|
||||||
)
|
|
||||||
else:
|
|
||||||
await ctx.embed_reply(
|
|
||||||
"Note on <@{}> created as Ticket #{}.".format(targetid, ticket.data.guild_ticketid)
|
|
||||||
)
|
|
||||||
@@ -1,126 +0,0 @@
|
|||||||
import datetime
|
|
||||||
import discord
|
|
||||||
|
|
||||||
from meta import client
|
|
||||||
from utils.lib import utc_now
|
|
||||||
from settings import GuildSettings
|
|
||||||
from data import NOT
|
|
||||||
|
|
||||||
from .. import data
|
|
||||||
from .Ticket import Ticket, TicketType, TicketState
|
|
||||||
|
|
||||||
|
|
||||||
@Ticket.register_ticket_type
|
|
||||||
class StudyBanTicket(Ticket):
|
|
||||||
_ticket_type = TicketType.STUDY_BAN
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
async def create(cls, guildid, targetid, moderatorid, reason, expiry=None, **kwargs):
|
|
||||||
"""
|
|
||||||
Create a new study ban ticket.
|
|
||||||
"""
|
|
||||||
# First create the ticket itself
|
|
||||||
ticket_row = data.tickets.insert(
|
|
||||||
guildid=guildid,
|
|
||||||
targetid=targetid,
|
|
||||||
ticket_type=cls._ticket_type,
|
|
||||||
ticket_state=TicketState.EXPIRING if expiry else TicketState.OPEN,
|
|
||||||
moderator_id=moderatorid,
|
|
||||||
auto=(moderatorid == client.user.id),
|
|
||||||
content=reason,
|
|
||||||
expiry=expiry,
|
|
||||||
**kwargs
|
|
||||||
)
|
|
||||||
|
|
||||||
# Create the Ticket
|
|
||||||
ticket = cls(ticket_row['ticketid'])
|
|
||||||
|
|
||||||
# Schedule ticket expiry, if applicable
|
|
||||||
if expiry:
|
|
||||||
ticket.update_expiry()
|
|
||||||
|
|
||||||
# Cancel any existing studyban expiry for this member
|
|
||||||
tickets = cls.fetch_tickets(
|
|
||||||
guildid=guildid,
|
|
||||||
ticketid=NOT(ticket_row['ticketid']),
|
|
||||||
targetid=targetid,
|
|
||||||
ticket_state=TicketState.EXPIRING
|
|
||||||
)
|
|
||||||
for ticket in tickets:
|
|
||||||
await ticket.cancel_expiry()
|
|
||||||
|
|
||||||
# Post the ticket
|
|
||||||
await ticket.post()
|
|
||||||
|
|
||||||
# Return the ticket
|
|
||||||
return ticket
|
|
||||||
|
|
||||||
async def _revert(self, reason=None):
|
|
||||||
"""
|
|
||||||
Revert the studyban by removing the role.
|
|
||||||
"""
|
|
||||||
guild_settings = GuildSettings(self.data.guildid)
|
|
||||||
role = guild_settings.studyban_role.value
|
|
||||||
target = self.target
|
|
||||||
|
|
||||||
if target and role:
|
|
||||||
try:
|
|
||||||
await target.remove_roles(
|
|
||||||
role,
|
|
||||||
reason="Reverting StudyBan: {}".format(reason)
|
|
||||||
)
|
|
||||||
except discord.HTTPException:
|
|
||||||
# TODO: Error log?
|
|
||||||
...
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
async def autoban(cls, guild, target, reason, **kwargs):
|
|
||||||
"""
|
|
||||||
Convenience method to automatically studyban a member, for the configured duration.
|
|
||||||
If the role is set, this will create and return a `StudyBanTicket` regardless of whether the
|
|
||||||
studyban was successful.
|
|
||||||
If the role is not set, or the ticket cannot be created, this will return `None`.
|
|
||||||
"""
|
|
||||||
# Get the studyban role, fail if there isn't one set, or the role doesn't exist
|
|
||||||
guild_settings = GuildSettings(guild.id)
|
|
||||||
role = guild_settings.studyban_role.value
|
|
||||||
if not role:
|
|
||||||
return None
|
|
||||||
|
|
||||||
# Attempt to add the role, record failure
|
|
||||||
try:
|
|
||||||
await target.add_roles(role, reason="Applying StudyBan: {}".format(reason[:400]))
|
|
||||||
except discord.HTTPException:
|
|
||||||
role_failed = True
|
|
||||||
else:
|
|
||||||
role_failed = False
|
|
||||||
|
|
||||||
# Calculate the applicable automatic duration and expiry
|
|
||||||
# First count the existing non-pardoned studybans for this target
|
|
||||||
studyban_count = data.tickets.select_one_where(
|
|
||||||
guildid=guild.id,
|
|
||||||
targetid=target.id,
|
|
||||||
ticket_type=cls._ticket_type,
|
|
||||||
ticket_state=NOT(TicketState.PARDONED),
|
|
||||||
select_columns=('COUNT(*)',)
|
|
||||||
)[0]
|
|
||||||
studyban_count = int(studyban_count)
|
|
||||||
|
|
||||||
# Then read the guild setting to find the applicable duration
|
|
||||||
studyban_durations = guild_settings.studyban_durations.value
|
|
||||||
if studyban_count < len(studyban_durations):
|
|
||||||
duration = studyban_durations[studyban_count]
|
|
||||||
expiry = utc_now() + datetime.timedelta(seconds=duration)
|
|
||||||
else:
|
|
||||||
duration = None
|
|
||||||
expiry = None
|
|
||||||
|
|
||||||
# Create the ticket and return
|
|
||||||
if role_failed:
|
|
||||||
kwargs['addendum'] = '\n'.join((
|
|
||||||
kwargs.get('addendum', ''),
|
|
||||||
"Could not add the studyban role! Please add the role manually and check my permissions."
|
|
||||||
))
|
|
||||||
return await cls.create(
|
|
||||||
guild.id, target.id, client.user.id, reason, duration=duration, expiry=expiry, **kwargs
|
|
||||||
)
|
|
||||||
@@ -1,153 +0,0 @@
|
|||||||
"""
|
|
||||||
Warn ticket implementation.
|
|
||||||
|
|
||||||
Guild moderators can officially warn a user via command.
|
|
||||||
This DMs the users with the warning.
|
|
||||||
"""
|
|
||||||
import datetime
|
|
||||||
import discord
|
|
||||||
from cmdClient.lib import ResponseTimedOut
|
|
||||||
|
|
||||||
from wards import guild_moderator
|
|
||||||
|
|
||||||
from ..module import module
|
|
||||||
from ..data import tickets
|
|
||||||
|
|
||||||
from .Ticket import Ticket, TicketType, TicketState
|
|
||||||
|
|
||||||
|
|
||||||
@Ticket.register_ticket_type
|
|
||||||
class WarnTicket(Ticket):
|
|
||||||
_ticket_type = TicketType.WARNING
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
async def create(cls, guildid, targetid, moderatorid, content, **kwargs):
|
|
||||||
"""
|
|
||||||
Create a new Warning for the target.
|
|
||||||
|
|
||||||
`kwargs` are passed transparently to the table insert method.
|
|
||||||
"""
|
|
||||||
ticket_row = tickets.insert(
|
|
||||||
guildid=guildid,
|
|
||||||
targetid=targetid,
|
|
||||||
ticket_type=cls._ticket_type,
|
|
||||||
ticket_state=TicketState.OPEN,
|
|
||||||
moderator_id=moderatorid,
|
|
||||||
content=content,
|
|
||||||
**kwargs
|
|
||||||
)
|
|
||||||
|
|
||||||
# Create the note ticket
|
|
||||||
ticket = cls(ticket_row['ticketid'])
|
|
||||||
|
|
||||||
# Post the ticket and return
|
|
||||||
await ticket.post()
|
|
||||||
return ticket
|
|
||||||
|
|
||||||
async def _revert(*args, **kwargs):
|
|
||||||
# Warnings don't have a revert process
|
|
||||||
pass
|
|
||||||
|
|
||||||
|
|
||||||
@module.cmd(
|
|
||||||
"warn",
|
|
||||||
group="Moderation",
|
|
||||||
desc="Officially warn a user for a misbehaviour."
|
|
||||||
)
|
|
||||||
@guild_moderator()
|
|
||||||
async def cmd_warn(ctx):
|
|
||||||
"""
|
|
||||||
Usage``:
|
|
||||||
{prefix}warn @target
|
|
||||||
{prefix}warn @target <reason>
|
|
||||||
Description:
|
|
||||||
|
|
||||||
The `target` must be specificed by mention or user id.
|
|
||||||
If the `reason` is not given, it will be prompted for.
|
|
||||||
Example:
|
|
||||||
{prefix}warn {ctx.author.mention} Don't actually read the documentation!
|
|
||||||
"""
|
|
||||||
if not ctx.args:
|
|
||||||
return await ctx.error_reply(
|
|
||||||
"**Usage:** `{}warn @target <reason>`.".format(ctx.best_prefix)
|
|
||||||
)
|
|
||||||
|
|
||||||
# Extract the target. We do require them to be in the server
|
|
||||||
splits = ctx.args.split(maxsplit=1)
|
|
||||||
target_str = splits[0].strip('<@!&> ')
|
|
||||||
if not target_str.isdigit():
|
|
||||||
return await ctx.error_reply(
|
|
||||||
"**Usage:** `{}warn @target <reason>`.\n"
|
|
||||||
"`target` must be provided by mention or userid.".format(ctx.best_prefix)
|
|
||||||
)
|
|
||||||
targetid = int(target_str)
|
|
||||||
target = ctx.guild.get_member(targetid)
|
|
||||||
if not target:
|
|
||||||
return await ctx.error_reply("Cannot warn a user who is not in the server!")
|
|
||||||
|
|
||||||
# Extract or prompt for the content
|
|
||||||
if len(splits) != 2:
|
|
||||||
try:
|
|
||||||
content = await ctx.input("Please give a reason for this warning!", timeout=300)
|
|
||||||
except ResponseTimedOut:
|
|
||||||
raise ResponseTimedOut("Prompt timed out, the member was not warned.")
|
|
||||||
else:
|
|
||||||
content = splits[1].strip()
|
|
||||||
|
|
||||||
# Create the warn ticket
|
|
||||||
ticket = await WarnTicket.create(
|
|
||||||
ctx.guild.id,
|
|
||||||
targetid,
|
|
||||||
ctx.author.id,
|
|
||||||
content
|
|
||||||
)
|
|
||||||
|
|
||||||
# Attempt to message the member
|
|
||||||
embed = discord.Embed(
|
|
||||||
title="You have received a warning!",
|
|
||||||
description=(
|
|
||||||
content
|
|
||||||
),
|
|
||||||
colour=discord.Colour.red(),
|
|
||||||
timestamp=datetime.datetime.utcnow()
|
|
||||||
)
|
|
||||||
embed.add_field(
|
|
||||||
name="Info",
|
|
||||||
value=(
|
|
||||||
"*Warnings appear in your moderation history. "
|
|
||||||
"Failure to comply, or repeated warnings, "
|
|
||||||
"may result in muting, studybanning, or server banning.*"
|
|
||||||
)
|
|
||||||
)
|
|
||||||
embed.set_footer(
|
|
||||||
icon_url=ctx.guild.icon_url,
|
|
||||||
text=ctx.guild.name
|
|
||||||
)
|
|
||||||
dm_msg = None
|
|
||||||
try:
|
|
||||||
dm_msg = await target.send(embed=embed)
|
|
||||||
except discord.HTTPException:
|
|
||||||
pass
|
|
||||||
|
|
||||||
# Get previous warnings
|
|
||||||
count = tickets.select_one_where(
|
|
||||||
guildid=ctx.guild.id,
|
|
||||||
targetid=targetid,
|
|
||||||
ticket_type=TicketType.WARNING,
|
|
||||||
ticket_state=[TicketState.OPEN, TicketState.EXPIRING],
|
|
||||||
select_columns=('COUNT(*)',)
|
|
||||||
)[0]
|
|
||||||
if count == 1:
|
|
||||||
prev_str = "This is their first warning."
|
|
||||||
else:
|
|
||||||
prev_str = "They now have `{}` warnings.".format(count)
|
|
||||||
|
|
||||||
await ctx.embed_reply(
|
|
||||||
"[Ticket #{}]({}): {} has been warned. {}\n{}".format(
|
|
||||||
ticket.data.guild_ticketid,
|
|
||||||
ticket.link,
|
|
||||||
target.mention,
|
|
||||||
prev_str,
|
|
||||||
"*Could not DM the user their warning!*" if not dm_msg else ''
|
|
||||||
)
|
|
||||||
)
|
|
||||||
@@ -1,5 +0,0 @@
|
|||||||
from . import module
|
|
||||||
|
|
||||||
from . import data
|
|
||||||
from . import config
|
|
||||||
from . import commands
|
|
||||||
@@ -1,14 +0,0 @@
|
|||||||
from .module import module
|
|
||||||
|
|
||||||
|
|
||||||
@module.cmd(
|
|
||||||
name="sponsors",
|
|
||||||
group="Meta",
|
|
||||||
desc="Check out our wonderful partners!",
|
|
||||||
)
|
|
||||||
async def cmd_sponsors(ctx):
|
|
||||||
"""
|
|
||||||
Usage``:
|
|
||||||
{prefix}sponsors
|
|
||||||
"""
|
|
||||||
await ctx.reply(**ctx.client.settings.sponsor_message.args(ctx))
|
|
||||||
@@ -1,92 +0,0 @@
|
|||||||
from cmdClient.checks import is_owner
|
|
||||||
|
|
||||||
from settings import AppSettings, Setting, KeyValueData, ListData
|
|
||||||
from settings.setting_types import Message, String, GuildIDList
|
|
||||||
|
|
||||||
from meta import client
|
|
||||||
from core.data import app_config
|
|
||||||
|
|
||||||
from .data import guild_whitelist
|
|
||||||
|
|
||||||
@AppSettings.attach_setting
|
|
||||||
class sponsor_prompt(String, KeyValueData, Setting):
|
|
||||||
attr_name = 'sponsor_prompt'
|
|
||||||
_default = None
|
|
||||||
|
|
||||||
write_ward = is_owner
|
|
||||||
|
|
||||||
display_name = 'sponsor_prompt'
|
|
||||||
category = 'Sponsors'
|
|
||||||
desc = "Text to send after core commands to encourage checking `sponsors`."
|
|
||||||
long_desc = (
|
|
||||||
"Text posted after several commands to encourage users to check the `sponsors` command. "
|
|
||||||
"Occurences of `{{prefix}}` will be replaced by the bot prefix."
|
|
||||||
)
|
|
||||||
|
|
||||||
_quote = False
|
|
||||||
|
|
||||||
_table_interface = app_config
|
|
||||||
_id_column = 'appid'
|
|
||||||
_key_column = 'key'
|
|
||||||
_value_column = 'value'
|
|
||||||
_key = 'sponsor_prompt'
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def _data_to_value(cls, id, data, **kwargs):
|
|
||||||
if data:
|
|
||||||
return data.replace("{prefix}", client.prefix)
|
|
||||||
else:
|
|
||||||
return None
|
|
||||||
|
|
||||||
@property
|
|
||||||
def success_response(self):
|
|
||||||
if self.value:
|
|
||||||
return "The sponsor prompt has been update."
|
|
||||||
else:
|
|
||||||
return "The sponsor prompt has been cleared."
|
|
||||||
|
|
||||||
|
|
||||||
@AppSettings.attach_setting
|
|
||||||
class sponsor_message(Message, KeyValueData, Setting):
|
|
||||||
attr_name = 'sponsor_message'
|
|
||||||
_default = '{"content": "Coming Soon!"}'
|
|
||||||
|
|
||||||
write_ward = is_owner
|
|
||||||
|
|
||||||
display_name = 'sponsor_message'
|
|
||||||
category = 'Sponsors'
|
|
||||||
desc = "`sponsors` command response."
|
|
||||||
|
|
||||||
long_desc = (
|
|
||||||
"Message to reply with when a user runs the `sponsors` command."
|
|
||||||
)
|
|
||||||
|
|
||||||
_table_interface = app_config
|
|
||||||
_id_column = 'appid'
|
|
||||||
_key_column = 'key'
|
|
||||||
_value_column = 'value'
|
|
||||||
_key = 'sponsor_message'
|
|
||||||
|
|
||||||
_cmd_str = "{prefix}sponsors --edit"
|
|
||||||
|
|
||||||
@property
|
|
||||||
def success_response(self):
|
|
||||||
return "The `sponsors` command message has been updated."
|
|
||||||
|
|
||||||
|
|
||||||
@AppSettings.attach_setting
|
|
||||||
class sponsor_guild_whitelist(GuildIDList, ListData, Setting):
|
|
||||||
attr_name = 'sponsor_guild_whitelist'
|
|
||||||
write_ward = is_owner
|
|
||||||
|
|
||||||
category = 'Sponsors'
|
|
||||||
display_name = 'sponsor_hidden_in'
|
|
||||||
desc = "Guilds where the sponsor prompt is not displayed."
|
|
||||||
long_desc = (
|
|
||||||
"A list of guilds where the sponsor prompt hint will be hidden (see the `sponsor_prompt` setting)."
|
|
||||||
)
|
|
||||||
|
|
||||||
_table_interface = guild_whitelist
|
|
||||||
_id_column = 'appid'
|
|
||||||
_data_column = 'guildid'
|
|
||||||
_force_unique = True
|
|
||||||
@@ -1,4 +0,0 @@
|
|||||||
from data import Table
|
|
||||||
|
|
||||||
|
|
||||||
guild_whitelist = Table("sponsor_guild_whitelist")
|
|
||||||
@@ -1,33 +0,0 @@
|
|||||||
import discord
|
|
||||||
|
|
||||||
from LionModule import LionModule
|
|
||||||
from LionContext import LionContext
|
|
||||||
|
|
||||||
from meta import client
|
|
||||||
|
|
||||||
|
|
||||||
module = LionModule("Sponsor")
|
|
||||||
|
|
||||||
|
|
||||||
sponsored_commands = {'profile', 'stats', 'weekly', 'monthly'}
|
|
||||||
|
|
||||||
|
|
||||||
@LionContext.reply.add_wrapper
|
|
||||||
async def sponsor_reply_wrapper(func, ctx: LionContext, *args, **kwargs):
|
|
||||||
if ctx.cmd and ctx.cmd.name in sponsored_commands:
|
|
||||||
if (prompt := ctx.client.settings.sponsor_prompt.value):
|
|
||||||
if ctx.guild:
|
|
||||||
show = ctx.guild.id not in ctx.client.settings.sponsor_guild_whitelist.value
|
|
||||||
show = show and not ctx.client.data.premium_guilds.queries.fetch_guild(ctx.guild.id)
|
|
||||||
else:
|
|
||||||
show = True
|
|
||||||
|
|
||||||
if show:
|
|
||||||
sponsor_hint = discord.Embed(
|
|
||||||
description=prompt,
|
|
||||||
colour=discord.Colour.dark_theme()
|
|
||||||
)
|
|
||||||
if 'embed' not in kwargs:
|
|
||||||
kwargs['embed'] = sponsor_hint
|
|
||||||
|
|
||||||
return await func(ctx, *args, **kwargs)
|
|
||||||
@@ -1,6 +0,0 @@
|
|||||||
from .module import module
|
|
||||||
|
|
||||||
from . import webhook
|
|
||||||
from . import commands
|
|
||||||
from . import data
|
|
||||||
from . import settings
|
|
||||||
@@ -1,77 +0,0 @@
|
|||||||
import discord
|
|
||||||
from .module import module
|
|
||||||
from cmdClient.checks import is_owner
|
|
||||||
from settings.user_settings import UserSettings
|
|
||||||
from LionContext import LionContext
|
|
||||||
|
|
||||||
from .webhook import on_dbl_vote
|
|
||||||
from .utils import lion_loveemote
|
|
||||||
|
|
||||||
|
|
||||||
@module.cmd(
|
|
||||||
"forcevote",
|
|
||||||
desc="Simulate a Topgg Vote from the given user.",
|
|
||||||
group="Bot Admin",
|
|
||||||
)
|
|
||||||
@is_owner()
|
|
||||||
async def cmd_forcevote(ctx: LionContext):
|
|
||||||
"""
|
|
||||||
Usage``:
|
|
||||||
{prefix}forcevote
|
|
||||||
Description:
|
|
||||||
Simulate Top.gg vote without actually a confirmation from Topgg site.
|
|
||||||
|
|
||||||
Can be used for force a vote for testing or if topgg has an error or production time bot error.
|
|
||||||
"""
|
|
||||||
target = ctx.author
|
|
||||||
|
|
||||||
# Identify the target
|
|
||||||
if ctx.args:
|
|
||||||
if not ctx.msg.mentions:
|
|
||||||
return await ctx.error_reply("Please mention a user to simulate a vote!")
|
|
||||||
target = ctx.msg.mentions[0]
|
|
||||||
|
|
||||||
await on_dbl_vote({"user": target.id, "type": "test"})
|
|
||||||
return await ctx.reply('Topgg vote simulation successful on {}'.format(target), suggest_vote=False)
|
|
||||||
|
|
||||||
|
|
||||||
@module.cmd(
|
|
||||||
"vote",
|
|
||||||
desc="[Vote](https://top.gg/bot/889078613817831495/vote) for me to get 25% more LCs!",
|
|
||||||
group="Economy",
|
|
||||||
aliases=('topgg', 'topggvote', 'upvote')
|
|
||||||
)
|
|
||||||
async def cmd_vote(ctx: LionContext):
|
|
||||||
"""
|
|
||||||
Usage``:
|
|
||||||
{prefix}vote
|
|
||||||
Description:
|
|
||||||
Get Top.gg bot's link for +25% Economy boost.
|
|
||||||
"""
|
|
||||||
embed = discord.Embed(
|
|
||||||
title="Claim your boost!",
|
|
||||||
description=(
|
|
||||||
"Please click [here](https://top.gg/bot/889078613817831495/vote) to vote and support our bot!\n\n"
|
|
||||||
"Thank you! {}.".format(lion_loveemote)
|
|
||||||
),
|
|
||||||
colour=discord.Colour.orange()
|
|
||||||
).set_thumbnail(
|
|
||||||
url="https://cdn.discordapp.com/attachments/908283085999706153/933012309532614666/lion-love.png"
|
|
||||||
)
|
|
||||||
return await ctx.reply(embed=embed, suggest_vote=False)
|
|
||||||
|
|
||||||
|
|
||||||
@module.cmd(
|
|
||||||
"vote_reminder",
|
|
||||||
group="Personal Settings",
|
|
||||||
desc="Turn on/off boost reminders."
|
|
||||||
)
|
|
||||||
async def cmd_remind_vote(ctx: LionContext):
|
|
||||||
"""
|
|
||||||
Usage:
|
|
||||||
`{prefix}vote_reminder on`
|
|
||||||
`{prefix}vote_reminder off`
|
|
||||||
|
|
||||||
Enable or disable DM boost reminders.
|
|
||||||
"""
|
|
||||||
await UserSettings.settings.vote_remainder.command(ctx, ctx.author.id)
|
|
||||||
@@ -1,9 +0,0 @@
|
|||||||
from data.interfaces import RowTable, Table
|
|
||||||
|
|
||||||
topggvotes = RowTable(
|
|
||||||
'topgg',
|
|
||||||
('voteid', 'userid', 'boostedTimestamp'),
|
|
||||||
'voteid'
|
|
||||||
)
|
|
||||||
|
|
||||||
guild_whitelist = Table('topgg_guild_whitelist')
|
|
||||||
@@ -1,80 +0,0 @@
|
|||||||
from LionModule import LionModule
|
|
||||||
from LionContext import LionContext
|
|
||||||
from core.lion import Lion
|
|
||||||
|
|
||||||
from modules.sponsors.module import sponsored_commands
|
|
||||||
|
|
||||||
from .utils import get_last_voted_timestamp, lion_loveemote, lion_yayemote
|
|
||||||
from .webhook import init_webhook
|
|
||||||
|
|
||||||
module = LionModule("Topgg")
|
|
||||||
|
|
||||||
upvote_info = "You have a boost available {}, to support our project and earn **25% more LionCoins** type `{}vote` {}"
|
|
||||||
|
|
||||||
|
|
||||||
@module.launch_task
|
|
||||||
async def attach_topgg_webhook(client):
|
|
||||||
if client.shard_id == 0:
|
|
||||||
init_webhook()
|
|
||||||
client.log("Attached top.gg voiting webhook.", context="TOPGG")
|
|
||||||
|
|
||||||
|
|
||||||
@module.launch_task
|
|
||||||
async def register_hook(client):
|
|
||||||
LionContext.reply.add_wrapper(topgg_reply_wrapper)
|
|
||||||
Lion.register_economy_bonus(economy_bonus)
|
|
||||||
|
|
||||||
client.log("Loaded top.gg hooks.", context="TOPGG")
|
|
||||||
|
|
||||||
|
|
||||||
@module.unload_task
|
|
||||||
async def unregister_hook(client):
|
|
||||||
Lion.unregister_economy_bonus(economy_bonus)
|
|
||||||
LionContext.reply.remove_wrapper(topgg_reply_wrapper.__name__)
|
|
||||||
|
|
||||||
client.log("Unloaded top.gg hooks.", context="TOPGG")
|
|
||||||
|
|
||||||
boostfree_groups = {'Meta'}
|
|
||||||
boostfree_commands = {'config', 'pomodoro'}
|
|
||||||
|
|
||||||
|
|
||||||
async def topgg_reply_wrapper(func, ctx: LionContext, *args, suggest_vote=True, **kwargs):
|
|
||||||
if not suggest_vote:
|
|
||||||
pass
|
|
||||||
elif not ctx.cmd:
|
|
||||||
pass
|
|
||||||
elif ctx.cmd.name in boostfree_commands or ctx.cmd.group in boostfree_groups:
|
|
||||||
pass
|
|
||||||
elif ctx.guild and ctx.guild.id in ctx.client.settings.topgg_guild_whitelist.value:
|
|
||||||
pass
|
|
||||||
elif ctx.guild and ctx.client.data.premium_guilds.queries.fetch_guild(ctx.guild.id):
|
|
||||||
pass
|
|
||||||
elif not get_last_voted_timestamp(ctx.author.id):
|
|
||||||
upvote_info_formatted = upvote_info.format(lion_yayemote, ctx.best_prefix, lion_loveemote)
|
|
||||||
|
|
||||||
if 'embed' in kwargs and ctx.cmd.name not in sponsored_commands:
|
|
||||||
# Add message as an extra embed field
|
|
||||||
kwargs['embed'].add_field(
|
|
||||||
name="\u200b",
|
|
||||||
value=(
|
|
||||||
upvote_info_formatted
|
|
||||||
),
|
|
||||||
inline=False
|
|
||||||
)
|
|
||||||
else:
|
|
||||||
# Add message to content
|
|
||||||
if 'content' in kwargs and kwargs['content']:
|
|
||||||
if len(kwargs['content']) + len(upvote_info_formatted) < 1998:
|
|
||||||
kwargs['content'] += '\n\n' + upvote_info_formatted
|
|
||||||
elif args:
|
|
||||||
if len(args[0]) + len(upvote_info_formatted) < 1998:
|
|
||||||
args = list(args)
|
|
||||||
args[0] += '\n\n' + upvote_info_formatted
|
|
||||||
else:
|
|
||||||
kwargs['content'] = upvote_info_formatted
|
|
||||||
|
|
||||||
return await func(ctx, *args, **kwargs)
|
|
||||||
|
|
||||||
|
|
||||||
def economy_bonus(lion):
|
|
||||||
return 1.25 if get_last_voted_timestamp(lion.userid) else 1
|
|
||||||
@@ -1,72 +0,0 @@
|
|||||||
from cmdClient.checks import is_owner
|
|
||||||
|
|
||||||
from settings import UserSettings, UserSetting, AppSettings
|
|
||||||
from settings.base import ListData, Setting
|
|
||||||
from settings.setting_types import Boolean, GuildIDList
|
|
||||||
|
|
||||||
from modules.reminders.reminder import Reminder
|
|
||||||
from modules.reminders.data import reminders
|
|
||||||
|
|
||||||
from .utils import create_remainder, remainder_content, topgg_upvote_link
|
|
||||||
from .data import guild_whitelist
|
|
||||||
|
|
||||||
|
|
||||||
@UserSettings.attach_setting
|
|
||||||
class topgg_vote_remainder(Boolean, UserSetting):
|
|
||||||
attr_name = 'vote_remainder'
|
|
||||||
_data_column = 'topgg_vote_reminder'
|
|
||||||
|
|
||||||
_default = True
|
|
||||||
|
|
||||||
display_name = 'vote_reminder'
|
|
||||||
desc = r"Toggle automatic reminders to support me for a 25% LionCoin boost."
|
|
||||||
long_desc = (
|
|
||||||
"Did you know that you can [vote for me]({vote_link}) to help me help other people reach their goals? "
|
|
||||||
"And you get a **25% boost** to all LionCoin income you make across all servers!\n"
|
|
||||||
"Enable this setting if you want me to let you know when you can vote again!"
|
|
||||||
).format(vote_link=topgg_upvote_link)
|
|
||||||
|
|
||||||
@property
|
|
||||||
def success_response(self):
|
|
||||||
if self.value:
|
|
||||||
# Check if reminder is already running
|
|
||||||
create_remainder(self.id)
|
|
||||||
|
|
||||||
return (
|
|
||||||
"Thank you for supporting me! I will remind in your DMs when you can vote next! "
|
|
||||||
"(Please make sure your DMs are open, otherwise I can't reach you!)"
|
|
||||||
)
|
|
||||||
else:
|
|
||||||
# Check if reminder is already running and get its id
|
|
||||||
r = reminders.select_one_where(
|
|
||||||
userid=self.id,
|
|
||||||
select_columns='reminderid',
|
|
||||||
content=remainder_content,
|
|
||||||
_extra="ORDER BY remind_at DESC LIMIT 1"
|
|
||||||
)
|
|
||||||
|
|
||||||
# Cancel and delete Remainder if already running
|
|
||||||
if r:
|
|
||||||
Reminder.delete(r['reminderid'])
|
|
||||||
|
|
||||||
return (
|
|
||||||
"I will no longer send you voting reminders."
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
@AppSettings.attach_setting
|
|
||||||
class topgg_guild_whitelist(GuildIDList, ListData, Setting):
|
|
||||||
attr_name = 'topgg_guild_whitelist'
|
|
||||||
write_ward = is_owner
|
|
||||||
|
|
||||||
category = 'Topgg Voting'
|
|
||||||
display_name = 'topgg_hidden_in'
|
|
||||||
desc = "Guilds where the topgg vote prompt is not displayed."
|
|
||||||
long_desc = (
|
|
||||||
"A list of guilds where the topgg vote prompt will be hidden."
|
|
||||||
)
|
|
||||||
|
|
||||||
_table_interface = guild_whitelist
|
|
||||||
_id_column = 'appid'
|
|
||||||
_data_column = 'guildid'
|
|
||||||
_force_unique = True
|
|
||||||
@@ -1,97 +0,0 @@
|
|||||||
import discord
|
|
||||||
import datetime
|
|
||||||
|
|
||||||
from meta import sharding
|
|
||||||
from meta import conf
|
|
||||||
from meta.client import client
|
|
||||||
from utils.lib import utc_now
|
|
||||||
from settings.setting_types import Integer
|
|
||||||
|
|
||||||
from modules.reminders.reminder import Reminder
|
|
||||||
from modules.reminders.data import reminders
|
|
||||||
|
|
||||||
from . import data as db
|
|
||||||
from data.conditions import GEQ
|
|
||||||
|
|
||||||
topgg_upvote_link = 'https://top.gg/bot/889078613817831495/vote'
|
|
||||||
remainder_content = (
|
|
||||||
"You can now vote again on top.gg!\n"
|
|
||||||
"Click [here]({}) to vote, thank you for the support!"
|
|
||||||
).format(topgg_upvote_link)
|
|
||||||
|
|
||||||
lion_loveemote = conf.emojis.getemoji('lionlove')
|
|
||||||
lion_yayemote = conf.emojis.getemoji('lionyay')
|
|
||||||
|
|
||||||
|
|
||||||
def get_last_voted_timestamp(userid: Integer):
|
|
||||||
"""
|
|
||||||
Will return None if user has not voted in [-12.5hrs till now]
|
|
||||||
else will return a Tuple containing timestamp of when exactly she voted
|
|
||||||
"""
|
|
||||||
return db.topggvotes.select_one_where(
|
|
||||||
userid=userid,
|
|
||||||
select_columns="boostedTimestamp",
|
|
||||||
boostedTimestamp=GEQ(utc_now() - datetime.timedelta(hours=12.5)),
|
|
||||||
_extra="ORDER BY boostedTimestamp DESC LIMIT 1"
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
def create_remainder(userid):
|
|
||||||
"""
|
|
||||||
Checks if a remainder is already running (immaterial of remind_at time)
|
|
||||||
If no remainder exists creates a new remainder and schedules it
|
|
||||||
"""
|
|
||||||
if not reminders.select_one_where(
|
|
||||||
userid=userid,
|
|
||||||
content=remainder_content,
|
|
||||||
_extra="ORDER BY remind_at DESC LIMIT 1"
|
|
||||||
):
|
|
||||||
last_vote_time = get_last_voted_timestamp(userid)
|
|
||||||
|
|
||||||
# if no, Create reminder
|
|
||||||
reminder = Reminder.create(
|
|
||||||
userid=userid,
|
|
||||||
# TODO using content as a selector is not a good method
|
|
||||||
content=remainder_content,
|
|
||||||
message_link=None,
|
|
||||||
interval=None,
|
|
||||||
title="Your boost is now available! {}".format(lion_yayemote),
|
|
||||||
footer="Use `{}vote_reminder off` to stop receiving reminders.".format(client.prefix),
|
|
||||||
remind_at=(
|
|
||||||
last_vote_time[0] + datetime.timedelta(hours=12.5)
|
|
||||||
if last_vote_time else
|
|
||||||
utc_now() + datetime.timedelta(minutes=5)
|
|
||||||
)
|
|
||||||
# remind_at=datetime.datetime.utcnow() + datetime.timedelta(minutes=2)
|
|
||||||
)
|
|
||||||
|
|
||||||
# Schedule reminder
|
|
||||||
if sharding.shard_number == 0:
|
|
||||||
reminder.schedule()
|
|
||||||
|
|
||||||
|
|
||||||
async def send_user_dm(userid):
|
|
||||||
# Send the message, if possible
|
|
||||||
if not (user := client.get_user(userid)):
|
|
||||||
try:
|
|
||||||
user = await client.fetch_user(userid)
|
|
||||||
except discord.HTTPException:
|
|
||||||
pass
|
|
||||||
if user:
|
|
||||||
try:
|
|
||||||
embed = discord.Embed(
|
|
||||||
title="Thank you for supporting our bot on Top.gg! {}".format(lion_yayemote),
|
|
||||||
description=(
|
|
||||||
"By voting every 12 hours you will allow us to reach and help "
|
|
||||||
"even more students all over the world.\n"
|
|
||||||
"Thank you for supporting us, enjoy your LionCoins boost!"
|
|
||||||
),
|
|
||||||
colour=discord.Colour.orange()
|
|
||||||
).set_image(
|
|
||||||
url="https://cdn.discordapp.com/attachments/908283085999706153/932737228440993822/lion-yay.png"
|
|
||||||
)
|
|
||||||
|
|
||||||
await user.send(embed=embed)
|
|
||||||
except discord.HTTPException:
|
|
||||||
# Nothing we can really do here. Maybe tell the user about their reminder next time?
|
|
||||||
pass
|
|
||||||
@@ -1,40 +0,0 @@
|
|||||||
from meta.client import client
|
|
||||||
from settings.user_settings import UserSettings
|
|
||||||
from utils.lib import utc_now
|
|
||||||
from meta.config import conf
|
|
||||||
|
|
||||||
import topgg
|
|
||||||
from .utils import db, send_user_dm, create_remainder
|
|
||||||
|
|
||||||
|
|
||||||
@client.event
|
|
||||||
async def on_dbl_vote(data):
|
|
||||||
"""An event that is called whenever someone votes for the bot on Top.gg."""
|
|
||||||
client.log(f"Received a vote: \n{data}", context='Topgg')
|
|
||||||
|
|
||||||
db.topggvotes.insert(
|
|
||||||
userid=data['user'],
|
|
||||||
boostedTimestamp=utc_now()
|
|
||||||
)
|
|
||||||
|
|
||||||
await send_user_dm(data['user'])
|
|
||||||
|
|
||||||
if UserSettings.settings.vote_remainder.value:
|
|
||||||
create_remainder(data['user'])
|
|
||||||
|
|
||||||
if data["type"] == "test":
|
|
||||||
return client.dispatch("dbl_test", data)
|
|
||||||
|
|
||||||
|
|
||||||
@client.event
|
|
||||||
async def on_dbl_test(data):
|
|
||||||
"""An event that is called whenever someone tests the webhook system for your bot on Top.gg."""
|
|
||||||
client.log(f"Received a test vote:\n{data}", context='Topgg')
|
|
||||||
|
|
||||||
|
|
||||||
def init_webhook():
|
|
||||||
client.topgg_webhook = topgg.WebhookManager(client).dbl_webhook(
|
|
||||||
conf.bot.get("topgg_route"),
|
|
||||||
conf.bot.get("topgg_password")
|
|
||||||
)
|
|
||||||
client.topgg_webhook.run(conf.bot.get("topgg_port"))
|
|
||||||
@@ -1,5 +0,0 @@
|
|||||||
from .module import module
|
|
||||||
|
|
||||||
from . import admin
|
|
||||||
from . import data
|
|
||||||
from . import tracker
|
|
||||||
@@ -1,83 +0,0 @@
|
|||||||
from settings import GuildSettings, GuildSetting
|
|
||||||
from wards import guild_admin
|
|
||||||
|
|
||||||
import settings
|
|
||||||
|
|
||||||
from .data import workout_channels
|
|
||||||
|
|
||||||
|
|
||||||
@GuildSettings.attach_setting
|
|
||||||
class workout_length(settings.Integer, GuildSetting):
|
|
||||||
category = "Workout"
|
|
||||||
|
|
||||||
attr_name = "min_workout_length"
|
|
||||||
_data_column = "min_workout_length"
|
|
||||||
|
|
||||||
display_name = "min_workout_length"
|
|
||||||
desc = "Minimum length of a workout."
|
|
||||||
|
|
||||||
_default = 20
|
|
||||||
|
|
||||||
long_desc = (
|
|
||||||
"Minimun time a user must spend in a workout channel for it to count as a valid workout. "
|
|
||||||
"Value must be given in minutes."
|
|
||||||
)
|
|
||||||
_accepts = "An integer number of minutes."
|
|
||||||
|
|
||||||
@property
|
|
||||||
def success_response(self):
|
|
||||||
return "The minimum workout length is now `{}` minutes.".format(self.formatted)
|
|
||||||
|
|
||||||
|
|
||||||
@GuildSettings.attach_setting
|
|
||||||
class workout_reward(settings.Integer, GuildSetting):
|
|
||||||
category = "Workout"
|
|
||||||
|
|
||||||
attr_name = "workout_reward"
|
|
||||||
_data_column = "workout_reward"
|
|
||||||
|
|
||||||
display_name = "workout_reward"
|
|
||||||
desc = "Number of daily LionCoins to reward for completing a workout."
|
|
||||||
|
|
||||||
_default = 350
|
|
||||||
|
|
||||||
long_desc = (
|
|
||||||
"Number of LionCoins given when a member completes their daily workout."
|
|
||||||
)
|
|
||||||
_accepts = "An integer number of LionCoins."
|
|
||||||
|
|
||||||
@property
|
|
||||||
def success_response(self):
|
|
||||||
return "The workout reward is now `{}` LionCoins.".format(self.formatted)
|
|
||||||
|
|
||||||
|
|
||||||
@GuildSettings.attach_setting
|
|
||||||
class workout_channels_setting(settings.ChannelList, settings.ListData, settings.Setting):
|
|
||||||
category = "Workout"
|
|
||||||
|
|
||||||
attr_name = 'workout_channels'
|
|
||||||
|
|
||||||
_table_interface = workout_channels
|
|
||||||
_id_column = 'guildid'
|
|
||||||
_data_column = 'channelid'
|
|
||||||
_setting = settings.VoiceChannel
|
|
||||||
|
|
||||||
write_ward = guild_admin
|
|
||||||
display_name = "workout_channels"
|
|
||||||
desc = "Channels in which members can do workouts."
|
|
||||||
|
|
||||||
_force_unique = True
|
|
||||||
|
|
||||||
long_desc = (
|
|
||||||
"Sessions in these channels will be treated as workouts."
|
|
||||||
)
|
|
||||||
|
|
||||||
# Flat cache, no need to expire objects
|
|
||||||
_cache = {}
|
|
||||||
|
|
||||||
@property
|
|
||||||
def success_response(self):
|
|
||||||
if self.value:
|
|
||||||
return "The workout channels have been updated:\n{}".format(self.formatted)
|
|
||||||
else:
|
|
||||||
return "The workout channels have been removed."
|
|
||||||
@@ -1,10 +0,0 @@
|
|||||||
from data import Table, RowTable
|
|
||||||
|
|
||||||
|
|
||||||
workout_channels = Table('workout_channels')
|
|
||||||
|
|
||||||
workout_sessions = RowTable(
|
|
||||||
'workout_sessions',
|
|
||||||
('sessionid', 'guildid', 'userid', 'start_time', 'duration', 'channelid'),
|
|
||||||
'sessionid'
|
|
||||||
)
|
|
||||||
@@ -1,4 +0,0 @@
|
|||||||
from LionModule import LionModule
|
|
||||||
|
|
||||||
|
|
||||||
module = LionModule("Workout")
|
|
||||||
@@ -1,256 +0,0 @@
|
|||||||
import asyncio
|
|
||||||
import logging
|
|
||||||
import datetime as dt
|
|
||||||
import discord
|
|
||||||
|
|
||||||
from core import Lion
|
|
||||||
from settings import GuildSettings
|
|
||||||
from meta import client
|
|
||||||
from data import NULL, tables
|
|
||||||
from data.conditions import THIS_SHARD
|
|
||||||
|
|
||||||
from .module import module
|
|
||||||
from .data import workout_sessions
|
|
||||||
from . import admin
|
|
||||||
|
|
||||||
|
|
||||||
leave_tasks = {}
|
|
||||||
|
|
||||||
|
|
||||||
async def on_workout_join(member):
|
|
||||||
key = (member.guild.id, member.id)
|
|
||||||
|
|
||||||
# Cancel a leave task if the member rejoined in time
|
|
||||||
if member.id in leave_tasks:
|
|
||||||
leave_tasks[key].cancel()
|
|
||||||
leave_tasks.pop(key)
|
|
||||||
return
|
|
||||||
|
|
||||||
# Create a started workout entry
|
|
||||||
workout = workout_sessions.create_row(
|
|
||||||
guildid=member.guild.id,
|
|
||||||
userid=member.id,
|
|
||||||
channelid=member.voice.channel.id
|
|
||||||
)
|
|
||||||
|
|
||||||
# Add to current workouts
|
|
||||||
client.objects['current_workouts'][key] = workout
|
|
||||||
|
|
||||||
# Log
|
|
||||||
client.log(
|
|
||||||
"User '{m.name}'(uid:{m.id}) started a workout in channel "
|
|
||||||
"'{m.voice.channel.name}' (cid:{m.voice.channel.id}) "
|
|
||||||
"of guild '{m.guild.name}' (gid:{m.guild.id}).".format(m=member),
|
|
||||||
context="WORKOUT_STARTED"
|
|
||||||
)
|
|
||||||
GuildSettings(member.guild.id).event_log.log(
|
|
||||||
"{} started a workout in {}".format(
|
|
||||||
member.mention,
|
|
||||||
member.voice.channel.mention
|
|
||||||
), title="Workout Started"
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
async def on_workout_leave(member):
|
|
||||||
key = (member.guild.id, member.id)
|
|
||||||
|
|
||||||
# Create leave task in case of temporary disconnect
|
|
||||||
task = asyncio.create_task(asyncio.sleep(3))
|
|
||||||
leave_tasks[key] = task
|
|
||||||
|
|
||||||
# Wait for the leave task, abort if it gets cancelled
|
|
||||||
try:
|
|
||||||
await task
|
|
||||||
if member.id in leave_tasks:
|
|
||||||
if leave_tasks[key] == task:
|
|
||||||
leave_tasks.pop(key)
|
|
||||||
else:
|
|
||||||
return
|
|
||||||
except asyncio.CancelledError:
|
|
||||||
# Task was cancelled by rejoining
|
|
||||||
if key in leave_tasks and leave_tasks[key] == task:
|
|
||||||
leave_tasks.pop(key)
|
|
||||||
return
|
|
||||||
|
|
||||||
# Retrieve workout row and remove from current workouts
|
|
||||||
workout = client.objects['current_workouts'].pop(key)
|
|
||||||
|
|
||||||
await workout_left(member, workout)
|
|
||||||
|
|
||||||
|
|
||||||
async def workout_left(member, workout):
|
|
||||||
time_diff = (dt.datetime.utcnow() - workout.start_time).total_seconds()
|
|
||||||
min_length = GuildSettings(member.guild.id).min_workout_length.value
|
|
||||||
if time_diff < 60 * min_length:
|
|
||||||
# Left workout before it was finished. Log and delete
|
|
||||||
client.log(
|
|
||||||
"User '{m.name}'(uid:{m.id}) left their workout in guild '{m.guild.name}' (gid:{m.guild.id}) "
|
|
||||||
"before it was complete! ({diff:.2f} minutes). Deleting workout.\n"
|
|
||||||
"{workout}".format(
|
|
||||||
m=member,
|
|
||||||
diff=time_diff / 60,
|
|
||||||
workout=workout
|
|
||||||
),
|
|
||||||
context="WORKOUT_ABORTED",
|
|
||||||
post=True
|
|
||||||
)
|
|
||||||
GuildSettings(member.guild.id).event_log.log(
|
|
||||||
"{} left their workout before it was complete! (`{:.2f}` minutes)".format(
|
|
||||||
member.mention,
|
|
||||||
time_diff / 60,
|
|
||||||
), title="Workout Left"
|
|
||||||
)
|
|
||||||
workout_sessions.delete_where(sessionid=workout.sessionid)
|
|
||||||
else:
|
|
||||||
# Completed the workout
|
|
||||||
client.log(
|
|
||||||
"User '{m.name}'(uid:{m.id}) completed their daily workout in guild '{m.guild.name}' (gid:{m.guild.id}) "
|
|
||||||
"({diff:.2f} minutes). Saving workout and notifying user.\n"
|
|
||||||
"{workout}".format(
|
|
||||||
m=member,
|
|
||||||
diff=time_diff / 60,
|
|
||||||
workout=workout
|
|
||||||
),
|
|
||||||
context="WORKOUT_COMPLETED",
|
|
||||||
post=True
|
|
||||||
)
|
|
||||||
workout.duration = time_diff
|
|
||||||
await workout_complete(member, workout)
|
|
||||||
|
|
||||||
|
|
||||||
async def workout_complete(member, workout):
|
|
||||||
key = (member.guild.id, member.id)
|
|
||||||
|
|
||||||
# update and notify
|
|
||||||
user = Lion.fetch(*key)
|
|
||||||
user_data = user.data
|
|
||||||
with user_data.batch_update():
|
|
||||||
user_data.workout_count = user_data.workout_count + 1
|
|
||||||
user_data.last_workout_start = workout.start_time
|
|
||||||
|
|
||||||
settings = GuildSettings(member.guild.id)
|
|
||||||
reward = settings.workout_reward.value
|
|
||||||
user.addCoins(reward, bonus=True)
|
|
||||||
|
|
||||||
settings.event_log.log(
|
|
||||||
"{} completed their daily workout and was rewarded `{}` coins! (`{:.2f}` minutes)".format(
|
|
||||||
member.mention,
|
|
||||||
int(reward * user.economy_bonus),
|
|
||||||
workout.duration / 60,
|
|
||||||
), title="Workout Completed"
|
|
||||||
)
|
|
||||||
|
|
||||||
embed = discord.Embed(
|
|
||||||
description=(
|
|
||||||
"Congratulations on completing your daily workout!\n"
|
|
||||||
"You have been rewarded with `{}` LionCoins. Good job!".format(int(reward * user.economy_bonus))
|
|
||||||
),
|
|
||||||
timestamp=dt.datetime.utcnow(),
|
|
||||||
colour=discord.Color.orange()
|
|
||||||
)
|
|
||||||
embed.set_footer(
|
|
||||||
text=member.guild.name,
|
|
||||||
icon_url=member.guild.icon_url
|
|
||||||
)
|
|
||||||
try:
|
|
||||||
await member.send(embed=embed)
|
|
||||||
except discord.Forbidden:
|
|
||||||
client.log(
|
|
||||||
"Couldn't notify user '{m.name}'(uid:{m.id}) about their completed workout! "
|
|
||||||
"They might have me blocked.".format(m=member),
|
|
||||||
context="WORKOUT_COMPLETED",
|
|
||||||
post=True
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
@client.add_after_event("voice_state_update")
|
|
||||||
async def workout_voice_tracker(client, member, before, after):
|
|
||||||
# Wait until launch tasks are complete
|
|
||||||
while not module.ready:
|
|
||||||
await asyncio.sleep(0.1)
|
|
||||||
|
|
||||||
if member.bot:
|
|
||||||
return
|
|
||||||
if member.id in client.user_blacklist():
|
|
||||||
return
|
|
||||||
if member.id in client.objects['ignored_members'][member.guild.id]:
|
|
||||||
return
|
|
||||||
|
|
||||||
# Check whether we are moving to/from a workout channel
|
|
||||||
settings = GuildSettings(member.guild.id)
|
|
||||||
channels = settings.workout_channels.value
|
|
||||||
from_workout = before.channel in channels
|
|
||||||
to_workout = after.channel in channels
|
|
||||||
|
|
||||||
if to_workout ^ from_workout:
|
|
||||||
# Ensure guild row exists
|
|
||||||
tables.guild_config.fetch_or_create(member.guild.id)
|
|
||||||
|
|
||||||
# Fetch workout user
|
|
||||||
user = Lion.fetch(member.guild.id, member.id)
|
|
||||||
|
|
||||||
# Ignore all workout events from users who have already completed their workout today
|
|
||||||
if user.data.last_workout_start is not None:
|
|
||||||
last_date = user.localize(user.data.last_workout_start).date()
|
|
||||||
today = user.localize(dt.datetime.utcnow()).date()
|
|
||||||
if last_date == today:
|
|
||||||
return
|
|
||||||
|
|
||||||
# TODO: Check if they have completed a workout today, if so, ignore
|
|
||||||
if to_workout and not from_workout:
|
|
||||||
await on_workout_join(member)
|
|
||||||
elif from_workout and not to_workout:
|
|
||||||
if (member.guild.id, member.id) in client.objects['current_workouts']:
|
|
||||||
await on_workout_leave(member)
|
|
||||||
else:
|
|
||||||
client.log(
|
|
||||||
"Possible missed workout!\n"
|
|
||||||
"Member '{m.name}'(uid:{m.id}) left the workout channel '{c.name}'(cid:{c.id}) "
|
|
||||||
"in guild '{m.guild.name}'(gid:{m.guild.id}), but we never saw them join!".format(
|
|
||||||
m=member,
|
|
||||||
c=before.channel
|
|
||||||
),
|
|
||||||
context="WORKOUT_TRACKER",
|
|
||||||
level=logging.ERROR,
|
|
||||||
post=True
|
|
||||||
)
|
|
||||||
settings.event_log.log(
|
|
||||||
"{} left the workout channel {}, but I never saw them join!".format(
|
|
||||||
member.mention,
|
|
||||||
before.channel.mention,
|
|
||||||
), title="Possible Missed Workout!"
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
@module.launch_task
|
|
||||||
async def load_workouts(client):
|
|
||||||
client.objects['current_workouts'] = {} # (guildid, userid) -> Row
|
|
||||||
# Process any incomplete workouts
|
|
||||||
workouts = workout_sessions.fetch_rows_where(
|
|
||||||
duration=NULL,
|
|
||||||
guildid=THIS_SHARD
|
|
||||||
)
|
|
||||||
count = 0
|
|
||||||
for workout in workouts:
|
|
||||||
channelids = admin.workout_channels_setting.get(workout.guildid).data
|
|
||||||
member = Lion.fetch(workout.guildid, workout.userid).member
|
|
||||||
if member:
|
|
||||||
if member.voice and (member.voice.channel.id in channelids):
|
|
||||||
client.objects['current_workouts'][(workout.guildid, workout.userid)] = workout
|
|
||||||
count += 1
|
|
||||||
else:
|
|
||||||
asyncio.create_task(workout_left(member, workout))
|
|
||||||
else:
|
|
||||||
client.log(
|
|
||||||
"Removing incomplete workout from "
|
|
||||||
"non-existent member (mid:{}) in guild (gid:{})".format(
|
|
||||||
workout.userid,
|
|
||||||
workout.guildid
|
|
||||||
),
|
|
||||||
context="WORKOUT_LAUNCH",
|
|
||||||
post=True
|
|
||||||
)
|
|
||||||
if count > 0:
|
|
||||||
client.log(
|
|
||||||
"Loaded {} in-progress workouts.".format(count), context="WORKOUT_LAUNCH", post=True
|
|
||||||
)
|
|
||||||
@@ -90,7 +90,7 @@ class TimerCog(LionCog):
|
|||||||
self.bot.core.guild_config.register_model_setting(self.settings.PomodoroChannel)
|
self.bot.core.guild_config.register_model_setting(self.settings.PomodoroChannel)
|
||||||
|
|
||||||
configcog = self.bot.get_cog('ConfigCog')
|
configcog = self.bot.get_cog('ConfigCog')
|
||||||
self.crossload_group(self.configure_group, configcog.configure_group)
|
self.crossload_group(self.configure_group, configcog.config_group)
|
||||||
|
|
||||||
if self.bot.is_ready():
|
if self.bot.is_ready():
|
||||||
await self.initialise()
|
await self.initialise()
|
||||||
@@ -977,7 +977,6 @@ class TimerCog(LionCog):
|
|||||||
@appcmds.describe(
|
@appcmds.describe(
|
||||||
pomodoro_channel=TimerSettings.PomodoroChannel._desc
|
pomodoro_channel=TimerSettings.PomodoroChannel._desc
|
||||||
)
|
)
|
||||||
@appcmds.default_permissions(manage_guild=True)
|
|
||||||
@low_management_ward
|
@low_management_ward
|
||||||
async def configure_pomodoro_command(self, ctx: LionContext,
|
async def configure_pomodoro_command(self, ctx: LionContext,
|
||||||
pomodoro_channel: Optional[discord.VoiceChannel | discord.TextChannel] = None):
|
pomodoro_channel: Optional[discord.VoiceChannel | discord.TextChannel] = None):
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ from settings.setting_types import ChannelSetting
|
|||||||
|
|
||||||
from core.data import CoreData
|
from core.data import CoreData
|
||||||
from babel.translator import ctx_translator
|
from babel.translator import ctx_translator
|
||||||
|
from wards import low_management_iward
|
||||||
|
|
||||||
from . import babel
|
from . import babel
|
||||||
|
|
||||||
@@ -14,7 +15,8 @@ class TimerSettings(SettingGroup):
|
|||||||
class PomodoroChannel(ModelData, ChannelSetting):
|
class PomodoroChannel(ModelData, ChannelSetting):
|
||||||
setting_id = 'pomodoro_channel'
|
setting_id = 'pomodoro_channel'
|
||||||
_event = 'guildset_pomodoro_channel'
|
_event = 'guildset_pomodoro_channel'
|
||||||
_set_cmd = 'configure pomodoro'
|
_set_cmd = 'config pomodoro'
|
||||||
|
_write_ward = low_management_iward
|
||||||
|
|
||||||
_display_name = _p('guildset:pomodoro_channel', "pomodoro_channel")
|
_display_name = _p('guildset:pomodoro_channel', "pomodoro_channel")
|
||||||
_desc = _p(
|
_desc = _p(
|
||||||
|
|||||||
@@ -30,6 +30,7 @@ class TimerConfigUI(ConfigUI):
|
|||||||
async def channel_menu(self, selection: discord.Interaction, selected: ChannelSelect):
|
async def channel_menu(self, selection: discord.Interaction, selected: ChannelSelect):
|
||||||
await selection.response.defer()
|
await selection.response.defer()
|
||||||
setting = self.instances[0]
|
setting = self.instances[0]
|
||||||
|
await setting.interaction_check(setting.parent_id, selection)
|
||||||
setting.value = selected.values[0] if selected.values else None
|
setting.value = selected.values[0] if selected.values else None
|
||||||
await setting.write()
|
await setting.write()
|
||||||
|
|
||||||
@@ -78,7 +79,7 @@ class TimerConfigUI(ConfigUI):
|
|||||||
class TimerDashboard(DashboardSection):
|
class TimerDashboard(DashboardSection):
|
||||||
section_name = _p(
|
section_name = _p(
|
||||||
'dash:pomodoro|title',
|
'dash:pomodoro|title',
|
||||||
"Pomodoro Configuration ({commands[configure pomodoro]})"
|
"Pomodoro Configuration ({commands[config pomodoro]})"
|
||||||
)
|
)
|
||||||
_option_name = _p(
|
_option_name = _p(
|
||||||
"dash:stats|dropdown|placeholder",
|
"dash:stats|dropdown|placeholder",
|
||||||
|
|||||||
@@ -140,7 +140,7 @@ class RankCog(LionCog):
|
|||||||
self.bot.core.guild_config.register_model_setting(self.settings.DMRanks)
|
self.bot.core.guild_config.register_model_setting(self.settings.DMRanks)
|
||||||
|
|
||||||
configcog = self.bot.get_cog('ConfigCog')
|
configcog = self.bot.get_cog('ConfigCog')
|
||||||
self.crossload_group(self.configure_group, configcog.configure_group)
|
self.crossload_group(self.configure_group, configcog.admin_config_group)
|
||||||
|
|
||||||
def ranklock(self, guildid):
|
def ranklock(self, guildid):
|
||||||
lock = self._rank_locks.get(guildid, None)
|
lock = self._rank_locks.get(guildid, None)
|
||||||
@@ -926,7 +926,6 @@ class RankCog(LionCog):
|
|||||||
dm_ranks=RankSettings.DMRanks._desc,
|
dm_ranks=RankSettings.DMRanks._desc,
|
||||||
rank_channel=RankSettings.RankChannel._desc,
|
rank_channel=RankSettings.RankChannel._desc,
|
||||||
)
|
)
|
||||||
@appcmds.default_permissions(administrator=True)
|
|
||||||
@high_management_ward
|
@high_management_ward
|
||||||
async def configure_ranks_cmd(self, ctx: LionContext,
|
async def configure_ranks_cmd(self, ctx: LionContext,
|
||||||
rank_type: Optional[Transformed[RankTypeChoice, AppCommandOptionType.string]] = None,
|
rank_type: Optional[Transformed[RankTypeChoice, AppCommandOptionType.string]] = None,
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ from settings.setting_types import BoolSetting, ChannelSetting, EnumSetting
|
|||||||
|
|
||||||
from core.data import RankType, CoreData
|
from core.data import RankType, CoreData
|
||||||
from babel.translator import ctx_translator
|
from babel.translator import ctx_translator
|
||||||
|
from wards import high_management_iward
|
||||||
|
|
||||||
from . import babel
|
from . import babel
|
||||||
|
|
||||||
@@ -40,7 +41,8 @@ class RankSettings(SettingGroup):
|
|||||||
|
|
||||||
setting_id = 'rank_type'
|
setting_id = 'rank_type'
|
||||||
_event = 'guildset_rank_type'
|
_event = 'guildset_rank_type'
|
||||||
_set_cmd = 'configure ranks'
|
_set_cmd = 'admin config ranks'
|
||||||
|
_write_ward = high_management_iward
|
||||||
|
|
||||||
_display_name = _p('guildset:rank_type', "rank_type")
|
_display_name = _p('guildset:rank_type', "rank_type")
|
||||||
_desc = _p(
|
_desc = _p(
|
||||||
@@ -98,7 +100,8 @@ class RankSettings(SettingGroup):
|
|||||||
If DMRanks is set, this will only be used when the target user has disabled DM notifications.
|
If DMRanks is set, this will only be used when the target user has disabled DM notifications.
|
||||||
"""
|
"""
|
||||||
setting_id = 'rank_channel'
|
setting_id = 'rank_channel'
|
||||||
_set_cmd = 'configure ranks'
|
_set_cmd = 'admin config ranks'
|
||||||
|
_write_ward = high_management_iward
|
||||||
|
|
||||||
_display_name = _p('guildset:rank_channel', "rank_channel")
|
_display_name = _p('guildset:rank_channel', "rank_channel")
|
||||||
_desc = _p(
|
_desc = _p(
|
||||||
@@ -148,7 +151,8 @@ class RankSettings(SettingGroup):
|
|||||||
Whether to DM rank notifications.
|
Whether to DM rank notifications.
|
||||||
"""
|
"""
|
||||||
setting_id = 'dm_ranks'
|
setting_id = 'dm_ranks'
|
||||||
_set_cmd = 'configure ranks'
|
_set_cmd = 'admin config ranks'
|
||||||
|
_write_ward = high_management_iward
|
||||||
|
|
||||||
_display_name = _p('guildset:dm_ranks', "dm_ranks")
|
_display_name = _p('guildset:dm_ranks', "dm_ranks")
|
||||||
_desc = _p(
|
_desc = _p(
|
||||||
|
|||||||
@@ -69,6 +69,7 @@ class RankConfigUI(ConfigUI):
|
|||||||
async def type_menu(self, selection: discord.Interaction, selected: Select):
|
async def type_menu(self, selection: discord.Interaction, selected: Select):
|
||||||
await selection.response.defer(thinking=True)
|
await selection.response.defer(thinking=True)
|
||||||
setting = self.instances[0]
|
setting = self.instances[0]
|
||||||
|
await setting.interaction_check(setting.parent_id, selection)
|
||||||
value = selected.values[0]
|
value = selected.values[0]
|
||||||
data = RankType((value,))
|
data = RankType((value,))
|
||||||
setting.data = data
|
setting.data = data
|
||||||
@@ -117,6 +118,7 @@ class RankConfigUI(ConfigUI):
|
|||||||
async def channel_menu(self, selection: discord.Interaction, selected: ChannelSelect):
|
async def channel_menu(self, selection: discord.Interaction, selected: ChannelSelect):
|
||||||
await selection.response.defer()
|
await selection.response.defer()
|
||||||
setting = self.instances[2]
|
setting = self.instances[2]
|
||||||
|
await setting.interaction_check(setting.parent_id, selection)
|
||||||
setting.value = selected.values[0] if selected.values else None
|
setting.value = selected.values[0] if selected.values else None
|
||||||
await setting.write()
|
await setting.write()
|
||||||
|
|
||||||
@@ -168,7 +170,7 @@ class RankConfigUI(ConfigUI):
|
|||||||
class RankDashboard(DashboardSection):
|
class RankDashboard(DashboardSection):
|
||||||
section_name = _p(
|
section_name = _p(
|
||||||
'dash:rank|title',
|
'dash:rank|title',
|
||||||
"Rank Configuration ({commands[configure ranks]})",
|
"Rank Configuration ({commands[admin config ranks]})",
|
||||||
)
|
)
|
||||||
_option_name = _p(
|
_option_name = _p(
|
||||||
"dash:rank|dropdown|placeholder",
|
"dash:rank|dropdown|placeholder",
|
||||||
|
|||||||
@@ -20,6 +20,7 @@ from ..data import AnyRankData, RankData
|
|||||||
from ..utils import rank_model_from_type, format_stat_range, stat_data_to_value
|
from ..utils import rank_model_from_type, format_stat_range, stat_data_to_value
|
||||||
from .editor import RankEditor
|
from .editor import RankEditor
|
||||||
from .preview import RankPreviewUI
|
from .preview import RankPreviewUI
|
||||||
|
from .templates import get_guild_template
|
||||||
|
|
||||||
_p = babel._p
|
_p = babel._p
|
||||||
|
|
||||||
@@ -87,7 +88,73 @@ class RankOverviewUI(MessageUI):
|
|||||||
|
|
||||||
Ranks are determined by rank type.
|
Ranks are determined by rank type.
|
||||||
"""
|
"""
|
||||||
await press.response.send_message("Not Implemented Yet")
|
t = self.bot.translator.t
|
||||||
|
|
||||||
|
# Prevent role creation spam
|
||||||
|
if await self.rank_model.table.select_where(guildid=self.guild.id):
|
||||||
|
return await press.response.send_message(content=t(_p(
|
||||||
|
'ui:rank_overview|button:auto|error:already_created',
|
||||||
|
"The rank roles have already been created!"
|
||||||
|
)), ephemeral=True)
|
||||||
|
|
||||||
|
await press.response.defer(thinking=True)
|
||||||
|
|
||||||
|
if not self.guild.me.guild_permissions.manage_roles:
|
||||||
|
raise SafeCancellation(t(_p(
|
||||||
|
'ui:rank_overview|button:auto|error:my_permissions',
|
||||||
|
"I lack the 'Manage Roles' permission required to create rank roles!"
|
||||||
|
)))
|
||||||
|
|
||||||
|
# Get rank role template based on set RankType and VoiceMode
|
||||||
|
template = get_guild_template(self.rank_type, self.lguild.guild_mode.voice)
|
||||||
|
if not template:
|
||||||
|
# Safely error if rank type or voice mode isn't an expected value
|
||||||
|
raise SafeCancellation(t(_p(
|
||||||
|
'ui:rank_overview|button:auto|error:invalid_template',
|
||||||
|
"Unable to determine rank role template!")))
|
||||||
|
|
||||||
|
roles = []
|
||||||
|
async with self.cog.ranklock(self.guild.id):
|
||||||
|
for rank in reversed(template):
|
||||||
|
try:
|
||||||
|
colour = discord.Colour.from_str(rank.colour)
|
||||||
|
role = await self.guild.create_role(name=t(rank.name), colour=colour)
|
||||||
|
roles.append(role)
|
||||||
|
await self.rank_model.create(
|
||||||
|
roleid=role.id,
|
||||||
|
guildid=self.guild.id,
|
||||||
|
required=rank.required,
|
||||||
|
reward=rank.reward,
|
||||||
|
message=t(rank.message)
|
||||||
|
)
|
||||||
|
self.cog.flush_guild_ranks(self.guild.id)
|
||||||
|
|
||||||
|
# Error if manage roles is lost during the process. This shouldn't happen
|
||||||
|
except discord.Forbidden:
|
||||||
|
self.cog.flush_guild_ranks(self.guild.id)
|
||||||
|
raise SafeCancellation(t(_p(
|
||||||
|
'ui:rank_overview|button|auto|role_creation|error:forbidden',
|
||||||
|
"An error occurred while autocreating rank roles!\n"
|
||||||
|
"I lack the 'Manage Roles' permission required to create rank roles!"
|
||||||
|
)))
|
||||||
|
|
||||||
|
except discord.HTTPException:
|
||||||
|
self.cog.flush_guild_ranks(self.guild.id)
|
||||||
|
raise SafeCancellation(t(_p(
|
||||||
|
'ui:rank_overview|button:auto|role_creation|error:unknown',
|
||||||
|
"An error occurred while autocreating rank roles!\n"
|
||||||
|
"Please check the server has enough space for new roles "
|
||||||
|
"and try again."
|
||||||
|
)))
|
||||||
|
|
||||||
|
success_msg = t(_p(
|
||||||
|
'ui:rank_overview|button:auto|role_creation|success',
|
||||||
|
"Successfully created the following rank roles:\n{roles}"
|
||||||
|
)).format(roles="\n".join(role.mention for role in roles))
|
||||||
|
embed = discord.Embed(
|
||||||
|
colour=discord.Colour.brand_green(),
|
||||||
|
description=success_msg)
|
||||||
|
await press.edit_original_response(embed=embed)
|
||||||
|
|
||||||
async def auto_button_refresh(self):
|
async def auto_button_refresh(self):
|
||||||
self.auto_button.label = self.bot.translator.t(_p(
|
self.auto_button.label = self.bot.translator.t(_p(
|
||||||
@@ -384,11 +451,17 @@ class RankOverviewUI(MessageUI):
|
|||||||
# No ranks, give hints about adding ranks
|
# No ranks, give hints about adding ranks
|
||||||
desc = t(_p(
|
desc = t(_p(
|
||||||
'ui:rank_overview|embed:noranks|desc',
|
'ui:rank_overview|embed:noranks|desc',
|
||||||
"No activity ranks have been set up!\n"
|
"No activity ranks have been set up!"
|
||||||
"Press 'AUTO' to automatically create a "
|
|
||||||
"standard heirachy of voice | text | xp ranks, "
|
|
||||||
"or select a role or press Create below!"
|
|
||||||
))
|
))
|
||||||
|
if show_note:
|
||||||
|
auto_addendum = t(_p(
|
||||||
|
'ui:rank_overview|embed:noranks|desc|admin_addendum',
|
||||||
|
"Press 'Auto Create' to automatically create a "
|
||||||
|
"standard heirachy of ranks.\n"
|
||||||
|
"To manually create ranks, press 'Create Rank' below, or select a role!"
|
||||||
|
))
|
||||||
|
desc = "\n".join((desc, auto_addendum))
|
||||||
|
|
||||||
if self.rank_type is RankType.VOICE:
|
if self.rank_type is RankType.VOICE:
|
||||||
title = t(_p(
|
title = t(_p(
|
||||||
'ui:rank_overview|embed|title|type:voice',
|
'ui:rank_overview|embed|title|type:voice',
|
||||||
@@ -430,7 +503,7 @@ class RankOverviewUI(MessageUI):
|
|||||||
"Ranks are determined by *all-time* statistics.\n"
|
"Ranks are determined by *all-time* statistics.\n"
|
||||||
"To reward ranks from a later time (e.g. to have monthly/quarterly/yearly ranks) "
|
"To reward ranks from a later time (e.g. to have monthly/quarterly/yearly ranks) "
|
||||||
"set the `season_start` with {stats_cmd}"
|
"set the `season_start` with {stats_cmd}"
|
||||||
)).format(stats_cmd=self.bot.core.mention_cmd('configure statistics'))
|
)).format(stats_cmd=self.bot.core.mention_cmd('admin config statistics'))
|
||||||
if self.rank_type is RankType.VOICE:
|
if self.rank_type is RankType.VOICE:
|
||||||
addendum = t(_p(
|
addendum = t(_p(
|
||||||
'ui:rank_overview|embed|field:note|value|voice_addendum',
|
'ui:rank_overview|embed|field:note|value|voice_addendum',
|
||||||
|
|||||||
303
src/modules/ranks/ui/templates.py
Normal file
303
src/modules/ranks/ui/templates.py
Normal file
@@ -0,0 +1,303 @@
|
|||||||
|
from collections import namedtuple
|
||||||
|
from core.data import RankType
|
||||||
|
from core.lion_guild import VoiceMode
|
||||||
|
|
||||||
|
from meta import conf, LionBot
|
||||||
|
from babel.translator import ctx_translator
|
||||||
|
|
||||||
|
from .. import babel
|
||||||
|
|
||||||
|
_p = babel._p
|
||||||
|
|
||||||
|
RankBase = namedtuple("RankBase", ("name", "required", "reward", "message", "colour"))
|
||||||
|
|
||||||
|
"""
|
||||||
|
Reward message defaults
|
||||||
|
"""
|
||||||
|
|
||||||
|
voice_reward_msg = _p(
|
||||||
|
'ui:rank_editor|input:message|default|type:voice',
|
||||||
|
"Congratulations {user_mention}!\n"
|
||||||
|
"For working hard for **{requires}**, you have achieved the rank of "
|
||||||
|
"**{role_name}** in **{guild_name}**! Keep up the good work."
|
||||||
|
)
|
||||||
|
|
||||||
|
xp_reward_msg = _p(
|
||||||
|
'ui:rank_editor|input:message|default|type:xp',
|
||||||
|
"Congratulations {user_mention}!\n"
|
||||||
|
"For earning **{requires}**, you have achieved the guild rank of "
|
||||||
|
"**{role_name}** in **{guild_name}**!"
|
||||||
|
)
|
||||||
|
|
||||||
|
msg_reward_msg = _p(
|
||||||
|
'ui:rank_editor|input:message|default|type:msg',
|
||||||
|
"Congratulations {user_mention}!\n"
|
||||||
|
"For sending **{requires}**, you have achieved the guild rank of "
|
||||||
|
"**{role_name}** in **{guild_name}**!"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
"""
|
||||||
|
Rank templates based on voice activity
|
||||||
|
"""
|
||||||
|
|
||||||
|
study_voice_template = [
|
||||||
|
RankBase(
|
||||||
|
name=_p('rank_autocreate|template|type:study_voice|level:1',
|
||||||
|
"Voice Level 1 (1h)"),
|
||||||
|
required=3600,
|
||||||
|
reward=1000,
|
||||||
|
message=voice_reward_msg,
|
||||||
|
colour="#1f28e2"
|
||||||
|
),
|
||||||
|
RankBase(
|
||||||
|
name=_p('rank_autocreate|template|type:study_voice|level:2',
|
||||||
|
"Voice Level 2 (3h)"),
|
||||||
|
required=10800,
|
||||||
|
reward=2000,
|
||||||
|
message=voice_reward_msg,
|
||||||
|
colour="#006bff"
|
||||||
|
),
|
||||||
|
RankBase(
|
||||||
|
name=_p('rank_autocreate|template|type:study_voice|level:3',
|
||||||
|
"Voice Level 3 (6h)"),
|
||||||
|
required=21600,
|
||||||
|
reward=3000,
|
||||||
|
message=voice_reward_msg,
|
||||||
|
colour="#0091ff"
|
||||||
|
),
|
||||||
|
RankBase(
|
||||||
|
name=_p('rank_autocreate|template|type:study_voice|level:4',
|
||||||
|
"Voice Level 4 (10h)"),
|
||||||
|
required=36000,
|
||||||
|
reward=4000,
|
||||||
|
message=voice_reward_msg,
|
||||||
|
colour="#00adf5"
|
||||||
|
),
|
||||||
|
RankBase(
|
||||||
|
name=_p('rank_autocreate|template|type:study_voice|level:5',
|
||||||
|
"Voice Level 5 (20h)"),
|
||||||
|
required=72000,
|
||||||
|
reward=5000,
|
||||||
|
message=voice_reward_msg,
|
||||||
|
colour="#00c6bf"
|
||||||
|
),
|
||||||
|
RankBase(
|
||||||
|
name=_p('rank_autocreate|template|type:study_voice|level:6',
|
||||||
|
"Voice Level 6 (40h)"),
|
||||||
|
required=144000,
|
||||||
|
reward=6000,
|
||||||
|
message=voice_reward_msg,
|
||||||
|
colour="#00db86"
|
||||||
|
),
|
||||||
|
RankBase(
|
||||||
|
name=_p('rank_autocreate|template|type:study_voice|level:7',
|
||||||
|
"Voice Level 7 (80h)"),
|
||||||
|
required=288000,
|
||||||
|
reward=7000,
|
||||||
|
message=voice_reward_msg,
|
||||||
|
colour="#7cea5a"
|
||||||
|
)
|
||||||
|
]
|
||||||
|
|
||||||
|
general_voice_template = [
|
||||||
|
RankBase(
|
||||||
|
name=_p('rank_autocreate|template|type:general_voice|level:1',
|
||||||
|
"Voice Level 1 (1h)"),
|
||||||
|
required=3600,
|
||||||
|
reward=1000,
|
||||||
|
message=voice_reward_msg,
|
||||||
|
colour="#1f28e2"
|
||||||
|
),
|
||||||
|
RankBase(
|
||||||
|
name=_p('rank_autocreate|template|type:general_voice|level:2',
|
||||||
|
"Voice Level 2 (2h)"),
|
||||||
|
required=7200,
|
||||||
|
reward=2000,
|
||||||
|
message=voice_reward_msg,
|
||||||
|
colour="#006bff"
|
||||||
|
),
|
||||||
|
RankBase(
|
||||||
|
name=_p('rank_autocreate|template|type:general_voice|level:3',
|
||||||
|
"Voice Level 3 (4h)"),
|
||||||
|
required=14400,
|
||||||
|
reward=3000,
|
||||||
|
message=voice_reward_msg,
|
||||||
|
colour="#0091ff"
|
||||||
|
),
|
||||||
|
RankBase(
|
||||||
|
name=_p('rank_autocreate|template|type:general_voice|level:4',
|
||||||
|
"Voice Level 4 (8h)"),
|
||||||
|
required=28800,
|
||||||
|
reward=4000,
|
||||||
|
message=voice_reward_msg,
|
||||||
|
colour="#00adf5"
|
||||||
|
),
|
||||||
|
RankBase(
|
||||||
|
name=_p('rank_autocreate|template|type:general_voice|level:5',
|
||||||
|
"Voice Level 5 (16h)"),
|
||||||
|
required=57600,
|
||||||
|
reward=5000,
|
||||||
|
message=voice_reward_msg,
|
||||||
|
colour="#00c6bf"
|
||||||
|
),
|
||||||
|
RankBase(
|
||||||
|
name=_p('rank_autocreate|template|type:general_voice|level:6',
|
||||||
|
"Voice Level 6 (32h)"),
|
||||||
|
required=115200,
|
||||||
|
reward=6000,
|
||||||
|
message=voice_reward_msg,
|
||||||
|
colour="#00db86"
|
||||||
|
),
|
||||||
|
RankBase(
|
||||||
|
name=_p('rank_autocreate|template|type:general_voice|level:7',
|
||||||
|
"Voice Level 7 (64h)"),
|
||||||
|
required=230400,
|
||||||
|
reward=7000,
|
||||||
|
message=voice_reward_msg,
|
||||||
|
colour="#7cea5a"
|
||||||
|
)
|
||||||
|
]
|
||||||
|
|
||||||
|
"""
|
||||||
|
Rank templates based on message XP earned
|
||||||
|
"""
|
||||||
|
|
||||||
|
xp_template = [
|
||||||
|
RankBase(
|
||||||
|
name=_p('rank_autocreate|template|type:xp|level:1',
|
||||||
|
"XP Level 1 (2000)"),
|
||||||
|
required=2000,
|
||||||
|
reward=1000,
|
||||||
|
message=xp_reward_msg,
|
||||||
|
colour="#1f28e2"
|
||||||
|
),
|
||||||
|
RankBase(
|
||||||
|
name=_p('rank_autocreate|template|type:xp|level:2',
|
||||||
|
"XP Level 2 (4000)"),
|
||||||
|
required=4000,
|
||||||
|
reward=2000,
|
||||||
|
message=xp_reward_msg,
|
||||||
|
colour="#006bff"
|
||||||
|
),
|
||||||
|
RankBase(
|
||||||
|
name=_p('rank_autocreate|template|type:xp|level:3',
|
||||||
|
"XP Level 3 (8000)"),
|
||||||
|
required=8000,
|
||||||
|
reward=3000,
|
||||||
|
message=xp_reward_msg,
|
||||||
|
colour="#0091ff"
|
||||||
|
),
|
||||||
|
RankBase(
|
||||||
|
name=_p('rank_autocreate|template|type:xp|level:4',
|
||||||
|
"XP Level 4 (16000)"),
|
||||||
|
required=16000,
|
||||||
|
reward=4000,
|
||||||
|
message=xp_reward_msg,
|
||||||
|
colour="#00adf5"
|
||||||
|
),
|
||||||
|
RankBase(
|
||||||
|
name=_p('rank_autocreate|template|type:xp|level:5',
|
||||||
|
"XP Level 5 (32000)"),
|
||||||
|
required=32000,
|
||||||
|
reward=5000,
|
||||||
|
message=xp_reward_msg,
|
||||||
|
colour="#00c6bf"
|
||||||
|
),
|
||||||
|
RankBase(
|
||||||
|
name=_p('rank_autocreate|template|type:xp|level:6',
|
||||||
|
"XP Level 6 (64000)"),
|
||||||
|
required=64000,
|
||||||
|
reward=6000,
|
||||||
|
message=xp_reward_msg,
|
||||||
|
colour="#00db86"
|
||||||
|
),
|
||||||
|
RankBase(
|
||||||
|
name=_p('rank_autocreate|template|type:xp|level:7',
|
||||||
|
"XP Level 7 (128000)"),
|
||||||
|
required=128000,
|
||||||
|
reward=7000,
|
||||||
|
message=xp_reward_msg,
|
||||||
|
colour="#7cea5a"
|
||||||
|
)
|
||||||
|
]
|
||||||
|
|
||||||
|
"""
|
||||||
|
Rank templates based on messages sent
|
||||||
|
"""
|
||||||
|
|
||||||
|
msg_template = [
|
||||||
|
RankBase(
|
||||||
|
name=_p('rank_autocreate|template|type:msg|level:1',
|
||||||
|
"Message Level 1 (200)"),
|
||||||
|
required=200,
|
||||||
|
reward=1000,
|
||||||
|
message=msg_reward_msg,
|
||||||
|
colour="#1f28e2"
|
||||||
|
),
|
||||||
|
RankBase(
|
||||||
|
name=_p('rank_autocreate|template|type:msg|level:2',
|
||||||
|
"Message Level 2 (400)"),
|
||||||
|
required=400,
|
||||||
|
reward=2000,
|
||||||
|
message=msg_reward_msg,
|
||||||
|
colour="#006bff"
|
||||||
|
),
|
||||||
|
RankBase(
|
||||||
|
name=_p('rank_autocreate|template|type:msg|level:3',
|
||||||
|
"Message Level 3 (800)"),
|
||||||
|
required=800,
|
||||||
|
reward=3000,
|
||||||
|
message=msg_reward_msg,
|
||||||
|
colour="#0091ff"
|
||||||
|
),
|
||||||
|
RankBase(
|
||||||
|
name=_p('rank_autocreate|template|type:msg|level:4',
|
||||||
|
"Message Level 4 (1600)"),
|
||||||
|
required=1600,
|
||||||
|
reward=4000,
|
||||||
|
message=msg_reward_msg,
|
||||||
|
colour="#00adf5"
|
||||||
|
),
|
||||||
|
RankBase(
|
||||||
|
name=_p('rank_autocreate|template|type:msg|level:5',
|
||||||
|
"Message Level 5 (3200)"),
|
||||||
|
required=3200,
|
||||||
|
reward=5000,
|
||||||
|
message=msg_reward_msg,
|
||||||
|
colour="#00c6bf"
|
||||||
|
),
|
||||||
|
RankBase(
|
||||||
|
name=_p('rank_autocreate|template|type:msg|level:6',
|
||||||
|
"Message Level 6 (6400)"),
|
||||||
|
required=6400,
|
||||||
|
reward=6000,
|
||||||
|
message=msg_reward_msg,
|
||||||
|
colour="#00db86"
|
||||||
|
),
|
||||||
|
RankBase(
|
||||||
|
name=_p('rank_autocreate|template|type:msg|level:7',
|
||||||
|
"Message Level 7 (12800)"),
|
||||||
|
required=12800,
|
||||||
|
reward=7000,
|
||||||
|
message=msg_reward_msg,
|
||||||
|
colour="#7cea5a"
|
||||||
|
)
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
def get_guild_template(rank_type: RankType, voice_mode: VoiceMode):
|
||||||
|
"""
|
||||||
|
Returns the best fit rank template
|
||||||
|
based on the guild's rank type and voice mode.
|
||||||
|
"""
|
||||||
|
if rank_type == RankType.VOICE:
|
||||||
|
if voice_mode == VoiceMode.STUDY:
|
||||||
|
return study_voice_template
|
||||||
|
if voice_mode == VoiceMode.VOICE:
|
||||||
|
return general_voice_template
|
||||||
|
if rank_type == RankType.XP:
|
||||||
|
return xp_template
|
||||||
|
if rank_type == RankType.MESSAGE:
|
||||||
|
return msg_template
|
||||||
|
return None
|
||||||
@@ -332,6 +332,7 @@ class Reminders(LionCog):
|
|||||||
"View and set your reminders."
|
"View and set your reminders."
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
@appcmds.guild_only
|
||||||
async def cmd_reminders(self, ctx: LionContext):
|
async def cmd_reminders(self, ctx: LionContext):
|
||||||
"""
|
"""
|
||||||
Display the reminder widget for this user.
|
Display the reminder widget for this user.
|
||||||
@@ -353,6 +354,7 @@ class Reminders(LionCog):
|
|||||||
name=_p('cmd:remindme', "remindme"),
|
name=_p('cmd:remindme', "remindme"),
|
||||||
description=_p('cmd:remindme|desc', "View and set task reminders."),
|
description=_p('cmd:remindme|desc', "View and set task reminders."),
|
||||||
)
|
)
|
||||||
|
@appcmds.guild_only
|
||||||
async def remindme_group(self, ctx: LionContext):
|
async def remindme_group(self, ctx: LionContext):
|
||||||
# Base command group for scheduling reminders.
|
# Base command group for scheduling reminders.
|
||||||
pass
|
pass
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ from utils.ui import Confirm
|
|||||||
from constants import MAX_COINS
|
from constants import MAX_COINS
|
||||||
from core.data import CoreData
|
from core.data import CoreData
|
||||||
|
|
||||||
from wards import low_management_ward
|
from wards import high_management_ward
|
||||||
|
|
||||||
from . import babel, logger
|
from . import babel, logger
|
||||||
from .data import RoomData
|
from .data import RoomData
|
||||||
@@ -47,7 +47,7 @@ class RoomCog(LionCog):
|
|||||||
self.bot.core.guild_config.register_model_setting(setting)
|
self.bot.core.guild_config.register_model_setting(setting)
|
||||||
|
|
||||||
configcog = self.bot.get_cog('ConfigCog')
|
configcog = self.bot.get_cog('ConfigCog')
|
||||||
self.crossload_group(self.configure_group, configcog.configure_group)
|
self.crossload_group(self.configure_group, configcog.admin_config_group)
|
||||||
|
|
||||||
if self.bot.is_ready():
|
if self.bot.is_ready():
|
||||||
await self.initialise()
|
await self.initialise()
|
||||||
@@ -414,7 +414,7 @@ class RoomCog(LionCog):
|
|||||||
t(_p(
|
t(_p(
|
||||||
'cmd:room_rent|error:not_setup',
|
'cmd:room_rent|error:not_setup',
|
||||||
"The private room system has not been set up! "
|
"The private room system has not been set up! "
|
||||||
"A private room category needs to be set first with `/configure rooms`."
|
"A private room category needs to be set first with `/admin config rooms`."
|
||||||
))
|
))
|
||||||
), ephemeral=True
|
), ephemeral=True
|
||||||
)
|
)
|
||||||
@@ -523,12 +523,31 @@ class RoomCog(LionCog):
|
|||||||
self._start(room)
|
self._start(room)
|
||||||
|
|
||||||
# Send tips message
|
# Send tips message
|
||||||
# TODO: Actual tips.
|
tips = (
|
||||||
await room.channel.send(
|
"Welcome to your very own private room {owner}!\n"
|
||||||
"{mention} welcome to your private room! You may use the menu below to configure it.".format(
|
"You may use the control panel below to quickly configure your room, including:\n"
|
||||||
mention=ctx.author.mention
|
"- Inviting (and removing) members,\n"
|
||||||
)
|
"- Depositing LionCoins into the room bank to pay the daily rent, and\n"
|
||||||
|
"- Adding your very own Pomodoro timer to the room.\n\n"
|
||||||
|
"You also have elevated Discord permissions over the room itself!\n"
|
||||||
|
"This includes managing messages, and changing the name, region,"
|
||||||
|
" and bitrate of the channel, or even deleting the room entirely!"
|
||||||
|
" (Beware you will not be refunded in this case.)\n\n"
|
||||||
|
"Finally, you now have access to some new commands:\n"
|
||||||
|
"{status_cmd}: This brings up the room control panel again,"
|
||||||
|
" in case the interface below times out or is deleted/hidden.\n"
|
||||||
|
"{deposit_cmd}: Room members may use this command to easily contribute LionCoins to the room bank.\n"
|
||||||
|
"{invite_cmd} and {kick_cmd}: Quickly invite (or remove) multiple members by mentioning them.\n"
|
||||||
|
"{transfer_cmd}: Transfer the room to another owner, keeping the balance (this is not reversible!)"
|
||||||
|
).format(
|
||||||
|
owner=ctx.author.mention,
|
||||||
|
status_cmd=self.bot.core.mention_cmd('room status'),
|
||||||
|
deposit_cmd=self.bot.core.mention_cmd('room deposit'),
|
||||||
|
invite_cmd=self.bot.core.mention_cmd('room invite'),
|
||||||
|
kick_cmd=self.bot.core.mention_cmd('room kick'),
|
||||||
|
transfer_cmd=self.bot.core.mention_cmd('room transfer'),
|
||||||
)
|
)
|
||||||
|
await room.channel.send(tips)
|
||||||
|
|
||||||
# Send config UI
|
# Send config UI
|
||||||
ui = RoomUI(self.bot, room, callerid=ctx.author.id, timeout=None)
|
ui = RoomUI(self.bot, room, callerid=ctx.author.id, timeout=None)
|
||||||
@@ -987,8 +1006,7 @@ class RoomCog(LionCog):
|
|||||||
@appcmds.describe(
|
@appcmds.describe(
|
||||||
**{setting.setting_id: setting._desc for setting in RoomSettings.model_settings}
|
**{setting.setting_id: setting._desc for setting in RoomSettings.model_settings}
|
||||||
)
|
)
|
||||||
@appcmds.default_permissions(manage_guild=True)
|
@high_management_ward
|
||||||
@low_management_ward
|
|
||||||
async def configure_rooms_cmd(self, ctx: LionContext,
|
async def configure_rooms_cmd(self, ctx: LionContext,
|
||||||
rooms_category: Optional[discord.CategoryChannel] = None,
|
rooms_category: Optional[discord.CategoryChannel] = None,
|
||||||
rooms_price: Optional[Range[int, 0, MAX_COINS]] = None,
|
rooms_price: Optional[Range[int, 0, MAX_COINS]] = None,
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ from settings.setting_types import ChannelSetting, IntegerSetting, BoolSetting
|
|||||||
from meta import conf
|
from meta import conf
|
||||||
from core.data import CoreData
|
from core.data import CoreData
|
||||||
from babel.translator import ctx_translator
|
from babel.translator import ctx_translator
|
||||||
|
from wards import low_management_iward, high_management_iward
|
||||||
|
|
||||||
from . import babel
|
from . import babel
|
||||||
|
|
||||||
@@ -15,7 +16,8 @@ class RoomSettings(SettingGroup):
|
|||||||
class Category(ModelData, ChannelSetting):
|
class Category(ModelData, ChannelSetting):
|
||||||
setting_id = 'rooms_category'
|
setting_id = 'rooms_category'
|
||||||
_event = 'guildset_rooms_category'
|
_event = 'guildset_rooms_category'
|
||||||
_set_cmd = 'configure rooms'
|
_set_cmd = 'admin config rooms'
|
||||||
|
_write_ward = high_management_iward
|
||||||
|
|
||||||
_display_name = _p(
|
_display_name = _p(
|
||||||
'guildset:room_category', "rooms_category"
|
'guildset:room_category', "rooms_category"
|
||||||
@@ -70,7 +72,8 @@ class RoomSettings(SettingGroup):
|
|||||||
class Rent(ModelData, IntegerSetting):
|
class Rent(ModelData, IntegerSetting):
|
||||||
setting_id = 'rooms_price'
|
setting_id = 'rooms_price'
|
||||||
_event = 'guildset_rooms_price'
|
_event = 'guildset_rooms_price'
|
||||||
_set_cmd = 'configure rooms'
|
_set_cmd = 'admin config rooms'
|
||||||
|
_write_ward = low_management_iward
|
||||||
|
|
||||||
_display_name = _p(
|
_display_name = _p(
|
||||||
'guildset:rooms_price', "room_rent"
|
'guildset:rooms_price', "room_rent"
|
||||||
@@ -107,7 +110,8 @@ class RoomSettings(SettingGroup):
|
|||||||
class MemberLimit(ModelData, IntegerSetting):
|
class MemberLimit(ModelData, IntegerSetting):
|
||||||
setting_id = 'rooms_slots'
|
setting_id = 'rooms_slots'
|
||||||
_event = 'guildset_rooms_slots'
|
_event = 'guildset_rooms_slots'
|
||||||
_set_cmd = 'configure rooms'
|
_set_cmd = 'admin config rooms'
|
||||||
|
_write_ward = low_management_iward
|
||||||
|
|
||||||
_display_name = _p('guildset:rooms_slots', "room_member_cap")
|
_display_name = _p('guildset:rooms_slots', "room_member_cap")
|
||||||
_desc = _p(
|
_desc = _p(
|
||||||
@@ -141,7 +145,8 @@ class RoomSettings(SettingGroup):
|
|||||||
class Visible(ModelData, BoolSetting):
|
class Visible(ModelData, BoolSetting):
|
||||||
setting_id = 'rooms_visible'
|
setting_id = 'rooms_visible'
|
||||||
_event = 'guildset_rooms_visible'
|
_event = 'guildset_rooms_visible'
|
||||||
_set_cmd = 'configure rooms'
|
_set_cmd = 'admin config rooms'
|
||||||
|
_write_ward = high_management_iward
|
||||||
|
|
||||||
_display_name = _p('guildset:rooms_visible', "room_visibility")
|
_display_name = _p('guildset:rooms_visible', "room_visibility")
|
||||||
_desc = _p(
|
_desc = _p(
|
||||||
|
|||||||
@@ -29,6 +29,7 @@ class RoomSettingUI(ConfigUI):
|
|||||||
async def category_menu(self, selection: discord.Interaction, selected: ChannelSelect):
|
async def category_menu(self, selection: discord.Interaction, selected: ChannelSelect):
|
||||||
await selection.response.defer()
|
await selection.response.defer()
|
||||||
setting = self.instances[0]
|
setting = self.instances[0]
|
||||||
|
await setting.interaction_check(setting.parent_id, selection)
|
||||||
setting.value = selected.values[0] if selected.values else None
|
setting.value = selected.values[0] if selected.values else None
|
||||||
await setting.write()
|
await setting.write()
|
||||||
|
|
||||||
@@ -42,6 +43,7 @@ class RoomSettingUI(ConfigUI):
|
|||||||
async def visible_button(self, press: discord.Interaction, pressed: Button):
|
async def visible_button(self, press: discord.Interaction, pressed: Button):
|
||||||
await press.response.defer()
|
await press.response.defer()
|
||||||
setting = next(inst for inst in self.instances if inst.setting_id == RoomSettings.Visible.setting_id)
|
setting = next(inst for inst in self.instances if inst.setting_id == RoomSettings.Visible.setting_id)
|
||||||
|
await setting.interaction_check(setting.parent_id, press)
|
||||||
setting.value = not setting.value
|
setting.value = not setting.value
|
||||||
await setting.write()
|
await setting.write()
|
||||||
|
|
||||||
@@ -95,7 +97,7 @@ class RoomSettingUI(ConfigUI):
|
|||||||
class RoomDashboard(DashboardSection):
|
class RoomDashboard(DashboardSection):
|
||||||
section_name = _p(
|
section_name = _p(
|
||||||
'dash:rooms|title',
|
'dash:rooms|title',
|
||||||
"Private Room Configuration ({commands[configure rooms]})"
|
"Private Room Configuration ({commands[admin config rooms]})"
|
||||||
)
|
)
|
||||||
_option_name = _p(
|
_option_name = _p(
|
||||||
"dash:economy|dropdown|placeholder",
|
"dash:economy|dropdown|placeholder",
|
||||||
|
|||||||
@@ -17,7 +17,7 @@ from meta.monitor import ComponentMonitor, ComponentStatus, StatusLevel
|
|||||||
from utils.lib import utc_now, error_embed
|
from utils.lib import utc_now, error_embed
|
||||||
from utils.ui import Confirm
|
from utils.ui import Confirm
|
||||||
from utils.data import MULTIVALUE_IN, MEMBERS
|
from utils.data import MULTIVALUE_IN, MEMBERS
|
||||||
from wards import low_management_ward
|
from wards import high_management_ward
|
||||||
from core.data import CoreData
|
from core.data import CoreData
|
||||||
from data import NULL, ORDER
|
from data import NULL, ORDER
|
||||||
from modules.economy.data import TransactionType
|
from modules.economy.data import TransactionType
|
||||||
@@ -118,7 +118,7 @@ class ScheduleCog(LionCog):
|
|||||||
await self.settings.SessionChannels.setup(self.bot)
|
await self.settings.SessionChannels.setup(self.bot)
|
||||||
|
|
||||||
configcog = self.bot.get_cog('ConfigCog')
|
configcog = self.bot.get_cog('ConfigCog')
|
||||||
self.crossload_group(self.configure_group, configcog.configure_group)
|
self.crossload_group(self.configure_group, configcog.admin_config_group)
|
||||||
|
|
||||||
if self.bot.is_ready():
|
if self.bot.is_ready():
|
||||||
await self.initialise()
|
await self.initialise()
|
||||||
@@ -1090,7 +1090,7 @@ class ScheduleCog(LionCog):
|
|||||||
@appcmds.describe(
|
@appcmds.describe(
|
||||||
**{param: option._desc for param, option in config_params.items()}
|
**{param: option._desc for param, option in config_params.items()}
|
||||||
)
|
)
|
||||||
@low_management_ward
|
@high_management_ward
|
||||||
async def configure_schedule_command(self, ctx: LionContext,
|
async def configure_schedule_command(self, ctx: LionContext,
|
||||||
session_lobby: Optional[discord.TextChannel | discord.VoiceChannel] = None,
|
session_lobby: Optional[discord.TextChannel | discord.VoiceChannel] = None,
|
||||||
session_room: Optional[discord.VoiceChannel] = None,
|
session_room: Optional[discord.VoiceChannel] = None,
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ from meta import conf
|
|||||||
from meta.errors import UserInputError
|
from meta.errors import UserInputError
|
||||||
from meta.sharding import THIS_SHARD
|
from meta.sharding import THIS_SHARD
|
||||||
from meta.logger import log_wrap
|
from meta.logger import log_wrap
|
||||||
|
from wards import low_management_iward, high_management_iward
|
||||||
|
|
||||||
from babel.translator import ctx_translator
|
from babel.translator import ctx_translator
|
||||||
|
|
||||||
@@ -63,7 +64,8 @@ class ScheduleSettings(SettingGroup):
|
|||||||
class SessionLobby(ModelData, ChannelSetting):
|
class SessionLobby(ModelData, ChannelSetting):
|
||||||
setting_id = 'session_lobby'
|
setting_id = 'session_lobby'
|
||||||
_event = 'guildset_session_lobby'
|
_event = 'guildset_session_lobby'
|
||||||
_set_cmd = 'configure schedule'
|
_set_cmd = 'admin config schedule'
|
||||||
|
_write_ward = high_management_iward
|
||||||
|
|
||||||
_display_name = _p('guildset:session_lobby', "session_lobby")
|
_display_name = _p('guildset:session_lobby', "session_lobby")
|
||||||
_desc = _p(
|
_desc = _p(
|
||||||
@@ -119,7 +121,8 @@ class ScheduleSettings(SettingGroup):
|
|||||||
@ScheduleConfig.register_model_setting
|
@ScheduleConfig.register_model_setting
|
||||||
class SessionRoom(ModelData, ChannelSetting):
|
class SessionRoom(ModelData, ChannelSetting):
|
||||||
setting_id = 'session_room'
|
setting_id = 'session_room'
|
||||||
_set_cmd = 'configure schedule'
|
_set_cmd = 'admin config schedule'
|
||||||
|
_write_ward = high_management_iward
|
||||||
|
|
||||||
_display_name = _p('guildset:session_room', "session_room")
|
_display_name = _p('guildset:session_room', "session_room")
|
||||||
_desc = _p(
|
_desc = _p(
|
||||||
@@ -163,6 +166,7 @@ class ScheduleSettings(SettingGroup):
|
|||||||
|
|
||||||
class SessionChannels(ListData, ChannelListSetting):
|
class SessionChannels(ListData, ChannelListSetting):
|
||||||
setting_id = 'session_channels'
|
setting_id = 'session_channels'
|
||||||
|
_write_ward = high_management_iward
|
||||||
|
|
||||||
_display_name = _p('guildset:session_channels', "session_channels")
|
_display_name = _p('guildset:session_channels', "session_channels")
|
||||||
_desc = _p(
|
_desc = _p(
|
||||||
@@ -238,7 +242,8 @@ class ScheduleSettings(SettingGroup):
|
|||||||
@ScheduleConfig.register_model_setting
|
@ScheduleConfig.register_model_setting
|
||||||
class ScheduleCost(ModelData, CoinSetting):
|
class ScheduleCost(ModelData, CoinSetting):
|
||||||
setting_id = 'schedule_cost'
|
setting_id = 'schedule_cost'
|
||||||
_set_cmd = 'configure schedule'
|
_set_cmd = 'admin config schedule'
|
||||||
|
_write_ward = low_management_iward
|
||||||
|
|
||||||
_display_name = _p('guildset:schedule_cost', "schedule_cost")
|
_display_name = _p('guildset:schedule_cost', "schedule_cost")
|
||||||
_desc = _p(
|
_desc = _p(
|
||||||
@@ -283,7 +288,8 @@ class ScheduleSettings(SettingGroup):
|
|||||||
@ScheduleConfig.register_model_setting
|
@ScheduleConfig.register_model_setting
|
||||||
class AttendanceReward(ModelData, CoinSetting):
|
class AttendanceReward(ModelData, CoinSetting):
|
||||||
setting_id = 'attendance_reward'
|
setting_id = 'attendance_reward'
|
||||||
_set_cmd = 'configure schedule'
|
_set_cmd = 'admin config schedule'
|
||||||
|
_write_ward = low_management_iward
|
||||||
|
|
||||||
_display_name = _p('guildset:attendance_reward', "attendance_reward")
|
_display_name = _p('guildset:attendance_reward', "attendance_reward")
|
||||||
_desc = _p(
|
_desc = _p(
|
||||||
@@ -327,7 +333,8 @@ class ScheduleSettings(SettingGroup):
|
|||||||
@ScheduleConfig.register_model_setting
|
@ScheduleConfig.register_model_setting
|
||||||
class AttendanceBonus(ModelData, CoinSetting):
|
class AttendanceBonus(ModelData, CoinSetting):
|
||||||
setting_id = 'attendance_bonus'
|
setting_id = 'attendance_bonus'
|
||||||
_set_cmd = 'configure schedule'
|
_set_cmd = 'admin config schedule'
|
||||||
|
_write_ward = low_management_iward
|
||||||
|
|
||||||
_display_name = _p('guildset:attendance_bonus', "group_attendance_bonus")
|
_display_name = _p('guildset:attendance_bonus', "group_attendance_bonus")
|
||||||
_desc = _p(
|
_desc = _p(
|
||||||
@@ -370,7 +377,8 @@ class ScheduleSettings(SettingGroup):
|
|||||||
@ScheduleConfig.register_model_setting
|
@ScheduleConfig.register_model_setting
|
||||||
class MinAttendance(ModelData, IntegerSetting):
|
class MinAttendance(ModelData, IntegerSetting):
|
||||||
setting_id = 'min_attendance'
|
setting_id = 'min_attendance'
|
||||||
_set_cmd = 'configure schedule'
|
_set_cmd = 'admin config schedule'
|
||||||
|
_write_ward = low_management_iward
|
||||||
|
|
||||||
_display_name = _p('guildset:min_attendance', "min_attendance")
|
_display_name = _p('guildset:min_attendance', "min_attendance")
|
||||||
_desc = _p(
|
_desc = _p(
|
||||||
@@ -437,8 +445,9 @@ class ScheduleSettings(SettingGroup):
|
|||||||
@ScheduleConfig.register_model_setting
|
@ScheduleConfig.register_model_setting
|
||||||
class BlacklistRole(ModelData, RoleSetting):
|
class BlacklistRole(ModelData, RoleSetting):
|
||||||
setting_id = 'schedule_blacklist_role'
|
setting_id = 'schedule_blacklist_role'
|
||||||
_set_cmd = 'configure schedule'
|
_set_cmd = 'admin config schedule'
|
||||||
_event = 'guildset_schedule_blacklist_role'
|
_event = 'guildset_schedule_blacklist_role'
|
||||||
|
_write_ward = high_management_iward
|
||||||
|
|
||||||
_display_name = _p('guildset:schedule_blacklist_role', "schedule_blacklist_role")
|
_display_name = _p('guildset:schedule_blacklist_role', "schedule_blacklist_role")
|
||||||
_desc = _p(
|
_desc = _p(
|
||||||
@@ -495,7 +504,8 @@ class ScheduleSettings(SettingGroup):
|
|||||||
@ScheduleConfig.register_model_setting
|
@ScheduleConfig.register_model_setting
|
||||||
class BlacklistAfter(ModelData, IntegerSetting):
|
class BlacklistAfter(ModelData, IntegerSetting):
|
||||||
setting_id = 'schedule_blacklist_after'
|
setting_id = 'schedule_blacklist_after'
|
||||||
_set_cmd = 'configure schedule'
|
_set_cmd = 'admin config schedule'
|
||||||
|
_write_ward = low_management_iward
|
||||||
|
|
||||||
_display_name = _p('guildset:schedule_blacklist_after', "schedule_blacklist_after")
|
_display_name = _p('guildset:schedule_blacklist_after', "schedule_blacklist_after")
|
||||||
_desc = _p(
|
_desc = _p(
|
||||||
|
|||||||
@@ -28,7 +28,21 @@ if TYPE_CHECKING:
|
|||||||
|
|
||||||
guide = _p(
|
guide = _p(
|
||||||
'ui:schedule|about',
|
'ui:schedule|about',
|
||||||
"Guide tips here TBD"
|
"**Do you think you can commit to a schedule and stick to it?**\n"
|
||||||
|
"**Schedule voice sessions here and get rewarded for keeping yourself accountable!**\n\n"
|
||||||
|
"Use the menu below to book timeslots using LionCoins. "
|
||||||
|
"If you are active (in the dedicated voice channels) during these times, "
|
||||||
|
"you will be rewarded, along with a large bonus if everyone who scheduled that slot "
|
||||||
|
"made it!\n"
|
||||||
|
"Beware though, if you fail to make it, all your booked sessions will be cancelled "
|
||||||
|
"with no refund! And if you keep failing to attend your scheduled sessions, "
|
||||||
|
"you may be forbidden from booking them in future.\n\n"
|
||||||
|
"When your scheduled session starts, you will recieve a ping from the schedule channel, "
|
||||||
|
"which will have more information about how to attend your session.\n"
|
||||||
|
"If you discover you can't make your scheduled session, please be responsible "
|
||||||
|
"and use this command to cancel or clear your schedule!\n\n"
|
||||||
|
"**Note:** *Make sure your timezone is set correctly (with `/my timezone`), "
|
||||||
|
"or the times I tell might not make sense!*"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -78,6 +78,7 @@ class ScheduleSettingUI(ConfigUI):
|
|||||||
# TODO: Setting value checks
|
# TODO: Setting value checks
|
||||||
await selection.response.defer()
|
await selection.response.defer()
|
||||||
setting = self.get_instance(ScheduleSettings.SessionLobby)
|
setting = self.get_instance(ScheduleSettings.SessionLobby)
|
||||||
|
await setting.interaction_check(setting.parent_id, selection)
|
||||||
setting.value = selected.values[0] if selected.values else None
|
setting.value = selected.values[0] if selected.values else None
|
||||||
await setting.write()
|
await setting.write()
|
||||||
|
|
||||||
@@ -95,6 +96,7 @@ class ScheduleSettingUI(ConfigUI):
|
|||||||
async def room_menu(self, selection: discord.Interaction, selected: ChannelSelect):
|
async def room_menu(self, selection: discord.Interaction, selected: ChannelSelect):
|
||||||
await selection.response.defer()
|
await selection.response.defer()
|
||||||
setting = self.get_instance(ScheduleSettings.SessionRoom)
|
setting = self.get_instance(ScheduleSettings.SessionRoom)
|
||||||
|
await setting.interaction_check(setting.parent_id, selection)
|
||||||
setting.value = selected.values[0] if selected.values else None
|
setting.value = selected.values[0] if selected.values else None
|
||||||
await setting.write()
|
await setting.write()
|
||||||
|
|
||||||
@@ -113,6 +115,7 @@ class ScheduleSettingUI(ConfigUI):
|
|||||||
# TODO: Consider XORing input
|
# TODO: Consider XORing input
|
||||||
await selection.response.defer()
|
await selection.response.defer()
|
||||||
setting = self.get_instance(ScheduleSettings.SessionChannels)
|
setting = self.get_instance(ScheduleSettings.SessionChannels)
|
||||||
|
await setting.interaction_check(setting.parent_id, selection)
|
||||||
setting.value = selected.values
|
setting.value = selected.values
|
||||||
await setting.write()
|
await setting.write()
|
||||||
|
|
||||||
@@ -158,6 +161,7 @@ class ScheduleSettingUI(ConfigUI):
|
|||||||
async def blacklist_role_menu(self, selection: discord.Interaction, selected: RoleSelect):
|
async def blacklist_role_menu(self, selection: discord.Interaction, selected: RoleSelect):
|
||||||
await selection.response.defer()
|
await selection.response.defer()
|
||||||
setting = self.get_instance(ScheduleSettings.BlacklistRole)
|
setting = self.get_instance(ScheduleSettings.BlacklistRole)
|
||||||
|
await setting.interaction_check(setting.parent_id, selection)
|
||||||
setting.value = selected.values[0] if selected.values else None
|
setting.value = selected.values[0] if selected.values else None
|
||||||
# TODO: Warning for insufficient permissions?
|
# TODO: Warning for insufficient permissions?
|
||||||
await setting.write()
|
await setting.write()
|
||||||
@@ -227,7 +231,7 @@ class ScheduleSettingUI(ConfigUI):
|
|||||||
class ScheduleDashboard(DashboardSection):
|
class ScheduleDashboard(DashboardSection):
|
||||||
section_name = _p(
|
section_name = _p(
|
||||||
'dash:schedule|title',
|
'dash:schedule|title',
|
||||||
"Scheduled Session Configuration ({commands[configure schedule]})"
|
"Scheduled Session Configuration ({commands[admin config schedule]})"
|
||||||
)
|
)
|
||||||
_option_name = _p(
|
_option_name = _p(
|
||||||
"dash:schedule|dropdown|placeholder",
|
"dash:schedule|dropdown|placeholder",
|
||||||
@@ -248,7 +252,7 @@ class ScheduleDashboard(DashboardSection):
|
|||||||
page.add_field(
|
page.add_field(
|
||||||
name=t(_p(
|
name=t(_p(
|
||||||
'dash:schedule|section:schedule_channels|name',
|
'dash:schedule|section:schedule_channels|name',
|
||||||
"Scheduled Session Channels ({commands[configure schedule]})",
|
"Scheduled Session Channels ({commands[admin config schedule]})",
|
||||||
)).format(commands=self.bot.core.mention_cache),
|
)).format(commands=self.bot.core.mention_cache),
|
||||||
value=table,
|
value=table,
|
||||||
inline=False
|
inline=False
|
||||||
@@ -258,7 +262,7 @@ class ScheduleDashboard(DashboardSection):
|
|||||||
page.add_field(
|
page.add_field(
|
||||||
name=t(_p(
|
name=t(_p(
|
||||||
'dash:schedule|section:schedule_rewards|name',
|
'dash:schedule|section:schedule_rewards|name',
|
||||||
"Scheduled Session Rewards ({commands[configure schedule]})",
|
"Scheduled Session Rewards ({commands[admin config schedule]})",
|
||||||
)).format(commands=self.bot.core.mention_cache),
|
)).format(commands=self.bot.core.mention_cache),
|
||||||
value=table,
|
value=table,
|
||||||
inline=False
|
inline=False
|
||||||
@@ -268,7 +272,7 @@ class ScheduleDashboard(DashboardSection):
|
|||||||
page.add_field(
|
page.add_field(
|
||||||
name=t(_p(
|
name=t(_p(
|
||||||
'dash:schedule|section:schedule_blacklist|name',
|
'dash:schedule|section:schedule_blacklist|name',
|
||||||
"Scheduled Session Blacklist ({commands[configure schedule]})",
|
"Scheduled Session Blacklist ({commands[admin config schedule]})",
|
||||||
)).format(commands=self.bot.core.mention_cache),
|
)).format(commands=self.bot.core.mention_cache),
|
||||||
value=table,
|
value=table,
|
||||||
inline=False
|
inline=False
|
||||||
|
|||||||
10
src/modules/sponsors/__init__.py
Normal file
10
src/modules/sponsors/__init__.py
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
import logging
|
||||||
|
from babel.translator import LocalBabel
|
||||||
|
|
||||||
|
babel = LocalBabel('sponsors')
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
async def setup(bot):
|
||||||
|
from .cog import SponsorCog
|
||||||
|
await bot.add_cog(SponsorCog(bot))
|
||||||
126
src/modules/sponsors/cog.py
Normal file
126
src/modules/sponsors/cog.py
Normal file
@@ -0,0 +1,126 @@
|
|||||||
|
from typing import Optional
|
||||||
|
import asyncio
|
||||||
|
|
||||||
|
import discord
|
||||||
|
from discord.ext import commands as cmds
|
||||||
|
import discord.app_commands as appcmds
|
||||||
|
|
||||||
|
from meta import LionCog, LionBot, LionContext
|
||||||
|
from wards import sys_admin_ward
|
||||||
|
|
||||||
|
from . import logger, babel
|
||||||
|
from .data import SponsorData
|
||||||
|
from .settings import SponsorSettings
|
||||||
|
from .settingui import SponsorUI
|
||||||
|
|
||||||
|
_p = babel._p
|
||||||
|
|
||||||
|
|
||||||
|
class SponsorCog(LionCog):
|
||||||
|
def __init__(self, bot: LionBot):
|
||||||
|
self.bot = bot
|
||||||
|
self.data: SponsorData = bot.db.load_registry(SponsorData())
|
||||||
|
self.settings = SponsorSettings
|
||||||
|
|
||||||
|
self.whitelisted = self.settings.Whitelist._cache
|
||||||
|
|
||||||
|
async def cog_load(self):
|
||||||
|
await self.data.init()
|
||||||
|
if (leo_setting_cog := self.bot.get_cog('LeoSettings')) is not None:
|
||||||
|
leo_setting_cog.bot_setting_groups.append(self.settings)
|
||||||
|
self.crossload_group(self.leo_group, leo_setting_cog.leo_group)
|
||||||
|
|
||||||
|
async def do_sponsor_prompt(self, interaction: discord.Interaction):
|
||||||
|
"""
|
||||||
|
Send the sponsor prompt as a followup to this interaction, if applicable.
|
||||||
|
"""
|
||||||
|
if not interaction.is_expired():
|
||||||
|
# TODO: caching
|
||||||
|
whitelist = (await self.settings.Whitelist.get(self.bot.appname)).value
|
||||||
|
if interaction.guild and interaction.guild.id in whitelist:
|
||||||
|
return
|
||||||
|
setting = await self.settings.SponsorPrompt.get(self.bot.appname)
|
||||||
|
value = setting.value
|
||||||
|
if value:
|
||||||
|
args = setting.value_to_args(self.bot.appname, value)
|
||||||
|
followup = interaction.followup
|
||||||
|
await followup.send(**args.send_args, ephemeral=True)
|
||||||
|
|
||||||
|
@cmds.hybrid_command(
|
||||||
|
name=_p('cmd:sponsors', "sponsors"),
|
||||||
|
description=_p(
|
||||||
|
'cmd:sponsors|desc',
|
||||||
|
"Check out our wonderful partners!"
|
||||||
|
)
|
||||||
|
)
|
||||||
|
async def sponsor_cmd(self, ctx: LionContext):
|
||||||
|
"""
|
||||||
|
Display the sponsors message, if set.
|
||||||
|
"""
|
||||||
|
if ctx.interaction:
|
||||||
|
await ctx.interaction.response.defer(thinking=True, ephemeral=True)
|
||||||
|
|
||||||
|
sponsor = await self.settings.SponsorMessage.get(self.bot.appname)
|
||||||
|
value = sponsor.value
|
||||||
|
if value:
|
||||||
|
args = sponsor.value_to_args(self.bot.appname, value)
|
||||||
|
await ctx.reply(**args.send_args)
|
||||||
|
else:
|
||||||
|
await ctx.reply(
|
||||||
|
"Coming Soon!"
|
||||||
|
)
|
||||||
|
|
||||||
|
@LionCog.placeholder_group
|
||||||
|
@cmds.hybrid_group("leo", with_app_command=False)
|
||||||
|
async def leo_group(self, ctx: LionContext):
|
||||||
|
...
|
||||||
|
|
||||||
|
@leo_group.command(
|
||||||
|
name=_p(
|
||||||
|
'cmd:leo_sponsors', "sponsors"
|
||||||
|
),
|
||||||
|
description=_p(
|
||||||
|
'cmd:leo_sponsors|desc',
|
||||||
|
"Configure the sponsor text and whitelist."
|
||||||
|
)
|
||||||
|
)
|
||||||
|
@appcmds.rename(
|
||||||
|
sponsor_prompt=SponsorSettings.SponsorPrompt._display_name,
|
||||||
|
sponsor_message=SponsorSettings.SponsorMessage._display_name,
|
||||||
|
)
|
||||||
|
@appcmds.describe(
|
||||||
|
sponsor_prompt=SponsorSettings.SponsorPrompt._desc,
|
||||||
|
sponsor_message=SponsorSettings.SponsorMessage._desc,
|
||||||
|
)
|
||||||
|
@sys_admin_ward
|
||||||
|
async def leo_sponsors_cmd(self, ctx: LionContext,
|
||||||
|
sponsor_prompt: Optional[discord.Attachment] = None,
|
||||||
|
sponsor_message: Optional[discord.Attachment] = None,
|
||||||
|
):
|
||||||
|
"""
|
||||||
|
Open the configuration UI for sponsors, and optionally set the prompt and message.
|
||||||
|
"""
|
||||||
|
if not ctx.interaction:
|
||||||
|
return
|
||||||
|
|
||||||
|
await ctx.interaction.response.defer(thinking=True)
|
||||||
|
modified = []
|
||||||
|
|
||||||
|
if sponsor_prompt is not None:
|
||||||
|
setting = self.settings.SponsorPrompt
|
||||||
|
content = await setting.download_attachment(sponsor_prompt)
|
||||||
|
instance = await setting.from_string(self.bot.appname, content)
|
||||||
|
modified.append(instance)
|
||||||
|
|
||||||
|
if sponsor_message is not None:
|
||||||
|
setting = self.settings.SponsorMessage
|
||||||
|
content = await setting.download_attachment(sponsor_message)
|
||||||
|
instance = await setting.from_string(self.bot.appname, content)
|
||||||
|
modified.append(instance)
|
||||||
|
|
||||||
|
for instance in modified:
|
||||||
|
await instance.write()
|
||||||
|
|
||||||
|
ui = SponsorUI(self.bot, self.bot.appname, ctx.channel.id)
|
||||||
|
await ui.run(ctx.interaction)
|
||||||
|
await ui.wait()
|
||||||
5
src/modules/sponsors/data.py
Normal file
5
src/modules/sponsors/data.py
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
from data import Registry, Table
|
||||||
|
|
||||||
|
|
||||||
|
class SponsorData(Registry):
|
||||||
|
sponsor_whitelist = Table('sponsor_guild_whitelist')
|
||||||
87
src/modules/sponsors/settings.py
Normal file
87
src/modules/sponsors/settings.py
Normal file
@@ -0,0 +1,87 @@
|
|||||||
|
from settings.data import ListData, ModelData
|
||||||
|
from settings.groups import SettingGroup
|
||||||
|
from settings.setting_types import GuildIDListSetting
|
||||||
|
|
||||||
|
from core.setting_types import MessageSetting
|
||||||
|
from core.data import CoreData
|
||||||
|
from wards import sys_admin_iward
|
||||||
|
from . import babel
|
||||||
|
from .data import SponsorData
|
||||||
|
|
||||||
|
_p = babel._p
|
||||||
|
|
||||||
|
|
||||||
|
class SponsorSettings(SettingGroup):
|
||||||
|
class Whitelist(ListData, GuildIDListSetting):
|
||||||
|
setting_id = 'sponsor_whitelist'
|
||||||
|
_write_ward = sys_admin_iward
|
||||||
|
|
||||||
|
_display_name = _p(
|
||||||
|
'botset:sponsor_whitelist', "sponsor_whitelist"
|
||||||
|
)
|
||||||
|
_desc = _p(
|
||||||
|
'botset:sponsor_whitelist|desc',
|
||||||
|
"List of guildids where the sponsor prompt is not shown."
|
||||||
|
)
|
||||||
|
_long_desc = _p(
|
||||||
|
'botset:sponsor_whitelist|long_desc',
|
||||||
|
"The sponsor prompt will not appear in the set guilds."
|
||||||
|
)
|
||||||
|
_accepts = _p(
|
||||||
|
'botset:sponsor_whitelist|accetps',
|
||||||
|
"Comma separated list of guildids."
|
||||||
|
)
|
||||||
|
|
||||||
|
_table_interface = SponsorData.sponsor_whitelist
|
||||||
|
_id_column = 'appid'
|
||||||
|
_data_column = 'guildid'
|
||||||
|
_order_column = 'guildid'
|
||||||
|
|
||||||
|
class SponsorPrompt(ModelData, MessageSetting):
|
||||||
|
setting_id = 'sponsor_prompt'
|
||||||
|
_set_cmd = 'leo sponsors'
|
||||||
|
_write_ward = sys_admin_iward
|
||||||
|
|
||||||
|
_display_name = _p(
|
||||||
|
'botset:sponsor_prompt', "sponsor_prompt"
|
||||||
|
)
|
||||||
|
_desc = _p(
|
||||||
|
'botset:sponsor_prompt|desc',
|
||||||
|
"Message to add underneath core commands."
|
||||||
|
)
|
||||||
|
_long_desc = _p(
|
||||||
|
'botset:sponsor_prompt|long_desc',
|
||||||
|
"Content of the message to send after core commands such as stats,"
|
||||||
|
" reminding users to check the sponsors command."
|
||||||
|
)
|
||||||
|
|
||||||
|
_model = CoreData.BotConfig
|
||||||
|
_column = CoreData.BotConfig.sponsor_prompt.name
|
||||||
|
|
||||||
|
async def editor_callback(self, editor_data):
|
||||||
|
self.value = editor_data
|
||||||
|
await self.write()
|
||||||
|
|
||||||
|
class SponsorMessage(ModelData, MessageSetting):
|
||||||
|
setting_id = 'sponsor_message'
|
||||||
|
_set_cmd = 'leo sponsors'
|
||||||
|
_write_ward = sys_admin_iward
|
||||||
|
|
||||||
|
_display_name = _p(
|
||||||
|
'botset:sponsor_message', "sponsor_message"
|
||||||
|
)
|
||||||
|
_desc = _p(
|
||||||
|
'botset:sponsor_message|desc',
|
||||||
|
"Message to send in response to /sponsors command."
|
||||||
|
)
|
||||||
|
_long_desc = _p(
|
||||||
|
'botset:sponsor_message|long_desc',
|
||||||
|
"Content of the message to send when a user runs the `/sponsors` command."
|
||||||
|
)
|
||||||
|
|
||||||
|
_model = CoreData.BotConfig
|
||||||
|
_column = CoreData.BotConfig.sponsor_message.name
|
||||||
|
|
||||||
|
async def editor_callback(self, editor_data):
|
||||||
|
self.value = editor_data
|
||||||
|
await self.write()
|
||||||
122
src/modules/sponsors/settingui.py
Normal file
122
src/modules/sponsors/settingui.py
Normal file
@@ -0,0 +1,122 @@
|
|||||||
|
import asyncio
|
||||||
|
|
||||||
|
import discord
|
||||||
|
from discord.ui.button import button, Button, ButtonStyle
|
||||||
|
|
||||||
|
from meta import LionBot
|
||||||
|
|
||||||
|
from utils.ui import ConfigUI
|
||||||
|
from utils.lib import MessageArgs
|
||||||
|
from utils.ui.msgeditor import MsgEditor
|
||||||
|
|
||||||
|
from .settings import SponsorSettings as Settings
|
||||||
|
from . import babel, logger
|
||||||
|
|
||||||
|
_p = babel._p
|
||||||
|
|
||||||
|
|
||||||
|
class SponsorUI(ConfigUI):
|
||||||
|
setting_classes = (
|
||||||
|
Settings.SponsorPrompt,
|
||||||
|
Settings.SponsorMessage,
|
||||||
|
Settings.Whitelist,
|
||||||
|
)
|
||||||
|
|
||||||
|
def __init__(self, bot: LionBot, appname: str, channelid: int, **kwargs):
|
||||||
|
self.settings = bot.get_cog('SponsorCog').settings
|
||||||
|
super().__init__(bot, appname, channelid, **kwargs)
|
||||||
|
|
||||||
|
# ----- UI Components -----
|
||||||
|
@button(
|
||||||
|
label="SPONSOR_PROMPT_BUTTON_PLACEHOLDER",
|
||||||
|
style=ButtonStyle.blurple
|
||||||
|
)
|
||||||
|
async def sponsor_prompt_button(self, press: discord.Interaction, pressed: Button):
|
||||||
|
await press.response.defer(thinking=True, ephemeral=True)
|
||||||
|
setting = self.get_instance(Settings.SponsorPrompt)
|
||||||
|
|
||||||
|
value = setting.value
|
||||||
|
if value is None:
|
||||||
|
value = {'content': "Empty"}
|
||||||
|
|
||||||
|
editor = MsgEditor(
|
||||||
|
self.bot,
|
||||||
|
value,
|
||||||
|
callback=setting.editor_callback,
|
||||||
|
callerid=press.user.id,
|
||||||
|
)
|
||||||
|
self._slaves.append(editor)
|
||||||
|
await editor.run(press)
|
||||||
|
|
||||||
|
async def sponsor_prompt_button_refresh(self):
|
||||||
|
button = self.sponsor_prompt_button
|
||||||
|
t = self.bot.translator.t
|
||||||
|
button.label = t(_p(
|
||||||
|
'ui:sponsors|button:sponsor_prompt|label',
|
||||||
|
"Sponsor Prompt"
|
||||||
|
))
|
||||||
|
|
||||||
|
@button(
|
||||||
|
label="SPONSOR_MESSAGE_BUTTON_PLACEHOLDER",
|
||||||
|
style=ButtonStyle.blurple
|
||||||
|
)
|
||||||
|
async def sponsor_message_button(self, press: discord.Interaction, pressed: Button):
|
||||||
|
await press.response.defer(thinking=True, ephemeral=True)
|
||||||
|
setting = self.get_instance(Settings.SponsorMessage)
|
||||||
|
|
||||||
|
value = setting.value
|
||||||
|
if value is None:
|
||||||
|
value = {'content': "Empty"}
|
||||||
|
|
||||||
|
editor = MsgEditor(
|
||||||
|
self.bot,
|
||||||
|
value,
|
||||||
|
callback=setting.editor_callback,
|
||||||
|
callerid=press.user.id,
|
||||||
|
)
|
||||||
|
self._slaves.append(editor)
|
||||||
|
await editor.run(press)
|
||||||
|
|
||||||
|
async def sponsor_message_button_refresh(self):
|
||||||
|
button = self.sponsor_message_button
|
||||||
|
t = self.bot.translator.t
|
||||||
|
button.label = t(_p(
|
||||||
|
'ui:sponsors|button:sponsor_message|label',
|
||||||
|
"Sponsor Message"
|
||||||
|
))
|
||||||
|
# ----- UI Flow -----
|
||||||
|
async def make_message(self) -> MessageArgs:
|
||||||
|
t = self.bot.translator.t
|
||||||
|
title = t(_p(
|
||||||
|
'ui:sponsors|embed|title',
|
||||||
|
"Leo Sponsor Panel"
|
||||||
|
))
|
||||||
|
embed = discord.Embed(
|
||||||
|
title=title,
|
||||||
|
colour=discord.Colour.orange()
|
||||||
|
)
|
||||||
|
for setting in self.instances:
|
||||||
|
embed.add_field(**setting.embed_field, inline=False)
|
||||||
|
|
||||||
|
return MessageArgs(embed=embed)
|
||||||
|
|
||||||
|
async def reload(self):
|
||||||
|
self.instances = [
|
||||||
|
await setting.get(self.bot.appname)
|
||||||
|
for setting in self.setting_classes
|
||||||
|
]
|
||||||
|
|
||||||
|
async def refresh_components(self):
|
||||||
|
to_refresh = (
|
||||||
|
self.edit_button_refresh(),
|
||||||
|
self.close_button_refresh(),
|
||||||
|
self.reset_button_refresh(),
|
||||||
|
self.sponsor_message_button_refresh(),
|
||||||
|
self.sponsor_prompt_button_refresh(),
|
||||||
|
)
|
||||||
|
await asyncio.gather(*to_refresh)
|
||||||
|
|
||||||
|
self.set_layout(
|
||||||
|
(self.sponsor_prompt_button, self.sponsor_message_button,
|
||||||
|
self.edit_button, self.reset_button, self.close_button)
|
||||||
|
)
|
||||||
@@ -12,7 +12,7 @@ from core.lion_guild import VoiceMode
|
|||||||
from utils.lib import error_embed
|
from utils.lib import error_embed
|
||||||
from utils.ui import LeoUI, AButton, utc_now
|
from utils.ui import LeoUI, AButton, utc_now
|
||||||
from gui.base import CardMode
|
from gui.base import CardMode
|
||||||
from wards import low_management_ward
|
from wards import high_management_ward
|
||||||
|
|
||||||
from . import babel
|
from . import babel
|
||||||
from .data import StatsData
|
from .data import StatsData
|
||||||
@@ -41,7 +41,7 @@ class StatsCog(LionCog):
|
|||||||
self.bot.core.guild_config.register_setting(self.settings.UnrankedRoles)
|
self.bot.core.guild_config.register_setting(self.settings.UnrankedRoles)
|
||||||
|
|
||||||
configcog = self.bot.get_cog('ConfigCog')
|
configcog = self.bot.get_cog('ConfigCog')
|
||||||
self.crossload_group(self.configure_group, configcog.configure_group)
|
self.crossload_group(self.configure_group, configcog.admin_config_group)
|
||||||
|
|
||||||
@cmds.hybrid_command(
|
@cmds.hybrid_command(
|
||||||
name=_p('cmd:me', "me"),
|
name=_p('cmd:me', "me"),
|
||||||
@@ -55,6 +55,8 @@ class StatsCog(LionCog):
|
|||||||
await ctx.interaction.response.defer(thinking=True)
|
await ctx.interaction.response.defer(thinking=True)
|
||||||
ui = ProfileUI(self.bot, ctx.author, ctx.guild)
|
ui = ProfileUI(self.bot, ctx.author, ctx.guild)
|
||||||
await ui.run(ctx.interaction)
|
await ui.run(ctx.interaction)
|
||||||
|
if sponsors := self.bot.get_cog('SponsorCog'):
|
||||||
|
await sponsors.do_sponsor_prompt(ctx.interaction)
|
||||||
await ui.wait()
|
await ui.wait()
|
||||||
|
|
||||||
@cmds.hybrid_command(
|
@cmds.hybrid_command(
|
||||||
@@ -101,6 +103,9 @@ class StatsCog(LionCog):
|
|||||||
file = discord.File(profile_data, 'profile.png')
|
file = discord.File(profile_data, 'profile.png')
|
||||||
await ctx.reply(file=file)
|
await ctx.reply(file=file)
|
||||||
|
|
||||||
|
if sponsors := self.bot.get_cog('SponsorCog'):
|
||||||
|
await sponsors.do_sponsor_prompt(ctx.interaction)
|
||||||
|
|
||||||
@cmds.hybrid_command(
|
@cmds.hybrid_command(
|
||||||
name=_p('cmd:stats', "stats"),
|
name=_p('cmd:stats', "stats"),
|
||||||
description=_p(
|
description=_p(
|
||||||
@@ -116,6 +121,10 @@ class StatsCog(LionCog):
|
|||||||
await ctx.interaction.response.defer(thinking=True)
|
await ctx.interaction.response.defer(thinking=True)
|
||||||
ui = WeeklyMonthlyUI(self.bot, ctx.author, ctx.guild)
|
ui = WeeklyMonthlyUI(self.bot, ctx.author, ctx.guild)
|
||||||
await ui.run(ctx.interaction)
|
await ui.run(ctx.interaction)
|
||||||
|
|
||||||
|
if sponsors := self.bot.get_cog('SponsorCog'):
|
||||||
|
await sponsors.do_sponsor_prompt(ctx.interaction)
|
||||||
|
|
||||||
await ui.wait()
|
await ui.wait()
|
||||||
|
|
||||||
@cmds.hybrid_command(
|
@cmds.hybrid_command(
|
||||||
@@ -151,6 +160,10 @@ class StatsCog(LionCog):
|
|||||||
await ctx.interaction.response.defer(thinking=True)
|
await ctx.interaction.response.defer(thinking=True)
|
||||||
ui = LeaderboardUI(self.bot, ctx.author, ctx.guild)
|
ui = LeaderboardUI(self.bot, ctx.author, ctx.guild)
|
||||||
await ui.run(ctx.interaction)
|
await ui.run(ctx.interaction)
|
||||||
|
|
||||||
|
if sponsors := self.bot.get_cog('SponsorCog'):
|
||||||
|
await sponsors.do_sponsor_prompt(ctx.interaction)
|
||||||
|
|
||||||
await ui.wait()
|
await ui.wait()
|
||||||
|
|
||||||
@cmds.hybrid_command(
|
@cmds.hybrid_command(
|
||||||
@@ -204,8 +217,7 @@ class StatsCog(LionCog):
|
|||||||
"Time from which to start counting activity for rank badges and season leaderboards. (YYYY-MM-DD)"
|
"Time from which to start counting activity for rank badges and season leaderboards. (YYYY-MM-DD)"
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
@appcmds.default_permissions(manage_guild=True)
|
@high_management_ward
|
||||||
@low_management_ward
|
|
||||||
async def configure_statistics_cmd(self, ctx: LionContext,
|
async def configure_statistics_cmd(self, ctx: LionContext,
|
||||||
season_start: Optional[str] = None):
|
season_start: Optional[str] = None):
|
||||||
t = self.bot.translator.t
|
t = self.bot.translator.t
|
||||||
|
|||||||
@@ -70,7 +70,7 @@ async def get_stats_card(bot: LionBot, userid: int, guildid: int, mode: CardMode
|
|||||||
refkey = (guildid, userid)
|
refkey = (guildid, userid)
|
||||||
else:
|
else:
|
||||||
model = data.UserExp
|
model = data.UserExp
|
||||||
msg_since = msgmodel.member_messages_between
|
msg_since = msgmodel.user_messages_since
|
||||||
refkey = (userid,)
|
refkey = (userid,)
|
||||||
ref_since = model.xp_since
|
ref_since = model.xp_since
|
||||||
ref_between = model.xp_between
|
ref_between = model.xp_between
|
||||||
|
|||||||
@@ -21,6 +21,7 @@ from utils.lib import MessageArgs
|
|||||||
from core.data import CoreData
|
from core.data import CoreData
|
||||||
from core.lion_guild import VoiceMode
|
from core.lion_guild import VoiceMode
|
||||||
from babel.translator import ctx_translator
|
from babel.translator import ctx_translator
|
||||||
|
from wards import low_management_iward, high_management_iward
|
||||||
|
|
||||||
from . import babel
|
from . import babel
|
||||||
from .data import StatsData, StatisticType
|
from .data import StatsData, StatisticType
|
||||||
@@ -83,7 +84,8 @@ class StatisticsSettings(SettingGroup):
|
|||||||
Time is assumed to be in set guild timezone (although supports +00 syntax)
|
Time is assumed to be in set guild timezone (although supports +00 syntax)
|
||||||
"""
|
"""
|
||||||
setting_id = 'season_start'
|
setting_id = 'season_start'
|
||||||
_set_cmd = 'configure statistics'
|
_set_cmd = 'admin config statistics'
|
||||||
|
_write_ward = high_management_iward
|
||||||
|
|
||||||
_display_name = _p('guildset:season_start', "season_start")
|
_display_name = _p('guildset:season_start', "season_start")
|
||||||
_desc = _p(
|
_desc = _p(
|
||||||
@@ -155,6 +157,7 @@ class StatisticsSettings(SettingGroup):
|
|||||||
List of roles not displayed on the leaderboard
|
List of roles not displayed on the leaderboard
|
||||||
"""
|
"""
|
||||||
setting_id = 'unranked_roles'
|
setting_id = 'unranked_roles'
|
||||||
|
_write_ward = high_management_iward
|
||||||
|
|
||||||
_display_name = _p('guildset:unranked_roles', "unranked_roles")
|
_display_name = _p('guildset:unranked_roles', "unranked_roles")
|
||||||
_desc = _p(
|
_desc = _p(
|
||||||
@@ -211,6 +214,7 @@ class StatisticsSettings(SettingGroup):
|
|||||||
Default is determined by current guild mode
|
Default is determined by current guild mode
|
||||||
"""
|
"""
|
||||||
setting_id = 'visible_stats'
|
setting_id = 'visible_stats'
|
||||||
|
_write_ward = high_management_iward
|
||||||
|
|
||||||
_setting = StatTypeSetting
|
_setting = StatTypeSetting
|
||||||
|
|
||||||
@@ -263,6 +267,7 @@ class StatisticsSettings(SettingGroup):
|
|||||||
Which of the three stats to display by default
|
Which of the three stats to display by default
|
||||||
"""
|
"""
|
||||||
setting_id = 'default_stat'
|
setting_id = 'default_stat'
|
||||||
|
_write_ward = high_management_iward
|
||||||
|
|
||||||
_display_name = _p('guildset:default_stat', "default_stat")
|
_display_name = _p('guildset:default_stat', "default_stat")
|
||||||
_desc = _p(
|
_desc = _p(
|
||||||
@@ -294,6 +299,7 @@ class StatisticsConfigUI(ConfigUI):
|
|||||||
"""
|
"""
|
||||||
await selection.response.defer(thinking=True)
|
await selection.response.defer(thinking=True)
|
||||||
setting = self.instances[1]
|
setting = self.instances[1]
|
||||||
|
await setting.interaction_check(setting.parent_id, selection)
|
||||||
setting.value = selected.values
|
setting.value = selected.values
|
||||||
await setting.write()
|
await setting.write()
|
||||||
# Don't need to refresh due to instance hooks
|
# Don't need to refresh due to instance hooks
|
||||||
@@ -314,6 +320,7 @@ class StatisticsConfigUI(ConfigUI):
|
|||||||
"""
|
"""
|
||||||
await selection.response.defer(thinking=True)
|
await selection.response.defer(thinking=True)
|
||||||
setting = self.instances[2]
|
setting = self.instances[2]
|
||||||
|
await setting.interaction_check(setting.parent_id, selection)
|
||||||
data = [StatisticType((value,)) for value in selected.values]
|
data = [StatisticType((value,)) for value in selected.values]
|
||||||
setting.data = data
|
setting.data = data
|
||||||
await setting.write()
|
await setting.write()
|
||||||
@@ -405,7 +412,7 @@ class StatisticsConfigUI(ConfigUI):
|
|||||||
class StatisticsDashboard(DashboardSection):
|
class StatisticsDashboard(DashboardSection):
|
||||||
section_name = _p(
|
section_name = _p(
|
||||||
'dash:stats|title',
|
'dash:stats|title',
|
||||||
"Activity Statistics Configuration ({commands[configure statistics]})"
|
"Activity Statistics Configuration ({commands[admin config statistics]})"
|
||||||
)
|
)
|
||||||
_option_name = _p(
|
_option_name = _p(
|
||||||
"dash:stats|dropdown|placeholder",
|
"dash:stats|dropdown|placeholder",
|
||||||
|
|||||||
@@ -538,6 +538,10 @@ class LeaderboardUI(StatsUI):
|
|||||||
page_row
|
page_row
|
||||||
]
|
]
|
||||||
|
|
||||||
|
voting = self.bot.get_cog('TopggCog')
|
||||||
|
if voting and not await voting.check_voted_recently(self.userid):
|
||||||
|
self._layout.append((voting.vote_button(),))
|
||||||
|
|
||||||
async def reload(self):
|
async def reload(self):
|
||||||
"""
|
"""
|
||||||
Reload UI data, applying cache where possible.
|
Reload UI data, applying cache where possible.
|
||||||
|
|||||||
@@ -297,6 +297,10 @@ class ProfileUI(StatsUI):
|
|||||||
(self.stats_button, self.edit_button, self.close_button)
|
(self.stats_button, self.edit_button, self.close_button)
|
||||||
]
|
]
|
||||||
|
|
||||||
|
voting = self.bot.get_cog('TopggCog')
|
||||||
|
if voting and not await voting.check_voted_recently(self.userid):
|
||||||
|
self._layout.append((voting.vote_button(),))
|
||||||
|
|
||||||
async def _render_stats(self):
|
async def _render_stats(self):
|
||||||
"""
|
"""
|
||||||
Create and render the profile card.
|
Create and render the profile card.
|
||||||
|
|||||||
@@ -750,6 +750,11 @@ class WeeklyMonthlyUI(StatsUI):
|
|||||||
(self.type_menu,),
|
(self.type_menu,),
|
||||||
(self.edit_button, self.select_button, self.global_button, self.close_button)
|
(self.edit_button, self.select_button, self.global_button, self.close_button)
|
||||||
]
|
]
|
||||||
|
|
||||||
|
voting = self.bot.get_cog('TopggCog')
|
||||||
|
if voting and not await voting.check_voted_recently(self.userid):
|
||||||
|
self._layout.append((voting.vote_button(),))
|
||||||
|
|
||||||
if self._showing_selector:
|
if self._showing_selector:
|
||||||
await self.period_menu_refresh()
|
await self.period_menu_refresh()
|
||||||
self._layout.append((self.period_menu,))
|
self._layout.append((self.period_menu,))
|
||||||
|
|||||||
@@ -139,7 +139,7 @@ class TasklistCog(LionCog):
|
|||||||
self.bot.add_view(TasklistCaller(self.bot))
|
self.bot.add_view(TasklistCaller(self.bot))
|
||||||
|
|
||||||
configcog = self.bot.get_cog('ConfigCog')
|
configcog = self.bot.get_cog('ConfigCog')
|
||||||
self.crossload_group(self.configure_group, configcog.configure_group)
|
self.crossload_group(self.configure_group, configcog.config_group)
|
||||||
|
|
||||||
@LionCog.listener('on_tasks_completed')
|
@LionCog.listener('on_tasks_completed')
|
||||||
@log_wrap(action="reward tasks completed")
|
@log_wrap(action="reward tasks completed")
|
||||||
@@ -261,6 +261,7 @@ class TasklistCog(LionCog):
|
|||||||
"Open your tasklist."
|
"Open your tasklist."
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
@appcmds.guild_only
|
||||||
async def tasklist_cmd(self, ctx: LionContext):
|
async def tasklist_cmd(self, ctx: LionContext):
|
||||||
if not ctx.interaction:
|
if not ctx.interaction:
|
||||||
return
|
return
|
||||||
@@ -270,6 +271,7 @@ class TasklistCog(LionCog):
|
|||||||
name=_p('group:tasks', "tasks"),
|
name=_p('group:tasks', "tasks"),
|
||||||
description=_p('group:tasks|desc', "Base command group for tasklist commands.")
|
description=_p('group:tasks|desc', "Base command group for tasklist commands.")
|
||||||
)
|
)
|
||||||
|
@appcmds.guild_only
|
||||||
async def tasklist_group(self, ctx: LionContext):
|
async def tasklist_group(self, ctx: LionContext):
|
||||||
raise NotImplementedError
|
raise NotImplementedError
|
||||||
|
|
||||||
@@ -984,7 +986,6 @@ class TasklistCog(LionCog):
|
|||||||
reward=TasklistSettings.task_reward._desc,
|
reward=TasklistSettings.task_reward._desc,
|
||||||
reward_limit=TasklistSettings.task_reward_limit._desc
|
reward_limit=TasklistSettings.task_reward_limit._desc
|
||||||
)
|
)
|
||||||
@appcmds.default_permissions(manage_guild=True)
|
|
||||||
@low_management_ward
|
@low_management_ward
|
||||||
async def configure_tasklist_cmd(self, ctx: LionContext,
|
async def configure_tasklist_cmd(self, ctx: LionContext,
|
||||||
reward: Optional[int] = None,
|
reward: Optional[int] = None,
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ from utils.lib import tabulate
|
|||||||
from utils.ui import LeoUI, FastModal, error_handler_for, ModalRetryUI, DashboardSection
|
from utils.ui import LeoUI, FastModal, error_handler_for, ModalRetryUI, DashboardSection
|
||||||
from core.data import CoreData
|
from core.data import CoreData
|
||||||
from babel.translator import ctx_translator
|
from babel.translator import ctx_translator
|
||||||
|
from wards import low_management_iward, high_management_iward
|
||||||
|
|
||||||
from . import babel
|
from . import babel
|
||||||
from .data import TasklistData
|
from .data import TasklistData
|
||||||
@@ -28,7 +29,8 @@ class TasklistSettings(SettingGroup):
|
|||||||
Exposed via `/configure tasklist`, and the standard configuration interface.
|
Exposed via `/configure tasklist`, and the standard configuration interface.
|
||||||
"""
|
"""
|
||||||
setting_id = 'task_reward'
|
setting_id = 'task_reward'
|
||||||
_set_cmd = 'configure tasklist'
|
_set_cmd = 'config tasklist'
|
||||||
|
_write_ward = low_management_iward
|
||||||
|
|
||||||
_display_name = _p('guildset:task_reward', "task_reward")
|
_display_name = _p('guildset:task_reward', "task_reward")
|
||||||
_desc = _p(
|
_desc = _p(
|
||||||
@@ -68,7 +70,8 @@ class TasklistSettings(SettingGroup):
|
|||||||
|
|
||||||
class task_reward_limit(ModelData, IntegerSetting):
|
class task_reward_limit(ModelData, IntegerSetting):
|
||||||
setting_id = 'task_reward_limit'
|
setting_id = 'task_reward_limit'
|
||||||
_set_cmd = 'configure tasklist'
|
_set_cmd = 'config tasklist'
|
||||||
|
_write_ward = low_management_iward
|
||||||
|
|
||||||
_display_name = _p('guildset:task_reward_limit', "task_reward_limit")
|
_display_name = _p('guildset:task_reward_limit', "task_reward_limit")
|
||||||
_desc = _p(
|
_desc = _p(
|
||||||
@@ -109,6 +112,7 @@ class TasklistSettings(SettingGroup):
|
|||||||
|
|
||||||
class tasklist_channels(ListData, ChannelListSetting):
|
class tasklist_channels(ListData, ChannelListSetting):
|
||||||
setting_id = 'tasklist_channels'
|
setting_id = 'tasklist_channels'
|
||||||
|
_write_ward = low_management_iward
|
||||||
|
|
||||||
_display_name = _p('guildset:tasklist_channels', "tasklist_channels")
|
_display_name = _p('guildset:tasklist_channels', "tasklist_channels")
|
||||||
_desc = _p(
|
_desc = _p(
|
||||||
@@ -317,7 +321,7 @@ class TasklistConfigUI(LeoUI):
|
|||||||
|
|
||||||
|
|
||||||
class TasklistDashboard(DashboardSection):
|
class TasklistDashboard(DashboardSection):
|
||||||
section_name = _p('dash:tasklist|name', "Tasklist Configuration ({commands[configure tasklist]})")
|
section_name = _p('dash:tasklist|name', "Tasklist Configuration ({commands[config tasklist]})")
|
||||||
_option_name = _p(
|
_option_name = _p(
|
||||||
"dash:tasklist|dropdown|placeholder",
|
"dash:tasklist|dropdown|placeholder",
|
||||||
"Tasklist Options Panel"
|
"Tasklist Options Panel"
|
||||||
|
|||||||
10
src/modules/topgg/__init__.py
Normal file
10
src/modules/topgg/__init__.py
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
import logging
|
||||||
|
from babel.translator import LocalBabel
|
||||||
|
|
||||||
|
babel = LocalBabel('topgg')
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
async def setup(bot):
|
||||||
|
from .cog import TopggCog
|
||||||
|
await bot.add_cog(TopggCog(bot))
|
||||||
135
src/modules/topgg/cog.py
Normal file
135
src/modules/topgg/cog.py
Normal file
@@ -0,0 +1,135 @@
|
|||||||
|
from typing import Optional
|
||||||
|
import asyncio
|
||||||
|
|
||||||
|
import discord
|
||||||
|
from discord.ext import commands as cmds
|
||||||
|
import discord.app_commands as appcmds
|
||||||
|
from discord.ui.button import Button, ButtonStyle
|
||||||
|
from topgg import WebhookManager
|
||||||
|
from data.queries import ORDER
|
||||||
|
|
||||||
|
from meta import LionCog, LionBot, LionContext
|
||||||
|
from meta.logger import log_wrap
|
||||||
|
from wards import sys_admin_ward
|
||||||
|
from utils.lib import utc_now
|
||||||
|
from babel.translator import ctx_locale
|
||||||
|
|
||||||
|
from . import logger, babel
|
||||||
|
from .data import TopggData
|
||||||
|
|
||||||
|
_p = babel._p
|
||||||
|
|
||||||
|
topgg_upvote_link = 'https://top.gg/bot/889078613817831495/vote'
|
||||||
|
|
||||||
|
|
||||||
|
class TopggCog(LionCog):
|
||||||
|
def __init__(self, bot: LionBot):
|
||||||
|
self.bot = bot
|
||||||
|
self.data: TopggData = bot.db.load_registry(TopggData())
|
||||||
|
|
||||||
|
self.topgg_webhook: Optional[WebhookManager] = None
|
||||||
|
|
||||||
|
async def cog_load(self):
|
||||||
|
await self.data.init()
|
||||||
|
|
||||||
|
tgg_config = self.bot.config.topgg
|
||||||
|
if tgg_config.getboolean('enabled', False):
|
||||||
|
economy = self.bot.get_cog('Economy')
|
||||||
|
economy.register_economy_bonus(self.voting_bonus, name='voting')
|
||||||
|
|
||||||
|
if self.bot.shard_id != 0:
|
||||||
|
logger.debug(
|
||||||
|
f"Not initialising topgg executor in shard {self.bot.shard_id}"
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
self.topgg_webhook = WebhookManager(self.bot).dbl_webhook(
|
||||||
|
route=tgg_config.get('route'),
|
||||||
|
auth_key=tgg_config.get('auth'),
|
||||||
|
)
|
||||||
|
self.topgg_webhook.run(tgg_config.getint('port'))
|
||||||
|
|
||||||
|
logger.info(
|
||||||
|
"Topgg webhook registered."
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
logger.info(
|
||||||
|
"Topgg disabled via config, not initialising module."
|
||||||
|
)
|
||||||
|
|
||||||
|
@LionCog.listener('on_dbl_vote')
|
||||||
|
@log_wrap(action="Handle DBL Vote")
|
||||||
|
async def handle_dbl_vote(self, data):
|
||||||
|
logger.info(f"Recieved TopGG vote: {data}")
|
||||||
|
userid = data['user']
|
||||||
|
|
||||||
|
await self.data.TopGG.create(
|
||||||
|
userid=userid,
|
||||||
|
boostedtimestamp=utc_now()
|
||||||
|
)
|
||||||
|
await self._send_thanks_dm(userid)
|
||||||
|
|
||||||
|
async def voting_bonus(self, userid):
|
||||||
|
# Provides 1.25 multiplicative bonus if they have voted within 12h
|
||||||
|
if await self.check_voted_recently(userid):
|
||||||
|
return 1.25
|
||||||
|
else:
|
||||||
|
return 1
|
||||||
|
|
||||||
|
async def check_voted_recently(self, userid):
|
||||||
|
records = await self.data.TopGG.fetch_where(
|
||||||
|
userid=userid
|
||||||
|
).order_by('boostedtimestamp', ORDER.DESC).limit(1)
|
||||||
|
|
||||||
|
return records and (utc_now() - records[0].boostedtimestamp).total_seconds() < 3600 * 12
|
||||||
|
|
||||||
|
def vote_button(self):
|
||||||
|
t = self.bot.translator.t
|
||||||
|
|
||||||
|
button = Button(
|
||||||
|
style=ButtonStyle.link,
|
||||||
|
label=t(_p(
|
||||||
|
'button:vote|label',
|
||||||
|
"Vote for me!"
|
||||||
|
)),
|
||||||
|
emoji=self.bot.config.emojis.coin,
|
||||||
|
url=topgg_upvote_link,
|
||||||
|
)
|
||||||
|
return button
|
||||||
|
|
||||||
|
async def _send_thanks_dm(self, userid: int):
|
||||||
|
user = self.bot.get_user(userid)
|
||||||
|
if user is None:
|
||||||
|
try:
|
||||||
|
user = await self.bot.fetch_user(userid)
|
||||||
|
except discord.HTTPException:
|
||||||
|
logger.warning(
|
||||||
|
f"Could not find voting user <uid: {userid}> to send thanks."
|
||||||
|
)
|
||||||
|
return
|
||||||
|
|
||||||
|
t = self.bot.translator.t
|
||||||
|
luser = await self.bot.core.lions.fetch_user(userid)
|
||||||
|
locale = await self.bot.get_cog('BabelCog').get_user_locale(userid)
|
||||||
|
ctx_locale.set(locale)
|
||||||
|
|
||||||
|
embed = discord.Embed(
|
||||||
|
colour=discord.Colour.brand_green(),
|
||||||
|
title=t(_p(
|
||||||
|
'embed:voting_thanks|title',
|
||||||
|
"Thank you for supporting me on Top.gg! {yay}"
|
||||||
|
)).format(yay=self.bot.config.emojis.lionyay),
|
||||||
|
description=t(_p(
|
||||||
|
'embed:voting_thanks|desc',
|
||||||
|
"Thank you for supporting us, enjoy your LionCoins boost!"
|
||||||
|
))
|
||||||
|
|
||||||
|
).set_image(
|
||||||
|
url="https://cdn.discordapp.com/attachments/908283085999706153/932737228440993822/lion-yay.png"
|
||||||
|
)
|
||||||
|
|
||||||
|
try:
|
||||||
|
await user.send(embed=embed)
|
||||||
|
except discord.HTTPException:
|
||||||
|
logger.warning(
|
||||||
|
f"Could not send voting thanks to user <uid: {userid}>."
|
||||||
|
)
|
||||||
13
src/modules/topgg/data.py
Normal file
13
src/modules/topgg/data.py
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
from data import Registry, Table, RowModel
|
||||||
|
from data.columns import Integer, Timestamp
|
||||||
|
|
||||||
|
|
||||||
|
class TopggData(Registry):
|
||||||
|
class TopGG(RowModel):
|
||||||
|
_tablename_ = 'topgg'
|
||||||
|
|
||||||
|
voteid = Integer(primary=True)
|
||||||
|
userid = Integer()
|
||||||
|
boostedtimestamp = Timestamp()
|
||||||
|
|
||||||
|
guild_whitelist = Table('topgg_guild_whitelist')
|
||||||
@@ -57,7 +57,7 @@ class VideoCog(LionCog):
|
|||||||
"Could not load ConfigCog. VideoCog configuration will not crossload."
|
"Could not load ConfigCog. VideoCog configuration will not crossload."
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
self.crossload_group(self.configure_group, configcog.configure_group)
|
self.crossload_group(self.configure_group, configcog.admin_config_group)
|
||||||
|
|
||||||
if self.bot.is_ready():
|
if self.bot.is_ready():
|
||||||
await self.initialise()
|
await self.initialise()
|
||||||
@@ -522,7 +522,7 @@ class VideoCog(LionCog):
|
|||||||
video_blacklist_durations=VideoSettings.VideoBlacklistDurations._desc,
|
video_blacklist_durations=VideoSettings.VideoBlacklistDurations._desc,
|
||||||
video_grace_period=VideoSettings.VideoGracePeriod._desc,
|
video_grace_period=VideoSettings.VideoGracePeriod._desc,
|
||||||
)
|
)
|
||||||
@low_management_ward
|
@high_management_ward
|
||||||
async def configure_video(self, ctx: LionContext,
|
async def configure_video(self, ctx: LionContext,
|
||||||
video_blacklist: Optional[discord.Role] = None,
|
video_blacklist: Optional[discord.Role] = None,
|
||||||
video_blacklist_durations: Optional[str] = None,
|
video_blacklist_durations: Optional[str] = None,
|
||||||
@@ -572,4 +572,3 @@ class VideoCog(LionCog):
|
|||||||
ui = VideoSettingUI(self.bot, ctx.guild.id, ctx.channel.id)
|
ui = VideoSettingUI(self.bot, ctx.guild.id, ctx.channel.id)
|
||||||
await ui.run(ctx.interaction)
|
await ui.run(ctx.interaction)
|
||||||
await ui.wait()
|
await ui.wait()
|
||||||
|
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ from meta.sharding import THIS_SHARD
|
|||||||
from meta.logger import log_wrap
|
from meta.logger import log_wrap
|
||||||
from core.data import CoreData
|
from core.data import CoreData
|
||||||
from babel.translator import ctx_translator
|
from babel.translator import ctx_translator
|
||||||
|
from wards import low_management_iward, high_management_iward
|
||||||
|
|
||||||
from . import babel, logger
|
from . import babel, logger
|
||||||
from .data import VideoData
|
from .data import VideoData
|
||||||
@@ -25,6 +26,7 @@ class VideoSettings(SettingGroup):
|
|||||||
class VideoChannels(ListData, ChannelListSetting):
|
class VideoChannels(ListData, ChannelListSetting):
|
||||||
setting_id = "video_channels"
|
setting_id = "video_channels"
|
||||||
_event = 'guildset_video_channels'
|
_event = 'guildset_video_channels'
|
||||||
|
_write_ward = high_management_iward
|
||||||
|
|
||||||
_display_name = _p('guildset:video_channels', "video_channels")
|
_display_name = _p('guildset:video_channels', "video_channels")
|
||||||
_desc = _p(
|
_desc = _p(
|
||||||
@@ -101,6 +103,7 @@ class VideoSettings(SettingGroup):
|
|||||||
class VideoBlacklist(ModelData, RoleSetting):
|
class VideoBlacklist(ModelData, RoleSetting):
|
||||||
setting_id = "video_blacklist"
|
setting_id = "video_blacklist"
|
||||||
_event = 'guildset_video_blacklist'
|
_event = 'guildset_video_blacklist'
|
||||||
|
_write_ward = high_management_iward
|
||||||
|
|
||||||
_display_name = _p('guildset:video_blacklist', "video_blacklist")
|
_display_name = _p('guildset:video_blacklist', "video_blacklist")
|
||||||
_desc = _p(
|
_desc = _p(
|
||||||
@@ -158,6 +161,7 @@ class VideoSettings(SettingGroup):
|
|||||||
class VideoBlacklistDurations(ListData, ListSetting, InteractiveSetting):
|
class VideoBlacklistDurations(ListData, ListSetting, InteractiveSetting):
|
||||||
setting_id = 'video_durations'
|
setting_id = 'video_durations'
|
||||||
_setting = DurationSetting
|
_setting = DurationSetting
|
||||||
|
_write_ward = high_management_iward
|
||||||
|
|
||||||
_display_name = _p('guildset:video_durations', "video_blacklist_durations")
|
_display_name = _p('guildset:video_durations', "video_blacklist_durations")
|
||||||
_desc = _p(
|
_desc = _p(
|
||||||
@@ -217,6 +221,7 @@ class VideoSettings(SettingGroup):
|
|||||||
class VideoGracePeriod(ModelData, DurationSetting):
|
class VideoGracePeriod(ModelData, DurationSetting):
|
||||||
setting_id = "video_grace_period"
|
setting_id = "video_grace_period"
|
||||||
_event = 'guildset_video_grace_period'
|
_event = 'guildset_video_grace_period'
|
||||||
|
_write_ward = high_management_iward
|
||||||
|
|
||||||
_display_name = _p('guildset:video_grace_period', "video_grace_period")
|
_display_name = _p('guildset:video_grace_period', "video_grace_period")
|
||||||
_desc = _p(
|
_desc = _p(
|
||||||
@@ -252,6 +257,7 @@ class VideoSettings(SettingGroup):
|
|||||||
class VideoExempt(ListData, RoleListSetting):
|
class VideoExempt(ListData, RoleListSetting):
|
||||||
setting_id = "video_exempt"
|
setting_id = "video_exempt"
|
||||||
_event = 'guildset_video_exempt'
|
_event = 'guildset_video_exempt'
|
||||||
|
_write_ward = high_management_iward
|
||||||
|
|
||||||
_display_name = _p('guildset:video_exempt', "video_exempt")
|
_display_name = _p('guildset:video_exempt', "video_exempt")
|
||||||
_desc = _p(
|
_desc = _p(
|
||||||
|
|||||||
@@ -45,6 +45,7 @@ class VideoSettingUI(ConfigUI):
|
|||||||
await selection.response.defer(thinking=True, ephemeral=True)
|
await selection.response.defer(thinking=True, ephemeral=True)
|
||||||
|
|
||||||
setting = self.get_instance(VideoSettings.VideoChannels)
|
setting = self.get_instance(VideoSettings.VideoChannels)
|
||||||
|
await setting.interaction_check(setting.parent_id, selection)
|
||||||
setting.value = selected.values
|
setting.value = selected.values
|
||||||
await setting.write()
|
await setting.write()
|
||||||
await selection.delete_original_response()
|
await selection.delete_original_response()
|
||||||
@@ -70,6 +71,7 @@ class VideoSettingUI(ConfigUI):
|
|||||||
await selection.response.defer(thinking=True, ephemeral=True)
|
await selection.response.defer(thinking=True, ephemeral=True)
|
||||||
|
|
||||||
setting = self.get_instance(VideoSettings.VideoExempt)
|
setting = self.get_instance(VideoSettings.VideoExempt)
|
||||||
|
await setting.interaction_check(setting.parent_id, selection)
|
||||||
setting.value = selected.values
|
setting.value = selected.values
|
||||||
await setting.write()
|
await setting.write()
|
||||||
await selection.delete_original_response()
|
await selection.delete_original_response()
|
||||||
@@ -95,6 +97,7 @@ class VideoSettingUI(ConfigUI):
|
|||||||
await selection.response.defer(thinking=True, ephemeral=True)
|
await selection.response.defer(thinking=True, ephemeral=True)
|
||||||
|
|
||||||
setting = self.get_instance(VideoSettings.VideoBlacklist)
|
setting = self.get_instance(VideoSettings.VideoBlacklist)
|
||||||
|
await setting.interaction_check(setting.parent_id, selection)
|
||||||
setting.value = selected.values[0] if selected.values else None
|
setting.value = selected.values[0] if selected.values else None
|
||||||
if setting.value:
|
if setting.value:
|
||||||
await equippable_role(self.bot, setting.value, selection.user)
|
await equippable_role(self.bot, setting.value, selection.user)
|
||||||
@@ -153,7 +156,7 @@ class VideoSettingUI(ConfigUI):
|
|||||||
class VideoDashboard(DashboardSection):
|
class VideoDashboard(DashboardSection):
|
||||||
section_name = _p(
|
section_name = _p(
|
||||||
"dash:video|title",
|
"dash:video|title",
|
||||||
"Video Channel Settings ({commands[configure video_channels]})"
|
"Video Channel Settings ({commands[admin config video_channels]})"
|
||||||
)
|
)
|
||||||
_option_name = _p(
|
_option_name = _p(
|
||||||
"dash:video|option|name",
|
"dash:video|option|name",
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user