Merge pull request #3 from StudyLions/staging

Bugfixes and minor UI improvements
This commit is contained in:
Interitio
2021-10-26 09:04:23 +03:00
committed by GitHub
15 changed files with 201 additions and 41 deletions

View File

@@ -1,4 +1,9 @@
from cmdClient import Command, Module
import asyncio
import traceback
import logging
import discord
from cmdClient import Command, Module, FailedCheck
from cmdClient.lib import SafeCancellation
from meta import log
@@ -79,3 +84,87 @@ class LionModule(Module):
# Check guild's own member blacklist
if ctx.author.id in ctx.client.objects['ignored_members'][ctx.guild.id]:
raise SafeCancellation
# Check channel permissions are sane
if not ctx.ch.permissions_for(ctx.guild.me).send_messages:
raise SafeCancellation
if not ctx.ch.permissions_for(ctx.guild.me).embed_links:
await ctx.reply("I need permission to send embeds in this channel before I can run any commands!")
raise SafeCancellation
# Start typing
await ctx.ch.trigger_typing()
async def on_exception(self, ctx, exception):
try:
raise exception
except (FailedCheck, SafeCancellation):
# cmdClient generated and handled exceptions
raise exception
except (asyncio.CancelledError, asyncio.TimeoutError):
# Standard command and task exceptions, cmdClient will also handle these
raise exception
except discord.Forbidden:
# Unknown uncaught Forbidden
try:
# Attempt a general error reply
await ctx.reply("I don't have enough channel or server permissions to complete that command here!")
except discord.Forbidden:
# We can't send anything at all. Exit quietly, but log.
full_traceback = traceback.format_exc()
log(("Caught an unhandled 'Forbidden' while "
"executing command '{cmdname}' from module '{module}' "
"from user '{message.author}' (uid:{message.author.id}) "
"in guild '{message.guild}' (gid:{guildid}) "
"in channel '{message.channel}' (cid:{message.channel.id}).\n"
"Message Content:\n"
"{content}\n"
"{traceback}\n\n"
"{flat_ctx}").format(
cmdname=ctx.cmd.name,
module=ctx.cmd.module.name,
message=ctx.msg,
guildid=ctx.guild.id if ctx.guild else None,
content='\n'.join('\t' + line for line in ctx.msg.content.splitlines()),
traceback=full_traceback,
flat_ctx=ctx.flatten()
),
context="mid:{}".format(ctx.msg.id),
level=logging.WARNING)
except Exception as e:
# Unknown exception!
full_traceback = traceback.format_exc()
only_error = "".join(traceback.TracebackException.from_exception(e).format_exception_only())
log(("Caught an unhandled exception while "
"executing command '{cmdname}' from module '{module}' "
"from user '{message.author}' (uid:{message.author.id}) "
"in guild '{message.guild}' (gid:{guildid}) "
"in channel '{message.channel}' (cid:{message.channel.id}).\n"
"Message Content:\n"
"{content}\n"
"{traceback}\n\n"
"{flat_ctx}").format(
cmdname=ctx.cmd.name,
module=ctx.cmd.module.name,
message=ctx.msg,
guildid=ctx.guild.id if ctx.guild else None,
content='\n'.join('\t' + line for line in ctx.msg.content.splitlines()),
traceback=full_traceback,
flat_ctx=ctx.flatten()
),
context="mid:{}".format(ctx.msg.id),
level=logging.ERROR)
error_embed = discord.Embed(title="Something went wrong!")
error_embed.description = (
"An unexpected error occurred while processing your command!\n"
"Our development team has been notified, and the issue should be fixed soon.\n"
)
if logging.getLogger().getEffectiveLevel() < logging.INFO:
error_embed.add_field(
name="Exception",
value="`{}`".format(only_error)
)
await ctx.reply(embed=error_embed)

View File

@@ -10,3 +10,4 @@ from .reminders import *
from .renting import *
from .moderation import *
from .accountability import *
from .plugins import *

View File

@@ -409,12 +409,13 @@ class TimeSlot:
if self.channel:
try:
await self.channel.delete()
self.channel = None
except discord.HTTPException:
pass
if self.message:
try:
timestamp = self.start_time.timestamp()
timestamp = int(self.start_time.timestamp())
embed = discord.Embed(
title="Session <t:{}:t> - <t:{}:t>".format(
timestamp, timestamp + 3600

View File

@@ -1,3 +1,4 @@
import asyncio
import discord
import settings
@@ -37,12 +38,12 @@ class accountability_category(settings.Channel, settings.GuildSetting):
return "The accountability system has been started in **{}**.".format(self.value.name)
else:
if self.id in AG.cache:
aguild = AG.cache[self.id]
aguild = AG.cache.pop(self.id)
if aguild.current_slot:
aguild.current_lost.cancel()
asyncio.create_task(aguild.current_slot.cancel())
if aguild.upcoming_slot:
aguild.upcoming_slot.cancel()
return "The accountability system has been stopped."
asyncio.create_task(aguild.upcoming_slot.cancel())
return "The accountability system has been shut down."
else:
return "The accountability category has been unset."

View File

@@ -26,7 +26,7 @@ async def cmd_send(ctx):
# Extract target and amount
# Handle a slightly more flexible input than stated
splits = ctx.args.split()
digits = [split.isdigit() for split in splits]
digits = [split.isdigit() for split in splits[:2]]
mentions = ctx.msg.mentions
if len(splits) < 2 or not any(digits) or not (all(digits) or mentions):
return await _send_usage(ctx)

View File

@@ -1,6 +1,7 @@
import discord
from cmdClient.lib import SafeCancellation
from wards import guild_admin
from wards import guild_admin, guild_moderator
from settings import UserInputError, GuildSettings
from utils.lib import prop_tabulate
@@ -26,12 +27,40 @@ descriptions = {
desc="View and modify the server settings.",
flags=('add', 'remove'),
group="Guild Configuration")
@guild_admin()
@guild_moderator()
async def cmd_config(ctx, flags):
"""
Usage``:
{prefix}config
{prefix}config info
{prefix}config <setting>
{prefix}config <setting> <value>
Description:
Display the server configuration panel, and view/modify the server settings.
Use `{prefix}config` to see the settings with their current values, or `{prefix}config info` to \
show brief descriptions instead.
Use `{prefix}config <setting>` (e.g. `{prefix}config event_log`) to view a more detailed description for each setting, \
including the possible values.
Finally, use `{prefix}config <setting> <value>` to set the setting to the given value.
To unset a setting, or set it to the default, use `{prefix}config <setting> None`.
Additional usage for settings which accept a list of values:
`{prefix}config <setting> <value1>, <value2>, ...`
`{prefix}config <setting> --add <value1>, <value2>, ...`
`{prefix}config <setting> --remove <value1>, <value2>, ...`
Note that the first form *overwrites* the setting completely,\
while the second two will only *add* and *remove* values, respectively.
Examples``:
{prefix}config event_log
{prefix}config event_log {ctx.ch.name}
{prefix}config autoroles Member, Level 0, Level 10
{prefix}config autoroles --remove Level 10
"""
# Cache and map some info for faster access
setting_displaynames = {setting.display_name.lower(): setting for setting in GuildSettings.settings.values()}
if not ctx.args or ctx.args.lower() == 'help':
if not ctx.args or ctx.args.lower() in ('info', 'help'):
# Fill the setting cats
cats = {}
for setting in GuildSettings.settings.values():
@@ -60,8 +89,14 @@ async def cmd_config(ctx, flags):
colour=discord.Colour.orange(),
title=page_name,
description=(
"View brief setting descriptions with `{prefix}config help`.\n"
"See `{prefix}help config` for more general usage.".format(prefix=ctx.best_prefix)
"View brief setting descriptions with `{prefix}config info`.\n"
"Use e.g. `{prefix}config event_log` to see more details.\n"
"Modify a setting with e.g. `{prefix}config event_log {ctx.ch.name}`.\n"
"See the [Online Tutorial]({tutorial}) for a complete setup guide.".format(
prefix=ctx.best_prefix,
ctx=ctx,
tutorial="https://discord.studylions.com/tutorial"
)
)
)
for name, value in page.items():
@@ -71,7 +106,7 @@ async def cmd_config(ctx, flags):
if len(pages) > 1:
[
embed.set_footer(text="Page {}/{}".format(i+1, len(pages)))
embed.set_footer(text="Page {} of {}".format(i+1, len(pages)))
for i, embed in enumerate(pages)
]
await ctx.pager(pages)
@@ -98,9 +133,12 @@ async def cmd_config(ctx, flags):
await setting.get(ctx.guild.id).widget(ctx, flags=flags)
else:
# config <setting> <value>
# Ignoring the write ward currently and just enforcing admin
# Check the write ward
if not await setting.write_ward.run(ctx):
await ctx.error_reply(setting.write_ward.msg)
# if not await setting.write_ward.run(ctx):
# raise SafeCancellation(setting.write_ward.msg)
if not await guild_admin.run(ctx):
raise SafeCancellation("You need to be a server admin to modify settings!")
# Attempt to set config setting
try:

View File

@@ -732,7 +732,8 @@ async def cmd_reactionroles(ctx, flags):
# Add the reactions to the message, if possible
existing_reactions = set(
reaction.emoji.name if reaction.emoji.id is None else reaction.emoji.id
reaction.emoji if not reaction.custom_emoji else
(reaction.emoji.name if reaction.emoji.id is None else reaction.emoji.id)
for reaction in message.reactions
)
missing = [

View File

@@ -40,7 +40,7 @@ def schedule_expiry(guildid, userid, roleid, expiry, reactionid=None):
_wakeup_event.set()
def cancel_expiry(key):
def cancel_expiry(*key):
"""
Cancel expiry for the given member and role, if it exists.
"""

View File

@@ -546,7 +546,7 @@ class ReactionRoleMessage:
@client.add_after_event('raw_reaction_add')
async def reaction_role_add(client, payload):
reaction_message = ReactionRoleMessage.fetch(payload.message_id)
if not payload.member.bot and reaction_message and reaction_message.enabled:
if payload.guild_id and not payload.member.bot and reaction_message and reaction_message.enabled:
try:
await reaction_message.process_raw_reaction_add(payload)
except Exception:
@@ -564,7 +564,7 @@ async def reaction_role_add(client, payload):
@client.add_after_event('raw_reaction_remove')
async def reaction_role_remove(client, payload):
reaction_message = ReactionRoleMessage.fetch(payload.message_id)
if reaction_message and reaction_message.enabled:
if payload.guild_id and reaction_message and reaction_message.enabled:
try:
await reaction_message.process_raw_reaction_remove(payload)
except Exception:

View File

@@ -42,7 +42,11 @@ bot_admin_group_order = (
# TODO: Add config fields for this
title = "StudyLion Command List"
header = """
Use `{ctx.best_prefix}help <command>` (e.g. `{ctx.best_prefix}help send`) to see how to use each command.
[StudyLion](https://bot.studylions.com/) is a fully featured study assistant \
that tracks your study time and offers productivity tools \
such as to-do lists, task reminders, private study rooms, group accountability sessions, and much much more.\n
Use `{ctx.best_prefix}help <command>` (e.g. `{ctx.best_prefix}help send`) to learn how to use each command, \
or [click here](https://discord.studylions.com/tutorial) for a comprehensive tutorial.
"""

0
bot/modules/plugins/.gitignore vendored Normal file
View File

View File

@@ -37,6 +37,15 @@ async def cmd_rent(ctx):
# Fetch the members' room, if it exists
room = Room.fetch(ctx.guild.id, ctx.author.id)
# Handle pre-deletion of the room
if room and not room.channel:
ctx.guild_settings.event_log.log(
title="Private study room not found!",
description="{}'s study room was deleted before it expired!".format(ctx.author.mention)
)
room.delete()
room = None
if room:
# Show room status, or add/remove remebers
lower = ctx.args.lower()

View File

@@ -112,7 +112,8 @@ class Room:
@property
def owner(self):
"""
The Member owning the room, if we can find them
The Member owning the room.
May be `None` if the member is no longer in the guild, or is otherwise not visible.
"""
guild = client.get_guild(self.data.guildid)
if guild:
@@ -122,6 +123,7 @@ class Room:
def channel(self):
"""
The Channel corresponding to this rented room.
May be `None` if the channel was already deleted.
"""
guild = client.get_guild(self.data.guildid)
if guild:
@@ -176,9 +178,6 @@ class Room:
"""
Expire the room.
"""
owner = self.owner
guild_settings = GuildSettings(owner.guild.id)
if self.channel:
# Delete the discord channel
try:
@@ -189,9 +188,10 @@ class Room:
# Delete the room from data (cascades to member deletion)
self.delete()
guild_settings = GuildSettings(self.data.guildid)
guild_settings.event_log.log(
title="Private study room expired!",
description="{}'s private study room expired.".format(owner.mention)
description="<@{}>'s private study room expired.".format(self.data.ownerid)
)
async def add_members(self, *members):

View File

@@ -1,6 +1,7 @@
import json
import asyncio
import itertools
import traceback
from io import StringIO
from enum import IntEnum
from typing import Any, Optional
@@ -753,11 +754,19 @@ class Message(SettingType):
if as_json:
try:
args = json.loads(userstr)
except json.JSONDecodeError:
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 'embed' in args:
discord.Embed.from_dict(
args['embed']
)
except Exception as e:
only_error = "".join(traceback.TracebackException.from_exception(e).format_exception_only())
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)."
"[here](https://glitchii.github.io/embedbuilder/?editor=json).\n"
"```{}```".format(only_error)
)
if 'embed' in args and 'timestamp' in args['embed']:
args['embed'].pop('timestamp')
@@ -770,6 +779,8 @@ 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:
return "Invalid"
if 'embed' not in value and len(value['content']) < 100:
return "`{}`".format(value['content'])
else:
@@ -788,9 +799,9 @@ class Message(SettingType):
value = self.value
substitutions = self.substitution_keys(ctx, **kwargs)
args = {}
if 'content' in value:
if value.get('content', None):
args['content'] = multiple_replace(value['content'], substitutions)
if 'embed' in value:
if value.get('embed', None):
args['embed'] = discord.Embed.from_dict(
json.loads(multiple_replace(json.dumps(value['embed']), substitutions))
)
@@ -800,7 +811,7 @@ class Message(SettingType):
value = self.value
args = self.args(ctx, **kwargs)
if not value:
if not value or not args:
return await ctx.reply(embed=self.embed)
current_str = None

View File

@@ -1,6 +1,7 @@
import asyncio
import discord
from cmdClient import Context
from cmdClient.lib import SafeCancellation
from data import tables
from core import Lion
@@ -17,9 +18,11 @@ async def embed_reply(ctx, desc, colour=discord.Colour.orange(), **kwargs):
"""
embed = discord.Embed(description=desc, colour=colour, **kwargs)
try:
return await ctx.reply(embed=embed, reference=ctx.msg)
except discord.NotFound:
return await ctx.reply(embed=embed)
return await ctx.reply(embed=embed, reference=ctx.msg.to_reference(fail_if_not_exists=False))
except discord.Forbidden:
if not ctx.guild or ctx.ch.permissions_for(ctx.guild.me).send_mssages:
await ctx.reply("Command failed, I don't have permission to send embeds in this channel!")
raise SafeCancellation
@Context.util
@@ -34,15 +37,17 @@ async def error_reply(ctx, error_str, **kwargs):
)
message = None
try:
message = await ctx.ch.send(embed=embed, reference=ctx.msg, **kwargs)
except discord.NotFound:
message = await ctx.ch.send(embed=embed, **kwargs)
message = await ctx.ch.send(
embed=embed,
reference=ctx.msg.to_reference(fail_if_not_exists=False),
**kwargs
)
ctx.sent_messages.append(message)
return message
except discord.Forbidden:
message = await ctx.reply(error_str)
finally:
if message:
ctx.sent_messages.append(message)
return message
if not ctx.guild or ctx.ch.permissions_for(ctx.guild.me).send_mssages:
await ctx.reply("Command failed, I don't have permission to send embeds in this channel!")
raise SafeCancellation
@Context.util