You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
Fox-V3/ccrole/ccrole.py

446 lines
15 KiB

import asyncio
import logging
import re
import discord
from discord.ext.commands import RoleConverter, Greedy, CommandError, ArgumentParsingError
from discord.ext.commands.view import StringView
from redbot.core import Config, checks, commands
from redbot.core.bot import Red
from redbot.core.utils.chat_formatting import box, pagify
from redbot.core.utils.mod import get_audit_reason
7 years ago
log = logging.getLogger("red.fox_v3.ccrole")
7 years ago
async def _get_roles_from_content(ctx, content):
# greedy = Greedy[RoleConverter]
view = StringView(content)
rc = RoleConverter()
# "Borrowed" from discord.ext.commands.Command._transform_greedy_pos
result = []
while not view.eof:
# for use with a manual undo
previous = view.index
view.skip_ws()
try:
argument = view.get_quoted_word()
value = await rc.convert(ctx, argument)
except (CommandError, ArgumentParsingError):
view.index = previous
break
else:
result.append(value)
return [r.id for r in result]
# Old method
# content_list = content.split(",")
# try:
# role_list = [
# discord.utils.get(ctx.guild.roles, name=role.strip(" ")).id for role in content_list
# ]
# except (discord.HTTPException, AttributeError): # None.id is attribute error
# return None
# else:
# return role_list
class CCRole(commands.Cog):
"""
Custom commands
Creates commands used to display text and adjust roles
"""
7 years ago
def __init__(self, bot: Red):
super().__init__()
7 years ago
self.bot = bot
7 years ago
self.config = Config.get_conf(self, identifier=9999114111108101)
6 years ago
default_guild = {"cmdlist": {}, "settings": {}}
7 years ago
self.config.register_guild(**default_guild)
7 years ago
async def red_delete_data_for_user(self, **kwargs):
"""Nothing to delete"""
return
6 years ago
@commands.guild_only()
@commands.group()
async def ccrole(self, ctx: commands.Context):
7 years ago
"""Custom commands management with roles
Highly customizable custom commands with role management."""
pass
7 years ago
7 years ago
@ccrole.command(name="add")
7 years ago
@checks.mod_or_permissions(administrator=True)
async def ccrole_add(self, ctx, command: str):
7 years ago
"""Adds a custom command with roles
When adding text, put arguments in `{}` to eval them
Options: `{author}`, `{target}`, `{server}`, `{channel}`, `{message}`"""
# TODO: Clean this up so it's not so repetitive
# The call/answer format has better options as well
# Saying "none" over and over can trigger automod actions as well
# Also, allow `ctx.tick()` instead of sending a message
7 years ago
command = command.lower()
7 years ago
if command in self.bot.all_commands:
await ctx.send("That command is already a standard command.")
7 years ago
return
7 years ago
guild = ctx.guild
author = ctx.author
channel = ctx.channel
7 years ago
cmd_list = self.config.guild(guild).cmdlist
7 years ago
if await cmd_list.get_raw(command, default=None):
6 years ago
await ctx.send(
"This command already exists. Delete it with `{}ccrole delete` first.".format(
ctx.prefix
)
)
return
7 years ago
# Roles to add
6 years ago
await ctx.send(
"What roles should it add?\n"
"Say `None` to skip adding roles"
6 years ago
)
7 years ago
def check(m):
return m.author == author and m.channel == channel
7 years ago
try:
6 years ago
answer = await self.bot.wait_for("message", timeout=120, check=check)
7 years ago
except asyncio.TimeoutError:
await ctx.send("Timed out, canceling")
return
arole_list = []
if answer.content.upper() != "NONE":
arole_list = await _get_roles_from_content(ctx, answer.content)
7 years ago
if arole_list is None:
await ctx.send("Invalid answer, canceling")
return
# Roles to remove
6 years ago
await ctx.send(
"What roles should it remove?\n"
"Say `None` to skip removing roles"
6 years ago
)
7 years ago
try:
6 years ago
answer = await self.bot.wait_for("message", timeout=120, check=check)
7 years ago
except asyncio.TimeoutError:
await ctx.send("Timed out, canceling")
return
rrole_list = []
if answer.content.upper() != "NONE":
rrole_list = await _get_roles_from_content(ctx, answer.content)
7 years ago
if rrole_list is None:
await ctx.send("Invalid answer, canceling")
return
# Roles to use
await ctx.send(
"What roles are allowed to use this command?\n"
"Say `None` to allow all roles"
6 years ago
)
7 years ago
try:
6 years ago
answer = await self.bot.wait_for("message", timeout=120, check=check)
7 years ago
except asyncio.TimeoutError:
await ctx.send("Timed out, canceling")
return
prole_list = []
if answer.content.upper() != "NONE":
prole_list = await _get_roles_from_content(ctx, answer.content)
7 years ago
if prole_list is None:
await ctx.send("Invalid answer, canceling")
return
# Selfrole
await ctx.send(
"Is this a targeted command?(yes/no)\n" "No will make this a selfrole command"
)
7 years ago
try:
6 years ago
answer = await self.bot.wait_for("message", timeout=120, check=check)
7 years ago
except asyncio.TimeoutError:
await ctx.send("Timed out, canceling")
return
if answer.content.upper() in ["Y", "YES"]:
targeted = True
7 years ago
await ctx.send("This command will be **`targeted`**")
7 years ago
else:
targeted = False
7 years ago
await ctx.send("This command will be **`selfrole`**")
# Message to send
await ctx.send(
6 years ago
"What message should the bot say when using this command?\n"
"Say `None` to send no message and just react with ✅\n"
6 years ago
"Eval Options: `{author}`, `{target}`, `{server}`, `{channel}`, `{message}`\n"
"For example: `Welcome {target.mention} to {server.name}!`"
)
7 years ago
try:
6 years ago
answer = await self.bot.wait_for("message", timeout=120, check=check)
7 years ago
except asyncio.TimeoutError:
await ctx.send("Timed out, canceling")
return
text = None
if answer.content.upper() != "NONE":
text = answer.content
# Save the command
6 years ago
out = {
"text": text,
"aroles": arole_list,
"rroles": rrole_list,
"proles": prole_list,
"targeted": targeted,
}
7 years ago
await cmd_list.set_raw(command, value=out)
7 years ago
await ctx.send("Custom Command **`{}`** successfully added".format(command))
7 years ago
@ccrole.command(name="delete")
7 years ago
@checks.mod_or_permissions(administrator=True)
async def ccrole_delete(self, ctx, command: str):
7 years ago
"""Deletes a custom command
7 years ago
7 years ago
Example:
7 years ago
`[p]ccrole delete yourcommand`"""
7 years ago
guild = ctx.guild
7 years ago
command = command.lower()
if not await self.config.guild(guild).cmdlist.get_raw(command, default=None):
7 years ago
await ctx.send("That command doesn't exist")
7 years ago
else:
await self.config.guild(guild).cmdlist.set_raw(command, value=None)
7 years ago
await ctx.send("Custom command successfully deleted.")
7 years ago
@ccrole.command(name="details", aliases=["detail"])
7 years ago
async def ccrole_details(self, ctx, command: str):
"""Provide details about passed custom command"""
guild = ctx.guild
command = command.lower()
cmd = await self.config.guild(guild).cmdlist.get_raw(command, default=None)
if cmd is None:
await ctx.send("That command doesn't exist")
return
6 years ago
embed = discord.Embed(
title=command,
description="{} custom command".format(
"Targeted" if cmd["targeted"] else "Non-Targeted"
),
)
7 years ago
def process_roles(role_list):
if not role_list:
return "None"
6 years ago
return ", ".join(
discord.utils.get(ctx.guild.roles, id=roleid).name for roleid in role_list
6 years ago
)
7 years ago
embed.add_field(name="Text", value="```{}```".format(cmd["text"]), inline=False)
embed.add_field(name="Adds Roles", value=process_roles(cmd["aroles"]), inline=False)
embed.add_field(name="Removes Roles", value=process_roles(cmd["rroles"]), inline=False)
embed.add_field(name="Role Restrictions", value=process_roles(cmd["proles"]), inline=False)
7 years ago
await ctx.send(embed=embed)
7 years ago
@ccrole.command(name="list")
async def ccrole_list(self, ctx):
7 years ago
"""Shows custom commands list"""
7 years ago
guild = ctx.guild
cmd_list = await self.config.guild(guild).cmdlist()
cmd_list = {k: v for k, v in cmd_list.items() if v}
if not cmd_list:
await ctx.send(
"There are no custom commands in this server. Use `{}ccrole add` to start adding some.".format(
6 years ago
ctx.prefix
)
)
7 years ago
return
cmd_list = ", ".join(ctx.prefix + c for c in sorted(cmd_list.keys()))
cmd_list = "Custom commands:\n\n" + cmd_list
7 years ago
6 years ago
if (
4 years ago
len(cmd_list) < 1500
6 years ago
): # I'm allowed to have arbitrary numbers for when it's too much to dm dammit
await ctx.send(box(cmd_list))
7 years ago
else:
for page in pagify(cmd_list, delims=[" ", "\n"]):
7 years ago
await ctx.author.send(box(page))
await ctx.send("Command list DM'd")
7 years ago
@commands.Cog.listener()
async def on_message_without_command(self, message: discord.Message):
5 years ago
"""
Credit to:
https://github.com/Cog-Creators/Red-DiscordBot/blob/V3/develop/redbot/cogs/customcom/customcom.py#L508
for the message filtering
"""
# This covers message.author.bot check
if not await self.bot.message_eligible_as_command(message):
return
###########
is_private = isinstance(message.channel, discord.abc.PrivateChannel)
if is_private or len(message.content) < 2:
return
if await self.bot.cog_disabled_in_guild(self, message.guild):
return
ctx = await self.bot.get_context(message)
if ctx.prefix is None:
return
###########
# Thank you Cog-Creators
cmd = ctx.invoked_with
4 years ago
cmd = cmd.lower() # Continues the proud case-insensitivity tradition of ccrole
guild = ctx.guild
# message = ctx.message # Unneeded since switch to `on_message_without_command` from `on_command_error`
7 years ago
4 years ago
cmd_list = self.config.guild(guild).cmdlist
# cmd = message.content[len(prefix) :].split()[0].lower()
4 years ago
cmd = await cmd_list.get_raw(cmd, default=None)
if cmd is not None:
await self.eval_cc(cmd, message, ctx)
else:
log.debug(f"No custom command named {ctx.invoked_with} found")
7 years ago
async def get_prefix(self, message: discord.Message) -> str:
"""
Borrowed from alias cog
Tries to determine what prefix is used in a message object.
Looks to identify from longest prefix to smallest.
Will raise ValueError if no prefix is found.
:param message: Message object
:return:
"""
content = message.content
prefix_list = await self.bot.command_prefix(self.bot, message)
6 years ago
prefixes = sorted(prefix_list, key=lambda pfx: len(pfx), reverse=True)
7 years ago
for p in prefixes:
if content.startswith(p):
7 years ago
return p
7 years ago
raise ValueError
async def eval_cc(self, cmd, message: discord.Message, ctx: commands.Context):
7 years ago
"""Does all the work"""
if cmd["proles"] and not {role.id for role in message.author.roles} & set(cmd["proles"]):
log.debug(f"{message.author} missing required role to execute {ctx.invoked_with}")
return # Not authorized, do nothing
6 years ago
if cmd["targeted"]:
view: StringView = ctx.view
view.skip_ws()
guild: discord.Guild = ctx.guild
# print(f"Guild: {guild}")
target = view.get_quoted_word()
# print(f"Target: {target}")
if target:
# target = discord.utils.get(guild.members, mention=target)
try:
target = await commands.MemberConverter().convert(ctx, target)
except commands.BadArgument:
target = None
else:
target = None
if not target:
out_message = (
f"This custom command is targeted! @mention a target\n`"
f"{ctx.invoked_with} <target>`"
6 years ago
)
await ctx.send(out_message)
return
7 years ago
else:
target = message.author
reason = get_audit_reason(message.author)
6 years ago
if cmd["aroles"]:
arole_list = [
discord.utils.get(message.guild.roles, id=roleid) for roleid in cmd["aroles"]
]
try:
await target.add_roles(*arole_list, reason=reason)
except discord.Forbidden:
log.exception(f"Permission error: Unable to add roles")
await ctx.send("Permission error: Unable to add roles")
6 years ago
if cmd["rroles"]:
rrole_list = [
discord.utils.get(message.guild.roles, id=roleid) for roleid in cmd["rroles"]
]
try:
await target.remove_roles(*rrole_list, reason=reason)
except discord.Forbidden:
log.exception(f"Permission error: Unable to remove roles")
await ctx.send("Permission error: Unable to remove roles")
if cmd["text"] is not None:
out_message = self.format_cc(cmd, message, target)
await ctx.send(out_message, allowed_mentions=discord.AllowedMentions())
else:
await ctx.tick()
def format_cc(self, cmd, message, target):
6 years ago
out = cmd["text"]
results = re.findall("{([^}]+)\}", out)
for result in results:
param = self.transform_parameter(result, message, target)
out = out.replace("{" + result + "}", param)
return out
def transform_parameter(self, result, message, target):
"""
For security reasons only specific objects are allowed
Internals are ignored
Copied from customcom.CustomCommands.transform_parameter and added `target`
"""
raw_result = "{" + result + "}"
objects = {
"message": message,
"author": message.author,
"channel": message.channel,
"server": message.guild,
"guild": message.guild,
6 years ago
"target": target,
}
if result in objects:
return str(objects[result])
try:
first, second = result.split(".")
except ValueError:
return raw_result
if first in objects and not second.startswith("_"):
first = objects[first]
else:
return raw_result
return str(getattr(first, second, raw_result))