feat(config): Split mod and admin config.
This commit is contained in:
@@ -41,7 +41,7 @@ class BabelCog(LionCog):
|
||||
self.bot.core.user_config.register_model_setting(LocaleSettings.UserLocale)
|
||||
|
||||
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')
|
||||
self.crossload_group(self.userconfig_group, userconfigcog.userconfig_group)
|
||||
@@ -114,8 +114,6 @@ class BabelCog(LionCog):
|
||||
language=LocaleSettings.GuildLocale._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
|
||||
async def cmd_configure_language(self, ctx: LionContext,
|
||||
language: Optional[str] = None,
|
||||
|
||||
@@ -7,6 +7,7 @@ from settings.groups import SettingGroup
|
||||
from meta.errors import UserInputError
|
||||
from meta.context import ctx_bot
|
||||
from core.data import CoreData
|
||||
from wards import low_management_iward
|
||||
|
||||
from .translator import ctx_translator
|
||||
from . import babel
|
||||
@@ -104,9 +105,10 @@ class LocaleSettings(SettingGroup):
|
||||
"""
|
||||
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'
|
||||
_write_ward = low_management_iward
|
||||
|
||||
_display_name = _p('guildset:force_locale', 'force_language')
|
||||
_desc = _p('guildset:force_locale|desc',
|
||||
@@ -144,15 +146,16 @@ class LocaleSettings(SettingGroup):
|
||||
def set_str(self):
|
||||
bot = ctx_bot.get()
|
||||
if bot:
|
||||
return bot.core.mention_cmd('configure language')
|
||||
return bot.core.mention_cmd('config language')
|
||||
|
||||
class GuildLocale(ModelData, LocaleSetting):
|
||||
"""
|
||||
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'
|
||||
_write_ward = low_management_iward
|
||||
|
||||
_display_name = _p('guildset:locale', 'language')
|
||||
_desc = _p('guildset:locale|desc', "Your preferred language for interacting with me.")
|
||||
@@ -180,4 +183,4 @@ class LocaleSettings(SettingGroup):
|
||||
def set_str(self):
|
||||
bot = ctx_bot.get()
|
||||
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):
|
||||
await press.response.defer()
|
||||
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
|
||||
await setting.write()
|
||||
|
||||
@@ -80,7 +81,7 @@ class LocaleSettingUI(ConfigUI):
|
||||
class LocaleDashboard(DashboardSection):
|
||||
section_name = _p(
|
||||
'dash:locale|title',
|
||||
"Server Language Configuration ({commands[configure language]})"
|
||||
"Server Language Configuration ({commands[config language]})"
|
||||
)
|
||||
_option_name = _p(
|
||||
"dash:locale|dropdown|placeholder",
|
||||
|
||||
@@ -25,12 +25,35 @@ class ConfigCog(LionCog):
|
||||
...
|
||||
|
||||
@cmds.hybrid_group(
|
||||
name=_p('group:configure', "configure"),
|
||||
description=_p('group:configure|desc', "View and adjust my configuration options."),
|
||||
name=_p('group:config', "config"),
|
||||
description=_p('group:config|desc', "View and adjust moderation-level configuration."),
|
||||
)
|
||||
@appcmds.guild_only
|
||||
@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.
|
||||
"""
|
||||
|
||||
@@ -29,14 +29,14 @@ class GuildConfigCog(LionCog):
|
||||
configcog = self.bot.get_cog('ConfigCog')
|
||||
if configcog is None:
|
||||
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(
|
||||
name="dashboard",
|
||||
description="At-a-glance view of the server's configuration."
|
||||
)
|
||||
@appcmds.guild_only
|
||||
@appcmds.default_permissions(manage_guild=True)
|
||||
@low_management_ward
|
||||
async def dashboard_cmd(self, ctx: LionContext):
|
||||
if not ctx.guild or not ctx.interaction:
|
||||
return
|
||||
@@ -64,8 +64,6 @@ class GuildConfigCog(LionCog):
|
||||
timezone=GeneralSettings.Timezone._desc,
|
||||
event_log=GeneralSettings.EventLog._desc,
|
||||
)
|
||||
@appcmds.guild_only()
|
||||
@appcmds.default_permissions(manage_guild=True)
|
||||
@low_management_ward
|
||||
async def cmd_configure_general(self, ctx: LionContext,
|
||||
timezone: Optional[str] = None,
|
||||
|
||||
@@ -9,6 +9,7 @@ from meta.context import ctx_bot
|
||||
from meta.errors import UserInputError
|
||||
from core.data import CoreData
|
||||
from babel.translator import ctx_translator
|
||||
from wards import low_management_iward
|
||||
|
||||
from . import babel
|
||||
|
||||
@@ -20,13 +21,14 @@ class GeneralSettings(SettingGroup):
|
||||
"""
|
||||
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,
|
||||
and the timezone used to display guild-wide statistics.
|
||||
"""
|
||||
setting_id = 'timezone'
|
||||
_event = 'guildset_timezone'
|
||||
_set_cmd = 'configure general'
|
||||
_set_cmd = 'config general'
|
||||
_write_ward = low_management_iward
|
||||
|
||||
_display_name = _p('guildset:timezone', "timezone")
|
||||
_desc = _p(
|
||||
@@ -58,7 +60,8 @@ class GeneralSettings(SettingGroup):
|
||||
"""
|
||||
setting_id = 'eventlog'
|
||||
_event = 'guildset_eventlog'
|
||||
_set_cmd = 'configure general'
|
||||
_set_cmd = 'config general'
|
||||
_write_ward = low_management_iward
|
||||
|
||||
_display_name = _p('guildset:eventlog', "event_log")
|
||||
_desc = _p(
|
||||
|
||||
@@ -41,6 +41,7 @@ class GeneralSettingUI(ConfigUI):
|
||||
await selection.response.defer(thinking=True, ephemeral=True)
|
||||
|
||||
setting = self.get_instance(GeneralSettings.EventLog)
|
||||
await setting.interaction_check(setting.parent_id, selection)
|
||||
|
||||
value = selected.values[0].resolve() if selected.values else None
|
||||
setting = await setting.from_value(self.guildid, value)
|
||||
@@ -95,7 +96,7 @@ class GeneralSettingUI(ConfigUI):
|
||||
class GeneralDashboard(DashboardSection):
|
||||
section_name = _p(
|
||||
"dash:general|title",
|
||||
"General Configuration ({commands[configure general]})"
|
||||
"General Configuration ({commands[admin config general]})"
|
||||
)
|
||||
_option_name = _p(
|
||||
"dash:general|option|name",
|
||||
|
||||
@@ -64,7 +64,7 @@ class Economy(LionCog):
|
||||
"Attempting to load the EconomyCog before ConfigCog! Failed to crossload configuration group."
|
||||
)
|
||||
else:
|
||||
self.crossload_group(self.configure_group, configcog.configure_group)
|
||||
self.crossload_group(self.configure_group, configcog.config_group)
|
||||
|
||||
# ----- Economy Bonus registration -----
|
||||
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.default_permissions(manage_guild=True)
|
||||
@moderator_ward
|
||||
async def configure_economy(self, ctx: LionContext,
|
||||
allow_transfers: Optional[appcmds.Choice[int]] = None,
|
||||
|
||||
@@ -17,6 +17,7 @@ from meta.logger import log_wrap
|
||||
from core.data import CoreData
|
||||
from core.setting_types import CoinSetting
|
||||
from babel.translator import ctx_translator
|
||||
from wards import low_management_iward
|
||||
|
||||
from . import babel, logger
|
||||
from .data import EconomyData
|
||||
@@ -32,6 +33,7 @@ class EconomySettings(SettingGroup):
|
||||
"""
|
||||
class CoinsPerXP(ModelData, CoinSetting):
|
||||
setting_id = 'coins_per_xp'
|
||||
_write_ward = low_management_iward
|
||||
|
||||
_display_name = _p('guildset:coins_per_xp', "coins_per_100xp")
|
||||
_desc = _p(
|
||||
@@ -63,10 +65,11 @@ class EconomySettings(SettingGroup):
|
||||
@property
|
||||
def set_str(self):
|
||||
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):
|
||||
setting_id = 'allow_transfers'
|
||||
_write_ward = low_management_iward
|
||||
|
||||
_display_name = _p('guildset:allow_transfers', "allow_transfers")
|
||||
_desc = _p(
|
||||
@@ -91,7 +94,7 @@ class EconomySettings(SettingGroup):
|
||||
@property
|
||||
def set_str(self):
|
||||
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
|
||||
def update_message(self):
|
||||
@@ -115,6 +118,7 @@ class EconomySettings(SettingGroup):
|
||||
|
||||
class StartingFunds(ModelData, CoinSetting):
|
||||
setting_id = 'starting_funds'
|
||||
_write_ward = low_management_iward
|
||||
|
||||
_display_name = _p('guildset:starting_funds', "starting_funds")
|
||||
_desc = _p(
|
||||
|
||||
@@ -64,7 +64,7 @@ class EconomyConfigUI(ConfigUI):
|
||||
class EconomyDashboard(DashboardSection):
|
||||
section_name = _p(
|
||||
'dash:economy|title',
|
||||
"Economy Configuration ({commands[configure economy]})"
|
||||
"Economy Configuration ({commands[config economy]})"
|
||||
)
|
||||
_option_name = _p(
|
||||
"dash:economy|dropdown|placeholder",
|
||||
|
||||
@@ -46,7 +46,7 @@ class MemberAdminCog(LionCog):
|
||||
"Configuration command cannot be crossloaded."
|
||||
)
|
||||
else:
|
||||
self.crossload_group(self.configure_group, configcog.configure_group)
|
||||
self.crossload_group(self.configure_group, configcog.config_group)
|
||||
|
||||
# ----- Cog API -----
|
||||
async def absent_remove_role(self, guildid, userid, roleid):
|
||||
|
||||
@@ -9,6 +9,7 @@ from settings import ListData, ModelData
|
||||
from settings.groups import SettingGroup
|
||||
from settings.setting_types import BoolSetting, ChannelSetting, RoleListSetting
|
||||
from utils.lib import recurse_map, replace_multiple, tabulate
|
||||
from wards import low_management_iward, high_management_iward
|
||||
|
||||
from . import babel
|
||||
from .data import MemberAdminData
|
||||
@@ -36,6 +37,7 @@ _greeting_subkey_desc = {
|
||||
class MemberAdminSettings(SettingGroup):
|
||||
class GreetingChannel(ModelData, ChannelSetting):
|
||||
setting_id = 'greeting_channel'
|
||||
_write_ward = low_management_iward
|
||||
|
||||
_display_name = _p('guildset:greeting_channel', "welcome_channel")
|
||||
_desc = _p(
|
||||
@@ -87,6 +89,7 @@ class MemberAdminSettings(SettingGroup):
|
||||
|
||||
class GreetingMessage(ModelData, MessageSetting):
|
||||
setting_id = 'greeting_message'
|
||||
_write_ward = low_management_iward
|
||||
|
||||
_display_name = _p(
|
||||
'guildset:greeting_message', "welcome_message"
|
||||
@@ -209,6 +212,7 @@ class MemberAdminSettings(SettingGroup):
|
||||
|
||||
class ReturningMessage(ModelData, MessageSetting):
|
||||
setting_id = 'returning_message'
|
||||
_write_ward = low_management_iward
|
||||
|
||||
_display_name = _p(
|
||||
'guildset:returning_message', "returning_message"
|
||||
@@ -335,6 +339,7 @@ class MemberAdminSettings(SettingGroup):
|
||||
|
||||
class Autoroles(ListData, RoleListSetting):
|
||||
setting_id = 'autoroles'
|
||||
_write_ward = high_management_iward
|
||||
|
||||
_display_name = _p(
|
||||
'guildset:autoroles', "autoroles"
|
||||
@@ -357,6 +362,7 @@ class MemberAdminSettings(SettingGroup):
|
||||
|
||||
class BotAutoroles(ListData, RoleListSetting):
|
||||
setting_id = 'bot_autoroles'
|
||||
_write_ward = high_management_iward
|
||||
|
||||
_display_name = _p(
|
||||
'guildset:bot_autoroles', "bot_autoroles"
|
||||
@@ -379,6 +385,7 @@ class MemberAdminSettings(SettingGroup):
|
||||
class RolePersistence(ModelData, BoolSetting):
|
||||
setting_id = 'role_persistence'
|
||||
_event = 'guildset_role_persistence'
|
||||
_write_ward = low_management_iward
|
||||
|
||||
_display_name = _p('guildset:role_persistence', "role_persistence")
|
||||
_desc = _p(
|
||||
|
||||
@@ -45,6 +45,7 @@ class MemberAdminUI(ConfigUI):
|
||||
"""
|
||||
await selection.response.defer(thinking=True, ephemeral=True)
|
||||
setting = self.get_instance(Settings.GreetingChannel)
|
||||
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()
|
||||
@@ -73,6 +74,7 @@ class MemberAdminUI(ConfigUI):
|
||||
await equippable_role(self.bot, role, selection.user)
|
||||
|
||||
setting = self.get_instance(Settings.Autoroles)
|
||||
await setting.interaction_check(setting.parent_id, selection)
|
||||
setting.value = selected.values
|
||||
await setting.write()
|
||||
# Instance hooks will update the menu
|
||||
@@ -102,6 +104,7 @@ class MemberAdminUI(ConfigUI):
|
||||
await equippable_role(self.bot, role, selection.user)
|
||||
|
||||
setting = self.get_instance(Settings.BotAutoroles)
|
||||
await setting.interaction_check(setting.parent_id, selection)
|
||||
setting.value = selected.values
|
||||
await setting.write()
|
||||
# Instance hooks will update the menu
|
||||
@@ -131,6 +134,7 @@ class MemberAdminUI(ConfigUI):
|
||||
await press.response.defer(thinking=True, ephemeral=True)
|
||||
t = self.bot.translator.t
|
||||
setting = self.get_instance(Settings.GreetingMessage)
|
||||
await setting.interaction_check(setting.parent_id, press)
|
||||
|
||||
value = setting.value
|
||||
if value is None:
|
||||
@@ -173,6 +177,7 @@ class MemberAdminUI(ConfigUI):
|
||||
await press.response.defer(thinking=True, ephemeral=True)
|
||||
t = self.bot.translator.t
|
||||
setting = self.get_instance(Settings.ReturningMessage)
|
||||
await setting.interaction_check(setting.parent_id, press)
|
||||
greeting = self.get_instance(Settings.GreetingMessage)
|
||||
|
||||
value = setting.value
|
||||
@@ -254,7 +259,7 @@ class MemberAdminUI(ConfigUI):
|
||||
class MemberAdminDashboard(DashboardSection):
|
||||
section_name = _p(
|
||||
"dash:member_admin|title",
|
||||
"Greetings and Initial Roles ({commands[configure welcome]})"
|
||||
"Greetings and Initial Roles ({commands[admin config welcome]})"
|
||||
)
|
||||
_option_name = _p(
|
||||
"dash:member_admin|dropdown|placeholder",
|
||||
@@ -278,7 +283,7 @@ class MemberAdminDashboard(DashboardSection):
|
||||
page.add_field(
|
||||
name=t(_p(
|
||||
'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),
|
||||
value=table,
|
||||
inline=False
|
||||
@@ -289,7 +294,7 @@ class MemberAdminDashboard(DashboardSection):
|
||||
page.add_field(
|
||||
name=t(_p(
|
||||
'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),
|
||||
value=table,
|
||||
inline=False
|
||||
|
||||
@@ -74,8 +74,8 @@ admin_extra = _p(
|
||||
Use {cmd_dashboard} to see an overview of the server configuration, \
|
||||
and quickly jump to the feature configuration panels to modify settings.
|
||||
|
||||
Configuration panels are also accessible directly through the `/configure` commands \
|
||||
and most features may be configured through these commands.
|
||||
Most settings may also be directly set through the `/config` and `/admin config` commands, \
|
||||
depending on whether the settings require moderator (manage server) or admin level permissions, respectively.
|
||||
|
||||
Other relevant commands for guild configuration below:
|
||||
`/editshop`: Add/Edit/Remove colour roles from the {coin} shop.
|
||||
|
||||
@@ -51,7 +51,7 @@ class ModerationCog(LionCog):
|
||||
"Moderation configuration will not crossload."
|
||||
)
|
||||
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():
|
||||
await self.initialise()
|
||||
|
||||
@@ -6,6 +6,7 @@ from settings.setting_types import (
|
||||
|
||||
from core.data import CoreData
|
||||
from babel.translator import ctx_translator
|
||||
from wards import low_management_iward, high_management_iward
|
||||
|
||||
from . import babel
|
||||
|
||||
@@ -16,6 +17,7 @@ class ModerationSettings(SettingGroup):
|
||||
class TicketLog(ModelData, ChannelSetting):
|
||||
setting_id = "ticket_log"
|
||||
_event = 'guildset_ticket_log'
|
||||
_write_ward = low_management_iward
|
||||
|
||||
_display_name = _p('guildset:ticket_log', "ticket_log")
|
||||
_desc = _p(
|
||||
@@ -66,6 +68,7 @@ class ModerationSettings(SettingGroup):
|
||||
class AlertChannel(ModelData, ChannelSetting):
|
||||
setting_id = "alert_channel"
|
||||
_event = 'guildset_alert_channel'
|
||||
_write_ward = low_management_iward
|
||||
|
||||
_display_name = _p('guildset:alert_channel', "alert_channel")
|
||||
_desc = _p(
|
||||
@@ -119,18 +122,23 @@ class ModerationSettings(SettingGroup):
|
||||
class ModRole(ModelData, RoleSetting):
|
||||
setting_id = "mod_role"
|
||||
_event = 'guildset_mod_role'
|
||||
_write_ward = high_management_iward
|
||||
|
||||
_display_name = _p('guildset:mod_role', "mod_role")
|
||||
_desc = _p(
|
||||
'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(
|
||||
'guildset:mod_role|long_desc',
|
||||
"Members with the set role will be able to access my configuration panels, "
|
||||
"and perform some moderation tasks, such as setting up pomodoro timers. "
|
||||
"Moderators cannot reconfigure most bot configuration, "
|
||||
"or perform operations they do not already have permission for in Discord."
|
||||
"Members with the moderator role are considered moderators,"
|
||||
" and are permitted to use moderator commands,"
|
||||
" such as viewing and pardoning moderation tickets,"
|
||||
" 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(
|
||||
'guildset:mod_role|accepts',
|
||||
@@ -149,11 +157,13 @@ class ModerationSettings(SettingGroup):
|
||||
resp = t(_p(
|
||||
'guildset:mod_role|set_response:set',
|
||||
"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)
|
||||
else:
|
||||
resp = t(_p(
|
||||
'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
|
||||
|
||||
@@ -171,6 +181,7 @@ class ModerationSettings(SettingGroup):
|
||||
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(
|
||||
|
||||
@@ -42,6 +42,7 @@ class ModerationSettingUI(ConfigUI):
|
||||
await selection.response.defer(thinking=True, ephemeral=True)
|
||||
|
||||
setting = self.get_instance(ModerationSettings.TicketLog)
|
||||
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()
|
||||
@@ -67,6 +68,7 @@ class ModerationSettingUI(ConfigUI):
|
||||
await selection.response.defer(thinking=True, ephemeral=True)
|
||||
|
||||
setting = self.get_instance(ModerationSettings.AlertChannel)
|
||||
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()
|
||||
@@ -92,6 +94,7 @@ class ModerationSettingUI(ConfigUI):
|
||||
await selection.response.defer(thinking=True, ephemeral=True)
|
||||
|
||||
setting = self.get_instance(ModerationSettings.ModRole)
|
||||
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()
|
||||
@@ -117,6 +120,7 @@ class ModerationSettingUI(ConfigUI):
|
||||
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()
|
||||
@@ -175,7 +179,7 @@ class ModerationSettingUI(ConfigUI):
|
||||
class ModerationDashboard(DashboardSection):
|
||||
section_name = _p(
|
||||
"dash:moderation|title",
|
||||
"Moderation Settings ({commands[configure moderation]})"
|
||||
"Moderation Settings ({commands[admin config moderation]})"
|
||||
)
|
||||
_option_name = _p(
|
||||
"dash:moderation|dropdown|placeholder",
|
||||
|
||||
@@ -90,7 +90,7 @@ class TimerCog(LionCog):
|
||||
self.bot.core.guild_config.register_model_setting(self.settings.PomodoroChannel)
|
||||
|
||||
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():
|
||||
await self.initialise()
|
||||
@@ -977,7 +977,6 @@ class TimerCog(LionCog):
|
||||
@appcmds.describe(
|
||||
pomodoro_channel=TimerSettings.PomodoroChannel._desc
|
||||
)
|
||||
@appcmds.default_permissions(manage_guild=True)
|
||||
@low_management_ward
|
||||
async def configure_pomodoro_command(self, ctx: LionContext,
|
||||
pomodoro_channel: Optional[discord.VoiceChannel | discord.TextChannel] = None):
|
||||
|
||||
@@ -4,6 +4,7 @@ from settings.setting_types import ChannelSetting
|
||||
|
||||
from core.data import CoreData
|
||||
from babel.translator import ctx_translator
|
||||
from wards import low_management_iward
|
||||
|
||||
from . import babel
|
||||
|
||||
@@ -14,7 +15,8 @@ class TimerSettings(SettingGroup):
|
||||
class PomodoroChannel(ModelData, ChannelSetting):
|
||||
setting_id = '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")
|
||||
_desc = _p(
|
||||
|
||||
@@ -30,6 +30,7 @@ class TimerConfigUI(ConfigUI):
|
||||
async def channel_menu(self, selection: discord.Interaction, selected: ChannelSelect):
|
||||
await selection.response.defer()
|
||||
setting = self.instances[0]
|
||||
await setting.interaction_check(setting.parent_id, selection)
|
||||
setting.value = selected.values[0] if selected.values else None
|
||||
await setting.write()
|
||||
|
||||
@@ -78,7 +79,7 @@ class TimerConfigUI(ConfigUI):
|
||||
class TimerDashboard(DashboardSection):
|
||||
section_name = _p(
|
||||
'dash:pomodoro|title',
|
||||
"Pomodoro Configuration ({commands[configure pomodoro]})"
|
||||
"Pomodoro Configuration ({commands[admin config pomodoro]})"
|
||||
)
|
||||
_option_name = _p(
|
||||
"dash:stats|dropdown|placeholder",
|
||||
|
||||
@@ -140,7 +140,7 @@ class RankCog(LionCog):
|
||||
self.bot.core.guild_config.register_model_setting(self.settings.DMRanks)
|
||||
|
||||
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):
|
||||
lock = self._rank_locks.get(guildid, None)
|
||||
@@ -926,7 +926,6 @@ class RankCog(LionCog):
|
||||
dm_ranks=RankSettings.DMRanks._desc,
|
||||
rank_channel=RankSettings.RankChannel._desc,
|
||||
)
|
||||
@appcmds.default_permissions(administrator=True)
|
||||
@high_management_ward
|
||||
async def configure_ranks_cmd(self, ctx: LionContext,
|
||||
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 babel.translator import ctx_translator
|
||||
from wards import high_management_iward
|
||||
|
||||
from . import babel
|
||||
|
||||
@@ -40,7 +41,8 @@ class RankSettings(SettingGroup):
|
||||
|
||||
setting_id = '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")
|
||||
_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.
|
||||
"""
|
||||
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")
|
||||
_desc = _p(
|
||||
@@ -148,7 +151,8 @@ class RankSettings(SettingGroup):
|
||||
Whether to DM rank notifications.
|
||||
"""
|
||||
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")
|
||||
_desc = _p(
|
||||
|
||||
@@ -69,6 +69,7 @@ class RankConfigUI(ConfigUI):
|
||||
async def type_menu(self, selection: discord.Interaction, selected: Select):
|
||||
await selection.response.defer(thinking=True)
|
||||
setting = self.instances[0]
|
||||
await setting.interaction_check(setting.parent_id, selection)
|
||||
value = selected.values[0]
|
||||
data = RankType((value,))
|
||||
setting.data = data
|
||||
@@ -117,6 +118,7 @@ class RankConfigUI(ConfigUI):
|
||||
async def channel_menu(self, selection: discord.Interaction, selected: ChannelSelect):
|
||||
await selection.response.defer()
|
||||
setting = self.instances[2]
|
||||
await setting.interaction_check(setting.parent_id, selection)
|
||||
setting.value = selected.values[0] if selected.values else None
|
||||
await setting.write()
|
||||
|
||||
@@ -168,7 +170,7 @@ class RankConfigUI(ConfigUI):
|
||||
class RankDashboard(DashboardSection):
|
||||
section_name = _p(
|
||||
'dash:rank|title',
|
||||
"Rank Configuration ({commands[configure ranks]})",
|
||||
"Rank Configuration ({commands[admin config ranks]})",
|
||||
)
|
||||
_option_name = _p(
|
||||
"dash:rank|dropdown|placeholder",
|
||||
|
||||
@@ -430,7 +430,7 @@ class RankOverviewUI(MessageUI):
|
||||
"Ranks are determined by *all-time* statistics.\n"
|
||||
"To reward ranks from a later time (e.g. to have monthly/quarterly/yearly ranks) "
|
||||
"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:
|
||||
addendum = t(_p(
|
||||
'ui:rank_overview|embed|field:note|value|voice_addendum',
|
||||
|
||||
@@ -16,7 +16,7 @@ from utils.ui import Confirm
|
||||
from constants import MAX_COINS
|
||||
from core.data import CoreData
|
||||
|
||||
from wards import low_management_ward
|
||||
from wards import high_management_ward
|
||||
|
||||
from . import babel, logger
|
||||
from .data import RoomData
|
||||
@@ -47,7 +47,7 @@ class RoomCog(LionCog):
|
||||
self.bot.core.guild_config.register_model_setting(setting)
|
||||
|
||||
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():
|
||||
await self.initialise()
|
||||
@@ -414,7 +414,7 @@ class RoomCog(LionCog):
|
||||
t(_p(
|
||||
'cmd:room_rent|error:not_setup',
|
||||
"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
|
||||
)
|
||||
@@ -987,8 +987,7 @@ class RoomCog(LionCog):
|
||||
@appcmds.describe(
|
||||
**{setting.setting_id: setting._desc for setting in RoomSettings.model_settings}
|
||||
)
|
||||
@appcmds.default_permissions(manage_guild=True)
|
||||
@low_management_ward
|
||||
@high_management_ward
|
||||
async def configure_rooms_cmd(self, ctx: LionContext,
|
||||
rooms_category: Optional[discord.CategoryChannel] = 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 core.data import CoreData
|
||||
from babel.translator import ctx_translator
|
||||
from wards import low_management_iward, high_management_iward
|
||||
|
||||
from . import babel
|
||||
|
||||
@@ -15,7 +16,8 @@ class RoomSettings(SettingGroup):
|
||||
class Category(ModelData, ChannelSetting):
|
||||
setting_id = 'rooms_category'
|
||||
_event = 'guildset_rooms_category'
|
||||
_set_cmd = 'configure rooms'
|
||||
_set_cmd = 'admin config rooms'
|
||||
_write_ward = high_management_iward
|
||||
|
||||
_display_name = _p(
|
||||
'guildset:room_category', "rooms_category"
|
||||
@@ -70,7 +72,8 @@ class RoomSettings(SettingGroup):
|
||||
class Rent(ModelData, IntegerSetting):
|
||||
setting_id = 'rooms_price'
|
||||
_event = 'guildset_rooms_price'
|
||||
_set_cmd = 'configure rooms'
|
||||
_set_cmd = 'admin config rooms'
|
||||
_write_ward = low_management_iward
|
||||
|
||||
_display_name = _p(
|
||||
'guildset:rooms_price', "room_rent"
|
||||
@@ -107,7 +110,8 @@ class RoomSettings(SettingGroup):
|
||||
class MemberLimit(ModelData, IntegerSetting):
|
||||
setting_id = '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")
|
||||
_desc = _p(
|
||||
@@ -141,7 +145,8 @@ class RoomSettings(SettingGroup):
|
||||
class Visible(ModelData, BoolSetting):
|
||||
setting_id = '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")
|
||||
_desc = _p(
|
||||
|
||||
@@ -29,6 +29,7 @@ class RoomSettingUI(ConfigUI):
|
||||
async def category_menu(self, selection: discord.Interaction, selected: ChannelSelect):
|
||||
await selection.response.defer()
|
||||
setting = self.instances[0]
|
||||
await setting.interaction_check(setting.parent_id, selection)
|
||||
setting.value = selected.values[0] if selected.values else None
|
||||
await setting.write()
|
||||
|
||||
@@ -42,6 +43,7 @@ class RoomSettingUI(ConfigUI):
|
||||
async def visible_button(self, press: discord.Interaction, pressed: Button):
|
||||
await press.response.defer()
|
||||
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
|
||||
await setting.write()
|
||||
|
||||
@@ -95,7 +97,7 @@ class RoomSettingUI(ConfigUI):
|
||||
class RoomDashboard(DashboardSection):
|
||||
section_name = _p(
|
||||
'dash:rooms|title',
|
||||
"Private Room Configuration ({commands[configure rooms]})"
|
||||
"Private Room Configuration ({commands[admin config rooms]})"
|
||||
)
|
||||
_option_name = _p(
|
||||
"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.ui import Confirm
|
||||
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 data import NULL, ORDER
|
||||
from modules.economy.data import TransactionType
|
||||
@@ -118,7 +118,7 @@ class ScheduleCog(LionCog):
|
||||
await self.settings.SessionChannels.setup(self.bot)
|
||||
|
||||
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():
|
||||
await self.initialise()
|
||||
@@ -1090,7 +1090,7 @@ class ScheduleCog(LionCog):
|
||||
@appcmds.describe(
|
||||
**{param: option._desc for param, option in config_params.items()}
|
||||
)
|
||||
@low_management_ward
|
||||
@high_management_ward
|
||||
async def configure_schedule_command(self, ctx: LionContext,
|
||||
session_lobby: Optional[discord.TextChannel | discord.VoiceChannel] = None,
|
||||
session_room: Optional[discord.VoiceChannel] = None,
|
||||
|
||||
@@ -11,6 +11,7 @@ from meta import conf
|
||||
from meta.errors import UserInputError
|
||||
from meta.sharding import THIS_SHARD
|
||||
from meta.logger import log_wrap
|
||||
from wards import low_management_iward, high_management_iward
|
||||
|
||||
from babel.translator import ctx_translator
|
||||
|
||||
@@ -63,7 +64,8 @@ class ScheduleSettings(SettingGroup):
|
||||
class SessionLobby(ModelData, ChannelSetting):
|
||||
setting_id = '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")
|
||||
_desc = _p(
|
||||
@@ -119,7 +121,8 @@ class ScheduleSettings(SettingGroup):
|
||||
@ScheduleConfig.register_model_setting
|
||||
class SessionRoom(ModelData, ChannelSetting):
|
||||
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")
|
||||
_desc = _p(
|
||||
@@ -163,6 +166,7 @@ class ScheduleSettings(SettingGroup):
|
||||
|
||||
class SessionChannels(ListData, ChannelListSetting):
|
||||
setting_id = 'session_channels'
|
||||
_write_ward = high_management_iward
|
||||
|
||||
_display_name = _p('guildset:session_channels', "session_channels")
|
||||
_desc = _p(
|
||||
@@ -238,7 +242,8 @@ class ScheduleSettings(SettingGroup):
|
||||
@ScheduleConfig.register_model_setting
|
||||
class ScheduleCost(ModelData, CoinSetting):
|
||||
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")
|
||||
_desc = _p(
|
||||
@@ -283,7 +288,8 @@ class ScheduleSettings(SettingGroup):
|
||||
@ScheduleConfig.register_model_setting
|
||||
class AttendanceReward(ModelData, CoinSetting):
|
||||
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")
|
||||
_desc = _p(
|
||||
@@ -327,7 +333,8 @@ class ScheduleSettings(SettingGroup):
|
||||
@ScheduleConfig.register_model_setting
|
||||
class AttendanceBonus(ModelData, CoinSetting):
|
||||
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")
|
||||
_desc = _p(
|
||||
@@ -370,7 +377,8 @@ class ScheduleSettings(SettingGroup):
|
||||
@ScheduleConfig.register_model_setting
|
||||
class MinAttendance(ModelData, IntegerSetting):
|
||||
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")
|
||||
_desc = _p(
|
||||
@@ -437,8 +445,9 @@ class ScheduleSettings(SettingGroup):
|
||||
@ScheduleConfig.register_model_setting
|
||||
class BlacklistRole(ModelData, RoleSetting):
|
||||
setting_id = 'schedule_blacklist_role'
|
||||
_set_cmd = 'configure schedule'
|
||||
_set_cmd = 'admin config schedule'
|
||||
_event = 'guildset_schedule_blacklist_role'
|
||||
_write_ward = high_management_iward
|
||||
|
||||
_display_name = _p('guildset:schedule_blacklist_role', "schedule_blacklist_role")
|
||||
_desc = _p(
|
||||
@@ -495,7 +504,8 @@ class ScheduleSettings(SettingGroup):
|
||||
@ScheduleConfig.register_model_setting
|
||||
class BlacklistAfter(ModelData, IntegerSetting):
|
||||
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")
|
||||
_desc = _p(
|
||||
|
||||
@@ -78,6 +78,7 @@ class ScheduleSettingUI(ConfigUI):
|
||||
# TODO: Setting value checks
|
||||
await selection.response.defer()
|
||||
setting = self.get_instance(ScheduleSettings.SessionLobby)
|
||||
await setting.interaction_check(setting.parent_id, selection)
|
||||
setting.value = selected.values[0] if selected.values else None
|
||||
await setting.write()
|
||||
|
||||
@@ -95,6 +96,7 @@ class ScheduleSettingUI(ConfigUI):
|
||||
async def room_menu(self, selection: discord.Interaction, selected: ChannelSelect):
|
||||
await selection.response.defer()
|
||||
setting = self.get_instance(ScheduleSettings.SessionRoom)
|
||||
await setting.interaction_check(setting.parent_id, selection)
|
||||
setting.value = selected.values[0] if selected.values else None
|
||||
await setting.write()
|
||||
|
||||
@@ -113,6 +115,7 @@ class ScheduleSettingUI(ConfigUI):
|
||||
# TODO: Consider XORing input
|
||||
await selection.response.defer()
|
||||
setting = self.get_instance(ScheduleSettings.SessionChannels)
|
||||
await setting.interaction_check(setting.parent_id, selection)
|
||||
setting.value = selected.values
|
||||
await setting.write()
|
||||
|
||||
@@ -158,6 +161,7 @@ class ScheduleSettingUI(ConfigUI):
|
||||
async def blacklist_role_menu(self, selection: discord.Interaction, selected: RoleSelect):
|
||||
await selection.response.defer()
|
||||
setting = self.get_instance(ScheduleSettings.BlacklistRole)
|
||||
await setting.interaction_check(setting.parent_id, selection)
|
||||
setting.value = selected.values[0] if selected.values else None
|
||||
# TODO: Warning for insufficient permissions?
|
||||
await setting.write()
|
||||
@@ -227,7 +231,7 @@ class ScheduleSettingUI(ConfigUI):
|
||||
class ScheduleDashboard(DashboardSection):
|
||||
section_name = _p(
|
||||
'dash:schedule|title',
|
||||
"Scheduled Session Configuration ({commands[configure schedule]})"
|
||||
"Scheduled Session Configuration ({commands[admin config schedule]})"
|
||||
)
|
||||
_option_name = _p(
|
||||
"dash:schedule|dropdown|placeholder",
|
||||
@@ -248,7 +252,7 @@ class ScheduleDashboard(DashboardSection):
|
||||
page.add_field(
|
||||
name=t(_p(
|
||||
'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),
|
||||
value=table,
|
||||
inline=False
|
||||
@@ -258,7 +262,7 @@ class ScheduleDashboard(DashboardSection):
|
||||
page.add_field(
|
||||
name=t(_p(
|
||||
'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),
|
||||
value=table,
|
||||
inline=False
|
||||
@@ -268,7 +272,7 @@ class ScheduleDashboard(DashboardSection):
|
||||
page.add_field(
|
||||
name=t(_p(
|
||||
'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),
|
||||
value=table,
|
||||
inline=False
|
||||
|
||||
@@ -12,7 +12,7 @@ from core.lion_guild import VoiceMode
|
||||
from utils.lib import error_embed
|
||||
from utils.ui import LeoUI, AButton, utc_now
|
||||
from gui.base import CardMode
|
||||
from wards import low_management_ward
|
||||
from wards import high_management_ward
|
||||
|
||||
from . import babel
|
||||
from .data import StatsData
|
||||
@@ -41,7 +41,7 @@ class StatsCog(LionCog):
|
||||
self.bot.core.guild_config.register_setting(self.settings.UnrankedRoles)
|
||||
|
||||
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(
|
||||
name=_p('cmd:me', "me"),
|
||||
@@ -204,8 +204,7 @@ class StatsCog(LionCog):
|
||||
"Time from which to start counting activity for rank badges and season leaderboards. (YYYY-MM-DD)"
|
||||
)
|
||||
)
|
||||
@appcmds.default_permissions(manage_guild=True)
|
||||
@low_management_ward
|
||||
@high_management_ward
|
||||
async def configure_statistics_cmd(self, ctx: LionContext,
|
||||
season_start: Optional[str] = None):
|
||||
t = self.bot.translator.t
|
||||
|
||||
@@ -21,6 +21,7 @@ from utils.lib import MessageArgs
|
||||
from core.data import CoreData
|
||||
from core.lion_guild import VoiceMode
|
||||
from babel.translator import ctx_translator
|
||||
from wards import low_management_iward, high_management_iward
|
||||
|
||||
from . import babel
|
||||
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)
|
||||
"""
|
||||
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")
|
||||
_desc = _p(
|
||||
@@ -155,6 +157,7 @@ class StatisticsSettings(SettingGroup):
|
||||
List of roles not displayed on the leaderboard
|
||||
"""
|
||||
setting_id = 'unranked_roles'
|
||||
_write_ward = high_management_iward
|
||||
|
||||
_display_name = _p('guildset:unranked_roles', "unranked_roles")
|
||||
_desc = _p(
|
||||
@@ -211,6 +214,7 @@ class StatisticsSettings(SettingGroup):
|
||||
Default is determined by current guild mode
|
||||
"""
|
||||
setting_id = 'visible_stats'
|
||||
_write_ward = high_management_iward
|
||||
|
||||
_setting = StatTypeSetting
|
||||
|
||||
@@ -263,6 +267,7 @@ class StatisticsSettings(SettingGroup):
|
||||
Which of the three stats to display by default
|
||||
"""
|
||||
setting_id = 'default_stat'
|
||||
_write_ward = high_management_iward
|
||||
|
||||
_display_name = _p('guildset:default_stat', "default_stat")
|
||||
_desc = _p(
|
||||
@@ -294,6 +299,7 @@ class StatisticsConfigUI(ConfigUI):
|
||||
"""
|
||||
await selection.response.defer(thinking=True)
|
||||
setting = self.instances[1]
|
||||
await setting.interaction_check(setting.parent_id, selection)
|
||||
setting.value = selected.values
|
||||
await setting.write()
|
||||
# Don't need to refresh due to instance hooks
|
||||
@@ -314,6 +320,7 @@ class StatisticsConfigUI(ConfigUI):
|
||||
"""
|
||||
await selection.response.defer(thinking=True)
|
||||
setting = self.instances[2]
|
||||
await setting.interaction_check(setting.parent_id, selection)
|
||||
data = [StatisticType((value,)) for value in selected.values]
|
||||
setting.data = data
|
||||
await setting.write()
|
||||
@@ -405,7 +412,7 @@ class StatisticsConfigUI(ConfigUI):
|
||||
class StatisticsDashboard(DashboardSection):
|
||||
section_name = _p(
|
||||
'dash:stats|title',
|
||||
"Activity Statistics Configuration ({commands[configure statistics]})"
|
||||
"Activity Statistics Configuration ({commands[admin config statistics]})"
|
||||
)
|
||||
_option_name = _p(
|
||||
"dash:stats|dropdown|placeholder",
|
||||
|
||||
@@ -139,7 +139,7 @@ class TasklistCog(LionCog):
|
||||
self.bot.add_view(TasklistCaller(self.bot))
|
||||
|
||||
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')
|
||||
@log_wrap(action="reward tasks completed")
|
||||
@@ -984,7 +984,6 @@ class TasklistCog(LionCog):
|
||||
reward=TasklistSettings.task_reward._desc,
|
||||
reward_limit=TasklistSettings.task_reward_limit._desc
|
||||
)
|
||||
@appcmds.default_permissions(manage_guild=True)
|
||||
@low_management_ward
|
||||
async def configure_tasklist_cmd(self, ctx: LionContext,
|
||||
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 core.data import CoreData
|
||||
from babel.translator import ctx_translator
|
||||
from wards import low_management_iward, high_management_iward
|
||||
|
||||
from . import babel
|
||||
from .data import TasklistData
|
||||
@@ -28,7 +29,8 @@ class TasklistSettings(SettingGroup):
|
||||
Exposed via `/configure tasklist`, and the standard configuration interface.
|
||||
"""
|
||||
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")
|
||||
_desc = _p(
|
||||
@@ -68,7 +70,8 @@ class TasklistSettings(SettingGroup):
|
||||
|
||||
class task_reward_limit(ModelData, IntegerSetting):
|
||||
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")
|
||||
_desc = _p(
|
||||
@@ -109,6 +112,7 @@ class TasklistSettings(SettingGroup):
|
||||
|
||||
class tasklist_channels(ListData, ChannelListSetting):
|
||||
setting_id = 'tasklist_channels'
|
||||
_write_ward = low_management_iward
|
||||
|
||||
_display_name = _p('guildset:tasklist_channels', "tasklist_channels")
|
||||
_desc = _p(
|
||||
@@ -317,7 +321,7 @@ class TasklistConfigUI(LeoUI):
|
||||
|
||||
|
||||
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(
|
||||
"dash:tasklist|dropdown|placeholder",
|
||||
"Tasklist Options Panel"
|
||||
|
||||
@@ -57,7 +57,7 @@ class VideoCog(LionCog):
|
||||
"Could not load ConfigCog. VideoCog configuration will not crossload."
|
||||
)
|
||||
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():
|
||||
await self.initialise()
|
||||
@@ -522,7 +522,7 @@ class VideoCog(LionCog):
|
||||
video_blacklist_durations=VideoSettings.VideoBlacklistDurations._desc,
|
||||
video_grace_period=VideoSettings.VideoGracePeriod._desc,
|
||||
)
|
||||
@low_management_ward
|
||||
@high_management_ward
|
||||
async def configure_video(self, ctx: LionContext,
|
||||
video_blacklist: Optional[discord.Role] = None,
|
||||
video_blacklist_durations: Optional[str] = None,
|
||||
@@ -572,4 +572,3 @@ class VideoCog(LionCog):
|
||||
ui = VideoSettingUI(self.bot, ctx.guild.id, ctx.channel.id)
|
||||
await ui.run(ctx.interaction)
|
||||
await ui.wait()
|
||||
|
||||
|
||||
@@ -14,6 +14,7 @@ from meta.sharding import THIS_SHARD
|
||||
from meta.logger import log_wrap
|
||||
from core.data import CoreData
|
||||
from babel.translator import ctx_translator
|
||||
from wards import low_management_iward, high_management_iward
|
||||
|
||||
from . import babel, logger
|
||||
from .data import VideoData
|
||||
@@ -25,6 +26,7 @@ class VideoSettings(SettingGroup):
|
||||
class VideoChannels(ListData, ChannelListSetting):
|
||||
setting_id = "video_channels"
|
||||
_event = 'guildset_video_channels'
|
||||
_write_ward = high_management_iward
|
||||
|
||||
_display_name = _p('guildset:video_channels', "video_channels")
|
||||
_desc = _p(
|
||||
@@ -101,6 +103,7 @@ class VideoSettings(SettingGroup):
|
||||
class VideoBlacklist(ModelData, RoleSetting):
|
||||
setting_id = "video_blacklist"
|
||||
_event = 'guildset_video_blacklist'
|
||||
_write_ward = high_management_iward
|
||||
|
||||
_display_name = _p('guildset:video_blacklist', "video_blacklist")
|
||||
_desc = _p(
|
||||
@@ -158,6 +161,7 @@ class VideoSettings(SettingGroup):
|
||||
class VideoBlacklistDurations(ListData, ListSetting, InteractiveSetting):
|
||||
setting_id = 'video_durations'
|
||||
_setting = DurationSetting
|
||||
_write_ward = high_management_iward
|
||||
|
||||
_display_name = _p('guildset:video_durations', "video_blacklist_durations")
|
||||
_desc = _p(
|
||||
@@ -217,6 +221,7 @@ class VideoSettings(SettingGroup):
|
||||
class VideoGracePeriod(ModelData, DurationSetting):
|
||||
setting_id = "video_grace_period"
|
||||
_event = 'guildset_video_grace_period'
|
||||
_write_ward = high_management_iward
|
||||
|
||||
_display_name = _p('guildset:video_grace_period', "video_grace_period")
|
||||
_desc = _p(
|
||||
@@ -252,6 +257,7 @@ class VideoSettings(SettingGroup):
|
||||
class VideoExempt(ListData, RoleListSetting):
|
||||
setting_id = "video_exempt"
|
||||
_event = 'guildset_video_exempt'
|
||||
_write_ward = high_management_iward
|
||||
|
||||
_display_name = _p('guildset:video_exempt', "video_exempt")
|
||||
_desc = _p(
|
||||
|
||||
@@ -45,6 +45,7 @@ class VideoSettingUI(ConfigUI):
|
||||
await selection.response.defer(thinking=True, ephemeral=True)
|
||||
|
||||
setting = self.get_instance(VideoSettings.VideoChannels)
|
||||
await setting.interaction_check(setting.parent_id, selection)
|
||||
setting.value = selected.values
|
||||
await setting.write()
|
||||
await selection.delete_original_response()
|
||||
@@ -70,6 +71,7 @@ class VideoSettingUI(ConfigUI):
|
||||
await selection.response.defer(thinking=True, ephemeral=True)
|
||||
|
||||
setting = self.get_instance(VideoSettings.VideoExempt)
|
||||
await setting.interaction_check(setting.parent_id, selection)
|
||||
setting.value = selected.values
|
||||
await setting.write()
|
||||
await selection.delete_original_response()
|
||||
@@ -95,6 +97,7 @@ class VideoSettingUI(ConfigUI):
|
||||
await selection.response.defer(thinking=True, ephemeral=True)
|
||||
|
||||
setting = self.get_instance(VideoSettings.VideoBlacklist)
|
||||
await setting.interaction_check(setting.parent_id, selection)
|
||||
setting.value = selected.values[0] if selected.values else None
|
||||
if setting.value:
|
||||
await equippable_role(self.bot, setting.value, selection.user)
|
||||
@@ -153,7 +156,7 @@ class VideoSettingUI(ConfigUI):
|
||||
class VideoDashboard(DashboardSection):
|
||||
section_name = _p(
|
||||
"dash:video|title",
|
||||
"Video Channel Settings ({commands[configure video_channels]})"
|
||||
"Video Channel Settings ({commands[admin config video_channels]})"
|
||||
)
|
||||
_option_name = _p(
|
||||
"dash:video|option|name",
|
||||
|
||||
@@ -7,6 +7,7 @@ from discord import ui
|
||||
from discord.ui.button import ButtonStyle, Button, button
|
||||
from discord.ui.modal import Modal
|
||||
from discord.ui.text_input import TextInput
|
||||
from meta.errors import UserInputError
|
||||
|
||||
from utils.lib import tabulate, recover_context
|
||||
from utils.ui import FastModal
|
||||
@@ -192,6 +193,9 @@ class InteractiveSetting(BaseSetting[ParentID, SettingData, SettingValue]):
|
||||
# Event handlers should be of the form Callable[ParentID, SettingData]
|
||||
_event: Optional[str] = None
|
||||
|
||||
# Interaction ward that should be validated via interaction_check
|
||||
_write_ward: Optional[Callable[[discord.Interaction], Coroutine[Any, Any, bool]]] = None
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
@@ -488,6 +492,16 @@ class InteractiveSetting(BaseSetting[ParentID, SettingData, SettingValue]):
|
||||
"""
|
||||
pass
|
||||
|
||||
@classmethod
|
||||
async def interaction_check(cls, parent_id, interaction: discord.Interaction, **kwargs):
|
||||
if cls._write_ward is not None and not await cls._write_ward(interaction):
|
||||
# TODO: Combine the check system so we can do customised errors here
|
||||
t = ctx_translator.get().t
|
||||
raise UserInputError(t(_p(
|
||||
'setting|interaction_check|error',
|
||||
"You do not have sufficient permissions to do this!"
|
||||
)))
|
||||
|
||||
|
||||
"""
|
||||
command callback for set command?
|
||||
|
||||
@@ -16,7 +16,7 @@ from meta.app import appname
|
||||
from meta.monitor import ComponentMonitor, ComponentStatus, StatusLevel
|
||||
from utils.lib import utc_now, error_embed
|
||||
|
||||
from wards import low_management_ward, sys_admin_ward
|
||||
from wards import low_management_ward, sys_admin_ward, low_management_iward
|
||||
from . import babel, logger
|
||||
from .data import TextTrackerData
|
||||
|
||||
@@ -116,7 +116,7 @@ class TextTrackerCog(LionCog):
|
||||
"Attempting to load the TextTrackerCog before ConfigCog! Failed to crossload configuration group."
|
||||
)
|
||||
else:
|
||||
self.crossload_group(self.configure_group, configcog.configure_group)
|
||||
self.crossload_group(self.configure_group, configcog.config_group)
|
||||
|
||||
if self.bot.is_ready():
|
||||
await self.initialise()
|
||||
@@ -318,7 +318,6 @@ class TextTrackerCog(LionCog):
|
||||
xp_per_period=TextTrackerSettings.XPPerPeriod._desc,
|
||||
word_xp=TextTrackerSettings.WordXP._desc,
|
||||
)
|
||||
@appcmds.default_permissions(manage_guild=True)
|
||||
@low_management_ward
|
||||
async def configure_text_tracking_cmd(self, ctx: LionContext,
|
||||
xp_per_period: Optional[appcmds.Range[int, 0, 2**15]] = None,
|
||||
|
||||
@@ -11,6 +11,7 @@ from meta.sharding import THIS_SHARD
|
||||
from meta.logger import log_wrap
|
||||
from core.data import CoreData
|
||||
from babel.translator import ctx_translator
|
||||
from wards import low_management_iward
|
||||
|
||||
from . import babel, logger
|
||||
from .data import TextTrackerData
|
||||
@@ -28,7 +29,8 @@ class TextTrackerSettings(SettingGroup):
|
||||
"""
|
||||
class XPPerPeriod(ModelData, IntegerSetting):
|
||||
setting_id = 'xp_per_period'
|
||||
_set_cmd = 'configure message_exp'
|
||||
_set_cmd = 'config message_exp'
|
||||
_write_ward = low_management_iward
|
||||
|
||||
_display_name = _p('guildset:xp_per_period', "xp_per_5min")
|
||||
_desc = _p(
|
||||
@@ -60,7 +62,8 @@ class TextTrackerSettings(SettingGroup):
|
||||
|
||||
class WordXP(ModelData, IntegerSetting):
|
||||
setting_id = 'word_xp'
|
||||
_set_cmd = 'configure message_exp'
|
||||
_set_cmd = 'config message_exp'
|
||||
_write_ward = low_management_iward
|
||||
|
||||
_display_name = _p('guildset:word_xp', "xp_per_100words")
|
||||
_desc = _p(
|
||||
@@ -91,6 +94,7 @@ class TextTrackerSettings(SettingGroup):
|
||||
|
||||
class UntrackedTextChannels(ListData, ChannelListSetting):
|
||||
setting_id = 'untracked_text_channels'
|
||||
_write_ward = low_management_iward
|
||||
|
||||
_display_name = _p('guildset:untracked_text_channels', "untracked_text_channels")
|
||||
_desc = _p(
|
||||
|
||||
@@ -35,6 +35,7 @@ class TextTrackerConfigUI(ConfigUI):
|
||||
async def untracked_channels_menu(self, selection: discord.Interaction, selected):
|
||||
await selection.response.defer()
|
||||
setting = self.instances[2]
|
||||
await setting.interaction_check(setting.parent_id, selection)
|
||||
setting.value = selected.values
|
||||
await setting.write()
|
||||
|
||||
@@ -86,7 +87,7 @@ class TextTrackerConfigUI(ConfigUI):
|
||||
class TextTrackerDashboard(DashboardSection):
|
||||
section_name = _p(
|
||||
'dash:text_tracking|title',
|
||||
"Message XP configuration ({commands[configure message_exp]})",
|
||||
"Message XP configuration ({commands[config message_exp]})",
|
||||
)
|
||||
_option_name = _p(
|
||||
"dash:text_tracking|dropdown|placeholder",
|
||||
|
||||
@@ -133,7 +133,7 @@ class VoiceTrackerCog(LionCog):
|
||||
"Attempting to load VoiceTrackerCog before ConfigCog! Cannot crossload configuration group."
|
||||
)
|
||||
else:
|
||||
self.crossload_group(self.configure_group, configcog.configure_group)
|
||||
self.crossload_group(self.configure_group, configcog.config_group)
|
||||
|
||||
if self.bot.is_ready():
|
||||
await self.initialise()
|
||||
@@ -867,7 +867,6 @@ class VoiceTrackerCog(LionCog):
|
||||
hourly_live_bonus=VoiceTrackerSettings.HourlyLiveBonus._desc,
|
||||
daily_voice_cap=VoiceTrackerSettings.DailyVoiceCap._desc,
|
||||
)
|
||||
@appcmds.default_permissions(manage_guild=True)
|
||||
@low_management_ward
|
||||
async def configure_voice_tracking_cmd(self, ctx: LionContext,
|
||||
hourly_reward: Optional[int] = None, # TODO: Change these to Ranges
|
||||
|
||||
@@ -14,6 +14,7 @@ from meta.sharding import THIS_SHARD
|
||||
from meta.logger import log_wrap
|
||||
from utils.lib import MessageArgs
|
||||
from utils.ui import LeoUI, ConfigUI, DashboardSection
|
||||
from wards import low_management_iward
|
||||
|
||||
from core.data import CoreData
|
||||
from core.lion_guild import VoiceMode
|
||||
@@ -35,7 +36,8 @@ class VoiceTrackerSettings(SettingGroup):
|
||||
class UntrackedChannels(ListData, ChannelListSetting):
|
||||
setting_id = 'untracked_channels'
|
||||
_event = 'guildset_untracked_channels'
|
||||
_set_cmd = 'configure voice_rewards'
|
||||
_set_cmd = 'config voice_rewards'
|
||||
_write_ward = low_management_iward
|
||||
|
||||
_display_name = _p('guildset:untracked_channels', "untracked_channels")
|
||||
_desc = _p(
|
||||
@@ -112,7 +114,8 @@ class VoiceTrackerSettings(SettingGroup):
|
||||
class HourlyReward(ModelData, IntegerSetting):
|
||||
setting_id = 'hourly_reward'
|
||||
_event = 'on_guildset_hourly_reward'
|
||||
_set_cmd = 'configure voice_rewards'
|
||||
_set_cmd = 'config voice_rewards'
|
||||
_write_ward = low_management_iward
|
||||
|
||||
_display_name = _p('guildset:hourly_reward', "hourly_reward")
|
||||
_desc = _p(
|
||||
@@ -192,7 +195,8 @@ class VoiceTrackerSettings(SettingGroup):
|
||||
"""
|
||||
setting_id = 'hourly_live_bonus'
|
||||
_event = 'on_guildset_hourly_live_bonus'
|
||||
_set_cmd = 'configure voice_rewards'
|
||||
_set_cmd = 'config voice_rewards'
|
||||
_write_ward = low_management_iward
|
||||
|
||||
_display_name = _p('guildset:hourly_live_bonus', "hourly_live_bonus")
|
||||
_desc = _p(
|
||||
@@ -243,7 +247,8 @@ class VoiceTrackerSettings(SettingGroup):
|
||||
class DailyVoiceCap(ModelData, DurationSetting):
|
||||
setting_id = 'daily_voice_cap'
|
||||
_event = 'on_guildset_daily_voice_cap'
|
||||
_set_cmd = 'configure voice_rewards'
|
||||
_set_cmd = 'config voice_rewards'
|
||||
_write_ward = low_management_iward
|
||||
|
||||
_display_name = _p('guildset:daily_voice_cap', "daily_voice_cap")
|
||||
_desc = _p(
|
||||
@@ -465,6 +470,7 @@ class VoiceTrackerConfigUI(ConfigUI):
|
||||
async def untracked_channels_menu(self, selection: discord.Interaction, selected):
|
||||
await selection.response.defer()
|
||||
setting = self.instances[3]
|
||||
await setting.interaction_check(setting.parent_id, selection)
|
||||
setting.value = selected.values
|
||||
await setting.write()
|
||||
|
||||
@@ -528,7 +534,7 @@ class VoiceTrackerConfigUI(ConfigUI):
|
||||
class VoiceTrackerDashboard(DashboardSection):
|
||||
section_name = _p(
|
||||
'dash:voice_tracker|title',
|
||||
"Voice Tracker Configuration ({commands[configure voice_rewards]})"
|
||||
"Voice Tracker Configuration ({commands[config voice_rewards]})"
|
||||
)
|
||||
_option_name = _p(
|
||||
"dash:voice_tracking|dropdown|placeholder",
|
||||
|
||||
@@ -126,6 +126,7 @@ class ConfigUI(LeoUI):
|
||||
new_data = None
|
||||
else:
|
||||
# If this raises a UserInputError, it will be caught and the modal retried
|
||||
await setting.interaction_check(setting.parent_id, interaction)
|
||||
new_data = await setting._parse_string(setting.parent_id, input_value)
|
||||
setting.data = new_data
|
||||
modified.append(setting)
|
||||
|
||||
Reference in New Issue
Block a user