Merge branch 'staging' into feature-gems

This commit is contained in:
2022-04-02 11:38:47 +03:00
26 changed files with 704 additions and 48 deletions

1
.github/FUNDING.yml vendored
View File

@@ -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

10
LICENSE.md Normal file
View File

@@ -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.

116
README.md
View File

@@ -1,51 +1,115 @@
# 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://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://i.imgur.com/rlsH8a6.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://i.imgur.com/BMFK2gJ.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://i.imgur.com/6dMSqDh.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://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.
- **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://i.imgur.com/UcNXpv3.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.
- **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.
<a href="https://imgur.com/ziPdJGw"><img src="https://i.imgur.com/ziPdJGws.png" title="source: imgur.com" /></a>

View File

@@ -1,2 +1,2 @@
CONFIG_FILE = "config/bot.conf"
DATA_VERSION = 10
DATA_VERSION = 11

View File

@@ -1,5 +1,7 @@
from . import data # noqa
from . import patches
from .module import module
from .lion import Lion
from . import blacklists

View File

@@ -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',

111
bot/core/patches.py Normal file
View File

@@ -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

View File

@@ -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()

View File

@@ -13,3 +13,4 @@ from .renting import *
from .moderation import *
from .accountability import *
from .plugins import *
from .sponsors import *

View File

@@ -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 = {

View File

@@ -0,0 +1,5 @@
from . import module
from . import data
from . import config
from . import commands

View File

@@ -0,0 +1,14 @@
from .module import module
@module.cmd(
name="sponsors",
group="Meta",
desc="Check out our wonderful partners!",
)
async def cmd_sponsors(ctx):
"""
Usage``:
{prefix}sponsors
"""
await ctx.reply(**ctx.client.settings.sponsor_message.args(ctx))

View File

@@ -0,0 +1,92 @@
from cmdClient.checks import is_owner
from settings import AppSettings, Setting, KeyValueData, ListData
from settings.setting_types import Message, String, GuildIDList
from meta import client
from core.data import app_config
from .data import guild_whitelist
@AppSettings.attach_setting
class sponsor_prompt(String, KeyValueData, Setting):
attr_name = 'sponsor_prompt'
_default = None
write_ward = is_owner
display_name = 'sponsor_prompt'
category = 'Sponsors'
desc = "Text to send after core commands to encourage checking `sponsors`."
long_desc = (
"Text posted after several commands to encourage users to check the `sponsors` command. "
"Occurences of `{{prefix}}` will be replaced by the bot prefix."
)
_quote = False
_table_interface = app_config
_id_column = 'appid'
_key_column = 'key'
_value_column = 'value'
_key = 'sponsor_prompt'
@classmethod
def _data_to_value(cls, id, data, **kwargs):
if data:
return data.replace("{prefix}", client.prefix)
else:
return None
@property
def success_response(self):
if self.value:
return "The sponsor prompt has been update."
else:
return "The sponsor prompt has been cleared."
@AppSettings.attach_setting
class sponsor_message(Message, KeyValueData, Setting):
attr_name = 'sponsor_message'
_default = '{"content": "Coming Soon!"}'
write_ward = is_owner
display_name = 'sponsor_message'
category = 'Sponsors'
desc = "`sponsors` command response."
long_desc = (
"Message to reply with when a user runs the `sponsors` command."
)
_table_interface = app_config
_id_column = 'appid'
_key_column = 'key'
_value_column = 'value'
_key = 'sponsor_message'
_cmd_str = "{prefix}sponsors --edit"
@property
def success_response(self):
return "The `sponsors` command message has been updated."
@AppSettings.attach_setting
class sponsor_guild_whitelist(GuildIDList, ListData, Setting):
attr_name = 'sponsor_guild_whitelist'
write_ward = is_owner
category = 'Sponsors'
display_name = 'sponsor_hidden_in'
desc = "Guilds where the sponsor prompt is not displayed."
long_desc = (
"A list of guilds where the sponsor prompt hint will be hidden (see the `sponsor_prompt` setting)."
)
_table_interface = guild_whitelist
_id_column = 'appid'
_data_column = 'guildid'
_force_unique = True

View File

@@ -0,0 +1,4 @@
from data import Table
guild_whitelist = Table("sponsor_guild_whitelist")

View File

@@ -0,0 +1,27 @@
import discord
from LionModule import LionModule
from LionContext import LionContext
from meta import client
module = LionModule("Sponsor")
sponsored_commands = {'profile', 'stats', 'weekly', 'monthly'}
@LionContext.reply.add_wrapper
async def sponsor_reply_wrapper(func, ctx: LionContext, *args, **kwargs):
if ctx.cmd and ctx.cmd.name in sponsored_commands:
if (prompt := ctx.client.settings.sponsor_prompt.value):
if not ctx.guild or ctx.guild.id not in ctx.client.settings.sponsor_guild_whitelist.value:
sponsor_hint = discord.Embed(
description=prompt,
colour=discord.Colour.dark_theme()
)
if 'embed' not in kwargs:
kwargs['embed'] = sponsor_hint
return await func(ctx, *args, **kwargs)

View File

@@ -4,3 +4,4 @@ from . import exec_cmds
from . import guild_log
from . import status
from . import blacklist
from . import botconfig

View File

@@ -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 <setting>
{prefix}botconfig <setting> <value>
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 <setting>
# View config embed for provided setting
await setting.get(appid).widget(ctx, flags=flags)
else:
# config <setting> <value>
# 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()
))

View File

@@ -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')

View File

@@ -2,6 +2,8 @@ from LionModule import LionModule
from LionContext import LionContext
from core.lion import Lion
from modules.sponsors.module import sponsored_commands
from .utils import get_last_voted_timestamp, lion_loveemote, lion_yayemote
from .webhook import init_webhook
@@ -39,12 +41,16 @@ boostfree_commands = {'config', 'pomodoro'}
async def topgg_reply_wrapper(func, ctx: LionContext, *args, suggest_vote=True, **kwargs):
if not suggest_vote:
pass
elif ctx.cmd and (ctx.cmd.name in boostfree_commands or ctx.cmd.group in boostfree_groups):
elif not ctx.cmd:
pass
elif ctx.cmd.name in boostfree_commands or ctx.cmd.group in boostfree_groups:
pass
elif ctx.guild and ctx.guild.id in ctx.client.settings.topgg_guild_whitelist.value:
pass
elif not get_last_voted_timestamp(ctx.author.id):
upvote_info_formatted = upvote_info.format(lion_yayemote, ctx.best_prefix, lion_loveemote)
if 'embed' in kwargs:
if 'embed' in kwargs and ctx.cmd.name not in sponsored_commands:
# Add message as an extra embed field
kwargs['embed'].add_field(
name="\u200b",

View File

@@ -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

View File

@@ -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

View File

@@ -0,0 +1,5 @@
import settings
from utils.lib import DotDict
class AppSettings(settings.ObjectSettings):
settings = DotDict()

View File

@@ -1,3 +1,4 @@
import json
import discord
from cmdClient.cmdClient import cmdClient
from cmdClient.lib import SafeCancellation
@@ -201,13 +202,13 @@ class Setting:
raise NotImplementedError
@classmethod
async def command(cls, ctx, id):
async def command(cls, ctx, id, flags=()):
"""
Standardised command viewing/setting interface for the setting.
"""
if not ctx.args:
if not ctx.args and not ctx.msg.attachments:
# View config embed for provided cls
await ctx.reply(embed=cls.get(id).embed)
await cls.get(id).widget(ctx, flags=flags)
else:
# Check the write ward
if cls.write_ward and not await cls.write_ward.run(ctx):
@@ -459,5 +460,55 @@ class ListData:
cls._cache[id] = data
class KeyValueData:
"""
Mixin for settings implemented in a Key-Value table.
The underlying table should have a Unique constraint on the `(_id_column, _key_column)` pair.
"""
_table_interface: Table = None
_id_column: str = None
_key_column: str = None
_value_column: str = None
_key: str = None
@classmethod
def _reader(cls, id: ..., **kwargs):
params = {
"select_columns": (cls._value_column, ),
cls._id_column: id,
cls._key_column: cls._key
}
row = cls._table_interface.select_one_where(**params)
data = row[cls._value_column] if row else None
if data is not None:
data = json.loads(data)
return data
@classmethod
def _writer(cls, id: ..., data: ..., **kwargs):
params = {
cls._id_column: id,
cls._key_column: cls._key
}
if data is not None:
values = {
cls._value_column: json.dumps(data)
}
cls._table_interface.upsert(
constraint=f"{cls._id_column}, {cls._key_column}",
**params,
**values
)
else:
cls._table_interface.delete_where(**params)
class UserInputError(SafeCancellation):
pass

View File

@@ -473,6 +473,64 @@ class Emoji(SettingType):
return str(data)
class GuildID(SettingType):
"""
Integer type for storing Guild IDs. Stores any snowflake.
Types:
data: Optional[int]
The stored integer value.
value: Optional[int]
The stored integer value.
"""
accepts = "Any snowflake id."
@classmethod
def _data_from_value(cls, id: int, value: Optional[bool], **kwargs):
"""
Both data and value are of type Optional[int].
Directly return the provided value as data.
"""
return value
@classmethod
def _data_to_value(cls, id: int, data: Optional[bool], **kwargs):
"""
Both data and value are of type Optional[int].
Directly return the internal data as the value.
"""
return data
@classmethod
async def _parse_userstr(cls, ctx: Context, id: int, userstr: str, **kwargs):
"""
Relies on integer casting to convert the user string
"""
if not userstr or userstr.lower() == "none":
return None
try:
num = int(userstr)
except Exception:
raise UserInputError("Couldn't parse provided guild id.") from None
return num
@classmethod
def _format_data(cls, id: int, data: Optional[int], **kwargs):
"""
Return the string version of the data.
"""
if data is None:
return None
elif (guild := client.get_guild(data)):
return f"`{data}` ({guild.name})"
elif (row := client.data.guild_config.fetch(data)):
return f"`{data}` ({row.name})"
else:
return f"`{data}`"
class Timezone(SettingType):
"""
Timezone type, storing a valid timezone string.
@@ -713,6 +771,8 @@ class Message(SettingType):
_substitution_desc = {
}
_cmd_str = '{prefix} config {setting}'
@classmethod
def _data_from_value(cls, id, value, **kwargs):
if value is None:
@@ -755,12 +815,17 @@ class Message(SettingType):
if as_json:
try:
args = json.loads(userstr)
if not isinstance(args, dict) or (not args.get('content', None) and not args.get('embed', None)):
raise ValueError("At least one of the 'content' or 'embed' data fields are required.")
if not isinstance(args, dict) or (not {'content', 'embed', 'embeds'}.intersection(args.keys())):
raise ValueError("At least one of the 'content', 'embed', or 'embeds' fields are required.")
if 'embed' in args:
discord.Embed.from_dict(
args['embed']
)
if 'embeds' in args:
for embed in args['embeds']:
discord.Embed.from_dict(
embed
)
except Exception as e:
only_error = "".join(traceback.TracebackException.from_exception(e).format_exception_only())
raise UserInputError(
@@ -771,6 +836,8 @@ class Message(SettingType):
)
if 'embed' in args and 'timestamp' in args['embed']:
args['embed'].pop('timestamp')
if 'embeds' in args:
[embed.pop('timestamp', None) for embed in args['embeds']]
return json.dumps(args)
else:
return json.dumps({'content': userstr})
@@ -780,9 +847,9 @@ class Message(SettingType):
if data is None:
return "Empty"
value = cls._data_to_value(id, data, **kwargs)
if 'embed' not in value and 'content' not in value:
if not {'embed', 'content', 'embeds'}.intersection(value.keys()):
return "Invalid"
if 'embed' not in value and len(value['content']) < 100:
if 'content' in value and 'embed' not in value and 'embeds' not in value and len(value['content']) < 100:
return "`{}`".format(value['content'])
else:
return "Too long to display here!"
@@ -806,6 +873,13 @@ class Message(SettingType):
args['embed'] = discord.Embed.from_dict(
json.loads(multiple_replace(json.dumps(value['embed']), substitutions))
)
if value.get('embeds', None):
args['embeds'] = [
discord.Embed.from_dict(
json.loads(multiple_replace(json.dumps(embed), substitutions))
)
for embed in value['embeds']
]
return args
async def widget(self, ctx, **kwargs):
@@ -818,7 +892,7 @@ class Message(SettingType):
current_str = None
preview = None
file_content = None
if 'embed' in value or len(value['content']) > 1024:
if 'embed' in value or 'embeds' in value or len(value['content']) > 1024:
current_str = "See attached file."
file_content = json.dumps(value, indent=4)
elif "`" in value['content']:
@@ -844,14 +918,16 @@ class Message(SettingType):
embed.add_field(
name="Setting Guide",
value=(
"• For plain text without an embed, use `{prefix}config {setting} <text>`.\n"
"• For plain text without an embed, use `{cmd_str} <text>`.\n"
"• To include an embed, build the message [here]({builder}) "
"and upload the json code as a file with the `{prefix}config {setting}` command.\n"
"• To reset the message to the default, use `{prefix}config {setting} None`."
"and upload the json code as a file with the `{cmd_str}` command.\n"
"• To reset the message to the default, use `{cmd_str} None`."
).format(
cmd_str=self._cmd_str,
builder="https://glitchii.github.io/embedbuilder/?editor=gui"
).format(
prefix=ctx.best_prefix,
setting=self.display_name,
builder="https://glitchii.github.io/embedbuilder/?editor=gui"
),
inline=False
)
@@ -1028,3 +1104,16 @@ class StringList(SettingList):
"Write `--add` or `--remove` to add or remove strings."
)
_setting = String
class GuildIDList(SettingList):
"""
List of guildids.
"""
accepts = (
"Comma separated list of guild ids. Use `None` to unset. "
"Write `--add` or `--remove` to add or remove ids. "
"The provided ids are not verified in any way."
)
_setting = GuildID

View File

@@ -0,0 +1,27 @@
-- App Config Data {{{
CREATE TABLE AppConfig(
appid TEXT,
key TEXT,
value TEXT,
PRIMARY KEY(appid, key)
);
-- }}}
-- Sponsor Data {{{
CREATE TABLE sponsor_guild_whitelist(
appid TEXT,
guildid BIGINT,
PRIMARY KEY(appid, guildid)
);
-- }}}
-- Topgg Data {{{
CREATE TABLE topgg_guild_whitelist(
appid TEXT,
guildid BIGINT,
PRIMARY KEY(appid, guildid)
);
-- }}}
INSERT INTO VersionHistory (version, author) VALUES (11, 'v10-v11 migration');

View File

@@ -4,7 +4,7 @@ CREATE TABLE VersionHistory(
time TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP NOT NULL,
author TEXT
);
INSERT INTO VersionHistory (version, author) VALUES (10, 'Initial Creation');
INSERT INTO VersionHistory (version, author) VALUES (11, 'Initial Creation');
CREATE OR REPLACE FUNCTION update_timestamp_column()
@@ -22,6 +22,13 @@ CREATE TABLE AppData(
last_study_badge_scan TIMESTAMP
);
CREATE TABLE AppConfig(
appid TEXT,
key TEXT,
value TEXT,
PRIMARY KEY(appid, key)
);
CREATE TABLE global_user_blacklist(
userid BIGINT PRIMARY KEY,
ownerid BIGINT NOT NULL,
@@ -37,7 +44,6 @@ CREATE TABLE global_guild_blacklist(
);
-- }}}
-- User configuration data {{{
CREATE TABLE user_config(
userid BIGINT PRIMARY KEY,
@@ -800,6 +806,20 @@ create TABLE topgg(
boostedTimestamp TIMESTAMPTZ NOT NULL
);
CREATE INDEX topgg_userid_timestamp ON topgg (userid, boostedTimestamp);
CREATE TABLE topgg_guild_whitelist(
appid TEXT,
guildid BIGINT,
PRIMARY KEY(appid, guildid)
);
-- }}}
-- Sponsor Data {{{
CREATE TABLE sponsor_guild_whitelist(
appid TEXT,
guildid BIGINT,
PRIMARY KEY(appid, guildid)
);
-- }}}
-- LionGem audit log {{{