From ad455e5ac3c91b3739a0c33df43d14fb441deb01 Mon Sep 17 00:00:00 2001 From: Conatum Date: Fri, 20 Oct 2023 19:52:22 +0300 Subject: [PATCH] feat: Implement Sponsors. --- data/migration/v13-v14/migration.sql | 7 ++ data/schema.sql | 15 +--- src/constants.py | 2 +- src/core/data.py | 4 + src/modules/__init__.py | 1 + src/modules/sponsors/__init__.py | 10 +++ src/modules/sponsors/cog.py | 126 +++++++++++++++++++++++++++ src/modules/sponsors/data.py | 5 ++ src/modules/sponsors/settings.py | 87 ++++++++++++++++++ src/modules/sponsors/settingui.py | 122 ++++++++++++++++++++++++++ src/modules/statistics/cog.py | 13 +++ src/settings/setting_types.py | 2 +- 12 files changed, 380 insertions(+), 14 deletions(-) create mode 100644 data/migration/v13-v14/migration.sql create mode 100644 src/modules/sponsors/__init__.py create mode 100644 src/modules/sponsors/cog.py create mode 100644 src/modules/sponsors/data.py create mode 100644 src/modules/sponsors/settings.py create mode 100644 src/modules/sponsors/settingui.py diff --git a/data/migration/v13-v14/migration.sql b/data/migration/v13-v14/migration.sql new file mode 100644 index 00000000..3882faac --- /dev/null +++ b/data/migration/v13-v14/migration.sql @@ -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; diff --git a/data/schema.sql b/data/schema.sql index 26a0c607..279550dd 100644 --- a/data/schema.sql +++ b/data/schema.sql @@ -4,7 +4,7 @@ CREATE TABLE VersionHistory( time TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP NOT NULL, 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() @@ -17,17 +17,6 @@ $$ language 'plpgsql'; -- }}} -- 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( userid BIGINT PRIMARY KEY, @@ -50,6 +39,8 @@ CREATE TABLE app_config( CREATE TABLE bot_config( appname TEXT PRIMARY KEY REFERENCES app_config(appname) ON DELETE CASCADE, + sponsor_prompt TEXT, + sponsor_message TEXT, default_skin TEXT ); diff --git a/src/constants.py b/src/constants.py index 87b445d1..cb4c4d77 100644 --- a/src/constants.py +++ b/src/constants.py @@ -1,5 +1,5 @@ CONFIG_FILE = "config/bot.conf" -DATA_VERSION = 13 +DATA_VERSION = 14 MAX_COINS = 2147483647 - 1 diff --git a/src/core/data.py b/src/core/data.py index 177fccd6..47fed694 100644 --- a/src/core/data.py +++ b/src/core/data.py @@ -47,6 +47,8 @@ class CoreData(Registry, name="core"): ------ CREATE TABLE bot_config( appname TEXT PRIMARY KEY REFERENCES app_config(appname) ON DELETE CASCADE, + sponsor_prompt TEXT, + sponsor_message TEXT, default_skin TEXT ); """ @@ -54,6 +56,8 @@ class CoreData(Registry, name="core"): appname = String(primary=True) default_skin = String() + sponsor_prompt = String() + sponsor_message = String() class Shard(RowModel): """ diff --git a/src/modules/__init__.py b/src/modules/__init__.py index 0f34b603..d977087c 100644 --- a/src/modules/__init__.py +++ b/src/modules/__init__.py @@ -18,6 +18,7 @@ active = [ '.moderation', '.video_channels', '.meta', + '.sponsors', '.test', ] diff --git a/src/modules/sponsors/__init__.py b/src/modules/sponsors/__init__.py new file mode 100644 index 00000000..78988da2 --- /dev/null +++ b/src/modules/sponsors/__init__.py @@ -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)) diff --git a/src/modules/sponsors/cog.py b/src/modules/sponsors/cog.py new file mode 100644 index 00000000..8362dc36 --- /dev/null +++ b/src/modules/sponsors/cog.py @@ -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() diff --git a/src/modules/sponsors/data.py b/src/modules/sponsors/data.py new file mode 100644 index 00000000..5fa9b683 --- /dev/null +++ b/src/modules/sponsors/data.py @@ -0,0 +1,5 @@ +from data import Registry, Table + + +class SponsorData(Registry): + sponsor_whitelist = Table('sponsor_guild_whitelist') diff --git a/src/modules/sponsors/settings.py b/src/modules/sponsors/settings.py new file mode 100644 index 00000000..3b57b77f --- /dev/null +++ b/src/modules/sponsors/settings.py @@ -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() diff --git a/src/modules/sponsors/settingui.py b/src/modules/sponsors/settingui.py new file mode 100644 index 00000000..738c64ca --- /dev/null +++ b/src/modules/sponsors/settingui.py @@ -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) + ) diff --git a/src/modules/statistics/cog.py b/src/modules/statistics/cog.py index 07c6d469..c40c7872 100644 --- a/src/modules/statistics/cog.py +++ b/src/modules/statistics/cog.py @@ -55,6 +55,8 @@ class StatsCog(LionCog): await ctx.interaction.response.defer(thinking=True) ui = ProfileUI(self.bot, ctx.author, ctx.guild) await ui.run(ctx.interaction) + if sponsors := self.bot.get_cog('SponsorCog'): + await sponsors.do_sponsor_prompt(ctx.interaction) await ui.wait() @cmds.hybrid_command( @@ -101,6 +103,9 @@ class StatsCog(LionCog): file = discord.File(profile_data, 'profile.png') await ctx.reply(file=file) + if sponsors := self.bot.get_cog('SponsorCog'): + await sponsors.do_sponsor_prompt(ctx.interaction) + @cmds.hybrid_command( name=_p('cmd:stats', "stats"), description=_p( @@ -116,6 +121,10 @@ class StatsCog(LionCog): await ctx.interaction.response.defer(thinking=True) ui = WeeklyMonthlyUI(self.bot, ctx.author, ctx.guild) await ui.run(ctx.interaction) + + if sponsors := self.bot.get_cog('SponsorCog'): + await sponsors.do_sponsor_prompt(ctx.interaction) + await ui.wait() @cmds.hybrid_command( @@ -151,6 +160,10 @@ class StatsCog(LionCog): await ctx.interaction.response.defer(thinking=True) ui = LeaderboardUI(self.bot, ctx.author, ctx.guild) await ui.run(ctx.interaction) + + if sponsors := self.bot.get_cog('SponsorCog'): + await sponsors.do_sponsor_prompt(ctx.interaction) + await ui.wait() @cmds.hybrid_command( diff --git a/src/settings/setting_types.py b/src/settings/setting_types.py index 71239cfc..4cf41776 100644 --- a/src/settings/setting_types.py +++ b/src/settings/setting_types.py @@ -1381,7 +1381,7 @@ class StringListSetting(InteractiveSetting, ListSetting): _setting = StringSetting -class GuildIDListSetting(InteractiveSetting, ListSetting): +class GuildIDListSetting(ListSetting, InteractiveSetting): """ List of guildids. """