Merge branch 'utils' into python-rewrite
This commit is contained in:
@@ -1,2 +1,2 @@
|
|||||||
CONFIG_FILE = "config/bot.conf"
|
CONFIG_FILE = "config/bot.conf"
|
||||||
DATA_VERSION = 3
|
DATA_VERSION = 4
|
||||||
|
|||||||
@@ -74,6 +74,7 @@ lions = RowTable(
|
|||||||
'workout_count', 'last_workout_start',
|
'workout_count', 'last_workout_start',
|
||||||
'last_study_badgeid',
|
'last_study_badgeid',
|
||||||
'video_warned',
|
'video_warned',
|
||||||
|
'_timestamp'
|
||||||
),
|
),
|
||||||
('guildid', 'userid'),
|
('guildid', 'userid'),
|
||||||
cache=TTLCache(5000, ttl=60*5),
|
cache=TTLCache(5000, ttl=60*5),
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import pytz
|
|||||||
|
|
||||||
from meta import client
|
from meta import client
|
||||||
from data import tables as tb
|
from data import tables as tb
|
||||||
from settings import UserSettings
|
from settings import UserSettings, GuildSettings
|
||||||
|
|
||||||
|
|
||||||
class Lion:
|
class Lion:
|
||||||
@@ -41,7 +41,13 @@ class Lion:
|
|||||||
if key in cls._lions:
|
if key in cls._lions:
|
||||||
return cls._lions[key]
|
return cls._lions[key]
|
||||||
else:
|
else:
|
||||||
tb.lions.fetch_or_create(key)
|
lion = tb.lions.fetch(key)
|
||||||
|
if not lion:
|
||||||
|
tb.lions.create_row(
|
||||||
|
guildid=guildid,
|
||||||
|
userid=userid,
|
||||||
|
coins=GuildSettings(guildid).starting_funds.value
|
||||||
|
)
|
||||||
return cls(guildid, userid)
|
return cls(guildid, userid)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ from typing import Dict
|
|||||||
from discord.utils import sleep_until
|
from discord.utils import sleep_until
|
||||||
|
|
||||||
from meta import client
|
from meta import client
|
||||||
|
from utils.interactive import discord_shield
|
||||||
from data import NULL, NOTNULL, tables
|
from data import NULL, NOTNULL, tables
|
||||||
from data.conditions import LEQ
|
from data.conditions import LEQ
|
||||||
from settings import GuildSettings
|
from settings import GuildSettings
|
||||||
@@ -460,3 +461,31 @@ async def unload_accountability(client):
|
|||||||
Save the current sessions and cancel the runloop in preparation for client shutdown.
|
Save the current sessions and cancel the runloop in preparation for client shutdown.
|
||||||
"""
|
"""
|
||||||
...
|
...
|
||||||
|
|
||||||
|
|
||||||
|
@client.add_after_event('member_join')
|
||||||
|
async def restore_accountability(client, member):
|
||||||
|
"""
|
||||||
|
Restore accountability channel permissions when a member rejoins the server, if applicable.
|
||||||
|
"""
|
||||||
|
aguild = AccountabilityGuild.cache.get(member.guild.id, None)
|
||||||
|
if aguild:
|
||||||
|
if aguild.current_slot and member.id in aguild.current_slot.members:
|
||||||
|
# Restore member permission for current slot
|
||||||
|
slot = aguild.current_slot
|
||||||
|
if slot.channel:
|
||||||
|
asyncio.create_task(discord_shield(
|
||||||
|
slot.channel.set_permissions(
|
||||||
|
member,
|
||||||
|
overwrite=slot._member_overwrite
|
||||||
|
)
|
||||||
|
))
|
||||||
|
if aguild.upcoming_slot and member.id in aguild.upcoming_slot.members:
|
||||||
|
slot = aguild.upcoming_slot
|
||||||
|
if slot.channel:
|
||||||
|
asyncio.create_task(discord_shield(
|
||||||
|
slot.channel.set_permissions(
|
||||||
|
member,
|
||||||
|
overwrite=slot._member_overwrite
|
||||||
|
)
|
||||||
|
))
|
||||||
|
|||||||
@@ -2,3 +2,4 @@ from .module import module
|
|||||||
|
|
||||||
from . import guild_config
|
from . import guild_config
|
||||||
from . import statreset
|
from . import statreset
|
||||||
|
from . import utils
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ from .module import module
|
|||||||
|
|
||||||
# Pages of configuration categories to display
|
# Pages of configuration categories to display
|
||||||
cat_pages = {
|
cat_pages = {
|
||||||
'Administration': ('Meta', 'Guild Roles'),
|
'Administration': ('Meta', 'Guild Roles', 'New Members'),
|
||||||
'Moderation': ('Moderation', 'Video Channels'),
|
'Moderation': ('Moderation', 'Video Channels'),
|
||||||
'Productivity': ('Study Tracking', 'TODO List', 'Workout'),
|
'Productivity': ('Study Tracking', 'TODO List', 'Workout'),
|
||||||
'Study Rooms': ('Rented Rooms', 'Accountability Rooms'),
|
'Study Rooms': ('Rented Rooms', 'Accountability Rooms'),
|
||||||
@@ -21,6 +21,7 @@ cat_pages = {
|
|||||||
descriptions = {
|
descriptions = {
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@module.cmd("config",
|
@module.cmd("config",
|
||||||
desc="View and modify the server settings.",
|
desc="View and modify the server settings.",
|
||||||
flags=('add', 'remove'),
|
flags=('add', 'remove'),
|
||||||
@@ -91,27 +92,27 @@ async def cmd_config(ctx, flags):
|
|||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
if len(parts) == 1:
|
if len(parts) == 1 and not ctx.msg.attachments:
|
||||||
# config <setting>
|
# config <setting>
|
||||||
# View config embed for provided setting
|
# View config embed for provided setting
|
||||||
await ctx.reply(embed=setting.get(ctx.guild.id).embed)
|
await setting.get(ctx.guild.id).widget(ctx, flags=flags)
|
||||||
else:
|
else:
|
||||||
# config <setting> <value>
|
# config <setting> <value>
|
||||||
# Check the write ward
|
# Check the write ward
|
||||||
if not await setting.write_ward.run(ctx):
|
if not await setting.write_ward.run(ctx):
|
||||||
await ctx.error_reply(setting.msg)
|
await ctx.error_reply(setting.write_ward.msg)
|
||||||
|
|
||||||
# Attempt to set config setting
|
# Attempt to set config setting
|
||||||
try:
|
try:
|
||||||
parsed = await setting.parse(ctx.guild.id, ctx, parts[1])
|
parsed = await setting.parse(ctx.guild.id, ctx, parts[1] if len(parts) > 1 else '')
|
||||||
parsed.write(add_only=flags['add'], remove_only=flags['remove'])
|
parsed.write(add_only=flags['add'], remove_only=flags['remove'])
|
||||||
except UserInputError as e:
|
except UserInputError as e:
|
||||||
await ctx.reply(embed=discord.Embed(
|
await ctx.reply(embed=discord.Embed(
|
||||||
description="{} {}".format('❌', e.msg),
|
description="{} {}".format('❌', e.msg),
|
||||||
Colour=discord.Colour.red()
|
colour=discord.Colour.red()
|
||||||
))
|
))
|
||||||
else:
|
else:
|
||||||
await ctx.reply(embed=discord.Embed(
|
await ctx.reply(embed=discord.Embed(
|
||||||
description="{} {}".format('✅', setting.get(ctx.guild.id).success_response),
|
description="{} {}".format('✅', setting.get(ctx.guild.id).success_response),
|
||||||
Colour=discord.Colour.green()
|
colour=discord.Colour.green()
|
||||||
))
|
))
|
||||||
|
|||||||
3
bot/modules/guild_admin/new_members/__init__.py
Normal file
3
bot/modules/guild_admin/new_members/__init__.py
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
from . import settings
|
||||||
|
from . import greetings
|
||||||
|
from . import roles
|
||||||
6
bot/modules/guild_admin/new_members/data.py
Normal file
6
bot/modules/guild_admin/new_members/data.py
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
from data import Table, RowTable
|
||||||
|
|
||||||
|
|
||||||
|
autoroles = Table('autoroles')
|
||||||
|
bot_autoroles = Table('bot_autoroles')
|
||||||
|
past_member_roles = Table('past_member_roles')
|
||||||
29
bot/modules/guild_admin/new_members/greetings.py
Normal file
29
bot/modules/guild_admin/new_members/greetings.py
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
import discord
|
||||||
|
from cmdClient.Context import Context
|
||||||
|
|
||||||
|
from meta import client
|
||||||
|
|
||||||
|
from .settings import greeting_message, greeting_channel, returning_message
|
||||||
|
|
||||||
|
|
||||||
|
@client.add_after_event('member_join')
|
||||||
|
async def send_greetings(client, member):
|
||||||
|
guild = member.guild
|
||||||
|
|
||||||
|
returning = bool(client.data.lions.fetch((guild.id, member.id)))
|
||||||
|
|
||||||
|
# Handle greeting message
|
||||||
|
channel = greeting_channel.get(guild.id).value
|
||||||
|
if channel is not None:
|
||||||
|
if channel == greeting_channel.DMCHANNEL:
|
||||||
|
channel = member
|
||||||
|
|
||||||
|
ctx = Context(client, guild=guild, author=member)
|
||||||
|
if returning:
|
||||||
|
args = returning_message.get(guild.id).args(ctx)
|
||||||
|
else:
|
||||||
|
args = greeting_message.get(guild.id).args(ctx)
|
||||||
|
try:
|
||||||
|
await channel.send(**args)
|
||||||
|
except discord.HTTPException:
|
||||||
|
pass
|
||||||
115
bot/modules/guild_admin/new_members/roles.py
Normal file
115
bot/modules/guild_admin/new_members/roles.py
Normal file
@@ -0,0 +1,115 @@
|
|||||||
|
import asyncio
|
||||||
|
import discord
|
||||||
|
from collections import defaultdict
|
||||||
|
|
||||||
|
from meta import client
|
||||||
|
from core import Lion
|
||||||
|
from settings import GuildSettings
|
||||||
|
|
||||||
|
from .settings import autoroles, bot_autoroles, role_persistence
|
||||||
|
from .data import past_member_roles
|
||||||
|
|
||||||
|
|
||||||
|
# Locks to avoid storing the roles while adding them
|
||||||
|
# The locking is cautious, leaving data unchanged upon collision
|
||||||
|
locks = defaultdict(asyncio.Lock)
|
||||||
|
|
||||||
|
|
||||||
|
@client.add_after_event('member_join')
|
||||||
|
async def join_role_tracker(client, member):
|
||||||
|
"""
|
||||||
|
Add autoroles or saved roles as needed.
|
||||||
|
"""
|
||||||
|
guild = member.guild
|
||||||
|
if not guild.me.guild_permissions.manage_roles:
|
||||||
|
# We can't manage the roles here, don't try to give/restore the member roles
|
||||||
|
return
|
||||||
|
|
||||||
|
async with locks[(guild.id, member.id)]:
|
||||||
|
if role_persistence.get(guild.id).value and client.data.lions.fetch((guild.id, member.id)):
|
||||||
|
# Lookup stored roles
|
||||||
|
role_rows = past_member_roles.select_where(
|
||||||
|
guildid=guild.id,
|
||||||
|
userid=member.id
|
||||||
|
)
|
||||||
|
# Identify roles from roleids
|
||||||
|
roles = (guild.get_role(row['roleid']) for row in role_rows)
|
||||||
|
# Remove non-existent roles
|
||||||
|
roles = (role for role in roles if role is not None)
|
||||||
|
# Remove roles the client can't add
|
||||||
|
roles = [role for role in roles if role < guild.me.top_role]
|
||||||
|
if roles:
|
||||||
|
try:
|
||||||
|
await member.add_roles(
|
||||||
|
*roles,
|
||||||
|
reason="Restoring saved roles.",
|
||||||
|
)
|
||||||
|
except discord.HTTPException:
|
||||||
|
# This shouldn't ususally happen, but there are valid cases where it can
|
||||||
|
# E.g. the user left while we were restoring their roles
|
||||||
|
pass
|
||||||
|
# Event log!
|
||||||
|
GuildSettings(guild.id).event_log.log(
|
||||||
|
"Restored the following roles for returning member {}:\n{}".format(
|
||||||
|
member.mention,
|
||||||
|
', '.join(role.mention for role in roles)
|
||||||
|
),
|
||||||
|
title="Saved roles restored"
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
# Add autoroles
|
||||||
|
roles = bot_autoroles.get(guild.id).value if member.bot else autoroles.get(guild.id).value
|
||||||
|
# Remove roles the client can't add
|
||||||
|
roles = [role for role in roles if role < guild.me.top_role]
|
||||||
|
if roles:
|
||||||
|
try:
|
||||||
|
await member.add_roles(
|
||||||
|
*roles,
|
||||||
|
reason="Adding autoroles.",
|
||||||
|
)
|
||||||
|
except discord.HTTPException:
|
||||||
|
# This shouldn't ususally happen, but there are valid cases where it can
|
||||||
|
# E.g. the user left while we were adding autoroles
|
||||||
|
pass
|
||||||
|
# Event log!
|
||||||
|
GuildSettings(guild.id).event_log.log(
|
||||||
|
"Gave {} the guild autoroles:\n{}".format(
|
||||||
|
member.mention,
|
||||||
|
', '.join(role.mention for role in roles)
|
||||||
|
),
|
||||||
|
titles="Autoroles added"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@client.add_after_event('member_remove')
|
||||||
|
async def left_role_tracker(client, member):
|
||||||
|
"""
|
||||||
|
Delete and re-store member roles when they leave the server.
|
||||||
|
"""
|
||||||
|
if (member.guild.id, member.id) in locks and locks[(member.guild.id, member.id)].locked():
|
||||||
|
# Currently processing a join event
|
||||||
|
# Which means the member left while we were adding their roles
|
||||||
|
# Cautiously return, not modifying the saved role data
|
||||||
|
return
|
||||||
|
|
||||||
|
# Delete existing member roles for this user
|
||||||
|
# NOTE: Not concurrency-safe
|
||||||
|
past_member_roles.delete_where(
|
||||||
|
guildid=member.guild.id,
|
||||||
|
userid=member.id,
|
||||||
|
)
|
||||||
|
if role_persistence.get(member.guild.id).value:
|
||||||
|
# Make sure the user has an associated lion, so we can detect when they rejoin
|
||||||
|
Lion.fetch(member.guild.id, member.id)
|
||||||
|
|
||||||
|
# Then insert the current member roles
|
||||||
|
values = [
|
||||||
|
(member.guild.id, member.id, role.id)
|
||||||
|
for role in member.roles
|
||||||
|
if not role.is_bot_managed() and not role.is_integration() and not role.is_default()
|
||||||
|
]
|
||||||
|
if values:
|
||||||
|
past_member_roles.insert_many(
|
||||||
|
*values,
|
||||||
|
insert_keys=('guildid', 'userid', 'roleid')
|
||||||
|
)
|
||||||
303
bot/modules/guild_admin/new_members/settings.py
Normal file
303
bot/modules/guild_admin/new_members/settings.py
Normal file
@@ -0,0 +1,303 @@
|
|||||||
|
import datetime
|
||||||
|
import discord
|
||||||
|
|
||||||
|
import settings
|
||||||
|
from settings import GuildSettings, GuildSetting
|
||||||
|
import settings.setting_types as stypes
|
||||||
|
from wards import guild_admin
|
||||||
|
|
||||||
|
from .data import autoroles, bot_autoroles
|
||||||
|
|
||||||
|
|
||||||
|
@GuildSettings.attach_setting
|
||||||
|
class greeting_channel(stypes.Channel, GuildSetting):
|
||||||
|
"""
|
||||||
|
Setting describing the destination of the greeting message.
|
||||||
|
|
||||||
|
Extended to support the following special values, with input and output supported.
|
||||||
|
Data `None` corresponds to `Off`.
|
||||||
|
Data `1` corresponds to `DM`.
|
||||||
|
"""
|
||||||
|
DMCHANNEL = object()
|
||||||
|
|
||||||
|
category = "New Members"
|
||||||
|
|
||||||
|
attr_name = 'greeting_channel'
|
||||||
|
_data_column = 'greeting_channel'
|
||||||
|
|
||||||
|
display_name = "greeting_channel"
|
||||||
|
desc = "Channel to send the greeting message in"
|
||||||
|
|
||||||
|
long_desc = (
|
||||||
|
"Channel to post the `greeting_message` in when a new user joins the server. "
|
||||||
|
"Accepts `DM` to indicate the greeting should be direct messaged to the new member."
|
||||||
|
)
|
||||||
|
_accepts = (
|
||||||
|
"Text Channel name/id/mention, or `DM`, or `None` to disable."
|
||||||
|
)
|
||||||
|
_chan_type = discord.ChannelType.text
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def _data_to_value(cls, id, data, **kwargs):
|
||||||
|
if data is None:
|
||||||
|
return None
|
||||||
|
elif data == 1:
|
||||||
|
return cls.DMCHANNEL
|
||||||
|
else:
|
||||||
|
return super()._data_to_value(id, data, **kwargs)
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def _data_from_value(cls, id, value, **kwargs):
|
||||||
|
if value is None:
|
||||||
|
return None
|
||||||
|
elif value == cls.DMCHANNEL:
|
||||||
|
return 1
|
||||||
|
else:
|
||||||
|
return super()._data_from_value(id, value, **kwargs)
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
async def _parse_userstr(cls, ctx, id, userstr, **kwargs):
|
||||||
|
lower = userstr.lower()
|
||||||
|
if lower in ('0', 'none', 'off'):
|
||||||
|
return None
|
||||||
|
elif lower == 'dm':
|
||||||
|
return 1
|
||||||
|
else:
|
||||||
|
return await super()._parse_userstr(ctx, id, userstr, **kwargs)
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def _format_data(cls, id, data, **kwargs):
|
||||||
|
if data is None:
|
||||||
|
return "Off"
|
||||||
|
elif data == 1:
|
||||||
|
return "DM"
|
||||||
|
else:
|
||||||
|
return "<#{}>".format(data)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def success_response(self):
|
||||||
|
value = self.value
|
||||||
|
if not value:
|
||||||
|
return "Greeting messages are disabled."
|
||||||
|
elif value == self.DMCHANNEL:
|
||||||
|
return "Greeting messages will be sent via direct message."
|
||||||
|
else:
|
||||||
|
return "Greeting messages will be posted in {}".format(self.formatted)
|
||||||
|
|
||||||
|
|
||||||
|
@GuildSettings.attach_setting
|
||||||
|
class greeting_message(stypes.Message, GuildSetting):
|
||||||
|
category = "New Members"
|
||||||
|
|
||||||
|
attr_name = 'greeting_message'
|
||||||
|
_data_column = 'greeting_message'
|
||||||
|
|
||||||
|
display_name = 'greeting_message'
|
||||||
|
desc = "Greeting message sent to welcome new members."
|
||||||
|
|
||||||
|
long_desc = (
|
||||||
|
"Message to send to the configured `greeting_channel` when a member joins the server for the first time."
|
||||||
|
)
|
||||||
|
|
||||||
|
_default = r"""
|
||||||
|
{
|
||||||
|
"embed": {
|
||||||
|
"title": "Welcome!",
|
||||||
|
"thumbnail": {"url": "{guild_icon}"},
|
||||||
|
"description": "Hi {mention}!\nWelcome to **{guild_name}**! You are the **{member_count}**th member.\nThere are currently **{studying_count}** people studying.\nGood luck and stay productive!",
|
||||||
|
"color": 15695665
|
||||||
|
}
|
||||||
|
}
|
||||||
|
"""
|
||||||
|
|
||||||
|
_substitution_desc = {
|
||||||
|
'{mention}': "Mention the new member.",
|
||||||
|
'{user_name}': "Username of the new member.",
|
||||||
|
'{user_avatar}': "Avatar of the new member.",
|
||||||
|
'{guild_name}': "Name of this server.",
|
||||||
|
'{guild_icon}': "Server icon url.",
|
||||||
|
'{member_count}': "Number of members in the server.",
|
||||||
|
'{studying_count}': "Number of current voice channel members.",
|
||||||
|
}
|
||||||
|
|
||||||
|
def substitution_keys(self, ctx, **kwargs):
|
||||||
|
return {
|
||||||
|
'{mention}': ctx.author.mention,
|
||||||
|
'{user_name}': ctx.author.name,
|
||||||
|
'{user_avatar}': str(ctx.author.avatar_url),
|
||||||
|
'{guild_name}': ctx.guild.name,
|
||||||
|
'{guild_icon}': str(ctx.guild.icon_url),
|
||||||
|
'{member_count}': str(len(ctx.guild.members)),
|
||||||
|
'{studying_count}': str(len([member for ch in ctx.guild.voice_channels for member in ch.members]))
|
||||||
|
}
|
||||||
|
|
||||||
|
@property
|
||||||
|
def success_response(self):
|
||||||
|
return "The greeting message has been set!"
|
||||||
|
|
||||||
|
|
||||||
|
@GuildSettings.attach_setting
|
||||||
|
class returning_message(stypes.Message, GuildSetting):
|
||||||
|
category = "New Members"
|
||||||
|
|
||||||
|
attr_name = 'returning_message'
|
||||||
|
_data_column = 'returning_message'
|
||||||
|
|
||||||
|
display_name = 'returning_message'
|
||||||
|
desc = "Greeting message sent to returning members."
|
||||||
|
|
||||||
|
long_desc = (
|
||||||
|
"Message to send to the configured `greeting_channel` when a member returns to the server."
|
||||||
|
)
|
||||||
|
|
||||||
|
_default = r"""
|
||||||
|
{
|
||||||
|
"embed": {
|
||||||
|
"title": "Welcome Back {user_name}!",
|
||||||
|
"thumbnail": {"url": "{guild_icon}"},
|
||||||
|
"description": "Welcome back to **{guild_name}**!\nYou last studied with us <t:{last_time}:R>.\nThere are currently **{studying_count}** people studying.\nGood luck and stay productive!",
|
||||||
|
"color": 15695665
|
||||||
|
}
|
||||||
|
}
|
||||||
|
"""
|
||||||
|
|
||||||
|
_substitution_desc = {
|
||||||
|
'{mention}': "Mention the returning member.",
|
||||||
|
'{user_name}': "Username of the member.",
|
||||||
|
'{user_avatar}': "Avatar of the member.",
|
||||||
|
'{guild_name}': "Name of this server.",
|
||||||
|
'{guild_icon}': "Server icon url.",
|
||||||
|
'{member_count}': "Number of members in the server.",
|
||||||
|
'{studying_count}': "Number of current voice channel members.",
|
||||||
|
'{last_time}': "Unix timestamp of the last time the member studied.",
|
||||||
|
}
|
||||||
|
|
||||||
|
def substitution_keys(self, ctx, **kwargs):
|
||||||
|
return {
|
||||||
|
'{mention}': ctx.author.mention,
|
||||||
|
'{user_name}': ctx.author.name,
|
||||||
|
'{user_avatar}': str(ctx.author.avatar_url),
|
||||||
|
'{guild_name}': ctx.guild.name,
|
||||||
|
'{guild_icon}': str(ctx.guild.icon_url),
|
||||||
|
'{member_count}': str(len(ctx.guild.members)),
|
||||||
|
'{studying_count}': str(len([member for ch in ctx.guild.voice_channels for member in ch.members])),
|
||||||
|
'{last_time}': int(ctx.alion.data._timestamp.replace(tzinfo=datetime.timezone.utc).timestamp()),
|
||||||
|
}
|
||||||
|
|
||||||
|
@property
|
||||||
|
def success_response(self):
|
||||||
|
return "The returning message has been set!"
|
||||||
|
|
||||||
|
|
||||||
|
@GuildSettings.attach_setting
|
||||||
|
class starting_funds(stypes.Integer, GuildSetting):
|
||||||
|
category = "New Members"
|
||||||
|
|
||||||
|
attr_name = 'starting_funds'
|
||||||
|
_data_column = 'starting_funds'
|
||||||
|
|
||||||
|
display_name = 'starting_funds'
|
||||||
|
desc = "Coins given when a user first joins."
|
||||||
|
|
||||||
|
long_desc = (
|
||||||
|
"Members will be given this number of coins the first time they join the server."
|
||||||
|
)
|
||||||
|
|
||||||
|
_default = 0
|
||||||
|
|
||||||
|
@property
|
||||||
|
def success_response(self):
|
||||||
|
return "Members will be given `{}` coins when they first join the server.".format(self.formatted)
|
||||||
|
|
||||||
|
|
||||||
|
@GuildSettings.attach_setting
|
||||||
|
class autoroles(stypes.RoleList, settings.ListData, settings.Setting):
|
||||||
|
category = "New Members"
|
||||||
|
write_ward = guild_admin
|
||||||
|
|
||||||
|
attr_name = 'autoroles'
|
||||||
|
|
||||||
|
_table_interface = autoroles
|
||||||
|
_id_column = 'guildid'
|
||||||
|
_data_column = 'roleid'
|
||||||
|
|
||||||
|
display_name = "autoroles"
|
||||||
|
desc = "Roles to give automatically to new members."
|
||||||
|
|
||||||
|
_force_unique = True
|
||||||
|
|
||||||
|
long_desc = (
|
||||||
|
"These roles will be given automatically to users when they join the server. "
|
||||||
|
"If `role_persistence` is enabled, the roles will only be given the first time a user joins the server."
|
||||||
|
)
|
||||||
|
|
||||||
|
# Flat cache, no need to expire
|
||||||
|
_cache = {}
|
||||||
|
|
||||||
|
@property
|
||||||
|
def success_response(self):
|
||||||
|
if self.value:
|
||||||
|
return "New members will be given the following roles:\n{}".format(self.formatted)
|
||||||
|
else:
|
||||||
|
return "New members will not automatically be given any roles."
|
||||||
|
|
||||||
|
|
||||||
|
@GuildSettings.attach_setting
|
||||||
|
class bot_autoroles(stypes.RoleList, settings.ListData, settings.Setting):
|
||||||
|
category = "New Members"
|
||||||
|
write_ward = guild_admin
|
||||||
|
|
||||||
|
attr_name = 'bot_autoroles'
|
||||||
|
|
||||||
|
_table_interface = bot_autoroles
|
||||||
|
_id_column = 'guildid'
|
||||||
|
_data_column = 'roleid'
|
||||||
|
|
||||||
|
display_name = "bot_autoroles"
|
||||||
|
desc = "Roles to give automatically to new bots."
|
||||||
|
|
||||||
|
_force_unique = True
|
||||||
|
|
||||||
|
long_desc = (
|
||||||
|
"These roles will be given automatically to bots when they join the server. "
|
||||||
|
"If `role_persistence` is enabled, the roles will only be given the first time a bot joins the server."
|
||||||
|
)
|
||||||
|
|
||||||
|
# Flat cache, no need to expire
|
||||||
|
_cache = {}
|
||||||
|
|
||||||
|
@property
|
||||||
|
def success_response(self):
|
||||||
|
if self.value:
|
||||||
|
return "New bots will be given the following roles:\n{}".format(self.formatted)
|
||||||
|
else:
|
||||||
|
return "New bots will not automatically be given any roles."
|
||||||
|
|
||||||
|
|
||||||
|
@GuildSettings.attach_setting
|
||||||
|
class role_persistence(stypes.Boolean, GuildSetting):
|
||||||
|
category = "New Members"
|
||||||
|
|
||||||
|
attr_name = "role_persistence"
|
||||||
|
|
||||||
|
_data_column = 'persist_roles'
|
||||||
|
|
||||||
|
display_name = "role_persistence"
|
||||||
|
desc = "Whether to remember member roles when they leave the server."
|
||||||
|
_outputs = {True: "Enabled", False: "Disabled"}
|
||||||
|
_default = True
|
||||||
|
|
||||||
|
long_desc = (
|
||||||
|
"When enabled, restores member roles when they rejoin the server.\n"
|
||||||
|
"This enables profile roles and purchased roles, such as field of study and colour roles, "
|
||||||
|
"as well as moderation roles, "
|
||||||
|
"such as the studyban and mute roles, to persist even when a member leaves and rejoins.\n"
|
||||||
|
"Note: Members who leave while this is disabled will not have their roles restored."
|
||||||
|
)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def success_response(self):
|
||||||
|
if self.value:
|
||||||
|
return "Roles will now be restored when a member rejoins."
|
||||||
|
else:
|
||||||
|
return "Member roles will no longer be saved or restored."
|
||||||
@@ -243,7 +243,7 @@ class Room:
|
|||||||
guild_settings.event_log.log(
|
guild_settings.event_log.log(
|
||||||
title="Failed to update study room permissions!",
|
title="Failed to update study room permissions!",
|
||||||
description=("An error occured while removing the "
|
description=("An error occured while removing the "
|
||||||
"following members from the private room {}.\n{}").format(
|
"following members from the private room {}.\n{}").format(
|
||||||
self.channel.mention,
|
self.channel.mention,
|
||||||
', '.join(member.mention for member in members)
|
', '.join(member.mention for member in members)
|
||||||
),
|
),
|
||||||
@@ -282,3 +282,38 @@ async def load_rented_rooms(client):
|
|||||||
"Loaded {} private study channels.".format(len(rows)),
|
"Loaded {} private study channels.".format(len(rows)),
|
||||||
context="LOAD_RENTED_ROOMS"
|
context="LOAD_RENTED_ROOMS"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@client.add_after_event('member_join')
|
||||||
|
async def restore_room_permission(client, member):
|
||||||
|
"""
|
||||||
|
If a member has, or is part of, a private room when they rejoin, restore their permissions.
|
||||||
|
"""
|
||||||
|
# First check whether they own a room
|
||||||
|
owned = Room.fetch(member.guild.id, member.id)
|
||||||
|
if owned and owned.channel:
|
||||||
|
# Restore their room permissions
|
||||||
|
try:
|
||||||
|
await owned.channel.set_permissions(
|
||||||
|
member,
|
||||||
|
overwrite=Room.owner_overwrite
|
||||||
|
)
|
||||||
|
except discord.HTTPException:
|
||||||
|
pass
|
||||||
|
|
||||||
|
# Then check if they are in any other rooms
|
||||||
|
in_room_rows = rented_members.select_where(
|
||||||
|
_extra="LEFT JOIN rented USING (channelid) WHERE userid={} AND guildid={}".format(
|
||||||
|
member.id, member.guild.id
|
||||||
|
)
|
||||||
|
)
|
||||||
|
for row in in_room_rows:
|
||||||
|
room = Room.fetch(member.guild.id, row['ownerid'])
|
||||||
|
if room and row['ownerid'] != member.id and room.channel:
|
||||||
|
try:
|
||||||
|
await room.channel.set_permissions(
|
||||||
|
member,
|
||||||
|
overwrite=Room.member_overwrite
|
||||||
|
)
|
||||||
|
except discord.HTTPException:
|
||||||
|
pass
|
||||||
|
|||||||
@@ -51,6 +51,14 @@ class Setting:
|
|||||||
embed.description = "{}\n{}".format(self.long_desc.format(self=self, client=self.client), table)
|
embed.description = "{}\n{}".format(self.long_desc.format(self=self, client=self.client), table)
|
||||||
return embed
|
return embed
|
||||||
|
|
||||||
|
async def widget(self, ctx: Context, **kwargs):
|
||||||
|
"""
|
||||||
|
Show the setting widget for this setting.
|
||||||
|
By default this displays the setting embed.
|
||||||
|
Settings may override this if they need more complex widget context or logic.
|
||||||
|
"""
|
||||||
|
return await ctx.reply(embed=self.embed)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def summary(self):
|
def summary(self):
|
||||||
"""
|
"""
|
||||||
|
|||||||
@@ -1,5 +1,8 @@
|
|||||||
|
import json
|
||||||
|
import asyncio
|
||||||
import datetime
|
import datetime
|
||||||
import itertools
|
import itertools
|
||||||
|
from io import StringIO
|
||||||
from enum import IntEnum
|
from enum import IntEnum
|
||||||
from typing import Any, Optional
|
from typing import Any, Optional
|
||||||
|
|
||||||
@@ -9,11 +12,14 @@ from cmdClient.Context import Context
|
|||||||
from cmdClient.lib import SafeCancellation
|
from cmdClient.lib import SafeCancellation
|
||||||
|
|
||||||
from meta import client
|
from meta import client
|
||||||
from utils.lib import parse_dur, strfdur, strfdelta
|
from utils.lib import parse_dur, strfdur, strfdelta, prop_tabulate, multiple_replace
|
||||||
|
|
||||||
from .base import UserInputError
|
from .base import UserInputError
|
||||||
|
|
||||||
|
|
||||||
|
preview_emoji = '🔍'
|
||||||
|
|
||||||
|
|
||||||
class SettingType:
|
class SettingType:
|
||||||
"""
|
"""
|
||||||
Abstract class representing a setting type.
|
Abstract class representing a setting type.
|
||||||
@@ -672,6 +678,209 @@ class Duration(SettingType):
|
|||||||
return "`{}`".format(strfdelta(datetime.timedelta(seconds=data)))
|
return "`{}`".format(strfdelta(datetime.timedelta(seconds=data)))
|
||||||
|
|
||||||
|
|
||||||
|
class Message(SettingType):
|
||||||
|
"""
|
||||||
|
Message type storing json-encoded message arguments.
|
||||||
|
Messages without an embed are displayed differently from those with an embed.
|
||||||
|
|
||||||
|
Types:
|
||||||
|
data: str
|
||||||
|
A json dictionary with the fields `content` and `embed`.
|
||||||
|
value: dict
|
||||||
|
An argument dictionary suitable for `Message.send` or `Message.edit`.
|
||||||
|
"""
|
||||||
|
|
||||||
|
_substitution_desc = {
|
||||||
|
}
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def _data_from_value(cls, id, value, **kwargs):
|
||||||
|
if value is None:
|
||||||
|
return None
|
||||||
|
|
||||||
|
return json.dumps(value)
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def _data_to_value(cls, id, data, **kwargs):
|
||||||
|
if data is None:
|
||||||
|
return None
|
||||||
|
|
||||||
|
return json.loads(data)
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
async def parse(cls, id: int, ctx: Context, userstr: str, **kwargs):
|
||||||
|
"""
|
||||||
|
Return a setting instance initialised from a parsed user string.
|
||||||
|
"""
|
||||||
|
if ctx.msg.attachments:
|
||||||
|
attachment = ctx.msg.attachments[0]
|
||||||
|
if 'text' in attachment.content_type or 'json' in attachment.content_type:
|
||||||
|
userstr = (await attachment.read()).decode()
|
||||||
|
data = await cls._parse_userstr(ctx, id, userstr, as_json=True, **kwargs)
|
||||||
|
else:
|
||||||
|
raise UserInputError("Can't read the attached file!")
|
||||||
|
else:
|
||||||
|
data = await cls._parse_userstr(ctx, id, userstr, **kwargs)
|
||||||
|
return cls(id, data, **kwargs)
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
async def _parse_userstr(cls, ctx, id, userstr, as_json=False, **kwargs):
|
||||||
|
"""
|
||||||
|
Parse the provided string as either a content-only string, or json-format arguments.
|
||||||
|
Provided string is not trusted, and is parsed in a safe manner.
|
||||||
|
"""
|
||||||
|
if userstr.lower() == 'none':
|
||||||
|
return None
|
||||||
|
|
||||||
|
if as_json:
|
||||||
|
try:
|
||||||
|
args = json.loads(userstr)
|
||||||
|
except json.JSONDecodeError:
|
||||||
|
raise UserInputError(
|
||||||
|
"Couldn't parse your message! "
|
||||||
|
"You can test and fix it on the embed builder "
|
||||||
|
"[here](https://glitchii.github.io/embedbuilder/?editor=json)."
|
||||||
|
)
|
||||||
|
if 'embed' in args and 'timestamp' in args['embed']:
|
||||||
|
args['embed'].pop('timestamp')
|
||||||
|
return json.dumps(args)
|
||||||
|
else:
|
||||||
|
return json.dumps({'content': userstr})
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def _format_data(cls, id, data, **kwargs):
|
||||||
|
if data is None:
|
||||||
|
return "Empty"
|
||||||
|
value = cls._data_to_value(id, data, **kwargs)
|
||||||
|
if 'embed' not in value and len(value['content']) < 100:
|
||||||
|
return "`{}`".format(value['content'])
|
||||||
|
else:
|
||||||
|
return "Too long to display here!"
|
||||||
|
|
||||||
|
def substitution_keys(self, ctx, **kwargs):
|
||||||
|
"""
|
||||||
|
Instances should override this to provide their own substitution implementation.
|
||||||
|
"""
|
||||||
|
return {}
|
||||||
|
|
||||||
|
def args(self, ctx, **kwargs):
|
||||||
|
"""
|
||||||
|
Applies the substitutions with the given context to generate the final message args.
|
||||||
|
"""
|
||||||
|
value = self.value
|
||||||
|
substitutions = self.substitution_keys(ctx, **kwargs)
|
||||||
|
args = {}
|
||||||
|
if 'content' in value:
|
||||||
|
args['content'] = multiple_replace(value['content'], substitutions)
|
||||||
|
if 'embed' in value:
|
||||||
|
args['embed'] = discord.Embed.from_dict(
|
||||||
|
json.loads(multiple_replace(json.dumps(value['embed']), substitutions))
|
||||||
|
)
|
||||||
|
return args
|
||||||
|
|
||||||
|
async def widget(self, ctx, **kwargs):
|
||||||
|
value = self.value
|
||||||
|
args = self.args(ctx, **kwargs)
|
||||||
|
|
||||||
|
if not value:
|
||||||
|
return await ctx.reply(embed=self.embed)
|
||||||
|
|
||||||
|
current_str = None
|
||||||
|
preview = None
|
||||||
|
file_content = None
|
||||||
|
if 'embed' in value or len(value['content']) > 1024:
|
||||||
|
current_str = "See attached file."
|
||||||
|
file_content = json.dumps(value, indent=4)
|
||||||
|
elif "`" in value['content']:
|
||||||
|
current_str = "```{}```".format(value['content'])
|
||||||
|
if len(args['content']) < 1000:
|
||||||
|
preview = args['content']
|
||||||
|
else:
|
||||||
|
current_str = "`{}`".format(value['content'])
|
||||||
|
if len(args['content']) < 1000:
|
||||||
|
preview = args['content']
|
||||||
|
|
||||||
|
description = "{}\n\n**Current Value**: {}".format(
|
||||||
|
self.long_desc.format(self=self, client=self.client),
|
||||||
|
current_str
|
||||||
|
)
|
||||||
|
|
||||||
|
embed = discord.Embed(
|
||||||
|
title="Configuration options for `{}`".format(self.display_name),
|
||||||
|
description=description
|
||||||
|
)
|
||||||
|
if preview:
|
||||||
|
embed.add_field(name="Message Preview", value=preview, inline=False)
|
||||||
|
embed.add_field(
|
||||||
|
name="Setting Guide",
|
||||||
|
value=(
|
||||||
|
"• For plain text without an embed, use `{prefix}config {setting} <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`."
|
||||||
|
).format(
|
||||||
|
prefix=ctx.best_prefix,
|
||||||
|
setting=self.display_name,
|
||||||
|
builder="https://glitchii.github.io/embedbuilder/?editor=gui"
|
||||||
|
),
|
||||||
|
inline=False
|
||||||
|
)
|
||||||
|
if self._substitution_desc:
|
||||||
|
embed.add_field(
|
||||||
|
name="Substitution Keys",
|
||||||
|
value=(
|
||||||
|
"*The following keys will be substituted for their current values.*\n{}"
|
||||||
|
).format(
|
||||||
|
prop_tabulate(*zip(*self._substitution_desc.items()), colon=False)
|
||||||
|
),
|
||||||
|
inline=False
|
||||||
|
)
|
||||||
|
embed.set_footer(
|
||||||
|
text="React with {} to preview the message.".format(preview_emoji)
|
||||||
|
)
|
||||||
|
if file_content:
|
||||||
|
with StringIO() as message_file:
|
||||||
|
message_file.write(file_content)
|
||||||
|
message_file.seek(0)
|
||||||
|
out_file = discord.File(message_file, filename="{}.json".format(self.display_name))
|
||||||
|
out_msg = await ctx.reply(embed=embed, file=out_file)
|
||||||
|
else:
|
||||||
|
out_msg = await ctx.reply(embed=embed)
|
||||||
|
|
||||||
|
# Add the preview reaction and send the preview when requested
|
||||||
|
try:
|
||||||
|
await out_msg.add_reaction(preview_emoji)
|
||||||
|
except discord.HTTPException:
|
||||||
|
return
|
||||||
|
|
||||||
|
try:
|
||||||
|
await ctx.client.wait_for(
|
||||||
|
'reaction_add',
|
||||||
|
check=lambda r, u: r.message.id == out_msg.id and r.emoji == preview_emoji and u == ctx.author,
|
||||||
|
timeout=180
|
||||||
|
)
|
||||||
|
except asyncio.TimeoutError:
|
||||||
|
try:
|
||||||
|
await out_msg.remove_reaction(preview_emoji, ctx.client.user)
|
||||||
|
except discord.HTTPException:
|
||||||
|
pass
|
||||||
|
else:
|
||||||
|
try:
|
||||||
|
await ctx.offer_delete(
|
||||||
|
await ctx.reply(**args, allowed_mentions=discord.AllowedMentions.none())
|
||||||
|
)
|
||||||
|
except discord.HTTPException as e:
|
||||||
|
await ctx.reply(
|
||||||
|
embed=discord.Embed(
|
||||||
|
colour=discord.Colour.red(),
|
||||||
|
title="Preview failed! Error below",
|
||||||
|
description="```{}```".format(
|
||||||
|
e
|
||||||
|
)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
class SettingList(SettingType):
|
class SettingList(SettingType):
|
||||||
"""
|
"""
|
||||||
List of a particular type of setting.
|
List of a particular type of setting.
|
||||||
|
|||||||
@@ -17,7 +17,7 @@ tick = '✅'
|
|||||||
cross = '❌'
|
cross = '❌'
|
||||||
|
|
||||||
|
|
||||||
def prop_tabulate(prop_list, value_list, indent=True):
|
def prop_tabulate(prop_list, value_list, indent=True, colon=True):
|
||||||
"""
|
"""
|
||||||
Turns a list of properties and corresponding list of values into
|
Turns a list of properties and corresponding list of values into
|
||||||
a pretty string with one `prop: value` pair each line,
|
a pretty string with one `prop: value` pair each line,
|
||||||
@@ -39,7 +39,7 @@ def prop_tabulate(prop_list, value_list, indent=True):
|
|||||||
max_len = max(len(prop) for prop in prop_list)
|
max_len = max(len(prop) for prop in prop_list)
|
||||||
return "".join(["`{}{}{}`\t{}{}".format(" " * (max_len - len(prop)) if indent else "",
|
return "".join(["`{}{}{}`\t{}{}".format(" " * (max_len - len(prop)) if indent else "",
|
||||||
prop,
|
prop,
|
||||||
":" if len(prop) else " " * 2,
|
(":" if len(prop) else " " * 2) if colon else '',
|
||||||
value_list[i],
|
value_list[i],
|
||||||
'' if str(value_list[i]).endswith("```") else '\n')
|
'' if str(value_list[i]).endswith("```") else '\n')
|
||||||
for i, prop in enumerate(prop_list)])
|
for i, prop in enumerate(prop_list)])
|
||||||
@@ -528,3 +528,14 @@ def utc_now():
|
|||||||
Return the current timezone-aware utc timestamp.
|
Return the current timezone-aware utc timestamp.
|
||||||
"""
|
"""
|
||||||
return datetime.datetime.utcnow().replace(tzinfo=datetime.timezone.utc)
|
return datetime.datetime.utcnow().replace(tzinfo=datetime.timezone.utc)
|
||||||
|
|
||||||
|
|
||||||
|
def multiple_replace(string, rep_dict):
|
||||||
|
if rep_dict:
|
||||||
|
pattern = re.compile(
|
||||||
|
"|".join([re.escape(k) for k in sorted(rep_dict, key=len, reverse=True)]),
|
||||||
|
flags=re.DOTALL
|
||||||
|
)
|
||||||
|
return pattern.sub(lambda x: str(rep_dict[x.group(0)]), string)
|
||||||
|
else:
|
||||||
|
return string
|
||||||
|
|||||||
31
data/migration/v3-v4/migration.sql
Normal file
31
data/migration/v3-v4/migration.sql
Normal file
@@ -0,0 +1,31 @@
|
|||||||
|
ALTER TABLE guild_config
|
||||||
|
ADD COLUMN greeting_channel BIGINT,
|
||||||
|
ADD COLUMN greeting_message TEXT,
|
||||||
|
ADD COLUMN returning_message TEXT,
|
||||||
|
ADD COLUMN starting_funds INTEGER,
|
||||||
|
ADD COLUMN persist_roles BOOLEAN;
|
||||||
|
|
||||||
|
CREATE INDEX rented_members_users ON rented_members (userid);
|
||||||
|
|
||||||
|
CREATE TABLE autoroles(
|
||||||
|
guildid BIGINT NOT NULL ,
|
||||||
|
roleid BIGINT NOT NULL
|
||||||
|
);
|
||||||
|
CREATE INDEX autoroles_guilds ON autoroles (guildid);
|
||||||
|
|
||||||
|
CREATE TABLE bot_autoroles(
|
||||||
|
guildid BIGINT NOT NULL ,
|
||||||
|
roleid BIGINT NOT NULL
|
||||||
|
);
|
||||||
|
CREATE INDEX bot_autoroles_guilds ON bot_autoroles (guildid);
|
||||||
|
|
||||||
|
CREATE TABLE past_member_roles(
|
||||||
|
guildid BIGINT NOT NULL,
|
||||||
|
userid BIGINT NOT NULL,
|
||||||
|
roleid BIGINT NOT NULL,
|
||||||
|
_timestamp TIMESTAMPTZ DEFAULT now(),
|
||||||
|
FOREIGN KEY (guildid, userid) REFERENCES members (guildid, userid)
|
||||||
|
);
|
||||||
|
CREATE INDEX member_role_persistence_members ON past_member_roles (guildid, userid);
|
||||||
|
|
||||||
|
INSERT INTO VersionHistory (version, author) VALUES (4, 'v3-v4 Migration');
|
||||||
@@ -4,7 +4,7 @@ CREATE TABLE VersionHistory(
|
|||||||
time TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP NOT NULL,
|
time TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP NOT NULL,
|
||||||
author TEXT
|
author TEXT
|
||||||
);
|
);
|
||||||
INSERT INTO VersionHistory (version, author) VALUES (3, 'Initial Creation');
|
INSERT INTO VersionHistory (version, author) VALUES (4, 'Initial Creation');
|
||||||
|
|
||||||
|
|
||||||
CREATE OR REPLACE FUNCTION update_timestamp_column()
|
CREATE OR REPLACE FUNCTION update_timestamp_column()
|
||||||
@@ -72,7 +72,12 @@ CREATE TABLE guild_config(
|
|||||||
accountability_reward INTEGER,
|
accountability_reward INTEGER,
|
||||||
accountability_price INTEGER,
|
accountability_price INTEGER,
|
||||||
video_studyban BOOLEAN,
|
video_studyban BOOLEAN,
|
||||||
video_grace_period INTEGER
|
video_grace_period INTEGER,
|
||||||
|
greeting_channel BIGINT,
|
||||||
|
greeting_message TEXT,
|
||||||
|
returning_message TEXT,
|
||||||
|
starting_funds INTEGER,
|
||||||
|
persist_roles BOOLEAN
|
||||||
);
|
);
|
||||||
|
|
||||||
CREATE TABLE ignored_members(
|
CREATE TABLE ignored_members(
|
||||||
@@ -92,6 +97,18 @@ CREATE TABLE donator_roles(
|
|||||||
roleid BIGINT NOT NULL
|
roleid BIGINT NOT NULL
|
||||||
);
|
);
|
||||||
CREATE INDEX donator_roles_guilds ON donator_roles (guildid);
|
CREATE INDEX donator_roles_guilds ON donator_roles (guildid);
|
||||||
|
|
||||||
|
CREATE TABLE autoroles(
|
||||||
|
guildid BIGINT NOT NULL,
|
||||||
|
roleid BIGINT NOT NULL
|
||||||
|
);
|
||||||
|
CREATE INDEX autoroles_guilds ON autoroles (guildid);
|
||||||
|
|
||||||
|
CREATE TABLE bot_autoroles(
|
||||||
|
guildid BIGINT NOT NULL ,
|
||||||
|
roleid BIGINT NOT NULL
|
||||||
|
);
|
||||||
|
CREATE INDEX bot_autoroles_guilds ON bot_autoroles (guildid);
|
||||||
-- }}}
|
-- }}}
|
||||||
|
|
||||||
-- Workout data {{{
|
-- Workout data {{{
|
||||||
@@ -434,6 +451,7 @@ CREATE TABLE rented_members(
|
|||||||
userid BIGINT NOT NULL
|
userid BIGINT NOT NULL
|
||||||
);
|
);
|
||||||
CREATE INDEX rented_members_channels ON rented_members (channelid);
|
CREATE INDEX rented_members_channels ON rented_members (channelid);
|
||||||
|
CREATE INDEX rented_members_users ON rented_members (userid);
|
||||||
-- }}}
|
-- }}}
|
||||||
|
|
||||||
-- Accountability Rooms {{{
|
-- Accountability Rooms {{{
|
||||||
@@ -473,4 +491,15 @@ CREATE VIEW accountability_open_slots AS
|
|||||||
WHERE closed_at IS NULL
|
WHERE closed_at IS NULL
|
||||||
ORDER BY start_at ASC;
|
ORDER BY start_at ASC;
|
||||||
-- }}}
|
-- }}}
|
||||||
|
|
||||||
|
-- Member Role Data {{{
|
||||||
|
CREATE TABLE past_member_roles(
|
||||||
|
guildid BIGINT NOT NULL,
|
||||||
|
userid BIGINT NOT NULL,
|
||||||
|
roleid BIGINT NOT NULL,
|
||||||
|
_timestamp TIMESTAMPTZ DEFAULT now(),
|
||||||
|
FOREIGN KEY (guildid, userid) REFERENCES members (guildid, userid)
|
||||||
|
);
|
||||||
|
CREATE INDEX member_role_persistence_members ON past_member_roles (guildid, userid);
|
||||||
|
-- }}}
|
||||||
-- vim: set fdm=marker:
|
-- vim: set fdm=marker:
|
||||||
|
|||||||
Reference in New Issue
Block a user