[i18n] Pass over modlog, permissions, reports, streams, trivia, warnings

Signed-off-by: Toby Harradine <tobyharradine@gmail.com>
This commit is contained in:
Toby Harradine 2018-10-05 17:37:26 +10:00
parent fa692ccc0b
commit 443f2ca556
10 changed files with 498 additions and 435 deletions

View File

@ -386,7 +386,7 @@ class Mod(commands.Cog):
async def ban(
self, ctx: commands.Context, user: discord.Member, days: str = None, *, reason: str = None
):
"""Ban a user from the current server.
"""Ban a user from this server.
Deletes `<days>` worth of messages.
@ -469,7 +469,7 @@ class Mod(commands.Cog):
@commands.bot_has_permissions(ban_members=True)
@checks.admin_or_permissions(ban_members=True)
async def hackban(self, ctx: commands.Context, user_id: int, *, reason: str = None):
"""Pre-emptively ban a user from the current server.
"""Pre-emptively ban a user from this server.
A user ID needs to be provided in order to ban
using this command.
@ -531,7 +531,7 @@ class Mod(commands.Cog):
async def tempban(
self, ctx: commands.Context, user: discord.Member, days: int = 1, *, reason: str = None
):
"""Temporarily ban a user from the current server."""
"""Temporarily ban a user from this server."""
guild = ctx.guild
author = ctx.author
days_delta = timedelta(days=int(days))
@ -672,7 +672,7 @@ class Mod(commands.Cog):
@commands.bot_has_permissions(ban_members=True)
@checks.admin_or_permissions(ban_members=True)
async def unban(self, ctx: commands.Context, user_id: int, *, reason: str = None):
"""Unban a user from the current server.
"""Unban a user from this server.
Requires specifying the target user's ID. To find this, you may either:
1. Copy it from the mod log case (if one was created), or
@ -1104,7 +1104,7 @@ class Mod(commands.Cog):
async def unmute_channel(
self, ctx: commands.Context, user: discord.Member, *, reason: str = None
):
"""Unmute a user in the current channel."""
"""Unmute a user in this channel."""
channel = ctx.channel
author = ctx.author
guild = ctx.guild
@ -1137,7 +1137,7 @@ class Mod(commands.Cog):
async def unmute_guild(
self, ctx: commands.Context, user: discord.Member, *, reason: str = None
):
"""Unmute a user in the current server."""
"""Unmute a user in this server."""
guild = ctx.guild
author = ctx.author
@ -1236,7 +1236,7 @@ class Mod(commands.Cog):
@ignore.command(name="server", aliases=["guild"])
@checks.admin_or_permissions(manage_guild=True)
async def ignore_guild(self, ctx: commands.Context):
"""Ignore commands in the current server."""
"""Ignore commands in this server."""
guild = ctx.guild
if not await self.settings.guild(guild).ignored():
await self.settings.guild(guild).ignored.set(True)
@ -1270,7 +1270,7 @@ class Mod(commands.Cog):
@unignore.command(name="server", aliases=["guild"])
@checks.admin_or_permissions(manage_guild=True)
async def unignore_guild(self, ctx: commands.Context):
"""Remove the current server from the ignore list."""
"""Remove this server from the ignore list."""
guild = ctx.message.guild
if await self.settings.guild(guild).ignored():
await self.settings.guild(guild).ignored.set(False)

View File

@ -1,3 +1,5 @@
from typing import Optional
import discord
from redbot.core import checks, modlog, commands
@ -10,7 +12,7 @@ _ = Translator("ModLog", __file__)
@cog_i18n(_)
class ModLog(commands.Cog):
"""Log for mod actions"""
"""Manage log channels for moderation actions."""
def __init__(self, bot: Red):
super().__init__()
@ -19,23 +21,28 @@ class ModLog(commands.Cog):
@commands.group()
@checks.guildowner_or_permissions(administrator=True)
async def modlogset(self, ctx: commands.Context):
"""Settings for the mod log"""
"""Manage modlog settings."""
pass
@modlogset.command()
@commands.guild_only()
async def modlog(self, ctx: commands.Context, channel: discord.TextChannel = None):
"""Sets a channel as mod log
"""Set a channel as the modlog.
Leaving the channel parameter empty will deactivate it"""
Omit `<channel>` to disable the modlog.
"""
guild = ctx.guild
if channel:
if channel.permissions_for(guild.me).send_messages:
await modlog.set_modlog_channel(guild, channel)
await ctx.send(_("Mod events will be sent to {}").format(channel.mention))
await ctx.send(
_("Mod events will be sent to {channel}").format(channel=channel.mention)
)
else:
await ctx.send(
_("I do not have permissions to send messages in {}!").format(channel.mention)
_("I do not have permissions to send messages in {channel}!").format(
channel=channel.mention
)
)
else:
try:
@ -49,39 +56,36 @@ class ModLog(commands.Cog):
@modlogset.command(name="cases")
@commands.guild_only()
async def set_cases(self, ctx: commands.Context, action: str = None):
"""Enables or disables case creation for each type of mod action"""
"""Enable or disable case creation for a mod action."""
guild = ctx.guild
if action is None: # No args given
casetypes = await modlog.get_all_casetypes(guild)
await ctx.send_help()
title = _("Current settings:")
msg = ""
lines = []
for ct in casetypes:
enabled = await ct.is_enabled()
value = "enabled" if enabled else "disabled"
msg += "%s : %s\n" % (ct.name, value)
enabled = "enabled" if await ct.is_enabled() else "disabled"
lines.append(f"{ct.name} : {enabled}")
msg = title + "\n" + box(msg)
await ctx.send(msg)
await ctx.send(_("Current settings:\n") + box("\n".join(lines)))
return
casetype = await modlog.get_casetype(action, guild)
if not casetype:
await ctx.send(_("That action is not registered"))
else:
enabled = await casetype.is_enabled()
await casetype.set_enabled(True if not enabled else False)
msg = _("Case creation for {} actions is now {}.").format(
action, "enabled" if not enabled else "disabled"
await casetype.set_enabled(not enabled)
await ctx.send(
_("Case creation for {action_name} actions is now {enabled}.").format(
action_name=action, enabled="enabled" if not enabled else "disabled"
)
)
await ctx.send(msg)
@modlogset.command()
@commands.guild_only()
async def resetcases(self, ctx: commands.Context):
"""Resets modlog's cases"""
"""Reset all modlog cases in this server."""
guild = ctx.guild
await modlog.reset_cases(guild)
await ctx.send(_("Cases have been reset."))
@ -89,7 +93,7 @@ class ModLog(commands.Cog):
@commands.command()
@commands.guild_only()
async def case(self, ctx: commands.Context, number: int):
"""Shows the specified case"""
"""Show the specified case."""
try:
case = await modlog.get_case(number, ctx.guild, self.bot)
except RuntimeError:
@ -101,24 +105,21 @@ class ModLog(commands.Cog):
else:
await ctx.send(await case.message_content(embed=False))
@commands.command(usage="[case] <reason>")
@commands.command()
@commands.guild_only()
async def reason(self, ctx: commands.Context, *, reason: str):
"""Lets you specify a reason for mod-log's cases
async def reason(self, ctx: commands.Context, case: Optional[int], *, reason: str):
"""Specify a reason for a modlog case.
Please note that you can only edit cases you are
the owner of unless you are a mod/admin or the server owner.
the owner of unless you are a mod, admin or server owner.
If no number is specified, the latest case will be used."""
If no case number is specified, the latest case will be used.
"""
author = ctx.author
guild = ctx.guild
potential_case = reason.split()[0]
if potential_case.isdigit():
case = int(potential_case)
reason = reason.replace(potential_case, "")
else:
case = str(int(await modlog.get_next_case_number(guild)) - 1)
# latest case
if case is None:
# get the latest case
case = int(await modlog.get_next_case_number(guild)) - 1
try:
case_before = await modlog.get_case(case, guild, self.bot)
except RuntimeError:

View File

@ -1,5 +1,9 @@
from typing import NamedTuple, Union, Optional
from typing import NamedTuple, Union, Optional, cast, Type
from redbot.core import commands
from redbot.core.i18n import Translator
_ = Translator("PermissionsConverters", __file__)
class CogOrCommand(NamedTuple):
@ -18,39 +22,34 @@ class CogOrCommand(NamedTuple):
return cls(type="COMMAND", name=cmd.qualified_name, obj=cmd)
raise commands.BadArgument(
'Cog or command "{arg}" not found. Please note that this is case sensitive.'
"".format(arg=arg)
_(
'Cog or command "{name}" not found. Please note that this is case sensitive.'
).format(name=arg)
)
class RuleType:
def RuleType(arg: str) -> bool:
if arg.lower() in ("allow", "whitelist", "allowed"):
return True
if arg.lower() in ("deny", "blacklist", "denied"):
return False
# noinspection PyUnusedLocal
@classmethod
async def convert(cls, ctx: commands.Context, arg: str) -> bool:
if arg.lower() in ("allow", "whitelist", "allowed"):
return True
if arg.lower() in ("deny", "blacklist", "denied"):
return False
raise commands.BadArgument(
'"{arg}" is not a valid rule. Valid rules are "allow" or "deny"'.format(arg=arg)
)
raise commands.BadArgument(
_('"{arg}" is not a valid rule. Valid rules are "allow" or "deny"').format(arg=arg)
)
class ClearableRuleType:
def ClearableRuleType(arg: str) -> Optional[bool]:
if arg.lower() in ("allow", "whitelist", "allowed"):
return True
if arg.lower() in ("deny", "blacklist", "denied"):
return False
if arg.lower() in ("clear", "reset"):
return None
# noinspection PyUnusedLocal
@classmethod
async def convert(cls, ctx: commands.Context, arg: str) -> Optional[bool]:
if arg.lower() in ("allow", "whitelist", "allowed"):
return True
if arg.lower() in ("deny", "blacklist", "denied"):
return False
if arg.lower() in ("clear", "reset"):
return None
raise commands.BadArgument(
raise commands.BadArgument(
_(
'"{arg}" is not a valid rule. Valid rules are "allow" or "deny", or "clear" to '
"remove the rule".format(arg=arg)
)
"remove the rule"
).format(arg=arg)
)

View File

@ -2,7 +2,7 @@ import asyncio
import io
import textwrap
from copy import copy
from typing import Union, Optional, Dict, List, Tuple, Any, Iterator, ItemsView
from typing import Union, Optional, Dict, List, Tuple, Any, Iterator, ItemsView, cast
import discord
import yaml
@ -287,9 +287,11 @@ class Permissions(commands.Cog):
`<who_or_what>` is the user, channel, role or server the rule
is for.
"""
# noinspection PyTypeChecker
await self._add_rule(
rule=allow_or_deny, cog_or_cmd=cog_or_command, model_id=who_or_what.id, guild_id=0
rule=cast(bool, allow_or_deny),
cog_or_cmd=cog_or_command,
model_id=who_or_what.id,
guild_id=0,
)
await ctx.send(_("Rule added."))
@ -312,9 +314,8 @@ class Permissions(commands.Cog):
`<who_or_what>` is the user, channel or role the rule is for.
"""
# noinspection PyTypeChecker
await self._add_rule(
rule=allow_or_deny,
rule=cast(bool, allow_or_deny),
cog_or_cmd=cog_or_command,
model_id=who_or_what.id,
guild_id=ctx.guild.id,
@ -381,9 +382,10 @@ class Permissions(commands.Cog):
`<cog_or_command>` is the cog or command to set the default
rule for. This is case sensitive.
"""
# noinspection PyTypeChecker
await self._set_default_rule(
rule=allow_or_deny, cog_or_cmd=cog_or_command, guild_id=ctx.guild.id
rule=cast(Optional[bool], allow_or_deny),
cog_or_cmd=cog_or_command,
guild_id=ctx.guild.id,
)
await ctx.send(_("Default set."))
@ -403,9 +405,8 @@ class Permissions(commands.Cog):
`<cog_or_command>` is the cog or command to set the default
rule for. This is case sensitive.
"""
# noinspection PyTypeChecker
await self._set_default_rule(
rule=allow_or_deny, cog_or_cmd=cog_or_command, guild_id=GLOBAL
rule=cast(Optional[bool], allow_or_deny), cog_or_cmd=cog_or_command, guild_id=GLOBAL
)
await ctx.send(_("Default set."))

View File

@ -1,6 +1,6 @@
import logging
import asyncio
from typing import Union
from typing import Union, List
from datetime import timedelta
from copy import copy
import contextlib
@ -60,23 +60,20 @@ class Reports(commands.Cog):
@commands.guild_only()
@commands.group(name="reportset")
async def reportset(self, ctx: commands.Context):
"""
Settings for the report system.
"""
"""Manage Reports."""
pass
@checks.admin_or_permissions(manage_guild=True)
@reportset.command(name="output")
async def setoutput(self, ctx: commands.Context, channel: discord.TextChannel):
"""Set the channel where reports will show up"""
async def reportset_output(self, ctx: commands.Context, channel: discord.TextChannel):
"""Set the channel where reports will be sent."""
await self.config.guild(ctx.guild).output_channel.set(channel.id)
await ctx.send(_("The report channel has been set."))
@checks.admin_or_permissions(manage_guild=True)
@reportset.command(name="toggle", aliases=["toggleactive"])
async def report_toggle(self, ctx: commands.Context):
"""Enables or Disables reporting for the server"""
async def reportset_toggle(self, ctx: commands.Context):
"""Enable or Disable reporting for this server."""
active = await self.config.guild(ctx.guild).active()
active = not active
await self.config.guild(ctx.guild).active.set(active)
@ -168,7 +165,7 @@ class Reports(commands.Cog):
if channel is None:
return None
files = await Tunnel.files_from_attatch(msg)
files: List[discord.File] = await Tunnel.files_from_attatch(msg)
ticket_number = await self.config.guild(guild).next_ticket()
await self.config.guild(guild).next_ticket.set(ticket_number + 1)
@ -204,11 +201,10 @@ class Reports(commands.Cog):
@commands.group(name="report", invoke_without_command=True)
async def report(self, ctx: commands.Context, *, _report: str = ""):
"""
Send a report.
"""Send a report.
Use without arguments for interactive reporting, or do
[p]report <text> to use it non-interactively.
`[p]report <text>` to use it non-interactively.
"""
author = ctx.author
guild = ctx.guild
@ -323,8 +319,7 @@ class Reports(commands.Cog):
@checks.mod_or_permissions(manage_members=True)
@report.command(name="interact")
async def response(self, ctx, ticket_number: int):
"""
Open a message tunnel.
"""Open a message tunnel.
This tunnel will forward things you say in this channel
to the ticket opener's direct messages.
@ -354,8 +349,7 @@ class Reports(commands.Cog):
)
big_topic = _(
"{who} opened a 2-way communication "
"about ticket number {ticketnum}. Anything you say or upload here "
" Anything you say or upload here "
"(8MB file size limitation on uploads) "
"will be forwarded to them until the communication is closed.\n"
"You can close a communication at any point by reacting with "
@ -364,8 +358,12 @@ class Reports(commands.Cog):
"\N{WHITE HEAVY CHECK MARK}.\n"
"Tunnels are not persistent across bot restarts."
)
topic = big_topic.format(
ticketnum=ticket_number, who=_("A moderator in `{guild.name}` has").format(guild=guild)
topic = (
_(
"A moderator in the server `{guild.name}` has opened a 2-way communication about "
"ticket number {ticket_number}."
).format(guild=guild, ticket_number=ticket_number)
+ big_topic
)
try:
m = await tun.communicate(message=ctx.message, topic=topic, skip_message_content=True)
@ -373,4 +371,9 @@ class Reports(commands.Cog):
await ctx.send(_("That user has DMs disabled."))
else:
self.tunnel_store[(guild, ticket_number)] = {"tun": tun, "msgs": m}
await ctx.send(big_topic.format(who=_("You have"), ticketnum=ticket_number))
await ctx.send(
_(
"You have opened a 2-way communication about ticket number {ticket_number}."
).format(ticket_number=ticket_number)
+ big_topic
)

View File

@ -1,3 +1,5 @@
import contextlib
import discord
from redbot.core import Config, checks, commands
from redbot.core.utils.chat_formatting import pagify
@ -22,7 +24,7 @@ from .errors import (
StreamsError,
InvalidTwitchCredentials,
)
from . import streamtypes as StreamClasses
from . import streamtypes as _streamtypes
from collections import defaultdict
import asyncio
import re
@ -76,14 +78,14 @@ class Streams(commands.Cog):
@commands.command()
async def twitch(self, ctx: commands.Context, channel_name: str):
"""Checks if a Twitch channel is live"""
"""Check if a Twitch channel is live."""
token = await self.db.tokens.get_raw(TwitchStream.__name__, default=None)
stream = TwitchStream(name=channel_name, token=token)
await self.check_online(ctx, stream)
@commands.command()
async def youtube(self, ctx: commands.Context, channel_id_or_name: str):
"""Checks if a Youtube channel is live"""
"""Check if a YouTube channel is live."""
apikey = await self.db.tokens.get_raw(YoutubeStream.__name__, default=None)
is_name = self.check_name_or_id(channel_id_or_name)
if is_name:
@ -94,23 +96,24 @@ class Streams(commands.Cog):
@commands.command()
async def hitbox(self, ctx: commands.Context, channel_name: str):
"""Checks if a Hitbox channel is live"""
"""Check if a Hitbox channel is live."""
stream = HitboxStream(name=channel_name)
await self.check_online(ctx, stream)
@commands.command()
async def mixer(self, ctx: commands.Context, channel_name: str):
"""Checks if a Mixer channel is live"""
"""Check if a Mixer channel is live."""
stream = MixerStream(name=channel_name)
await self.check_online(ctx, stream)
@commands.command()
async def picarto(self, ctx: commands.Context, channel_name: str):
"""Checks if a Picarto channel is live"""
"""Check if a Picarto channel is live."""
stream = PicartoStream(name=channel_name)
await self.check_online(ctx, stream)
async def check_online(self, ctx: commands.Context, stream):
@staticmethod
async def check_online(ctx: commands.Context, stream):
try:
embed = await stream.is_online()
except OfflineStream:
@ -119,15 +122,17 @@ class Streams(commands.Cog):
await ctx.send(_("That channel doesn't seem to exist."))
except InvalidTwitchCredentials:
await ctx.send(
_("The twitch token is either invalid or has not been set. See `{}`.").format(
"{}streamset twitchtoken".format(ctx.prefix)
)
_(
"The Twitch token is either invalid or has not been set. See "
"`{prefix}streamset twitchtoken`."
).format(prefix=ctx.prefix)
)
except InvalidYoutubeCredentials:
await ctx.send(
_("Your Youtube API key is either invalid or has not been set. See {}.").format(
"`{}streamset youtubekey`".format(ctx.prefix)
)
_(
"The YouTube API key is either invalid or has not been set. See "
"`{prefix}streamset youtubekey`."
).format(prefix=ctx.prefix)
)
except APIError:
await ctx.send(
@ -140,11 +145,12 @@ class Streams(commands.Cog):
@commands.guild_only()
@checks.mod()
async def streamalert(self, ctx: commands.Context):
"""Manage automated stream alerts."""
pass
@streamalert.group(name="twitch", invoke_without_command=True)
async def _twitch(self, ctx: commands.Context, channel_name: str = None):
"""Twitch stream alerts"""
"""Manage Twitch stream notifications."""
if channel_name is not None:
await ctx.invoke(self.twitch_alert_channel, channel_name)
else:
@ -152,7 +158,7 @@ class Streams(commands.Cog):
@_twitch.command(name="channel")
async def twitch_alert_channel(self, ctx: commands.Context, channel_name: str):
"""Sets a Twitch alert notification in the channel"""
"""Toggle alerts in this channel for a Twitch stream."""
if re.fullmatch(r"<#\d+>", channel_name):
await ctx.send("Please supply the name of a *Twitch* channel, not a Discord channel.")
return
@ -160,33 +166,39 @@ class Streams(commands.Cog):
@_twitch.command(name="community")
async def twitch_alert_community(self, ctx: commands.Context, community: str):
"""Sets an alert notification in the channel for the specified twitch community."""
"""Toggle alerts in this channel for a Twitch community."""
await self.community_alert(ctx, TwitchCommunity, community.lower())
@streamalert.command(name="youtube")
async def youtube_alert(self, ctx: commands.Context, channel_name_or_id: str):
"""Sets a Youtube alert notification in the channel"""
"""Toggle alerts in this channel for a YouTube stream."""
await self.stream_alert(ctx, YoutubeStream, channel_name_or_id)
@streamalert.command(name="hitbox")
async def hitbox_alert(self, ctx: commands.Context, channel_name: str):
"""Sets a Hitbox alert notification in the channel"""
"""Toggle alerts in this channel for a Hitbox stream."""
await self.stream_alert(ctx, HitboxStream, channel_name)
@streamalert.command(name="mixer")
async def mixer_alert(self, ctx: commands.Context, channel_name: str):
"""Sets a Mixer alert notification in the channel"""
"""Toggle alerts in this channel for a Mixer stream."""
await self.stream_alert(ctx, MixerStream, channel_name)
@streamalert.command(name="picarto")
async def picarto_alert(self, ctx: commands.Context, channel_name: str):
"""Sets a Picarto alert notification in the channel"""
"""Toggle alerts in this channel for a Picarto stream."""
await self.stream_alert(ctx, PicartoStream, channel_name)
@streamalert.command(name="stop")
@streamalert.command(name="stop", usage="[disable_all=No]")
async def streamalert_stop(self, ctx: commands.Context, _all: bool = False):
"""Stops all stream notifications in the channel
Adding 'yes' will disable all notifications in the server"""
"""Disable all stream alerts in this channel or server.
`[p]streamalert stop` will disable this channel's stream
alerts.
Do `[p]streamalert stop yes` to disable all stream alerts in
this server.
"""
streams = self.streams.copy()
local_channel_ids = [c.id for c in ctx.guild.channels]
to_remove = []
@ -208,9 +220,10 @@ class Streams(commands.Cog):
self.streams = streams
await self.save_streams()
msg = _("All the alerts in the {} have been disabled.").format(
"server" if _all else "channel"
)
if _all:
msg = _("All the stream alerts in this server have been disabled.")
else:
msg = _("All the stream alerts in this channel have been disabled.")
await ctx.send(msg)
@ -250,16 +263,18 @@ class Streams(commands.Cog):
exists = await self.check_exists(stream)
except InvalidTwitchCredentials:
await ctx.send(
_("Your twitch token is either invalid or has not been set. See {}.").format(
"`{}streamset twitchtoken`".format(ctx.prefix)
)
_(
"The Twitch token is either invalid or has not been set. See "
"`{prefix}streamset twitchtoken`."
).format(prefix=ctx.prefix)
)
return
except InvalidYoutubeCredentials:
await ctx.send(
_(
"Your Youtube API key is either invalid or has not been set. See {}."
).format("`{}streamset youtubekey`".format(ctx.prefix))
"The YouTube API key is either invalid or has not been set. See "
"`{prefix}streamset youtubekey`."
).format(prefix=ctx.prefix)
)
return
except APIError:
@ -283,9 +298,10 @@ class Streams(commands.Cog):
await community.get_community_streams()
except InvalidTwitchCredentials:
await ctx.send(
_("The twitch token is either invalid or has not been set. See {}.").format(
"`{}streamset twitchtoken`".format(ctx.prefix)
)
_(
"The Twitch token is either invalid or has not been set. See "
"`{prefix}streamset twitchtoken`."
).format(prefix=ctx.prefix)
)
return
except CommunityNotFound:
@ -309,14 +325,15 @@ class Streams(commands.Cog):
@streamset.command()
@checks.is_owner()
async def twitchtoken(self, ctx: commands.Context, token: str):
"""Set the Client ID for twitch.
"""Set the Client ID for Twitch.
To do this, follow these steps:
1. Go to this page: https://dev.twitch.tv/dashboard/apps.
2. Click *Register Your Application*
3. Enter a name, set the OAuth Redirect URI to `http://localhost`, and
select an Application Category of your choosing.
4. Click *Register*, and on the following page, copy the Client ID.
5. Paste the Client ID into this command. Done!
1. Go to this page: https://dev.twitch.tv/dashboard/apps.
2. Click *Register Your Application*
3. Enter a name, set the OAuth Redirect URI to `http://localhost`, and
select an Application Category of your choosing.
4. Click *Register*, and on the following page, copy the Client ID.
5. Paste the Client ID into this command. Done!
"""
await self.db.tokens.set_raw("TwitchStream", value=token)
await self.db.tokens.set_raw("TwitchCommunity", value=token)
@ -325,64 +342,59 @@ class Streams(commands.Cog):
@streamset.command()
@checks.is_owner()
async def youtubekey(self, ctx: commands.Context, key: str):
"""Sets the API key for Youtube.
"""Set the API key for YouTube.
To get one, do the following:
1. Create a project (see https://support.google.com/googleapi/answer/6251787 for details)
2. Enable the Youtube Data API v3 (see https://support.google.com/googleapi/answer/6158841 for instructions)
3. Set up your API key (see https://support.google.com/googleapi/answer/6158862 for instructions)
2. Enable the YouTube Data API v3 (see https://support.google.com/googleapi/answer/6158841
for instructions)
3. Set up your API key (see https://support.google.com/googleapi/answer/6158862 for
instructions)
4. Copy your API key and paste it into this command. Done!
"""
await self.db.tokens.set_raw("YoutubeStream", value=key)
await ctx.send(_("Youtube key set."))
await ctx.send(_("YouTube key set."))
@streamset.group()
@commands.guild_only()
async def mention(self, ctx: commands.Context):
"""Sets mentions for alerts."""
"""Manage mention settings for stream alerts."""
pass
@mention.command(aliases=["everyone"])
@commands.guild_only()
async def all(self, ctx: commands.Context):
"""Toggles everyone mention"""
"""Toggle the `@\u200beveryone` mention."""
guild = ctx.guild
current_setting = await self.db.guild(guild).mention_everyone()
if current_setting:
await self.db.guild(guild).mention_everyone.set(False)
await ctx.send(
_("{} will no longer be mentioned when a stream or community is live").format(
"@\u200beveryone"
)
)
await ctx.send(_("`@\u200beveryone` will no longer be mentioned for stream alerts."))
else:
await self.db.guild(guild).mention_everyone.set(True)
await ctx.send(
_("When a stream or community " "is live, {} will be mentioned.").format(
"@\u200beveryone"
)
_("When a stream or community is live, `@\u200beveryone` will be mentioned.")
)
@mention.command(aliases=["here"])
@commands.guild_only()
async def online(self, ctx: commands.Context):
"""Toggles here mention"""
"""Toggle the `@\u200bhere` mention."""
guild = ctx.guild
current_setting = await self.db.guild(guild).mention_here()
if current_setting:
await self.db.guild(guild).mention_here.set(False)
await ctx.send(_("{} will no longer be mentioned for an alert.").format("@\u200bhere"))
await ctx.send(_("`@\u200bhere` will no longer be mentioned for stream alerts."))
else:
await self.db.guild(guild).mention_here.set(True)
await ctx.send(
_("When a stream or community " "is live, {} will be mentioned.").format(
"@\u200bhere"
)
_("When a stream or community is live, `@\u200bhere` will be mentioned.")
)
@mention.command()
@commands.guild_only()
async def role(self, ctx: commands.Context, *, role: discord.Role):
"""Toggles role mention"""
"""Toggle a role mention."""
current_setting = await self.db.role(role).mention()
if not role.mentionable:
await ctx.send("That role is not mentionable!")
@ -390,27 +402,27 @@ class Streams(commands.Cog):
if current_setting:
await self.db.role(role).mention.set(False)
await ctx.send(
_("{} will no longer be mentioned for an alert.").format(
"@\u200b{}".format(role.name)
_("`@\u200b{role.name}` will no longer be mentioned for stream alerts.").format(
role=role
)
)
else:
await self.db.role(role).mention.set(True)
await ctx.send(
_("When a stream or community " "is live, {} will be mentioned." "").format(
"@\u200b{}".format(role.name)
)
_(
"When a stream or community is live, `@\u200b{role.name}` will be mentioned."
).format(role=role)
)
@streamset.command()
@commands.guild_only()
async def autodelete(self, ctx: commands.Context, on_off: bool):
"""Toggles automatic deletion of notifications for streams that go offline"""
"""Toggle alert deletion for when streams go offline."""
await self.db.guild(ctx.guild).autodelete.set(on_off)
if on_off:
await ctx.send("The notifications will be deleted once streams go offline.")
await ctx.send(_("The notifications will be deleted once streams go offline."))
else:
await ctx.send("Notifications will never be deleted.")
await ctx.send(_("Notifications will no longer be deleted."))
async def add_or_remove(self, ctx: commands.Context, stream):
if ctx.channel.id not in stream.channels:
@ -418,18 +430,18 @@ class Streams(commands.Cog):
if stream not in self.streams:
self.streams.append(stream)
await ctx.send(
_("I'll now send a notification in this channel when {} is live.").format(
stream.name
)
_(
"I'll now send a notification in this channel when {stream.name} is live."
).format(stream=stream)
)
else:
stream.channels.remove(ctx.channel.id)
if not stream.channels:
self.streams.remove(stream)
await ctx.send(
_("I won't send notifications about {} in this channel anymore.").format(
stream.name
)
_(
"I won't send notifications about {stream.name} in this channel anymore."
).format(stream=stream)
)
await self.save_streams()
@ -442,9 +454,8 @@ class Streams(commands.Cog):
await ctx.send(
_(
"I'll send a notification in this channel when a "
"channel is live in the {} community."
""
).format(community.name)
"channel is live in the {community.name} community."
).format(community=community)
)
else:
community.channels.remove(ctx.channel.id)
@ -453,9 +464,8 @@ class Streams(commands.Cog):
await ctx.send(
_(
"I won't send notifications about channels streaming "
"in the {} community in this channel anymore."
""
).format(community.name)
"in the {community.name} community in this channel anymore."
).format(community=community)
)
await self.save_communities()
@ -481,7 +491,8 @@ class Streams(commands.Cog):
if community.type == _class.__name__ and community.name.lower() == name.lower():
return community
async def check_exists(self, stream):
@staticmethod
async def check_exists(stream):
try:
await stream.is_online()
except OfflineStream:
@ -506,40 +517,36 @@ class Streams(commands.Cog):
async def check_streams(self):
for stream in self.streams:
try:
embed = await stream.is_online()
except OfflineStream:
if not stream._messages_cache:
continue
for message in stream._messages_cache:
try:
autodelete = await self.db.guild(message.guild).autodelete()
if autodelete:
await message.delete()
except:
pass
stream._messages_cache.clear()
await self.save_streams()
except:
pass
else:
if stream._messages_cache:
continue
for channel_id in stream.channels:
channel = self.bot.get_channel(channel_id)
mention_str = await self._get_mention_str(channel.guild)
with contextlib.suppress(Exception):
try:
embed = await stream.is_online()
except OfflineStream:
if not stream._messages_cache:
continue
for message in stream._messages_cache:
with contextlib.suppress(Exception):
autodelete = await self.db.guild(message.guild).autodelete()
if autodelete:
await message.delete()
stream._messages_cache.clear()
await self.save_streams()
else:
if stream._messages_cache:
continue
for channel_id in stream.channels:
channel = self.bot.get_channel(channel_id)
mention_str = await self._get_mention_str(channel.guild)
if mention_str:
content = "{}, {} is live!".format(mention_str, stream.name)
else:
content = "{} is live!".format(stream.name)
if mention_str:
content = _("{mention}, {stream.name} is live!").format(
mention=mention_str, stream=stream
)
else:
content = _("{stream.name} is live!").format(stream=stream.name)
try:
m = await channel.send(content, embed=embed)
stream._messages_cache.append(m)
await self.save_streams()
except:
pass
async def _get_mention_str(self, guild: discord.Guild):
settings = self.db.guild(guild)
@ -555,45 +562,46 @@ class Streams(commands.Cog):
async def check_communities(self):
for community in self.communities:
try:
stream_list = await community.get_community_streams()
except CommunityNotFound:
print(_("The Community {} was not found!").format(community.name))
continue
except OfflineCommunity:
if not community._messages_cache:
with contextlib.suppress(Exception):
try:
stream_list = await community.get_community_streams()
except CommunityNotFound:
print(
_("The Community {community.name} was not found!").format(
community=community
)
)
continue
for message in community._messages_cache:
try:
autodelete = await self.db.guild(message.guild).autodelete()
if autodelete:
await message.delete()
except:
pass
community._messages_cache.clear()
await self.save_communities()
except:
pass
else:
for channel in community.channels:
chn = self.bot.get_channel(channel)
streams = await self.filter_streams(stream_list, chn)
emb = await community.make_embed(streams)
chn_msg = [m for m in community._messages_cache if m.channel == chn]
if not chn_msg:
mentions = await self._get_mention_str(chn.guild)
if mentions:
msg = await chn.send(mentions, embed=emb)
except OfflineCommunity:
if not community._messages_cache:
continue
for message in community._messages_cache:
with contextlib.suppress(Exception):
autodelete = await self.db.guild(message.guild).autodelete()
if autodelete:
await message.delete()
community._messages_cache.clear()
await self.save_communities()
else:
for channel in community.channels:
chn = self.bot.get_channel(channel)
streams = await self.filter_streams(stream_list, chn)
emb = await community.make_embed(streams)
chn_msg = [m for m in community._messages_cache if m.channel == chn]
if not chn_msg:
mentions = await self._get_mention_str(chn.guild)
if mentions:
msg = await chn.send(mentions, embed=emb)
else:
msg = await chn.send(embed=emb)
community._messages_cache.append(msg)
await self.save_communities()
else:
msg = await chn.send(embed=emb)
community._messages_cache.append(msg)
await self.save_communities()
else:
chn_msg = sorted(chn_msg, key=lambda x: x.created_at, reverse=True)[0]
community._messages_cache.remove(chn_msg)
await chn_msg.edit(embed=emb)
community._messages_cache.append(chn_msg)
await self.save_communities()
chn_msg = sorted(chn_msg, key=lambda x: x.created_at, reverse=True)[0]
community._messages_cache.remove(chn_msg)
await chn_msg.edit(embed=emb)
community._messages_cache.append(chn_msg)
await self.save_communities()
async def filter_streams(self, streams: list, channel: discord.TextChannel) -> list:
filtered = []
@ -611,7 +619,7 @@ class Streams(commands.Cog):
streams = []
for raw_stream in await self.db.streams():
_class = getattr(StreamClasses, raw_stream["type"], None)
_class = getattr(_streamtypes, raw_stream["type"], None)
if not _class:
continue
raw_msg_cache = raw_stream["messages"]
@ -631,7 +639,7 @@ class Streams(commands.Cog):
communities = []
for raw_community in await self.db.communities():
_class = getattr(StreamClasses, raw_community["type"], None)
_class = getattr(_streamtypes, raw_community["type"], None)
if not _class:
continue
raw_msg_cache = raw_community["messages"]

View File

@ -4,20 +4,30 @@ import time
import random
from collections import Counter
import discord
from redbot.core.bank import deposit_credits
from redbot.core.utils.chat_formatting import box
from redbot.core import bank
from redbot.core.i18n import Translator
from redbot.core.utils.chat_formatting import box, bold, humanize_list
from redbot.core.utils.common_filters import normalize_smartquotes
from .log import LOG
__all__ = ["TriviaSession"]
_REVEAL_MESSAGES = ("I know this one! {}!", "Easy: {}.", "Oh really? It's {} of course.")
_FAIL_MESSAGES = (
"To the next one I guess...",
"Moving on...",
"I'm sure you'll know the answer of the next one.",
"\N{PENSIVE FACE} Next one.",
T_ = Translator("TriviaSession", __file__)
_ = lambda s: s
_REVEAL_MESSAGES = (
_("I know this one! {answer}!"),
_("Easy: {answer}."),
_("Oh really? It's {answer} of course."),
)
_FAIL_MESSAGES = (
_("To the next one I guess..."),
_("Moving on..."),
_("I'm sure you'll know the answer of the next one."),
_("\N{PENSIVE FACE} Next one."),
)
_ = T_
class TriviaSession:
@ -104,7 +114,7 @@ class TriviaSession:
async with self.ctx.typing():
await asyncio.sleep(3)
self.count += 1
msg = "**Question number {}!**\n\n{}".format(self.count, question)
msg = bold(_("**Question number {num}!").format(num=self.count)) + "\n\n" + question
await self.ctx.send(msg)
continue_ = await self.wait_for_answer(answers, delay, timeout)
if continue_ is False:
@ -113,7 +123,7 @@ class TriviaSession:
await self.end_game()
break
else:
await self.ctx.send("There are no more questions!")
await self.ctx.send(_("There are no more questions!"))
await self.end_game()
async def _send_startup_msg(self):
@ -121,20 +131,13 @@ class TriviaSession:
for idx, tup in enumerate(self.settings["lists"].items()):
name, author = tup
if author:
title = "{} (by {})".format(name, author)
title = _("{trivia_list} (by {author})").format(trivia_list=name, author=author)
else:
title = name
list_names.append(title)
num_lists = len(list_names)
if num_lists > 2:
# at least 3 lists, join all but last with comma
msg = ", ".join(list_names[: num_lists - 1])
# join onto last with "and"
msg = " and ".join((msg, list_names[num_lists - 1]))
else:
# either 1 or 2 lists, join together with "and"
msg = " and ".join(list_names)
await self.ctx.send("Starting Trivia: " + msg)
await self.ctx.send(
_("Starting Trivia: {list_names}").format(list_names=humanize_list(list_names))
)
def _iter_questions(self):
"""Iterate over questions and answers for this session.
@ -179,20 +182,20 @@ class TriviaSession:
)
except asyncio.TimeoutError:
if time.time() - self._last_response >= timeout:
await self.ctx.send("Guys...? Well, I guess I'll stop then.")
await self.ctx.send(_("Guys...? Well, I guess I'll stop then."))
self.stop()
return False
if self.settings["reveal_answer"]:
reply = random.choice(_REVEAL_MESSAGES).format(answers[0])
reply = T_(random.choice(_REVEAL_MESSAGES)).format(answer=answers[0])
else:
reply = random.choice(_FAIL_MESSAGES)
reply = T_(random.choice(_FAIL_MESSAGES))
if self.settings["bot_plays"]:
reply += " **+1** for me!"
reply += _(" **+1** for me!")
self.scores[self.ctx.guild.me] += 1
await self.ctx.send(reply)
else:
self.scores[message.author] += 1
reply = "You got it {}! **+1** to you!".format(message.author.display_name)
reply = _("You got it {user}! **+1** to you!").format(user=message.author.display_name)
await self.ctx.send(reply)
return True
@ -282,10 +285,16 @@ class TriviaSession:
amount = int(multiplier * score)
if amount > 0:
LOG.debug("Paying trivia winner: %d credits --> %s", amount, str(winner))
await deposit_credits(winner, int(multiplier * score))
await bank.deposit_credits(winner, int(multiplier * score))
await self.ctx.send(
"Congratulations, {0}, you have received {1} credits"
" for coming first.".format(winner.display_name, amount)
_(
"Congratulations, {user}, you have received {num} {currency}"
" for coming first."
).format(
user=winner.display_name,
num=amount,
currency=await bank.get_currency_name(self.ctx.guild),
)
)
@ -313,9 +322,9 @@ def _parse_answers(answers):
for answer in answers:
if isinstance(answer, bool):
if answer is True:
ret.extend(["True", "Yes"])
ret.extend(["True", "Yes", _("Yes")])
else:
ret.extend(["False", "No"])
ret.extend(["False", "No", _("No")])
else:
ret.append(str(answer))
# Uniquify list

View File

@ -7,7 +7,8 @@ import discord
from redbot.core import commands
from redbot.core import Config, checks
from redbot.core.data_manager import cog_data_path
from redbot.core.utils.chat_formatting import box, pagify
from redbot.core.i18n import Translator, cog_i18n
from redbot.core.utils.chat_formatting import box, pagify, bold
from redbot.cogs.bank import check_global_setting_admin
from .log import LOG
from .session import TriviaSession
@ -16,6 +17,8 @@ __all__ = ["Trivia", "UNIQUE_ID", "get_core_lists"]
UNIQUE_ID = 0xB3C0E453
_ = Translator("Trivia", __file__)
class InvalidListError(Exception):
"""A Trivia list file is in invalid format."""
@ -23,6 +26,7 @@ class InvalidListError(Exception):
pass
@cog_i18n(_)
class Trivia(commands.Cog):
"""Play trivia with friends!"""
@ -47,20 +51,21 @@ class Trivia(commands.Cog):
@commands.guild_only()
@checks.mod_or_permissions(administrator=True)
async def triviaset(self, ctx: commands.Context):
"""Manage trivia settings."""
"""Manage Trivia settings."""
if ctx.invoked_subcommand is None:
settings = self.conf.guild(ctx.guild)
settings_dict = await settings.all()
msg = box(
"**Current settings**\n"
"Bot gains points: {bot_plays}\n"
"Answer time limit: {delay} seconds\n"
"Lack of response timeout: {timeout} seconds\n"
"Points to win: {max_score}\n"
"Reveal answer on timeout: {reveal_answer}\n"
"Payout multiplier: {payout_multiplier}\n"
"Allow lists to override settings: {allow_override}"
"".format(**settings_dict),
_(
"**Current settings**\n"
"Bot gains points: {bot_plays}\n"
"Answer time limit: {delay} seconds\n"
"Lack of response timeout: {timeout} seconds\n"
"Points to win: {max_score}\n"
"Reveal answer on timeout: {reveal_answer}\n"
"Payout multiplier: {payout_multiplier}\n"
"Allow lists to override settings: {allow_override}"
).format(**settings_dict),
lang="py",
)
await ctx.send(msg)
@ -69,33 +74,34 @@ class Trivia(commands.Cog):
async def triviaset_max_score(self, ctx: commands.Context, score: int):
"""Set the total points required to win."""
if score < 0:
await ctx.send("Score must be greater than 0.")
await ctx.send(_("Score must be greater than 0."))
return
settings = self.conf.guild(ctx.guild)
await settings.max_score.set(score)
await ctx.send("Done. Points required to win set to {}.".format(score))
await ctx.send(_("Done. Points required to win set to {num}.").format(num=score))
@triviaset.command(name="timelimit")
async def triviaset_timelimit(self, ctx: commands.Context, seconds: float):
"""Set the maximum seconds permitted to answer a question."""
if seconds < 4.0:
await ctx.send("Must be at least 4 seconds.")
await ctx.send(_("Must be at least 4 seconds."))
return
settings = self.conf.guild(ctx.guild)
await settings.delay.set(seconds)
await ctx.send("Done. Maximum seconds to answer set to {}.".format(seconds))
await ctx.send(_("Done. Maximum seconds to answer set to {num}.").format(num=seconds))
@triviaset.command(name="stopafter")
async def triviaset_stopafter(self, ctx: commands.Context, seconds: float):
"""Set how long until trivia stops due to no response."""
settings = self.conf.guild(ctx.guild)
if seconds < await settings.delay():
await ctx.send("Must be larger than the answer time limit.")
await ctx.send(_("Must be larger than the answer time limit."))
return
await settings.timeout.set(seconds)
await ctx.send(
"Done. Trivia sessions will now time out after {}"
" seconds of no responses.".format(seconds)
_(
"Done. Trivia sessions will now time out after {num} seconds of no responses."
).format(num=seconds)
)
@triviaset.command(name="override")
@ -103,46 +109,46 @@ class Trivia(commands.Cog):
"""Allow/disallow trivia lists to override settings."""
settings = self.conf.guild(ctx.guild)
await settings.allow_override.set(enabled)
enabled = "now" if enabled else "no longer"
await ctx.send(
"Done. Trivia lists can {} override the trivia settings"
" for this server.".format(enabled)
)
if enabled:
await ctx.send(
_(
"Done. Trivia lists can now override the trivia settings for this server."
).format(now=enabled)
)
else:
await ctx.send(
_(
"Done. Trivia lists can no longer override the trivia settings for this "
"server."
).format(now=enabled)
)
@triviaset.command(name="botplays")
async def trivaset_bot_plays(self, ctx: commands.Context, true_or_false: bool):
@triviaset.command(name="botplays", usage="<true_or_false>")
async def trivaset_bot_plays(self, ctx: commands.Context, enabled: bool):
"""Set whether or not the bot gains points.
If enabled, the bot will gain a point if no one guesses correctly.
"""
settings = self.conf.guild(ctx.guild)
await settings.bot_plays.set(true_or_false)
await ctx.send(
"Done. "
+ (
"I'll gain a point if users don't answer in time."
if true_or_false
else "Alright, I won't embarass you at trivia anymore."
)
)
await settings.bot_plays.set(enabled)
if enabled:
await ctx.send(_("Done. I'll now gain a point if users don't answer in time."))
else:
await ctx.send(_("Alright, I won't embarass you at trivia anymore."))
@triviaset.command(name="revealanswer")
async def trivaset_reveal_answer(self, ctx: commands.Context, true_or_false: bool):
@triviaset.command(name="revealanswer", usage="<true_or_false>")
async def trivaset_reveal_answer(self, ctx: commands.Context, enabled: bool):
"""Set whether or not the answer is revealed.
If enabled, the bot will reveal the answer if no one guesses correctly
in time.
"""
settings = self.conf.guild(ctx.guild)
await settings.reveal_answer.set(true_or_false)
await ctx.send(
"Done. "
+ (
"I'll reveal the answer if no one knows it."
if true_or_false
else "I won't reveal the answer to the questions anymore."
)
)
await settings.reveal_answer.set(enabled)
if enabled:
await ctx.send(_("Done. I'll reveal the answer if no one knows it."))
else:
await ctx.send(_("Alright, I won't reveal the answer to the questions anymore."))
@triviaset.command(name="payout")
@check_global_setting_admin()
@ -158,13 +164,13 @@ class Trivia(commands.Cog):
"""
settings = self.conf.guild(ctx.guild)
if multiplier < 0:
await ctx.send("Multiplier must be at least 0.")
await ctx.send(_("Multiplier must be at least 0."))
return
await settings.payout_multiplier.set(multiplier)
if not multiplier:
await ctx.send("Done. I will no longer reward the winner with a payout.")
return
await ctx.send("Done. Payout multiplier set to {}.".format(multiplier))
if multiplier:
await ctx.send(_("Done. Payout multiplier set to {num}.").format(num=multiplier))
else:
await ctx.send(_("Done. I will no longer reward the winner with a payout."))
@commands.group(invoke_without_command=True)
@commands.guild_only()
@ -180,7 +186,7 @@ class Trivia(commands.Cog):
categories = [c.lower() for c in categories]
session = self._get_trivia_session(ctx.channel)
if session is not None:
await ctx.send("There is already an ongoing trivia session in this channel.")
await ctx.send(_("There is already an ongoing trivia session in this channel."))
return
trivia_dict = {}
authors = []
@ -191,15 +197,17 @@ class Trivia(commands.Cog):
dict_ = self.get_trivia_list(category)
except FileNotFoundError:
await ctx.send(
"Invalid category `{0}`. See `{1}trivia list`"
" for a list of trivia categories."
"".format(category, ctx.prefix)
_(
"Invalid category `{name}`. See `{prefix}trivia list` for a list of "
"trivia categories."
).format(name=category, prefix=ctx.prefix)
)
except InvalidListError:
await ctx.send(
"There was an error parsing the trivia list for"
" the `{}` category. It may be formatted"
" incorrectly.".format(category)
_(
"There was an error parsing the trivia list for the `{name}` category. It "
"may be formatted incorrectly."
).format(name=category)
)
else:
trivia_dict.update(dict_)
@ -208,7 +216,7 @@ class Trivia(commands.Cog):
return
if not trivia_dict:
await ctx.send(
"The trivia list was parsed successfully, however it appears to be empty!"
_("The trivia list was parsed successfully, however it appears to be empty!")
)
return
settings = await self.conf.guild(ctx.guild).all()
@ -225,7 +233,7 @@ class Trivia(commands.Cog):
"""Stop an ongoing trivia session."""
session = self._get_trivia_session(ctx.channel)
if session is None:
await ctx.send("There is no ongoing trivia session in this channel.")
await ctx.send(_("There is no ongoing trivia session in this channel."))
return
author = ctx.author
auth_checks = (
@ -238,20 +246,28 @@ class Trivia(commands.Cog):
if any(auth_checks):
await session.end_game()
session.force_stop()
await ctx.send("Trivia stopped.")
await ctx.send(_("Trivia stopped."))
else:
await ctx.send("You are not allowed to do that.")
await ctx.send(_("You are not allowed to do that."))
@trivia.command(name="list")
async def trivia_list(self, ctx: commands.Context):
"""List available trivia categories."""
lists = set(p.stem for p in self._all_lists())
msg = box("**Available trivia lists**\n\n{}".format(", ".join(sorted(lists))))
if len(msg) > 1000:
await ctx.author.send(msg)
return
await ctx.send(msg)
if await ctx.embed_requested():
await ctx.send(
embed=discord.Embed(
title=_("Available trivia lists"),
colour=await ctx.embed_colour(),
description=", ".join(sorted(lists)),
)
)
else:
msg = box(bold(_("Available trivia lists")) + "\n\n" + ", ".join(sorted(lists)))
if len(msg) > 1000:
await ctx.author.send(msg)
else:
await ctx.send(msg)
@trivia.group(name="leaderboard", aliases=["lboard"], autohelp=False)
async def trivia_leaderboard(self, ctx: commands.Context):
@ -273,19 +289,21 @@ class Trivia(commands.Cog):
):
"""Leaderboard for this server.
<sort_by> can be any of the following fields:
- wins : total wins
- avg : average score
- total : total correct answers
`<sort_by>` can be any of the following fields:
- `wins` : total wins
- `avg` : average score
- `total` : total correct answers
- `games` : total games played
<top> is the number of ranks to show on the leaderboard.
`<top>` is the number of ranks to show on the leaderboard.
"""
key = self._get_sort_key(sort_by)
if key is None:
await ctx.send(
"Unknown field `{}`, see `{}help trivia "
"leaderboard server` for valid fields to sort by."
"".format(sort_by, ctx.prefix)
_(
"Unknown field `{field_name}`, see `{prefix}help trivia leaderboard server` "
"for valid fields to sort by."
).format(field_name=sort_by, prefix=ctx.prefix)
)
return
guild = ctx.guild
@ -300,20 +318,21 @@ class Trivia(commands.Cog):
):
"""Global trivia leaderboard.
<sort_by> can be any of the following fields:
- wins : total wins
- avg : average score
- total : total correct answers from all sessions
- games : total games played
`<sort_by>` can be any of the following fields:
- `wins` : total wins
- `avg` : average score
- `total` : total correct answers from all sessions
- `games` : total games played
<top> is the number of ranks to show on the leaderboard.
`<top>` is the number of ranks to show on the leaderboard.
"""
key = self._get_sort_key(sort_by)
if key is None:
await ctx.send(
"Unknown field `{}`, see `{}help trivia "
"leaderboard global` for valid fields to sort by."
"".format(sort_by, ctx.prefix)
_(
"Unknown field `{field_name}`, see `{prefix}help trivia leaderboard server` "
"for valid fields to sort by."
).format(field_name=sort_by, prefix=ctx.prefix)
)
return
data = await self.conf.all_members()
@ -365,7 +384,7 @@ class Trivia(commands.Cog):
"""
if not data:
await ctx.send("There are no scores on record!")
await ctx.send(_("There are no scores on record!"))
return
leaderboard = self._get_leaderboard(data, key, top)
ret = []
@ -386,7 +405,7 @@ class Trivia(commands.Cog):
try:
priority.remove(key)
except ValueError:
raise ValueError("{} is not a valid key.".format(key))
raise ValueError(f"{key} is not a valid key.")
# Put key last in reverse priority
priority.append(key)
items = data.items()
@ -395,16 +414,15 @@ class Trivia(commands.Cog):
max_name_len = max(map(lambda m: len(str(m)), data.keys()))
# Headers
headers = (
"Rank",
"Member{}".format(" " * (max_name_len - 6)),
"Wins",
"Games Played",
"Total Score",
"Average Score",
_("Rank"),
_("Member") + " " * (max_name_len - 6),
_("Wins"),
_("Games Played"),
_("Total Score"),
_("Average Score"),
)
lines = [" | ".join(headers)]
lines = [" | ".join(headers), " | ".join(("-" * len(h) for h in headers))]
# Header underlines
lines.append(" | ".join(("-" * len(h) for h in headers)))
for rank, tup in enumerate(items, 1):
member, m_data = tup
# Align fields to header width

View File

@ -22,7 +22,7 @@ _ = Translator("Warnings", __file__)
@cog_i18n(_)
class Warnings(commands.Cog):
"""A warning system for Red"""
"""Warn misbehaving users and take automated actions."""
default_guild = {"actions": [], "reasons": {}, "allow_custom_reasons": False}
@ -48,31 +48,42 @@ class Warnings(commands.Cog):
@commands.guild_only()
@checks.guildowner_or_permissions(administrator=True)
async def warningset(self, ctx: commands.Context):
"""Warning settings"""
"""Manage settings for Warnings."""
pass
@warningset.command()
@commands.guild_only()
async def allowcustomreasons(self, ctx: commands.Context, allowed: bool):
"""Enable or Disable custom reasons for a warning"""
"""Enable or disable 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"))
)
if allowed:
await ctx.send(_("Custom reasons have been enabled."))
else:
await ctx.send(_("Custom reasons have been disabled."))
@commands.group()
@commands.guild_only()
@checks.guildowner_or_permissions(administrator=True)
async def warnaction(self, ctx: commands.Context):
"""Action management"""
"""Manage automated actions for Warnings.
Actions are essentially command macros. Any command can be run
when the action is initially triggered, and/or when the action
is lifted.
Actions must be given a name and a points threshold. When a
user is warned enough so that their points go over this
threshold, the action will be executed.
"""
pass
@warnaction.command(name="add")
@commands.guild_only()
async def action_add(self, ctx: commands.Context, name: str, points: int):
"""Create an action to be taken at a specified point count
Duplicate action names are not allowed
"""Create an automated action.
Duplicate action names are not allowed.
"""
guild = ctx.guild
@ -103,7 +114,7 @@ class Warnings(commands.Cog):
@warnaction.command(name="del")
@commands.guild_only()
async def action_del(self, ctx: commands.Context, action_name: str):
"""Delete the point count action with the specified name"""
"""Delete the action with the specified name."""
guild = ctx.guild
guild_settings = self.config.guild(guild)
async with guild_settings.actions() as registered_actions:
@ -116,23 +127,29 @@ class Warnings(commands.Cog):
registered_actions.remove(to_remove)
await ctx.tick()
else:
await ctx.send(_("No action named {} exists!").format(action_name))
await ctx.send(_("No action named {name} exists!").format(name=action_name))
@commands.group()
@commands.guild_only()
@checks.guildowner_or_permissions(administrator=True)
async def warnreason(self, ctx: commands.Context):
"""Add reasons for warnings"""
"""Manage warning reasons.
Reasons must be given a name, description and points value. The
name of the reason must be given when a user is warned.
"""
pass
@warnreason.command(name="add")
@warnreason.command(name="create", aliases=["add"])
@commands.guild_only()
async def reason_add(self, ctx: commands.Context, name: str, points: int, *, description: str):
"""Add a reason to be available for warnings"""
async def reason_create(
self, ctx: commands.Context, name: str, points: int, *, description: str
):
"""Create a warning reason."""
guild = ctx.guild
if name.lower() == "custom":
await ctx.send("That cannot be used as a reason name!")
await ctx.send(_("*Custom* cannot be used as a reason name!"))
return
to_add = {"points": points, "description": description}
completed = {name.lower(): to_add}
@ -142,12 +159,12 @@ class Warnings(commands.Cog):
async with guild_settings.reasons() as registered_reasons:
registered_reasons.update(completed)
await ctx.send(_("That reason has been registered."))
await ctx.send(_("The new reason has been registered."))
@warnreason.command(name="del")
@warnreason.command(name="del", aliases=["remove"])
@commands.guild_only()
async def reason_del(self, ctx: commands.Context, reason_name: str):
"""Delete the reason with the specified name"""
"""Delete a warning reason."""
guild = ctx.guild
guild_settings = self.config.guild(guild)
async with guild_settings.reasons() as registered_reasons:
@ -160,7 +177,7 @@ class Warnings(commands.Cog):
@commands.guild_only()
@checks.admin_or_permissions(ban_members=True)
async def reasonlist(self, ctx: commands.Context):
"""List all configured reasons for warnings"""
"""List all configured reasons for Warnings."""
guild = ctx.guild
guild_settings = self.config.guild(guild)
msg_list = []
@ -174,9 +191,9 @@ class Warnings(commands.Cog):
msg_list.append(em)
else:
msg_list.append(
"Name: {}\nPoints: {}\nDescription: {}".format(
r, v["points"], v["description"]
)
_(
"Name: {reason_name}\nPoints: {points}\nDescription: {description}"
).format(reason_name=r, **v)
)
if msg_list:
await menu(ctx, msg_list, DEFAULT_CONTROLS)
@ -187,7 +204,7 @@ class Warnings(commands.Cog):
@commands.guild_only()
@checks.admin_or_permissions(ban_members=True)
async def actionlist(self, ctx: commands.Context):
"""List the actions to be taken at specific point values"""
"""List all configured automated actions for Warnings."""
guild = ctx.guild
guild_settings = self.config.guild(guild)
msg_list = []
@ -201,10 +218,10 @@ class Warnings(commands.Cog):
msg_list.append(em)
else:
msg_list.append(
"Name: {}\nPoints: {}\nExceed command: {}\n"
"Drop command: {}".format(
r["action_name"], r["points"], r["exceed_command"], r["drop_command"]
)
_(
"Name: {action_name}\nPoints: {points}\n"
"Exceed command: {exceed_command}\nDrop command: {drop_command}"
).format(**r)
)
if msg_list:
await menu(ctx, msg_list, DEFAULT_CONTROLS)
@ -215,8 +232,10 @@ class Warnings(commands.Cog):
@commands.guild_only()
@checks.admin_or_permissions(ban_members=True)
async def warn(self, ctx: commands.Context, 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
"""Warn the user for the specified reason.
`<reason>` must be a registered reason name, or *custom* if
custom reasons are enabled.
"""
if user == ctx.author:
await ctx.send(_("You cannot warn yourself."))
@ -226,9 +245,9 @@ class Warnings(commands.Cog):
if not custom_allowed:
await ctx.send(
_(
"Custom reasons are not allowed! Please see {} for "
"Custom reasons are not allowed! Please see `{prefix}reasonlist` for "
"a complete list of valid reasons."
).format("`{}reasonlist`".format(ctx.prefix))
).format(prefix=ctx.prefix)
)
return
reason_type = await self.custom_warning_reason(ctx)
@ -272,9 +291,7 @@ class Warnings(commands.Cog):
await warning_points_add_check(self.config, ctx, user, current_point_count)
try:
em = discord.Embed(
title=_("Warning from {mod_name}#{mod_discrim}").format(
mod_name=ctx.author.display_name, mod_discrim=ctx.author.discriminator
),
title=_("Warning from {user}").format(user=ctx.author),
description=reason_type["description"],
)
em.add_field(name=_("Points"), value=str(reason_type["points"]))
@ -286,19 +303,17 @@ class Warnings(commands.Cog):
)
except discord.HTTPException:
pass
await ctx.send(
_("User {user_name}#{user_discrim} has been warned.").format(
user_name=user.display_name, user_discrim=user.discriminator
)
)
await ctx.send(_("User {user} has been warned.").format(user=user))
@commands.command()
@commands.guild_only()
async def warnings(self, ctx: commands.Context, userid: int = None):
"""Show warnings for the specified user.
If userid is None, show warnings for the person running the command
"""List the warnings for the specified user.
Emit `<userid>` to see your own warnings.
Note that showing warnings for users other than yourself requires
appropriate permissions
appropriate permissions.
"""
if userid is None:
user = ctx.author
@ -326,18 +341,24 @@ class Warnings(commands.Cog):
)
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"]
msg += _(
"{num_points} point warning {reason_name} issued by {user} for "
"{description}\n"
).format(
num_points=user_warnings[key]["points"],
reason_name=key,
user=mod,
description=user_warnings[key]["description"],
)
await ctx.send_interactive(
pagify(msg, shorten_by=58), box_lang="Warnings for {}".format(user)
pagify(msg, shorten_by=58), box_lang=_("Warnings for {user}").format(user=user)
)
@commands.command()
@commands.guild_only()
@checks.admin_or_permissions(ban_members=True)
async def unwarn(self, ctx: commands.Context, user_id: int, warn_id: str):
"""Removes the specified warning from the user specified"""
"""Remove a warning from a user."""
if user_id == ctx.author.id:
await ctx.send(_("You cannot remove warnings from yourself."))
return
@ -351,7 +372,7 @@ class Warnings(commands.Cog):
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!")
await ctx.send(_("That warning doesn't exist!"))
return
else:
current_point_count -= user_warnings[warn_id]["points"]

View File

@ -4,7 +4,7 @@ from redbot.core.utils.chat_formatting import pagify
import io
import sys
import weakref
from typing import List
from typing import List, Optional
from .common_filters import filter_mass_mentions
_instances = weakref.WeakValueDictionary({})
@ -86,7 +86,11 @@ class Tunnel(metaclass=TunnelMeta):
@staticmethod
async def message_forwarder(
*, destination: discord.abc.Messageable, content: str = None, embed=None, files=[]
*,
destination: discord.abc.Messageable,
content: str = None,
embed=None,
files: Optional[List[discord.File]] = None
) -> List[discord.Message]:
"""
This does the actual sending, use this instead of a full tunnel
@ -95,19 +99,19 @@ class Tunnel(metaclass=TunnelMeta):
Parameters
----------
destination: `discord.abc.Messageable`
destination: discord.abc.Messageable
Where to send
content: `str`
content: str
The message content
embed: `discord.Embed`
embed: discord.Embed
The embed to send
files: `list` of `discord.File`
files: Optional[List[discord.File]]
A list of files to send.
Returns
-------
list of `discord.Message`
The `discord.Message`\ (s) sent as a result
List[discord.Message]
The messages sent as a result.
Raises
------
@ -117,7 +121,6 @@ class Tunnel(metaclass=TunnelMeta):
see `discord.abc.Messageable.send`
"""
rets = []
files = files if files else None
if content:
for page in pagify(content):
rets.append(await destination.send(page, files=files, embed=embed))