diff --git a/bot/constants.py b/bot/constants.py index 297c49a9..7038953b 100644 --- a/bot/constants.py +++ b/bot/constants.py @@ -1,2 +1,2 @@ CONFIG_FILE = "config/bot.conf" -DATA_VERSION = 10 +DATA_VERSION = 11 diff --git a/bot/core/__init__.py b/bot/core/__init__.py index 9be4f2bd..be157adb 100644 --- a/bot/core/__init__.py +++ b/bot/core/__init__.py @@ -1,5 +1,7 @@ from . import data # noqa +from . import patches + from .module import module from .lion import Lion from . import blacklists diff --git a/bot/core/data.py b/bot/core/data.py index 58c1331b..71e45b52 100644 --- a/bot/core/data.py +++ b/bot/core/data.py @@ -11,6 +11,9 @@ meta = RowTable( attach_as='meta', ) +# TODO: Consider converting to RowTable for per-shard config caching +app_config = Table('AppConfig') + user_config = RowTable( 'user_config', diff --git a/bot/core/patches.py b/bot/core/patches.py new file mode 100644 index 00000000..a292c443 --- /dev/null +++ b/bot/core/patches.py @@ -0,0 +1,111 @@ +""" +Temporary patches for the discord.py library to support new features of the discord API. +""" +from discord.http import Route, HTTPClient +from discord.abc import Messageable +from discord.utils import InvalidArgument +from discord import File, AllowedMentions + + +def send_message(self, channel_id, content, *, tts=False, embeds=None, + nonce=None, allowed_mentions=None, message_reference=None): + r = Route('POST', '/channels/{channel_id}/messages', channel_id=channel_id) + payload = {} + + if content: + payload['content'] = content + + if tts: + payload['tts'] = True + + if embeds: + payload['embeds'] = embeds + + if nonce: + payload['nonce'] = nonce + + if allowed_mentions: + payload['allowed_mentions'] = allowed_mentions + + if message_reference: + payload['message_reference'] = message_reference + + return self.request(r, json=payload) + + +HTTPClient.send_message = send_message + + +async def send(self, content=None, *, tts=False, embed=None, embeds=None, file=None, + files=None, delete_after=None, nonce=None, + allowed_mentions=None, reference=None, + mention_author=None): + + channel = await self._get_channel() + state = self._state + content = str(content) if content is not None else None + if embed is not None: + if embeds is not None: + embeds.append(embed) + else: + embeds = [embed] + embed = embed.to_dict() + if embeds is not None: + embeds = [embed.to_dict() for embed in embeds] + + if allowed_mentions is not None: + if state.allowed_mentions is not None: + allowed_mentions = state.allowed_mentions.merge(allowed_mentions).to_dict() + else: + allowed_mentions = allowed_mentions.to_dict() + else: + allowed_mentions = state.allowed_mentions and state.allowed_mentions.to_dict() + + if mention_author is not None: + allowed_mentions = allowed_mentions or AllowedMentions().to_dict() + allowed_mentions['replied_user'] = bool(mention_author) + + if reference is not None: + try: + reference = reference.to_message_reference_dict() + except AttributeError: + raise InvalidArgument('reference parameter must be Message or MessageReference') from None + + if file is not None and files is not None: + raise InvalidArgument('cannot pass both file and files parameter to send()') + + if file is not None: + if not isinstance(file, File): + raise InvalidArgument('file parameter must be File') + + try: + data = await state.http.send_files(channel.id, files=[file], allowed_mentions=allowed_mentions, + content=content, tts=tts, embed=embed, nonce=nonce, + message_reference=reference) + finally: + file.close() + + elif files is not None: + if len(files) > 10: + raise InvalidArgument('files parameter must be a list of up to 10 elements') + elif not all(isinstance(file, File) for file in files): + raise InvalidArgument('files parameter must be a list of File') + + try: + data = await state.http.send_files(channel.id, files=files, content=content, tts=tts, + embed=embed, nonce=nonce, allowed_mentions=allowed_mentions, + message_reference=reference) + finally: + for f in files: + f.close() + else: + data = await state.http.send_message(channel.id, content, tts=tts, embeds=embeds, + nonce=nonce, allowed_mentions=allowed_mentions, + message_reference=reference) + + ret = state.create_message(channel=channel, data=data) + if delete_after is not None: + await ret.delete(delay=delete_after) + return ret + +Messageable.send = send diff --git a/bot/main.py b/bot/main.py index 066bf86e..1f401733 100644 --- a/bot/main.py +++ b/bot/main.py @@ -4,6 +4,9 @@ from data import tables import core # noqa +# Note: This MUST be imported after core, due to table definition orders +from settings import AppSettings + import modules # noqa # Load and attach app specific data @@ -15,6 +18,8 @@ client.appdata = core.data.meta.fetch_or_create(appname) client.data = tables +client.settings = AppSettings(conf.bot['data_appid']) + # Initialise all modules client.initialise_modules() diff --git a/bot/modules/__init__.py b/bot/modules/__init__.py index 20630a1b..ee4a7bb4 100644 --- a/bot/modules/__init__.py +++ b/bot/modules/__init__.py @@ -13,3 +13,4 @@ from .renting import * from .moderation import * from .accountability import * from .plugins import * +from .sponsors import * diff --git a/bot/modules/meta/help.py b/bot/modules/meta/help.py index 1329fc02..719a87df 100644 --- a/bot/modules/meta/help.py +++ b/bot/modules/meta/help.py @@ -10,7 +10,7 @@ from .lib import guide_link new_emoji = " 🆕" -new_commands = {'achievements', 'nerd', 'invite', 'support'} +new_commands = {'botconfig', 'sponsors'} # Set the command groups to appear in the help group_hints = { diff --git a/bot/modules/sponsors/__init__.py b/bot/modules/sponsors/__init__.py new file mode 100644 index 00000000..615a9085 --- /dev/null +++ b/bot/modules/sponsors/__init__.py @@ -0,0 +1,5 @@ +from . import module + +from . import data +from . import config +from . import commands diff --git a/bot/modules/sponsors/commands.py b/bot/modules/sponsors/commands.py new file mode 100644 index 00000000..5ddd8b93 --- /dev/null +++ b/bot/modules/sponsors/commands.py @@ -0,0 +1,14 @@ +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)) diff --git a/bot/modules/sponsors/config.py b/bot/modules/sponsors/config.py new file mode 100644 index 00000000..c9d25b56 --- /dev/null +++ b/bot/modules/sponsors/config.py @@ -0,0 +1,92 @@ +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 diff --git a/bot/modules/sponsors/data.py b/bot/modules/sponsors/data.py new file mode 100644 index 00000000..c3a26d3a --- /dev/null +++ b/bot/modules/sponsors/data.py @@ -0,0 +1,4 @@ +from data import Table + + +guild_whitelist = Table("sponsor_guild_whitelist") diff --git a/bot/modules/sponsors/module.py b/bot/modules/sponsors/module.py new file mode 100644 index 00000000..ae9ba299 --- /dev/null +++ b/bot/modules/sponsors/module.py @@ -0,0 +1,27 @@ +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 not ctx.guild or ctx.guild.id not in ctx.client.settings.sponsor_guild_whitelist.value: + 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) diff --git a/bot/modules/sysadmin/__init__.py b/bot/modules/sysadmin/__init__.py index 8b2cee5a..62591ad4 100644 --- a/bot/modules/sysadmin/__init__.py +++ b/bot/modules/sysadmin/__init__.py @@ -4,3 +4,4 @@ from . import exec_cmds from . import guild_log from . import status from . import blacklist +from . import botconfig diff --git a/bot/modules/sysadmin/botconfig.py b/bot/modules/sysadmin/botconfig.py new file mode 100644 index 00000000..3bccf050 --- /dev/null +++ b/bot/modules/sysadmin/botconfig.py @@ -0,0 +1,96 @@ +import difflib +import discord +from cmdClient.checks import is_owner + +from settings import UserInputError + +from utils.lib import prop_tabulate + +from .module import module + + +@module.cmd("botconfig", + desc="Update global bot configuration.", + flags=('add', 'remove'), + group="Bot Admin") +@is_owner() +async def cmd_botconfig(ctx, flags): + """ + Usage`` + {prefix}botconfig + {prefix}botconfig info + {prefix}botconfig + {prefix}botconfig + Description: + Usage directly follows the `config` command for guild configuration. + """ + # Cache and map some info for faster access + setting_displaynames = {setting.display_name.lower(): setting for setting in ctx.client.settings.settings.values()} + appid = ctx.client.conf['data_appid'] + + if not ctx.args or ctx.args.lower() in ('info', 'help'): + # Fill the setting cats + cats = {} + for setting in ctx.client.settings.settings.values(): + cat = cats.get(setting.category, []) + cat.append(setting) + cats[setting.category] = cat + + # Format the cats + sections = {} + for catname, cat in cats.items(): + catprops = { + setting.display_name: setting.get(appid).summary if not ctx.args else setting.desc + for setting in cat + } + # TODO: Add cat description here + sections[catname] = prop_tabulate(*zip(*catprops.items())) + + # Build the cat page + embed = discord.Embed( + colour=discord.Colour.orange(), + title="App Configuration" + ) + for name, section in sections.items(): + embed.add_field(name=name, value=section, inline=False) + + await ctx.reply(embed=embed) + else: + # Some args were given + parts = ctx.args.split(maxsplit=1) + + name = parts[0] + setting = setting_displaynames.get(name.lower(), None) + if setting is None: + matches = difflib.get_close_matches(name, setting_displaynames.keys(), n=2) + match = "`{}`".format('` or `'.join(matches)) if matches else None + return await ctx.error_reply( + "Couldn't find a setting called `{}`!\n" + "{}" + "Use `{}botconfig info` to see all the available settings.".format( + name, + "Maybe you meant {}?\n".format(match) if match else "", + ctx.best_prefix + ) + ) + + if len(parts) == 1 and not ctx.msg.attachments: + # config + # View config embed for provided setting + await setting.get(appid).widget(ctx, flags=flags) + else: + # config + # Attempt to set config setting + try: + parsed = await setting.parse(appid, ctx, parts[1] if len(parts) > 1 else '') + parsed.write(add_only=flags['add'], remove_only=flags['remove']) + except UserInputError as e: + await ctx.reply(embed=discord.Embed( + description="{} {}".format('❌', e.msg), + colour=discord.Colour.red() + )) + else: + await ctx.reply(embed=discord.Embed( + description="{} {}".format('✅', setting.get(appid).success_response), + colour=discord.Colour.green() + )) diff --git a/bot/modules/topgg/data.py b/bot/modules/topgg/data.py index b12a4120..3bad8ae9 100644 --- a/bot/modules/topgg/data.py +++ b/bot/modules/topgg/data.py @@ -1,4 +1,4 @@ -from data.interfaces import RowTable +from data.interfaces import RowTable, Table topggvotes = RowTable( 'topgg', @@ -6,3 +6,4 @@ topggvotes = RowTable( 'voteid' ) +guild_whitelist = Table('topgg_guild_whitelist') diff --git a/bot/modules/topgg/module.py b/bot/modules/topgg/module.py index d2ff79f5..a3872192 100644 --- a/bot/modules/topgg/module.py +++ b/bot/modules/topgg/module.py @@ -2,6 +2,8 @@ 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 @@ -39,12 +41,16 @@ boostfree_commands = {'config', 'pomodoro'} async def topgg_reply_wrapper(func, ctx: LionContext, *args, suggest_vote=True, **kwargs): if not suggest_vote: pass - elif ctx.cmd and (ctx.cmd.name in boostfree_commands or ctx.cmd.group in boostfree_groups): + 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 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: + 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", diff --git a/bot/modules/topgg/settings.py b/bot/modules/topgg/settings.py index 33daad75..c59acd90 100644 --- a/bot/modules/topgg/settings.py +++ b/bot/modules/topgg/settings.py @@ -1,10 +1,14 @@ -from settings.user_settings import UserSettings, UserSetting -from settings.setting_types import Boolean +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 @@ -48,3 +52,21 @@ class topgg_vote_remainder(Boolean, UserSetting): 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 diff --git a/bot/settings/__init__.py b/bot/settings/__init__.py index 3f72ea44..fdd6d18d 100644 --- a/bot/settings/__init__.py +++ b/bot/settings/__init__.py @@ -3,3 +3,4 @@ from .setting_types import * # noqa from .user_settings import UserSettings, UserSetting # noqa from .guild_settings import GuildSettings, GuildSetting # noqa +from .app_settings import AppSettings diff --git a/bot/settings/app_settings.py b/bot/settings/app_settings.py new file mode 100644 index 00000000..637c4b51 --- /dev/null +++ b/bot/settings/app_settings.py @@ -0,0 +1,5 @@ +import settings +from utils.lib import DotDict + +class AppSettings(settings.ObjectSettings): + settings = DotDict() diff --git a/bot/settings/base.py b/bot/settings/base.py index 8b6f67a1..a2750796 100644 --- a/bot/settings/base.py +++ b/bot/settings/base.py @@ -1,3 +1,4 @@ +import json import discord from cmdClient.cmdClient import cmdClient from cmdClient.lib import SafeCancellation @@ -201,13 +202,13 @@ class Setting: raise NotImplementedError @classmethod - async def command(cls, ctx, id): + async def command(cls, ctx, id, flags=()): """ Standardised command viewing/setting interface for the setting. """ - if not ctx.args: + if not ctx.args and not ctx.msg.attachments: # View config embed for provided cls - await ctx.reply(embed=cls.get(id).embed) + await cls.get(id).widget(ctx, flags=flags) else: # Check the write ward if cls.write_ward and not await cls.write_ward.run(ctx): @@ -459,5 +460,55 @@ class ListData: cls._cache[id] = data +class KeyValueData: + """ + Mixin for settings implemented in a Key-Value table. + The underlying table should have a Unique constraint on the `(_id_column, _key_column)` pair. + """ + _table_interface: Table = None + + _id_column: str = None + + _key_column: str = None + + _value_column: str = None + + _key: str = None + + @classmethod + def _reader(cls, id: ..., **kwargs): + params = { + "select_columns": (cls._value_column, ), + cls._id_column: id, + cls._key_column: cls._key + } + + row = cls._table_interface.select_one_where(**params) + data = row[cls._value_column] if row else None + + if data is not None: + data = json.loads(data) + + return data + + @classmethod + def _writer(cls, id: ..., data: ..., **kwargs): + params = { + cls._id_column: id, + cls._key_column: cls._key + } + if data is not None: + values = { + cls._value_column: json.dumps(data) + } + cls._table_interface.upsert( + constraint=f"{cls._id_column}, {cls._key_column}", + **params, + **values + ) + else: + cls._table_interface.delete_where(**params) + + class UserInputError(SafeCancellation): pass diff --git a/bot/settings/setting_types.py b/bot/settings/setting_types.py index 4b5e1dd3..8c2863ad 100644 --- a/bot/settings/setting_types.py +++ b/bot/settings/setting_types.py @@ -473,6 +473,64 @@ class Emoji(SettingType): return str(data) +class GuildID(SettingType): + """ + Integer type for storing Guild IDs. Stores any snowflake. + + Types: + data: Optional[int] + The stored integer value. + value: Optional[int] + The stored integer value. + """ + accepts = "Any snowflake id." + + @classmethod + def _data_from_value(cls, id: int, value: Optional[bool], **kwargs): + """ + Both data and value are of type Optional[int]. + Directly return the provided value as data. + """ + return value + + @classmethod + def _data_to_value(cls, id: int, data: Optional[bool], **kwargs): + """ + Both data and value are of type Optional[int]. + Directly return the internal data as the value. + """ + return data + + @classmethod + async def _parse_userstr(cls, ctx: Context, id: int, userstr: str, **kwargs): + """ + Relies on integer casting to convert the user string + """ + if not userstr or userstr.lower() == "none": + return None + + try: + num = int(userstr) + except Exception: + raise UserInputError("Couldn't parse provided guild id.") from None + + return num + + @classmethod + def _format_data(cls, id: int, data: Optional[int], **kwargs): + """ + Return the string version of the data. + """ + if data is None: + return None + elif (guild := client.get_guild(data)): + return f"`{data}` ({guild.name})" + elif (row := client.data.guild_config.fetch(data)): + return f"`{data}` ({row.name})" + else: + return f"`{data}`" + + class Timezone(SettingType): """ Timezone type, storing a valid timezone string. @@ -713,6 +771,8 @@ class Message(SettingType): _substitution_desc = { } + _cmd_str = '{prefix} config {setting}' + @classmethod def _data_from_value(cls, id, value, **kwargs): if value is None: @@ -755,12 +815,17 @@ class Message(SettingType): if as_json: try: args = json.loads(userstr) - if not isinstance(args, dict) or (not args.get('content', None) and not args.get('embed', None)): - raise ValueError("At least one of the 'content' or 'embed' data fields are required.") + if not isinstance(args, dict) or (not {'content', 'embed', 'embeds'}.intersection(args.keys())): + raise ValueError("At least one of the 'content', 'embed', or 'embeds' fields are required.") if 'embed' in args: discord.Embed.from_dict( args['embed'] ) + if 'embeds' in args: + for embed in args['embeds']: + discord.Embed.from_dict( + embed + ) except Exception as e: only_error = "".join(traceback.TracebackException.from_exception(e).format_exception_only()) raise UserInputError( @@ -771,6 +836,8 @@ class Message(SettingType): ) if 'embed' in args and 'timestamp' in args['embed']: args['embed'].pop('timestamp') + if 'embeds' in args: + [embed.pop('timestamp', None) for embed in args['embeds']] return json.dumps(args) else: return json.dumps({'content': userstr}) @@ -780,9 +847,9 @@ class Message(SettingType): if data is None: return "Empty" value = cls._data_to_value(id, data, **kwargs) - if 'embed' not in value and 'content' not in value: + if not {'embed', 'content', 'embeds'}.intersection(value.keys()): return "Invalid" - if 'embed' not in value and len(value['content']) < 100: + if 'content' in value and 'embed' not in value and 'embeds' not in value and len(value['content']) < 100: return "`{}`".format(value['content']) else: return "Too long to display here!" @@ -806,6 +873,13 @@ class Message(SettingType): args['embed'] = discord.Embed.from_dict( json.loads(multiple_replace(json.dumps(value['embed']), substitutions)) ) + if value.get('embeds', None): + args['embeds'] = [ + discord.Embed.from_dict( + json.loads(multiple_replace(json.dumps(embed), substitutions)) + ) + for embed in value['embeds'] + ] return args async def widget(self, ctx, **kwargs): @@ -818,7 +892,7 @@ class Message(SettingType): current_str = None preview = None file_content = None - if 'embed' in value or len(value['content']) > 1024: + if 'embed' in value or 'embeds' in value or len(value['content']) > 1024: current_str = "See attached file." file_content = json.dumps(value, indent=4) elif "`" in value['content']: @@ -844,14 +918,16 @@ class Message(SettingType): embed.add_field( name="Setting Guide", value=( - "• For plain text without an embed, use `{prefix}config {setting} `.\n" + "• For plain text without an embed, use `{cmd_str} `.\n" "• To include an embed, build the message [here]({builder}) " - "and upload the json code as a file with the `{prefix}config {setting}` command.\n" - "• To reset the message to the default, use `{prefix}config {setting} None`." + "and upload the json code as a file with the `{cmd_str}` command.\n" + "• To reset the message to the default, use `{cmd_str} None`." + ).format( + cmd_str=self._cmd_str, + builder="https://glitchii.github.io/embedbuilder/?editor=gui" ).format( prefix=ctx.best_prefix, setting=self.display_name, - builder="https://glitchii.github.io/embedbuilder/?editor=gui" ), inline=False ) @@ -1028,3 +1104,16 @@ class StringList(SettingList): "Write `--add` or `--remove` to add or remove strings." ) _setting = String + + +class GuildIDList(SettingList): + """ + List of guildids. + """ + accepts = ( + "Comma separated list of guild ids. Use `None` to unset. " + "Write `--add` or `--remove` to add or remove ids. " + "The provided ids are not verified in any way." + ) + + _setting = GuildID diff --git a/data/migration/v10-v11/migration.sql b/data/migration/v10-v11/migration.sql new file mode 100644 index 00000000..09a75c73 --- /dev/null +++ b/data/migration/v10-v11/migration.sql @@ -0,0 +1,27 @@ +-- App Config Data {{{ +CREATE TABLE AppConfig( + appid TEXT, + key TEXT, + value TEXT, + PRIMARY KEY(appid, key) +); +-- }}} + + +-- Sponsor Data {{{ +CREATE TABLE sponsor_guild_whitelist( + appid TEXT, + guildid BIGINT, + PRIMARY KEY(appid, guildid) +); +-- }}} + +-- Topgg Data {{{ +CREATE TABLE topgg_guild_whitelist( + appid TEXT, + guildid BIGINT, + PRIMARY KEY(appid, guildid) +); +-- }}} + +INSERT INTO VersionHistory (version, author) VALUES (11, 'v10-v11 migration'); diff --git a/data/schema.sql b/data/schema.sql index 5287648e..69592305 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 (10, 'Initial Creation'); +INSERT INTO VersionHistory (version, author) VALUES (11, 'Initial Creation'); CREATE OR REPLACE FUNCTION update_timestamp_column() @@ -22,6 +22,13 @@ CREATE TABLE AppData( 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, ownerid BIGINT NOT NULL, @@ -37,7 +44,6 @@ CREATE TABLE global_guild_blacklist( ); -- }}} - -- User configuration data {{{ CREATE TABLE user_config( userid BIGINT PRIMARY KEY, @@ -799,6 +805,20 @@ create TABLE topgg( boostedTimestamp TIMESTAMPTZ NOT NULL ); CREATE INDEX topgg_userid_timestamp ON topgg (userid, boostedTimestamp); + +CREATE TABLE topgg_guild_whitelist( + appid TEXT, + guildid BIGINT, + PRIMARY KEY(appid, guildid) +); +-- }}} + +-- Sponsor Data {{{ +CREATE TABLE sponsor_guild_whitelist( + appid TEXT, + guildid BIGINT, + PRIMARY KEY(appid, guildid) +); -- }}} -- vim: set fdm=marker: