[V3] Warning system (#1173)

* [V3 Warning] initial work on a warning system for v3

* [V3 Warning] rename some stuff and add a case type

* [V3 Warnings] rename package from warning

* [V3 Warnings] restructuring commands

* [V3 Warnings] remove expiry stuff + other refactoring

* [V3 Warnings] refactoring action logic

* [V3 Warnings] rewrites to action logic

* [V3 Warnings] add regen_messages.py
This commit is contained in:
palmtree5 2018-02-25 20:14:38 -09:00 committed by GitHub
parent 21de95e0a6
commit c1ac78eea4
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
5 changed files with 651 additions and 0 deletions

View File

@ -0,0 +1,5 @@
from .warnings import Warnings
def setup(bot):
bot.add_cog(Warnings(bot))

View File

@ -0,0 +1,142 @@
from copy import copy
from discord.ext import commands
import asyncio
import inspect
import discord
from redbot.core import RedContext, Config, checks
from redbot.core.i18n import CogI18n
_ = CogI18n("Warnings", __file__)
async def warning_points_add_check(config: Config, ctx: RedContext, user: discord.Member, points: int):
"""Handles any action that needs to be taken or not based on the points"""
guild = ctx.guild
guild_settings = config.guild(guild)
act = {}
async with guild_settings.actions() as registered_actions:
for a in registered_actions.keys():
if points >= registered_actions[a]["point_count"]:
act = registered_actions[a]
else:
break
if act: # some action needs to be taken
await create_and_invoke_context(ctx, act["exceed_command"], user)
async def warning_points_remove_check(config: Config, ctx: RedContext, user: discord.Member, points: int):
guild = ctx.guild
guild_settings = config.guild(guild)
act = {}
async with guild_settings.actions() as registered_actions:
for a in registered_actions.keys():
if points >= registered_actions[a]["point_count"]:
act = registered_actions[a]
else:
break
if act: # some action needs to be taken
await create_and_invoke_context(ctx, act["drop_command"], user)
async def create_and_invoke_context(realctx: RedContext, command_str: str, user: discord.Member):
m = copy(realctx.message)
m.content = command_str.format(user=user.mention, prefix=realctx.prefix)
fctx = await realctx.bot.get_context(m, cls=RedContext)
try:
await realctx.bot.invoke(fctx)
except (commands.CheckFailure, commands.CommandOnCooldown):
await fctx.reinvoke()
def get_command_from_input(bot, userinput: str):
com = None
orig = userinput
while com is None:
com = bot.get_command(userinput)
if com is None:
userinput = ' '.join(userinput.split(' ')[:-1])
if len(userinput) == 0:
break
if com is None:
return None, _("I could not find a command from that input!")
check_str = inspect.getsource(checks.is_owner)
if any(inspect.getsource(x) in check_str for x in com.checks):
# command the user specified has the is_owner check
return None, _("That command requires bot owner. I can't "
"allow you to use that for an action")
return "{prefix}" + orig, None
async def get_command_for_exceeded_points(ctx: RedContext):
"""Gets the command to be executed when the user is at or exceeding
the points threshold for the action"""
await ctx.send(
_("Enter the command to be run when the user exceeds the points for "
"this action to occur.\nEnter it exactly as you would if you were "
"actually trying to run the command, except don't put a prefix and "
"use {user} in place of any user/member arguments\n\n"
"WARNING: The command entered will be run without regard to checks or cooldowns. "
"Commands requiring bot owner are not allowed for security reasons.\n\n"
"Please wait 15 seconds before entering your response.")
)
await asyncio.sleep(15)
await ctx.send(_("You may enter your response now."))
def same_author_check(m):
return m.author == ctx.author
try:
msg = await ctx.bot.wait_for("message", check=same_author_check, timeout=30)
except asyncio.TimeoutError:
await ctx.send(_("Ok then."))
return None
command, m = get_command_from_input(ctx.bot, msg.content)
if command is None:
await ctx.send(m)
return None
return command
async def get_command_for_dropping_points(ctx: RedContext):
"""
Gets the command to be executed when the user drops below the points
threshold
This is intended to be used for reversal of the action that was executed
when the user exceeded the threshold
"""
await ctx.send(
_("Enter the command to be run when the user returns to a value below "
"the points for this action to occur. Please note that this is "
"intended to be used for reversal of the action taken when the user "
"exceeded the action's point value\nEnter it exactly as you would "
"if you were actually trying to run the command, except don't put a prefix "
"and use {user} in place of any user/member arguments\n\n"
"WARNING: The command entered will be run without regard to checks or cooldowns. "
"Commands requiring bot owner are not allowed for security reasons.\n\n"
"Please wait 15 seconds before entering your response.")
)
await asyncio.sleep(15)
await ctx.send(_("You may enter your response now."))
def same_author_check(m):
return m.author == ctx.author
try:
msg = await ctx.bot.wait_for("message", check=same_author_check, timeout=30)
except asyncio.TimeoutError:
await ctx.send(_("Ok then."))
return None
command, m = get_command_from_input(ctx.bot, msg.content)
if command is None:
await ctx.send(m)
return None
return command

View File

@ -0,0 +1,117 @@
# SOME DESCRIPTIVE TITLE.
# Copyright (C) YEAR ORGANIZATION
# FIRST AUTHOR <EMAIL@ADDRESS>, YEAR.
#
msgid ""
msgstr ""
"Project-Id-Version: PACKAGE VERSION\n"
"POT-Creation-Date: 2018-02-25 17:26+AKST\n"
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
"Language-Team: LANGUAGE <LL@li.org>\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=CHARSET\n"
"Content-Transfer-Encoding: ENCODING\n"
"Generated-By: pygettext.py 1.5\n"
#: ../helpers.py:62
msgid "I could not find a command from that input!"
msgstr ""
#: ../helpers.py:67
msgid "That command requires bot owner. I can't allow you to use that for an action"
msgstr ""
#: ../helpers.py:76
msgid ""
"Enter the command to be run when the user exceeds the points for this action to occur.\n"
"Enter it exactly as you would if you were actually trying to run the command, except don't put a prefix and use {user} in place of any user/member arguments\n"
"\n"
"WARNING: The command entered will be run without regard to checks or cooldowns. Commands requiring bot owner are not allowed for security reasons.\n"
"\n"
"Please wait 15 seconds before entering your response."
msgstr ""
#: ../helpers.py:86 ../helpers.py:126
msgid "You may enter your response now."
msgstr ""
#: ../helpers.py:94 ../helpers.py:134
msgid "Ok then."
msgstr ""
#: ../helpers.py:114
msgid ""
"Enter the command to be run when the user returns to a value below the points for this action to occur. Please note that this is intended to be used for reversal of the action taken when the user exceeded the action's point value\n"
"Enter it exactly as you would if you were actually trying to run the command, except don't put a prefix and use {user} in place of any user/member arguments\n"
"\n"
"WARNING: The command entered will be run without regard to checks or cooldowns. Commands requiring bot owner are not allowed for security reasons.\n"
"\n"
"Please wait 15 seconds before entering your response."
msgstr ""
#: ../warnings.py:66
msgid "Custom reasons have been {}"
msgstr ""
#: ../warnings.py:66
msgid "disabled"
msgstr ""
#: ../warnings.py:66
msgid "enabled"
msgstr ""
#: ../warnings.py:92 ../warnings.py:351 ../warnings.py:368
msgid "Ok then"
msgstr ""
#: ../warnings.py:117
msgid "Duplicate action name found!"
msgstr ""
#: ../warnings.py:171
msgid "That reason has been registered"
msgstr ""
#: ../warnings.py:181
msgid "Removed reason {}"
msgstr ""
#: ../warnings.py:183
msgid "That is not a registered reason name"
msgstr ""
#: ../warnings.py:230
msgid "Custom reasons are not allowed! Please see {} for a complete list of valid reasons"
msgstr ""
#: ../warnings.py:243
msgid "That is not a registered reason!"
msgstr ""
#: ../warnings.py:277
msgid "You are not allowed to check warnings for other users!"
msgstr ""
#: ../warnings.py:290
msgid "That user has no warnings!"
msgstr ""
#: ../warnings.py:347
msgid "How many points should be given for this reason?"
msgstr ""
#: ../warnings.py:356
msgid "That isn't a number!"
msgstr ""
#: ../warnings.py:360
msgid "The point value needs to be greater than 0!"
msgstr ""
#: ../warnings.py:364
msgid "Enter a description for this reason"
msgstr ""

View File

@ -0,0 +1,16 @@
import subprocess
TO_TRANSLATE = [
'../warnings.py',
'../helpers.py'
]
def regen_messages():
subprocess.run(
['pygettext', '-n'] + TO_TRANSLATE
)
if __name__ == "__main__":
regen_messages()

View File

@ -0,0 +1,371 @@
from collections import namedtuple
from discord.ext import commands
import discord
import asyncio
from redbot.cogs.warnings.helpers import warning_points_add_check, get_command_for_exceeded_points, \
get_command_for_dropping_points, warning_points_remove_check
from redbot.core import Config, modlog, checks
from redbot.core.bot import Red
from redbot.core.context import RedContext
from redbot.core.i18n import CogI18n
from redbot.core.utils.mod import is_admin_or_superior
from redbot.core.utils.chat_formatting import warning, pagify
_ = CogI18n("Warnings", __file__)
class Warnings:
"""A warning system for Red"""
default_guild = {
"actions": [],
"reasons": {},
"allow_custom_reasons": False
}
default_member = {
"total_points": 0,
"status": "",
"warnings": {}
}
def __init__(self, bot: Red):
self.config = Config.get_conf(self, identifier=5757575755)
self.config.register_guild(**self.default_guild)
self.config.register_member(**self.default_member)
self.bot = bot
loop = asyncio.get_event_loop()
loop.create_task(self.register_warningtype())
@staticmethod
async def register_warningtype():
try:
await modlog.register_casetype(
"warning", True, "\N{WARNING SIGN}", "Warning", None
)
except RuntimeError:
pass
@commands.group()
@commands.guild_only()
@checks.guildowner_or_permissions(administrator=True)
async def warningset(self, ctx: RedContext):
"""Warning settings"""
if ctx.invoked_subcommand is None:
await ctx.send_help()
@warningset.command()
@commands.guild_only()
async def allowcustomreasons(self, ctx: RedContext, allowed: bool):
"""Allow or disallow custom reasons for a warning"""
guild = ctx.guild
await self.config.guild(guild).allow_custom_reasons.set(allowed)
await ctx.send(
_("Custom reasons have been {}").format(_("enabled") if allowed else _("disabled"))
)
@commands.group()
@commands.guild_only()
@checks.guildowner_or_permissions(administrator=True)
async def warnaction(self, ctx: RedContext):
"""Action management"""
if ctx.invoked_subcommand is None:
await ctx.send_help()
@warnaction.command(name="add")
@commands.guild_only()
async def action_add(self, ctx: RedContext, name: str, points: int):
"""Create an action to be taken at a specified point count
Duplicate action names are not allowed"""
guild = ctx.guild
await ctx.send("Would you like to enter commands to be run? (y/n)")
def same_author_check(m):
return m.author == ctx.author
try:
msg = await ctx.bot.wait_for("message", check=same_author_check, timeout=30)
except asyncio.TimeoutError:
await ctx.send(_("Ok then"))
return
if msg.content.lower() == "y":
exceed_command = await get_command_for_exceeded_points(ctx)
if exceed_command is None:
return
drop_command = await get_command_for_dropping_points(ctx)
if drop_command is None:
return
else:
exceed_command = None
drop_command = None
to_add = {
"action_name": name,
"points": points,
"exceed_command": exceed_command,
"drop_command": drop_command
}
# Have all details for the action, now save the action
guild_settings = self.config.guild(guild)
async with guild_settings.actions() as registered_actions:
for act in registered_actions:
if act["action_name"] == to_add["action_name"]:
await ctx.send(_("Duplicate action name found!"))
break
else:
registered_actions.append(to_add)
# Sort in descending order by point count for ease in
# finding the highest possible action to take
registered_actions.sort(key=lambda a: a["point_count"], reverse=True)
await ctx.tick()
@warnaction.command(name="del")
@commands.guild_only()
async def action_del(self, ctx: RedContext, action_name: str):
"""Delete the point count action with the specified name"""
guild = ctx.guild
guild_settings = self.config.guild(guild)
async with guild_settings.actions() as registered_actions:
to_remove = None
for act in registered_actions:
if act["action_name"] == action_name:
to_remove = act
break
if to_remove:
registered_actions.remove(to_remove)
@commands.group()
@commands.guild_only()
@checks.guildowner_or_permissions(administrator=True)
async def warnreason(self, ctx: RedContext):
"""Add reasons for warnings"""
if ctx.invoked_subcommand is None:
await ctx.send_help()
@warnreason.command(name="add")
@commands.guild_only()
async def reason_add(self, ctx: RedContext, name: str, points: int, *, description: str):
"""Add a reason to be available for warnings"""
guild = ctx.guild
if name.lower() == "custom":
await ctx.send("That cannot be used as a reason name!")
return
to_add = {
"points": points,
"description": description
}
completed = {
name.lower(): to_add
}
guild_settings = self.config.guild(guild)
async with guild_settings.reasons() as registered_reasons:
registered_reasons.update(completed)
await ctx.send(_("That reason has been registered"))
@warnreason.command(name="del")
@commands.guild_only()
async def reason_del(self, ctx: RedContext, reason_name: str):
"""Delete the reason with the specified name"""
guild = ctx.guild
guild_settings = self.config.guild(guild)
async with guild_settings.reasons() as registered_reasons:
if registered_reasons.pop(reason_name.lower(), None):
await ctx.send(_("Removed reason {}").format(reason_name))
else:
await ctx.send(_("That is not a registered reason name"))
@commands.command()
@commands.guild_only()
@checks.admin_or_permissions(ban_members=True)
async def reasonlist(self, ctx: RedContext):
"""List all configured reasons for warnings"""
guild = ctx.guild
guild_settings = self.config.guild(guild)
msg_list = []
async with guild_settings.reasons() as registered_reasons:
for r in registered_reasons.keys():
msg_list.append(
"Name: {}\nPoints: {}\nAction: {}".format(
r, r["points"], r["action"]
)
)
await ctx.send_interactive(msg_list)
@commands.command()
@commands.guild_only()
@checks.admin_or_permissions(ban_members=True)
async def actionlist(self, ctx: RedContext):
"""List the actions to be taken at specific point values"""
guild = ctx.guild
guild_settings = self.config.guild(guild)
msg_list = []
async with guild_settings.actions() as registered_actions:
for r in registered_actions.keys():
msg_list.append(
"Name: {}\nPoints: {}\nDescription: {}".format(
r, r["points"], r["description"]
)
)
await ctx.send_interactive(msg_list)
@commands.command()
@commands.guild_only()
@checks.admin_or_permissions(ban_members=True)
async def warn(self, ctx: RedContext, user: discord.Member, reason: str):
"""Warn the user for the specified reason
Reason must be a registered reason, or custom if custom reasons are allowed"""
reason_type = {}
if reason.lower() == "custom":
custom_allowed = await self.config.guild(ctx.guild).allow_custom_reasons()
if not custom_allowed:
await ctx.send(
_(
"Custom reasons are not allowed! Please see {} for "
"a complete list of valid reasons"
).format(
"`{}reasonlist`".format(ctx.prefix)
)
)
return
reason_type = await self.custom_warning_reason(ctx)
else:
guild_settings = self.config.guild(ctx.guild)
async with guild_settings.reasons() as registered_reasons:
if reason.lower() not in registered_reasons:
await ctx.send(_("That is not a registered reason!"))
else:
reason_type = registered_reasons[reason.lower()]
member_settings = self.config.member(user)
current_point_count = await member_settings.total_points()
warning_to_add = {
str(ctx.message.id): {
"points": reason_type["points"],
"description": reason_type["description"],
"mod": ctx.author.id
}
}
async with member_settings.warnings() as user_warnings:
user_warnings.update(warning_to_add)
current_point_count += reason_type["points"]
await member_settings.total_points.set(current_point_count)
await warning_points_add_check(self.config, ctx, user, current_point_count)
await ctx.tick()
@commands.command()
@commands.guild_only()
async def warnings(self, ctx: RedContext, userid: int=None):
"""Show warnings for the specified user.
If userid is None, show warnings for the person running the command
Note that showing warnings for users other than yourself requires
appropriate permissions"""
if userid is None:
user = ctx.author
else:
if not is_admin_or_superior(self.bot, ctx.author):
await ctx.send(
warning(
_("You are not allowed to check "
"warnings for other users!")
)
)
return
else:
user = ctx.guild.get_member(userid)
if user is None: # user not in guild
user = namedtuple("Member", "id guild")(userid, ctx.guild)
msg = ""
member_settings = self.config.member(user)
async with member_settings.warnings() as user_warnings:
if not user_warnings.keys(): # no warnings for the user
await ctx.send(_("That user has no warnings!"))
else:
for key in user_warnings.keys():
mod = ctx.guild.get_member(user_warnings[key]["mod"])
if mod is None:
mod = discord.utils.get(
self.bot.get_all_members(),
id=user_warnings[key]["mod"]
)
if mod is None:
mod = await self.bot.get_user_info(
user_warnings[key]["mod"]
)
msg += "{} point warning {} issued by {} for {}\n".format(
user_warnings[key]["points"],
key,
mod,
user_warnings[key]["description"]
)
await ctx.send_interactive(
pagify(msg), box_lang="Warnings for {}".format(user)
)
@commands.command()
@commands.guild_only()
@checks.admin_or_permissions(ban_members=True)
async def unwarn(self, ctx: RedContext, user_id: int, warn_id: str):
"""Removes the specified warning from the user specified"""
guild = ctx.guild
member = guild.get_member(user_id)
if member is None: # no longer in guild, but need a "member" object
member = namedtuple("Member", "guild id")(guild, user_id)
member_settings = self.config.member(member)
current_point_count = await member_settings.total_points()
await warning_points_remove_check(self.config, ctx, member, current_point_count)
async with member_settings.warnings() as user_warnings:
if warn_id not in user_warnings.keys():
await ctx.send("That warning doesn't exist!")
return
else:
current_point_count -= user_warnings[warn_id]["points"]
await member_settings.total_points.set(current_point_count)
user_warnings.pop(warn_id)
await ctx.tick()
@staticmethod
async def custom_warning_reason(ctx: RedContext):
"""Handles getting description and points for custom reasons"""
to_add = {
"points": 0,
"description": ""
}
def same_author_check(m):
return m.author == ctx.author
await ctx.send(_("How many points should be given for this reason?"))
try:
msg = await ctx.bot.wait_for("message", check=same_author_check, timeout=30)
except asyncio.TimeoutError:
await ctx.send(_("Ok then"))
return
try:
int(msg.content)
except ValueError:
await ctx.send(_("That isn't a number!"))
return
else:
if int(msg.content) <= 0:
await ctx.send(_("The point value needs to be greater than 0!"))
return
to_add["points"] = int(msg.content)
await ctx.send(_("Enter a description for this reason"))
try:
msg = await ctx.bot.wait_for("message", check=same_author_check, timeout=30)
except asyncio.TimeoutError:
await ctx.send(_("Ok then"))
return
to_add["description"] = msg.content
return to_add