325 lines
12 KiB
Python
325 lines
12 KiB
Python
import asyncio
|
|
import discord
|
|
|
|
from cmdClient.lib import ResponseTimedOut
|
|
from wards import guild_admin
|
|
from settings import UserInputError
|
|
|
|
from ..module import module
|
|
|
|
from .tracker import ReactionRoleMessage
|
|
from .data import reaction_role_reactions
|
|
from . import settings
|
|
|
|
|
|
example_str = "🧮 mathematics, 🫀 biology, 💻 computer science, 🖼️ design, 🩺 medicine"
|
|
|
|
|
|
@module.cmd(
|
|
"reactionroles",
|
|
group="Guild Admin",
|
|
desc="Create or configure reaction role messages.",
|
|
aliases=('rroles',),
|
|
flags=(
|
|
'delete', 'remove',
|
|
'enable', 'disable',
|
|
'required_role==', 'removable=', 'maximum=', 'refunds=', 'log=', 'default_price=',
|
|
'price=', 'timeout=='
|
|
)
|
|
)
|
|
@guild_admin()
|
|
async def cmd_reactionroles(ctx, flags):
|
|
"""
|
|
Usage``:
|
|
{prefix}rroles messageid
|
|
{prefix}rroles messageid emoji1 role1, emoji2 role2, emoji3 role3, ...
|
|
{prefix}rroles messageid --message_setting value
|
|
{prefix}rroles messageid emoji --emoji_setting value
|
|
{prefix}rroles messageid --remove emoji1, emoji2, ...
|
|
{prefix}rroles messageid --enable
|
|
{prefix}rroles messageid --disable
|
|
{prefix}rroles messageid --delete
|
|
Description:
|
|
Reaction roles are message reactions that give members specific roles when used.
|
|
This commands allows you to create and configure messages with reaction roles.
|
|
Getting started:
|
|
To get started, choose the message you want to add reaction roles to, and copy the link to that message. \
|
|
Then run the command `{prefix}rroles link` (replacing `link` with the link you copied), and \
|
|
follow the prompts.
|
|
Configuration:
|
|
After creation, you can view the message configuration and reaction roles with `{prefix}rroles link` \
|
|
(you can also use the message id instead of the link, or reply to the message).
|
|
|
|
You can set one of the configuration options with `{prefix}rroles link --setting value`.\
|
|
For example to make it impossible to remove the reaction roles,\
|
|
run `{prefix}rroles link --removable off`.
|
|
|
|
There are also some configurable per-reaction settings, such as the price of a role.\
|
|
To see these, use `{prefix}rroles link emoji` (replacing `emoji` with the reaction emoji) \
|
|
and set them with e.g. `{prefix}rroles link emoji --price 200`.
|
|
Message Settings::
|
|
maximum: Maximum number of roles obtainable from this message.
|
|
log: Whether to log reaction role usage into the event log.
|
|
removable: Whether the reactions roles can be remove by unreacting.
|
|
refunds: Whether to refund the role price when removing the role.
|
|
default_price: The default price of each role on this message.
|
|
required_role: The role required to use these reactions roles.
|
|
Reaction Settings::
|
|
price: The price of this reaction role.
|
|
timeout: The amount of time the role lasts. (TBD)
|
|
Examples:
|
|
...
|
|
"""
|
|
if not ctx.args and not ctx.msg.reference:
|
|
# No target message provided, list the current reaction messages
|
|
# Or give a brief guide if there are no current reaction messages
|
|
...
|
|
|
|
target_id = None
|
|
target_chid = None
|
|
remaining = ""
|
|
|
|
if ctx.msg.reference:
|
|
target_id = ctx.msg.reference.message_id
|
|
target_chid = ctx.msg.reference.channel_id
|
|
remaining = ctx.args
|
|
elif ctx.args:
|
|
# Try to parse the target message
|
|
# Expect a link or a messageid as the first argument
|
|
splits = ctx.args.split(maxsplit=1)
|
|
maybe_target = splits[0]
|
|
if len(splits) > 1:
|
|
remaining = splits[1]
|
|
if maybe_target.isdigit():
|
|
# Assume it is a message id
|
|
target_id = int(maybe_target)
|
|
elif maybe_target.contains('/'):
|
|
# Assume it is a link
|
|
# Try and parse it
|
|
link_splits = maybe_target.rsplit('/', maxsplit=2)
|
|
if len(link_splits) > 1 and link_splits[-1].isdigit() and link_splits[-2].isdigit():
|
|
# Definitely a link
|
|
target_id = int(link_splits[-1])
|
|
target_chid = int(link_splits[-2])
|
|
|
|
if not target_id:
|
|
return await ctx.error_reply(
|
|
"Please provide the message link or message id as the first argument."
|
|
)
|
|
|
|
# We have a target, fetch the ReactionMessage if it exists
|
|
target = ReactionRoleMessage.fetch(target_id)
|
|
if target:
|
|
# View or edit target
|
|
|
|
# Exclusive flags, delete and remove ignore all other flags
|
|
if flags['delete']:
|
|
# Handle deletion of the ReactionRoleMessage
|
|
...
|
|
if flags['remove']:
|
|
# Handle emoji removal
|
|
...
|
|
|
|
# Check whether we are editing a particular reaction
|
|
# TODO: We might be updating roles or adding reactions as well
|
|
if remaining:
|
|
emojistr = remaining
|
|
# TODO....
|
|
|
|
# Lines for edit output
|
|
edit_lines = [] # (success_state, string)
|
|
# Columns to update
|
|
update = {}
|
|
|
|
# Message edit flags
|
|
# Gets and modifies the settings, but doesn't write
|
|
if flags['disable']:
|
|
update['enabled'] = False
|
|
edit_lines.append((True, "Reaction role message disabled."))
|
|
elif flags['enable']:
|
|
update['enabled'] = True
|
|
edit_lines.append((True, "Reaction role message enabled."))
|
|
|
|
if flags['required_role']:
|
|
try:
|
|
setting = await settings.required_role.parse(target.messageid, ctx, flags['required_role'])
|
|
except UserInputError as e:
|
|
edit_lines.append((False, e.msg))
|
|
else:
|
|
edit_lines.append((True, setting.success_response))
|
|
update[setting._data_column] = setting.data
|
|
if flags['removable']:
|
|
try:
|
|
setting = await settings.removable.parse(target.messageid, ctx, flags['removable'])
|
|
except UserInputError as e:
|
|
edit_lines.append((False, e.msg))
|
|
else:
|
|
edit_lines.append((True, setting.success_response))
|
|
update[setting._data_column] = setting.data
|
|
if flags['maximum']:
|
|
try:
|
|
setting = await settings.maximum.parse(target.messageid, ctx, flags['maximum'])
|
|
except UserInputError as e:
|
|
edit_lines.append((False, e.msg))
|
|
else:
|
|
edit_lines.append((True, setting.success_response))
|
|
update[setting._data_column] = setting.data
|
|
if flags['refunds']:
|
|
try:
|
|
setting = await settings.refunds.parse(target.messageid, ctx, flags['refunds'])
|
|
except UserInputError as e:
|
|
edit_lines.append((False, e.msg))
|
|
else:
|
|
edit_lines.append((True, setting.success_response))
|
|
update[setting._data_column] = setting.data
|
|
if flags['log']:
|
|
try:
|
|
setting = await settings.log.parse(target.messageid, ctx, flags['log'])
|
|
except UserInputError as e:
|
|
edit_lines.append((False, e.msg))
|
|
else:
|
|
edit_lines.append((True, setting.success_response))
|
|
update[setting._data_column] = setting.data
|
|
if flags['default_price']:
|
|
try:
|
|
setting = await settings.default_price.parse(target.messageid, ctx, flags['default_price'])
|
|
except UserInputError as e:
|
|
edit_lines.append((False, e.msg))
|
|
else:
|
|
edit_lines.append((True, setting.success_response))
|
|
update[setting._data_column] = setting.data
|
|
|
|
# Update the data all at once
|
|
target.data.update(**update)
|
|
|
|
# Then format and respond with the edit message
|
|
|
|
# TODO: Emoji edit flags
|
|
...
|
|
else:
|
|
# Start creation process
|
|
# First find the target message
|
|
message = None
|
|
if target_chid:
|
|
channel = ctx.guild.get_channel(target_chid)
|
|
if channel:
|
|
message = await channel.fetch_message(target_id)
|
|
else:
|
|
# We only have a messageid, need to search for it through all the guild channels
|
|
message = await ctx.find_message(target_id)
|
|
|
|
if message is None:
|
|
return await ctx.error_reply(
|
|
"Could not find the specified message!"
|
|
)
|
|
|
|
# Confirm that they want to create a new reaction role message.
|
|
embed = discord.Embed(
|
|
colour=discord.Colour.orange(),
|
|
description="Do you want to set up new reaction roles for [this message]({}) (`y(es)`/`n(o)`)?".format(
|
|
message.jump_url
|
|
)
|
|
)
|
|
out_msg = await ctx.reply(embed=embed)
|
|
if not await ctx.ask(msg=None, use_msg=out_msg):
|
|
return
|
|
|
|
# Set up the message as a new ReactionRole message.
|
|
# First obtain the initial emojis
|
|
if not remaining:
|
|
# Prompt for the initial emojis
|
|
embed = discord.Embed(
|
|
title="What reaction roles would you like to add?",
|
|
description=(
|
|
"Please now enter the reaction roles you would like to associate, "
|
|
"in the form `emoji role`, where `role` is given by id or partial name. For example:"
|
|
"```{}```".format(example_str)
|
|
)
|
|
)
|
|
out_msg = await ctx.reply(embed=embed)
|
|
|
|
# Wait for a response
|
|
def check(msg):
|
|
return msg.author == ctx.author and msg.channel == ctx.ch and msg.content
|
|
|
|
try:
|
|
reply = await ctx.client.wait_for('message', check=check, timeout=300)
|
|
except asyncio.TimeoutError:
|
|
raise ResponseTimedOut("Prompt timed out, no reaction roles were added.")
|
|
finally:
|
|
try:
|
|
await out_msg.delete()
|
|
except discord.HTTPException:
|
|
pass
|
|
|
|
rolestrs = reply.content
|
|
|
|
try:
|
|
await reply.delete()
|
|
except discord.HTTPException:
|
|
pass
|
|
else:
|
|
rolestrs = remaining
|
|
|
|
# Attempt to parse the emojis
|
|
# First split based on newline and comma
|
|
splits = (split.strip() for line in rolestrs.splitlines() for split in line.split(','))
|
|
splits = [split for split in splits if split]
|
|
|
|
# Do a quick check to make sure everything is in the correct format
|
|
unsplit = next((split for split in splits if ' ' not in split), None)
|
|
if unsplit:
|
|
return await ctx.error_reply(
|
|
"Couldn't parse the reaction role `{}` into the form `emoji role`.".format(unsplit)
|
|
)
|
|
|
|
# Now go through and extract the emojis and roles
|
|
# TODO: Handle duplicate emojis?
|
|
# TODO: Error handling on the roles, make sure we can actually add them
|
|
reactions = {}
|
|
for split in splits:
|
|
emojistr, rolestr = split.split(maxsplit=1)
|
|
# Parse emoji
|
|
# TODO: Custom emoji handler, probably store in a PartialEmoji
|
|
if ':' in emojistr:
|
|
return ctx.error_reply(
|
|
"Sorry, at this time we only support built-in emojis! Custom emoji support coming soon."
|
|
)
|
|
emoji = emojistr
|
|
|
|
# Parse role
|
|
# TODO: More graceful error handling
|
|
role = await ctx.find_role(rolestr, interactive=True, allow_notfound=False)
|
|
|
|
reactions[emoji] = role
|
|
|
|
# TODO: Parse any provided settings, and pass them to the data constructor
|
|
|
|
# Create the ReactionRoleMessage
|
|
rmsg = ReactionRoleMessage.create(
|
|
message.id,
|
|
message.guild.id,
|
|
message.channel.id
|
|
)
|
|
|
|
# Insert the reaction data directly
|
|
# TODO: Again consider emoji setting data here, for common settings?
|
|
# TODO: Will need to be changed for custom emojis
|
|
reaction_role_reactions.insert_many(
|
|
*((message.id, role.id, emoji) for emoji, role in reactions.items()),
|
|
insert_keys=('messageid', 'roleid', 'emoji_name')
|
|
)
|
|
|
|
# Refresh the ReactionRoleMessage to pick up the new reactions
|
|
rmsg.refresh()
|
|
|
|
# Ack the creation
|
|
await ctx.embed_reply(
|
|
(
|
|
"Created `{}` new reaction roles.\n"
|
|
"Please add the reactions to [the message]({}) to make them available for use!".format(
|
|
len(reactions), rmsg.message_link
|
|
)
|
|
)
|
|
)
|