From fbf49c58e24782fd25323448c6fd7d0019bfbe1d Mon Sep 17 00:00:00 2001 From: AriHoresh <78949378+AriHoresh@users.noreply.github.com> Date: Sun, 23 Jan 2022 12:41:00 +0100 Subject: [PATCH 01/14] Readme updates - media and SEO opt --- README.md | 115 ++++++++++++++++++++++++++++++++++++++++++------------ 1 file changed, 89 insertions(+), 26 deletions(-) diff --git a/README.md b/README.md index b1517193..9eba8d89 100644 --- a/README.md +++ b/README.md @@ -1,51 +1,114 @@ -# StudyLion - Discord Study & Productivity Bot -StudyLion is a Discord bot that tracks members' study and work time while offering members the ability to view their statistics and use productivity tools such as: To-do lists, pomodoro timers, reminders, and much more. + +## StudyLion - Discord Study & Productivity Bot + +StudyLion is a Discord bot that tracks members' study and work time while offering members the ability to view their statistics and use productivity tools such as: To-do lists, pomodoro timers, reminders, and much more. + + [**Invite StudyLion here**](https://discord.studylions.com/invite "here"), and get started with `!help`. -Join the [**support server**](https://discord.gg/studylions "support server") or start a [**discussion**](https://github.com/StudyLions/StudyLion/discussions "disscussion") if you have any questions or issues. -### 🧠 The Idea ------------- +Join the [**support server**](https://discord.gg/studylions "support server") to contact us if you need help configuring the bot on your server, or start a [**discussion**](https://github.com/StudyLions/StudyLion/discussions "disscussion") to report issues and bugs. + + + +### The Idea + + In the past couple of years, we noticed a new trend on Discord – instead of being a platform designed only for gamers, many students joined it as well, forming communities dedicated to studying and working together. -I have a study community myself called [The Study Lions](http://discord.gg/studylions "The Study Lions"). -The community members decided to raise funds and hire an amazing developer that created our own unique study/productivity bot. -As soon as we published the bot, hundreds of new students made their first step and started using our virtual study rooms as well! -Over the months we got many suggestions so we kept updating and adding more and more features to the bot! +This bot was founder by [Ari Horesh](https://www.youtube.com/arihoresh) (Ari Horesh#0001) to support these forming study communities and allow students all over the world to study better. -I decided to invest further and make the bot public and open-source, so more study servers will be able to enjoy it as well, this way we can connect all study servers and create a network of students. +### Self Hosting + +We offer private instances based on availablity (a private bot for your community) to server owners who want their own branding (logo, color scheme, private and seperate database, better response-rate, and customizability to the text itself). +If you are intrested, contact the founder at contact@arihoresh.com . + +You can self-host and fork the bot using the following steps, but beware that this version **does not include** our visual graphical user interface, which is only include in the custom private instances or our the public instance. + +Follow the steps below to self-host the bot. +- Clone the repo recursively (which makes sure to include the cmdClient submodule, otherwise you need to initialise it separately) +- Install the requirements from `requirements.txt` +- Install Postgresql, and setup a database with the schema given in `data/schema.sql` +- Copy `config/example-bot.conf` to `config/bot.conf`, filling in the appropriate information, including database connection arguments. +- Start the bot from the top level `run.py`. + +We do not offer support for self-hosted bots, the code is provided as is without warranty of any kind. + +## Features + + +- **Students Cards and Statistics** + +Allow users to create their own private student profile cards and set customs study field tags by using `!stats` and `!setprofile` + +![Discord Study Bot Profile Card](https://entermedschool.com/wp-content/uploads/2022/01/Aris-Profile.png) + +- **Camera only study rooms** + +Set specific channels to force users to use their webcam to study. + +![discord study rooms](https://entermedschool.com/wp-content/uploads/2022/01/discord-study-rooms.png) -### 📙 Features ------------- -StudyLion has the following primary features: -- **Camera only study rooms** -Set specific channels to force users to use their webcam to study. - **To-Do List** -Users can create and share their own to-do lists, and get rewards when completing a task! -- **Reminders** -Users can set their own private reminders, to drink water, stretch, or anything else they want to remember, every X minutes, hours, days, or maybe even just once. -- **Accountability Rooms** -This feature allows the users to use their coins to schedule a time to study at. + +Users can create and share their own to-do lists, and get rewards when completing a task! Use `!todo` to launch our interactive to do list! + +- **Reminders** + +Users can set their own private reminders, to drink water, stretch, or anything else they want to remember, every X minutes, hours, days, or maybe even just once. + +Example: `!remindme to drink water every 3h` will send you a reminder every 3 hours to drink water. + +![discord bot to do lists and reminders](https://entermedschool.com/wp-content/uploads/2022/01/to-do-3.png) + +- **Scheduled Sessions** + +This feature allows the users to use their coins to schedule a time to study at. Book rooms using `!rooms book` + Not attending prevents everyone in the room from getting the bonus. + +![scheuduled study rooms discord](https://entermedschool.com/wp-content/uploads/2022/01/accountability-1.png) + - **Study and Work Statistics** -Users can view their daily, weekly, monthly and all-time stats, as well as their study streak. + +In addition to the profile cards, users can view their daily, weekly, monthly and all-time stats, as well as their study streak. Use `!weekly` and `!monthly` to view your revision statistics in more detail. + +![weekly and monthly statistics discord study](https://entermedschool.com/wp-content/uploads/2022/01/statistics-discord.png) + - **Pomodoro Timers** -The bot will show the timer in the title of the study room and play a sound at the start and end of each session. -- **Private Study Rooms** -Allows the members to create their own private study rooms and invite their friends to join! + +The bot will show the timer in the title of the study room and play a sound at the start and end of each session. +Commands: `!timer` , `!pomodoro` + +![Pomodoro timer Discord](https://entermedschool.com/wp-content/uploads/2022/01/pomodortimersdone.png) + +- **Private Study Rooms** + +Allows the members to create their own private study rooms and invite their friends to join! +Rent a room using `!rent [usernames]`. + - **Workout Rooms** + Allows the Admins to create workout rooms with a bonus for people who workout. + - **Study Tiers and Achievements** + Reward users based on their total study time, allow them to get better ranks, and show off how long they've been working. + +![to do lists](https://entermedschool.com/wp-content/uploads/2022/01/leaderboard-discord.png) + - **Full-Scale Economy System** + Reward users for studying, allow them to use the coins to buy private study rooms, schedule accountability rooms, and even change their name's color. + - **Full-Scale Moderation System** + Punish cheaters, audit-log, welcome message, and so much more using our full-scale moderation system. +### Tutorials -### ❓ Tutorials ------------- A command list and general documentation for StudyLion may be found using the `!help` command, and documentation for a specific command, e.g. `config`, may be found with `!help config`. + Make sure to check the [full documentation](https://www.notion.so/izabellakis/StudyLion-Bot-Tutorials-f493268fcd12436c9674afef2e151707 "StudyLion Tutorial") to stay updated. From 65ae2ef503025e7a6c5eb521bc70836b8abd9c84 Mon Sep 17 00:00:00 2001 From: AriHoresh <78949378+AriHoresh@users.noreply.github.com> Date: Sun, 23 Jan 2022 12:55:48 +0100 Subject: [PATCH 02/14] delete - gofundme page --- .github/FUNDING.yml | 1 - 1 file changed, 1 deletion(-) diff --git a/.github/FUNDING.yml b/.github/FUNDING.yml index f341ce9e..772d9e0d 100644 --- a/.github/FUNDING.yml +++ b/.github/FUNDING.yml @@ -9,4 +9,3 @@ community_bridge: # Replace with a single Community Bridge project-name e.g., cl liberapay: # Replace with a single Liberapay username issuehunt: # Replace with a single IssueHunt username otechie: # Replace with a single Otechie username -custom: https://gofundme.com/LionBot From d50eac6455b3a885a47f105fdc50ecd8e0becbba Mon Sep 17 00:00:00 2001 From: AriHoresh <78949378+AriHoresh@users.noreply.github.com> Date: Tue, 25 Jan 2022 14:06:31 +0100 Subject: [PATCH 03/14] Can't spend 24 hours without doing anything --- README.md | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/README.md b/README.md index 9eba8d89..1691eed3 100644 --- a/README.md +++ b/README.md @@ -43,13 +43,13 @@ We do not offer support for self-hosted bots, the code is provided as is without Allow users to create their own private student profile cards and set customs study field tags by using `!stats` and `!setprofile` -![Discord Study Bot Profile Card](https://entermedschool.com/wp-content/uploads/2022/01/Aris-Profile.png) +![Discord Study Bot Profile Card](https://i.imgur.com/dEZvawb.png) - **Camera only study rooms** Set specific channels to force users to use their webcam to study. -![discord study rooms](https://entermedschool.com/wp-content/uploads/2022/01/discord-study-rooms.png) +![discord study rooms](https://i.imgur.com/rlsH8a6.png) - **To-Do List** @@ -61,7 +61,7 @@ Users can set their own private reminders, to drink water, stretch, or anything Example: `!remindme to drink water every 3h` will send you a reminder every 3 hours to drink water. -![discord bot to do lists and reminders](https://entermedschool.com/wp-content/uploads/2022/01/to-do-3.png) +![discord bot to do lists and reminders](https://i.imgur.com/BMFK2gJ.png) - **Scheduled Sessions** @@ -69,20 +69,20 @@ This feature allows the users to use their coins to schedule a time to study at. Not attending prevents everyone in the room from getting the bonus. -![scheuduled study rooms discord](https://entermedschool.com/wp-content/uploads/2022/01/accountability-1.png) +![scheuduled study rooms discord](https://i.imgur.com/6dMSqDh.png) - **Study and Work Statistics** In addition to the profile cards, users can view their daily, weekly, monthly and all-time stats, as well as their study streak. Use `!weekly` and `!monthly` to view your revision statistics in more detail. -![weekly and monthly statistics discord study](https://entermedschool.com/wp-content/uploads/2022/01/statistics-discord.png) +![weekly and monthly statistics discord study](https://i.imgur.com/i7JutEh.png) - **Pomodoro Timers** The bot will show the timer in the title of the study room and play a sound at the start and end of each session. Commands: `!timer` , `!pomodoro` -![Pomodoro timer Discord](https://entermedschool.com/wp-content/uploads/2022/01/pomodortimersdone.png) +![Pomodoro timer Discord](https://i.imgur.com/UcNXpv3.png) - **Private Study Rooms** @@ -97,7 +97,6 @@ Allows the Admins to create workout rooms with a bonus for people who workout. Reward users based on their total study time, allow them to get better ranks, and show off how long they've been working. -![to do lists](https://entermedschool.com/wp-content/uploads/2022/01/leaderboard-discord.png) - **Full-Scale Economy System** @@ -112,3 +111,5 @@ Punish cheaters, audit-log, welcome message, and so much more using our full-sca A command list and general documentation for StudyLion may be found using the `!help` command, and documentation for a specific command, e.g. `config`, may be found with `!help config`. Make sure to check the [full documentation](https://www.notion.so/izabellakis/StudyLion-Bot-Tutorials-f493268fcd12436c9674afef2e151707 "StudyLion Tutorial") to stay updated. + + From 3c8303f89be9647c4b2f472525b7b05c5606cdd2 Mon Sep 17 00:00:00 2001 From: AriHoresh <78949378+AriHoresh@users.noreply.github.com> Date: Fri, 28 Jan 2022 17:36:32 +0100 Subject: [PATCH 04/14] NO WARRANTY - Tentative. --- LICENSE.md | 10 ++++++++++ 1 file changed, 10 insertions(+) create mode 100644 LICENSE.md diff --git a/LICENSE.md b/LICENSE.md new file mode 100644 index 00000000..3a6fd6bb --- /dev/null +++ b/LICENSE.md @@ -0,0 +1,10 @@ +Copyright (c) 2022, Ari Horesh. +All rights reserved. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. From da9f9b02b139bfa35f7509090c74173b941178dd Mon Sep 17 00:00:00 2001 From: Conatum Date: Sat, 12 Mar 2022 17:27:25 +0200 Subject: [PATCH 05/14] feature (sponsors): Add sponsor command. Add new sponsor prompts. Add new sponsor command. Add `sponsor_text` table. Add sponsor global config settings. Update `Setting.command` to use new `widget`. Add custom `cmd_str` support to `Message` `SettingType`. --- bot/modules/__init__.py | 1 + bot/modules/sponsors/__init__.py | 5 ++ bot/modules/sponsors/commands.py | 27 +++++++++++ bot/modules/sponsors/config.py | 70 ++++++++++++++++++++++++++++ bot/modules/sponsors/data.py | 4 ++ bot/modules/sponsors/module.py | 27 +++++++++++ bot/modules/topgg/module.py | 8 +++- bot/settings/base.py | 6 +-- bot/settings/setting_types.py | 12 +++-- data/migration/v10-v11/migration.sql | 9 ++++ data/schema.sql | 11 ++++- 11 files changed, 170 insertions(+), 10 deletions(-) create mode 100644 bot/modules/sponsors/__init__.py create mode 100644 bot/modules/sponsors/commands.py create mode 100644 bot/modules/sponsors/config.py create mode 100644 bot/modules/sponsors/data.py create mode 100644 bot/modules/sponsors/module.py create mode 100644 data/migration/v10-v11/migration.sql 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/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..e937739c --- /dev/null +++ b/bot/modules/sponsors/commands.py @@ -0,0 +1,27 @@ +from cmdClient.checks import is_owner + +from .module import module +from .config import settings + + +@module.cmd( + name="sponsors", + group="Meta", + desc="Check out our wonderful partners!", + flags=('edit', 'prompt') +) +async def cmd_sponsors(ctx, flags): + """ + Usage``: + {prefix}sponsors + """ + if await is_owner.run(ctx) and any(flags.values()): + if flags['edit']: + # Run edit setting command + await settings.sponsor_message.command(ctx, 0) + elif flags['prompt']: + # Run prompt setting command + await settings.sponsor_prompt.command(ctx, 0) + else: + # Display message + await ctx.reply(**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..0cda5bc6 --- /dev/null +++ b/bot/modules/sponsors/config.py @@ -0,0 +1,70 @@ +from cmdClient.checks import is_owner + +from settings.base import Setting, ColumnData, ObjectSettings +from settings.setting_types import Message, String + +from meta import client +from utils.lib import DotDict + +from .data import sponsor_text + + +class SponsorSettings(ObjectSettings): + settings = DotDict() + pass + + +@SponsorSettings.attach_setting +class sponsor_prompt(String, ColumnData, Setting): + attr_name = 'sponsor_prompt' + _default = "Type {prefix}sponsors to check our wonderful partners!" + + write_ward = is_owner + + display_name = 'sponsor_prompt' + 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 + + _data_column = 'prompt_text' + _table_interface = sponsor_text + _id_column = 'ID' + _upsert = True + _create_row = True + + @classmethod + def _data_to_value(cls, id, data, **kwargs): + if data: + return data.replace("{prefix}", client.prefix) + else: + return None + + +@SponsorSettings.attach_setting +class sponsor_message(Message, ColumnData, Setting): + attr_name = 'sponsor_message' + _default = '{"content": "Coming Soon!"}' + + write_ward = is_owner + + display_name = 'sponsor_message' + desc = "`sponsors` command response." + + long_desc = ( + "Message to reply with when a user runs the `sponsors` command." + ) + + _data_column = 'command_response' + _table_interface = sponsor_text + _id_column = 'ID' + _upsert = True + _create_row = True + + _cmd_str = "{prefix}sponsors --edit" + + +settings = SponsorSettings(0) diff --git a/bot/modules/sponsors/data.py b/bot/modules/sponsors/data.py new file mode 100644 index 00000000..39f4fcd3 --- /dev/null +++ b/bot/modules/sponsors/data.py @@ -0,0 +1,4 @@ +from data import Table + + +sponsor_text = Table("sponsor_text") diff --git a/bot/modules/sponsors/module.py b/bot/modules/sponsors/module.py new file mode 100644 index 00000000..9f08c0c1 --- /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 + +from .config import settings + + +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: + sponsor_hint = discord.Embed( + description=settings.sponsor_prompt.value, + 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/topgg/module.py b/bot/modules/topgg/module.py index d2ff79f5..c03f4e2a 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,14 @@ 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 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/settings/base.py b/bot/settings/base.py index 8b6f67a1..44678a97 100644 --- a/bot/settings/base.py +++ b/bot/settings/base.py @@ -201,13 +201,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): diff --git a/bot/settings/setting_types.py b/bot/settings/setting_types.py index 4b5e1dd3..4502c106 100644 --- a/bot/settings/setting_types.py +++ b/bot/settings/setting_types.py @@ -713,6 +713,8 @@ class Message(SettingType): _substitution_desc = { } + _cmd_str = '{prefix} config {setting}' + @classmethod def _data_from_value(cls, id, value, **kwargs): if value is None: @@ -844,14 +846,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 ) diff --git a/data/migration/v10-v11/migration.sql b/data/migration/v10-v11/migration.sql new file mode 100644 index 00000000..6c3b1f69 --- /dev/null +++ b/data/migration/v10-v11/migration.sql @@ -0,0 +1,9 @@ +-- Sponsor Data {{{ +CREATE TABLE sponsor_text( + ID INTEGER PRIMARY KEY DEFAULT 0, + prompt_text TEXT, + command_response TEXT +); +-- }}} + +INSERT INTO VersionHistory (version, author) VALUES (11, 'v10-v11 migration'); diff --git a/data/schema.sql b/data/schema.sql index 5287648e..de7ee92f 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() @@ -38,6 +38,15 @@ CREATE TABLE global_guild_blacklist( -- }}} +-- Sponsor Data {{{ +CREATE TABLE sponsor_text( + ID INTEGER PRIMARY KEY DEFAULT 0, + prompt_text TEXT, + command_response TEXT +); +-- }}} + + -- User configuration data {{{ CREATE TABLE user_config( userid BIGINT PRIMARY KEY, From b5e283bf8c4c21576198c88b9b18780739fce072 Mon Sep 17 00:00:00 2001 From: Conatum Date: Sat, 19 Mar 2022 13:12:24 +0200 Subject: [PATCH 06/14] feature (core): Add multi-embed library patch. --- bot/core/__init__.py | 2 + bot/core/patches.py | 111 +++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 113 insertions(+) create mode 100644 bot/core/patches.py 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/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 From bccbf38310620db81193e00850133279ecee80d2 Mon Sep 17 00:00:00 2001 From: Conatum Date: Sat, 19 Mar 2022 13:17:30 +0200 Subject: [PATCH 07/14] (settings): Add multi-embed support to Message. --- bot/settings/setting_types.py | 24 +++++++++++++++++++----- 1 file changed, 19 insertions(+), 5 deletions(-) diff --git a/bot/settings/setting_types.py b/bot/settings/setting_types.py index 4502c106..565ed5a1 100644 --- a/bot/settings/setting_types.py +++ b/bot/settings/setting_types.py @@ -757,12 +757,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( @@ -773,6 +778,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}) @@ -782,9 +789,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!" @@ -808,6 +815,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): @@ -820,7 +834,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']: From ace84c93886fbb2211fd4e7b1f4081bb4116e18a Mon Sep 17 00:00:00 2001 From: Conatum Date: Sat, 19 Mar 2022 15:54:34 +0200 Subject: [PATCH 08/14] (core): Add app-global setting storage. New `AppConfig` key-value table for arbitrary app config. New `KeyValueData` setting data mixin. New `AppSettings` settings group. Attached `AppSettings` as `client.settings`. Migrated sponsor settings to `AppSettings`. --- bot/core/data.py | 3 ++ bot/main.py | 5 +++ bot/modules/sponsors/commands.py | 7 ++-- bot/modules/sponsors/config.py | 44 ++++++++++-------------- bot/modules/sponsors/module.py | 4 +-- bot/settings/app_settings.py | 5 +++ bot/settings/base.py | 51 ++++++++++++++++++++++++++++ data/migration/v10-v11/migration.sql | 22 +++++++++--- data/schema.sql | 27 +++++++++------ 9 files changed, 120 insertions(+), 48 deletions(-) create mode 100644 bot/settings/app_settings.py 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/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/sponsors/commands.py b/bot/modules/sponsors/commands.py index e937739c..424f56a2 100644 --- a/bot/modules/sponsors/commands.py +++ b/bot/modules/sponsors/commands.py @@ -1,7 +1,6 @@ from cmdClient.checks import is_owner from .module import module -from .config import settings @module.cmd( @@ -18,10 +17,10 @@ async def cmd_sponsors(ctx, flags): if await is_owner.run(ctx) and any(flags.values()): if flags['edit']: # Run edit setting command - await settings.sponsor_message.command(ctx, 0) + await ctx.client.settings.sponsor_message.command(ctx, ctx.client.conf.bot['data_appid']) elif flags['prompt']: # Run prompt setting command - await settings.sponsor_prompt.command(ctx, 0) + await ctx.client.settings.sponsor_prompt.command(ctx, ctx.client.conf.bot['data_appid']) else: # Display message - await ctx.reply(**settings.sponsor_message.args(ctx)) + await ctx.reply(**ctx.client.settings.sponsor_message.args(ctx)) diff --git a/bot/modules/sponsors/config.py b/bot/modules/sponsors/config.py index 0cda5bc6..f8bcde25 100644 --- a/bot/modules/sponsors/config.py +++ b/bot/modules/sponsors/config.py @@ -1,23 +1,16 @@ from cmdClient.checks import is_owner -from settings.base import Setting, ColumnData, ObjectSettings +from settings import AppSettings, Setting, KeyValueData, ListData from settings.setting_types import Message, String from meta import client -from utils.lib import DotDict - -from .data import sponsor_text +from core.data import app_config -class SponsorSettings(ObjectSettings): - settings = DotDict() - pass - - -@SponsorSettings.attach_setting -class sponsor_prompt(String, ColumnData, Setting): +@AppSettings.attach_setting +class sponsor_prompt(String, KeyValueData, Setting): attr_name = 'sponsor_prompt' - _default = "Type {prefix}sponsors to check our wonderful partners!" + _default = None write_ward = is_owner @@ -30,11 +23,11 @@ class sponsor_prompt(String, ColumnData, Setting): _quote = False - _data_column = 'prompt_text' - _table_interface = sponsor_text - _id_column = 'ID' - _upsert = True - _create_row = True + _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): @@ -44,8 +37,8 @@ class sponsor_prompt(String, ColumnData, Setting): return None -@SponsorSettings.attach_setting -class sponsor_message(Message, ColumnData, Setting): +@AppSettings.attach_setting +class sponsor_message(Message, KeyValueData, Setting): attr_name = 'sponsor_message' _default = '{"content": "Coming Soon!"}' @@ -58,13 +51,10 @@ class sponsor_message(Message, ColumnData, Setting): "Message to reply with when a user runs the `sponsors` command." ) - _data_column = 'command_response' - _table_interface = sponsor_text - _id_column = 'ID' - _upsert = True - _create_row = True + _table_interface = app_config + _id_column = 'appid' + _key_column = 'key' + _value_column = 'value' + _key = 'sponsor_message' _cmd_str = "{prefix}sponsors --edit" - - -settings = SponsorSettings(0) diff --git a/bot/modules/sponsors/module.py b/bot/modules/sponsors/module.py index 9f08c0c1..489af5ce 100644 --- a/bot/modules/sponsors/module.py +++ b/bot/modules/sponsors/module.py @@ -5,8 +5,6 @@ from LionContext import LionContext from meta import client -from .config import settings - module = LionModule("Sponsor") @@ -18,7 +16,7 @@ sponsored_commands = {'profile', 'stats', 'weekly', 'monthly'} async def sponsor_reply_wrapper(func, ctx: LionContext, *args, **kwargs): if ctx.cmd and ctx.cmd.name in sponsored_commands: sponsor_hint = discord.Embed( - description=settings.sponsor_prompt.value, + description=ctx.client.settings.sponsor_prompt.value, colour=discord.Colour.dark_theme() ) if 'embed' not in kwargs: 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 44678a97..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 @@ -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/data/migration/v10-v11/migration.sql b/data/migration/v10-v11/migration.sql index 6c3b1f69..0e8740b0 100644 --- a/data/migration/v10-v11/migration.sql +++ b/data/migration/v10-v11/migration.sql @@ -1,8 +1,22 @@ +-- App Config Data {{{ +CREATE TABLE AppConfig( + appid TEXT, + key TEXT, + value TEXT, + PRIMARY KEY(appid, key) +); +-- }}} + + -- Sponsor Data {{{ -CREATE TABLE sponsor_text( - ID INTEGER PRIMARY KEY DEFAULT 0, - prompt_text TEXT, - command_response TEXT +CREATE TABLE sponsor_guild_whitelist( + guildid INTEGER PRIMARY KEY +); +-- }}} + +-- Topgg Data {{{ +CREATE TABLE topgg_guild_whitelist( + guildid INTEGER PRIMARY KEY ); -- }}} diff --git a/data/schema.sql b/data/schema.sql index de7ee92f..97f468fd 100644 --- a/data/schema.sql +++ b/data/schema.sql @@ -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,16 +44,6 @@ CREATE TABLE global_guild_blacklist( ); -- }}} - --- Sponsor Data {{{ -CREATE TABLE sponsor_text( - ID INTEGER PRIMARY KEY DEFAULT 0, - prompt_text TEXT, - command_response TEXT -); --- }}} - - -- User configuration data {{{ CREATE TABLE user_config( userid BIGINT PRIMARY KEY, @@ -808,6 +805,16 @@ create TABLE topgg( boostedTimestamp TIMESTAMPTZ NOT NULL ); CREATE INDEX topgg_userid_timestamp ON topgg (userid, boostedTimestamp); + +CREATE TABLE topgg_guild_whitelist( + guildid INTEGER PRIMARY KEY +); +-- }}} + +-- Sponsor Data {{{ +CREATE TABLE sponsor_guild_whitelist( + guildid INTEGER PRIMARY KEY +); -- }}} -- vim: set fdm=marker: From b21e0e21e50cd576a6e5cb06cad32ee100b3689c Mon Sep 17 00:00:00 2001 From: Conatum Date: Sat, 19 Mar 2022 16:17:54 +0200 Subject: [PATCH 09/14] feature (botadmin): Add `botconfig` command. --- bot/modules/sponsors/config.py | 2 + bot/modules/sysadmin/__init__.py | 1 + bot/modules/sysadmin/botconfig.py | 96 +++++++++++++++++++++++++++++++ 3 files changed, 99 insertions(+) create mode 100644 bot/modules/sysadmin/botconfig.py diff --git a/bot/modules/sponsors/config.py b/bot/modules/sponsors/config.py index f8bcde25..b4170a0f 100644 --- a/bot/modules/sponsors/config.py +++ b/bot/modules/sponsors/config.py @@ -15,6 +15,7 @@ class sponsor_prompt(String, KeyValueData, Setting): 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. " @@ -45,6 +46,7 @@ class sponsor_message(Message, KeyValueData, Setting): write_ward = is_owner display_name = 'sponsor_message' + category = 'Sponsors' desc = "`sponsors` command response." long_desc = ( 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() + )) From 2ff83b90fadd27fc03a54f4b8ee91433f6861cb5 Mon Sep 17 00:00:00 2001 From: Conatum Date: Sat, 19 Mar 2022 16:18:33 +0200 Subject: [PATCH 10/14] (sponsors): Allow `prompt` to be nullable. --- bot/modules/sponsors/commands.py | 16 ++-------------- bot/modules/sponsors/module.py | 13 +++++++------ bot/settings/__init__.py | 1 + 3 files changed, 10 insertions(+), 20 deletions(-) diff --git a/bot/modules/sponsors/commands.py b/bot/modules/sponsors/commands.py index 424f56a2..5ddd8b93 100644 --- a/bot/modules/sponsors/commands.py +++ b/bot/modules/sponsors/commands.py @@ -1,5 +1,3 @@ -from cmdClient.checks import is_owner - from .module import module @@ -7,20 +5,10 @@ from .module import module name="sponsors", group="Meta", desc="Check out our wonderful partners!", - flags=('edit', 'prompt') ) -async def cmd_sponsors(ctx, flags): +async def cmd_sponsors(ctx): """ Usage``: {prefix}sponsors """ - if await is_owner.run(ctx) and any(flags.values()): - if flags['edit']: - # Run edit setting command - await ctx.client.settings.sponsor_message.command(ctx, ctx.client.conf.bot['data_appid']) - elif flags['prompt']: - # Run prompt setting command - await ctx.client.settings.sponsor_prompt.command(ctx, ctx.client.conf.bot['data_appid']) - else: - # Display message - await ctx.reply(**ctx.client.settings.sponsor_message.args(ctx)) + await ctx.reply(**ctx.client.settings.sponsor_message.args(ctx)) diff --git a/bot/modules/sponsors/module.py b/bot/modules/sponsors/module.py index 489af5ce..d709a16d 100644 --- a/bot/modules/sponsors/module.py +++ b/bot/modules/sponsors/module.py @@ -15,11 +15,12 @@ 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: - sponsor_hint = discord.Embed( - description=ctx.client.settings.sponsor_prompt.value, - colour=discord.Colour.dark_theme() - ) - if 'embed' not in kwargs: - kwargs['embed'] = sponsor_hint + if (prompt := ctx.client.settings.sponsor_prompt.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/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 From 9ea40e5433a7641ec0171ff11c50c03e2225d6d7 Mon Sep 17 00:00:00 2001 From: Conatum Date: Sat, 19 Mar 2022 16:57:51 +0200 Subject: [PATCH 11/14] feature (sponsors): Add guild whitelist. Add `sponsor_hidden_in` app setting. Add new `GuildID` and `GuildIDList` setting types. --- bot/modules/sponsors/config.py | 32 ++++++++++++- bot/modules/sponsors/data.py | 2 +- bot/modules/sponsors/module.py | 13 ++--- bot/settings/setting_types.py | 71 ++++++++++++++++++++++++++++ data/migration/v10-v11/migration.sql | 8 +++- data/schema.sql | 8 +++- 6 files changed, 122 insertions(+), 12 deletions(-) diff --git a/bot/modules/sponsors/config.py b/bot/modules/sponsors/config.py index b4170a0f..c9d25b56 100644 --- a/bot/modules/sponsors/config.py +++ b/bot/modules/sponsors/config.py @@ -1,11 +1,12 @@ from cmdClient.checks import is_owner from settings import AppSettings, Setting, KeyValueData, ListData -from settings.setting_types import Message, String +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): @@ -37,6 +38,13 @@ class sponsor_prompt(String, KeyValueData, Setting): 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): @@ -60,3 +68,25 @@ class sponsor_message(Message, KeyValueData, Setting): _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 index 39f4fcd3..c3a26d3a 100644 --- a/bot/modules/sponsors/data.py +++ b/bot/modules/sponsors/data.py @@ -1,4 +1,4 @@ from data import Table -sponsor_text = Table("sponsor_text") +guild_whitelist = Table("sponsor_guild_whitelist") diff --git a/bot/modules/sponsors/module.py b/bot/modules/sponsors/module.py index d709a16d..ae9ba299 100644 --- a/bot/modules/sponsors/module.py +++ b/bot/modules/sponsors/module.py @@ -16,11 +16,12 @@ sponsored_commands = {'profile', 'stats', 'weekly', 'monthly'} 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): - sponsor_hint = discord.Embed( - description=prompt, - colour=discord.Colour.dark_theme() - ) - if 'embed' not in kwargs: - kwargs['embed'] = sponsor_hint + 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/settings/setting_types.py b/bot/settings/setting_types.py index 565ed5a1..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. @@ -1046,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 index 0e8740b0..09a75c73 100644 --- a/data/migration/v10-v11/migration.sql +++ b/data/migration/v10-v11/migration.sql @@ -10,13 +10,17 @@ CREATE TABLE AppConfig( -- Sponsor Data {{{ CREATE TABLE sponsor_guild_whitelist( - guildid INTEGER PRIMARY KEY + appid TEXT, + guildid BIGINT, + PRIMARY KEY(appid, guildid) ); -- }}} -- Topgg Data {{{ CREATE TABLE topgg_guild_whitelist( - guildid INTEGER PRIMARY KEY + appid TEXT, + guildid BIGINT, + PRIMARY KEY(appid, guildid) ); -- }}} diff --git a/data/schema.sql b/data/schema.sql index 97f468fd..69592305 100644 --- a/data/schema.sql +++ b/data/schema.sql @@ -807,13 +807,17 @@ create TABLE topgg( CREATE INDEX topgg_userid_timestamp ON topgg (userid, boostedTimestamp); CREATE TABLE topgg_guild_whitelist( - guildid INTEGER PRIMARY KEY + appid TEXT, + guildid BIGINT, + PRIMARY KEY(appid, guildid) ); -- }}} -- Sponsor Data {{{ CREATE TABLE sponsor_guild_whitelist( - guildid INTEGER PRIMARY KEY + appid TEXT, + guildid BIGINT, + PRIMARY KEY(appid, guildid) ); -- }}} From fa09266d3a4e04c1f38f1cd8f45c75a9c000864c Mon Sep 17 00:00:00 2001 From: Conatum Date: Sat, 19 Mar 2022 17:08:31 +0200 Subject: [PATCH 12/14] feature (topgg): Add guild whitelist. --- bot/modules/topgg/data.py | 3 ++- bot/modules/topgg/module.py | 2 ++ bot/modules/topgg/settings.py | 26 ++++++++++++++++++++++++-- 3 files changed, 28 insertions(+), 3 deletions(-) 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 c03f4e2a..a3872192 100644 --- a/bot/modules/topgg/module.py +++ b/bot/modules/topgg/module.py @@ -45,6 +45,8 @@ async def topgg_reply_wrapper(func, ctx: LionContext, *args, suggest_vote=True, 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) 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 From f3a0b5f70ae87641140c5569d3ffb7d27c3a3b63 Mon Sep 17 00:00:00 2001 From: Conatum Date: Sat, 19 Mar 2022 17:13:40 +0200 Subject: [PATCH 13/14] routine: Bump data version. --- bot/constants.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 From 1698098bbb3fe694aa8106a9d8bdf36203329a57 Mon Sep 17 00:00:00 2001 From: Conatum Date: Sat, 19 Mar 2022 17:48:03 +0200 Subject: [PATCH 14/14] routine: Update new command list. --- bot/modules/meta/help.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 = {