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`
+
+
+
+- **Camera only study rooms**
+
+Set specific channels to force users to use their webcam to study.
+
+
-### 📙 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.
+
+
+
+- **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.
+
+
+
- **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.
+
+
+
- **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`
+
+
+
+- **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.
+
+
+
- **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`
-
+
- **Camera only study rooms**
Set specific channels to force users to use their webcam to study.
-
+
- **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.
-
+
- **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.
-
+
- **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.
-
+
- **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`
-
+
- **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.
-
- **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 = {