mirror of
https://github.com/Cog-Creators/Red-DiscordBot.git
synced 2025-11-20 18:06:08 -05:00
[V3 Everything] Package bot and write setup scripts (#964)
Ya'll are gonna hate me. * Initial modifications * Add initial setup.py * working setup py help * Modify setup file to package stuff * Move a bunch of shit and fix imports * Fix or skip tests * Must add init files for find_packages to work * Move main to scripts folder and rename * Add shebangs * Copy over translation files * WORKING PIP INSTALL * add dependency information * Hardcoded version for now, will need to figure out a better way to do this * OKAY ITS FINALLY FUCKING WORKING * Add this guy * Fix stuff * Change readme to rst * Remove double sentry opt in * Oopsie * Fix this thing * Aaaand fix test * Aaaand fix test * Fix core cog importing and default cog install path * Adjust readme * change instance name from optional to required * Ayyy let's do more dependency injection
This commit is contained in:
0
redbot/__init__.py
Normal file
0
redbot/__init__.py
Normal file
155
redbot/__main__.py
Normal file
155
redbot/__main__.py
Normal file
@@ -0,0 +1,155 @@
|
||||
#!/usr/bin/env python
|
||||
|
||||
# Discord Version check
|
||||
|
||||
import sys
|
||||
|
||||
import discord
|
||||
|
||||
if discord.version_info.major < 1:
|
||||
print("You are not running the rewritten version of discord.py.\n\n"
|
||||
"In order to use Red v3 you MUST be running d.py version"
|
||||
" >= 1.0.0.")
|
||||
sys.exit(1)
|
||||
|
||||
from redbot.core.bot import Red, ExitCodes
|
||||
from redbot.core.cog_manager import CogManagerUI
|
||||
from redbot.core.data_manager import load_basic_configuration
|
||||
from redbot.core.global_checks import init_global_checks
|
||||
from redbot.core.events import init_events
|
||||
from redbot.core.sentry_setup import init_sentry_logging
|
||||
from redbot.core.cli import interactive_config, confirm, parse_cli_flags, ask_sentry
|
||||
from redbot.core.core_commands import Core
|
||||
from redbot.core.dev_commands import Dev
|
||||
import asyncio
|
||||
import logging.handlers
|
||||
import logging
|
||||
import os
|
||||
|
||||
|
||||
#
|
||||
# Red - Discord Bot v3
|
||||
#
|
||||
# Made by Twentysix, improved by many
|
||||
#
|
||||
|
||||
|
||||
def init_loggers(cli_flags):
|
||||
# d.py stuff
|
||||
dpy_logger = logging.getLogger("discord")
|
||||
dpy_logger.setLevel(logging.WARNING)
|
||||
console = logging.StreamHandler()
|
||||
console.setLevel(logging.WARNING)
|
||||
dpy_logger.addHandler(console)
|
||||
|
||||
# Red stuff
|
||||
|
||||
logger = logging.getLogger("red")
|
||||
|
||||
red_format = logging.Formatter(
|
||||
'%(asctime)s %(levelname)s %(module)s %(funcName)s %(lineno)d: '
|
||||
'%(message)s',
|
||||
datefmt="[%d/%m/%Y %H:%M]")
|
||||
|
||||
stdout_handler = logging.StreamHandler(sys.stdout)
|
||||
stdout_handler.setFormatter(red_format)
|
||||
|
||||
if cli_flags.debug:
|
||||
os.environ['PYTHONASYNCIODEBUG'] = '1'
|
||||
logger.setLevel(logging.DEBUG)
|
||||
else:
|
||||
logger.setLevel(logging.WARNING)
|
||||
|
||||
from redbot.core.data_manager import core_data_path
|
||||
logfile_path = core_data_path() / 'red.log'
|
||||
fhandler = logging.handlers.RotatingFileHandler(
|
||||
filename=str(logfile_path), encoding='utf-8', mode='a',
|
||||
maxBytes=10**7, backupCount=5)
|
||||
fhandler.setFormatter(red_format)
|
||||
|
||||
logger.addHandler(fhandler)
|
||||
logger.addHandler(stdout_handler)
|
||||
|
||||
# Sentry stuff
|
||||
sentry_logger = logging.getLogger("red.sentry")
|
||||
sentry_logger.setLevel(logging.WARNING)
|
||||
|
||||
return logger, sentry_logger
|
||||
|
||||
|
||||
async def _get_prefix_and_token(red, indict):
|
||||
"""
|
||||
Again, please blame <@269933075037814786> for this.
|
||||
:param indict:
|
||||
:return:
|
||||
"""
|
||||
indict['token'] = await red.db.token()
|
||||
indict['prefix'] = await red.db.prefix()
|
||||
indict['enable_sentry'] = await red.db.enable_sentry()
|
||||
|
||||
|
||||
def main():
|
||||
cli_flags = parse_cli_flags(sys.argv[1:])
|
||||
load_basic_configuration(cli_flags.instance_name)
|
||||
log, sentry_log = init_loggers(cli_flags)
|
||||
description = "Red v3 - Alpha"
|
||||
red = Red(cli_flags, description=description, pm_help=None)
|
||||
init_global_checks(red)
|
||||
init_events(red, cli_flags)
|
||||
red.add_cog(Core())
|
||||
red.add_cog(CogManagerUI())
|
||||
if cli_flags.dev:
|
||||
red.add_cog(Dev())
|
||||
loop = asyncio.get_event_loop()
|
||||
tmp_data = {}
|
||||
loop.run_until_complete(_get_prefix_and_token(red, tmp_data))
|
||||
token = os.environ.get("RED_TOKEN", tmp_data['token'])
|
||||
prefix = cli_flags.prefix or tmp_data['prefix']
|
||||
if token is None or not prefix:
|
||||
if cli_flags.no_prompt is False:
|
||||
new_token = interactive_config(red, token_set=bool(token),
|
||||
prefix_set=bool(prefix))
|
||||
if new_token:
|
||||
token = new_token
|
||||
else:
|
||||
log.critical("Token and prefix must be set in order to login.")
|
||||
sys.exit(1)
|
||||
loop.run_until_complete(_get_prefix_and_token(red, tmp_data))
|
||||
if tmp_data['enable_sentry']:
|
||||
init_sentry_logging(sentry_log)
|
||||
cleanup_tasks = True
|
||||
try:
|
||||
loop.run_until_complete(red.start(token, bot=not cli_flags.not_bot))
|
||||
except discord.LoginFailure:
|
||||
cleanup_tasks = False # No login happened, no need for this
|
||||
log.critical("This token doesn't seem to be valid. If it belongs to "
|
||||
"a user account, remember that the --not-bot flag "
|
||||
"must be used. For self-bot functionalities instead, "
|
||||
"--self-bot")
|
||||
db_token = red.db.token()
|
||||
if db_token and not cli_flags.no_prompt:
|
||||
print("\nDo you want to reset the token? (y/n)")
|
||||
if confirm("> "):
|
||||
loop.run_until_complete(red.db.token.set(""))
|
||||
print("Token has been reset.")
|
||||
except KeyboardInterrupt:
|
||||
log.info("Keyboard interrupt detected. Quitting...")
|
||||
loop.run_until_complete(red.logout())
|
||||
red._shutdown_mode = ExitCodes.SHUTDOWN
|
||||
except Exception as e:
|
||||
log.critical("Fatal exception", exc_info=e)
|
||||
sentry_log.critical("Fatal Exception", exc_info=e)
|
||||
loop.run_until_complete(red.logout())
|
||||
finally:
|
||||
if cleanup_tasks:
|
||||
pending = asyncio.Task.all_tasks(loop=red.loop)
|
||||
gathered = asyncio.gather(*pending, loop=red.loop)
|
||||
gathered.cancel()
|
||||
red.loop.run_until_complete(gathered)
|
||||
gathered.exception()
|
||||
|
||||
sys.exit(red._shutdown_mode.value)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
0
redbot/cogs/__init__.py
Normal file
0
redbot/cogs/__init__.py
Normal file
6
redbot/cogs/alias/__init__.py
Normal file
6
redbot/cogs/alias/__init__.py
Normal file
@@ -0,0 +1,6 @@
|
||||
from .alias import Alias
|
||||
from discord.ext import commands
|
||||
|
||||
|
||||
def setup(bot: commands.Bot):
|
||||
bot.add_cog(Alias(bot))
|
||||
368
redbot/cogs/alias/alias.py
Normal file
368
redbot/cogs/alias/alias.py
Normal file
@@ -0,0 +1,368 @@
|
||||
from copy import copy
|
||||
from typing import Generator, Tuple, Iterable
|
||||
|
||||
import discord
|
||||
from redbot.core import Config
|
||||
from redbot.core.i18n import CogI18n
|
||||
from redbot.core.utils.chat_formatting import box
|
||||
from discord.ext import commands
|
||||
|
||||
from redbot.core.bot import Red
|
||||
from .alias_entry import AliasEntry
|
||||
|
||||
_ = CogI18n("Alias", __file__)
|
||||
|
||||
|
||||
class Alias:
|
||||
"""
|
||||
Alias
|
||||
|
||||
Aliases are per server shortcuts for commands. They
|
||||
can act as both a lambda (storing arguments for repeated use)
|
||||
or as simply a shortcut to saying "x y z".
|
||||
|
||||
When run, aliases will accept any additional arguments
|
||||
and append them to the stored alias
|
||||
"""
|
||||
|
||||
default_global_settings = {
|
||||
"entries": []
|
||||
}
|
||||
|
||||
default_guild_settings = {
|
||||
"enabled": False,
|
||||
"entries": [] # Going to be a list of dicts
|
||||
}
|
||||
|
||||
def __init__(self, bot: Red):
|
||||
self.bot = bot
|
||||
self.file_path = "data/alias/aliases.json"
|
||||
self._aliases = Config.get_conf(self, 8927348724)
|
||||
|
||||
self._aliases.register_global(**self.default_global_settings)
|
||||
self._aliases.register_guild(**self.default_guild_settings)
|
||||
|
||||
async def unloaded_aliases(self, guild: discord.Guild) -> Generator[AliasEntry, None, None]:
|
||||
return (AliasEntry.from_json(d) for d in (await self._aliases.guild(guild).entries()))
|
||||
|
||||
async def unloaded_global_aliases(self) -> Generator[AliasEntry, None, None]:
|
||||
return (AliasEntry.from_json(d) for d in (await self._aliases.entries()))
|
||||
|
||||
async def loaded_aliases(self, guild: discord.Guild) -> Generator[AliasEntry, None, None]:
|
||||
return (AliasEntry.from_json(d, bot=self.bot)
|
||||
for d in (await self._aliases.guild(guild).entries()))
|
||||
|
||||
async def loaded_global_aliases(self) -> Generator[AliasEntry, None, None]:
|
||||
return (AliasEntry.from_json(d, bot=self.bot) for d in (await self._aliases.entries()))
|
||||
|
||||
async def is_alias(self, guild: discord.Guild, alias_name: str,
|
||||
server_aliases: Iterable[AliasEntry]=()) -> (bool, AliasEntry):
|
||||
|
||||
if not server_aliases:
|
||||
server_aliases = await self.unloaded_aliases(guild)
|
||||
|
||||
global_aliases = await self.unloaded_global_aliases()
|
||||
|
||||
for aliases in (server_aliases, global_aliases):
|
||||
for alias in aliases:
|
||||
if alias.name == alias_name:
|
||||
return True, alias
|
||||
|
||||
return False, None
|
||||
|
||||
def is_command(self, alias_name: str) -> bool:
|
||||
command = self.bot.get_command(alias_name)
|
||||
return command is not None
|
||||
|
||||
@staticmethod
|
||||
def is_valid_alias_name(alias_name: str) -> bool:
|
||||
return alias_name.isidentifier()
|
||||
|
||||
async def add_alias(self, ctx: commands.Context, alias_name: str,
|
||||
command: Tuple[str], global_: bool=False) -> AliasEntry:
|
||||
alias = AliasEntry(alias_name, command, ctx.author, global_=global_)
|
||||
|
||||
if global_:
|
||||
curr_aliases = await self._aliases.entries()
|
||||
curr_aliases.append(alias.to_json())
|
||||
await self._aliases.entries.set(curr_aliases)
|
||||
else:
|
||||
curr_aliases = await self._aliases.guild(ctx.guild).entries()
|
||||
|
||||
curr_aliases.append(alias.to_json())
|
||||
await self._aliases.guild(ctx.guild).entries.set(curr_aliases)
|
||||
|
||||
await self._aliases.guild(ctx.guild).enabled.set(True)
|
||||
return alias
|
||||
|
||||
async def delete_alias(self, ctx: commands.Context, alias_name: str,
|
||||
global_: bool=False) -> bool:
|
||||
if global_:
|
||||
aliases = await self.unloaded_global_aliases()
|
||||
setter_func = self._aliases.entries.set
|
||||
else:
|
||||
aliases = await self.unloaded_aliases(ctx.guild)
|
||||
setter_func = self._aliases.guild(ctx.guild).entries.set
|
||||
|
||||
did_delete_alias = False
|
||||
|
||||
to_keep = []
|
||||
for alias in aliases:
|
||||
if alias.name != alias_name:
|
||||
to_keep.append(alias)
|
||||
else:
|
||||
did_delete_alias = True
|
||||
|
||||
await setter_func(
|
||||
[a.to_json() for a in to_keep]
|
||||
)
|
||||
|
||||
return did_delete_alias
|
||||
|
||||
def get_prefix(self, message: discord.Message) -> str:
|
||||
"""
|
||||
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:
|
||||
"""
|
||||
guild = message.guild
|
||||
content = message.content
|
||||
prefixes = sorted(self.bot.command_prefix(self.bot, message),
|
||||
key=lambda pfx: len(pfx),
|
||||
reverse=True)
|
||||
for p in prefixes:
|
||||
if content.startswith(p):
|
||||
return p
|
||||
raise ValueError(_("No prefix found."))
|
||||
|
||||
def get_extra_args_from_alias(self, message: discord.Message, prefix: str,
|
||||
alias: AliasEntry) -> str:
|
||||
"""
|
||||
When an alias is executed by a user in chat this function tries
|
||||
to get any extra arguments passed in with the call.
|
||||
Whitespace will be trimmed from both ends.
|
||||
:param message:
|
||||
:param prefix:
|
||||
:param alias:
|
||||
:return:
|
||||
"""
|
||||
known_content_length = len(prefix) + len(alias.name)
|
||||
extra = message.content[known_content_length:].strip()
|
||||
return extra
|
||||
|
||||
async def maybe_call_alias(self, message: discord.Message,
|
||||
aliases: Iterable[AliasEntry]=None):
|
||||
try:
|
||||
prefix = self.get_prefix(message)
|
||||
except ValueError:
|
||||
return
|
||||
|
||||
try:
|
||||
potential_alias = message.content[len(prefix):].split(" ")[0]
|
||||
except IndexError:
|
||||
return False
|
||||
|
||||
is_alias, alias = await self.is_alias(message.guild, potential_alias, server_aliases=aliases)
|
||||
|
||||
if is_alias:
|
||||
await self.call_alias(message, prefix, alias)
|
||||
|
||||
async def call_alias(self, message: discord.Message, prefix: str,
|
||||
alias: AliasEntry):
|
||||
new_message = copy(message)
|
||||
args = self.get_extra_args_from_alias(message, prefix, alias)
|
||||
|
||||
# noinspection PyDunderSlots
|
||||
new_message.content = "{}{} {}".format(prefix, alias.command, args)
|
||||
await self.bot.process_commands(new_message)
|
||||
|
||||
@commands.group()
|
||||
@commands.guild_only()
|
||||
async def alias(self, ctx: commands.Context):
|
||||
"""Manage per-server aliases for commands"""
|
||||
if ctx.invoked_subcommand is None:
|
||||
await self.bot.send_cmd_help(ctx)
|
||||
|
||||
@alias.group(name="global")
|
||||
async def global_(self, ctx: commands.Context):
|
||||
"""
|
||||
Manage global aliases.
|
||||
"""
|
||||
if ctx.invoked_subcommand is None or \
|
||||
isinstance(ctx.invoked_subcommand, commands.Group):
|
||||
await self.bot.send_cmd_help(ctx)
|
||||
|
||||
@alias.command(name="add")
|
||||
@commands.guild_only()
|
||||
async def _add_alias(self, ctx: commands.Context,
|
||||
alias_name: str, *, command):
|
||||
"""
|
||||
Add an alias for a command.
|
||||
"""
|
||||
#region Alias Add Validity Checking
|
||||
is_command = self.is_command(alias_name)
|
||||
if is_command:
|
||||
await ctx.send(("You attempted to create a new alias"
|
||||
" with the name {} but that"
|
||||
" name is already a command on this bot.").format(alias_name))
|
||||
return
|
||||
|
||||
is_alias, _ = await self.is_alias(ctx.guild, alias_name)
|
||||
if is_alias:
|
||||
await ctx.send(("You attempted to create a new alias"
|
||||
" with the name {} but that"
|
||||
" alias already exists on this server.").format(alias_name))
|
||||
return
|
||||
|
||||
is_valid_name = self.is_valid_alias_name(alias_name)
|
||||
if not is_valid_name:
|
||||
await ctx.send(("You attempted to create a new alias"
|
||||
" with the name {} but that"
|
||||
" name is an invalid alias name. Alias"
|
||||
" names may only contain letters, numbers,"
|
||||
" and underscores and must start with a letter.").format(alias_name))
|
||||
return
|
||||
#endregion
|
||||
|
||||
# At this point we know we need to make a new alias
|
||||
# and that the alias name is valid.
|
||||
|
||||
await self.add_alias(ctx, alias_name, command)
|
||||
|
||||
await ctx.send(_("A new alias with the trigger `{}`"
|
||||
" has been created.").format(alias_name))
|
||||
|
||||
@global_.command(name="add")
|
||||
async def _add_global_alias(self, ctx: commands.Context,
|
||||
alias_name: str, *, command):
|
||||
"""
|
||||
Add a global alias for a command.
|
||||
"""
|
||||
# region Alias Add Validity Checking
|
||||
is_command = self.is_command(alias_name)
|
||||
if is_command:
|
||||
await ctx.send(("You attempted to create a new global alias"
|
||||
" with the name {} but that"
|
||||
" name is already a command on this bot.").format(alias_name))
|
||||
return
|
||||
|
||||
is_alias, _ = self.is_alias(ctx.guild, alias_name)
|
||||
if is_alias:
|
||||
await ctx.send(("You attempted to create a new alias"
|
||||
" with the name {} but that"
|
||||
" alias already exists on this server.").format(alias_name))
|
||||
return
|
||||
|
||||
is_valid_name = self.is_valid_alias_name(alias_name)
|
||||
if not is_valid_name:
|
||||
await ctx.send(("You attempted to create a new alias"
|
||||
" with the name {} but that"
|
||||
" name is an invalid alias name. Alias"
|
||||
" names may only contain letters, numbers,"
|
||||
" and underscores and must start with a letter.").format(alias_name))
|
||||
return
|
||||
# endregion
|
||||
|
||||
await self.add_alias(ctx, alias_name, command, global_=True)
|
||||
|
||||
await ctx.send(_("A new global alias with the trigger `{}`"
|
||||
" has been created.").format(alias_name))
|
||||
|
||||
@alias.command(name="help")
|
||||
@commands.guild_only()
|
||||
async def _help_alias(self, ctx: commands.Context, alias_name: str):
|
||||
"""Tries to execute help for the base command of the alias"""
|
||||
is_alias, alias = self.is_alias(ctx.guild, alias_name=alias_name)
|
||||
if is_alias:
|
||||
base_cmd = alias.command[0]
|
||||
|
||||
new_msg = copy(ctx.message)
|
||||
new_msg.content = "{}help {}".format(ctx.prefix, base_cmd)
|
||||
await self.bot.process_commands(new_msg)
|
||||
else:
|
||||
ctx.send(_("No such alias exists."))
|
||||
|
||||
@alias.command(name="show")
|
||||
@commands.guild_only()
|
||||
async def _show_alias(self, ctx: commands.Context, alias_name: str):
|
||||
"""Shows what command the alias executes."""
|
||||
is_alias, alias = await self.is_alias(ctx.guild, alias_name)
|
||||
|
||||
if is_alias:
|
||||
await ctx.send(_("The `{}` alias will execute the"
|
||||
" command `{}`").format(alias_name, alias.command))
|
||||
else:
|
||||
await ctx.send(_("There is no alias with the name `{}`").format(alias_name))
|
||||
|
||||
@alias.command(name="del")
|
||||
@commands.guild_only()
|
||||
async def _del_alias(self, ctx: commands.Context, alias_name: str):
|
||||
"""
|
||||
Deletes an existing alias on this server.
|
||||
"""
|
||||
aliases = await self.unloaded_aliases(ctx.guild)
|
||||
try:
|
||||
next(aliases)
|
||||
except StopIteration:
|
||||
await ctx.send(_("There are no aliases on this guild."))
|
||||
return
|
||||
|
||||
if await self.delete_alias(ctx, alias_name):
|
||||
await ctx.send(_("Alias with the name `{}` was successfully"
|
||||
" deleted.").format(alias_name))
|
||||
else:
|
||||
await ctx.send(_("Alias with name `{}` was not found.").format(alias_name))
|
||||
|
||||
@global_.command(name="del")
|
||||
async def _del_global_alias(self, ctx: commands.Context, alias_name: str):
|
||||
"""
|
||||
Deletes an existing global alias.
|
||||
"""
|
||||
aliases = await self.unloaded_global_aliases()
|
||||
try:
|
||||
next(aliases)
|
||||
except StopIteration:
|
||||
await ctx.send(_("There are no aliases on this bot."))
|
||||
return
|
||||
|
||||
if await self.delete_alias(ctx, alias_name, global_=True):
|
||||
await ctx.send(_("Alias with the name `{}` was successfully"
|
||||
" deleted.").format(alias_name))
|
||||
else:
|
||||
await ctx.send(_("Alias with name `{}` was not found.").format(alias_name))
|
||||
|
||||
@alias.command(name="list")
|
||||
@commands.guild_only()
|
||||
async def _list_alias(self, ctx: commands.Context):
|
||||
"""
|
||||
Lists the available aliases on this server.
|
||||
"""
|
||||
names = [_("Aliases:"), ] + sorted(["+ " + a.name for a in (await self.unloaded_aliases(ctx.guild))])
|
||||
if len(names) == 0:
|
||||
await ctx.send(_("There are no aliases on this server."))
|
||||
else:
|
||||
await ctx.send(box("\n".join(names), "diff"))
|
||||
|
||||
@global_.command(name="list")
|
||||
async def _list_global_alias(self, ctx: commands.Context):
|
||||
"""
|
||||
Lists the available global aliases on this bot.
|
||||
"""
|
||||
names = [_("Aliases:"), ] + sorted(["+ " + a.name for a in await self.unloaded_global_aliases()])
|
||||
if len(names) == 0:
|
||||
await ctx.send(_("There are no aliases on this server."))
|
||||
else:
|
||||
await ctx.send(box("\n".join(names), "diff"))
|
||||
|
||||
async def on_message(self, message: discord.Message):
|
||||
aliases = list(await self.unloaded_global_aliases())
|
||||
if message.guild is not None:
|
||||
aliases = aliases + list(await self.unloaded_aliases(message.guild))
|
||||
|
||||
if len(aliases) == 0:
|
||||
return
|
||||
|
||||
await self.maybe_call_alias(message, aliases=aliases)
|
||||
63
redbot/cogs/alias/alias_entry.py
Normal file
63
redbot/cogs/alias/alias_entry.py
Normal file
@@ -0,0 +1,63 @@
|
||||
from typing import Tuple
|
||||
from discord.ext import commands
|
||||
|
||||
import discord
|
||||
|
||||
|
||||
class AliasEntry:
|
||||
def __init__(self, name: str, command: Tuple[str],
|
||||
creator: discord.Member, global_: bool=False):
|
||||
super().__init__()
|
||||
self.has_real_data = False
|
||||
self.name = name
|
||||
self.command = command
|
||||
self.creator = creator
|
||||
|
||||
self.global_ = global_
|
||||
|
||||
self.guild = None
|
||||
if hasattr(creator, "guild"):
|
||||
self.guild = creator.guild
|
||||
|
||||
self.uses = 0
|
||||
|
||||
def inc(self):
|
||||
"""
|
||||
Increases the `uses` stat by 1.
|
||||
:return: new use count
|
||||
"""
|
||||
self.uses += 1
|
||||
return self.uses
|
||||
|
||||
def to_json(self) -> dict:
|
||||
try:
|
||||
creator = str(self.creator.id)
|
||||
guild = str(self.guild.id)
|
||||
except AttributeError:
|
||||
creator = self.creator
|
||||
guild = self.guild
|
||||
|
||||
return {
|
||||
"name": self.name,
|
||||
"command": self.command,
|
||||
"creator": creator,
|
||||
"guild": guild,
|
||||
"global": self.global_,
|
||||
"uses": self.uses
|
||||
}
|
||||
|
||||
@classmethod
|
||||
def from_json(cls, data: dict, bot: commands.Bot=None):
|
||||
ret = cls(data["name"], data["command"],
|
||||
data["creator"], global_=data["global"])
|
||||
|
||||
if bot:
|
||||
ret.has_real_data = True
|
||||
ret.creator = bot.get_user(int(data["creator"]))
|
||||
guild = bot.get_guild(int(data["guild"]))
|
||||
ret.guild = guild
|
||||
else:
|
||||
ret.guild = data["guild"]
|
||||
|
||||
ret.uses = data.get("uses", 0)
|
||||
return ret
|
||||
65
redbot/cogs/alias/locales/es.po
Normal file
65
redbot/cogs/alias/locales/es.po
Normal file
@@ -0,0 +1,65 @@
|
||||
# Copyright (C) 2017 Red-DiscordBot
|
||||
# UltimatePancake <pier.gaetani@gmail.com>, 2017.
|
||||
#
|
||||
msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: \n"
|
||||
"POT-Creation-Date: 2017-08-26 17:23+EDT\n"
|
||||
"PO-Revision-Date: 2017-08-26 18:37-0600\n"
|
||||
"Language-Team: \n"
|
||||
"MIME-Version: 1.0\n"
|
||||
"Content-Type: text/plain; charset=UTF-8\n"
|
||||
"Content-Transfer-Encoding: 8bit\n"
|
||||
"Generated-By: pygettext.py 1.5\n"
|
||||
"X-Generator: Poedit 2.0.3\n"
|
||||
"Last-Translator: UltimatePancake <pier.gaetani@gmail.com>\n"
|
||||
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
|
||||
"Language: es\n"
|
||||
|
||||
#: ../alias.py:138
|
||||
msgid "No prefix found."
|
||||
msgstr "No se encontró prefijo."
|
||||
|
||||
#: ../alias.py:234
|
||||
msgid "A new alias with the trigger `{}` has been created."
|
||||
msgstr "Un nuevo alias con el disparador `{}` ha sido creado."
|
||||
|
||||
#: ../alias.py:270
|
||||
msgid "A new global alias with the trigger `{}` has been created."
|
||||
msgstr "Un nuevo alias global con el disparador `{}` ha sido creado."
|
||||
|
||||
#: ../alias.py:285
|
||||
msgid "No such alias exists."
|
||||
msgstr "Dicho alias no existe."
|
||||
|
||||
#: ../alias.py:294
|
||||
msgid "The `{}` alias will execute the command `{}`"
|
||||
msgstr "El alias `{}` ejecutará el comando `{}`"
|
||||
|
||||
#: ../alias.py:297
|
||||
msgid "There is no alias with the name `{}`"
|
||||
msgstr "No existe un alias con el nombre `{}`"
|
||||
|
||||
#: ../alias.py:309
|
||||
msgid "There are no aliases on this guild."
|
||||
msgstr "No hay alias en este gremio."
|
||||
|
||||
#: ../alias.py:313 ../alias.py:331
|
||||
msgid "Alias with the name `{}` was successfully deleted."
|
||||
msgstr "El alias `{}` fue eliminado exitósamente."
|
||||
|
||||
#: ../alias.py:316 ../alias.py:334
|
||||
msgid "Alias with name `{}` was not found."
|
||||
msgstr "El alias `{}` no fue encontrado."
|
||||
|
||||
#: ../alias.py:327
|
||||
msgid "There are no aliases on this bot."
|
||||
msgstr "No hay alias en este bot."
|
||||
|
||||
#: ../alias.py:342 ../alias.py:353
|
||||
msgid "Aliases:"
|
||||
msgstr "Alias:"
|
||||
|
||||
#: ../alias.py:344 ../alias.py:355
|
||||
msgid "There are no aliases on this server."
|
||||
msgstr "No hay alias en este servidor."
|
||||
65
redbot/cogs/alias/locales/messages.pot
Normal file
65
redbot/cogs/alias/locales/messages.pot
Normal file
@@ -0,0 +1,65 @@
|
||||
# SOME DESCRIPTIVE TITLE.
|
||||
# Copyright (C) YEAR ORGANIZATION
|
||||
# FIRST AUTHOR <EMAIL@ADDRESS>, YEAR.
|
||||
#
|
||||
msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: PACKAGE VERSION\n"
|
||||
"POT-Creation-Date: 2017-08-26 17:23+EDT\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"
|
||||
|
||||
|
||||
#: ../alias.py:138
|
||||
msgid "No prefix found."
|
||||
msgstr ""
|
||||
|
||||
#: ../alias.py:234
|
||||
msgid "A new alias with the trigger `{}` has been created."
|
||||
msgstr ""
|
||||
|
||||
#: ../alias.py:270
|
||||
msgid "A new global alias with the trigger `{}` has been created."
|
||||
msgstr ""
|
||||
|
||||
#: ../alias.py:285
|
||||
msgid "No such alias exists."
|
||||
msgstr ""
|
||||
|
||||
#: ../alias.py:294
|
||||
msgid "The `{}` alias will execute the command `{}`"
|
||||
msgstr ""
|
||||
|
||||
#: ../alias.py:297
|
||||
msgid "There is no alias with the name `{}`"
|
||||
msgstr ""
|
||||
|
||||
#: ../alias.py:309
|
||||
msgid "There are no aliases on this guild."
|
||||
msgstr ""
|
||||
|
||||
#: ../alias.py:313 ../alias.py:331
|
||||
msgid "Alias with the name `{}` was successfully deleted."
|
||||
msgstr ""
|
||||
|
||||
#: ../alias.py:316 ../alias.py:334
|
||||
msgid "Alias with name `{}` was not found."
|
||||
msgstr ""
|
||||
|
||||
#: ../alias.py:327
|
||||
msgid "There are no aliases on this bot."
|
||||
msgstr ""
|
||||
|
||||
#: ../alias.py:342 ../alias.py:353
|
||||
msgid "Aliases:"
|
||||
msgstr ""
|
||||
|
||||
#: ../alias.py:344 ../alias.py:355
|
||||
msgid "There are no aliases on this server."
|
||||
msgstr ""
|
||||
|
||||
5
redbot/cogs/audio/__init__.py
Normal file
5
redbot/cogs/audio/__init__.py
Normal file
@@ -0,0 +1,5 @@
|
||||
from .audio import Audio
|
||||
|
||||
|
||||
def setup(bot):
|
||||
bot.add_cog(Audio(bot))
|
||||
112
redbot/cogs/audio/audio.py
Normal file
112
redbot/cogs/audio/audio.py
Normal file
@@ -0,0 +1,112 @@
|
||||
from discord.ext import commands
|
||||
from discord import FFmpegPCMAudio, PCMVolumeTransformer
|
||||
import os
|
||||
import youtube_dl
|
||||
import discord
|
||||
|
||||
|
||||
# Just a little experimental audio cog not meant for final release
|
||||
|
||||
|
||||
class Audio:
|
||||
"""Audio commands"""
|
||||
|
||||
def __init__(self, bot):
|
||||
self.bot = bot
|
||||
|
||||
@commands.command()
|
||||
async def local(self, ctx, *, filename: str):
|
||||
"""Play mp3"""
|
||||
if ctx.author.voice is None:
|
||||
await ctx.send("Join a voice channel first!")
|
||||
return
|
||||
|
||||
if ctx.voice_client:
|
||||
if ctx.voice_client.channel != ctx.author.voice.channel:
|
||||
await ctx.voice_client.disconnect()
|
||||
path = os.path.join("cogs", "audio", "songs", filename + ".mp3")
|
||||
if not os.path.isfile(path):
|
||||
await ctx.send("Let's play a file that exists pls")
|
||||
return
|
||||
player = PCMVolumeTransformer(FFmpegPCMAudio(path), volume=1)
|
||||
voice = await ctx.author.voice.channel.connect()
|
||||
voice.play(player)
|
||||
await ctx.send("{} is playing a song...".format(ctx.author))
|
||||
|
||||
@commands.command()
|
||||
async def play(self, ctx, url: str):
|
||||
"""Play youtube url"""
|
||||
url = url.strip("<").strip(">")
|
||||
if ctx.author.voice is None:
|
||||
await ctx.send("Join a voice channel first!")
|
||||
return
|
||||
elif "youtube.com" not in url.lower():
|
||||
await ctx.send("Youtube links pls")
|
||||
return
|
||||
|
||||
if ctx.voice_client:
|
||||
if ctx.voice_client.channel != ctx.author.voice.channel:
|
||||
await ctx.voice_client.disconnect()
|
||||
yt = YoutubeSource(url)
|
||||
player = PCMVolumeTransformer(yt, volume=1)
|
||||
voice = await ctx.author.voice.channel.connect()
|
||||
voice.play(player)
|
||||
await ctx.send("{} is playing a song...".format(ctx.author))
|
||||
|
||||
@commands.command()
|
||||
async def stop(self, ctx):
|
||||
"""Stops the music and disconnects"""
|
||||
if ctx.voice_client:
|
||||
ctx.voice_client.source.cleanup()
|
||||
await ctx.voice_client.disconnect()
|
||||
else:
|
||||
await ctx.send("I'm not even connected to a voice channel!", delete_after=2)
|
||||
await ctx.message.delete()
|
||||
|
||||
@commands.command()
|
||||
async def pause(self, ctx):
|
||||
"""Pauses the music"""
|
||||
if ctx.voice_client:
|
||||
ctx.voice_client.pause()
|
||||
await ctx.send("👌", delete_after=2)
|
||||
else:
|
||||
await ctx.send("I'm not even connected to a voice channel!", delete_after=2)
|
||||
await ctx.message.delete()
|
||||
|
||||
@commands.command()
|
||||
async def resume(self, ctx):
|
||||
"""Resumes the music"""
|
||||
if ctx.voice_client:
|
||||
ctx.voice_client.resume()
|
||||
await ctx.send("👌", delete_after=2)
|
||||
else:
|
||||
await ctx.send("I'm not even connected to a voice channel!", delete_after=2)
|
||||
await ctx.message.delete()
|
||||
|
||||
@commands.command(hidden=True)
|
||||
async def volume(self, ctx, n: float):
|
||||
"""Sets the volume"""
|
||||
if ctx.voice_client:
|
||||
ctx.voice_client.source.volume = n
|
||||
await ctx.send("Volume set.", delete_after=2)
|
||||
else:
|
||||
await ctx.send("I'm not even connected to a voice channel!", delete_after=2)
|
||||
await ctx.message.delete()
|
||||
|
||||
def __unload(self):
|
||||
for vc in self.bot.voice_clients:
|
||||
if vc.source:
|
||||
vc.source.cleanup()
|
||||
self.bot.loop.create_task(vc.disconnect())
|
||||
|
||||
|
||||
class YoutubeSource(discord.FFmpegPCMAudio):
|
||||
def __init__(self, url):
|
||||
opts = {
|
||||
'format': 'webm[abr>0]/bestaudio/best',
|
||||
'prefer_ffmpeg': True,
|
||||
'quiet': True
|
||||
}
|
||||
ytdl = youtube_dl.YoutubeDL(opts)
|
||||
self.info = ytdl.extract_info(url, download=False)
|
||||
super().__init__(self.info['url'])
|
||||
5
redbot/cogs/bank/__init__.py
Normal file
5
redbot/cogs/bank/__init__.py
Normal file
@@ -0,0 +1,5 @@
|
||||
from .bank import Bank, check_global_setting_guildowner, check_global_setting_admin
|
||||
|
||||
|
||||
def setup(bot):
|
||||
bot.add_cog(Bank(bot))
|
||||
86
redbot/cogs/bank/bank.py
Normal file
86
redbot/cogs/bank/bank.py
Normal file
@@ -0,0 +1,86 @@
|
||||
from redbot.core import checks, bank
|
||||
from redbot.core.i18n import CogI18n
|
||||
from discord.ext import commands
|
||||
|
||||
from redbot.core.bot import Red # Only used for type hints
|
||||
|
||||
_ = CogI18n('Bank', __file__)
|
||||
|
||||
|
||||
def check_global_setting_guildowner():
|
||||
"""
|
||||
Command decorator. If the bank is not global, it checks if the author is
|
||||
either the guildowner or has the administrator permission.
|
||||
"""
|
||||
async def pred(ctx: commands.Context):
|
||||
author = ctx.author
|
||||
if await ctx.bot.is_owner(author):
|
||||
return True
|
||||
if not await bank.is_global():
|
||||
permissions = ctx.channel.permissions_for(author)
|
||||
return author == ctx.guild.owner or permissions.administrator
|
||||
|
||||
return commands.check(pred)
|
||||
|
||||
|
||||
def check_global_setting_admin():
|
||||
"""
|
||||
Command decorator. If the bank is not global, it checks if the author is
|
||||
either a bot admin or has the manage_guild permission.
|
||||
"""
|
||||
async def pred(ctx: commands.Context):
|
||||
author = ctx.author
|
||||
if await ctx.bot.is_owner(author):
|
||||
return True
|
||||
if not await bank.is_global():
|
||||
permissions = ctx.channel.permissions_for(author)
|
||||
is_guild_owner = author == ctx.guild.owner
|
||||
admin_role = await ctx.bot.db.guild(ctx.guild).admin_role()
|
||||
return admin_role in author.roles or is_guild_owner or permissions.manage_guild
|
||||
|
||||
return commands.check(pred)
|
||||
|
||||
|
||||
class Bank:
|
||||
"""Bank"""
|
||||
|
||||
def __init__(self, bot: Red):
|
||||
self.bot = bot
|
||||
|
||||
# SECTION commands
|
||||
|
||||
@commands.group()
|
||||
@checks.guildowner_or_permissions(administrator=True)
|
||||
async def bankset(self, ctx: commands.Context):
|
||||
"""Base command for bank settings"""
|
||||
if ctx.invoked_subcommand is None:
|
||||
await self.bot.send_cmd_help(ctx)
|
||||
|
||||
@bankset.command(name="toggleglobal")
|
||||
@checks.is_owner()
|
||||
async def bankset_toggleglobal(self, ctx: commands.Context):
|
||||
"""Toggles whether the bank is global or not
|
||||
If the bank is global, it will become per-guild
|
||||
If the bank is per-guild, it will become global"""
|
||||
cur_setting = await bank.is_global()
|
||||
await bank.set_global(not cur_setting, ctx.author)
|
||||
|
||||
word = _("per-guild") if cur_setting else _("global")
|
||||
|
||||
await ctx.send(_("The bank is now {}.").format(word))
|
||||
|
||||
@bankset.command(name="bankname")
|
||||
@check_global_setting_guildowner()
|
||||
async def bankset_bankname(self, ctx: commands.Context, *, name: str):
|
||||
"""Set the bank's name"""
|
||||
await bank.set_bank_name(name, ctx.guild)
|
||||
await ctx.send(_("Bank's name has been set to {}").format(name))
|
||||
|
||||
@bankset.command(name="creditsname")
|
||||
@check_global_setting_guildowner()
|
||||
async def bankset_creditsname(self, ctx: commands.Context, *, name: str):
|
||||
"""Set the name for the bank's currency"""
|
||||
await bank.set_currency_name(name, ctx.guild)
|
||||
await ctx.send(_("Currency name has been set to {}").format(name))
|
||||
|
||||
# ENDSECTION
|
||||
37
redbot/cogs/bank/errors.py
Normal file
37
redbot/cogs/bank/errors.py
Normal file
@@ -0,0 +1,37 @@
|
||||
class BankError(Exception):
|
||||
pass
|
||||
|
||||
class BankNotGlobal(BankError):
|
||||
pass
|
||||
|
||||
|
||||
class BankIsGlobal(BankError):
|
||||
pass
|
||||
|
||||
|
||||
class AccountAlreadyExists(BankError):
|
||||
pass
|
||||
|
||||
|
||||
class NoAccount(BankError):
|
||||
pass
|
||||
|
||||
|
||||
class NoSenderAccount(NoAccount):
|
||||
pass
|
||||
|
||||
|
||||
class NoReceiverAccount(NoAccount):
|
||||
pass
|
||||
|
||||
|
||||
class InsufficientBalance(BankError):
|
||||
pass
|
||||
|
||||
|
||||
class NegativeValue(BankError):
|
||||
pass
|
||||
|
||||
|
||||
class SameSenderAndReceiver(BankError):
|
||||
pass
|
||||
37
redbot/cogs/bank/locales/es.po
Normal file
37
redbot/cogs/bank/locales/es.po
Normal file
@@ -0,0 +1,37 @@
|
||||
# Copyright (C) 2017 Red-DiscordBot
|
||||
# UltimatePancake <pier.gaetani@gmail.com>, 2017.
|
||||
#
|
||||
msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: \n"
|
||||
"POT-Creation-Date: 2017-08-26 17:32+EDT\n"
|
||||
"PO-Revision-Date: 2017-08-26 20:35-0600\n"
|
||||
"Language-Team: \n"
|
||||
"MIME-Version: 1.0\n"
|
||||
"Content-Type: text/plain; charset=UTF-8\n"
|
||||
"Content-Transfer-Encoding: 8bit\n"
|
||||
"Generated-By: pygettext.py 1.5\n"
|
||||
"X-Generator: Poedit 2.0.3\n"
|
||||
"Last-Translator: UltimatePancake <pier.gaetani@gmail.com>\n"
|
||||
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
|
||||
"Language: es\n"
|
||||
|
||||
#: ../bank.py:68
|
||||
msgid "global"
|
||||
msgstr "global"
|
||||
|
||||
#: ../bank.py:68
|
||||
msgid "per-guild"
|
||||
msgstr "por gremio"
|
||||
|
||||
#: ../bank.py:70
|
||||
msgid "The bank is now {}."
|
||||
msgstr "El banco es ahora {}."
|
||||
|
||||
#: ../bank.py:77
|
||||
msgid "Bank's name has been set to {}"
|
||||
msgstr "Nombre del banco es ahora {}"
|
||||
|
||||
#: ../bank.py:84
|
||||
msgid "Currency name has been set to {}"
|
||||
msgstr "Nombre de la moneda es ahora {}"
|
||||
37
redbot/cogs/bank/locales/messages.pot
Normal file
37
redbot/cogs/bank/locales/messages.pot
Normal file
@@ -0,0 +1,37 @@
|
||||
# SOME DESCRIPTIVE TITLE.
|
||||
# Copyright (C) YEAR ORGANIZATION
|
||||
# FIRST AUTHOR <EMAIL@ADDRESS>, YEAR.
|
||||
#
|
||||
msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: PACKAGE VERSION\n"
|
||||
"POT-Creation-Date: 2017-08-26 17:32+EDT\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"
|
||||
|
||||
|
||||
#: ../bank.py:68
|
||||
msgid "global"
|
||||
msgstr ""
|
||||
|
||||
#: ../bank.py:68
|
||||
msgid "per-guild"
|
||||
msgstr ""
|
||||
|
||||
#: ../bank.py:70
|
||||
msgid "The bank is now {}."
|
||||
msgstr ""
|
||||
|
||||
#: ../bank.py:77
|
||||
msgid "Bank's name has been set to {}"
|
||||
msgstr ""
|
||||
|
||||
#: ../bank.py:84
|
||||
msgid "Currency name has been set to {}"
|
||||
msgstr ""
|
||||
|
||||
6
redbot/cogs/downloader/__init__.py
Normal file
6
redbot/cogs/downloader/__init__.py
Normal file
@@ -0,0 +1,6 @@
|
||||
from redbot.core.bot import Red
|
||||
from .downloader import Downloader
|
||||
|
||||
|
||||
def setup(bot: Red):
|
||||
bot.add_cog(Downloader(bot))
|
||||
45
redbot/cogs/downloader/checks.py
Normal file
45
redbot/cogs/downloader/checks.py
Normal file
@@ -0,0 +1,45 @@
|
||||
import asyncio
|
||||
|
||||
import discord
|
||||
from discord.ext import commands
|
||||
|
||||
__all__ = ["install_agreement", ]
|
||||
|
||||
REPO_INSTALL_MSG = (
|
||||
"You're about to add a 3rd party repository. The creator of Red"
|
||||
" and its community have no responsibility for any potential "
|
||||
"damage that the content of 3rd party repositories might cause."
|
||||
"\n\nBy typing '**I agree**' you declare that you have read and"
|
||||
" fully understand the above message. This message won't be "
|
||||
"shown again until the next reboot.\n\nYou have **30** seconds"
|
||||
" to reply to this message."
|
||||
)
|
||||
|
||||
|
||||
def install_agreement():
|
||||
async def pred(ctx: commands.Context):
|
||||
downloader = ctx.command.instance
|
||||
if downloader is None:
|
||||
return True
|
||||
elif downloader.already_agreed:
|
||||
return True
|
||||
elif ctx.invoked_subcommand is None or \
|
||||
isinstance(ctx.invoked_subcommand, commands.Group):
|
||||
return True
|
||||
|
||||
def does_agree(msg: discord.Message):
|
||||
return ctx.author == msg.author and \
|
||||
ctx.channel == msg.channel and \
|
||||
msg.content == "I agree"
|
||||
|
||||
await ctx.send(REPO_INSTALL_MSG)
|
||||
|
||||
try:
|
||||
await ctx.bot.wait_for('message', check=does_agree, timeout=30)
|
||||
except asyncio.TimeoutError:
|
||||
await ctx.send("Your response has timed out, please try again.")
|
||||
return False
|
||||
|
||||
downloader.already_agreed = True
|
||||
return True
|
||||
return commands.check(pred)
|
||||
24
redbot/cogs/downloader/converters.py
Normal file
24
redbot/cogs/downloader/converters.py
Normal file
@@ -0,0 +1,24 @@
|
||||
import discord
|
||||
from discord.ext import commands
|
||||
from .repo_manager import RepoManager
|
||||
from .installable import Installable
|
||||
|
||||
|
||||
class RepoName(commands.Converter):
|
||||
async def convert(self, ctx: commands.Context, arg: str) -> str:
|
||||
return RepoManager.validate_and_normalize_repo_name(arg)
|
||||
|
||||
|
||||
class InstalledCog(commands.Converter):
|
||||
async def convert(self, ctx: commands.Context, arg: str) -> Installable:
|
||||
downloader = ctx.bot.get_cog("Downloader")
|
||||
if downloader is None:
|
||||
raise commands.CommandError("Downloader not loaded.")
|
||||
|
||||
cog = discord.utils.get(downloader.installed_cogs, name=arg)
|
||||
if cog is None:
|
||||
raise commands.BadArgument(
|
||||
"That cog is not installed"
|
||||
)
|
||||
|
||||
return cog
|
||||
394
redbot/cogs/downloader/downloader.py
Normal file
394
redbot/cogs/downloader/downloader.py
Normal file
@@ -0,0 +1,394 @@
|
||||
import os
|
||||
import shutil
|
||||
from pathlib import Path
|
||||
from sys import path as syspath
|
||||
from typing import Tuple, Union
|
||||
|
||||
import discord
|
||||
from redbot.core import Config
|
||||
from redbot.core import checks
|
||||
from redbot.core.i18n import CogI18n
|
||||
from redbot.core.utils.chat_formatting import box
|
||||
from discord.ext import commands
|
||||
|
||||
from redbot.core.bot import Red
|
||||
from .checks import install_agreement
|
||||
from .converters import RepoName, InstalledCog
|
||||
from .errors import CloningError, ExistingGitRepo
|
||||
from .installable import Installable
|
||||
from .log import log
|
||||
from .repo_manager import RepoManager, Repo
|
||||
|
||||
_ = CogI18n('Downloader', __file__)
|
||||
|
||||
|
||||
class Downloader:
|
||||
def __init__(self, bot: Red):
|
||||
self.bot = bot
|
||||
|
||||
self.conf = Config.get_conf(self, identifier=998240343,
|
||||
force_registration=True)
|
||||
|
||||
self.conf.register_global(
|
||||
repos={},
|
||||
installed=[]
|
||||
)
|
||||
|
||||
self.already_agreed = False
|
||||
|
||||
self.LIB_PATH = self.bot.main_dir / "lib"
|
||||
self.SHAREDLIB_PATH = self.LIB_PATH / "cog_shared"
|
||||
self.SHAREDLIB_INIT = self.SHAREDLIB_PATH / "__init__.py"
|
||||
|
||||
self.LIB_PATH.mkdir(parents=True, exist_ok=True)
|
||||
self.SHAREDLIB_PATH.mkdir(parents=True, exist_ok=True)
|
||||
if not self.SHAREDLIB_INIT.exists():
|
||||
with self.SHAREDLIB_INIT.open(mode='w') as _:
|
||||
pass
|
||||
|
||||
if str(self.LIB_PATH) not in syspath:
|
||||
syspath.insert(1, str(self.LIB_PATH))
|
||||
|
||||
self._repo_manager = RepoManager(self.conf)
|
||||
|
||||
async def cog_install_path(self):
|
||||
"""
|
||||
Returns the current cog install path.
|
||||
:return:
|
||||
"""
|
||||
return await self.bot.cog_mgr.install_path()
|
||||
|
||||
async def installed_cogs(self) -> Tuple[Installable]:
|
||||
"""
|
||||
Returns the dictionary mapping cog name to install location
|
||||
and repo name.
|
||||
:return:
|
||||
"""
|
||||
installed = await self.conf.installed()
|
||||
# noinspection PyTypeChecker
|
||||
return tuple(Installable.from_json(v) for v in installed)
|
||||
|
||||
async def _add_to_installed(self, cog: Installable):
|
||||
"""
|
||||
Marks a cog as installed.
|
||||
:param cog:
|
||||
:return:
|
||||
"""
|
||||
installed = await self.conf.installed()
|
||||
cog_json = cog.to_json()
|
||||
|
||||
if cog_json not in installed:
|
||||
installed.append(cog_json)
|
||||
await self.conf.installed.set(installed)
|
||||
|
||||
async def _remove_from_installed(self, cog: Installable):
|
||||
"""
|
||||
Removes a cog from the saved list of installed cogs.
|
||||
:param cog:
|
||||
:return:
|
||||
"""
|
||||
installed = await self.conf.installed()
|
||||
cog_json = cog.to_json()
|
||||
|
||||
if cog_json in installed:
|
||||
installed.remove(cog_json)
|
||||
await self.conf.installed.set(installed)
|
||||
|
||||
async def _reinstall_cogs(self, cogs: Tuple[Installable]) -> Tuple[Installable]:
|
||||
"""
|
||||
Installs a list of cogs, used when updating.
|
||||
:param cogs:
|
||||
:return: Any cogs that failed to copy
|
||||
"""
|
||||
failed = []
|
||||
for cog in cogs:
|
||||
if not await cog.copy_to(await self.cog_install_path()):
|
||||
failed.append(cog)
|
||||
|
||||
# noinspection PyTypeChecker
|
||||
return tuple(failed)
|
||||
|
||||
async def _reinstall_libraries(self, cogs: Tuple[Installable]) -> Tuple[Installable]:
|
||||
"""
|
||||
Reinstalls any shared libraries from the repos of cogs that
|
||||
were updated.
|
||||
:param cogs:
|
||||
:return: Any libraries that failed to copy
|
||||
"""
|
||||
repo_names = set(cog.repo_name for cog in cogs)
|
||||
unfiltered_repos = (self._repo_manager.get_repo(r) for r in repo_names)
|
||||
repos = filter(lambda r: r is not None, unfiltered_repos)
|
||||
|
||||
failed = []
|
||||
|
||||
for repo in repos:
|
||||
if not await repo.install_libraries(target_dir=self.SHAREDLIB_PATH):
|
||||
failed.extend(repo.available_libraries)
|
||||
|
||||
# noinspection PyTypeChecker
|
||||
return tuple(failed)
|
||||
|
||||
async def _reinstall_requirements(self, cogs: Tuple[Installable]) -> bool:
|
||||
"""
|
||||
Reinstalls requirements for given cogs that have been updated.
|
||||
Returns a bool that indicates if all requirement installations
|
||||
were successful.
|
||||
:param cogs:
|
||||
:return:
|
||||
"""
|
||||
|
||||
# Reduces requirements to a single list with no repeats
|
||||
requirements = set(r for c in cogs for r in c.requirements)
|
||||
repo_names = self._repo_manager.get_all_repo_names()
|
||||
repos = [(self._repo_manager.get_repo(rn), []) for rn in repo_names]
|
||||
|
||||
# This for loop distributes the requirements across all repos
|
||||
# which will allow us to concurrently install requirements
|
||||
for i, req in enumerate(requirements):
|
||||
repo_index = i % len(repos)
|
||||
repos[repo_index][1].append(req)
|
||||
|
||||
has_reqs = list(filter(lambda item: len(item[1]) > 0, repos))
|
||||
|
||||
ret = True
|
||||
for repo, reqs in has_reqs:
|
||||
for req in reqs:
|
||||
# noinspection PyTypeChecker
|
||||
ret = ret and await repo.install_raw_requirements([req, ], self.LIB_PATH)
|
||||
return ret
|
||||
|
||||
@staticmethod
|
||||
async def _delete_cog(target: Path):
|
||||
"""
|
||||
Removes an (installed) cog.
|
||||
:param target: Path pointing to an existing file or directory
|
||||
:return:
|
||||
"""
|
||||
if not target.exists():
|
||||
return
|
||||
|
||||
if target.is_dir():
|
||||
shutil.rmtree(str(target))
|
||||
elif target.is_file():
|
||||
os.remove(str(target))
|
||||
|
||||
@commands.group()
|
||||
@checks.is_owner()
|
||||
async def repo(self, ctx):
|
||||
"""
|
||||
Command group for managing Downloader repos.
|
||||
"""
|
||||
if ctx.invoked_subcommand is None:
|
||||
await self.bot.send_cmd_help(ctx)
|
||||
|
||||
@repo.command(name="add")
|
||||
@install_agreement()
|
||||
async def _repo_add(self, ctx, name: RepoName, repo_url: str, branch: str=None):
|
||||
"""
|
||||
Add a new repo to Downloader.
|
||||
|
||||
Name can only contain characters A-z, numbers and underscore
|
||||
Branch will default to master if not specified
|
||||
"""
|
||||
try:
|
||||
# noinspection PyTypeChecker
|
||||
await self._repo_manager.add_repo(
|
||||
name=name,
|
||||
url=repo_url,
|
||||
branch=branch
|
||||
)
|
||||
except ExistingGitRepo:
|
||||
await ctx.send(_("That git repo has already been added under another name."))
|
||||
except CloningError:
|
||||
await ctx.send(_("Something went wrong during the cloning process."))
|
||||
log.exception(_("Something went wrong during the cloning process."))
|
||||
else:
|
||||
await ctx.send(_("Repo `{}` successfully added.").format(name))
|
||||
|
||||
@repo.command(name="delete")
|
||||
async def _repo_del(self, ctx, repo_name: Repo):
|
||||
"""
|
||||
Removes a repo from Downloader and its' files.
|
||||
"""
|
||||
await self._repo_manager.delete_repo(repo_name.name)
|
||||
|
||||
await ctx.send(_("The repo `{}` has been deleted successfully.").format(repo_name.name))
|
||||
|
||||
@repo.command(name="list")
|
||||
async def _repo_list(self, ctx):
|
||||
"""
|
||||
Lists all installed repos.
|
||||
"""
|
||||
repos = self._repo_manager.get_all_repo_names()
|
||||
joined = _("Installed Repos:\n") + "\n".join(["+ " + r for r in repos])
|
||||
|
||||
await ctx.send(box(joined, lang="diff"))
|
||||
|
||||
@commands.group()
|
||||
@checks.is_owner()
|
||||
async def cog(self, ctx):
|
||||
"""
|
||||
Command group for managing installable Cogs.
|
||||
"""
|
||||
if ctx.invoked_subcommand is None:
|
||||
await self.bot.send_cmd_help(ctx)
|
||||
|
||||
@cog.command(name="install")
|
||||
async def _cog_install(self, ctx, repo_name: Repo, cog_name: str):
|
||||
"""
|
||||
Installs a cog from the given repo.
|
||||
"""
|
||||
cog = discord.utils.get(repo_name.available_cogs, name=cog_name)
|
||||
if cog is None:
|
||||
await ctx.send(_("Error, there is no cog by the name of"
|
||||
" `{}` in the `{}` repo.").format(cog_name, repo_name.name))
|
||||
return
|
||||
|
||||
if not await repo_name.install_requirements(cog, self.LIB_PATH):
|
||||
await ctx.send(_("Failed to install the required libraries for"
|
||||
" `{}`: `{}`").format(cog.name, cog.requirements))
|
||||
return
|
||||
|
||||
await repo_name.install_cog(cog, await self.cog_install_path())
|
||||
|
||||
await self._add_to_installed(cog)
|
||||
|
||||
await repo_name.install_libraries(self.SHAREDLIB_PATH)
|
||||
|
||||
await ctx.send(_("`{}` cog successfully installed.").format(cog_name))
|
||||
|
||||
@cog.command(name="uninstall")
|
||||
async def _cog_uninstall(self, ctx, cog_name: InstalledCog):
|
||||
"""
|
||||
Allows you to uninstall cogs that were previously installed
|
||||
through Downloader.
|
||||
"""
|
||||
# noinspection PyUnresolvedReferences,PyProtectedMember
|
||||
real_name = cog_name.name
|
||||
|
||||
poss_installed_path = (await self.cog_install_path()) / real_name
|
||||
if poss_installed_path.exists():
|
||||
await self._delete_cog(poss_installed_path)
|
||||
# noinspection PyTypeChecker
|
||||
await self._remove_from_installed(cog_name)
|
||||
await ctx.send(_("`{}` was successfully removed.").format(real_name))
|
||||
else:
|
||||
await ctx.send(_("That cog was installed but can no longer"
|
||||
" be located. You may need to remove it's"
|
||||
" files manually if it is still usable."))
|
||||
|
||||
@cog.command(name="update")
|
||||
async def _cog_update(self, ctx, cog_name: InstalledCog=None):
|
||||
"""
|
||||
Updates all cogs or one of your choosing.
|
||||
"""
|
||||
if cog_name is None:
|
||||
updated = await self._repo_manager.update_all_repos()
|
||||
installed_cogs = set(await self.installed_cogs())
|
||||
updated_cogs = set(cog for repo in updated.keys() for cog in repo.available_cogs)
|
||||
|
||||
installed_and_updated = updated_cogs & installed_cogs
|
||||
|
||||
# noinspection PyTypeChecker
|
||||
await self._reinstall_requirements(installed_and_updated)
|
||||
|
||||
# noinspection PyTypeChecker
|
||||
await self._reinstall_cogs(installed_and_updated)
|
||||
|
||||
# noinspection PyTypeChecker
|
||||
await self._reinstall_libraries(installed_and_updated)
|
||||
await ctx.send(_("Cog update completed successfully."))
|
||||
|
||||
@cog.command(name="list")
|
||||
async def _cog_list(self, ctx, repo_name: Repo):
|
||||
"""
|
||||
Lists all available cogs from a single repo.
|
||||
"""
|
||||
cogs = repo_name.available_cogs
|
||||
cogs = _("Available Cogs:\n") + "\n".join(
|
||||
["+ {}: {}".format(c.name, c.short or "") for c in cogs])
|
||||
|
||||
await ctx.send(box(cogs, lang="diff"))
|
||||
|
||||
@cog.command(name="info")
|
||||
async def _cog_info(self, ctx, repo_name: Repo, cog_name: str):
|
||||
"""
|
||||
Lists information about a single cog.
|
||||
"""
|
||||
cog = discord.utils.get(repo_name.available_cogs, name=cog_name)
|
||||
if cog is None:
|
||||
await ctx.send(_("There is no cog `{}` in the repo `{}`").format(
|
||||
cog_name, repo_name.name
|
||||
))
|
||||
return
|
||||
|
||||
msg = _("Information on {}:\n{}").format(cog.name, cog.description or "")
|
||||
await ctx.send(box(msg))
|
||||
|
||||
async def is_installed(self, cog_name: str) -> (bool, Union[Installable, None]):
|
||||
"""
|
||||
Checks to see if a cog with the given name was installed
|
||||
through Downloader.
|
||||
:param cog_name:
|
||||
:return: is_installed, Installable
|
||||
"""
|
||||
for installable in await self.installed_cogs():
|
||||
if installable.name == cog_name:
|
||||
return True, installable
|
||||
return False, None
|
||||
|
||||
def format_findcog_info(self, command_name: str,
|
||||
cog_installable: Union[Installable, object]=None) -> str:
|
||||
"""
|
||||
Formats the info for output to discord
|
||||
:param command_name:
|
||||
:param cog_installable: Can be an Installable instance or a Cog instance.
|
||||
:return: str
|
||||
"""
|
||||
if isinstance(cog_installable, Installable):
|
||||
made_by = ", ".join(cog_installable.author) or _("Missing from info.json")
|
||||
repo = self._repo_manager.get_repo(cog_installable.repo_name)
|
||||
repo_url = repo.url
|
||||
cog_name = cog_installable.name
|
||||
else:
|
||||
made_by = "26 & co."
|
||||
repo_url = "https://github.com/Twentysix26/Red-DiscordBot"
|
||||
cog_name = cog_installable.__class__.__name__
|
||||
|
||||
msg = _("Command: {}\nMade by: {}\nRepo: {}\nCog name: {}")
|
||||
|
||||
return msg.format(command_name, made_by, repo_url, cog_name)
|
||||
|
||||
def cog_name_from_instance(self, instance: object) -> str:
|
||||
"""
|
||||
Determines the cog name that Downloader knows from the cog instance.
|
||||
|
||||
Probably.
|
||||
:param instance:
|
||||
:return:
|
||||
"""
|
||||
splitted = instance.__module__.split('.')
|
||||
return splitted[-2]
|
||||
|
||||
@commands.command()
|
||||
async def findcog(self, ctx: commands.Context, command_name: str):
|
||||
"""
|
||||
Figures out which cog a command comes from. Only works with loaded
|
||||
cogs.
|
||||
"""
|
||||
command = ctx.bot.all_commands.get(command_name)
|
||||
|
||||
if command is None:
|
||||
await ctx.send(_("That command doesn't seem to exist."))
|
||||
return
|
||||
|
||||
# Check if in installed cogs
|
||||
cog_name = self.cog_name_from_instance(command.instance)
|
||||
installed, cog_installable = await self.is_installed(cog_name)
|
||||
if installed:
|
||||
msg = self.format_findcog_info(command_name, cog_installable)
|
||||
else:
|
||||
# Assume it's in a base cog
|
||||
msg = self.format_findcog_info(command_name, command.instance)
|
||||
|
||||
await ctx.send(box(msg))
|
||||
84
redbot/cogs/downloader/errors.py
Normal file
84
redbot/cogs/downloader/errors.py
Normal file
@@ -0,0 +1,84 @@
|
||||
__all__ = ["DownloaderException", "GitException", "InvalidRepoName", "ExistingGitRepo",
|
||||
"MissingGitRepo", "CloningError", "CurrentHashError", "HardResetError",
|
||||
"UpdateError", "GitDiffError", "PipError"]
|
||||
|
||||
|
||||
class DownloaderException(Exception):
|
||||
"""
|
||||
Base class for Downloader exceptions.
|
||||
"""
|
||||
pass
|
||||
|
||||
|
||||
class GitException(DownloaderException):
|
||||
"""
|
||||
Generic class for git exceptions.
|
||||
"""
|
||||
|
||||
|
||||
class InvalidRepoName(DownloaderException):
|
||||
"""
|
||||
Throw when a repo name is invalid. Check
|
||||
the message for a more detailed reason.
|
||||
"""
|
||||
pass
|
||||
|
||||
|
||||
class ExistingGitRepo(DownloaderException):
|
||||
"""
|
||||
Thrown when trying to clone into a folder where a
|
||||
git repo already exists.
|
||||
"""
|
||||
pass
|
||||
|
||||
|
||||
class MissingGitRepo(DownloaderException):
|
||||
"""
|
||||
Thrown when a git repo is expected to exist but
|
||||
does not.
|
||||
"""
|
||||
pass
|
||||
|
||||
|
||||
class CloningError(GitException):
|
||||
"""
|
||||
Thrown when git clone returns a non zero exit code.
|
||||
"""
|
||||
pass
|
||||
|
||||
|
||||
class CurrentHashError(GitException):
|
||||
"""
|
||||
Thrown when git returns a non zero exit code attempting
|
||||
to determine the current commit hash.
|
||||
"""
|
||||
pass
|
||||
|
||||
|
||||
class HardResetError(GitException):
|
||||
"""
|
||||
Thrown when there is an issue trying to execute a hard reset
|
||||
(usually prior to a repo update).
|
||||
"""
|
||||
pass
|
||||
|
||||
|
||||
class UpdateError(GitException):
|
||||
"""
|
||||
Thrown when git pull returns a non zero error code.
|
||||
"""
|
||||
pass
|
||||
|
||||
|
||||
class GitDiffError(GitException):
|
||||
"""
|
||||
Thrown when a git diff fails.
|
||||
"""
|
||||
pass
|
||||
|
||||
|
||||
class PipError(DownloaderException):
|
||||
"""
|
||||
Thrown when pip returns a non-zero return code.
|
||||
"""
|
||||
pass
|
||||
170
redbot/cogs/downloader/installable.py
Normal file
170
redbot/cogs/downloader/installable.py
Normal file
@@ -0,0 +1,170 @@
|
||||
import json
|
||||
import distutils.dir_util
|
||||
import shutil
|
||||
from enum import Enum
|
||||
from pathlib import Path
|
||||
from typing import Union, MutableMapping, Any
|
||||
|
||||
from .log import log
|
||||
from .json_mixins import RepoJSONMixin
|
||||
|
||||
|
||||
class InstallableType(Enum):
|
||||
UNKNOWN = 0
|
||||
COG = 1
|
||||
SHARED_LIBRARY = 2
|
||||
|
||||
|
||||
class Installable(RepoJSONMixin):
|
||||
"""
|
||||
Base class for anything the Downloader cog can install.
|
||||
- Modules
|
||||
- Repo Libraries
|
||||
- Other stuff?
|
||||
"""
|
||||
|
||||
INFO_FILE_DESCRIPTION = """
|
||||
|
||||
"""
|
||||
|
||||
def __init__(self, location: Path):
|
||||
"""
|
||||
Base installable initializer.
|
||||
|
||||
:param location: Location (file or folder) to the installable.
|
||||
"""
|
||||
super().__init__(location)
|
||||
|
||||
self._location = location
|
||||
|
||||
self.repo_name = self._location.parent.stem
|
||||
|
||||
self.author = ()
|
||||
self.bot_version = (3, 0, 0)
|
||||
self.hidden = False
|
||||
self.required_cogs = {} # Cog name -> repo URL
|
||||
self.requirements = ()
|
||||
self.tags = ()
|
||||
self.type = InstallableType.UNKNOWN
|
||||
|
||||
if self._info_file.exists():
|
||||
self._process_info_file(self._info_file)
|
||||
|
||||
if self._info == {}:
|
||||
self.type = InstallableType.COG
|
||||
|
||||
def __eq__(self, other):
|
||||
# noinspection PyProtectedMember
|
||||
return self._location == other._location
|
||||
|
||||
def __hash__(self):
|
||||
return hash(self._location)
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
return self._location.stem
|
||||
|
||||
async def copy_to(self, target_dir: Path) -> bool:
|
||||
"""
|
||||
Copies this cog/shared_lib to the given directory. This
|
||||
will overwrite any files in the target directory.
|
||||
|
||||
:param pathlib.Path target_dir: The installation directory to install to.
|
||||
:return: Status of installation
|
||||
:rtype: bool
|
||||
"""
|
||||
if self._location.is_file():
|
||||
copy_func = shutil.copy2
|
||||
else:
|
||||
copy_func = distutils.dir_util.copy_tree
|
||||
|
||||
# noinspection PyBroadException
|
||||
try:
|
||||
copy_func(
|
||||
src=str(self._location),
|
||||
dst=str(target_dir / self._location.stem)
|
||||
)
|
||||
except:
|
||||
log.exception("Error occurred when copying path:"
|
||||
" {}".format(self._location))
|
||||
return False
|
||||
return True
|
||||
|
||||
def _read_info_file(self):
|
||||
super()._read_info_file()
|
||||
|
||||
if self._info_file.exists():
|
||||
self._process_info_file()
|
||||
|
||||
def _process_info_file(self, info_file_path: Path=None) -> MutableMapping[str, Any]:
|
||||
"""
|
||||
Processes an information file. Loads dependencies among other
|
||||
information into this object.
|
||||
|
||||
:type info_file_path:
|
||||
:param info_file_path: Optional path to information file, defaults to `self.__info_file`
|
||||
:return: Raw information dictionary
|
||||
"""
|
||||
info_file_path = info_file_path or self._info_file
|
||||
if info_file_path is None or not info_file_path.is_file():
|
||||
raise ValueError("No valid information file path was found.")
|
||||
|
||||
info = {}
|
||||
with info_file_path.open(encoding='utf-8') as f:
|
||||
try:
|
||||
info = json.load(f)
|
||||
except json.JSONDecodeError:
|
||||
info = {}
|
||||
log.exception("Invalid JSON information file at path:"
|
||||
" {}".format(info_file_path))
|
||||
else:
|
||||
self._info = info
|
||||
|
||||
try:
|
||||
author = tuple(info.get("author", []))
|
||||
except ValueError:
|
||||
author = ()
|
||||
self.author = author
|
||||
|
||||
try:
|
||||
bot_version = tuple(info.get("bot_version", [3, 0, 0]))
|
||||
except ValueError:
|
||||
bot_version = 2
|
||||
self.bot_version = bot_version
|
||||
|
||||
try:
|
||||
hidden = bool(info.get("hidden", False))
|
||||
except ValueError:
|
||||
hidden = False
|
||||
self.hidden = hidden
|
||||
|
||||
self.required_cogs = info.get("required_cogs", {})
|
||||
|
||||
self.requirements = info.get("requirements", ())
|
||||
|
||||
try:
|
||||
tags = tuple(info.get("tags", ()))
|
||||
except ValueError:
|
||||
tags = ()
|
||||
self.tags = tags
|
||||
|
||||
installable_type = info.get("type", "")
|
||||
if installable_type in ("", "COG"):
|
||||
self.type = InstallableType.COG
|
||||
elif installable_type == "SHARED_LIBRARY":
|
||||
self.type = InstallableType.SHARED_LIBRARY
|
||||
self.hidden = True
|
||||
else:
|
||||
self.type = InstallableType.UNKNOWN
|
||||
|
||||
return info
|
||||
|
||||
def to_json(self):
|
||||
return {
|
||||
"location": self._location.relative_to(Path.cwd()).parts
|
||||
}
|
||||
|
||||
@classmethod
|
||||
def from_json(cls, data: dict):
|
||||
location = Path.cwd() / Path(*data["location"])
|
||||
return cls(location=location)
|
||||
37
redbot/cogs/downloader/json_mixins.py
Normal file
37
redbot/cogs/downloader/json_mixins.py
Normal file
@@ -0,0 +1,37 @@
|
||||
import json
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
class RepoJSONMixin:
|
||||
INFO_FILE_NAME = "info.json"
|
||||
|
||||
def __init__(self, repo_folder: Path):
|
||||
self._repo_folder = repo_folder
|
||||
|
||||
self.author = None
|
||||
self.install_msg = None
|
||||
self.short = None
|
||||
self.description = None
|
||||
|
||||
self._info_file = repo_folder / self.INFO_FILE_NAME
|
||||
if self._info_file.exists():
|
||||
self._read_info_file()
|
||||
|
||||
self._info = {}
|
||||
|
||||
def _read_info_file(self):
|
||||
if not (self._info_file.exists() or self._info_file.is_file()):
|
||||
return
|
||||
|
||||
try:
|
||||
with self._info_file.open(encoding='utf-8') as f:
|
||||
info = json.load(f)
|
||||
except json.JSONDecodeError:
|
||||
return
|
||||
else:
|
||||
self._info = info
|
||||
|
||||
self.author = info.get("author")
|
||||
self.install_msg = info.get("install_msg")
|
||||
self.short = info.get("short")
|
||||
self.description = info.get("description")
|
||||
97
redbot/cogs/downloader/locales/de.po
Normal file
97
redbot/cogs/downloader/locales/de.po
Normal file
@@ -0,0 +1,97 @@
|
||||
# SOME DESCRIPTIVE TITLE.
|
||||
# Copyright (C) YEAR ORGANIZATION
|
||||
# FIRST AUTHOR <EMAIL@ADDRESS>, YEAR.
|
||||
#
|
||||
msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: \n"
|
||||
"POT-Creation-Date: 2017-08-26 16:31+EDT\n"
|
||||
"PO-Revision-Date: 2017-08-26 17:00-0400\n"
|
||||
"MIME-Version: 1.0\n"
|
||||
"Content-Type: text/plain; charset=UTF-8\n"
|
||||
"Content-Transfer-Encoding: 8bit\n"
|
||||
"Generated-By: pygettext.py 1.5\n"
|
||||
"Last-Translator: tekulvw\n"
|
||||
"Language-Team: \n"
|
||||
"Language: de\n"
|
||||
"X-Generator: Poedit 1.8.7.1\n"
|
||||
|
||||
#: ../downloader.py:202
|
||||
msgid "That git repo has already been added under another name."
|
||||
msgstr "Diese git repo wurder bereist unter einem anderem Namen hinzugefügt."
|
||||
|
||||
#: ../downloader.py:204 ../downloader.py:205
|
||||
msgid "Something went wrong during the cloning process."
|
||||
msgstr "Etwas ist beim klonen schief gelaufen."
|
||||
|
||||
#: ../downloader.py:207
|
||||
msgid "Repo `{}` successfully added."
|
||||
msgstr "Repo `{}` erfolgreich hinzugefügt."
|
||||
|
||||
#: ../downloader.py:216
|
||||
msgid "The repo `{}` has been deleted successfully."
|
||||
msgstr "Die Repo `{}` wurde erfolgreich gelöscht."
|
||||
|
||||
#: ../downloader.py:224
|
||||
msgid "Installed Repos:\n"
|
||||
msgstr "Installierte Repos:\n"
|
||||
|
||||
#: ../downloader.py:244
|
||||
msgid "Error, there is no cog by the name of `{}` in the `{}` repo."
|
||||
msgstr "Fehler: kein Cog mit dem Namen `{}` in der Repo `{}`."
|
||||
|
||||
#: ../downloader.py:249
|
||||
msgid "Failed to install the required libraries for `{}`: `{}`"
|
||||
msgstr "Installation erforderliche Abhängigkeiten für`{}` fehlgeschlagen: `{}`"
|
||||
|
||||
#: ../downloader.py:259
|
||||
msgid "`{}` cog successfully installed."
|
||||
msgstr "`{}` Cog erfolgreich installiert."
|
||||
|
||||
#: ../downloader.py:275
|
||||
msgid "`{}` was successfully removed."
|
||||
msgstr "`{}` erfolgreich entfernt."
|
||||
|
||||
#: ../downloader.py:277
|
||||
msgid "That cog was installed but can no longer be located. You may need to remove it's files manually if it is still usable."
|
||||
msgstr "Diese Cog ist installiert konnte aber nicht gefunden werden. Wenn es noch benutzbar ist kann es sein das du die Dateien manuell löschen musst."
|
||||
|
||||
#: ../downloader.py:301
|
||||
msgid "Cog update completed successfully."
|
||||
msgstr "Cog Update erfolgreich."
|
||||
|
||||
#: ../downloader.py:309
|
||||
msgid "Available Cogs:\n"
|
||||
msgstr "Vorhandene Cogs:\n"
|
||||
|
||||
#: ../downloader.py:321
|
||||
msgid "There is no cog `{}` in the repo `{}`"
|
||||
msgstr "Kein Cog namens `{}` in der Repo `{}"
|
||||
|
||||
#: ../downloader.py:326
|
||||
msgid ""
|
||||
"Information on {}:\n"
|
||||
"{}"
|
||||
msgstr ""
|
||||
"Information zu {}:\n"
|
||||
"{}"
|
||||
|
||||
#: ../downloader.py:350
|
||||
msgid "Missing from info.json"
|
||||
msgstr "Nicht in info.json"
|
||||
|
||||
#: ../downloader.py:359
|
||||
msgid ""
|
||||
"Command: {}\n"
|
||||
"Made by: {}\n"
|
||||
"Repo: {}\n"
|
||||
"Cog name: {}"
|
||||
msgstr ""
|
||||
"Befehl: {}\n"
|
||||
"Von: {}\n"
|
||||
"Repo: {}\n"
|
||||
"Cog Name: {}"
|
||||
|
||||
#: ../downloader.py:383
|
||||
msgid "That command doesn't seem to exist."
|
||||
msgstr "Dieser Befehl existiert nicht."
|
||||
96
redbot/cogs/downloader/locales/es.po
Normal file
96
redbot/cogs/downloader/locales/es.po
Normal file
@@ -0,0 +1,96 @@
|
||||
# Copyright (C) 2017 Red-DiscordBot
|
||||
# UltimatePancake <pier.gaetani@gmail.com>, 2017.
|
||||
#
|
||||
msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: \n"
|
||||
"POT-Creation-Date: 2017-08-26 16:31+EDT\n"
|
||||
"PO-Revision-Date: 2017-08-26 20:44-0600\n"
|
||||
"MIME-Version: 1.0\n"
|
||||
"Content-Type: text/plain; charset=UTF-8\n"
|
||||
"Content-Transfer-Encoding: 8bit\n"
|
||||
"Generated-By: pygettext.py 1.5\n"
|
||||
"Last-Translator: UltimatePancake <pier.gaetani@gmail.com>\n"
|
||||
"Language-Team: \n"
|
||||
"Language: es\n"
|
||||
"X-Generator: Poedit 2.0.3\n"
|
||||
|
||||
#: ../downloader.py:202
|
||||
msgid "That git repo has already been added under another name."
|
||||
msgstr "Ese repositorio ya ha sido agregado con otro nombre."
|
||||
|
||||
#: ../downloader.py:204 ../downloader.py:205
|
||||
msgid "Something went wrong during the cloning process."
|
||||
msgstr "Error durante la clonación."
|
||||
|
||||
#: ../downloader.py:207
|
||||
msgid "Repo `{}` successfully added."
|
||||
msgstr "Repositorio `{}` agregado exitósamente."
|
||||
|
||||
#: ../downloader.py:216
|
||||
msgid "The repo `{}` has been deleted successfully."
|
||||
msgstr "Repositorio `{}` eliminado exitósamente."
|
||||
|
||||
#: ../downloader.py:224
|
||||
msgid "Installed Repos:\n"
|
||||
msgstr "Repositorios instalados:\n"
|
||||
|
||||
#: ../downloader.py:244
|
||||
msgid "Error, there is no cog by the name of `{}` in the `{}` repo."
|
||||
msgstr "Error: No existe un cog llamado `{}` en el repositorio `{}`."
|
||||
|
||||
#: ../downloader.py:249
|
||||
msgid "Failed to install the required libraries for `{}`: `{}`"
|
||||
msgstr "Error instalando las librerías requeridas para `{}`: `{}`"
|
||||
|
||||
#: ../downloader.py:259
|
||||
msgid "`{}` cog successfully installed."
|
||||
msgstr "`{}` instalado exitósamente."
|
||||
|
||||
#: ../downloader.py:275
|
||||
msgid "`{}` was successfully removed."
|
||||
msgstr "`{}` eliminado exitósamente."
|
||||
|
||||
#: ../downloader.py:277
|
||||
msgid "That cog was installed but can no longer be located. You may need to remove it's files manually if it is still usable."
|
||||
msgstr "El cog fue instalado pero ya no se puede localizar. Puede ser necesario eliminar sus archivos manualmente si aun es utilizable."
|
||||
|
||||
#: ../downloader.py:301
|
||||
msgid "Cog update completed successfully."
|
||||
msgstr "Cog actualizado exitósamente."
|
||||
|
||||
#: ../downloader.py:309
|
||||
msgid "Available Cogs:\n"
|
||||
msgstr "Cogs disponibles:\n"
|
||||
|
||||
#: ../downloader.py:321
|
||||
msgid "There is no cog `{}` in the repo `{}`"
|
||||
msgstr "No existe un cog `{}` en el repositorio `{}`"
|
||||
|
||||
#: ../downloader.py:326
|
||||
msgid ""
|
||||
"Information on {}:\n"
|
||||
"{}"
|
||||
msgstr ""
|
||||
"Información sobre {}:\n"
|
||||
"{}"
|
||||
|
||||
#: ../downloader.py:350
|
||||
msgid "Missing from info.json"
|
||||
msgstr "Ausente de info.json"
|
||||
|
||||
#: ../downloader.py:359
|
||||
msgid ""
|
||||
"Command: {}\n"
|
||||
"Made by: {}\n"
|
||||
"Repo: {}\n"
|
||||
"Cog name: {}"
|
||||
msgstr ""
|
||||
"Comando: {}\n"
|
||||
"Creado por: {}\n"
|
||||
"Repositorio: {}\n"
|
||||
"Nombre del cog: {}"
|
||||
|
||||
#: ../downloader.py:383
|
||||
msgid "That command doesn't seem to exist."
|
||||
msgstr "Ese comando no parece existir."
|
||||
97
redbot/cogs/downloader/locales/fr.po
Normal file
97
redbot/cogs/downloader/locales/fr.po
Normal file
@@ -0,0 +1,97 @@
|
||||
# SOME DESCRIPTIVE TITLE.
|
||||
# Copyright (C) YEAR ORGANIZATION
|
||||
# FIRST AUTHOR <EMAIL@ADDRESS>, YEAR.
|
||||
#
|
||||
msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: \n"
|
||||
"POT-Creation-Date: 2017-08-26 16:31+EDT\n"
|
||||
"PO-Revision-Date: 2017-08-26 23:14+0200\n"
|
||||
"MIME-Version: 1.0\n"
|
||||
"Content-Type: text/plain; charset=UTF-8\n"
|
||||
"Content-Transfer-Encoding: 8bit\n"
|
||||
"Generated-By: pygettext.py 1.5\n"
|
||||
"Last-Translator: \n"
|
||||
"Language-Team: \n"
|
||||
"Language: fr\n"
|
||||
"X-Generator: Poedit 2.0.1\n"
|
||||
|
||||
#: ../downloader.py:202
|
||||
msgid "That git repo has already been added under another name."
|
||||
msgstr "Ce repo git a déjà été ajouté sous un autre nom"
|
||||
|
||||
#: ../downloader.py:204 ../downloader.py:205
|
||||
msgid "Something went wrong during the cloning process."
|
||||
msgstr "Quelque chose s'est mal passé pendant l'installation."
|
||||
|
||||
#: ../downloader.py:207
|
||||
msgid "Repo `{}` successfully added."
|
||||
msgstr "Le repo `{}` a été ajouté avec succès"
|
||||
|
||||
#: ../downloader.py:216
|
||||
msgid "The repo `{}` has been deleted successfully."
|
||||
msgstr "Le repo `{}` a été supprimé avec succès"
|
||||
|
||||
#: ../downloader.py:224
|
||||
msgid "Installed Repos:\n"
|
||||
msgstr "Repos installés:\n"
|
||||
|
||||
#: ../downloader.py:244
|
||||
msgid "Error, there is no cog by the name of `{}` in the `{}` repo."
|
||||
msgstr "Erreur, il n'y a pas de cog du nom de `{}` dans le repo `{}`."
|
||||
|
||||
#: ../downloader.py:249
|
||||
msgid "Failed to install the required libraries for `{}`: `{}`"
|
||||
msgstr "Échec lors de l'installation des bibliothèques de `{}`: `{}`"
|
||||
|
||||
#: ../downloader.py:259
|
||||
msgid "`{}` cog successfully installed."
|
||||
msgstr "Le cog `{}` a été ajouté avec succès"
|
||||
|
||||
#: ../downloader.py:275
|
||||
msgid "`{}` was successfully removed."
|
||||
msgstr "Le cog `{}` a été retiré avec succès"
|
||||
|
||||
#: ../downloader.py:277
|
||||
msgid "That cog was installed but can no longer be located. You may need to remove it's files manually if it is still usable."
|
||||
msgstr "Ce cog a été installé mais ne peut plus être trouvé. Vous devez retirer manuellement son dossier si il est encore utilisable."
|
||||
|
||||
#: ../downloader.py:301
|
||||
msgid "Cog update completed successfully."
|
||||
msgstr "Mise à jour du cog effectuée avec succès"
|
||||
|
||||
#: ../downloader.py:309
|
||||
msgid "Available Cogs:\n"
|
||||
msgstr "Cogs disponibles:\n"
|
||||
|
||||
#: ../downloader.py:321
|
||||
msgid "There is no cog `{}` in the repo `{}`"
|
||||
msgstr "Il n'y a pas de cog `{}` dans le repo `{}`"
|
||||
|
||||
#: ../downloader.py:326
|
||||
msgid ""
|
||||
"Information on {}:\n"
|
||||
"{}"
|
||||
msgstr ""
|
||||
"Informations sur {}:\n"
|
||||
"{}"
|
||||
|
||||
#: ../downloader.py:350
|
||||
msgid "Missing from info.json"
|
||||
msgstr "Informations manquantes de info.json"
|
||||
|
||||
#: ../downloader.py:359
|
||||
msgid ""
|
||||
"Command: {}\n"
|
||||
"Made by: {}\n"
|
||||
"Repo: {}\n"
|
||||
"Cog name: {}"
|
||||
msgstr ""
|
||||
"Commande: {]\n"
|
||||
"Créé par: {}\n"
|
||||
"Repo: {}\n"
|
||||
"Nom du cog: {}"
|
||||
|
||||
#: ../downloader.py:383
|
||||
msgid "That command doesn't seem to exist."
|
||||
msgstr "Cette commande ne semble pas exister"
|
||||
93
redbot/cogs/downloader/locales/it.po
Normal file
93
redbot/cogs/downloader/locales/it.po
Normal file
@@ -0,0 +1,93 @@
|
||||
# SOME DESCRIPTIVE TITLE.
|
||||
# Copyright (C) YEAR ORGANIZATION
|
||||
# FIRST AUTHOR <EMAIL@ADDRESS>, YEAR.
|
||||
#
|
||||
msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: PACKAGE VERSION\n"
|
||||
"POT-Creation-Date: 2017-08-26 17:05+EDT\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"
|
||||
|
||||
|
||||
#: ../downloader.py:202
|
||||
msgid "That git repo has already been added under another name."
|
||||
msgstr ""
|
||||
|
||||
#: ../downloader.py:204 ../downloader.py:205
|
||||
msgid "Something went wrong during the cloning process."
|
||||
msgstr ""
|
||||
|
||||
#: ../downloader.py:207
|
||||
msgid "Repo `{}` successfully added."
|
||||
msgstr ""
|
||||
|
||||
#: ../downloader.py:216
|
||||
msgid "The repo `{}` has been deleted successfully."
|
||||
msgstr ""
|
||||
|
||||
#: ../downloader.py:224
|
||||
msgid ""
|
||||
"Installed Repos:\n"
|
||||
msgstr ""
|
||||
|
||||
#: ../downloader.py:244
|
||||
msgid "Error, there is no cog by the name of `{}` in the `{}` repo."
|
||||
msgstr ""
|
||||
|
||||
#: ../downloader.py:249
|
||||
msgid "Failed to install the required libraries for `{}`: `{}`"
|
||||
msgstr ""
|
||||
|
||||
#: ../downloader.py:259
|
||||
msgid "`{}` cog successfully installed."
|
||||
msgstr ""
|
||||
|
||||
#: ../downloader.py:275
|
||||
msgid "`{}` was successfully removed."
|
||||
msgstr ""
|
||||
|
||||
#: ../downloader.py:277
|
||||
msgid "That cog was installed but can no longer be located. You may need to remove it's files manually if it is still usable."
|
||||
msgstr ""
|
||||
|
||||
#: ../downloader.py:301
|
||||
msgid "Cog update completed successfully."
|
||||
msgstr ""
|
||||
|
||||
#: ../downloader.py:309
|
||||
msgid ""
|
||||
"Available Cogs:\n"
|
||||
msgstr ""
|
||||
|
||||
#: ../downloader.py:321
|
||||
msgid "There is no cog `{}` in the repo `{}`"
|
||||
msgstr ""
|
||||
|
||||
#: ../downloader.py:326
|
||||
msgid ""
|
||||
"Information on {}:\n"
|
||||
"{}"
|
||||
msgstr ""
|
||||
|
||||
#: ../downloader.py:350
|
||||
msgid "Missing from info.json"
|
||||
msgstr ""
|
||||
|
||||
#: ../downloader.py:359
|
||||
msgid ""
|
||||
"Command: {}\n"
|
||||
"Made by: {}\n"
|
||||
"Repo: {}\n"
|
||||
"Cog name: {}"
|
||||
msgstr ""
|
||||
|
||||
#: ../downloader.py:383
|
||||
msgid "That command doesn't seem to exist."
|
||||
msgstr ""
|
||||
|
||||
93
redbot/cogs/downloader/locales/messages.pot
Normal file
93
redbot/cogs/downloader/locales/messages.pot
Normal file
@@ -0,0 +1,93 @@
|
||||
# SOME DESCRIPTIVE TITLE.
|
||||
# Copyright (C) YEAR ORGANIZATION
|
||||
# FIRST AUTHOR <EMAIL@ADDRESS>, YEAR.
|
||||
#
|
||||
msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: PACKAGE VERSION\n"
|
||||
"POT-Creation-Date: 2017-08-26 16:24+EDT\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"
|
||||
|
||||
|
||||
#: ../downloader.py:202
|
||||
msgid "That git repo has already been added under another name."
|
||||
msgstr ""
|
||||
|
||||
#: ../downloader.py:204 ../downloader.py:205
|
||||
msgid "Something went wrong during the cloning process."
|
||||
msgstr ""
|
||||
|
||||
#: ../downloader.py:207
|
||||
msgid "Repo `{}` successfully added."
|
||||
msgstr ""
|
||||
|
||||
#: ../downloader.py:216
|
||||
msgid "The repo `{}` has been deleted successfully."
|
||||
msgstr ""
|
||||
|
||||
#: ../downloader.py:224
|
||||
msgid ""
|
||||
"Installed Repos:\n"
|
||||
msgstr ""
|
||||
|
||||
#: ../downloader.py:244
|
||||
msgid "Error, there is no cog by the name of `{}` in the `{}` repo."
|
||||
msgstr ""
|
||||
|
||||
#: ../downloader.py:249
|
||||
msgid "Failed to install the required libraries for `{}`: `{}`"
|
||||
msgstr ""
|
||||
|
||||
#: ../downloader.py:259
|
||||
msgid "`{}` cog successfully installed."
|
||||
msgstr ""
|
||||
|
||||
#: ../downloader.py:275
|
||||
msgid "`{}` was successfully removed."
|
||||
msgstr ""
|
||||
|
||||
#: ../downloader.py:277
|
||||
msgid "That cog was installed but can no longer be located. You may need to remove it's files manually if it is still usable."
|
||||
msgstr ""
|
||||
|
||||
#: ../downloader.py:301
|
||||
msgid "Cog update completed successfully."
|
||||
msgstr ""
|
||||
|
||||
#: ../downloader.py:309
|
||||
msgid ""
|
||||
"Available Cogs:\n"
|
||||
msgstr ""
|
||||
|
||||
#: ../downloader.py:321
|
||||
msgid "There is no cog `{}` in the repo `{}`"
|
||||
msgstr ""
|
||||
|
||||
#: ../downloader.py:326
|
||||
msgid ""
|
||||
"Information on {}:\n"
|
||||
"{}"
|
||||
msgstr ""
|
||||
|
||||
#: ../downloader.py:350
|
||||
msgid "Missing from info.json"
|
||||
msgstr ""
|
||||
|
||||
#: ../downloader.py:359
|
||||
msgid ""
|
||||
"Command: {}\n"
|
||||
"Made by: {}\n"
|
||||
"Repo: {}\n"
|
||||
"Cog name: {}"
|
||||
msgstr ""
|
||||
|
||||
#: ../downloader.py:383
|
||||
msgid "That command doesn't seem to exist."
|
||||
msgstr ""
|
||||
|
||||
94
redbot/cogs/downloader/locales/nl.po
Normal file
94
redbot/cogs/downloader/locales/nl.po
Normal file
@@ -0,0 +1,94 @@
|
||||
# SOME DESCRIPTIVE TITLE.
|
||||
# Copyright (C) YEAR ORGANIZATION
|
||||
# FIRST AUTHOR <EMAIL@ADDRESS>, YEAR.
|
||||
#
|
||||
msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: \n"
|
||||
"POT-Creation-Date: 2017-08-26 16:35-0400\n"
|
||||
"PO-Revision-Date: 2017-08-26 16:42-0400\n"
|
||||
"Language-Team: \n"
|
||||
"MIME-Version: 1.0\n"
|
||||
"Content-Type: text/plain; charset=UTF-8\n"
|
||||
"Content-Transfer-Encoding: 8bit\n"
|
||||
"Generated-By: pygettext.py 1.5\n"
|
||||
"X-Generator: Poedit 1.8.7.1\n"
|
||||
"Last-Translator: \n"
|
||||
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
|
||||
"Language: nl\n"
|
||||
|
||||
#: ../downloader.py:202
|
||||
msgid "That git repo has already been added under another name."
|
||||
msgstr ""
|
||||
|
||||
#: ../downloader.py:204 ../downloader.py:205
|
||||
msgid "Something went wrong during the cloning process."
|
||||
msgstr ""
|
||||
|
||||
#: ../downloader.py:207
|
||||
msgid "Repo `{}` successfully added."
|
||||
msgstr ""
|
||||
|
||||
#: ../downloader.py:216
|
||||
msgid "The repo `{}` has been deleted successfully."
|
||||
msgstr ""
|
||||
|
||||
#: ../downloader.py:224
|
||||
msgid "Installed Repos:\n"
|
||||
msgstr ""
|
||||
|
||||
#: ../downloader.py:244
|
||||
msgid "Error, there is no cog by the name of `{}` in the `{}` repo."
|
||||
msgstr ""
|
||||
|
||||
#: ../downloader.py:249
|
||||
msgid "Failed to install the required libraries for `{}`: `{}`"
|
||||
msgstr ""
|
||||
|
||||
#: ../downloader.py:259
|
||||
msgid "`{}` cog successfully installed."
|
||||
msgstr ""
|
||||
|
||||
#: ../downloader.py:275
|
||||
msgid "`{}` was successfully removed."
|
||||
msgstr ""
|
||||
|
||||
#: ../downloader.py:277
|
||||
msgid ""
|
||||
"That cog was installed but can no longer be located. You may need to remove "
|
||||
"it's files manually if it is still usable."
|
||||
msgstr ""
|
||||
|
||||
#: ../downloader.py:301
|
||||
msgid "Cog update completed successfully."
|
||||
msgstr ""
|
||||
|
||||
#: ../downloader.py:309
|
||||
msgid "Available Cogs:\n"
|
||||
msgstr ""
|
||||
|
||||
#: ../downloader.py:321
|
||||
msgid "There is no cog `{}` in the repo `{}`"
|
||||
msgstr ""
|
||||
|
||||
#: ../downloader.py:326
|
||||
msgid ""
|
||||
"Information on {}:\n"
|
||||
"{}"
|
||||
msgstr ""
|
||||
|
||||
#: ../downloader.py:350
|
||||
msgid "Missing from info.json"
|
||||
msgstr ""
|
||||
|
||||
#: ../downloader.py:359
|
||||
msgid ""
|
||||
"Command: {}\n"
|
||||
"Made by: {}\n"
|
||||
"Repo: {}\n"
|
||||
"Cog name: {}"
|
||||
msgstr ""
|
||||
|
||||
#: ../downloader.py:383
|
||||
msgid "That command doesn't seem to exist."
|
||||
msgstr ""
|
||||
3
redbot/cogs/downloader/log.py
Normal file
3
redbot/cogs/downloader/log.py
Normal file
@@ -0,0 +1,3 @@
|
||||
import logging
|
||||
|
||||
log = logging.getLogger("red.downloader")
|
||||
559
redbot/cogs/downloader/repo_manager.py
Normal file
559
redbot/cogs/downloader/repo_manager.py
Normal file
@@ -0,0 +1,559 @@
|
||||
import asyncio
|
||||
import functools
|
||||
import os
|
||||
import pkgutil
|
||||
import shutil
|
||||
from concurrent.futures import ThreadPoolExecutor
|
||||
from pathlib import Path
|
||||
from subprocess import run as sp_run, PIPE
|
||||
from sys import executable
|
||||
from typing import Tuple, MutableMapping, Union
|
||||
|
||||
from discord.ext import commands
|
||||
|
||||
from redbot.core import Config
|
||||
from redbot.core import data_manager
|
||||
from .errors import *
|
||||
from .installable import Installable, InstallableType
|
||||
from .json_mixins import RepoJSONMixin
|
||||
from .log import log
|
||||
|
||||
|
||||
class Repo(RepoJSONMixin):
|
||||
GIT_CLONE = "git clone -b {branch} {url} {folder}"
|
||||
GIT_CLONE_NO_BRANCH = "git clone {url} {folder}"
|
||||
GIT_CURRENT_BRANCH = "git -C {path} rev-parse --abbrev-ref HEAD"
|
||||
GIT_LATEST_COMMIT = "git -C {path} rev-parse {branch}"
|
||||
GIT_HARD_RESET = "git -C {path} reset --hard origin/{branch} -q"
|
||||
GIT_PULL = "git -C {path} pull -q --ff-only"
|
||||
GIT_DIFF_FILE_STATUS = ("git -C {path} diff --no-commit-id --name-status"
|
||||
" {old_hash} {new_hash}")
|
||||
GIT_LOG = ("git -C {path} log --relative-date --reverse {old_hash}.."
|
||||
" {relative_file_path}")
|
||||
|
||||
PIP_INSTALL = "{python} -m pip install -U -t {target_dir} {reqs}"
|
||||
|
||||
def __init__(self, name: str, url: str, branch: str, folder_path: Path,
|
||||
available_modules: Tuple[Installable]=(), loop: asyncio.AbstractEventLoop=None):
|
||||
self.url = url
|
||||
self.branch = branch
|
||||
|
||||
self.name = name
|
||||
|
||||
self.folder_path = folder_path
|
||||
self.folder_path.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
super().__init__(self.folder_path)
|
||||
|
||||
self.available_modules = available_modules
|
||||
|
||||
self._executor = ThreadPoolExecutor(1)
|
||||
|
||||
self._repo_lock = asyncio.Lock()
|
||||
|
||||
self._loop = loop
|
||||
if self._loop is None:
|
||||
self._loop = asyncio.get_event_loop()
|
||||
|
||||
@classmethod
|
||||
async def convert(cls, ctx: commands.Context, argument: str):
|
||||
downloader_cog = ctx.bot.get_cog("Downloader")
|
||||
if downloader_cog is None:
|
||||
raise commands.CommandError("No Downloader cog found.")
|
||||
|
||||
# noinspection PyProtectedMember
|
||||
repo_manager = downloader_cog._repo_manager
|
||||
poss_repo = repo_manager.get_repo(argument)
|
||||
if poss_repo is None:
|
||||
raise commands.BadArgument("Repo by the name {} does not exist.".format(argument))
|
||||
return poss_repo
|
||||
|
||||
def _existing_git_repo(self) -> (bool, Path):
|
||||
git_path = self.folder_path / '.git'
|
||||
return git_path.exists(), git_path
|
||||
|
||||
async def _get_file_update_statuses(
|
||||
self, old_hash: str, new_hash: str) -> MutableMapping[str, str]:
|
||||
"""
|
||||
Gets the file update status letters for each changed file between
|
||||
the two hashes.
|
||||
:param old_hash: Pre-update
|
||||
:param new_hash: Post-update
|
||||
:return: Mapping of filename -> status_letter
|
||||
"""
|
||||
p = await self._run(
|
||||
self.GIT_DIFF_FILE_STATUS.format(
|
||||
path=self.folder_path,
|
||||
old_hash=old_hash,
|
||||
new_hash=new_hash
|
||||
)
|
||||
)
|
||||
|
||||
if p.returncode != 0:
|
||||
raise GitDiffError("Git diff failed for repo at path:"
|
||||
" {}".format(self.folder_path))
|
||||
|
||||
stdout = p.stdout.strip().decode().split('\n')
|
||||
|
||||
ret = {}
|
||||
|
||||
for filename in stdout:
|
||||
# TODO: filter these filenames by ones in self.available_modules
|
||||
status, _, filepath = filename.partition('\t')
|
||||
ret[filepath] = status
|
||||
|
||||
return ret
|
||||
|
||||
async def _get_commit_notes(self, old_commit_hash: str,
|
||||
relative_file_path: str) -> str:
|
||||
"""
|
||||
Gets the commit notes from git log.
|
||||
:param old_commit_hash: Point in time to start getting messages
|
||||
:param relative_file_path: Path relative to the repo folder of the file
|
||||
to get messages for.
|
||||
:return: Git commit note log
|
||||
"""
|
||||
p = await self._run(
|
||||
self.GIT_LOG.format(
|
||||
path=self.folder_path,
|
||||
old_hash=old_commit_hash,
|
||||
relative_file_path=relative_file_path
|
||||
)
|
||||
)
|
||||
|
||||
if p.returncode != 0:
|
||||
raise GitException("An exception occurred while executing git log on"
|
||||
" this repo: {}".format(self.folder_path))
|
||||
|
||||
return p.stdout.decode().strip()
|
||||
|
||||
def _update_available_modules(self) -> Tuple[str]:
|
||||
"""
|
||||
Updates the available modules attribute for this repo.
|
||||
:return: List of available modules.
|
||||
"""
|
||||
curr_modules = []
|
||||
"""
|
||||
for name in self.folder_path.iterdir():
|
||||
if name.is_dir():
|
||||
spec = importlib.util.spec_from_file_location(
|
||||
name.stem, location=str(name.parent)
|
||||
)
|
||||
if spec is not None:
|
||||
curr_modules.append(
|
||||
Installable(location=name)
|
||||
)
|
||||
"""
|
||||
for file_finder, name, is_pkg in pkgutil.walk_packages(path=[str(self.folder_path), ]):
|
||||
curr_modules.append(
|
||||
Installable(location=self.folder_path / name)
|
||||
)
|
||||
self.available_modules = curr_modules
|
||||
|
||||
# noinspection PyTypeChecker
|
||||
return tuple(self.available_modules)
|
||||
|
||||
async def _run(self, *args, **kwargs):
|
||||
env = os.environ.copy()
|
||||
env['GIT_TERMINAL_PROMPT'] = '0'
|
||||
kwargs['env'] = env
|
||||
async with self._repo_lock:
|
||||
return await self._loop.run_in_executor(
|
||||
self._executor,
|
||||
functools.partial(sp_run, *args, stdout=PIPE, **kwargs)
|
||||
)
|
||||
|
||||
async def clone(self) -> Tuple[str]:
|
||||
"""
|
||||
Clones a new repo.
|
||||
|
||||
:return: List of available module names from this repo.
|
||||
"""
|
||||
exists, path = self._existing_git_repo()
|
||||
if exists:
|
||||
raise ExistingGitRepo(
|
||||
"A git repo already exists at path: {}".format(path)
|
||||
)
|
||||
|
||||
if self.branch is not None:
|
||||
p = await self._run(
|
||||
self.GIT_CLONE.format(
|
||||
branch=self.branch,
|
||||
url=self.url,
|
||||
folder=self.folder_path
|
||||
).split()
|
||||
)
|
||||
else:
|
||||
p = await self._run(
|
||||
self.GIT_CLONE_NO_BRANCH.format(
|
||||
url=self.url,
|
||||
folder=self.folder_path
|
||||
).split()
|
||||
)
|
||||
|
||||
if p.returncode != 0:
|
||||
raise CloningError("Error when running git clone.")
|
||||
|
||||
if self.branch is None:
|
||||
self.branch = await self.current_branch()
|
||||
|
||||
self._read_info_file()
|
||||
|
||||
return self._update_available_modules()
|
||||
|
||||
async def current_branch(self) -> str:
|
||||
"""
|
||||
Determines the current branch using git commands.
|
||||
|
||||
:return: Current branch name
|
||||
"""
|
||||
exists, _ = self._existing_git_repo()
|
||||
if not exists:
|
||||
raise MissingGitRepo(
|
||||
"A git repo does not exist at path: {}".format(self.folder_path)
|
||||
)
|
||||
|
||||
p = await self._run(
|
||||
self.GIT_CURRENT_BRANCH.format(
|
||||
path=self.folder_path
|
||||
).split()
|
||||
)
|
||||
|
||||
if p.returncode != 0:
|
||||
raise GitException("Could not determine current branch"
|
||||
" at path: {}".format(self.folder_path))
|
||||
|
||||
return p.stdout.decode().strip()
|
||||
|
||||
async def current_commit(self, branch: str=None) -> str:
|
||||
"""
|
||||
Determines the current commit hash of the repo.
|
||||
|
||||
:param branch: Override for repo's branch attribute
|
||||
:return: Commit hash string
|
||||
"""
|
||||
if branch is None:
|
||||
branch = self.branch
|
||||
|
||||
exists, _ = self._existing_git_repo()
|
||||
if not exists:
|
||||
raise MissingGitRepo(
|
||||
"A git repo does not exist at path: {}".format(self.folder_path)
|
||||
)
|
||||
|
||||
p = await self._run(
|
||||
self.GIT_LATEST_COMMIT.format(
|
||||
path=self.folder_path,
|
||||
branch=branch
|
||||
).split()
|
||||
)
|
||||
|
||||
if p.returncode != 0:
|
||||
raise CurrentHashError("Unable to determine old commit hash.")
|
||||
|
||||
return p.stdout.decode().strip()
|
||||
|
||||
async def hard_reset(self, branch: str=None) -> None:
|
||||
"""
|
||||
Performs a hard reset on the current repo.
|
||||
|
||||
:param branch: Override for repo branch attribute.
|
||||
"""
|
||||
if branch is None:
|
||||
branch = self.branch
|
||||
|
||||
exists, _ = self._existing_git_repo()
|
||||
if not exists:
|
||||
raise MissingGitRepo(
|
||||
"A git repo does not exist at path: {}".format(self.folder_path)
|
||||
)
|
||||
|
||||
p = await self._run(
|
||||
self.GIT_HARD_RESET.format(
|
||||
path=self.folder_path,
|
||||
branch=branch
|
||||
).split()
|
||||
)
|
||||
|
||||
if p.returncode != 0:
|
||||
raise HardResetError("Some error occurred when trying to"
|
||||
" execute a hard reset on the repo at"
|
||||
" the following path: {}".format(self.folder_path))
|
||||
|
||||
async def update(self) -> (str, str):
|
||||
"""
|
||||
Updates the current branch of this repo.
|
||||
|
||||
:return: tuple of (old commit hash, new commit hash)
|
||||
:rtype: tuple
|
||||
"""
|
||||
curr_branch = await self.current_branch()
|
||||
old_commit = await self.current_commit(branch=curr_branch)
|
||||
|
||||
await self.hard_reset(branch=curr_branch)
|
||||
|
||||
p = await self._run(
|
||||
self.GIT_PULL.format(
|
||||
path=self.folder_path
|
||||
).split()
|
||||
)
|
||||
|
||||
if p.returncode != 0:
|
||||
raise UpdateError("Git pull returned a non zero exit code"
|
||||
" for the repo located at path: {}".format(self.folder_path))
|
||||
|
||||
new_commit = await self.current_commit(branch=curr_branch)
|
||||
|
||||
self._update_available_modules()
|
||||
self._read_info_file()
|
||||
|
||||
return old_commit, new_commit
|
||||
|
||||
async def install_cog(self, cog: Installable, target_dir: Path) -> bool:
|
||||
"""
|
||||
Copies a cog to the target directory.
|
||||
|
||||
:param Installable cog: Cog to install.
|
||||
:param pathlib.Path target_dir: Directory to install the cog in.
|
||||
:return: Installation success status.
|
||||
:rtype: bool
|
||||
"""
|
||||
if cog not in self.available_cogs:
|
||||
raise DownloaderException("That cog does not exist in this repo")
|
||||
|
||||
if not target_dir.is_dir():
|
||||
raise ValueError("That target directory is not actually a directory.")
|
||||
|
||||
if not target_dir.exists():
|
||||
raise ValueError("That target directory does not exist.")
|
||||
|
||||
return await cog.copy_to(target_dir=target_dir)
|
||||
|
||||
async def install_libraries(self, target_dir: Path, libraries: Tuple[Installable]=()) -> bool:
|
||||
"""
|
||||
Copies all shared libraries (or a given subset) to the target
|
||||
directory.
|
||||
|
||||
:param pathlib.Path target_dir: Directory to install shared libraries to.
|
||||
:param tuple(Installable) libraries: A subset of available libraries.
|
||||
:return: Status of all installs.
|
||||
:rtype: bool
|
||||
"""
|
||||
if libraries:
|
||||
if not all([i in self.available_libraries for i in libraries]):
|
||||
raise ValueError("Some given libraries are not available in this repo.")
|
||||
else:
|
||||
libraries = self.available_libraries
|
||||
|
||||
if libraries:
|
||||
return all([lib.copy_to(target_dir=target_dir) for lib in libraries])
|
||||
return True
|
||||
|
||||
async def install_requirements(self, cog: Installable, target_dir: Path) -> bool:
|
||||
"""
|
||||
Installs the requirements defined by the requirements
|
||||
attribute on the cog object and puts them in the given
|
||||
target directory.
|
||||
|
||||
:param Installable cog: Cog for which to install requirements.
|
||||
:param pathlib.Path target_dir: Path to which to install requirements.
|
||||
:return: Status of requirements install.
|
||||
:rtype: bool
|
||||
"""
|
||||
if not target_dir.is_dir():
|
||||
raise ValueError("Target directory is not a directory.")
|
||||
target_dir.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
return await self.install_raw_requirements(cog.requirements, target_dir)
|
||||
|
||||
async def install_raw_requirements(self, requirements: Tuple[str], target_dir: Path) -> bool:
|
||||
"""
|
||||
Installs a list of requirements using pip and places them into
|
||||
the given target directory.
|
||||
|
||||
:param tuple(str) requirements: List of requirement names to install via pip.
|
||||
:param pathlib.Path target_dir: Directory to install requirements to.
|
||||
:return: Status of all requirements install.
|
||||
:rtype: bool
|
||||
"""
|
||||
if len(requirements) == 0:
|
||||
return True
|
||||
|
||||
# TODO: Check and see if any of these modules are already available
|
||||
|
||||
p = await self._run(
|
||||
self.PIP_INSTALL.format(
|
||||
python=executable,
|
||||
target_dir=target_dir,
|
||||
reqs=" ".join(requirements)
|
||||
).split()
|
||||
)
|
||||
|
||||
if p.returncode != 0:
|
||||
log.error("Something went wrong when installing"
|
||||
" the following requirements:"
|
||||
" {}".format(", ".join(requirements)))
|
||||
return False
|
||||
return True
|
||||
|
||||
@property
|
||||
def available_cogs(self) -> Tuple[Installable]:
|
||||
"""
|
||||
Returns a list of available cogs (not shared libraries and not hidden).
|
||||
|
||||
:rtype: tuple(Installable)
|
||||
"""
|
||||
# noinspection PyTypeChecker
|
||||
return tuple(
|
||||
[m for m in self.available_modules
|
||||
if m.type == InstallableType.COG and not m.hidden]
|
||||
)
|
||||
|
||||
@property
|
||||
def available_libraries(self) -> Tuple[Installable]:
|
||||
"""
|
||||
Returns a list of available shared libraries in this repo.
|
||||
|
||||
:rtype: tuple(Installable)
|
||||
"""
|
||||
# noinspection PyTypeChecker
|
||||
return tuple(
|
||||
[m for m in self.available_modules
|
||||
if m.type == InstallableType.SHARED_LIBRARY]
|
||||
)
|
||||
|
||||
def to_json(self):
|
||||
return {
|
||||
"url": self.url,
|
||||
"name": self.name,
|
||||
"branch": self.branch,
|
||||
"folder_path": self.folder_path.relative_to(Path.cwd()).parts,
|
||||
"available_modules": [m.to_json() for m in self.available_modules]
|
||||
}
|
||||
|
||||
@classmethod
|
||||
def from_json(cls, data):
|
||||
# noinspection PyTypeChecker
|
||||
return Repo(data['name'], data['url'], data['branch'],
|
||||
Path.cwd() / Path(*data['folder_path']),
|
||||
tuple([Installable.from_json(m) for m in data['available_modules']]))
|
||||
|
||||
|
||||
class RepoManager:
|
||||
def __init__(self, downloader_config: Config):
|
||||
self.downloader_config = downloader_config
|
||||
|
||||
self._repos = {}
|
||||
|
||||
loop = asyncio.get_event_loop()
|
||||
loop.create_task(self._load_repos(set=True)) # str_name: Repo
|
||||
|
||||
@property
|
||||
def repos_folder(self) -> Path:
|
||||
data_folder = data_manager.cog_data_path(self)
|
||||
return data_folder / 'repos'
|
||||
|
||||
def does_repo_exist(self, name: str) -> bool:
|
||||
return name in self._repos
|
||||
|
||||
@staticmethod
|
||||
def validate_and_normalize_repo_name(name: str) -> str:
|
||||
if not name.isidentifier():
|
||||
raise InvalidRepoName("Not a valid Python variable name.")
|
||||
return name.lower()
|
||||
|
||||
async def add_repo(self, url: str, name: str, branch: str="master") -> Repo:
|
||||
"""
|
||||
Adds a repo and clones it.
|
||||
|
||||
:param url: URL of git repo to clone.
|
||||
:param name: Internal name of repo.
|
||||
:param branch: Branch to clone.
|
||||
:return: New repo object representing cloned repo.
|
||||
:rtype: Repo
|
||||
"""
|
||||
name = self.validate_and_normalize_repo_name(name)
|
||||
if self.does_repo_exist(name):
|
||||
raise InvalidRepoName(
|
||||
"That repo name you provided already exists."
|
||||
" Please choose another."
|
||||
)
|
||||
|
||||
# noinspection PyTypeChecker
|
||||
r = Repo(url=url, name=name, branch=branch,
|
||||
folder_path=self.repos_folder / name)
|
||||
await r.clone()
|
||||
|
||||
self._repos[name] = r
|
||||
await self._save_repos()
|
||||
|
||||
return r
|
||||
|
||||
def get_repo(self, name: str) -> Union[Repo, None]:
|
||||
"""
|
||||
Returns a repo object with the given name.
|
||||
|
||||
:param name: Repo name
|
||||
:return: Repo object or ``None`` if repo does not exist.
|
||||
:rtype: Union[Repo, None]
|
||||
"""
|
||||
return self._repos.get(name, None)
|
||||
|
||||
def get_all_repo_names(self) -> Tuple[str]:
|
||||
"""
|
||||
Returns a tuple of all repo names
|
||||
|
||||
:rtype: tuple(str)
|
||||
"""
|
||||
# noinspection PyTypeChecker
|
||||
return tuple(self._repos.keys())
|
||||
|
||||
async def delete_repo(self, name: str):
|
||||
"""
|
||||
Deletes a repo and its folders with the given name.
|
||||
|
||||
:param name: Name of the repo to delete.
|
||||
:raises MissingGitRepo: If the repo does not exist.
|
||||
"""
|
||||
repo = self.get_repo(name)
|
||||
if repo is None:
|
||||
raise MissingGitRepo("There is no repo with the name {}".format(name))
|
||||
|
||||
shutil.rmtree(str(repo.folder_path))
|
||||
|
||||
try:
|
||||
del self._repos[name]
|
||||
except KeyError:
|
||||
pass
|
||||
|
||||
await self._save_repos()
|
||||
|
||||
async def update_all_repos(self) -> MutableMapping[Repo, Tuple[str, str]]:
|
||||
"""
|
||||
Calls :py:meth:`Repo.update` on all repos.
|
||||
|
||||
:return:
|
||||
A mapping of :py:class:`Repo` objects that received new commits to a tuple containing old and
|
||||
new commit hashes.
|
||||
"""
|
||||
ret = {}
|
||||
for _, repo in self._repos.items():
|
||||
old, new = await repo.update()
|
||||
if old != new:
|
||||
ret[repo] = (old, new)
|
||||
|
||||
await self._save_repos()
|
||||
return ret
|
||||
|
||||
async def _load_repos(self, set=False) -> MutableMapping[str, Repo]:
|
||||
ret = {
|
||||
name: Repo.from_json(data) for name, data in
|
||||
(await self.downloader_config.repos()).items()
|
||||
}
|
||||
if set:
|
||||
self._repos = ret
|
||||
return ret
|
||||
|
||||
async def _save_repos(self):
|
||||
repo_json_info = {name: r.to_json() for name, r in self._repos.items()}
|
||||
await self.downloader_config.repos.set(repo_json_info)
|
||||
2
redbot/cogs/downloader/repos/.gitignore
vendored
Normal file
2
redbot/cogs/downloader/repos/.gitignore
vendored
Normal file
@@ -0,0 +1,2 @@
|
||||
*
|
||||
!.gitignore
|
||||
6
redbot/cogs/economy/__init__.py
Normal file
6
redbot/cogs/economy/__init__.py
Normal file
@@ -0,0 +1,6 @@
|
||||
from redbot.core.bot import Red
|
||||
from .economy import Economy
|
||||
|
||||
|
||||
def setup(bot: Red):
|
||||
bot.add_cog(Economy(bot))
|
||||
533
redbot/cogs/economy/economy.py
Normal file
533
redbot/cogs/economy/economy.py
Normal file
@@ -0,0 +1,533 @@
|
||||
import calendar
|
||||
import logging
|
||||
import random
|
||||
from collections import defaultdict, deque
|
||||
from enum import Enum
|
||||
|
||||
import discord
|
||||
|
||||
from redbot.cogs.bank import check_global_setting_guildowner, check_global_setting_admin
|
||||
from redbot.core import Config, bank
|
||||
from redbot.core.i18n import CogI18n
|
||||
from redbot.core.utils.chat_formatting import pagify, box
|
||||
from discord.ext import commands
|
||||
|
||||
from redbot.core.bot import Red
|
||||
|
||||
_ = CogI18n("Economy", __file__)
|
||||
|
||||
logger = logging.getLogger("red.economy")
|
||||
|
||||
NUM_ENC = "\N{COMBINING ENCLOSING KEYCAP}"
|
||||
|
||||
|
||||
class SMReel(Enum):
|
||||
cherries = "\N{CHERRIES}"
|
||||
cookie = "\N{COOKIE}"
|
||||
two = "\N{DIGIT TWO}" + NUM_ENC
|
||||
flc = "\N{FOUR LEAF CLOVER}"
|
||||
cyclone = "\N{CYCLONE}"
|
||||
sunflower = "\N{SUNFLOWER}"
|
||||
six = "\N{DIGIT SIX}" + NUM_ENC
|
||||
mushroom = "\N{MUSHROOM}"
|
||||
heart = "\N{HEAVY BLACK HEART}"
|
||||
snowflake = "\N{SNOWFLAKE}"
|
||||
|
||||
|
||||
PAYOUTS = {
|
||||
(SMReel.two, SMReel.two, SMReel.six): {
|
||||
"payout": lambda x: x * 2500 + x,
|
||||
"phrase": _("JACKPOT! 226! Your bid has been multiplied * 2500!")
|
||||
},
|
||||
(SMReel.flc, SMReel.flc, SMReel.flc): {
|
||||
"payout": lambda x: x + 1000,
|
||||
"phrase": _("4LC! +1000!")
|
||||
},
|
||||
(SMReel.cherries, SMReel.cherries, SMReel.cherries): {
|
||||
"payout": lambda x: x + 800,
|
||||
"phrase": _("Three cherries! +800!")
|
||||
},
|
||||
(SMReel.two, SMReel.six): {
|
||||
"payout": lambda x: x * 4 + x,
|
||||
"phrase": _("2 6! Your bid has been multiplied * 4!")
|
||||
},
|
||||
(SMReel.cherries, SMReel.cherries): {
|
||||
"payout": lambda x: x * 3 + x,
|
||||
"phrase": _("Two cherries! Your bid has been multiplied * 3!")
|
||||
},
|
||||
"3 symbols": {
|
||||
"payout": lambda x: x + 500,
|
||||
"phrase": _("Three symbols! +500!")
|
||||
},
|
||||
"2 symbols": {
|
||||
"payout": lambda x: x * 2 + x,
|
||||
"phrase": _("Two consecutive symbols! Your bid has been multiplied * 2!")
|
||||
},
|
||||
}
|
||||
|
||||
SLOT_PAYOUTS_MSG = _("Slot machine payouts:\n"
|
||||
"{two.value} {two.value} {six.value} Bet * 2500\n"
|
||||
"{flc.value} {flc.value} {flc.value} +1000\n"
|
||||
"{cherries.value} {cherries.value} {cherries.value} +800\n"
|
||||
"{two.value} {six.value} Bet * 4\n"
|
||||
"{cherries.value} {cherries.value} Bet * 3\n\n"
|
||||
"Three symbols: +500\n"
|
||||
"Two symbols: Bet * 2").format(**SMReel.__dict__)
|
||||
|
||||
|
||||
def guild_only_check():
|
||||
async def pred(ctx: commands.Context):
|
||||
if await bank.is_global():
|
||||
return True
|
||||
elif not await bank.is_global() and ctx.guild is not None:
|
||||
return True
|
||||
else:
|
||||
return False
|
||||
return commands.check(pred)
|
||||
|
||||
|
||||
class SetParser:
|
||||
def __init__(self, argument):
|
||||
allowed = ("+", "-")
|
||||
self.sum = int(argument)
|
||||
if argument and argument[0] in allowed:
|
||||
if self.sum < 0:
|
||||
self.operation = "withdraw"
|
||||
elif self.sum > 0:
|
||||
self.operation = "deposit"
|
||||
else:
|
||||
raise RuntimeError
|
||||
self.sum = abs(self.sum)
|
||||
elif argument.isdigit():
|
||||
self.operation = "set"
|
||||
else:
|
||||
raise RuntimeError
|
||||
|
||||
|
||||
class Economy:
|
||||
"""Economy
|
||||
|
||||
Get rich and have fun with imaginary currency!"""
|
||||
|
||||
default_guild_settings = {
|
||||
"PAYDAY_TIME": 300,
|
||||
"PAYDAY_CREDITS": 120,
|
||||
"SLOT_MIN": 5,
|
||||
"SLOT_MAX": 100,
|
||||
"SLOT_TIME": 0,
|
||||
"REGISTER_CREDITS": 0
|
||||
}
|
||||
|
||||
default_global_settings = default_guild_settings
|
||||
|
||||
default_member_settings = {
|
||||
"next_payday": 0,
|
||||
"last_slot": 0
|
||||
}
|
||||
|
||||
default_user_settings = default_member_settings
|
||||
|
||||
def __init__(self, bot: Red):
|
||||
self.bot = bot
|
||||
self.file_path = "data/economy/settings.json"
|
||||
self.config = Config.get_conf(self, 1256844281)
|
||||
self.config.register_guild(**self.default_guild_settings)
|
||||
self.config.register_global(**self.default_global_settings)
|
||||
self.config.register_member(**self.default_member_settings)
|
||||
self.config.register_user(**self.default_user_settings)
|
||||
self.slot_register = defaultdict(dict)
|
||||
|
||||
@commands.group(name="bank")
|
||||
async def _bank(self, ctx: commands.Context):
|
||||
"""Bank operations"""
|
||||
if ctx.invoked_subcommand is None:
|
||||
await self.bot.send_cmd_help(ctx)
|
||||
|
||||
@_bank.command()
|
||||
async def balance(self, ctx: commands.Context, user: discord.Member = None):
|
||||
"""Shows balance of user.
|
||||
|
||||
Defaults to yours."""
|
||||
if user is None:
|
||||
user = ctx.author
|
||||
|
||||
bal = await bank.get_balance(user)
|
||||
currency = await bank.get_currency_name(ctx.guild)
|
||||
|
||||
await ctx.send(_("{}'s balance is {} {}").format(
|
||||
user.display_name, bal, currency))
|
||||
|
||||
@_bank.command()
|
||||
async def transfer(self, ctx: commands.Context, to: discord.Member, amount: int):
|
||||
"""Transfer currency to other users"""
|
||||
from_ = ctx.author
|
||||
currency = await bank.get_currency_name(ctx.guild)
|
||||
|
||||
try:
|
||||
await bank.transfer_credits(from_, to, amount)
|
||||
except ValueError as e:
|
||||
await ctx.send(str(e))
|
||||
|
||||
await ctx.send(_("{} transferred {} {} to {}").format(
|
||||
from_.display_name, amount, currency, to.display_name
|
||||
))
|
||||
|
||||
@_bank.command(name="set")
|
||||
@check_global_setting_admin()
|
||||
async def _set(self, ctx: commands.Context, to: discord.Member, creds: SetParser):
|
||||
"""Sets balance of user's bank account. See help for more operations
|
||||
|
||||
Passing positive and negative values will add/remove currency instead
|
||||
|
||||
Examples:
|
||||
bank set @Twentysix 26 - Sets balance to 26
|
||||
bank set @Twentysix +2 - Increases balance by 2
|
||||
bank set @Twentysix -6 - Decreases balance by 6"""
|
||||
author = ctx.author
|
||||
currency = await bank.get_currency_name(ctx.guild)
|
||||
|
||||
if creds.operation == "deposit":
|
||||
await bank.deposit_credits(to, creds.sum)
|
||||
await ctx.send(_("{} added {} {} to {}'s account.").format(
|
||||
author.display_name, creds.sum, currency, to.display_name
|
||||
))
|
||||
elif creds.operation == "withdraw":
|
||||
await bank.withdraw_credits(to, creds.sum)
|
||||
await ctx.send(_("{} removed {} {} from {}'s account.").format(
|
||||
author.display_name, creds.sum, currency, to.display_name
|
||||
))
|
||||
else:
|
||||
await bank.set_balance(to, creds.sum)
|
||||
await ctx.send(_("{} set {}'s account to {} {}.").format(
|
||||
author.display_name, to.display_name, creds.sum, currency
|
||||
))
|
||||
|
||||
@_bank.command()
|
||||
@guild_only_check()
|
||||
@check_global_setting_guildowner()
|
||||
async def reset(self, ctx, confirmation: bool = False):
|
||||
"""Deletes all guild's bank accounts"""
|
||||
if confirmation is False:
|
||||
await ctx.send(
|
||||
_("This will delete all bank accounts for {}.\nIf you're sure, type "
|
||||
"{}bank reset yes").format(
|
||||
self.bot.user.name if await bank.is_global() else "this guild",
|
||||
ctx.prefix
|
||||
)
|
||||
)
|
||||
else:
|
||||
if await bank.is_global():
|
||||
# Bank being global means that the check would cause only
|
||||
# the owner and any co-owners to be able to run the command
|
||||
# so if we're in the function, it's safe to assume that the
|
||||
# author is authorized to use owner-only commands
|
||||
user = ctx.author
|
||||
else:
|
||||
user = ctx.guild.owner
|
||||
success = await bank.wipe_bank(user)
|
||||
if success:
|
||||
await ctx.send(_("All bank accounts of this guild have been "
|
||||
"deleted."))
|
||||
|
||||
@commands.command()
|
||||
@guild_only_check()
|
||||
async def payday(self, ctx: commands.Context):
|
||||
"""Get some free currency"""
|
||||
author = ctx.author
|
||||
guild = ctx.guild
|
||||
|
||||
cur_time = calendar.timegm(ctx.message.created_at.utctimetuple())
|
||||
credits_name = await bank.get_currency_name(ctx.guild)
|
||||
if await bank.is_global():
|
||||
next_payday = await self.config.user(author).next_payday()
|
||||
if cur_time >= next_payday:
|
||||
await bank.deposit_credits(author, await self.config.PAYDAY_CREDITS())
|
||||
next_payday = cur_time + await self.config.PAYDAY_TIME()
|
||||
await self.config.user(author).next_payday.set(next_payday)
|
||||
await ctx.send(
|
||||
_("{} Here, take some {}. Enjoy! (+{}"
|
||||
" {}!)").format(
|
||||
author.mention, credits_name,
|
||||
str(await self.config.PAYDAY_CREDITS()),
|
||||
credits_name
|
||||
)
|
||||
)
|
||||
else:
|
||||
dtime = self.display_time(next_payday - cur_time)
|
||||
await ctx.send(
|
||||
_("{} Too soon. For your next payday you have to"
|
||||
" wait {}.").format(author.mention, dtime)
|
||||
)
|
||||
else:
|
||||
next_payday = await self.config.member(author).next_payday()
|
||||
if cur_time >= next_payday:
|
||||
await bank.deposit_credits(author, await self.config.guild(guild).PAYDAY_CREDITS())
|
||||
next_payday = cur_time + await self.config.guild(guild).PAYDAY_TIME()
|
||||
await self.config.member(author).next_payday.set(next_payday)
|
||||
await ctx.send(
|
||||
_("{} Here, take some {}. Enjoy! (+{}"
|
||||
" {}!)").format(
|
||||
author.mention, credits_name,
|
||||
str(await self.config.guild(guild).PAYDAY_CREDITS()),
|
||||
credits_name))
|
||||
else:
|
||||
dtime = self.display_time(next_payday - cur_time)
|
||||
await ctx.send(
|
||||
_("{} Too soon. For your next payday you have to"
|
||||
" wait {}.").format(author.mention, dtime))
|
||||
|
||||
@commands.command()
|
||||
@guild_only_check()
|
||||
async def leaderboard(self, ctx: commands.Context, top: int = 10):
|
||||
"""Prints out the leaderboard
|
||||
|
||||
Defaults to top 10"""
|
||||
# Originally coded by Airenkun - edited by irdumb, rewritten by Palm__ for v3
|
||||
guild = ctx.guild
|
||||
if top < 1:
|
||||
top = 10
|
||||
if bank.is_global():
|
||||
bank_sorted = sorted(await bank.get_global_accounts(ctx.author),
|
||||
key=lambda x: x.balance, reverse=True)
|
||||
else:
|
||||
bank_sorted = sorted(await bank.get_guild_accounts(guild),
|
||||
key=lambda x: x.balance, reverse=True)
|
||||
if len(bank_sorted) < top:
|
||||
top = len(bank_sorted)
|
||||
topten = bank_sorted[:top]
|
||||
highscore = ""
|
||||
place = 1
|
||||
for acc in topten:
|
||||
dname = str(acc.name)
|
||||
if len(dname) >= 23 - len(str(acc.balance)):
|
||||
dname = dname[:(23 - len(str(acc.balance))) - 3]
|
||||
dname += "... "
|
||||
highscore += str(place).ljust(len(str(top)) + 1)
|
||||
highscore += dname.ljust(23 - len(str(acc.balance)))
|
||||
highscore += str(acc.balance) + "\n"
|
||||
place += 1
|
||||
if highscore != "":
|
||||
for page in pagify(highscore, shorten_by=12):
|
||||
await ctx.send(box(page, lang="py"))
|
||||
else:
|
||||
await ctx.send(_("There are no accounts in the bank."))
|
||||
|
||||
@commands.command()
|
||||
@guild_only_check()
|
||||
async def payouts(self, ctx: commands.Context):
|
||||
"""Shows slot machine payouts"""
|
||||
await ctx.author.send(SLOT_PAYOUTS_MSG)
|
||||
|
||||
@commands.command()
|
||||
@guild_only_check()
|
||||
async def slot(self, ctx: commands.Context, bid: int):
|
||||
"""Play the slot machine"""
|
||||
author = ctx.author
|
||||
guild = ctx.guild
|
||||
channel = ctx.channel
|
||||
if await bank.is_global():
|
||||
valid_bid = await self.config.SLOT_MIN() <= bid <= await self.config.SLOT_MAX()
|
||||
slot_time = await self.config.SLOT_TIME()
|
||||
last_slot = await self.config.user(author).last_slot()
|
||||
else:
|
||||
valid_bid = await self.config.guild(guild).SLOT_MIN() <= bid <= await self.config.guild(guild).SLOT_MAX()
|
||||
slot_time = await self.config.guild(guild).SLOT_TIME()
|
||||
last_slot = await self.config.member(author).last_slot()
|
||||
now = calendar.timegm(ctx.message.created_at.utctimetuple())
|
||||
|
||||
if (now - last_slot) < slot_time:
|
||||
await ctx.send(_("You're on cooldown, try again in a bit."))
|
||||
return
|
||||
if not valid_bid:
|
||||
await ctx.send(_("That's an invalid bid amount, sorry :/"))
|
||||
return
|
||||
if not await bank.can_spend(author, bid):
|
||||
await ctx.send(_("You ain't got enough money, friend."))
|
||||
return
|
||||
if await bank.is_global():
|
||||
await self.config.user(author).last_slot.set(now)
|
||||
else:
|
||||
await self.config.member(author).last_slot.set(now)
|
||||
await self.slot_machine(author, channel, bid)
|
||||
|
||||
async def slot_machine(self, author, channel, bid):
|
||||
default_reel = deque(SMReel)
|
||||
reels = []
|
||||
for i in range(3):
|
||||
default_reel.rotate(random.randint(-999, 999)) # weeeeee
|
||||
new_reel = deque(default_reel, maxlen=3) # we need only 3 symbols
|
||||
reels.append(new_reel) # for each reel
|
||||
rows = ((reels[0][0], reels[1][0], reels[2][0]),
|
||||
(reels[0][1], reels[1][1], reels[2][1]),
|
||||
(reels[0][2], reels[1][2], reels[2][2]))
|
||||
|
||||
slot = "~~\n~~" # Mobile friendly
|
||||
for i, row in enumerate(rows): # Let's build the slot to show
|
||||
sign = " "
|
||||
if i == 1:
|
||||
sign = ">"
|
||||
slot += "{}{} {} {}\n".format(sign, *[c.value for c in row])
|
||||
|
||||
payout = PAYOUTS.get(rows[1])
|
||||
if not payout:
|
||||
# Checks for two-consecutive-symbols special rewards
|
||||
payout = PAYOUTS.get((rows[1][0], rows[1][1]),
|
||||
PAYOUTS.get((rows[1][1], rows[1][2])))
|
||||
if not payout:
|
||||
# Still nothing. Let's check for 3 generic same symbols
|
||||
# or 2 consecutive symbols
|
||||
has_three = rows[1][0] == rows[1][1] == rows[1][2]
|
||||
has_two = (rows[1][0] == rows[1][1]) or (rows[1][1] == rows[1][2])
|
||||
if has_three:
|
||||
payout = PAYOUTS["3 symbols"]
|
||||
elif has_two:
|
||||
payout = PAYOUTS["2 symbols"]
|
||||
|
||||
if payout:
|
||||
then = await bank.get_balance(author)
|
||||
pay = payout["payout"](bid)
|
||||
now = then - bid + pay
|
||||
await bank.set_balance(author, now)
|
||||
await channel.send(_("{}\n{} {}\n\nYour bid: {}\n{} → {}!"
|
||||
"").format(slot, author.mention,
|
||||
payout["phrase"], bid, then, now))
|
||||
else:
|
||||
then = await bank.get_balance(author)
|
||||
await bank.withdraw_credits(author, bid)
|
||||
now = then - bid
|
||||
await channel.send(_("{}\n{} Nothing!\nYour bid: {}\n{} → {}!"
|
||||
"").format(slot, author.mention, bid, then, now))
|
||||
|
||||
@commands.group()
|
||||
@guild_only_check()
|
||||
@check_global_setting_admin()
|
||||
async def economyset(self, ctx: commands.Context):
|
||||
"""Changes economy module settings"""
|
||||
guild = ctx.guild
|
||||
if ctx.invoked_subcommand is None:
|
||||
await self.bot.send_cmd_help(ctx)
|
||||
if await bank.is_global():
|
||||
slot_min = await self.config.SLOT_MIN()
|
||||
slot_max = await self.config.SLOT_MAX()
|
||||
slot_time = await self.config.SLOT_TIME()
|
||||
payday_time = await self.config.PAYDAY_TIME()
|
||||
payday_amount = await self.config.PAYDAY_CREDITS()
|
||||
else:
|
||||
slot_min = await self.config.guild(guild).SLOT_MIN()
|
||||
slot_max = await self.config.guild(guild).SLOT_MAX()
|
||||
slot_time = await self.config.guild(guild).SLOT_TIME()
|
||||
payday_time = await self.config.guild(guild).PAYDAY_TIME()
|
||||
payday_amount = await self.config.guild(guild).PAYDAY_CREDITS()
|
||||
register_amount = await bank.get_default_balance(guild)
|
||||
msg = box(
|
||||
_("Minimum slot bid: {}\n"
|
||||
"Maximum slot bid: {}\n"
|
||||
"Slot cooldown: {}\n"
|
||||
"Payday amount: {}\n"
|
||||
"Payday cooldown: {}\n"
|
||||
"Amount given at account registration: {}"
|
||||
"").format(
|
||||
slot_min, slot_max, slot_time,
|
||||
payday_amount, payday_time, register_amount
|
||||
),
|
||||
_("Current Economy settings:")
|
||||
)
|
||||
await ctx.send(msg)
|
||||
|
||||
@economyset.command()
|
||||
async def slotmin(self, ctx: commands.Context, bid: int):
|
||||
"""Minimum slot machine bid"""
|
||||
if bid < 1:
|
||||
await ctx.send(_('Invalid min bid amount.'))
|
||||
return
|
||||
guild = ctx.guild
|
||||
if await bank.is_global():
|
||||
await self.config.SLOT_MIN.set(bid)
|
||||
else:
|
||||
await self.config.guild(guild).SLOT_MIN.set(bid)
|
||||
credits_name = await bank.get_currency_name(guild)
|
||||
await ctx.send(_("Minimum bid is now {} {}.").format(bid, credits_name))
|
||||
|
||||
@economyset.command()
|
||||
async def slotmax(self, ctx: commands.Context, bid: int):
|
||||
"""Maximum slot machine bid"""
|
||||
slot_min = await self.config.SLOT_MIN()
|
||||
if bid < 1 or bid < slot_min:
|
||||
await ctx.send(_('Invalid slotmax bid amount. Must be greater'
|
||||
' than slotmin.'))
|
||||
return
|
||||
guild = ctx.guild
|
||||
credits_name = await bank.get_currency_name(guild)
|
||||
if await bank.is_global():
|
||||
await self.config.SLOT_MAX.set(bid)
|
||||
else:
|
||||
await self.config.guild(guild).SLOT_MAX.set(bid)
|
||||
await ctx.send(_("Maximum bid is now {} {}.").format(bid, credits_name))
|
||||
|
||||
@economyset.command()
|
||||
async def slottime(self, ctx: commands.Context, seconds: int):
|
||||
"""Seconds between each slots use"""
|
||||
guild = ctx.guild
|
||||
if await bank.is_global():
|
||||
await self.config.SLOT_TIME.set(seconds)
|
||||
else:
|
||||
await self.config.guild(guild).SLOT_TIME.set(seconds)
|
||||
await ctx.send(_("Cooldown is now {} seconds.").format(seconds))
|
||||
|
||||
@economyset.command()
|
||||
async def paydaytime(self, ctx: commands.Context, seconds: int):
|
||||
"""Seconds between each payday"""
|
||||
guild = ctx.guild
|
||||
if await bank.is_global():
|
||||
await self.config.PAYDAY_TIME.set(seconds)
|
||||
else:
|
||||
await self.config.guild(guild).PAYDAY_TIME.set(seconds)
|
||||
await ctx.send(_("Value modified. At least {} seconds must pass "
|
||||
"between each payday.").format(seconds))
|
||||
|
||||
@economyset.command()
|
||||
async def paydayamount(self, ctx: commands.Context, creds: int):
|
||||
"""Amount earned each payday"""
|
||||
guild = ctx.guild
|
||||
credits_name = await bank.get_currency_name(guild)
|
||||
if creds <= 0:
|
||||
await ctx.send(_("Har har so funny."))
|
||||
return
|
||||
if await bank.is_global():
|
||||
await self.config.PAYDAY_CREDITS.set(creds)
|
||||
else:
|
||||
await self.config.guild(guild).PAYDAY_CREDITS.set(creds)
|
||||
await ctx.send(_("Every payday will now give {} {}."
|
||||
"").format(creds, credits_name))
|
||||
|
||||
@economyset.command()
|
||||
async def registeramount(self, ctx: commands.Context, creds: int):
|
||||
"""Amount given on registering an account"""
|
||||
guild = ctx.guild
|
||||
if creds < 0:
|
||||
creds = 0
|
||||
credits_name = await bank.get_currency_name(guild)
|
||||
await bank.set_default_balance(creds, guild)
|
||||
await ctx.send(_("Registering an account will now give {} {}."
|
||||
"").format(creds, credits_name))
|
||||
|
||||
# What would I ever do without stackoverflow?
|
||||
def display_time(self, seconds, granularity=2):
|
||||
intervals = ( # Source: http://stackoverflow.com/a/24542445
|
||||
(_('weeks'), 604800), # 60 * 60 * 24 * 7
|
||||
(_('days'), 86400), # 60 * 60 * 24
|
||||
(_('hours'), 3600), # 60 * 60
|
||||
(_('minutes'), 60),
|
||||
(_('seconds'), 1),
|
||||
)
|
||||
|
||||
result = []
|
||||
|
||||
for name, count in intervals:
|
||||
value = seconds // count
|
||||
if value:
|
||||
seconds -= value * count
|
||||
if value == 1:
|
||||
name = name.rstrip('s')
|
||||
result.append("{} {}".format(value, name))
|
||||
return ', '.join(result[:granularity])
|
||||
225
redbot/cogs/economy/locales/es.po
Normal file
225
redbot/cogs/economy/locales/es.po
Normal file
@@ -0,0 +1,225 @@
|
||||
# Copyright (C) 2017 Red-DiscordBot
|
||||
# UltimatePancake <pier.gaetani@gmail.com>, 2017.
|
||||
#
|
||||
msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: \n"
|
||||
"POT-Creation-Date: 2017-08-26 17:40+EDT\n"
|
||||
"PO-Revision-Date: 2017-08-26 20:48-0600\n"
|
||||
"Language-Team: \n"
|
||||
"MIME-Version: 1.0\n"
|
||||
"Content-Type: text/plain; charset=UTF-8\n"
|
||||
"Content-Transfer-Encoding: 8bit\n"
|
||||
"Generated-By: pygettext.py 1.5\n"
|
||||
"X-Generator: Poedit 2.0.3\n"
|
||||
"Last-Translator: UltimatePancake <pier.gaetani@gmail.com>\n"
|
||||
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
|
||||
"Language: es\n"
|
||||
|
||||
#: ../economy.py:38
|
||||
msgid "JACKPOT! 226! Your bid has been multiplied * 2500!"
|
||||
msgstr "¡PREMIO MAYOR! ¡226! ¡Apuesta multiplicada por 2500!"
|
||||
|
||||
#: ../economy.py:42
|
||||
msgid "4LC! +1000!"
|
||||
msgstr "¡4 tréboles! ¡+1000!"
|
||||
|
||||
#: ../economy.py:46
|
||||
msgid "Three cherries! +800!"
|
||||
msgstr "¡Triple cereza! ¡+800!"
|
||||
|
||||
#: ../economy.py:50
|
||||
msgid "2 6! Your bid has been multiplied * 4!"
|
||||
msgstr "¡2 6! ¡Apuesta multiplicada por 4!"
|
||||
|
||||
#: ../economy.py:54
|
||||
msgid "Two cherries! Your bid has been multiplied * 3!"
|
||||
msgstr "¡Doble cereza! ¡Apuesta multiplicada por 3!"
|
||||
|
||||
#: ../economy.py:58
|
||||
msgid "Three symbols! +500!"
|
||||
msgstr "¡Tres símbolos! ¡+500!"
|
||||
|
||||
#: ../economy.py:62
|
||||
msgid "Two consecutive symbols! Your bid has been multiplied * 2!"
|
||||
msgstr "¡Dos símbolos consecutivos! ¡Apuesta multiplicada por 2!"
|
||||
|
||||
#: ../economy.py:66
|
||||
msgid ""
|
||||
"Slot machine payouts:\n"
|
||||
"{two.value} {two.value} {six.value} Bet * 2500\n"
|
||||
"{flc.value} {flc.value} {flc.value} +1000\n"
|
||||
"{cherries.value} {cherries.value} {cherries.value} +800\n"
|
||||
"{two.value} {six.value} Bet * 4\n"
|
||||
"{cherries.value} {cherries.value} Bet * 3\n"
|
||||
"\n"
|
||||
"Three symbols: +500\n"
|
||||
"Two symbols: Bet * 2"
|
||||
msgstr ""
|
||||
"Premios de tragaperras:\n"
|
||||
"{two.value} {two.value} {six.value} Apuesta * 2500\n"
|
||||
"{flc.value} {flc.value} {flc.value} +1000\n"
|
||||
"{cherries.value} {cherries.value} {cherries.value} +800\n"
|
||||
"{two.value} {six.value} Apuesta * 4\n"
|
||||
"{cherries.value} {cherries.value} Apuesta * 3\n"
|
||||
"\n"
|
||||
"Tres símbolos: +500\n"
|
||||
"Dos símbolos: Apuesta * 2"
|
||||
|
||||
#: ../economy.py:155
|
||||
msgid "{}'s balance is {} {}"
|
||||
msgstr "El balance de {} es {} {}"
|
||||
|
||||
#: ../economy.py:169
|
||||
msgid "{} transferred {} {} to {}"
|
||||
msgstr "{} ha transferido {} {} a {}"
|
||||
|
||||
#: ../economy.py:189
|
||||
msgid "{} added {} {} to {}'s account."
|
||||
msgstr "{} agregó {} {} a la cuenta de {}."
|
||||
|
||||
#: ../economy.py:194
|
||||
msgid "{} removed {} {} from {}'s account."
|
||||
msgstr "{} quitó {} {} de la cuenta de {}."
|
||||
|
||||
#: ../economy.py:199
|
||||
msgid "{} set {}'s account to {} {}."
|
||||
msgstr "{} puso la cuenta de {} a {} {}."
|
||||
|
||||
#: ../economy.py:210
|
||||
msgid ""
|
||||
"This will delete all bank accounts for {}.\n"
|
||||
"If you're sure, type {}bank reset yes"
|
||||
msgstr ""
|
||||
"Esto eliminará todas las cuentas bancarias de {}.\n"
|
||||
"Si estás seguro, escribe {}bank reset yes"
|
||||
|
||||
#: ../economy.py:227
|
||||
msgid "All bank accounts of this guild have been deleted."
|
||||
msgstr "Todas las cuentas bancarias de este gremio han sido eliminadas."
|
||||
|
||||
#: ../economy.py:246 ../economy.py:266
|
||||
msgid "{} Here, take some {}. Enjoy! (+{} {}!)"
|
||||
msgstr "{}, toma, unos {}. Difruta! (+{} {}!)"
|
||||
|
||||
#: ../economy.py:256 ../economy.py:274
|
||||
msgid "{} Too soon. For your next payday you have to wait {}."
|
||||
msgstr "{} Muy pronto. Tu siguiente dia de pago es en {}."
|
||||
|
||||
#: ../economy.py:311
|
||||
msgid "There are no accounts in the bank."
|
||||
msgstr "No existen cuentas en el banco."
|
||||
|
||||
#: ../economy.py:337
|
||||
msgid "You're on cooldown, try again in a bit."
|
||||
msgstr "Estás en tiempo de espera, intenta de nuevo más tarde."
|
||||
|
||||
#: ../economy.py:340
|
||||
msgid "That's an invalid bid amount, sorry :/"
|
||||
msgstr "Cantidad de apuesta inválida, lo siento :/"
|
||||
|
||||
#: ../economy.py:343
|
||||
msgid "You ain't got enough money, friend."
|
||||
msgstr "No tienes suficiente dinero, amigo."
|
||||
|
||||
# dafuq is {} â {}!
|
||||
#: ../economy.py:389
|
||||
msgid ""
|
||||
"{}\n"
|
||||
"{} {}\n"
|
||||
"\n"
|
||||
"Your bid: {}\n"
|
||||
"{} → {}!"
|
||||
msgstr ""
|
||||
"{}\n"
|
||||
"{} {}\n"
|
||||
"Tu apuesta: {}\n"
|
||||
"{} → {}!"
|
||||
|
||||
#: ../economy.py:396
|
||||
msgid ""
|
||||
"{}\n"
|
||||
"{} Nothing!\n"
|
||||
"Your bid: {}\n"
|
||||
"{} → {}!"
|
||||
msgstr ""
|
||||
"{}\n"
|
||||
"{} Nada!\n"
|
||||
"Tu apuesta: {}\n"
|
||||
"{} → {}!"
|
||||
|
||||
#: ../economy.py:421
|
||||
msgid ""
|
||||
"Minimum slot bid: {}\n"
|
||||
"Maximum slot bid: {}\n"
|
||||
"Slot cooldown: {}\n"
|
||||
"Payday amount: {}\n"
|
||||
"Payday cooldown: {}\n"
|
||||
"Amount given at account registration: {}"
|
||||
msgstr ""
|
||||
"Apuesta mínima para tragaperras: {}\n"
|
||||
"Apuesta máxima para tragaperras: {}\n"
|
||||
"Tiempo de espera para tragaperras: {}\n"
|
||||
"Cantidad para día de pago: {}\n"
|
||||
"Tiempo de espera para día de pago: {}\n"
|
||||
"Cantidad al registrar: {}"
|
||||
|
||||
#: ../economy.py:431
|
||||
msgid "Current Economy settings:"
|
||||
msgstr "Configuración de economía actual:"
|
||||
|
||||
#: ../economy.py:439
|
||||
msgid "Invalid min bid amount."
|
||||
msgstr "Cantidad mínima de apuesta inválida."
|
||||
|
||||
#: ../economy.py:447
|
||||
msgid "Minimum bid is now {} {}."
|
||||
msgstr "Apuesta mínima es ahora {} {}."
|
||||
|
||||
#: ../economy.py:454
|
||||
msgid "Invalid slotmax bid amount. Must be greater than slotmin."
|
||||
msgstr "Cantidad de apuesta máxima para tragaperras inválido. Debe ser mayor al mínimo."
|
||||
|
||||
#: ../economy.py:463
|
||||
msgid "Maximum bid is now {} {}."
|
||||
msgstr "Apuesta máxima es ahora {} {}."
|
||||
|
||||
#: ../economy.py:473
|
||||
msgid "Cooldown is now {} seconds."
|
||||
msgstr "Tiempo de espera es ahora {} segundos."
|
||||
|
||||
#: ../economy.py:483
|
||||
msgid "Value modified. At least {} seconds must pass between each payday."
|
||||
msgstr "Valor modificado. Al menos {} segundos deben de pasar entre cada día de pago."
|
||||
|
||||
#: ../economy.py:492
|
||||
msgid "Har har so funny."
|
||||
msgstr "Muy gracioso..."
|
||||
|
||||
#: ../economy.py:498
|
||||
msgid "Every payday will now give {} {}."
|
||||
msgstr "Cada día de pago ahora dará {} {}."
|
||||
|
||||
#: ../economy.py:509
|
||||
msgid "Registering an account will now give {} {}."
|
||||
msgstr "Registrar una cuenta bancaria ahora dará {} {}."
|
||||
|
||||
#: ../economy.py:515
|
||||
msgid "weeks"
|
||||
msgstr "semanas"
|
||||
|
||||
#: ../economy.py:516
|
||||
msgid "days"
|
||||
msgstr "días"
|
||||
|
||||
#: ../economy.py:517
|
||||
msgid "hours"
|
||||
msgstr "horas"
|
||||
|
||||
#: ../economy.py:518
|
||||
msgid "minutes"
|
||||
msgstr "minutos"
|
||||
|
||||
#: ../economy.py:519
|
||||
msgid "seconds"
|
||||
msgstr "segundos"
|
||||
199
redbot/cogs/economy/locales/messages.pot
Normal file
199
redbot/cogs/economy/locales/messages.pot
Normal file
@@ -0,0 +1,199 @@
|
||||
# SOME DESCRIPTIVE TITLE.
|
||||
# Copyright (C) YEAR ORGANIZATION
|
||||
# FIRST AUTHOR <EMAIL@ADDRESS>, YEAR.
|
||||
#
|
||||
msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: PACKAGE VERSION\n"
|
||||
"POT-Creation-Date: 2017-08-26 17:40+EDT\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"
|
||||
|
||||
|
||||
#: ../economy.py:38
|
||||
msgid "JACKPOT! 226! Your bid has been multiplied * 2500!"
|
||||
msgstr ""
|
||||
|
||||
#: ../economy.py:42
|
||||
msgid "4LC! +1000!"
|
||||
msgstr ""
|
||||
|
||||
#: ../economy.py:46
|
||||
msgid "Three cherries! +800!"
|
||||
msgstr ""
|
||||
|
||||
#: ../economy.py:50
|
||||
msgid "2 6! Your bid has been multiplied * 4!"
|
||||
msgstr ""
|
||||
|
||||
#: ../economy.py:54
|
||||
msgid "Two cherries! Your bid has been multiplied * 3!"
|
||||
msgstr ""
|
||||
|
||||
#: ../economy.py:58
|
||||
msgid "Three symbols! +500!"
|
||||
msgstr ""
|
||||
|
||||
#: ../economy.py:62
|
||||
msgid "Two consecutive symbols! Your bid has been multiplied * 2!"
|
||||
msgstr ""
|
||||
|
||||
#: ../economy.py:66
|
||||
msgid ""
|
||||
"Slot machine payouts:\n"
|
||||
"{two.value} {two.value} {six.value} Bet * 2500\n"
|
||||
"{flc.value} {flc.value} {flc.value} +1000\n"
|
||||
"{cherries.value} {cherries.value} {cherries.value} +800\n"
|
||||
"{two.value} {six.value} Bet * 4\n"
|
||||
"{cherries.value} {cherries.value} Bet * 3\n"
|
||||
"\n"
|
||||
"Three symbols: +500\n"
|
||||
"Two symbols: Bet * 2"
|
||||
msgstr ""
|
||||
|
||||
#: ../economy.py:155
|
||||
msgid "{}'s balance is {} {}"
|
||||
msgstr ""
|
||||
|
||||
#: ../economy.py:169
|
||||
msgid "{} transferred {} {} to {}"
|
||||
msgstr ""
|
||||
|
||||
#: ../economy.py:189
|
||||
msgid "{} added {} {} to {}'s account."
|
||||
msgstr ""
|
||||
|
||||
#: ../economy.py:194
|
||||
msgid "{} removed {} {} from {}'s account."
|
||||
msgstr ""
|
||||
|
||||
#: ../economy.py:199
|
||||
msgid "{} set {}'s account to {} {}."
|
||||
msgstr ""
|
||||
|
||||
#: ../economy.py:210
|
||||
msgid ""
|
||||
"This will delete all bank accounts for {}.\n"
|
||||
"If you're sure, type {}bank reset yes"
|
||||
msgstr ""
|
||||
|
||||
#: ../economy.py:227
|
||||
msgid "All bank accounts of this guild have been deleted."
|
||||
msgstr ""
|
||||
|
||||
#: ../economy.py:246 ../economy.py:266
|
||||
msgid "{} Here, take some {}. Enjoy! (+{} {}!)"
|
||||
msgstr ""
|
||||
|
||||
#: ../economy.py:256 ../economy.py:274
|
||||
msgid "{} Too soon. For your next payday you have to wait {}."
|
||||
msgstr ""
|
||||
|
||||
#: ../economy.py:311
|
||||
msgid "There are no accounts in the bank."
|
||||
msgstr ""
|
||||
|
||||
#: ../economy.py:337
|
||||
msgid "You're on cooldown, try again in a bit."
|
||||
msgstr ""
|
||||
|
||||
#: ../economy.py:340
|
||||
msgid "That's an invalid bid amount, sorry :/"
|
||||
msgstr ""
|
||||
|
||||
#: ../economy.py:343
|
||||
msgid "You ain't got enough money, friend."
|
||||
msgstr ""
|
||||
|
||||
#: ../economy.py:389
|
||||
msgid ""
|
||||
"{}\n"
|
||||
"{} {}\n"
|
||||
"\n"
|
||||
"Your bid: {}\n"
|
||||
"{} → {}!"
|
||||
msgstr ""
|
||||
|
||||
#: ../economy.py:396
|
||||
msgid ""
|
||||
"{}\n"
|
||||
"{} Nothing!\n"
|
||||
"Your bid: {}\n"
|
||||
"{} → {}!"
|
||||
msgstr ""
|
||||
|
||||
#: ../economy.py:421
|
||||
msgid ""
|
||||
"Minimum slot bid: {}\n"
|
||||
"Maximum slot bid: {}\n"
|
||||
"Slot cooldown: {}\n"
|
||||
"Payday amount: {}\n"
|
||||
"Payday cooldown: {}\n"
|
||||
"Amount given at account registration: {}"
|
||||
msgstr ""
|
||||
|
||||
#: ../economy.py:431
|
||||
msgid "Current Economy settings:"
|
||||
msgstr ""
|
||||
|
||||
#: ../economy.py:439
|
||||
msgid "Invalid min bid amount."
|
||||
msgstr ""
|
||||
|
||||
#: ../economy.py:447
|
||||
msgid "Minimum bid is now {} {}."
|
||||
msgstr ""
|
||||
|
||||
#: ../economy.py:454
|
||||
msgid "Invalid slotmax bid amount. Must be greater than slotmin."
|
||||
msgstr ""
|
||||
|
||||
#: ../economy.py:463
|
||||
msgid "Maximum bid is now {} {}."
|
||||
msgstr ""
|
||||
|
||||
#: ../economy.py:473
|
||||
msgid "Cooldown is now {} seconds."
|
||||
msgstr ""
|
||||
|
||||
#: ../economy.py:483
|
||||
msgid "Value modified. At least {} seconds must pass between each payday."
|
||||
msgstr ""
|
||||
|
||||
#: ../economy.py:492
|
||||
msgid "Har har so funny."
|
||||
msgstr ""
|
||||
|
||||
#: ../economy.py:498
|
||||
msgid "Every payday will now give {} {}."
|
||||
msgstr ""
|
||||
|
||||
#: ../economy.py:509
|
||||
msgid "Registering an account will now give {} {}."
|
||||
msgstr ""
|
||||
|
||||
#: ../economy.py:515
|
||||
msgid "weeks"
|
||||
msgstr ""
|
||||
|
||||
#: ../economy.py:516
|
||||
msgid "days"
|
||||
msgstr ""
|
||||
|
||||
#: ../economy.py:517
|
||||
msgid "hours"
|
||||
msgstr ""
|
||||
|
||||
#: ../economy.py:518
|
||||
msgid "minutes"
|
||||
msgstr ""
|
||||
|
||||
#: ../economy.py:519
|
||||
msgid "seconds"
|
||||
msgstr ""
|
||||
|
||||
5
redbot/cogs/general/__init__.py
Normal file
5
redbot/cogs/general/__init__.py
Normal file
@@ -0,0 +1,5 @@
|
||||
from .general import General
|
||||
|
||||
|
||||
def setup(bot):
|
||||
bot.add_cog(General())
|
||||
340
redbot/cogs/general/general.py
Normal file
340
redbot/cogs/general/general.py
Normal file
@@ -0,0 +1,340 @@
|
||||
import datetime
|
||||
import time
|
||||
from enum import Enum
|
||||
from random import randint, choice
|
||||
from urllib.parse import quote_plus
|
||||
|
||||
import aiohttp
|
||||
import discord
|
||||
from redbot.core.i18n import CogI18n
|
||||
from discord.ext import commands
|
||||
|
||||
from redbot.core.utils.chat_formatting import escape, italics, pagify
|
||||
|
||||
_ = CogI18n("General", __file__)
|
||||
|
||||
|
||||
class RPS(Enum):
|
||||
rock = "\N{MOYAI}"
|
||||
paper = "\N{PAGE FACING UP}"
|
||||
scissors = "\N{BLACK SCISSORS}"
|
||||
|
||||
|
||||
class RPSParser:
|
||||
def __init__(self, argument):
|
||||
argument = argument.lower()
|
||||
if argument == "rock":
|
||||
self.choice = RPS.rock
|
||||
elif argument == "paper":
|
||||
self.choice = RPS.paper
|
||||
elif argument == "scissors":
|
||||
self.choice = RPS.scissors
|
||||
else:
|
||||
raise
|
||||
|
||||
|
||||
class General:
|
||||
"""General commands."""
|
||||
|
||||
def __init__(self):
|
||||
self.stopwatches = {}
|
||||
self.ball = [
|
||||
_("As I see it, yes"), _("It is certain"), _("It is decidedly so"),
|
||||
_("Most likely"), _("Outlook good"), _("Signs point to yes"),
|
||||
_("Without a doubt"), _("Yes"), _("Yes – definitely"), _("You may rely on it"),
|
||||
_("Reply hazy, try again"), _("Ask again later"),
|
||||
_("Better not tell you now"), _("Cannot predict now"),
|
||||
_("Concentrate and ask again"), _("Don't count on it"), _("My reply is no"),
|
||||
_("My sources say no"), _("Outlook not so good"), _("Very doubtful")
|
||||
]
|
||||
|
||||
@commands.command(hidden=True)
|
||||
async def ping(self, ctx):
|
||||
"""Pong."""
|
||||
await ctx.send("Pong.")
|
||||
|
||||
@commands.command()
|
||||
async def choose(self, ctx, *choices):
|
||||
"""Chooses between multiple choices.
|
||||
|
||||
To denote multiple choices, you should use double quotes.
|
||||
"""
|
||||
choices = [escape(c, mass_mentions=True) for c in choices]
|
||||
if len(choices) < 2:
|
||||
await ctx.send(_('Not enough choices to pick from.'))
|
||||
else:
|
||||
await ctx.send(choice(choices))
|
||||
|
||||
@commands.command()
|
||||
async def roll(self, ctx, number : int = 100):
|
||||
"""Rolls random number (between 1 and user choice)
|
||||
|
||||
Defaults to 100.
|
||||
"""
|
||||
author = ctx.author
|
||||
if number > 1:
|
||||
n = randint(1, number)
|
||||
await ctx.send(
|
||||
_("{} :game_die: {} :game_die:").format(author.mention, n)
|
||||
)
|
||||
else:
|
||||
await ctx.send(_("{} Maybe higher than 1? ;P").format(author.mention))
|
||||
|
||||
@commands.command()
|
||||
async def flip(self, ctx, user: discord.Member=None):
|
||||
"""Flips a coin... or a user.
|
||||
|
||||
Defaults to coin.
|
||||
"""
|
||||
if user != None:
|
||||
msg = ""
|
||||
if user.id == ctx.bot.user.id:
|
||||
user = ctx.author
|
||||
msg = _("Nice try. You think this is funny?"
|
||||
"How about *this* instead:\n\n")
|
||||
char = "abcdefghijklmnopqrstuvwxyz"
|
||||
tran = "ɐqɔpǝɟƃɥᴉɾʞlɯuodbɹsʇnʌʍxʎz"
|
||||
table = str.maketrans(char, tran)
|
||||
name = user.display_name.translate(table)
|
||||
char = char.upper()
|
||||
tran = "∀qƆpƎℲפHIſʞ˥WNOԀQᴚS┴∩ΛMX⅄Z"
|
||||
table = str.maketrans(char, tran)
|
||||
name = name.translate(table)
|
||||
await ctx.send(msg + "(╯°□°)╯︵ " + name[::-1])
|
||||
else:
|
||||
await ctx.send(
|
||||
_("*flips a coin and... ") + choice([_("HEADS!*"), _("TAILS!*")])
|
||||
)
|
||||
|
||||
@commands.command()
|
||||
async def rps(self, ctx, your_choice : RPSParser):
|
||||
"""Play rock paper scissors"""
|
||||
author = ctx.author
|
||||
player_choice = your_choice.choice
|
||||
red_choice = choice((RPS.rock, RPS.paper, RPS.scissors))
|
||||
cond = {
|
||||
(RPS.rock, RPS.paper) : False,
|
||||
(RPS.rock, RPS.scissors) : True,
|
||||
(RPS.paper, RPS.rock) : True,
|
||||
(RPS.paper, RPS.scissors) : False,
|
||||
(RPS.scissors, RPS.rock) : False,
|
||||
(RPS.scissors, RPS.paper) : True
|
||||
}
|
||||
|
||||
if red_choice == player_choice:
|
||||
outcome = None # Tie
|
||||
else:
|
||||
outcome = cond[(player_choice, red_choice)]
|
||||
|
||||
if outcome is True:
|
||||
await ctx.send(_("{} You win {}!").format(
|
||||
red_choice.value, author.mention
|
||||
))
|
||||
elif outcome is False:
|
||||
await ctx.send(_("{} You lose {}!").format(
|
||||
red_choice.value, author.mention
|
||||
))
|
||||
else:
|
||||
await ctx.send(_("{} We're square {}!").format(
|
||||
red_choice.value, author.mention
|
||||
))
|
||||
|
||||
@commands.command(name="8", aliases=["8ball"])
|
||||
async def _8ball(self, ctx, *, question : str):
|
||||
"""Ask 8 ball a question
|
||||
|
||||
Question must end with a question mark.
|
||||
"""
|
||||
if question.endswith("?") and question != "?":
|
||||
await ctx.send("`" + choice(self.ball) + "`")
|
||||
else:
|
||||
await ctx.send(_("That doesn't look like a question."))
|
||||
|
||||
@commands.command(aliases=["sw"])
|
||||
async def stopwatch(self, ctx):
|
||||
"""Starts/stops stopwatch"""
|
||||
author = ctx.author
|
||||
if not author.id in self.stopwatches:
|
||||
self.stopwatches[author.id] = int(time.perf_counter())
|
||||
await ctx.send(author.mention + _(" Stopwatch started!"))
|
||||
else:
|
||||
tmp = abs(self.stopwatches[author.id] - int(time.perf_counter()))
|
||||
tmp = str(datetime.timedelta(seconds=tmp))
|
||||
await ctx.send(author.mention + _(" Stopwatch stopped! Time: **") + tmp + "**")
|
||||
self.stopwatches.pop(author.id, None)
|
||||
|
||||
@commands.command()
|
||||
async def lmgtfy(self, ctx, *, search_terms : str):
|
||||
"""Creates a lmgtfy link"""
|
||||
search_terms = escape(search_terms.replace(" ", "+"), mass_mentions=True)
|
||||
await ctx.send("https://lmgtfy.com/?q={}".format(search_terms))
|
||||
|
||||
@commands.command(hidden=True)
|
||||
@commands.guild_only()
|
||||
async def hug(self, ctx, user : discord.Member, intensity : int=1):
|
||||
"""Because everyone likes hugs
|
||||
|
||||
Up to 10 intensity levels."""
|
||||
name = italics(user.display_name)
|
||||
if intensity <= 0:
|
||||
msg = "(っ˘̩╭╮˘̩)っ" + name
|
||||
elif intensity <= 3:
|
||||
msg = "(っ´▽`)っ" + name
|
||||
elif intensity <= 6:
|
||||
msg = "╰(*´︶`*)╯" + name
|
||||
elif intensity <= 9:
|
||||
msg = "(つ≧▽≦)つ" + name
|
||||
elif intensity >= 10:
|
||||
msg = "(づ ̄ ³ ̄)づ{} ⊂(´・ω・`⊂)".format(name)
|
||||
await ctx.send(msg)
|
||||
|
||||
@commands.command()
|
||||
@commands.guild_only()
|
||||
async def userinfo(self, ctx, *, user: discord.Member=None):
|
||||
"""Shows users's informations"""
|
||||
author = ctx.author
|
||||
guild = ctx.guild
|
||||
|
||||
if not user:
|
||||
user = author
|
||||
|
||||
# A special case for a special someone :^)
|
||||
special_date = datetime.datetime(2016, 1, 10, 6, 8, 4, 443000)
|
||||
is_special = (user.id == 96130341705637888 and
|
||||
guild.id == 133049272517001216)
|
||||
|
||||
roles = sorted(user.roles)[1:]
|
||||
|
||||
joined_at = user.joined_at if not is_special else special_date
|
||||
since_created = (ctx.message.created_at - user.created_at).days
|
||||
since_joined = (ctx.message.created_at - joined_at).days
|
||||
user_joined = joined_at.strftime("%d %b %Y %H:%M")
|
||||
user_created = user.created_at.strftime("%d %b %Y %H:%M")
|
||||
member_number = sorted(guild.members,
|
||||
key=lambda m: m.joined_at).index(user) + 1
|
||||
|
||||
created_on = _("{}\n({} days ago)").format(user_created, since_created)
|
||||
joined_on = _("{}\n({} days ago)").format(user_joined, since_joined)
|
||||
|
||||
game = _("Chilling in {} status").format(user.status)
|
||||
|
||||
if user.game and user.game.name and user.game.url:
|
||||
game = _("Streaming: [{}]({})").format(user.game, user.game.url)
|
||||
elif user.game and user.game.name:
|
||||
game = _("Playing {}").format(user.game)
|
||||
|
||||
if roles:
|
||||
roles = ", ".join([x.name for x in roles])
|
||||
else:
|
||||
roles = _("None")
|
||||
|
||||
data = discord.Embed(description=game, colour=user.colour)
|
||||
data.add_field(name=_("Joined Discord on"), value=created_on)
|
||||
data.add_field(name=_("Joined this guild on"), value=joined_on)
|
||||
data.add_field(name=_("Roles"), value=roles, inline=False)
|
||||
data.set_footer(text=_("Member #{} | User ID: {}"
|
||||
"").format(member_number, user.id))
|
||||
|
||||
name = str(user)
|
||||
name = " ~ ".join((name, user.nick)) if user.nick else name
|
||||
|
||||
if user.avatar:
|
||||
data.set_author(name=name, url=user.avatar_url)
|
||||
data.set_thumbnail(url=user.avatar_url)
|
||||
else:
|
||||
data.set_author(name=name)
|
||||
|
||||
try:
|
||||
await ctx.send(embed=data)
|
||||
except discord.HTTPException:
|
||||
await ctx.send(_("I need the `Embed links` permission "
|
||||
"to send this."))
|
||||
|
||||
@commands.command()
|
||||
@commands.guild_only()
|
||||
async def serverinfo(self, ctx):
|
||||
"""Shows guild's informations"""
|
||||
guild = ctx.guild
|
||||
online = len([m.status for m in guild.members
|
||||
if m.status == discord.Status.online or
|
||||
m.status == discord.Status.idle])
|
||||
total_users = len(guild.members)
|
||||
text_channels = len(guild.text_channels)
|
||||
voice_channels = len(guild.voice_channels)
|
||||
passed = (ctx.message.created_at - guild.created_at).days
|
||||
created_at = (_("Since {}. That's over {} days ago!"
|
||||
"").format(guild.created_at.strftime("%d %b %Y %H:%M"),
|
||||
passed))
|
||||
|
||||
colour = ''.join([choice('0123456789ABCDEF') for x in range(6)])
|
||||
colour = randint(0, 0xFFFFFF)
|
||||
|
||||
data = discord.Embed(
|
||||
description=created_at,
|
||||
colour=discord.Colour(value=colour))
|
||||
data.add_field(name=_("Region"), value=str(guild.region))
|
||||
data.add_field(name=_("Users"), value="{}/{}".format(online, total_users))
|
||||
data.add_field(name=_("Text Channels"), value=text_channels)
|
||||
data.add_field(name=_("Voice Channels"), value=voice_channels)
|
||||
data.add_field(name=_("Roles"), value=len(guild.roles))
|
||||
data.add_field(name=_("Owner"), value=str(guild.owner))
|
||||
data.set_footer(text=_("Guild ID: ") + str(guild.id))
|
||||
|
||||
if guild.icon_url:
|
||||
data.set_author(name=guild.name, url=guild.icon_url)
|
||||
data.set_thumbnail(url=guild.icon_url)
|
||||
else:
|
||||
data.set_author(name=guild.name)
|
||||
|
||||
try:
|
||||
await ctx.send(embed=data)
|
||||
except discord.HTTPException:
|
||||
await ctx.send(_("I need the `Embed links` permission "
|
||||
"to send this."))
|
||||
|
||||
@commands.command()
|
||||
async def urban(self, ctx, *, search_terms: str, definition_number: int=1):
|
||||
"""Urban Dictionary search
|
||||
|
||||
Definition number must be between 1 and 10"""
|
||||
def encode(s):
|
||||
return quote_plus(s, encoding='utf-8', errors='replace')
|
||||
|
||||
# definition_number is just there to show up in the help
|
||||
# all this mess is to avoid forcing double quotes on the user
|
||||
|
||||
search_terms = search_terms.split(" ")
|
||||
try:
|
||||
if len(search_terms) > 1:
|
||||
pos = int(search_terms[-1]) - 1
|
||||
search_terms = search_terms[:-1]
|
||||
else:
|
||||
pos = 0
|
||||
if pos not in range(0, 11): # API only provides the
|
||||
pos = 0 # top 10 definitions
|
||||
except ValueError:
|
||||
pos = 0
|
||||
|
||||
search_terms = {"term": "+".join([s for s in search_terms])}
|
||||
url = "http://api.urbandictionary.com/v0/define"
|
||||
try:
|
||||
async with aiohttp.ClientSession() as session:
|
||||
async with session.get(url, params=search_terms) as r:
|
||||
result = await r.json()
|
||||
item_list = result["list"]
|
||||
if item_list:
|
||||
definition = item_list[pos]['definition']
|
||||
example = item_list[pos]['example']
|
||||
defs = len(item_list)
|
||||
msg = ("**Definition #{} out of {}:\n**{}\n\n"
|
||||
"**Example:\n**{}".format(pos+1, defs, definition,
|
||||
example))
|
||||
msg = pagify(msg, ["\n"])
|
||||
for page in msg:
|
||||
await ctx.send(page)
|
||||
else:
|
||||
await ctx.send(_("Your search terms gave no results."))
|
||||
except IndexError:
|
||||
await ctx.send(_("There is no definition #{}").format(pos+1))
|
||||
except:
|
||||
await ctx.send(_("Error."))
|
||||
237
redbot/cogs/general/locales/es.po
Normal file
237
redbot/cogs/general/locales/es.po
Normal file
@@ -0,0 +1,237 @@
|
||||
# Copyright (C) 2017 Red-DiscordBot
|
||||
# UltimatePancake <pier.gaetani@gmail.com>, 2017.
|
||||
#
|
||||
msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: \n"
|
||||
"POT-Creation-Date: 2017-08-26 17:50+EDT\n"
|
||||
"PO-Revision-Date: 2017-08-26 20:26-0600\n"
|
||||
"Language-Team: \n"
|
||||
"MIME-Version: 1.0\n"
|
||||
"Content-Type: text/plain; charset=UTF-8\n"
|
||||
"Content-Transfer-Encoding: 8bit\n"
|
||||
"Generated-By: pygettext.py 1.5\n"
|
||||
"X-Generator: Poedit 2.0.3\n"
|
||||
"Last-Translator: UltimatePancake <pier.gaetani@gmail.com>\n"
|
||||
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
|
||||
"Language: es\n"
|
||||
|
||||
#: ../general.py:40
|
||||
msgid "As I see it, yes"
|
||||
msgstr "Como lo veo, si"
|
||||
|
||||
#: ../general.py:40
|
||||
msgid "It is certain"
|
||||
msgstr "Es cierto"
|
||||
|
||||
#: ../general.py:40
|
||||
msgid "It is decidedly so"
|
||||
msgstr "Decididamente"
|
||||
|
||||
#: ../general.py:41
|
||||
msgid "Most likely"
|
||||
msgstr "Probablemente"
|
||||
|
||||
#: ../general.py:41
|
||||
msgid "Outlook good"
|
||||
msgstr "El panorama se ve bien"
|
||||
|
||||
#: ../general.py:41
|
||||
msgid "Signs point to yes"
|
||||
msgstr "Todo apunta a sí"
|
||||
|
||||
#: ../general.py:42
|
||||
msgid "Without a doubt"
|
||||
msgstr "Sin duda alguna"
|
||||
|
||||
#: ../general.py:42
|
||||
msgid "Yes"
|
||||
msgstr "Sí"
|
||||
|
||||
#: ../general.py:42
|
||||
msgid "Yes – definitely"
|
||||
msgstr "Sí – definitivamente"
|
||||
|
||||
#: ../general.py:42
|
||||
msgid "You may rely on it"
|
||||
msgstr "Puedes contar con ello"
|
||||
|
||||
#: ../general.py:43
|
||||
msgid "Ask again later"
|
||||
msgstr "Pregunta más tarde"
|
||||
|
||||
#: ../general.py:43
|
||||
msgid "Reply hazy, try again"
|
||||
msgstr "Respuesta borrosa, intenta de nuevo"
|
||||
|
||||
#: ../general.py:44
|
||||
msgid "Better not tell you now"
|
||||
msgstr "Mejor no te digo en este momento"
|
||||
|
||||
#: ../general.py:44
|
||||
msgid "Cannot predict now"
|
||||
msgstr "No puedo predecir en este momento"
|
||||
|
||||
#: ../general.py:45
|
||||
msgid "Concentrate and ask again"
|
||||
msgstr "Concéntrate y pregunta de nuevo"
|
||||
|
||||
#: ../general.py:45
|
||||
msgid "Don't count on it"
|
||||
msgstr "No cuentes con ello"
|
||||
|
||||
#: ../general.py:45
|
||||
msgid "My reply is no"
|
||||
msgstr "Mi respuesta es no"
|
||||
|
||||
#: ../general.py:46
|
||||
msgid "My sources say no"
|
||||
msgstr "Mis fuentes dicen no"
|
||||
|
||||
#: ../general.py:46
|
||||
msgid "Outlook not so good"
|
||||
msgstr "El panorama no se ve bien"
|
||||
|
||||
#: ../general.py:46
|
||||
msgid "Very doubtful"
|
||||
msgstr "Lo dudo mucho"
|
||||
|
||||
#: ../general.py:62
|
||||
msgid "Not enough choices to pick from."
|
||||
msgstr "Insuficientes opciones para elegir"
|
||||
|
||||
#: ../general.py:76
|
||||
msgid "{} :game_die: {} :game_die:"
|
||||
msgstr "{} :game_die: {} :game_die:"
|
||||
|
||||
#: ../general.py:79
|
||||
msgid "{} ¿Maybe higher than 1? ;P"
|
||||
msgstr "{} ¿Tal vez más que 1? ;P"
|
||||
|
||||
#: ../general.py:91
|
||||
msgid ""
|
||||
"Nice try. You think this is funny?How about *this* instead:\n"
|
||||
"\n"
|
||||
msgstr ""
|
||||
"Buen intento. ¿Te parece gracioso? Qué tal *esto* mejor:\n"
|
||||
"\n"
|
||||
|
||||
#: ../general.py:104
|
||||
msgid "*flips a coin and... "
|
||||
msgstr "*tira una moneda y..."
|
||||
|
||||
#: ../general.py:104
|
||||
msgid "HEADS!*"
|
||||
msgstr "¡CARA!*"
|
||||
|
||||
#: ../general.py:104
|
||||
msgid "TAILS!*"
|
||||
msgstr "¡CRUZ!*"
|
||||
|
||||
#: ../general.py:128
|
||||
msgid "{} You win {}!"
|
||||
msgstr "{} ¡Ganas {}!"
|
||||
|
||||
#: ../general.py:132
|
||||
msgid "{} You lose {}!"
|
||||
msgstr "{} ¡Pierdes {}!"
|
||||
|
||||
#: ../general.py:136
|
||||
msgid "{} We're square {}!"
|
||||
msgstr "{} ¡Empates {}!"
|
||||
|
||||
#: ../general.py:149
|
||||
msgid "That doesn't look like a question."
|
||||
msgstr "Eso no parece pregunta."
|
||||
|
||||
#: ../general.py:157
|
||||
msgid " Stopwatch started!"
|
||||
msgstr " ¡Cronómetro comenzado!"
|
||||
|
||||
#: ../general.py:161
|
||||
msgid " Stopwatch stopped! Time: **"
|
||||
msgstr " ¡Cronómetro detenido! Tiempo: **"
|
||||
|
||||
#: ../general.py:214 ../general.py:215
|
||||
msgid ""
|
||||
"{}\n"
|
||||
"({} days ago)"
|
||||
msgstr ""
|
||||
"{}\n"
|
||||
"(Hace {} días)"
|
||||
|
||||
#: ../general.py:217
|
||||
msgid "Chilling in {} status"
|
||||
msgstr "Ahí no mas en estado {}"
|
||||
|
||||
#: ../general.py:220
|
||||
msgid "Streaming: [{}]({})"
|
||||
msgstr "Transmitiendo: [{}]({})"
|
||||
|
||||
#: ../general.py:222
|
||||
msgid "Playing {}"
|
||||
msgstr "Jugando {}"
|
||||
|
||||
#: ../general.py:227
|
||||
msgid "None"
|
||||
msgstr "Nada"
|
||||
|
||||
#: ../general.py:230
|
||||
msgid "Joined Discord on"
|
||||
msgstr "Registrado a Discord en"
|
||||
|
||||
#: ../general.py:231
|
||||
msgid "Joined this guild on"
|
||||
msgstr "Registrado a gremio en"
|
||||
|
||||
#: ../general.py:232 ../general.py:277
|
||||
msgid "Roles"
|
||||
msgstr "Roles"
|
||||
|
||||
#: ../general.py:233
|
||||
msgid "Member #{} | User ID: {}"
|
||||
msgstr "Miembro #{} | ID de usuario: {}"
|
||||
|
||||
#: ../general.py:248 ../general.py:290
|
||||
msgid "I need the `Embed links` permission to send this."
|
||||
msgstr "Necesito el permiso `Insertar Enlaces` para enviar esto."
|
||||
|
||||
#: ../general.py:263
|
||||
msgid "Since {}. That's over {} days ago!"
|
||||
msgstr "Desde {}. Hace {} días!"
|
||||
|
||||
#: ../general.py:273
|
||||
msgid "Region"
|
||||
msgstr "Región"
|
||||
|
||||
#: ../general.py:274
|
||||
msgid "Users"
|
||||
msgstr "Usuarios"
|
||||
|
||||
#: ../general.py:275
|
||||
msgid "Text Channels"
|
||||
msgstr "Canales de texto"
|
||||
|
||||
#: ../general.py:276
|
||||
msgid "Voice Channels"
|
||||
msgstr "Canalez de voz"
|
||||
|
||||
#: ../general.py:278
|
||||
msgid "Owner"
|
||||
msgstr "Dueño"
|
||||
|
||||
#: ../general.py:279
|
||||
msgid "Guild ID: "
|
||||
msgstr "ID de gremio:"
|
||||
|
||||
#: ../general.py:334
|
||||
msgid "Your search terms gave no results."
|
||||
msgstr "Tu búsqueda no ha dado resultados."
|
||||
|
||||
#: ../general.py:336
|
||||
msgid "There is no definition #{}"
|
||||
msgstr "No existe la definición #{}"
|
||||
|
||||
#: ../general.py:338
|
||||
msgid "Error."
|
||||
msgstr "Error."
|
||||
233
redbot/cogs/general/locales/messages.pot
Normal file
233
redbot/cogs/general/locales/messages.pot
Normal file
@@ -0,0 +1,233 @@
|
||||
# SOME DESCRIPTIVE TITLE.
|
||||
# Copyright (C) YEAR ORGANIZATION
|
||||
# FIRST AUTHOR <EMAIL@ADDRESS>, YEAR.
|
||||
#
|
||||
msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: PACKAGE VERSION\n"
|
||||
"POT-Creation-Date: 2017-08-26 17:50+EDT\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"
|
||||
|
||||
|
||||
#: ../general.py:40
|
||||
msgid "As I see it, yes"
|
||||
msgstr ""
|
||||
|
||||
#: ../general.py:40
|
||||
msgid "It is certain"
|
||||
msgstr ""
|
||||
|
||||
#: ../general.py:40
|
||||
msgid "It is decidedly so"
|
||||
msgstr ""
|
||||
|
||||
#: ../general.py:41
|
||||
msgid "Most likely"
|
||||
msgstr ""
|
||||
|
||||
#: ../general.py:41
|
||||
msgid "Outlook good"
|
||||
msgstr ""
|
||||
|
||||
#: ../general.py:41
|
||||
msgid "Signs point to yes"
|
||||
msgstr ""
|
||||
|
||||
#: ../general.py:42
|
||||
msgid "Without a doubt"
|
||||
msgstr ""
|
||||
|
||||
#: ../general.py:42
|
||||
msgid "Yes"
|
||||
msgstr ""
|
||||
|
||||
#: ../general.py:42
|
||||
msgid "Yes – definitely"
|
||||
msgstr ""
|
||||
|
||||
#: ../general.py:42
|
||||
msgid "You may rely on it"
|
||||
msgstr ""
|
||||
|
||||
#: ../general.py:43
|
||||
msgid "Ask again later"
|
||||
msgstr ""
|
||||
|
||||
#: ../general.py:43
|
||||
msgid "Reply hazy, try again"
|
||||
msgstr ""
|
||||
|
||||
#: ../general.py:44
|
||||
msgid "Better not tell you now"
|
||||
msgstr ""
|
||||
|
||||
#: ../general.py:44
|
||||
msgid "Cannot predict now"
|
||||
msgstr ""
|
||||
|
||||
#: ../general.py:45
|
||||
msgid "Concentrate and ask again"
|
||||
msgstr ""
|
||||
|
||||
#: ../general.py:45
|
||||
msgid "Don't count on it"
|
||||
msgstr ""
|
||||
|
||||
#: ../general.py:45
|
||||
msgid "My reply is no"
|
||||
msgstr ""
|
||||
|
||||
#: ../general.py:46
|
||||
msgid "My sources say no"
|
||||
msgstr ""
|
||||
|
||||
#: ../general.py:46
|
||||
msgid "Outlook not so good"
|
||||
msgstr ""
|
||||
|
||||
#: ../general.py:46
|
||||
msgid "Very doubtful"
|
||||
msgstr ""
|
||||
|
||||
#: ../general.py:62
|
||||
msgid "Not enough choices to pick from."
|
||||
msgstr ""
|
||||
|
||||
#: ../general.py:76
|
||||
msgid "{} :game_die: {} :game_die:"
|
||||
msgstr ""
|
||||
|
||||
#: ../general.py:79
|
||||
msgid "{} Maybe higher than 1? ;P"
|
||||
msgstr ""
|
||||
|
||||
#: ../general.py:91
|
||||
msgid ""
|
||||
"Nice try. You think this is funny?How about *this* instead:\n"
|
||||
"\n"
|
||||
msgstr ""
|
||||
|
||||
#: ../general.py:104
|
||||
msgid "*flips a coin and... "
|
||||
msgstr ""
|
||||
|
||||
#: ../general.py:104
|
||||
msgid "HEADS!*"
|
||||
msgstr ""
|
||||
|
||||
#: ../general.py:104
|
||||
msgid "TAILS!*"
|
||||
msgstr ""
|
||||
|
||||
#: ../general.py:128
|
||||
msgid "{} You win {}!"
|
||||
msgstr ""
|
||||
|
||||
#: ../general.py:132
|
||||
msgid "{} You lose {}!"
|
||||
msgstr ""
|
||||
|
||||
#: ../general.py:136
|
||||
msgid "{} We're square {}!"
|
||||
msgstr ""
|
||||
|
||||
#: ../general.py:149
|
||||
msgid "That doesn't look like a question."
|
||||
msgstr ""
|
||||
|
||||
#: ../general.py:157
|
||||
msgid " Stopwatch started!"
|
||||
msgstr ""
|
||||
|
||||
#: ../general.py:161
|
||||
msgid " Stopwatch stopped! Time: **"
|
||||
msgstr ""
|
||||
|
||||
#: ../general.py:214 ../general.py:215
|
||||
msgid ""
|
||||
"{}\n"
|
||||
"({} days ago)"
|
||||
msgstr ""
|
||||
|
||||
#: ../general.py:217
|
||||
msgid "Chilling in {} status"
|
||||
msgstr ""
|
||||
|
||||
#: ../general.py:220
|
||||
msgid "Streaming: [{}]({})"
|
||||
msgstr ""
|
||||
|
||||
#: ../general.py:222
|
||||
msgid "Playing {}"
|
||||
msgstr ""
|
||||
|
||||
#: ../general.py:227
|
||||
msgid "None"
|
||||
msgstr ""
|
||||
|
||||
#: ../general.py:230
|
||||
msgid "Joined Discord on"
|
||||
msgstr ""
|
||||
|
||||
#: ../general.py:231
|
||||
msgid "Joined this guild on"
|
||||
msgstr ""
|
||||
|
||||
#: ../general.py:232 ../general.py:277
|
||||
msgid "Roles"
|
||||
msgstr ""
|
||||
|
||||
#: ../general.py:233
|
||||
msgid "Member #{} | User ID: {}"
|
||||
msgstr ""
|
||||
|
||||
#: ../general.py:248 ../general.py:290
|
||||
msgid "I need the `Embed links` permission to send this."
|
||||
msgstr ""
|
||||
|
||||
#: ../general.py:263
|
||||
msgid "Since {}. That's over {} days ago!"
|
||||
msgstr ""
|
||||
|
||||
#: ../general.py:273
|
||||
msgid "Region"
|
||||
msgstr ""
|
||||
|
||||
#: ../general.py:274
|
||||
msgid "Users"
|
||||
msgstr ""
|
||||
|
||||
#: ../general.py:275
|
||||
msgid "Text Channels"
|
||||
msgstr ""
|
||||
|
||||
#: ../general.py:276
|
||||
msgid "Voice Channels"
|
||||
msgstr ""
|
||||
|
||||
#: ../general.py:278
|
||||
msgid "Owner"
|
||||
msgstr ""
|
||||
|
||||
#: ../general.py:279
|
||||
msgid "Guild ID: "
|
||||
msgstr ""
|
||||
|
||||
#: ../general.py:334
|
||||
msgid "Your search terms gave no results."
|
||||
msgstr ""
|
||||
|
||||
#: ../general.py:336
|
||||
msgid "There is no definition #{}"
|
||||
msgstr ""
|
||||
|
||||
#: ../general.py:338
|
||||
msgid "Error."
|
||||
msgstr ""
|
||||
|
||||
9
redbot/cogs/image/__init__.py
Normal file
9
redbot/cogs/image/__init__.py
Normal file
@@ -0,0 +1,9 @@
|
||||
from .image import Image
|
||||
import asyncio
|
||||
|
||||
|
||||
def setup(bot):
|
||||
n = Image(bot)
|
||||
loop = asyncio.get_event_loop()
|
||||
loop.create_task(n.set_giphy_key())
|
||||
bot.add_cog(n)
|
||||
159
redbot/cogs/image/image.py
Normal file
159
redbot/cogs/image/image.py
Normal file
@@ -0,0 +1,159 @@
|
||||
from random import shuffle
|
||||
|
||||
import aiohttp
|
||||
from discord.ext import commands
|
||||
|
||||
from redbot.core.i18n import CogI18n
|
||||
from redbot.core import checks, Config
|
||||
|
||||
_ = CogI18n("Image", __file__)
|
||||
|
||||
GIPHY_API_KEY = "dc6zaTOxFJmzC"
|
||||
|
||||
|
||||
class Image:
|
||||
"""Image related commands."""
|
||||
default_global = {
|
||||
"imgur_client_id": None
|
||||
}
|
||||
|
||||
def __init__(self, bot):
|
||||
self.bot = bot
|
||||
self.settings = Config.get_conf(self, identifier=2652104208, force_registration=True)
|
||||
self.settings.register_global(**self.default_global)
|
||||
self.session = aiohttp.ClientSession()
|
||||
self.imgur_base_url = "https://api.imgur.com/3/"
|
||||
|
||||
@commands.group(name="imgur")
|
||||
@commands.guild_only()
|
||||
async def _imgur(self, ctx):
|
||||
"""Retrieves pictures from imgur
|
||||
|
||||
Make sure to set the client ID using
|
||||
[p]imgurcreds"""
|
||||
if ctx.invoked_subcommand is None:
|
||||
await self.bot.send_cmd_help(ctx)
|
||||
|
||||
@_imgur.command(name="search")
|
||||
async def imgur_search(self, ctx, *, term: str):
|
||||
"""Searches Imgur for the specified term and returns up to 3 results"""
|
||||
url = self.imgur_base_url + "time/all/0"
|
||||
params = {"q": term}
|
||||
headers = {"Authorization": "Client-ID {}".format(await self.settings.imgur_client_id())}
|
||||
async with self.session.get(url, headers=headers, data=params) as search_get:
|
||||
data = await search_get.json()
|
||||
|
||||
if data["success"]:
|
||||
results = data["data"]
|
||||
if not results:
|
||||
await ctx.send(_("Your search returned no results"))
|
||||
return
|
||||
shuffle(results)
|
||||
msg = _("Search results...\n")
|
||||
for r in results[:3]:
|
||||
msg += r["gifv"] if "gifv" in r else r["link"]
|
||||
msg += "\n"
|
||||
await ctx.send(msg)
|
||||
else:
|
||||
await ctx.send(_("Something went wrong. Error code is {}").format(data["status"]))
|
||||
|
||||
@_imgur.command(name="subreddit")
|
||||
async def imgur_subreddit(self, ctx, subreddit: str, sort_type: str="top", window: str="day"):
|
||||
"""Gets images from the specified subreddit section
|
||||
|
||||
Sort types: new, top
|
||||
Time windows: day, week, month, year, all"""
|
||||
sort_type = sort_type.lower()
|
||||
window = window.lower()
|
||||
|
||||
if sort_type not in ("new", "top"):
|
||||
await ctx.send(_("Only 'new' and 'top' are a valid sort type."))
|
||||
return
|
||||
elif window not in ("day", "week", "month", "year", "all"):
|
||||
await self.bot.send_cmd_help(ctx)
|
||||
return
|
||||
|
||||
if sort_type == "new":
|
||||
sort = "time"
|
||||
elif sort_type == "top":
|
||||
sort = "top"
|
||||
|
||||
links = []
|
||||
headers = {"Authorization": "Client-ID {}".format(await self.settings.imgur_client_id())}
|
||||
url = self.imgur_base_url + "r/{}/{}/{}/0".format(subreddit, sort, window)
|
||||
|
||||
async with self.session.get(url, headers=headers) as sub_get:
|
||||
data = await sub_get.json()
|
||||
|
||||
if data["success"]:
|
||||
items = data["data"]
|
||||
if items:
|
||||
for item in items[:3]:
|
||||
link = item["gifv"] if "gifv" in item else item["link"]
|
||||
links.append("{}\n{}".format(item["title"], link))
|
||||
|
||||
if links:
|
||||
await ctx.send("\n".join(links))
|
||||
else:
|
||||
await ctx.send(_("No results found."))
|
||||
else:
|
||||
await ctx.send(_("Something went wrong. Error code is {}").format(data["status"]))
|
||||
|
||||
@checks.is_owner()
|
||||
@commands.command()
|
||||
async def imgurcreds(self, ctx, imgur_client_id: str):
|
||||
"""Sets the imgur client id
|
||||
You will need an account on Imgur to get this
|
||||
|
||||
You can get these by visiting https://api.imgur.com/oauth2/addclient
|
||||
and filling out the form. Enter a name for the application, select
|
||||
'Anonymous usage without user authorization' for the auth type,
|
||||
leave the app website blank, enter a valid email address, and
|
||||
enter a description. Check the box for the captcha, then click Next.
|
||||
Your client ID will be on the page that loads"""
|
||||
await self.settings.imgur_client_id.set(imgur_client_id)
|
||||
await ctx.send(_("Set the imgur client id!"))
|
||||
|
||||
@commands.command(pass_context=True, no_pm=True)
|
||||
async def gif(self, ctx, *keywords):
|
||||
"""Retrieves first search result from giphy"""
|
||||
if keywords:
|
||||
keywords = "+".join(keywords)
|
||||
else:
|
||||
await self.bot.send_cmd_help(ctx)
|
||||
return
|
||||
|
||||
url = ("http://api.giphy.com/v1/gifs/search?&api_key={}&q={}"
|
||||
"".format(GIPHY_API_KEY, keywords))
|
||||
|
||||
async with self.session.get(url) as r:
|
||||
result = await r.json()
|
||||
if r.status == 200:
|
||||
if result["data"]:
|
||||
await ctx.send(result["data"][0]["url"])
|
||||
else:
|
||||
await ctx.send(_("No results found."))
|
||||
else:
|
||||
await ctx.send(_("Error contacting the API"))
|
||||
|
||||
@commands.command(pass_context=True, no_pm=True)
|
||||
async def gifr(self, ctx, *keywords):
|
||||
"""Retrieves a random gif from a giphy search"""
|
||||
if keywords:
|
||||
keywords = "+".join(keywords)
|
||||
else:
|
||||
await self.bot.send_cmd_help(ctx)
|
||||
return
|
||||
|
||||
url = ("http://api.giphy.com/v1/gifs/random?&api_key={}&tag={}"
|
||||
"".format(GIPHY_API_KEY, keywords))
|
||||
|
||||
async with self.session.get(url) as r:
|
||||
result = await r.json()
|
||||
if r.status == 200:
|
||||
if result["data"]:
|
||||
await ctx.send(result["data"]["url"])
|
||||
else:
|
||||
await ctx.send(_("No results found."))
|
||||
else:
|
||||
await ctx.send(_("Error contacting the API"))
|
||||
45
redbot/cogs/image/locales/es.po
Normal file
45
redbot/cogs/image/locales/es.po
Normal file
@@ -0,0 +1,45 @@
|
||||
# Copyright (C) 2017 Red-DiscordBot
|
||||
# UltimatePancake <pier.gaetani@gmail.com>, 2017.
|
||||
#
|
||||
msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: \n"
|
||||
"POT-Creation-Date: 2017-08-26 17:57+EDT\n"
|
||||
"PO-Revision-Date: 2017-08-26 20:39-0600\n"
|
||||
"Language-Team: \n"
|
||||
"MIME-Version: 1.0\n"
|
||||
"Content-Type: text/plain; charset=UTF-8\n"
|
||||
"Content-Transfer-Encoding: 8bit\n"
|
||||
"Generated-By: pygettext.py 1.5\n"
|
||||
"X-Generator: Poedit 2.0.3\n"
|
||||
"Last-Translator: UltimatePancake <pier.gaetani@gmail.com>\n"
|
||||
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
|
||||
"Language: es\n"
|
||||
|
||||
#: ../image.py:48
|
||||
msgid "Your search returned no results"
|
||||
msgstr "Tu búsqueda no a dado resultados"
|
||||
|
||||
#: ../image.py:51
|
||||
msgid "Search results...\n"
|
||||
msgstr "Resultados de la búsqueda\n"
|
||||
|
||||
#: ../image.py:57 ../image.py:99
|
||||
msgid "Something went wrong. Error code is {}"
|
||||
msgstr "Algo malo ha ocurrido. Código de error: {}"
|
||||
|
||||
#: ../image.py:69
|
||||
msgid "Only 'new' and 'top' are a valid sort type."
|
||||
msgstr "Únicamente 'new' y 'top' son tipos de ordenamiento válidos."
|
||||
|
||||
#: ../image.py:97 ../image.py:134 ../image.py:156
|
||||
msgid "No results found."
|
||||
msgstr "No se han encontrado resultados."
|
||||
|
||||
#: ../image.py:114
|
||||
msgid "Set the imgur client id!"
|
||||
msgstr "Configurar el id de cliente de imgur!"
|
||||
|
||||
#: ../image.py:136 ../image.py:158
|
||||
msgid "Error contacting the API"
|
||||
msgstr "Error contactando al API"
|
||||
46
redbot/cogs/image/locales/messages.pot
Normal file
46
redbot/cogs/image/locales/messages.pot
Normal file
@@ -0,0 +1,46 @@
|
||||
# SOME DESCRIPTIVE TITLE.
|
||||
# Copyright (C) YEAR ORGANIZATION
|
||||
# FIRST AUTHOR <EMAIL@ADDRESS>, YEAR.
|
||||
#
|
||||
msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: PACKAGE VERSION\n"
|
||||
"POT-Creation-Date: 2017-08-26 17:57+EDT\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"
|
||||
|
||||
|
||||
#: ../image.py:48
|
||||
msgid "Your search returned no results"
|
||||
msgstr ""
|
||||
|
||||
#: ../image.py:51
|
||||
msgid ""
|
||||
"Search results...\n"
|
||||
msgstr ""
|
||||
|
||||
#: ../image.py:57 ../image.py:99
|
||||
msgid "Something went wrong. Error code is {}"
|
||||
msgstr ""
|
||||
|
||||
#: ../image.py:69
|
||||
msgid "Only 'new' and 'top' are a valid sort type."
|
||||
msgstr ""
|
||||
|
||||
#: ../image.py:97 ../image.py:134 ../image.py:156
|
||||
msgid "No results found."
|
||||
msgstr ""
|
||||
|
||||
#: ../image.py:114
|
||||
msgid "Set the imgur client id!"
|
||||
msgstr ""
|
||||
|
||||
#: ../image.py:136 ../image.py:158
|
||||
msgid "Error contacting the API"
|
||||
msgstr ""
|
||||
|
||||
7
redbot/core/__init__.py
Normal file
7
redbot/core/__init__.py
Normal file
@@ -0,0 +1,7 @@
|
||||
import pkg_resources
|
||||
|
||||
from .config import Config
|
||||
|
||||
__all__ = ["Config", "__version__"]
|
||||
|
||||
__version__ = version = pkg_resources.require("Red-DiscordBot")[0].version
|
||||
554
redbot/core/bank.py
Normal file
554
redbot/core/bank.py
Normal file
@@ -0,0 +1,554 @@
|
||||
import datetime
|
||||
import os
|
||||
from typing import Union, List
|
||||
|
||||
import discord
|
||||
|
||||
from redbot.core import Config
|
||||
|
||||
__all__ = ["Account", "get_balance", "set_balance", "withdraw_credits", "deposit_credits",
|
||||
"can_spend", "transfer_credits", "wipe_bank", "get_guild_accounts",
|
||||
"get_global_accounts", "get_account", "is_global", "set_global",
|
||||
"get_bank_name", "set_bank_name", "get_currency_name", "set_currency_name",
|
||||
"get_default_balance", "set_default_balance"]
|
||||
|
||||
_DEFAULT_GLOBAL = {
|
||||
"is_global": False,
|
||||
"bank_name": "Twentysix bank",
|
||||
"currency": "credits",
|
||||
"default_balance": 100
|
||||
}
|
||||
|
||||
_DEFAULT_GUILD = {
|
||||
"bank_name": "Twentysix bank",
|
||||
"currency": "credits",
|
||||
"default_balance": 100
|
||||
}
|
||||
|
||||
_DEFAULT_MEMBER = {
|
||||
"name": "",
|
||||
"balance": 0,
|
||||
"created_at": 0
|
||||
}
|
||||
|
||||
_DEFAULT_USER = _DEFAULT_MEMBER
|
||||
|
||||
_bank_type = type("Bank", (object,), {})
|
||||
|
||||
|
||||
class Account:
|
||||
"""A single account. This class should ONLY be instantiated by the bank itself."""
|
||||
|
||||
def __init__(self, name: str, balance: int, created_at: datetime.datetime):
|
||||
self.name = name
|
||||
self.balance = balance
|
||||
self.created_at = created_at
|
||||
|
||||
|
||||
def _register_defaults():
|
||||
_conf.register_global(**_DEFAULT_GLOBAL)
|
||||
_conf.register_guild(**_DEFAULT_GUILD)
|
||||
_conf.register_member(**_DEFAULT_MEMBER)
|
||||
_conf.register_user(**_DEFAULT_USER)
|
||||
|
||||
if not os.environ.get('BUILDING_DOCS'):
|
||||
_conf = Config.get_conf(_bank_type(), 384734293238749, force_registration=True)
|
||||
_register_defaults()
|
||||
|
||||
|
||||
def _encoded_current_time() -> int:
|
||||
"""
|
||||
Encoded current timestamp in UTC.
|
||||
:return:
|
||||
"""
|
||||
now = datetime.datetime.utcnow()
|
||||
return _encode_time(now)
|
||||
|
||||
|
||||
def _encode_time(time: datetime.datetime) -> int:
|
||||
"""
|
||||
Goes from datetime object to serializable int.
|
||||
:param time:
|
||||
:return:
|
||||
"""
|
||||
ret = int(time.timestamp())
|
||||
return ret
|
||||
|
||||
|
||||
def _decode_time(time: int) -> datetime.datetime:
|
||||
"""
|
||||
Returns decoded timestamp in UTC.
|
||||
:param time:
|
||||
:return:
|
||||
"""
|
||||
return datetime.datetime.utcfromtimestamp(time)
|
||||
|
||||
|
||||
async def get_balance(member: discord.Member) -> int:
|
||||
"""
|
||||
Gets the current balance of a member.
|
||||
|
||||
:param discord.Member member:
|
||||
The member whose balance to check.
|
||||
:return:
|
||||
The member's balance
|
||||
:rtype:
|
||||
int
|
||||
"""
|
||||
acc = await get_account(member)
|
||||
return acc.balance
|
||||
|
||||
|
||||
async def can_spend(member: discord.Member, amount: int) -> bool:
|
||||
"""
|
||||
Determines if a member can spend the given amount.
|
||||
|
||||
:param discord.Member member:
|
||||
The member wanting to spend.
|
||||
:param int amount:
|
||||
The amount the member wants to spend.
|
||||
:return:
|
||||
:code:`True` if the member has a sufficient balance to spend the amount, else :code:`False`.
|
||||
:rtype:
|
||||
bool
|
||||
"""
|
||||
if _invalid_amount(amount):
|
||||
return False
|
||||
return await get_balance(member) > amount
|
||||
|
||||
|
||||
async def set_balance(member: discord.Member, amount: int) -> int:
|
||||
"""
|
||||
Sets an account balance.
|
||||
|
||||
May raise ValueError if amount is invalid.
|
||||
|
||||
:param discord.Member member:
|
||||
The member whose balance to set.
|
||||
:param int amount:
|
||||
The amount to set the balance to.
|
||||
:return:
|
||||
New account balance.
|
||||
:rtype:
|
||||
int
|
||||
:raises ValueError:
|
||||
If attempting to set the balance to a negative number
|
||||
"""
|
||||
if amount < 0:
|
||||
raise ValueError("Not allowed to have negative balance.")
|
||||
if await is_global():
|
||||
group = _conf.user(member)
|
||||
else:
|
||||
group = _conf.member(member)
|
||||
await group.balance.set(amount)
|
||||
|
||||
if await group.created_at() == 0:
|
||||
time = _encoded_current_time()
|
||||
await group.created_at.set(time)
|
||||
|
||||
if await group.name() == "":
|
||||
await group.name.set(member.display_name)
|
||||
|
||||
return amount
|
||||
|
||||
|
||||
def _invalid_amount(amount: int) -> bool:
|
||||
return amount <= 0
|
||||
|
||||
|
||||
async def withdraw_credits(member: discord.Member, amount: int) -> int:
|
||||
"""
|
||||
Removes a certain amount of credits from an account.
|
||||
|
||||
May raise ValueError if the amount is invalid or if the account has
|
||||
insufficient funds.
|
||||
|
||||
:param discord.Member member:
|
||||
The member to withdraw credits from.
|
||||
:param int amount:
|
||||
The amount to withdraw.
|
||||
:return:
|
||||
New account balance.
|
||||
:rtype:
|
||||
int
|
||||
:raises ValueError:
|
||||
if the withdrawal amount is invalid or if the account has insufficient funds
|
||||
"""
|
||||
if _invalid_amount(amount):
|
||||
raise ValueError("Invalid withdrawal amount {} <= 0".format(amount))
|
||||
|
||||
bal = await get_balance(member)
|
||||
if amount > bal:
|
||||
raise ValueError("Insufficient funds {} > {}".format(amount, bal))
|
||||
|
||||
return await set_balance(member, bal - amount)
|
||||
|
||||
|
||||
async def deposit_credits(member: discord.Member, amount: int) -> int:
|
||||
"""
|
||||
Adds a given amount of credits to an account.
|
||||
|
||||
May raise ValueError if the amount is invalid.
|
||||
|
||||
:param discord.Member member:
|
||||
The member to deposit credits to.
|
||||
:param int amount:
|
||||
The amount to deposit.
|
||||
:return:
|
||||
The new balance
|
||||
:rtype:
|
||||
int
|
||||
:raises ValueError:
|
||||
If the deposit amount is invalid.
|
||||
"""
|
||||
if _invalid_amount(amount):
|
||||
raise ValueError("Invalid withdrawal amount {} <= 0".format(amount))
|
||||
|
||||
bal = await get_balance(member)
|
||||
return await set_balance(member, amount + bal)
|
||||
|
||||
|
||||
async def transfer_credits(from_: discord.Member, to: discord.Member, amount: int):
|
||||
"""
|
||||
Transfers a given amount of credits from one account to another.
|
||||
|
||||
May raise ValueError if the amount is invalid or if the :code:`from_`
|
||||
account has insufficient funds.
|
||||
|
||||
:param discord.Member from_:
|
||||
The member to transfer from.
|
||||
:param discord.Member to:
|
||||
The member to transfer to.
|
||||
:param int amount:
|
||||
The amount to transfer.
|
||||
:return:
|
||||
The new balance.
|
||||
:rtype:
|
||||
int
|
||||
:raises ValueError:
|
||||
If the amount is invalid or if :code:`from_` has insufficient funds.
|
||||
"""
|
||||
if _invalid_amount(amount):
|
||||
raise ValueError("Invalid transfer amount {} <= 0".format(amount))
|
||||
|
||||
await withdraw_credits(from_, amount)
|
||||
return await deposit_credits(to, amount)
|
||||
|
||||
|
||||
async def wipe_bank(user: Union[discord.User, discord.Member]):
|
||||
"""
|
||||
Deletes all accounts from the bank.
|
||||
|
||||
.. important::
|
||||
|
||||
A member is required if the bank is currently guild specific.
|
||||
|
||||
:param user:
|
||||
A user to be used in clearing the bank, this is required for technical
|
||||
reasons and it does not matter which user/member is used.
|
||||
:type user:
|
||||
discord.User or discord.Member
|
||||
"""
|
||||
if await is_global():
|
||||
await _conf.user(user).clear()
|
||||
else:
|
||||
await _conf.member(user).clear()
|
||||
|
||||
|
||||
async def get_guild_accounts(guild: discord.Guild) -> List[Account]:
|
||||
"""
|
||||
Gets all account data for the given guild.
|
||||
|
||||
May raise RuntimeError if the bank is currently global.
|
||||
|
||||
:param discord.Guild guild:
|
||||
The guild to get accounts for.
|
||||
:return:
|
||||
A generator for all guild accounts.
|
||||
:rtype:
|
||||
generator
|
||||
:raises RuntimeError:
|
||||
If the bank is global.
|
||||
"""
|
||||
if is_global():
|
||||
raise RuntimeError("The bank is currently global.")
|
||||
|
||||
ret = []
|
||||
accs = await _conf.member(guild.owner).all_from_kind()
|
||||
for user_id, acc in accs.items():
|
||||
acc_data = acc.copy() # There ya go kowlin
|
||||
acc_data['created_at'] = _decode_time(acc_data['created_at'])
|
||||
ret.append(Account(**acc_data))
|
||||
return ret
|
||||
|
||||
|
||||
async def get_global_accounts(user: discord.User) -> List[Account]:
|
||||
"""
|
||||
Gets all global account data.
|
||||
|
||||
May raise RuntimeError if the bank is currently guild specific.
|
||||
|
||||
:param discord.User user:
|
||||
A user to be used for getting accounts.
|
||||
:return:
|
||||
A generator of all global accounts.
|
||||
:rtype:
|
||||
generator
|
||||
:raises RuntimeError:
|
||||
If the bank is guild specific.
|
||||
"""
|
||||
if not is_global():
|
||||
raise RuntimeError("The bank is not currently global.")
|
||||
|
||||
ret = []
|
||||
accs = await _conf.user(user).all_from_kind() # this is a dict of user -> acc
|
||||
for user_id, acc in accs.items():
|
||||
acc_data = acc.copy()
|
||||
acc_data['created_at'] = _decode_time(acc_data['created_at'])
|
||||
ret.append(Account(**acc_data))
|
||||
|
||||
return ret
|
||||
|
||||
|
||||
async def get_account(member: Union[discord.Member, discord.User]) -> Account:
|
||||
"""
|
||||
Gets the appropriate account for the given user or member. A member is
|
||||
required if the bank is currently guild specific.
|
||||
|
||||
:param member:
|
||||
The user whose account to get.
|
||||
:type member:
|
||||
discord.User or discord.Member
|
||||
:return:
|
||||
The user's account.
|
||||
:rtype:
|
||||
:py:class:`Account`
|
||||
"""
|
||||
if await is_global():
|
||||
acc_data = (await _conf.user(member)()).copy()
|
||||
default = _DEFAULT_USER.copy()
|
||||
else:
|
||||
acc_data = (await _conf.member(member)()).copy()
|
||||
default = _DEFAULT_MEMBER.copy()
|
||||
|
||||
if acc_data == {}:
|
||||
acc_data = default
|
||||
acc_data['name'] = member.display_name
|
||||
try:
|
||||
acc_data['balance'] = await get_default_balance(member.guild)
|
||||
except AttributeError:
|
||||
acc_data['balance'] = await get_default_balance()
|
||||
|
||||
acc_data['created_at'] = _decode_time(acc_data['created_at'])
|
||||
return Account(**acc_data)
|
||||
|
||||
|
||||
async def is_global() -> bool:
|
||||
"""
|
||||
Determines if the bank is currently global.
|
||||
|
||||
:return:
|
||||
:code:`True` if the bank is global, otherwise :code:`False`.
|
||||
:rtype:
|
||||
bool
|
||||
"""
|
||||
return await _conf.is_global()
|
||||
|
||||
|
||||
async def set_global(global_: bool, user: Union[discord.User, discord.Member]) -> bool:
|
||||
"""
|
||||
Sets global status of the bank, requires the user parameter for technical reasons.
|
||||
|
||||
.. important::
|
||||
All accounts are reset when you switch!
|
||||
|
||||
:param global_:
|
||||
:code:`True` will set bank to global mode.
|
||||
:param user:
|
||||
Must be a Member object if changing TO global mode.
|
||||
:type user:
|
||||
discord.User or discord.Member
|
||||
:return:
|
||||
New bank mode, :code:`True` is global.
|
||||
:rtype:
|
||||
bool
|
||||
:raises RuntimeError:
|
||||
If bank is becoming global and :py:class:`discord.Member` was not provided.
|
||||
"""
|
||||
if (await is_global()) is global_:
|
||||
return global_
|
||||
|
||||
if is_global():
|
||||
await _conf.user(user).clear_all()
|
||||
elif isinstance(user, discord.Member):
|
||||
await _conf.member(user).clear_all()
|
||||
else:
|
||||
raise RuntimeError("You must provide a member if you're changing to global"
|
||||
" bank mode.")
|
||||
|
||||
await _conf.is_global.set(global_)
|
||||
return global_
|
||||
|
||||
|
||||
async def get_bank_name(guild: discord.Guild=None) -> str:
|
||||
"""
|
||||
Gets the current bank name. If the bank is guild-specific the
|
||||
guild parameter is required.
|
||||
|
||||
May raise RuntimeError if guild is missing and required.
|
||||
|
||||
:param discord.Guild guild:
|
||||
The guild to get the bank name for (required if bank is guild-specific).
|
||||
:return:
|
||||
The bank's name.
|
||||
:rtype:
|
||||
str
|
||||
:raises RuntimeError:
|
||||
If the bank is guild-specific and guild was not provided.
|
||||
"""
|
||||
if await is_global():
|
||||
return await _conf.bank_name()
|
||||
elif guild is not None:
|
||||
return await _conf.guild(guild).bank_name()
|
||||
else:
|
||||
raise RuntimeError("Guild parameter is required and missing.")
|
||||
|
||||
|
||||
async def set_bank_name(name: str, guild: discord.Guild=None) -> str:
|
||||
"""
|
||||
Sets the bank name, if bank is guild specific the guild parameter is
|
||||
required.
|
||||
|
||||
May throw RuntimeError if guild is required and missing.
|
||||
|
||||
:param str name:
|
||||
The new name for the bank.
|
||||
:param discord.Guild guild:
|
||||
The guild to set the bank name for (required if bank is guild-specific).
|
||||
:return:
|
||||
The new name for the bank.
|
||||
:rtype:
|
||||
str
|
||||
:raises RuntimeError:
|
||||
If the bank is guild-specific and guild was not provided.
|
||||
"""
|
||||
if await is_global():
|
||||
await _conf.bank_name.set(name)
|
||||
elif guild is not None:
|
||||
await _conf.guild(guild).bank_name.set(name)
|
||||
else:
|
||||
raise RuntimeError("Guild must be provided if setting the name of a guild"
|
||||
"-specific bank.")
|
||||
return name
|
||||
|
||||
|
||||
async def get_currency_name(guild: discord.Guild=None) -> str:
|
||||
"""
|
||||
Gets the currency name of the bank. The guild parameter is required if
|
||||
the bank is guild-specific.
|
||||
|
||||
May raise RuntimeError if the guild is missing and required.
|
||||
|
||||
:param discord.Guild guild:
|
||||
The guild to get the currency name for (required if bank is guild-specific).
|
||||
:return:
|
||||
The currency name.
|
||||
:rtype:
|
||||
str
|
||||
:raises RuntimeError:
|
||||
If the bank is guild-specific and guild was not provided.
|
||||
"""
|
||||
if await is_global():
|
||||
return await _conf.currency()
|
||||
elif guild is not None:
|
||||
return await _conf.guild(guild).currency()
|
||||
else:
|
||||
raise RuntimeError("Guild must be provided.")
|
||||
|
||||
|
||||
async def set_currency_name(name: str, guild: discord.Guild=None) -> str:
|
||||
"""
|
||||
Sets the currency name for the bank, if bank is guild specific the
|
||||
guild parameter is required.
|
||||
|
||||
May raise RuntimeError if guild is missing and required.
|
||||
|
||||
:param str name:
|
||||
The new name for the currency.
|
||||
:param discord.Guild guild:
|
||||
The guild to set the currency name for (required if bank is guild-specific).
|
||||
:return:
|
||||
The new name for the currency.
|
||||
:rtype:
|
||||
str
|
||||
:raises RuntimeError:
|
||||
If the bank is guild-specific and guild was not provided.
|
||||
"""
|
||||
if await is_global():
|
||||
await _conf.currency.set(name)
|
||||
elif guild is not None:
|
||||
await _conf.guild(guild).currency.set(name)
|
||||
else:
|
||||
raise RuntimeError("Guild must be provided if setting the currency"
|
||||
" name of a guild-specific bank.")
|
||||
return name
|
||||
|
||||
|
||||
async def get_default_balance(guild: discord.Guild=None) -> int:
|
||||
"""
|
||||
Gets the current default balance amount. If the bank is guild-specific
|
||||
you must pass guild.
|
||||
|
||||
May raise RuntimeError if guild is missing and required.
|
||||
|
||||
:param discord.Guild guild:
|
||||
The guild to get the default balance for (required if bank is guild-specific).
|
||||
:return:
|
||||
The bank's default balance.
|
||||
:rtype:
|
||||
str
|
||||
:raises RuntimeError:
|
||||
If the bank is guild-specific and guild was not provided.
|
||||
"""
|
||||
if await is_global():
|
||||
return await _conf.default_balance()
|
||||
elif guild is not None:
|
||||
return await _conf.guild(guild).default_balance()
|
||||
else:
|
||||
raise RuntimeError("Guild is missing and required!")
|
||||
|
||||
|
||||
async def set_default_balance(amount: int, guild: discord.Guild=None) -> int:
|
||||
"""
|
||||
Sets the default balance amount. Guild is required if the bank is
|
||||
guild-specific.
|
||||
|
||||
May raise RuntimeError if guild is missing and required.
|
||||
|
||||
May raise ValueError if amount is invalid.
|
||||
|
||||
:param int amount:
|
||||
The new default balance.
|
||||
:param discord.Guild guild:
|
||||
The guild to set the default balance for (required if bank is guild-specific).
|
||||
:return:
|
||||
The new default balance.
|
||||
:rtype:
|
||||
str
|
||||
:raises RuntimeError:
|
||||
If the bank is guild-specific and guild was not provided.
|
||||
:raises ValueError:
|
||||
If the amount is invalid.
|
||||
"""
|
||||
amount = int(amount)
|
||||
if amount < 0:
|
||||
raise ValueError("Amount must be greater than zero.")
|
||||
|
||||
if await is_global():
|
||||
await _conf.default_balance.set(amount)
|
||||
elif guild is not None:
|
||||
await _conf.guild(guild).default_balance.set(amount)
|
||||
else:
|
||||
raise RuntimeError("Guild is missing and required.")
|
||||
|
||||
return amount
|
||||
190
redbot/core/bot.py
Normal file
190
redbot/core/bot.py
Normal file
@@ -0,0 +1,190 @@
|
||||
import asyncio
|
||||
import os
|
||||
from collections import Counter
|
||||
from enum import Enum
|
||||
from importlib.machinery import ModuleSpec
|
||||
from pathlib import Path
|
||||
|
||||
import discord
|
||||
from discord.ext import commands
|
||||
from discord.ext.commands import GroupMixin
|
||||
|
||||
from .cog_manager import CogManager
|
||||
from . import Config
|
||||
from . import i18n
|
||||
|
||||
|
||||
class Red(commands.Bot):
|
||||
def __init__(self, cli_flags, bot_dir: Path=Path.cwd(), **kwargs):
|
||||
self._shutdown_mode = ExitCodes.CRITICAL
|
||||
self.db = Config.get_core_conf(force_registration=True)
|
||||
self._co_owners = cli_flags.co_owner
|
||||
|
||||
self.db.register_global(
|
||||
token=None,
|
||||
prefix=[],
|
||||
packages=[],
|
||||
owner=None,
|
||||
whitelist=[],
|
||||
blacklist=[],
|
||||
enable_sentry=None,
|
||||
locale='en'
|
||||
)
|
||||
|
||||
self.db.register_guild(
|
||||
prefix=[],
|
||||
whitelist=[],
|
||||
blacklist=[],
|
||||
admin_role=None,
|
||||
mod_role=None
|
||||
)
|
||||
|
||||
async def prefix_manager(bot, message):
|
||||
if not cli_flags.prefix:
|
||||
global_prefix = await bot.db.prefix()
|
||||
else:
|
||||
global_prefix = cli_flags.prefix
|
||||
if message.guild is None:
|
||||
return global_prefix
|
||||
server_prefix = await bot.db.guild(message.guild).prefix()
|
||||
return server_prefix if server_prefix else global_prefix
|
||||
|
||||
if "command_prefix" not in kwargs:
|
||||
kwargs["command_prefix"] = prefix_manager
|
||||
|
||||
if cli_flags.owner and "owner_id" not in kwargs:
|
||||
kwargs["owner_id"] = cli_flags.owner
|
||||
|
||||
if "owner_id" not in kwargs:
|
||||
loop = asyncio.get_event_loop()
|
||||
loop.run_until_complete(self._dict_abuse(kwargs))
|
||||
|
||||
self.counter = Counter()
|
||||
self.uptime = None
|
||||
|
||||
self.main_dir = bot_dir
|
||||
|
||||
self.cog_mgr = CogManager(paths=(str(self.main_dir / 'cogs'),))
|
||||
|
||||
super().__init__(**kwargs)
|
||||
|
||||
async def _dict_abuse(self, indict):
|
||||
"""
|
||||
Please blame <@269933075037814786> for this.
|
||||
|
||||
:param indict:
|
||||
:return:
|
||||
"""
|
||||
|
||||
indict['owner_id'] = await self.db.owner()
|
||||
i18n.set_locale(await self.db.locale())
|
||||
|
||||
async def is_owner(self, user):
|
||||
if user.id in self._co_owners:
|
||||
return True
|
||||
return await super().is_owner(user)
|
||||
|
||||
async def send_cmd_help(self, ctx):
|
||||
if ctx.invoked_subcommand:
|
||||
pages = await self.formatter.format_help_for(ctx, ctx.invoked_subcommand)
|
||||
for page in pages:
|
||||
await ctx.send(page)
|
||||
else:
|
||||
pages = await self.formatter.format_help_for(ctx, ctx.command)
|
||||
for page in pages:
|
||||
await ctx.send(page)
|
||||
|
||||
async def shutdown(self, *, restart=False):
|
||||
"""Gracefully quits Red with exit code 0
|
||||
|
||||
If restart is True, the exit code will be 26 instead
|
||||
Upon receiving that exit code, the launcher restarts Red"""
|
||||
if not restart:
|
||||
self._shutdown_mode = ExitCodes.SHUTDOWN
|
||||
else:
|
||||
self._shutdown_mode = ExitCodes.RESTART
|
||||
|
||||
await self.logout()
|
||||
|
||||
def list_packages(self):
|
||||
"""Lists packages present in the cogs the folder"""
|
||||
return os.listdir("cogs")
|
||||
|
||||
async def save_packages_status(self, packages):
|
||||
await self.db.packages.set(packages)
|
||||
|
||||
async def add_loaded_package(self, pkg_name: str):
|
||||
curr_pkgs = await self.db.packages()
|
||||
if pkg_name not in curr_pkgs:
|
||||
curr_pkgs.append(pkg_name)
|
||||
await self.save_packages_status(curr_pkgs)
|
||||
|
||||
async def remove_loaded_package(self, pkg_name: str):
|
||||
curr_pkgs = await self.db.packages()
|
||||
if pkg_name in curr_pkgs:
|
||||
await self.save_packages_status([p for p in curr_pkgs if p != pkg_name])
|
||||
|
||||
def load_extension(self, spec: ModuleSpec):
|
||||
name = spec.name.split('.')[-1]
|
||||
if name in self.extensions:
|
||||
return
|
||||
|
||||
lib = spec.loader.load_module()
|
||||
if not hasattr(lib, 'setup'):
|
||||
del lib
|
||||
raise discord.ClientException('extension does not have a setup function')
|
||||
|
||||
lib.setup(self)
|
||||
self.extensions[name] = lib
|
||||
|
||||
def unload_extension(self, name):
|
||||
lib = self.extensions.get(name)
|
||||
if lib is None:
|
||||
return
|
||||
|
||||
lib_name = lib.__name__ # Thank you
|
||||
|
||||
# find all references to the module
|
||||
|
||||
# remove the cogs registered from the module
|
||||
for cogname, cog in self.cogs.copy().items():
|
||||
if cog.__module__.startswith(lib_name):
|
||||
self.remove_cog(cogname)
|
||||
|
||||
# first remove all the commands from the module
|
||||
for cmd in self.all_commands.copy().values():
|
||||
if cmd.module.startswith(lib_name):
|
||||
if isinstance(cmd, GroupMixin):
|
||||
cmd.recursively_remove_all_commands()
|
||||
self.remove_command(cmd.name)
|
||||
|
||||
# then remove all the listeners from the module
|
||||
for event_list in self.extra_events.copy().values():
|
||||
remove = []
|
||||
for index, event in enumerate(event_list):
|
||||
if event.__module__.startswith(lib_name):
|
||||
remove.append(index)
|
||||
|
||||
for index in reversed(remove):
|
||||
del event_list[index]
|
||||
|
||||
try:
|
||||
func = getattr(lib, 'teardown')
|
||||
except AttributeError:
|
||||
pass
|
||||
else:
|
||||
try:
|
||||
func(self)
|
||||
except:
|
||||
pass
|
||||
finally:
|
||||
# finally remove the import..
|
||||
del lib
|
||||
del self.extensions[name]
|
||||
# del sys.modules[name]
|
||||
|
||||
|
||||
class ExitCodes(Enum):
|
||||
CRITICAL = 1
|
||||
SHUTDOWN = 0
|
||||
RESTART = 26
|
||||
78
redbot/core/checks.py
Normal file
78
redbot/core/checks.py
Normal file
@@ -0,0 +1,78 @@
|
||||
import discord
|
||||
from discord.ext import commands
|
||||
|
||||
|
||||
def is_owner(**kwargs):
|
||||
async def check(ctx):
|
||||
return await ctx.bot.is_owner(ctx.author, **kwargs)
|
||||
return commands.check(check)
|
||||
|
||||
|
||||
async def check_permissions(ctx, perms):
|
||||
if await ctx.bot.is_owner(ctx.author):
|
||||
return True
|
||||
elif not perms:
|
||||
return False
|
||||
resolved = ctx.channel.permissions_for(ctx.author)
|
||||
|
||||
return all(getattr(resolved, name, None) == value for name, value in perms.items())
|
||||
|
||||
|
||||
def mod_or_permissions(**perms):
|
||||
async def predicate(ctx):
|
||||
has_perms_or_is_owner = await check_permissions(ctx, perms)
|
||||
if ctx.guild is None:
|
||||
return has_perms_or_is_owner
|
||||
author = ctx.author
|
||||
settings = ctx.bot.db.guild(ctx.guild)
|
||||
mod_role_id = await settings.mod_role()
|
||||
admin_role_id = await settings.admin_role()
|
||||
|
||||
mod_role = discord.utils.get(ctx.guild.roles, id=mod_role_id)
|
||||
admin_role = discord.utils.get(ctx.guild.roles, id=admin_role_id)
|
||||
|
||||
is_staff = mod_role in author.roles or admin_role in author.roles
|
||||
is_guild_owner = author == ctx.guild.owner
|
||||
|
||||
return is_staff or has_perms_or_is_owner or is_guild_owner
|
||||
|
||||
return commands.check(predicate)
|
||||
|
||||
|
||||
def admin_or_permissions(**perms):
|
||||
async def predicate(ctx):
|
||||
has_perms_or_is_owner = await check_permissions(ctx, perms)
|
||||
if ctx.guild is None:
|
||||
return has_perms_or_is_owner
|
||||
author = ctx.author
|
||||
is_guild_owner = author == ctx.guild.owner
|
||||
admin_role_id = await ctx.bot.db.guild(ctx.guild).admin_role()
|
||||
admin_role = discord.utils.get(ctx.guild.roles, id=admin_role_id)
|
||||
|
||||
return admin_role in author.roles or has_perms_or_is_owner or is_guild_owner
|
||||
|
||||
return commands.check(predicate)
|
||||
|
||||
|
||||
def guildowner_or_permissions(**perms):
|
||||
async def predicate(ctx):
|
||||
has_perms_or_is_owner = await check_permissions(ctx, perms)
|
||||
if ctx.guild is None:
|
||||
return has_perms_or_is_owner
|
||||
is_guild_owner = ctx.author == ctx.guild.owner
|
||||
|
||||
return is_guild_owner or has_perms_or_is_owner
|
||||
|
||||
return commands.check(predicate)
|
||||
|
||||
|
||||
def guildowner():
|
||||
return guildowner_or_permissions()
|
||||
|
||||
|
||||
def admin():
|
||||
return admin_or_permissions()
|
||||
|
||||
|
||||
def mod():
|
||||
return mod_or_permissions()
|
||||
115
redbot/core/cli.py
Normal file
115
redbot/core/cli.py
Normal file
@@ -0,0 +1,115 @@
|
||||
import argparse
|
||||
import asyncio
|
||||
|
||||
from redbot.core.bot import Red
|
||||
|
||||
|
||||
def confirm(m=""):
|
||||
return input(m).lower().strip() in ("y", "yes")
|
||||
|
||||
|
||||
def interactive_config(red, token_set, prefix_set):
|
||||
loop = asyncio.get_event_loop()
|
||||
token = ""
|
||||
|
||||
print("Red - Discord Bot | Configuration process\n")
|
||||
|
||||
if not token_set:
|
||||
print("Please enter a valid token:")
|
||||
while not token:
|
||||
token = input("> ")
|
||||
if not len(token) >= 50:
|
||||
print("That doesn't look like a valid token.")
|
||||
token = ""
|
||||
if token:
|
||||
loop.run_until_complete(red.db.token.set(token))
|
||||
|
||||
if not prefix_set:
|
||||
prefix = ""
|
||||
print("\nPick a prefix. A prefix is what you type before a "
|
||||
"command. Example:\n"
|
||||
"!help\n^ The exclamation mark is the prefix in this case.\n"
|
||||
"Can be multiple characters. You will be able to change it "
|
||||
"later and add more of them.\nChoose your prefix:\n")
|
||||
while not prefix:
|
||||
prefix = input("Prefix> ")
|
||||
if len(prefix) > 10:
|
||||
print("Your prefix seems overly long. Are you sure it "
|
||||
"is correct? (y/n)")
|
||||
if not confirm("> "):
|
||||
prefix = ""
|
||||
if prefix:
|
||||
loop.run_until_complete(red.db.prefix.set([prefix]))
|
||||
|
||||
ask_sentry(red)
|
||||
|
||||
return token
|
||||
|
||||
|
||||
def ask_sentry(red: Red):
|
||||
loop = asyncio.get_event_loop()
|
||||
print("\nThank you for installing Red V3 alpha! The current version\n"
|
||||
" is not suited for production use and is aimed at testing\n"
|
||||
" the current and upcoming featureset, that's why we will\n"
|
||||
" also collect the fatal error logs to help us fix any new\n"
|
||||
" found issues in a timely manner. If you wish to opt in\n"
|
||||
" the process please type \"yes\":\n")
|
||||
if not confirm("> "):
|
||||
loop.run_until_complete(red.db.enable_sentry.set(False))
|
||||
else:
|
||||
loop.run_until_complete(red.db.enable_sentry.set(True))
|
||||
print("\nThank you for helping us with the development process!")
|
||||
|
||||
|
||||
def parse_cli_flags(args):
|
||||
parser = argparse.ArgumentParser(description="Red - Discord Bot")
|
||||
parser.add_argument("--owner", type=int,
|
||||
help="ID of the owner. Only who hosts "
|
||||
"Red should be owner, this has "
|
||||
"serious security implications.")
|
||||
parser.add_argument("--co-owner", type=int, action="append", default=[],
|
||||
help="ID of a co-owner. Only people who have access "
|
||||
"to the system that is hosting Red should be "
|
||||
"co-owners, as this gives them complete access "
|
||||
"to the system's data. This has serious "
|
||||
"security implications if misused. Can be "
|
||||
"multiple.")
|
||||
parser.add_argument("--prefix", "-p", action="append",
|
||||
help="Global prefix. Can be multiple")
|
||||
parser.add_argument("--no-prompt",
|
||||
action="store_true",
|
||||
help="Disables console inputs. Features requiring "
|
||||
"console interaction could be disabled as a "
|
||||
"result")
|
||||
parser.add_argument("--no-cogs",
|
||||
action="store_true",
|
||||
help="Starts Red with no cogs loaded, only core")
|
||||
parser.add_argument("--self-bot",
|
||||
action='store_true',
|
||||
help="Specifies if Red should log in as selfbot")
|
||||
parser.add_argument("--not-bot",
|
||||
action='store_true',
|
||||
help="Specifies if the token used belongs to a bot "
|
||||
"account.")
|
||||
parser.add_argument("--dry-run",
|
||||
action="store_true",
|
||||
help="Makes Red quit with code 0 just before the "
|
||||
"login. This is useful for testing the boot "
|
||||
"process.")
|
||||
parser.add_argument("--debug",
|
||||
action="store_true",
|
||||
help="Sets the loggers level as debug")
|
||||
parser.add_argument("--dev",
|
||||
action="store_true",
|
||||
help="Enables developer mode")
|
||||
parser.add_argument("instance_name",
|
||||
help="Name of the bot instance created during `redbot-setup`.")
|
||||
|
||||
args = parser.parse_args(args)
|
||||
|
||||
if args.prefix:
|
||||
args.prefix = sorted(args.prefix, reverse=True)
|
||||
else:
|
||||
args.prefix = []
|
||||
|
||||
return args
|
||||
290
redbot/core/cog_manager.py
Normal file
290
redbot/core/cog_manager.py
Normal file
@@ -0,0 +1,290 @@
|
||||
import pkgutil
|
||||
from importlib import invalidate_caches
|
||||
from importlib.machinery import ModuleSpec
|
||||
from pathlib import Path
|
||||
from typing import Tuple, Union, List
|
||||
|
||||
from . import checks
|
||||
from .config import Config
|
||||
from .i18n import CogI18n
|
||||
from .data_manager import cog_data_path
|
||||
from discord.ext import commands
|
||||
|
||||
from .utils.chat_formatting import box
|
||||
|
||||
__all__ = ["CogManager"]
|
||||
|
||||
|
||||
class CogManager:
|
||||
"""
|
||||
This module allows you to load cogs from multiple directories and even from outside the bot
|
||||
directory. You may also set a directory for downloader to install new cogs to, the default
|
||||
being the :code:`cogs/` folder in the root bot directory.
|
||||
"""
|
||||
def __init__(self, paths: Tuple[str]=()):
|
||||
self.conf = Config.get_conf(self, 2938473984732, True)
|
||||
tmp_cog_install_path = cog_data_path(self) / "cogs"
|
||||
tmp_cog_install_path.mkdir(parents=True, exist_ok=True)
|
||||
self.conf.register_global(
|
||||
paths=(),
|
||||
install_path=str(tmp_cog_install_path)
|
||||
)
|
||||
|
||||
self._paths = list(paths)
|
||||
|
||||
async def paths(self) -> Tuple[Path, ...]:
|
||||
"""
|
||||
All currently valid path directories.
|
||||
"""
|
||||
conf_paths = await self.conf.paths()
|
||||
other_paths = self._paths
|
||||
|
||||
all_paths = set(list(conf_paths) + list(other_paths))
|
||||
|
||||
paths = [Path(p) for p in all_paths]
|
||||
if self.install_path not in paths:
|
||||
paths.insert(0, await self.install_path())
|
||||
return tuple(p.resolve() for p in paths if p.is_dir())
|
||||
|
||||
async def install_path(self) -> Path:
|
||||
"""
|
||||
The install path for 3rd party cogs.
|
||||
"""
|
||||
p = Path(await self.conf.install_path())
|
||||
return p.resolve()
|
||||
|
||||
async def set_install_path(self, path: Path) -> Path:
|
||||
"""
|
||||
Install path setter, will return the absolute path to
|
||||
the given path.
|
||||
|
||||
.. note::
|
||||
|
||||
The bot will not remember your old cog install path which means
|
||||
that ALL PREVIOUSLY INSTALLED COGS will now be unfindable.
|
||||
|
||||
:param pathlib.Path path:
|
||||
The new directory for cog installs.
|
||||
:raises ValueError:
|
||||
If :code:`path` is not an existing directory.
|
||||
"""
|
||||
if not path.is_dir():
|
||||
raise ValueError("The install path must be an existing directory.")
|
||||
resolved = path.resolve()
|
||||
await self.conf.install_path.set(str(resolved))
|
||||
return resolved
|
||||
|
||||
@staticmethod
|
||||
def _ensure_path_obj(path: Union[Path, str]) -> Path:
|
||||
"""
|
||||
Guarantees an object will be a path object.
|
||||
|
||||
:param path:
|
||||
:type path:
|
||||
pathlib.Path or str
|
||||
:rtype:
|
||||
pathlib.Path
|
||||
"""
|
||||
try:
|
||||
path.exists()
|
||||
except AttributeError:
|
||||
path = Path(path)
|
||||
return path
|
||||
|
||||
async def add_path(self, path: Union[Path, str]):
|
||||
"""
|
||||
Adds a cog path to current list, will ignore duplicates. Does have
|
||||
a side effect of removing all invalid paths from the saved path
|
||||
list.
|
||||
|
||||
:param path:
|
||||
Path to add.
|
||||
:type path:
|
||||
pathlib.Path or str
|
||||
:raises ValueError:
|
||||
If :code:`path` does not resolve to an existing directory.
|
||||
"""
|
||||
path = self._ensure_path_obj(path)
|
||||
|
||||
# This makes the path absolute, will break if a bot install
|
||||
# changes OS/Computer?
|
||||
path = path.resolve()
|
||||
|
||||
if not path.is_dir():
|
||||
raise ValueError("'{}' is not a valid directory.".format(path))
|
||||
|
||||
if path == await self.install_path():
|
||||
raise ValueError("Cannot add the install path as an additional path.")
|
||||
|
||||
all_paths = set(await self.paths() + (path, ))
|
||||
# noinspection PyTypeChecker
|
||||
await self.set_paths(all_paths)
|
||||
|
||||
async def remove_path(self, path: Union[Path, str]) -> Tuple[Path, ...]:
|
||||
"""
|
||||
Removes a path from the current paths list.
|
||||
|
||||
:param path: Path to remove.
|
||||
:type path:
|
||||
pathlib.Path or str
|
||||
:return:
|
||||
Tuple of new valid paths.
|
||||
:rtype: tuple
|
||||
"""
|
||||
path = self._ensure_path_obj(path)
|
||||
all_paths = list(await self.paths())
|
||||
if path in all_paths:
|
||||
all_paths.remove(path) # Modifies in place
|
||||
await self.set_paths(all_paths)
|
||||
return tuple(all_paths)
|
||||
|
||||
async def set_paths(self, paths_: List[Path]):
|
||||
"""
|
||||
Sets the current paths list.
|
||||
|
||||
:param List[pathlib.Path] paths_:
|
||||
List of paths to set.
|
||||
"""
|
||||
str_paths = [str(p) for p in paths_]
|
||||
await self.conf.paths.set(str_paths)
|
||||
|
||||
async def find_cog(self, name: str) -> ModuleSpec:
|
||||
"""
|
||||
Finds a cog in the list of available paths.
|
||||
|
||||
:param name:
|
||||
Name of the cog to find.
|
||||
:raises RuntimeError:
|
||||
If there is no cog with the given name.
|
||||
:return:
|
||||
A module spec to be used for specialized cog loading.
|
||||
:rtype:
|
||||
importlib.machinery.ModuleSpec
|
||||
"""
|
||||
resolved_paths = [str(p.resolve()) for p in await self.paths()]
|
||||
for finder, module_name, _ in pkgutil.iter_modules(resolved_paths):
|
||||
if name == module_name:
|
||||
spec = finder.find_spec(name)
|
||||
if spec:
|
||||
return spec
|
||||
|
||||
raise RuntimeError("No module by the name of '{}' was found"
|
||||
" in any available path.".format(name))
|
||||
|
||||
@staticmethod
|
||||
def invalidate_caches():
|
||||
"""
|
||||
This is an alias for an importlib internal and should be called
|
||||
any time that a new module has been installed to a cog directory.
|
||||
|
||||
*I think.*
|
||||
"""
|
||||
invalidate_caches()
|
||||
|
||||
|
||||
_ = CogI18n("CogManagerUI", __file__)
|
||||
|
||||
|
||||
class CogManagerUI:
|
||||
@commands.command()
|
||||
@checks.is_owner()
|
||||
async def paths(self, ctx: commands.Context):
|
||||
"""
|
||||
Lists current cog paths in order of priority.
|
||||
"""
|
||||
install_path = await ctx.bot.cog_mgr.install_path()
|
||||
cog_paths = ctx.bot.cog_mgr.paths
|
||||
cog_paths = [p for p in cog_paths if p != install_path]
|
||||
|
||||
msg = _("Install Path: {}\n\n").format(install_path)
|
||||
|
||||
partial = []
|
||||
for i, p in enumerate(cog_paths, start=1):
|
||||
partial.append("{}. {}".format(i, p))
|
||||
|
||||
msg += "\n".join(partial)
|
||||
await ctx.send(box(msg))
|
||||
|
||||
@commands.command()
|
||||
@checks.is_owner()
|
||||
async def addpath(self, ctx: commands.Context, path: Path):
|
||||
"""
|
||||
Add a path to the list of available cog paths.
|
||||
"""
|
||||
if not path.is_dir():
|
||||
await ctx.send(_("That path is does not exist or does not"
|
||||
" point to a valid directory."))
|
||||
return
|
||||
|
||||
try:
|
||||
await ctx.bot.cog_mgr.add_path(path)
|
||||
except ValueError as e:
|
||||
await ctx.send(str(e))
|
||||
else:
|
||||
await ctx.send(_("Path successfully added."))
|
||||
|
||||
@commands.command()
|
||||
@checks.is_owner()
|
||||
async def removepath(self, ctx: commands.Context, path_number: int):
|
||||
"""
|
||||
Removes a path from the available cog paths given the path_number
|
||||
from !paths
|
||||
"""
|
||||
cog_paths = await ctx.bot.cog_mgr.paths()
|
||||
try:
|
||||
to_remove = cog_paths[path_number]
|
||||
except IndexError:
|
||||
await ctx.send(_("That is an invalid path number."))
|
||||
return
|
||||
|
||||
await ctx.bot.cog_mgr.remove_path(to_remove)
|
||||
await ctx.send(_("Path successfully removed."))
|
||||
|
||||
@commands.command()
|
||||
@checks.is_owner()
|
||||
async def reorderpath(self, ctx: commands.Context, from_: int, to: int):
|
||||
"""
|
||||
Reorders paths internally to allow discovery of different cogs.
|
||||
"""
|
||||
# Doing this because in the paths command they're 1 indexed
|
||||
from_ -= 1
|
||||
to -= 1
|
||||
|
||||
all_paths = list(await ctx.bot.cog_mgr.paths())
|
||||
try:
|
||||
to_move = all_paths.pop(from_)
|
||||
except IndexError:
|
||||
await ctx.send(_("Invalid 'from' index."))
|
||||
return
|
||||
|
||||
try:
|
||||
all_paths.insert(to, to_move)
|
||||
except IndexError:
|
||||
await ctx.send(_("Invalid 'to' index."))
|
||||
return
|
||||
|
||||
await ctx.bot.cog_mgr.set_paths(all_paths)
|
||||
await ctx.send(_("Paths reordered."))
|
||||
|
||||
@commands.command()
|
||||
@checks.is_owner()
|
||||
async def installpath(self, ctx: commands.Context, path: Path=None):
|
||||
"""
|
||||
Returns the current install path or sets it if one is provided.
|
||||
The provided path must be absolute or relative to the bot's
|
||||
directory and it must already exist.
|
||||
|
||||
No installed cogs will be transferred in the process.
|
||||
"""
|
||||
if path:
|
||||
if not path.is_absolute():
|
||||
path = (ctx.bot.main_dir / path).resolve()
|
||||
try:
|
||||
await ctx.bot.cog_mgr.set_install_path(path)
|
||||
except ValueError:
|
||||
await ctx.send(_("That path does not exist."))
|
||||
return
|
||||
|
||||
install_path = await ctx.bot.cog_mgr.install_path()
|
||||
await ctx.send(_("The bot will install new cogs to the `{}`"
|
||||
" directory.").format(install_path))
|
||||
650
redbot/core/config.py
Normal file
650
redbot/core/config.py
Normal file
@@ -0,0 +1,650 @@
|
||||
import logging
|
||||
from copy import deepcopy
|
||||
from typing import Callable, Union, Tuple
|
||||
|
||||
import discord
|
||||
|
||||
from .data_manager import cog_data_path, core_data_path
|
||||
from .drivers.red_json import JSON as JSONDriver
|
||||
|
||||
log = logging.getLogger("red.config")
|
||||
|
||||
|
||||
class Value:
|
||||
"""
|
||||
A singular "value" of data.
|
||||
|
||||
.. py:attribute:: identifiers
|
||||
|
||||
This attribute provides all the keys necessary to get a specific data element from a json document.
|
||||
|
||||
.. py:attribute:: default
|
||||
|
||||
The default value for the data element that :py:attr:`identifiers` points at.
|
||||
|
||||
.. py:attribute:: spawner
|
||||
|
||||
A reference to :py:attr:`.Config.spawner`.
|
||||
"""
|
||||
def __init__(self, identifiers: Tuple[str], default_value, spawner):
|
||||
self._identifiers = identifiers
|
||||
self.default = default_value
|
||||
|
||||
self.spawner = spawner
|
||||
|
||||
@property
|
||||
def identifiers(self):
|
||||
return tuple(str(i) for i in self._identifiers)
|
||||
|
||||
async def _get(self, default):
|
||||
driver = self.spawner.get_driver()
|
||||
try:
|
||||
ret = await driver.get(self.identifiers)
|
||||
except KeyError:
|
||||
return default if default is not None else self.default
|
||||
return ret
|
||||
|
||||
def __call__(self, default=None):
|
||||
"""
|
||||
Each :py:class:`Value` object is created by the :py:meth:`Group.__getattr__` method.
|
||||
The "real" data of the :py:class:`Value` object is accessed by this method. It is a replacement for a
|
||||
:python:`get()` method.
|
||||
|
||||
For example::
|
||||
|
||||
foo = await conf.guild(some_guild).foo()
|
||||
|
||||
# Is equivalent to this
|
||||
|
||||
group_obj = conf.guild(some_guild)
|
||||
value_obj = conf.foo
|
||||
foo = await value_obj()
|
||||
|
||||
.. important::
|
||||
|
||||
This is now, for all intents and purposes, a coroutine.
|
||||
|
||||
:param default:
|
||||
This argument acts as an override for the registered default provided by :py:attr:`default`. This argument
|
||||
is ignored if its value is :python:`None`.
|
||||
:type default: Optional[object]
|
||||
:return:
|
||||
A coroutine object that must be awaited.
|
||||
"""
|
||||
return self._get(default)
|
||||
|
||||
async def set(self, value):
|
||||
"""
|
||||
Sets the value of the data element indicate by :py:attr:`identifiers`.
|
||||
|
||||
For example::
|
||||
|
||||
# Sets global value "foo" to False
|
||||
await conf.foo.set(False)
|
||||
|
||||
# Sets guild specific value of "bar" to True
|
||||
await conf.guild(some_guild).bar.set(True)
|
||||
"""
|
||||
driver = self.spawner.get_driver()
|
||||
await driver.set(self.identifiers, value)
|
||||
|
||||
|
||||
class Group(Value):
|
||||
"""
|
||||
A "group" of data, inherits from :py:class:`.Value` which means that all of the attributes and methods available
|
||||
in :py:class:`.Value` are also available when working with a :py:class:`.Group` object.
|
||||
|
||||
.. py:attribute:: defaults
|
||||
|
||||
A dictionary of registered default values for this :py:class:`Group`.
|
||||
|
||||
.. py:attribute:: force_registration
|
||||
|
||||
See :py:attr:`.Config.force_registration`.
|
||||
"""
|
||||
def __init__(self, identifiers: Tuple[str],
|
||||
defaults: dict,
|
||||
spawner,
|
||||
force_registration: bool=False):
|
||||
self._defaults = defaults
|
||||
self.force_registration = force_registration
|
||||
self.spawner = spawner
|
||||
|
||||
super().__init__(identifiers, {}, self.spawner)
|
||||
|
||||
@property
|
||||
def defaults(self):
|
||||
return self._defaults.copy()
|
||||
|
||||
# noinspection PyTypeChecker
|
||||
def __getattr__(self, item: str) -> Union["Group", Value]:
|
||||
"""
|
||||
Takes in the next accessible item.
|
||||
|
||||
1. If it's found to be a group of data we return another :py:class:`Group` object.
|
||||
2. If it's found to be a data value we return a :py:class:`.Value` object.
|
||||
3. If it is not found and :py:attr:`force_registration` is :python:`True` then we raise
|
||||
:py:exc:`AttributeError`.
|
||||
4. Otherwise return a :py:class:`.Value` object.
|
||||
|
||||
:param str item:
|
||||
The name of the item a cog is attempting to access through normal Python attribute
|
||||
access.
|
||||
"""
|
||||
is_group = self.is_group(item)
|
||||
is_value = not is_group and self.is_value(item)
|
||||
new_identifiers = self.identifiers + (item, )
|
||||
if is_group:
|
||||
return Group(
|
||||
identifiers=new_identifiers,
|
||||
defaults=self._defaults[item],
|
||||
spawner=self.spawner,
|
||||
force_registration=self.force_registration
|
||||
)
|
||||
elif is_value:
|
||||
return Value(
|
||||
identifiers=new_identifiers,
|
||||
default_value=self._defaults[item],
|
||||
spawner=self.spawner
|
||||
)
|
||||
elif self.force_registration:
|
||||
raise AttributeError(
|
||||
"'{}' is not a valid registered Group"
|
||||
"or value.".format(item)
|
||||
)
|
||||
else:
|
||||
return Value(
|
||||
identifiers=new_identifiers,
|
||||
default_value=None,
|
||||
spawner=self.spawner
|
||||
)
|
||||
|
||||
@property
|
||||
def _super_group(self) -> 'Group':
|
||||
super_group = Group(
|
||||
self.identifiers[:-1],
|
||||
defaults={},
|
||||
spawner=self.spawner,
|
||||
force_registration=self.force_registration
|
||||
)
|
||||
return super_group
|
||||
|
||||
def is_group(self, item: str) -> bool:
|
||||
"""
|
||||
A helper method for :py:meth:`__getattr__`. Most developers will have no need to use this.
|
||||
|
||||
:param str item:
|
||||
See :py:meth:`__getattr__`.
|
||||
"""
|
||||
default = self._defaults.get(item)
|
||||
return isinstance(default, dict)
|
||||
|
||||
def is_value(self, item: str) -> bool:
|
||||
"""
|
||||
A helper method for :py:meth:`__getattr__`. Most developers will have no need to use this.
|
||||
|
||||
:param str item:
|
||||
See :py:meth:`__getattr__`.
|
||||
"""
|
||||
try:
|
||||
default = self._defaults[item]
|
||||
except KeyError:
|
||||
return False
|
||||
|
||||
return not isinstance(default, dict)
|
||||
|
||||
def get_attr(self, item: str, default=None, resolve=True):
|
||||
"""
|
||||
This is available to use as an alternative to using normal Python attribute access. It is required if you find
|
||||
a need for dynamic attribute access.
|
||||
|
||||
.. note::
|
||||
|
||||
Use of this method should be avoided wherever possible.
|
||||
|
||||
A possible use case::
|
||||
|
||||
@commands.command()
|
||||
async def some_command(self, ctx, item: str):
|
||||
user = ctx.author
|
||||
|
||||
# Where the value of item is the name of the data field in Config
|
||||
await ctx.send(await self.conf.user(user).get_attr(item))
|
||||
|
||||
:param str item:
|
||||
The name of the data field in :py:class:`.Config`.
|
||||
:param default:
|
||||
This is an optional override to the registered default for this item.
|
||||
:param resolve:
|
||||
If this is :code:`True` this function will return a coroutine that resolves to a "real" data value,
|
||||
if :code:`False` this function will return an instance of :py:class:`Group` or :py:class:`Value`
|
||||
depending on the type of the "real" data value.
|
||||
:rtype:
|
||||
Coroutine or Value
|
||||
"""
|
||||
value = getattr(self, item)
|
||||
if resolve:
|
||||
return value(default=default)
|
||||
else:
|
||||
return value
|
||||
|
||||
async def all(self) -> dict:
|
||||
"""
|
||||
This method allows you to get "all" of a particular group of data. It will return the dictionary of all data
|
||||
for a particular Guild/Channel/Role/User/Member etc.
|
||||
|
||||
.. note::
|
||||
|
||||
Any values that have not been set from the registered defaults will have their default values
|
||||
added to the dictionary that this method returns.
|
||||
|
||||
:rtype: dict
|
||||
"""
|
||||
defaults = self.defaults
|
||||
defaults.update(await self())
|
||||
return defaults
|
||||
|
||||
async def all_from_kind(self) -> dict:
|
||||
"""
|
||||
This method allows you to get all data from all entries in a given Kind. It will return a dictionary of Kind
|
||||
ID's -> data.
|
||||
|
||||
.. note::
|
||||
|
||||
Any values that have not been set from the registered defaults will have their default values
|
||||
added to the dictionary that this method returns.
|
||||
|
||||
.. important::
|
||||
|
||||
This method is overridden in :py:meth:`.MemberGroup.all_from_kind` and functions slightly differently.
|
||||
|
||||
:rtype: dict
|
||||
"""
|
||||
# noinspection PyTypeChecker
|
||||
all_from_kind = await self._super_group()
|
||||
|
||||
for k, v in all_from_kind.items():
|
||||
defaults = self.defaults
|
||||
defaults.update(v)
|
||||
all_from_kind[k] = defaults
|
||||
|
||||
return all_from_kind
|
||||
|
||||
async def set(self, value):
|
||||
if not isinstance(value, dict):
|
||||
raise ValueError(
|
||||
"You may only set the value of a group to be a dict."
|
||||
)
|
||||
await super().set(value)
|
||||
|
||||
async def set_attr(self, item: str, value):
|
||||
"""
|
||||
Please see :py:meth:`get_attr` for more information.
|
||||
|
||||
.. note::
|
||||
|
||||
Use of this method should be avoided wherever possible.
|
||||
"""
|
||||
value_obj = getattr(self, item)
|
||||
await value_obj.set(value)
|
||||
|
||||
async def clear(self):
|
||||
"""
|
||||
Wipes all data from the given Guild/Channel/Role/Member/User. If used on a global group, it will wipe all global
|
||||
data.
|
||||
"""
|
||||
await self.set({})
|
||||
|
||||
async def clear_all(self):
|
||||
"""
|
||||
Wipes all data from all Guilds/Channels/Roles/Members/Users. If used on a global group, this method wipes all
|
||||
data from everything.
|
||||
"""
|
||||
await self._super_group.set({})
|
||||
|
||||
|
||||
class MemberGroup(Group):
|
||||
"""
|
||||
A specific group class for use with member data only. Inherits from :py:class:`.Group`. In this group data is
|
||||
stored as :code:`GUILD_ID -> MEMBER_ID -> data`.
|
||||
"""
|
||||
@property
|
||||
def _super_group(self) -> Group:
|
||||
new_identifiers = self.identifiers[:2]
|
||||
group_obj = Group(
|
||||
identifiers=new_identifiers,
|
||||
defaults={},
|
||||
spawner=self.spawner
|
||||
)
|
||||
return group_obj
|
||||
|
||||
@property
|
||||
def _guild_group(self) -> Group:
|
||||
new_identifiers = self.identifiers[:3]
|
||||
group_obj = Group(
|
||||
identifiers=new_identifiers,
|
||||
defaults={},
|
||||
spawner=self.spawner
|
||||
)
|
||||
return group_obj
|
||||
|
||||
async def all_guilds(self) -> dict:
|
||||
"""
|
||||
Returns a dict of :code:`GUILD_ID -> MEMBER_ID -> data`.
|
||||
|
||||
.. note::
|
||||
|
||||
Any values that have not been set from the registered defaults will have their default values
|
||||
added to the dictionary that this method returns.
|
||||
|
||||
:rtype: dict
|
||||
"""
|
||||
# noinspection PyTypeChecker
|
||||
return await super().all_from_kind()
|
||||
|
||||
async def all_from_kind(self) -> dict:
|
||||
"""
|
||||
Returns a dict of all members from the same guild as the given one.
|
||||
|
||||
.. note::
|
||||
|
||||
Any values that have not been set from the registered defaults will have their default values
|
||||
added to the dictionary that this method returns.
|
||||
|
||||
:rtype: dict
|
||||
"""
|
||||
guild_member = await super().all_from_kind()
|
||||
return guild_member.get(self.identifiers[-2], {})
|
||||
|
||||
|
||||
|
||||
class Config:
|
||||
"""
|
||||
You should always use :func:`get_conf` or :func:`get_core_conf` to initialize a Config object.
|
||||
|
||||
.. important::
|
||||
Most config data should be accessed through its respective group method (e.g. :py:meth:`guild`)
|
||||
however the process for accessing global data is a bit different. There is no :python:`global` method
|
||||
because global data is accessed by normal attribute access::
|
||||
|
||||
await conf.foo()
|
||||
|
||||
.. py:attribute:: cog_name
|
||||
|
||||
The name of the cog that has requested a :py:class:`.Config` object.
|
||||
|
||||
.. py:attribute:: unique_identifier
|
||||
|
||||
Unique identifier provided to differentiate cog data when name conflicts occur.
|
||||
|
||||
.. py:attribute:: spawner
|
||||
|
||||
A callable object that returns some driver that implements :py:class:`.drivers.red_base.BaseDriver`.
|
||||
|
||||
.. py:attribute:: force_registration
|
||||
|
||||
A boolean that determines if :py:class:`.Config` should throw an error if a cog attempts to access an attribute
|
||||
which has not been previously registered.
|
||||
|
||||
.. note::
|
||||
|
||||
**You should use this.** By enabling force registration you give :py:class:`.Config` the ability to alert
|
||||
you instantly if you've made a typo when attempting to access data.
|
||||
"""
|
||||
GLOBAL = "GLOBAL"
|
||||
GUILD = "GUILD"
|
||||
CHANNEL = "TEXTCHANNEL"
|
||||
ROLE = "ROLE"
|
||||
USER = "USER"
|
||||
MEMBER = "MEMBER"
|
||||
|
||||
def __init__(self, cog_name: str, unique_identifier: str,
|
||||
driver_spawn: Callable,
|
||||
force_registration: bool=False,
|
||||
defaults: dict=None):
|
||||
self.cog_name = cog_name
|
||||
self.unique_identifier = unique_identifier
|
||||
|
||||
self.spawner = driver_spawn
|
||||
self.force_registration = force_registration
|
||||
self._defaults = defaults or {}
|
||||
|
||||
@property
|
||||
def defaults(self):
|
||||
return self._defaults.copy()
|
||||
|
||||
@classmethod
|
||||
def get_conf(cls, cog_instance, identifier: int,
|
||||
force_registration=False):
|
||||
"""
|
||||
Returns a Config instance based on a simplified set of initial
|
||||
variables.
|
||||
|
||||
:param cog_instance:
|
||||
:param identifier:
|
||||
Any random integer, used to keep your data
|
||||
distinct from any other cog with the same name.
|
||||
:param force_registration:
|
||||
Should config require registration
|
||||
of data keys before allowing you to get/set values?
|
||||
:return:
|
||||
A new config object.
|
||||
"""
|
||||
cog_path_override = cog_data_path(cog_instance)
|
||||
cog_name = cog_path_override.stem
|
||||
uuid = str(hash(identifier))
|
||||
|
||||
spawner = JSONDriver(cog_name, data_path_override=cog_path_override)
|
||||
return cls(cog_name=cog_name, unique_identifier=uuid,
|
||||
force_registration=force_registration,
|
||||
driver_spawn=spawner)
|
||||
|
||||
@classmethod
|
||||
def get_core_conf(cls, force_registration: bool=False):
|
||||
"""
|
||||
All core modules that require a config instance should use this classmethod instead of
|
||||
:py:meth:`get_conf`
|
||||
|
||||
:param int identifier:
|
||||
See :py:meth:`get_conf`
|
||||
:param force_registration:
|
||||
See :py:attr:`force_registration`
|
||||
:type force_registration: Optional[bool]
|
||||
"""
|
||||
driver_spawn = JSONDriver("Core", data_path_override=core_data_path())
|
||||
return cls(cog_name="Core", driver_spawn=driver_spawn,
|
||||
unique_identifier='0',
|
||||
force_registration=force_registration)
|
||||
|
||||
def __getattr__(self, item: str) -> Union[Group, Value]:
|
||||
"""
|
||||
This is used to generate Value or Group objects for global
|
||||
values.
|
||||
:param item:
|
||||
:return:
|
||||
"""
|
||||
global_group = self._get_base_group(self.GLOBAL)
|
||||
return getattr(global_group, item)
|
||||
|
||||
@staticmethod
|
||||
def _get_defaults_dict(key: str, value) -> dict:
|
||||
"""
|
||||
Since we're allowing nested config stuff now, not storing the
|
||||
_defaults as a flat dict sounds like a good idea. May turn
|
||||
out to be an awful one but we'll see.
|
||||
:param key:
|
||||
:param value:
|
||||
:return:
|
||||
"""
|
||||
ret = {}
|
||||
partial = ret
|
||||
splitted = key.split('__')
|
||||
for i, k in enumerate(splitted, start=1):
|
||||
if not k.isidentifier():
|
||||
raise RuntimeError("'{}' is an invalid config key.".format(k))
|
||||
if i == len(splitted):
|
||||
partial[k] = value
|
||||
else:
|
||||
partial[k] = {}
|
||||
partial = partial[k]
|
||||
return ret
|
||||
|
||||
@staticmethod
|
||||
def _update_defaults(to_add: dict, _partial: dict):
|
||||
"""
|
||||
This tries to update the _defaults dictionary with the nested
|
||||
partial dict generated by _get_defaults_dict. This WILL
|
||||
throw an error if you try to have both a value and a group
|
||||
registered under the same name.
|
||||
:param to_add:
|
||||
:param _partial:
|
||||
:return:
|
||||
"""
|
||||
for k, v in to_add.items():
|
||||
val_is_dict = isinstance(v, dict)
|
||||
if k in _partial:
|
||||
existing_is_dict = isinstance(_partial[k], dict)
|
||||
if val_is_dict != existing_is_dict:
|
||||
# != is XOR
|
||||
raise KeyError("You cannot register a Group and a Value under"
|
||||
" the same name.")
|
||||
if val_is_dict:
|
||||
Config._update_defaults(v, _partial=_partial[k])
|
||||
else:
|
||||
_partial[k] = v
|
||||
else:
|
||||
_partial[k] = v
|
||||
|
||||
def _register_default(self, key: str, **kwargs):
|
||||
if key not in self._defaults:
|
||||
self._defaults[key] = {}
|
||||
|
||||
data = deepcopy(kwargs)
|
||||
|
||||
for k, v in data.items():
|
||||
to_add = self._get_defaults_dict(k, v)
|
||||
self._update_defaults(to_add, self._defaults[key])
|
||||
|
||||
def register_global(self, **kwargs):
|
||||
"""
|
||||
Registers default values for attributes you wish to store in :py:class:`.Config` at a global level.
|
||||
|
||||
You can register a single value or multiple values::
|
||||
|
||||
conf.register_global(
|
||||
foo=True
|
||||
)
|
||||
|
||||
conf.register_global(
|
||||
bar=False,
|
||||
baz=None
|
||||
)
|
||||
|
||||
You can also now register nested values::
|
||||
|
||||
_defaults = {
|
||||
"foo": {
|
||||
"bar": True,
|
||||
"baz": False
|
||||
}
|
||||
}
|
||||
|
||||
# Will register `foo.bar` == True and `foo.baz` == False
|
||||
conf.register_global(
|
||||
**_defaults
|
||||
)
|
||||
|
||||
You can do the same thing without a :python:`_defaults` dict by using double underscore as a variable
|
||||
name separator::
|
||||
|
||||
# This is equivalent to the previous example
|
||||
conf.register_global(
|
||||
foo__bar=True,
|
||||
foo__baz=False
|
||||
)
|
||||
"""
|
||||
self._register_default(self.GLOBAL, **kwargs)
|
||||
|
||||
def register_guild(self, **kwargs):
|
||||
"""
|
||||
Registers default values on a per-guild level. See :py:meth:`register_global` for more details.
|
||||
"""
|
||||
self._register_default(self.GUILD, **kwargs)
|
||||
|
||||
def register_channel(self, **kwargs):
|
||||
"""
|
||||
Registers default values on a per-guild level. See :py:meth:`register_global` for more details.
|
||||
"""
|
||||
# We may need to add a voice channel category later
|
||||
self._register_default(self.CHANNEL, **kwargs)
|
||||
|
||||
def register_role(self, **kwargs):
|
||||
"""
|
||||
Registers default values on a per-guild level. See :py:meth:`register_global` for more details.
|
||||
"""
|
||||
self._register_default(self.ROLE, **kwargs)
|
||||
|
||||
def register_user(self, **kwargs):
|
||||
"""
|
||||
Registers default values on a per-guild level. See :py:meth:`register_global` for more details.
|
||||
"""
|
||||
self._register_default(self.USER, **kwargs)
|
||||
|
||||
def register_member(self, **kwargs):
|
||||
"""
|
||||
Registers default values on a per-guild level. See :py:meth:`register_global` for more details.
|
||||
"""
|
||||
self._register_default(self.MEMBER, **kwargs)
|
||||
|
||||
def _get_base_group(self, key: str, *identifiers: str,
|
||||
group_class=Group) -> Group:
|
||||
# noinspection PyTypeChecker
|
||||
return group_class(
|
||||
identifiers=(self.unique_identifier, key) + identifiers,
|
||||
defaults=self._defaults.get(key, {}),
|
||||
spawner=self.spawner,
|
||||
force_registration=self.force_registration
|
||||
)
|
||||
|
||||
def guild(self, guild: discord.Guild) -> Group:
|
||||
"""
|
||||
Returns a :py:class:`.Group` for the given guild.
|
||||
|
||||
:param discord.Guild guild: A discord.py guild object.
|
||||
"""
|
||||
return self._get_base_group(self.GUILD, guild.id)
|
||||
|
||||
def channel(self, channel: discord.TextChannel) -> Group:
|
||||
"""
|
||||
Returns a :py:class:`.Group` for the given channel. This does not currently support differences between
|
||||
text and voice channels.
|
||||
|
||||
:param discord.TextChannel channel: A discord.py text channel object.
|
||||
"""
|
||||
return self._get_base_group(self.CHANNEL, channel.id)
|
||||
|
||||
def role(self, role: discord.Role) -> Group:
|
||||
"""
|
||||
Returns a :py:class:`.Group` for the given role.
|
||||
|
||||
:param discord.Role role: A discord.py role object.
|
||||
"""
|
||||
return self._get_base_group(self.ROLE, role.id)
|
||||
|
||||
def user(self, user: discord.User) -> Group:
|
||||
"""
|
||||
Returns a :py:class:`.Group` for the given user.
|
||||
|
||||
:param discord.User user: A discord.py user object.
|
||||
"""
|
||||
return self._get_base_group(self.USER, user.id)
|
||||
|
||||
def member(self, member: discord.Member) -> MemberGroup:
|
||||
"""
|
||||
Returns a :py:class:`.MemberGroup` for the given member.
|
||||
|
||||
:param discord.Member member: A discord.py member object.
|
||||
"""
|
||||
return self._get_base_group(self.MEMBER, member.guild.id, member.id,
|
||||
group_class=MemberGroup)
|
||||
|
||||
412
redbot/core/core_commands.py
Normal file
412
redbot/core/core_commands.py
Normal file
@@ -0,0 +1,412 @@
|
||||
import asyncio
|
||||
import importlib
|
||||
import itertools
|
||||
import logging
|
||||
import sys
|
||||
from collections import namedtuple
|
||||
from random import SystemRandom
|
||||
from string import ascii_letters, digits
|
||||
|
||||
import aiohttp
|
||||
import discord
|
||||
from discord.ext import commands
|
||||
|
||||
from redbot.core import checks
|
||||
from redbot.core import i18n
|
||||
|
||||
import redbot.cogs # Don't remove this line or core cogs won't load
|
||||
|
||||
log = logging.getLogger("red")
|
||||
|
||||
OWNER_DISCLAIMER = ("⚠ **Only** the person who is hosting Red should be "
|
||||
"owner. **This has SERIOUS security implications. The "
|
||||
"owner can access any data that is present on the host "
|
||||
"system.** ⚠")
|
||||
|
||||
_ = i18n.CogI18n("Core", __file__)
|
||||
|
||||
|
||||
class Core:
|
||||
"""Commands related to core functions"""
|
||||
|
||||
@commands.command()
|
||||
@checks.is_owner()
|
||||
async def load(self, ctx, *, cog_name: str):
|
||||
"""Loads a package"""
|
||||
try:
|
||||
spec = await ctx.bot.cog_mgr.find_cog(cog_name)
|
||||
except RuntimeError:
|
||||
real_name = ".{}".format(cog_name)
|
||||
try:
|
||||
mod = importlib.import_module(real_name, package='redbot.cogs')
|
||||
except ImportError as e:
|
||||
await ctx.send(_("No module by that name was found in any"
|
||||
" cog path."))
|
||||
return
|
||||
spec = mod.__spec__
|
||||
|
||||
try:
|
||||
ctx.bot.load_extension(spec)
|
||||
except Exception as e:
|
||||
log.exception("Package loading failed", exc_info=e)
|
||||
await ctx.send(_("Failed to load package. Check your console or "
|
||||
"logs for details."))
|
||||
else:
|
||||
await ctx.bot.add_loaded_package(cog_name)
|
||||
await ctx.send(_("Done."))
|
||||
|
||||
@commands.group()
|
||||
@checks.is_owner()
|
||||
async def unload(self, ctx, *, cog_name: str):
|
||||
"""Unloads a package"""
|
||||
if cog_name in ctx.bot.extensions:
|
||||
ctx.bot.unload_extension(cog_name)
|
||||
await ctx.bot.remove_loaded_package(cog_name)
|
||||
await ctx.send(_("Done."))
|
||||
else:
|
||||
await ctx.send(_("That extension is not loaded."))
|
||||
|
||||
@commands.command(name="reload")
|
||||
@checks.is_owner()
|
||||
async def _reload(self, ctx, *, cog_name: str):
|
||||
"""Reloads a package"""
|
||||
ctx.bot.unload_extension(cog_name)
|
||||
self.cleanup_and_refresh_modules(cog_name)
|
||||
try:
|
||||
spec = await ctx.bot.cog_mgr.find_cog(cog_name)
|
||||
ctx.bot.load_extension(spec)
|
||||
except Exception as e:
|
||||
log.exception("Package reloading failed", exc_info=e)
|
||||
await ctx.send(_("Failed to reload package. Check your console or "
|
||||
"logs for details."))
|
||||
else:
|
||||
curr_pkgs = await ctx.bot.db.packages()
|
||||
await ctx.bot.save_packages_status(curr_pkgs)
|
||||
await ctx.send(_("Done."))
|
||||
|
||||
@commands.command(name="shutdown")
|
||||
@checks.is_owner()
|
||||
async def _shutdown(self, ctx, silently: bool=False):
|
||||
"""Shuts down the bot"""
|
||||
wave = "\N{WAVING HAND SIGN}"
|
||||
skin = "\N{EMOJI MODIFIER FITZPATRICK TYPE-3}"
|
||||
try: # We don't want missing perms to stop our shutdown
|
||||
if not silently:
|
||||
await ctx.send(_("Shutting down... ") + wave + skin)
|
||||
except:
|
||||
pass
|
||||
await ctx.bot.shutdown()
|
||||
|
||||
def cleanup_and_refresh_modules(self, module_name: str):
|
||||
"""Interally reloads modules so that changes are detected"""
|
||||
splitted = module_name.split('.')
|
||||
|
||||
def maybe_reload(new_name):
|
||||
try:
|
||||
lib = sys.modules[new_name]
|
||||
except KeyError:
|
||||
pass
|
||||
else:
|
||||
importlib._bootstrap._exec(lib.__spec__, lib)
|
||||
|
||||
modules = itertools.accumulate(splitted, lambda old, next: "{}.{}".format(old, next))
|
||||
for m in modules:
|
||||
maybe_reload(m)
|
||||
|
||||
children = {name: lib for name, lib in sys.modules.items() if name.startswith(module_name)}
|
||||
for child_name, lib in children.items():
|
||||
importlib._bootstrap._exec(lib.__spec__, lib)
|
||||
|
||||
@commands.group(name="set")
|
||||
async def _set(self, ctx):
|
||||
"""Changes Red's settings"""
|
||||
if ctx.invoked_subcommand is None:
|
||||
await ctx.bot.send_cmd_help(ctx)
|
||||
|
||||
@_set.command()
|
||||
@checks.guildowner()
|
||||
@commands.guild_only()
|
||||
async def adminrole(self, ctx, *, role: discord.Role):
|
||||
"""Sets the admin role for this server"""
|
||||
await ctx.bot.db.guild(ctx.guild).admin_role.set(role.id)
|
||||
await ctx.send(_("The admin role for this guild has been set."))
|
||||
|
||||
@_set.command()
|
||||
@checks.guildowner()
|
||||
@commands.guild_only()
|
||||
async def modrole(self, ctx, *, role: discord.Role):
|
||||
"""Sets the mod role for this server"""
|
||||
await ctx.bot.db.guild(ctx.guild).mod_role.set(role.id)
|
||||
await ctx.send(_("The mod role for this guild has been set."))
|
||||
|
||||
@_set.command()
|
||||
@checks.is_owner()
|
||||
async def avatar(self, ctx, url: str):
|
||||
"""Sets Red's avatar"""
|
||||
session = aiohttp.ClientSession()
|
||||
async with session.get(url) as r:
|
||||
data = await r.read()
|
||||
await session.close()
|
||||
|
||||
try:
|
||||
await ctx.bot.user.edit(avatar=data)
|
||||
except discord.HTTPException:
|
||||
await ctx.send(_("Failed. Remember that you can edit my avatar "
|
||||
"up to two times a hour. The URL must be a "
|
||||
"direct link to a JPG / PNG."))
|
||||
except discord.InvalidArgument:
|
||||
await ctx.send(_("JPG / PNG format only."))
|
||||
else:
|
||||
await ctx.send(_("Done."))
|
||||
|
||||
@_set.command(name="game")
|
||||
@checks.is_owner()
|
||||
@commands.guild_only()
|
||||
async def _game(self, ctx, *, game: str):
|
||||
"""Sets Red's playing status"""
|
||||
status = ctx.me.status
|
||||
game = discord.Game(name=game)
|
||||
await ctx.bot.change_presence(status=status, game=game)
|
||||
await ctx.send(_("Game set."))
|
||||
|
||||
@_set.command()
|
||||
@checks.is_owner()
|
||||
@commands.guild_only()
|
||||
async def status(self, ctx, *, status: str):
|
||||
"""Sets Red's status
|
||||
Available statuses:
|
||||
online
|
||||
idle
|
||||
dnd
|
||||
invisible"""
|
||||
|
||||
statuses = {
|
||||
"online" : discord.Status.online,
|
||||
"idle" : discord.Status.idle,
|
||||
"dnd" : discord.Status.dnd,
|
||||
"invisible" : discord.Status.invisible
|
||||
}
|
||||
|
||||
game = ctx.me.game
|
||||
|
||||
try:
|
||||
status = statuses[status.lower()]
|
||||
except KeyError:
|
||||
await ctx.bot.send_cmd_help(ctx)
|
||||
else:
|
||||
await ctx.bot.change_presence(status=status,
|
||||
game=game)
|
||||
await ctx.send(_("Status changed to %s.") % status)
|
||||
|
||||
@_set.command()
|
||||
@checks.is_owner()
|
||||
@commands.guild_only()
|
||||
async def stream(self, ctx, streamer=None, *, stream_title=None):
|
||||
"""Sets Red's streaming status
|
||||
Leaving both streamer and stream_title empty will clear it."""
|
||||
|
||||
status = ctx.me.status
|
||||
|
||||
if stream_title:
|
||||
stream_title = stream_title.strip()
|
||||
if "twitch.tv/" not in streamer:
|
||||
streamer = "https://www.twitch.tv/" + streamer
|
||||
game = discord.Game(type=1, url=streamer, name=stream_title)
|
||||
await ctx.bot.change_presence(game=game, status=status)
|
||||
elif streamer is not None:
|
||||
await ctx.bot.send_cmd_help(ctx)
|
||||
return
|
||||
else:
|
||||
await ctx.bot.change_presence(game=None, status=status)
|
||||
await ctx.send(_("Done."))
|
||||
|
||||
@_set.command(name="username", aliases=["name"])
|
||||
@checks.is_owner()
|
||||
async def _username(self, ctx, *, username: str):
|
||||
"""Sets Red's username"""
|
||||
try:
|
||||
await ctx.bot.user.edit(username=username)
|
||||
except discord.HTTPException:
|
||||
await ctx.send(_("Failed to change name. Remember that you can "
|
||||
"only do it up to 2 times an hour. Use "
|
||||
"nicknames if you need frequent changes. "
|
||||
"`{}set nickname`").format(ctx.prefix))
|
||||
else:
|
||||
await ctx.send(_("Done."))
|
||||
|
||||
@_set.command(name="nickname")
|
||||
@checks.admin()
|
||||
@commands.guild_only()
|
||||
async def _nickname(self, ctx, *, nickname: str):
|
||||
"""Sets Red's nickname"""
|
||||
try:
|
||||
await ctx.bot.user.edit(nick=nickname)
|
||||
except discord.Forbidden:
|
||||
await ctx.send(_("I lack the permissions to change my own "
|
||||
"nickname."))
|
||||
else:
|
||||
await ctx.send("Done.")
|
||||
|
||||
@_set.command(aliases=["prefixes"])
|
||||
@checks.is_owner()
|
||||
async def prefix(self, ctx, *prefixes):
|
||||
"""Sets Red's global prefix(es)"""
|
||||
if not prefixes:
|
||||
await ctx.bot.send_cmd_help(ctx)
|
||||
return
|
||||
prefixes = sorted(prefixes, reverse=True)
|
||||
await ctx.bot.db.prefix.set(prefixes)
|
||||
await ctx.send(_("Prefix set."))
|
||||
|
||||
@_set.command(aliases=["serverprefixes"])
|
||||
@checks.admin()
|
||||
@commands.guild_only()
|
||||
async def serverprefix(self, ctx, *prefixes):
|
||||
"""Sets Red's server prefix(es)"""
|
||||
if not prefixes:
|
||||
await ctx.bot.db.guild(ctx.guild).prefix.set([])
|
||||
await ctx.send(_("Guild prefixes have been reset."))
|
||||
return
|
||||
prefixes = sorted(prefixes, reverse=True)
|
||||
await ctx.bot.db.guild(ctx.guild).prefix.set(prefixes)
|
||||
await ctx.send(_("Prefix set."))
|
||||
|
||||
@_set.command()
|
||||
@commands.cooldown(1, 60 * 10, commands.BucketType.default)
|
||||
async def owner(self, ctx):
|
||||
"""Sets Red's main owner"""
|
||||
def check(m):
|
||||
return m.author == ctx.author and m.channel == ctx.channel
|
||||
|
||||
# According to the Python docs this is suitable for cryptographic use
|
||||
random = SystemRandom()
|
||||
length = random.randint(25, 35)
|
||||
chars = ascii_letters + digits
|
||||
token = ""
|
||||
|
||||
for i in range(length):
|
||||
token += random.choice(chars)
|
||||
log.info("{0} ({0.id}) requested to be set as owner."
|
||||
"".format(ctx.author))
|
||||
print(_("\nVerification token:"))
|
||||
print(token)
|
||||
|
||||
await ctx.send(_("Remember:\n") + OWNER_DISCLAIMER)
|
||||
await asyncio.sleep(5)
|
||||
|
||||
await ctx.send(_("I have printed a one-time token in the console. "
|
||||
"Copy and paste it here to confirm you are the owner."))
|
||||
|
||||
try:
|
||||
message = await ctx.bot.wait_for("message", check=check,
|
||||
timeout=60)
|
||||
except asyncio.TimeoutError:
|
||||
self.owner.reset_cooldown(ctx)
|
||||
await ctx.send(_("The set owner request has timed out."))
|
||||
else:
|
||||
if message.content.strip() == token:
|
||||
self.owner.reset_cooldown(ctx)
|
||||
await ctx.bot.db.owner.set(ctx.author.id)
|
||||
ctx.bot.owner_id = ctx.author.id
|
||||
await ctx.send(_("You have been set as owner."))
|
||||
else:
|
||||
await ctx.send(_("Invalid token."))
|
||||
|
||||
@_set.command()
|
||||
@checks.is_owner()
|
||||
async def locale(self, ctx: commands.Context, locale_name: str):
|
||||
"""
|
||||
Changes bot locale.
|
||||
"""
|
||||
i18n.set_locale(locale_name)
|
||||
|
||||
await ctx.bot.db.locale.set(locale_name)
|
||||
|
||||
await ctx.send(_("Locale has been set."))
|
||||
|
||||
@commands.command()
|
||||
@commands.cooldown(1, 60, commands.BucketType.user)
|
||||
async def contact(self, ctx, *, message: str):
|
||||
"""Sends a message to the owner"""
|
||||
guild = ctx.message.guild
|
||||
owner = discord.utils.get(ctx.bot.get_all_members(),
|
||||
id=ctx.bot.owner_id)
|
||||
author = ctx.message.author
|
||||
footer = _("User ID: %s") % author.id
|
||||
|
||||
if ctx.guild is None:
|
||||
source = _("through DM")
|
||||
else:
|
||||
source = _("from {}").format(guild)
|
||||
footer += _(" | Server ID: %s") % guild.id
|
||||
|
||||
# We need to grab the DM command prefix (global)
|
||||
# Since it can also be set through cli flags, bot.db is not a reliable
|
||||
# source. So we'll just mock a DM message instead.
|
||||
fake_message = namedtuple('Message', 'guild')
|
||||
prefixes = await ctx.bot.command_prefix(ctx.bot, fake_message(guild=None))
|
||||
prefix = prefixes[0]
|
||||
|
||||
content = _("Use `{}dm {} <text>` to reply to this user"
|
||||
"").format(prefix, author.id)
|
||||
|
||||
if isinstance(author, discord.Member):
|
||||
colour = author.colour
|
||||
else:
|
||||
colour = discord.Colour.red()
|
||||
|
||||
description = _("Sent by {} {}").format(author, source)
|
||||
|
||||
e = discord.Embed(colour=colour, description=message)
|
||||
if author.avatar_url:
|
||||
e.set_author(name=description, icon_url=author.avatar_url)
|
||||
else:
|
||||
e.set_author(name=description)
|
||||
e.set_footer(text=footer)
|
||||
|
||||
try:
|
||||
await owner.send(content, embed=e)
|
||||
except discord.InvalidArgument:
|
||||
await ctx.send(_("I cannot send your message, I'm unable to find "
|
||||
"my owner... *sigh*"))
|
||||
except:
|
||||
await ctx.send(_("I'm unable to deliver your message. Sorry."))
|
||||
else:
|
||||
await ctx.send(_("Your message has been sent."))
|
||||
|
||||
@commands.command()
|
||||
@checks.is_owner()
|
||||
async def dm(self, ctx, user_id: int, *, message: str):
|
||||
"""Sends a DM to a user
|
||||
|
||||
This command needs a user id to work.
|
||||
To get a user id enable 'developer mode' in Discord's
|
||||
settings, 'appearance' tab. Then right click a user
|
||||
and copy their id"""
|
||||
destination = discord.utils.get(ctx.bot.get_all_members(),
|
||||
id=user_id)
|
||||
if destination is None:
|
||||
await ctx.send(_("Invalid ID or user not found. You can only "
|
||||
"send messages to people I share a server "
|
||||
"with."))
|
||||
return
|
||||
|
||||
e = discord.Embed(colour=discord.Colour.red(), description=message)
|
||||
description = _("Owner of %s") % ctx.bot.user
|
||||
fake_message = namedtuple('Message', 'guild')
|
||||
prefixes = await ctx.bot.command_prefix(ctx.bot, fake_message(guild=None))
|
||||
prefix = prefixes[0]
|
||||
e.set_footer(text=_("You can reply to this message with %scontact"
|
||||
"") % prefix)
|
||||
if ctx.bot.user.avatar_url:
|
||||
e.set_author(name=description, icon_url=ctx.bot.user.avatar_url)
|
||||
else:
|
||||
e.set_author(name=description)
|
||||
|
||||
try:
|
||||
await destination.send(embed=e)
|
||||
except:
|
||||
await ctx.send(_("Sorry, I couldn't deliver your message "
|
||||
"to %s") % destination)
|
||||
else:
|
||||
await ctx.send(_("Message delivered to %s") % destination)
|
||||
74
redbot/core/data_manager.py
Normal file
74
redbot/core/data_manager.py
Normal file
@@ -0,0 +1,74 @@
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
import appdirs
|
||||
|
||||
from .json_io import JsonIO
|
||||
|
||||
jsonio = None
|
||||
basic_config = None
|
||||
|
||||
basic_config_default = {
|
||||
"DATA_PATH": None,
|
||||
"COG_PATH_APPEND": "cogs",
|
||||
"CORE_PATH_APPEND": "core"
|
||||
}
|
||||
|
||||
config_dir = Path(appdirs.AppDirs("Red-DiscordBot").user_config_dir)
|
||||
config_file = config_dir / 'config.json'
|
||||
|
||||
|
||||
def load_basic_configuration(instance_name: str):
|
||||
global jsonio
|
||||
global basic_config
|
||||
|
||||
jsonio = JsonIO(config_file)
|
||||
|
||||
try:
|
||||
config = jsonio._load_json()
|
||||
basic_config = config[instance_name]
|
||||
except (FileNotFoundError, KeyError):
|
||||
print("You need to configure the bot instance using `redbot-setup`"
|
||||
" prior to running the bot.")
|
||||
sys.exit(1)
|
||||
|
||||
|
||||
def _base_data_path() -> Path:
|
||||
if basic_config is None:
|
||||
raise RuntimeError("You must load the basic config before you"
|
||||
" can get the base data path.")
|
||||
path = basic_config['DATA_PATH']
|
||||
return Path(path).resolve()
|
||||
|
||||
|
||||
def cog_data_path(cog_instance=None) -> Path:
|
||||
"""
|
||||
Gets the base cog data path. If you want to get the folder with
|
||||
which to store your own cog's data please pass in an instance
|
||||
of your cog class.
|
||||
:param cog_instance:
|
||||
:return:
|
||||
"""
|
||||
try:
|
||||
base_data_path = Path(_base_data_path())
|
||||
except RuntimeError as e:
|
||||
raise RuntimeError("You must load the basic config before you"
|
||||
" can get the cog data path.") from e
|
||||
cog_path = base_data_path / basic_config['COG_PATH_APPEND']
|
||||
if cog_instance:
|
||||
cog_path = cog_path / cog_instance.__class__.__name__
|
||||
cog_path.mkdir(exist_ok=True, parents=True)
|
||||
|
||||
return cog_path.resolve()
|
||||
|
||||
|
||||
def core_data_path() -> Path:
|
||||
try:
|
||||
base_data_path = Path(_base_data_path())
|
||||
except RuntimeError as e:
|
||||
raise RuntimeError("You must load the basic config before you"
|
||||
" can get the core data path.") from e
|
||||
core_path = base_data_path / basic_config['CORE_PATH_APPEND']
|
||||
core_path.mkdir(exist_ok=True, parents=True)
|
||||
|
||||
return core_path.resolve()
|
||||
271
redbot/core/dev_commands.py
Normal file
271
redbot/core/dev_commands.py
Normal file
@@ -0,0 +1,271 @@
|
||||
import asyncio
|
||||
import inspect
|
||||
import io
|
||||
import textwrap
|
||||
import traceback
|
||||
from contextlib import redirect_stdout
|
||||
|
||||
import discord
|
||||
from . import checks
|
||||
from .i18n import CogI18n
|
||||
from discord.ext import commands
|
||||
|
||||
from .utils.chat_formatting import box, pagify
|
||||
|
||||
"""
|
||||
Notice:
|
||||
|
||||
95% of the below code came from R.Danny which can be found here:
|
||||
|
||||
https://github.com/Rapptz/RoboDanny/blob/master/cogs/repl.py
|
||||
"""
|
||||
|
||||
_ = CogI18n("Dev", __file__)
|
||||
|
||||
|
||||
class Dev:
|
||||
"""Various development focused utilities"""
|
||||
def __init__(self):
|
||||
self._last_result = None
|
||||
self.sessions = set()
|
||||
|
||||
@staticmethod
|
||||
def cleanup_code(content):
|
||||
"""Automatically removes code blocks from the code."""
|
||||
# remove ```py\n```
|
||||
if content.startswith('```') and content.endswith('```'):
|
||||
return '\n'.join(content.split('\n')[1:-1])
|
||||
|
||||
# remove `foo`
|
||||
return content.strip('` \n')
|
||||
|
||||
@staticmethod
|
||||
def get_syntax_error(e):
|
||||
if e.text is None:
|
||||
return '```py\n{0.__class__.__name__}: {0}\n```'.format(e)
|
||||
return '```py\n{0.text}{1:>{0.offset}}\n{2}: {0}```'.format(e, '^', type(e).__name__)
|
||||
|
||||
@staticmethod
|
||||
def sanitize_output(ctx: commands.Context, input: str) -> str:
|
||||
token = ctx.bot.http.token
|
||||
r = "[EXPUNGED]"
|
||||
result = input.replace(token, r)
|
||||
result = result.replace(token.lower(), r)
|
||||
result = result.replace(token.upper(), r)
|
||||
return result
|
||||
|
||||
@commands.command()
|
||||
@checks.is_owner()
|
||||
async def debug(self, ctx, *, code):
|
||||
"""
|
||||
Executes code and prints the result to discord.
|
||||
"""
|
||||
env = {
|
||||
'bot': ctx.bot,
|
||||
'ctx': ctx,
|
||||
'channel': ctx.channel,
|
||||
'author': ctx.author,
|
||||
'guild': ctx.guild,
|
||||
'message': ctx.message
|
||||
}
|
||||
|
||||
code = self.cleanup_code(code)
|
||||
|
||||
try:
|
||||
result = eval(code, env, locals())
|
||||
except SyntaxError as e:
|
||||
await ctx.send(self.get_syntax_error(e))
|
||||
return
|
||||
except Exception as e:
|
||||
await ctx.send('```py\n{}: {}```'.format(type(e).__name__, str(e)), )
|
||||
return
|
||||
|
||||
if asyncio.iscoroutine(result):
|
||||
result = await result
|
||||
|
||||
result = str(result)
|
||||
|
||||
result = self.sanitize_output(ctx, result)
|
||||
|
||||
await ctx.send(box(result, lang="py"))
|
||||
|
||||
@commands.command(name='eval')
|
||||
@checks.is_owner()
|
||||
async def _eval(self, ctx, *, body: str):
|
||||
"""
|
||||
Executes code as if it was the body of an async function
|
||||
code MUST be in a code block using three ticks and
|
||||
there MUST be a newline after the first set and
|
||||
before the last set. This function will ONLY output
|
||||
the return value of the function code AND anything
|
||||
that is output to stdout (e.g. using a print()
|
||||
statement).
|
||||
"""
|
||||
env = {
|
||||
'bot': ctx.bot,
|
||||
'ctx': ctx,
|
||||
'channel': ctx.channel,
|
||||
'author': ctx.author,
|
||||
'guild': ctx.guild,
|
||||
'message': ctx.message,
|
||||
'_': self._last_result
|
||||
}
|
||||
|
||||
env.update(globals())
|
||||
|
||||
body = self.cleanup_code(body)
|
||||
stdout = io.StringIO()
|
||||
|
||||
to_compile = 'async def func():\n%s' % textwrap.indent(body, ' ')
|
||||
|
||||
try:
|
||||
exec(to_compile, env)
|
||||
except SyntaxError as e:
|
||||
return await ctx.send(self.get_syntax_error(e))
|
||||
|
||||
func = env['func']
|
||||
try:
|
||||
with redirect_stdout(stdout):
|
||||
ret = await func()
|
||||
except:
|
||||
value = stdout.getvalue()
|
||||
await ctx.send(box('\n{}{}'.format(value, traceback.format_exc()), lang="py"))
|
||||
else:
|
||||
value = stdout.getvalue()
|
||||
try:
|
||||
await ctx.bot.add_reaction(ctx.message, '\u2705')
|
||||
except:
|
||||
pass
|
||||
|
||||
if ret is None:
|
||||
if value:
|
||||
value = self.sanitize_output(ctx, value)
|
||||
await ctx.send(box(value, lang="py"))
|
||||
else:
|
||||
ret = self.sanitize_output(ctx, ret)
|
||||
self._last_result = ret
|
||||
await ctx.send(box("{}{}".format(value, ret), lang="py"))
|
||||
|
||||
@commands.command()
|
||||
@checks.is_owner()
|
||||
async def repl(self, ctx):
|
||||
"""
|
||||
Opens an interactive REPL.
|
||||
"""
|
||||
variables = {
|
||||
'ctx': ctx,
|
||||
'bot': ctx.bot,
|
||||
'message': ctx.message,
|
||||
'guild': ctx.guild,
|
||||
'channel': ctx.channel,
|
||||
'author': ctx.author,
|
||||
'_': None,
|
||||
}
|
||||
|
||||
if ctx.channel.id in self.sessions:
|
||||
await ctx.send(_('Already running a REPL session in this channel. Exit it with `quit`.'))
|
||||
return
|
||||
|
||||
self.sessions.add(ctx.channel.id)
|
||||
await ctx.send(_('Enter code to execute or evaluate. `exit()` or `quit` to exit.'))
|
||||
|
||||
def msg_check(m):
|
||||
return m.author == ctx.author and m.channel == ctx.channel and \
|
||||
m.content.startswith('`')
|
||||
|
||||
while True:
|
||||
response = await ctx.bot.wait_for(
|
||||
"message",
|
||||
check=msg_check)
|
||||
|
||||
cleaned = self.cleanup_code(response.content)
|
||||
|
||||
if cleaned in ('quit', 'exit', 'exit()'):
|
||||
await ctx.send('Exiting.')
|
||||
self.sessions.remove(ctx.channel.id)
|
||||
return
|
||||
|
||||
executor = exec
|
||||
if cleaned.count('\n') == 0:
|
||||
# single statement, potentially 'eval'
|
||||
try:
|
||||
code = compile(cleaned, '<repl session>', 'eval')
|
||||
except SyntaxError:
|
||||
pass
|
||||
else:
|
||||
executor = eval
|
||||
|
||||
if executor is exec:
|
||||
try:
|
||||
code = compile(cleaned, '<repl session>', 'exec')
|
||||
except SyntaxError as e:
|
||||
await ctx.send(self.get_syntax_error(e))
|
||||
continue
|
||||
|
||||
variables['message'] = response
|
||||
|
||||
stdout = io.StringIO()
|
||||
|
||||
msg = None
|
||||
|
||||
try:
|
||||
with redirect_stdout(stdout):
|
||||
result = executor(code, variables)
|
||||
if inspect.isawaitable(result):
|
||||
result = await result
|
||||
except:
|
||||
value = stdout.getvalue()
|
||||
value = self.sanitize_output(ctx, value)
|
||||
msg = "{}{}".format(value, traceback.format_exc())
|
||||
else:
|
||||
value = stdout.getvalue()
|
||||
if result is not None:
|
||||
msg = "{}{}".format(value, result)
|
||||
variables['_'] = result
|
||||
elif value:
|
||||
msg = "{}".format(value)
|
||||
|
||||
try:
|
||||
for page in pagify(str(msg), shorten_by=12):
|
||||
page = self.sanitize_output(ctx, page)
|
||||
await ctx.send(box(page, "py"))
|
||||
except discord.Forbidden:
|
||||
pass
|
||||
except discord.HTTPException as e:
|
||||
await ctx.send(_('Unexpected error: `{}`').format(e))
|
||||
|
||||
@commands.command()
|
||||
@checks.is_owner()
|
||||
async def mock(self, ctx, user: discord.Member, *, command):
|
||||
"""Runs a command as if it was issued by a different user
|
||||
|
||||
The prefix must not be entered"""
|
||||
# Since we have stateful objects now this might be pretty bad
|
||||
# Sorry Danny
|
||||
old_author = ctx.author
|
||||
old_content = ctx.message.content
|
||||
ctx.message.author = user
|
||||
ctx.message.content = ctx.prefix + command
|
||||
|
||||
await ctx.bot.process_commands(ctx.message)
|
||||
|
||||
ctx.message.author = old_author
|
||||
ctx.message.content = old_content
|
||||
|
||||
@commands.command(name="mockmsg")
|
||||
@checks.is_owner()
|
||||
async def mock_msg(self, ctx, user: discord.Member, *, content: str):
|
||||
"""Bot receives a message is if it were sent by a different user.
|
||||
|
||||
Only reads the raw content of the message. Attachments, embeds etc. are ignored."""
|
||||
old_author = ctx.author
|
||||
old_content = ctx.message.content
|
||||
ctx.message.author = user
|
||||
ctx.message.content = content
|
||||
|
||||
ctx.bot.dispatch("message", ctx.message)
|
||||
|
||||
await asyncio.sleep(2) # If we change the author and content back too quickly,
|
||||
# the bot won't process the mocked message in time.
|
||||
ctx.message.author = old_author
|
||||
ctx.message.content = old_content
|
||||
0
redbot/core/drivers/__init__.py
Normal file
0
redbot/core/drivers/__init__.py
Normal file
12
redbot/core/drivers/red_base.py
Normal file
12
redbot/core/drivers/red_base.py
Normal file
@@ -0,0 +1,12 @@
|
||||
from typing import Tuple
|
||||
|
||||
|
||||
class BaseDriver:
|
||||
def get_driver(self):
|
||||
raise NotImplementedError
|
||||
|
||||
async def get(self, identifiers: Tuple[str]):
|
||||
raise NotImplementedError
|
||||
|
||||
async def set(self, identifiers: Tuple[str], value):
|
||||
raise NotImplementedError
|
||||
49
redbot/core/drivers/red_json.py
Normal file
49
redbot/core/drivers/red_json.py
Normal file
@@ -0,0 +1,49 @@
|
||||
from pathlib import Path
|
||||
from typing import Tuple
|
||||
|
||||
from ..json_io import JsonIO
|
||||
|
||||
from .red_base import BaseDriver
|
||||
|
||||
|
||||
class JSON(BaseDriver):
|
||||
def __init__(self, cog_name, *, data_path_override: Path=None,
|
||||
file_name_override: str="settings.json"):
|
||||
super().__init__()
|
||||
self.cog_name = cog_name
|
||||
self.file_name = file_name_override
|
||||
if data_path_override:
|
||||
self.data_path = data_path_override
|
||||
else:
|
||||
self.data_path = Path.cwd() / 'cogs' / '.data' / self.cog_name
|
||||
|
||||
self.data_path.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
self.data_path = self.data_path / self.file_name
|
||||
|
||||
self.jsonIO = JsonIO(self.data_path)
|
||||
|
||||
try:
|
||||
self.data = self.jsonIO._load_json()
|
||||
except FileNotFoundError:
|
||||
self.data = {}
|
||||
self.jsonIO._save_json(self.data)
|
||||
|
||||
def get_driver(self):
|
||||
return self
|
||||
|
||||
async def get(self, identifiers: Tuple[str]):
|
||||
partial = self.data
|
||||
for i in identifiers:
|
||||
partial = partial[i]
|
||||
return partial
|
||||
|
||||
async def set(self, identifiers, value):
|
||||
partial = self.data
|
||||
for i in identifiers[:-1]:
|
||||
if i not in partial:
|
||||
partial[i] = {}
|
||||
partial = partial[i]
|
||||
|
||||
partial[identifiers[-1]] = value
|
||||
await self.jsonIO._threadsafe_save_json(self.data)
|
||||
211
redbot/core/drivers/red_mongo.py
Normal file
211
redbot/core/drivers/red_mongo.py
Normal file
@@ -0,0 +1,211 @@
|
||||
import pymongo as m
|
||||
from .red_base import BaseDriver
|
||||
|
||||
|
||||
class RedMongoException(Exception):
|
||||
"""Base Red Mongo Exception class"""
|
||||
pass
|
||||
|
||||
|
||||
class MultipleMatches(RedMongoException):
|
||||
"""Raised when multiple documents match a single cog_name and
|
||||
cog_identifier pair."""
|
||||
pass
|
||||
|
||||
|
||||
class MissingCollection(RedMongoException):
|
||||
"""Raised when a collection is missing from the mongo db"""
|
||||
pass
|
||||
|
||||
|
||||
class Mongo(BaseDriver):
|
||||
def __init__(self, host, port=27017, admin_user=None, admin_pass=None,
|
||||
**kwargs):
|
||||
self.conn = m.MongoClient(host=host, port=port, **kwargs)
|
||||
|
||||
self.admin_user = admin_user
|
||||
self.admin_pass = admin_pass
|
||||
|
||||
self._db = self.conn.red
|
||||
if self.admin_user is not None and self.admin_pass is not None:
|
||||
self._db.authenticate(self.admin_user, self.admin_pass)
|
||||
|
||||
self._global = self._db.GLOBAL
|
||||
self._guild = self._db.GUILD
|
||||
self._channel = self._db.CHANNEL
|
||||
self._role = self._db.ROLE
|
||||
self._member = self._db.MEMBER
|
||||
self._user = self._db.USER
|
||||
|
||||
def get_global(self, cog_name, cog_identifier, _, key, *, default=None):
|
||||
doc = self._global.find(
|
||||
{"cog_name": cog_name, "cog_identifier": cog_identifier},
|
||||
projection=[key, ], batch_size=2)
|
||||
if doc.count() == 2:
|
||||
raise MultipleMatches("Too many matching documents at the GLOBAL"
|
||||
" level: ({}, {})".format(cog_name,
|
||||
cog_identifier))
|
||||
elif doc.count() == 1:
|
||||
return doc[0].get(key, default)
|
||||
return default
|
||||
|
||||
def get_guild(self, cog_name, cog_identifier, guild_id, key, *,
|
||||
default=None):
|
||||
doc = self._guild.find(
|
||||
{"cog_name": cog_name, "cog_identifier": cog_identifier,
|
||||
"guild_id": guild_id},
|
||||
projection=[key, ], batch_size=2)
|
||||
if doc.count() == 2:
|
||||
raise MultipleMatches("Too many matching documents at the GUILD"
|
||||
" level: ({}, {}, {})".format(
|
||||
cog_name, cog_identifier, guild_id))
|
||||
elif doc.count() == 1:
|
||||
return doc[0].get(key, default)
|
||||
return default
|
||||
|
||||
def get_channel(self, cog_name, cog_identifier, channel_id, key, *,
|
||||
default=None):
|
||||
doc = self._channel.find(
|
||||
{"cog_name": cog_name, "cog_identifier": cog_identifier,
|
||||
"channel_id": channel_id},
|
||||
projection=[key, ], batch_size=2)
|
||||
if doc.count() == 2:
|
||||
raise MultipleMatches("Too many matching documents at the CHANNEL"
|
||||
" level: ({}, {}, {})".format(
|
||||
cog_name, cog_identifier, channel_id))
|
||||
elif doc.count() == 1:
|
||||
return doc[0].get(key, default)
|
||||
return default
|
||||
|
||||
def get_role(self, cog_name, cog_identifier, role_id, key, *,
|
||||
default=None):
|
||||
doc = self._role.find(
|
||||
{"cog_name": cog_name, "cog_identifier": cog_identifier,
|
||||
"role_id": role_id},
|
||||
projection=[key, ], batch_size=2)
|
||||
if doc.count() == 2:
|
||||
raise MultipleMatches("Too many matching documents at the ROLE"
|
||||
" level: ({}, {}, {})".format(
|
||||
cog_name, cog_identifier, role_id))
|
||||
elif doc.count() == 1:
|
||||
return doc[0].get(key, default)
|
||||
return default
|
||||
|
||||
def get_member(self, cog_name, cog_identifier, user_id, guild_id, key, *,
|
||||
default=None):
|
||||
doc = self._member.find(
|
||||
{"cog_name": cog_name, "cog_identifier": cog_identifier,
|
||||
"user_id": user_id, "guild_id": guild_id},
|
||||
projection=[key, ], batch_size=2)
|
||||
if doc.count() == 2:
|
||||
raise MultipleMatches("Too many matching documents at the MEMBER"
|
||||
" level: ({}, {}, mid {}, sid {})".format(
|
||||
cog_name, cog_identifier, user_id,
|
||||
guild_id))
|
||||
elif doc.count() == 1:
|
||||
return doc[0].get(key, default)
|
||||
return default
|
||||
|
||||
def get_user(self, cog_name, cog_identifier, user_id, key, *,
|
||||
default=None):
|
||||
doc = self._user.find(
|
||||
{"cog_name": cog_name, "cog_identifier": cog_identifier,
|
||||
"user_id": user_id},
|
||||
projection=[key, ], batch_size=2)
|
||||
if doc.count() == 2:
|
||||
raise MultipleMatches("Too many matching documents at the USER"
|
||||
" level: ({}, {}, mid {})".format(
|
||||
cog_name, cog_identifier, user_id))
|
||||
elif doc.count() == 1:
|
||||
return doc[0].get(key, default)
|
||||
else:
|
||||
return default
|
||||
|
||||
def set_global(self, cog_name, cog_identifier, key, value, clear=False):
|
||||
filter = {"cog_name": cog_name, "cog_identifier": cog_identifier}
|
||||
data = {"$set": {key: value}}
|
||||
if self._global.count(filter) > 1:
|
||||
raise MultipleMatches("Too many matching documents at the GLOBAL"
|
||||
" level: ({}, {})".format(cog_name,
|
||||
cog_identifier))
|
||||
else:
|
||||
if clear:
|
||||
self._global.delete_one(filter)
|
||||
else:
|
||||
self._global.update_one(filter, data, upsert=True)
|
||||
|
||||
def set_guild(self, cog_name, cog_identifier, guild_id, key, value,
|
||||
clear=False):
|
||||
filter = {"cog_name": cog_name, "cog_identifier": cog_identifier,
|
||||
"guild_id": guild_id}
|
||||
data = {"$set": {key: value}}
|
||||
if self._guild.count(filter) > 1:
|
||||
raise MultipleMatches("Too many matching documents at the GUILD"
|
||||
" level: ({}, {}, {})".format(
|
||||
cog_name, cog_identifier, guild_id))
|
||||
else:
|
||||
if clear:
|
||||
self._guild.delete_one(filter)
|
||||
else:
|
||||
self._guild.update_one(filter, data, upsert=True)
|
||||
|
||||
def set_channel(self, cog_name, cog_identifier, channel_id, key, value,
|
||||
clear=False):
|
||||
filter = {"cog_name": cog_name, "cog_identifier": cog_identifier,
|
||||
"channel_id": channel_id}
|
||||
data = {"$set": {key: value}}
|
||||
if self._channel.count(filter) > 1:
|
||||
raise MultipleMatches("Too many matching documents at the CHANNEL"
|
||||
" level: ({}, {}, {})".format(
|
||||
cog_name, cog_identifier, channel_id))
|
||||
else:
|
||||
if clear:
|
||||
self._channel.delete_one(filter)
|
||||
else:
|
||||
self._channel.update_one(filter, data, upsert=True)
|
||||
|
||||
def set_role(self, cog_name, cog_identifier, role_id, key, value,
|
||||
clear=False):
|
||||
filter = {"cog_name": cog_name, "cog_identifier": cog_identifier,
|
||||
"role_id": role_id}
|
||||
data = {"$set": {key: value}}
|
||||
if self._role.count(filter) > 1:
|
||||
raise MultipleMatches("Too many matching documents at the ROLE"
|
||||
" level: ({}, {}, {})".format(
|
||||
cog_name, cog_identifier, role_id))
|
||||
else:
|
||||
if clear:
|
||||
self._role.delete_one(filter)
|
||||
else:
|
||||
self._role.update_one(filter, data, upsert=True)
|
||||
|
||||
def set_member(self, cog_name, cog_identifier, user_id, guild_id, key,
|
||||
value, clear=False):
|
||||
filter = {"cog_name": cog_name, "cog_identifier": cog_identifier,
|
||||
"guild_id": guild_id, "user_id": user_id}
|
||||
data = {"$set": {key: value}}
|
||||
if self._member.count(filter) > 1:
|
||||
raise MultipleMatches("Too many matching documents at the MEMBER"
|
||||
" level: ({}, {}, mid {}, sid {})".format(
|
||||
cog_name, cog_identifier, user_id,
|
||||
guild_id))
|
||||
else:
|
||||
if clear:
|
||||
self._member.delete_one(filter)
|
||||
else:
|
||||
self._member.update_one(filter, data, upsert=True)
|
||||
|
||||
def set_user(self, cog_name, cog_identifier, user_id, key, value,
|
||||
clear=False):
|
||||
filter = {"cog_name": cog_name, "cog_identifier": cog_identifier,
|
||||
"user_id": user_id}
|
||||
data = {"$set": {key: value}}
|
||||
if self._user.count(filter) > 1:
|
||||
raise MultipleMatches("Too many matching documents at the USER"
|
||||
" level: ({}, {}, mid {})".format(
|
||||
cog_name, cog_identifier, user_id))
|
||||
else:
|
||||
if clear:
|
||||
self._user.delete_one(filter)
|
||||
else:
|
||||
self._user.update_one(filter, data, upsert=True)
|
||||
135
redbot/core/events.py
Normal file
135
redbot/core/events.py
Normal file
@@ -0,0 +1,135 @@
|
||||
import datetime
|
||||
import logging
|
||||
import traceback
|
||||
|
||||
import discord
|
||||
from .sentry_setup import should_log
|
||||
from discord.ext import commands
|
||||
|
||||
from .utils.chat_formatting import inline
|
||||
|
||||
log = logging.getLogger("red")
|
||||
sentry_log = logging.getLogger("red.sentry")
|
||||
|
||||
INTRO = ("{0}===================\n"
|
||||
"{0} Red - Discord Bot \n"
|
||||
"{0}===================\n"
|
||||
"".format(" " * 20))
|
||||
|
||||
|
||||
def init_events(bot, cli_flags):
|
||||
|
||||
@bot.event
|
||||
async def on_connect():
|
||||
if bot.uptime is None:
|
||||
print("Connected to Discord. Getting ready...")
|
||||
|
||||
@bot.event
|
||||
async def on_ready():
|
||||
if bot.uptime is not None:
|
||||
return
|
||||
|
||||
bot.uptime = datetime.datetime.utcnow()
|
||||
|
||||
print(INTRO)
|
||||
|
||||
if cli_flags.no_cogs is False:
|
||||
print("Loading packages...")
|
||||
failed = []
|
||||
packages = await bot.db.packages()
|
||||
|
||||
for package in packages:
|
||||
try:
|
||||
spec = await bot.cog_mgr.find_cog(package)
|
||||
bot.load_extension(spec)
|
||||
except Exception as e:
|
||||
log.exception("Failed to load package {}".format(package),
|
||||
exc_info=e)
|
||||
await bot.remove_loaded_package(package)
|
||||
if packages:
|
||||
print("Loaded packages: " + ", ".join(packages))
|
||||
|
||||
guilds = len(bot.guilds)
|
||||
users = len(set([m for m in bot.get_all_members()]))
|
||||
|
||||
try:
|
||||
data = await bot.application_info()
|
||||
invite_url = discord.utils.oauth_url(data.id)
|
||||
except:
|
||||
if bot.user.bot:
|
||||
invite_url = "Could not fetch invite url"
|
||||
else:
|
||||
invite_url = None
|
||||
|
||||
if guilds:
|
||||
print("Ready and operational on {} servers with a total of {} "
|
||||
"users.".format(guilds, users))
|
||||
else:
|
||||
print("Ready. I'm not in any server yet!")
|
||||
|
||||
if invite_url:
|
||||
print("\nInvite URL: {}\n".format(invite_url))
|
||||
|
||||
@bot.event
|
||||
async def on_command_error(ctx, error):
|
||||
if isinstance(error, commands.MissingRequiredArgument):
|
||||
await bot.send_cmd_help(ctx)
|
||||
elif isinstance(error, commands.BadArgument):
|
||||
await bot.send_cmd_help(ctx)
|
||||
elif isinstance(error, commands.DisabledCommand):
|
||||
await ctx.send("That command is disabled.")
|
||||
elif isinstance(error, commands.CommandInvokeError):
|
||||
# Need to test if the following still works
|
||||
"""
|
||||
no_dms = "Cannot send messages to this user"
|
||||
is_help_cmd = ctx.command.qualified_name == "help"
|
||||
is_forbidden = isinstance(error.original, discord.Forbidden)
|
||||
if is_help_cmd and is_forbidden and error.original.text == no_dms:
|
||||
msg = ("I couldn't send the help message to you in DM. Either"
|
||||
" you blocked me or you disabled DMs in this server.")
|
||||
await ctx.send(msg)
|
||||
return
|
||||
"""
|
||||
log.exception("Exception in command '{}'"
|
||||
"".format(ctx.command.qualified_name),
|
||||
exc_info=error.original)
|
||||
message = ("Error in command '{}'. Check your console or "
|
||||
"logs for details."
|
||||
"".format(ctx.command.qualified_name))
|
||||
exception_log = ("Exception in command '{}'\n"
|
||||
"".format(ctx.command.qualified_name))
|
||||
exception_log += "".join(traceback.format_exception(type(error),
|
||||
error, error.__traceback__))
|
||||
bot._last_exception = exception_log
|
||||
await ctx.send(inline(message))
|
||||
|
||||
module = ctx.command.module
|
||||
if should_log(module):
|
||||
sentry_log.exception("Exception in command '{}'"
|
||||
"".format(ctx.command.qualified_name),
|
||||
exc_info=error.original)
|
||||
elif isinstance(error, commands.CommandNotFound):
|
||||
pass
|
||||
elif isinstance(error, commands.CheckFailure):
|
||||
await ctx.send("⛔ You are not authorized to issue that command.")
|
||||
elif isinstance(error, commands.NoPrivateMessage):
|
||||
await ctx.send("That command is not available in DMs.")
|
||||
elif isinstance(error, commands.CommandOnCooldown):
|
||||
await ctx.send("This command is on cooldown. "
|
||||
"Try again in {:.2f}s"
|
||||
"".format(error.retry_after))
|
||||
else:
|
||||
log.exception(type(error).__name__, exc_info=error)
|
||||
|
||||
@bot.event
|
||||
async def on_message(message):
|
||||
bot.counter["messages_read"] += 1
|
||||
await bot.process_commands(message)
|
||||
|
||||
@bot.event
|
||||
async def on_resumed():
|
||||
bot.counter["sessions_resumed"] += 1
|
||||
|
||||
@bot.event
|
||||
async def on_command(command):
|
||||
bot.counter["processed_commands"] += 1
|
||||
38
redbot/core/global_checks.py
Normal file
38
redbot/core/global_checks.py
Normal file
@@ -0,0 +1,38 @@
|
||||
"""The checks in this module run on every command."""
|
||||
from discord.ext import commands
|
||||
|
||||
|
||||
def init_global_checks(bot):
|
||||
|
||||
@bot.check
|
||||
async def global_perms(ctx):
|
||||
"""Check the user is/isn't globally whitelisted/blacklisted."""
|
||||
if await bot.is_owner(ctx.author):
|
||||
return True
|
||||
|
||||
whitelist = await bot.db.whitelist()
|
||||
if whitelist:
|
||||
return ctx.author.id in whitelist
|
||||
|
||||
return ctx.author.id not in await bot.db.blacklist()
|
||||
|
||||
@bot.check
|
||||
async def local_perms(ctx: commands.Context):
|
||||
"""Check the user is/isn't locally whitelisted/blacklisted."""
|
||||
if await bot.is_owner(ctx.author):
|
||||
return True
|
||||
elif ctx.guild is None:
|
||||
return True
|
||||
guild_settings = bot.db.guild(ctx.guild)
|
||||
local_blacklist = await guild_settings.blacklist()
|
||||
local_whitelist = await guild_settings.whitelist()
|
||||
|
||||
if local_whitelist:
|
||||
return ctx.author.id in local_whitelist
|
||||
|
||||
return ctx.author.id not in local_blacklist
|
||||
|
||||
@bot.check
|
||||
async def bots(ctx):
|
||||
"""Check the user is not another bot."""
|
||||
return not ctx.author.bot
|
||||
203
redbot/core/i18n.py
Normal file
203
redbot/core/i18n.py
Normal file
@@ -0,0 +1,203 @@
|
||||
import re
|
||||
from pathlib import Path
|
||||
|
||||
__all__ = ['get_locale', 'set_locale', 'reload_locales', 'CogI18n']
|
||||
|
||||
_current_locale = 'en_us'
|
||||
|
||||
WAITING_FOR_MSGID = 1
|
||||
IN_MSGID = 2
|
||||
WAITING_FOR_MSGSTR = 3
|
||||
IN_MSGSTR = 4
|
||||
|
||||
MSGID = 'msgid "'
|
||||
MSGSTR = 'msgstr "'
|
||||
|
||||
_i18n_cogs = {}
|
||||
|
||||
|
||||
def get_locale():
|
||||
return _current_locale
|
||||
|
||||
|
||||
def set_locale(locale):
|
||||
global _current_locale
|
||||
_current_locale = locale
|
||||
reload_locales()
|
||||
|
||||
|
||||
def reload_locales():
|
||||
for cog_name, i18n in _i18n_cogs.items():
|
||||
i18n.load_translations()
|
||||
|
||||
|
||||
def _parse(translation_file):
|
||||
"""
|
||||
Custom gettext parsing of translation files. All credit for this code goes
|
||||
to ProgVal/Valentin Lorentz and the Limnoria project.
|
||||
|
||||
https://github.com/ProgVal/Limnoria/blob/master/src/i18n.py
|
||||
|
||||
:param translation_file:
|
||||
An open file-like object containing translations.
|
||||
:return:
|
||||
A set of 2-tuples containing the original string and the translated version.
|
||||
"""
|
||||
step = WAITING_FOR_MSGID
|
||||
translations = set()
|
||||
for line in translation_file:
|
||||
line = line[0:-1] # Remove the ending \n
|
||||
line = line
|
||||
|
||||
if line.startswith(MSGID):
|
||||
# Don't check if step is WAITING_FOR_MSGID
|
||||
untranslated = ''
|
||||
translated = ''
|
||||
data = line[len(MSGID):-1]
|
||||
if len(data) == 0: # Multiline mode
|
||||
step = IN_MSGID
|
||||
else:
|
||||
untranslated += data
|
||||
step = WAITING_FOR_MSGSTR
|
||||
|
||||
elif step is IN_MSGID and line.startswith('"') and \
|
||||
line.endswith('"'):
|
||||
untranslated += line[1:-1]
|
||||
elif step is IN_MSGID and untranslated == '': # Empty MSGID
|
||||
step = WAITING_FOR_MSGID
|
||||
elif step is IN_MSGID: # the MSGID is finished
|
||||
step = WAITING_FOR_MSGSTR
|
||||
|
||||
if step is WAITING_FOR_MSGSTR and line.startswith(MSGSTR):
|
||||
data = line[len(MSGSTR):-1]
|
||||
if len(data) == 0: # Multiline mode
|
||||
step = IN_MSGSTR
|
||||
else:
|
||||
translations |= {(untranslated, data)}
|
||||
step = WAITING_FOR_MSGID
|
||||
|
||||
elif step is IN_MSGSTR and line.startswith('"') and \
|
||||
line.endswith('"'):
|
||||
translated += line[1:-1]
|
||||
elif step is IN_MSGSTR: # the MSGSTR is finished
|
||||
step = WAITING_FOR_MSGID
|
||||
if translated == '':
|
||||
translated = untranslated
|
||||
translations |= {(untranslated, translated)}
|
||||
if step is IN_MSGSTR:
|
||||
if translated == '':
|
||||
translated = untranslated
|
||||
translations |= {(untranslated, translated)}
|
||||
return translations
|
||||
|
||||
|
||||
def _normalize(string, remove_newline=False):
|
||||
"""
|
||||
String normalization.
|
||||
|
||||
All credit for this code goes
|
||||
to ProgVal/Valentin Lorentz and the Limnoria project.
|
||||
|
||||
https://github.com/ProgVal/Limnoria/blob/master/src/i18n.py
|
||||
|
||||
:param string:
|
||||
:param remove_newline:
|
||||
:return:
|
||||
"""
|
||||
def normalize_whitespace(s):
|
||||
"""Normalizes the whitespace in a string; \s+ becomes one space."""
|
||||
if not s:
|
||||
return str(s) # not the same reference
|
||||
starts_with_space = (s[0] in ' \n\t\r')
|
||||
ends_with_space = (s[-1] in ' \n\t\r')
|
||||
if remove_newline:
|
||||
newline_re = re.compile('[\r\n]+')
|
||||
s = ' '.join(filter(bool, newline_re.split(s)))
|
||||
s = ' '.join(filter(bool, s.split('\t')))
|
||||
s = ' '.join(filter(bool, s.split(' ')))
|
||||
if starts_with_space:
|
||||
s = ' ' + s
|
||||
if ends_with_space:
|
||||
s += ' '
|
||||
return s
|
||||
|
||||
string = string.replace('\\n\\n', '\n\n')
|
||||
string = string.replace('\\n', ' ')
|
||||
string = string.replace('\\"', '"')
|
||||
string = string.replace("\'", "'")
|
||||
string = normalize_whitespace(string)
|
||||
string = string.strip('\n')
|
||||
string = string.strip('\t')
|
||||
return string
|
||||
|
||||
|
||||
def get_locale_path(cog_folder: Path, extension: str) -> Path:
|
||||
"""
|
||||
Gets the folder path containing localization files.
|
||||
|
||||
:param Path cog_folder:
|
||||
The cog folder that we want localizations for.
|
||||
:param str extension:
|
||||
Extension of localization files.
|
||||
:return:
|
||||
Path of possible localization file, it may not exist.
|
||||
"""
|
||||
return cog_folder / 'locales' / "{}.{}".format(get_locale(), extension)
|
||||
|
||||
|
||||
class CogI18n:
|
||||
def __init__(self, name, file_location):
|
||||
"""
|
||||
Initializes the internationalization object for a given cog.
|
||||
|
||||
:param name: Your cog name.
|
||||
:param file_location:
|
||||
This should always be ``__file__`` otherwise your localizations
|
||||
will not load.
|
||||
"""
|
||||
self.cog_folder = Path(file_location).resolve().parent
|
||||
self.cog_name = name
|
||||
self.translations = {}
|
||||
|
||||
_i18n_cogs.update({self.cog_name: self})
|
||||
|
||||
self.load_translations()
|
||||
|
||||
def __call__(self, untranslated: str):
|
||||
normalized_untranslated = _normalize(untranslated, True)
|
||||
try:
|
||||
return self.translations[normalized_untranslated]
|
||||
except KeyError:
|
||||
return untranslated
|
||||
|
||||
def load_translations(self):
|
||||
"""
|
||||
Loads the current translations for this cog.
|
||||
"""
|
||||
self.translations = {}
|
||||
translation_file = None
|
||||
locale_path = get_locale_path(self.cog_folder, 'po')
|
||||
try:
|
||||
|
||||
try:
|
||||
translation_file = locale_path.open('ru')
|
||||
except ValueError: # We are using Windows
|
||||
translation_file = locale_path.open('r')
|
||||
self._parse(translation_file)
|
||||
except (IOError, FileNotFoundError): # The translation is unavailable
|
||||
pass
|
||||
finally:
|
||||
if translation_file is not None:
|
||||
translation_file.close()
|
||||
|
||||
def _parse(self, translation_file):
|
||||
self.translations = {}
|
||||
for translation in _parse(translation_file):
|
||||
self._add_translation(*translation)
|
||||
|
||||
def _add_translation(self, untranslated, translated):
|
||||
untranslated = _normalize(untranslated, True)
|
||||
translated = _normalize(translated)
|
||||
if translated:
|
||||
self.translations.update({untranslated: translated})
|
||||
|
||||
55
redbot/core/json_io.py
Normal file
55
redbot/core/json_io.py
Normal file
@@ -0,0 +1,55 @@
|
||||
import functools
|
||||
import json
|
||||
import os
|
||||
import asyncio
|
||||
import logging
|
||||
from uuid import uuid4
|
||||
|
||||
# This is basically our old DataIO, except that it's now threadsafe
|
||||
# and just a base for much more elaborate classes
|
||||
from pathlib import Path
|
||||
|
||||
log = logging.getLogger("red")
|
||||
|
||||
PRETTY = {"indent": 4, "sort_keys": True, "separators": (',', ' : ')}
|
||||
MINIFIED = {"sort_keys": True, "separators": (',', ':')}
|
||||
|
||||
|
||||
class JsonIO:
|
||||
"""Basic functions for atomic saving / loading of json files"""
|
||||
def __init__(self, path: Path=Path.cwd()):
|
||||
"""
|
||||
:param path: Full path to file.
|
||||
"""
|
||||
self._lock = asyncio.Lock()
|
||||
self.path = path
|
||||
|
||||
# noinspection PyUnresolvedReferences
|
||||
def _save_json(self, data, settings=PRETTY):
|
||||
log.debug("Saving file {}".format(self.path))
|
||||
filename = self.path.stem
|
||||
tmp_file = "{}-{}.tmp".format(filename, uuid4().fields[0])
|
||||
tmp_path = self.path.parent / tmp_file
|
||||
with tmp_path.open(encoding="utf-8", mode="w") as f:
|
||||
json.dump(data, f, **settings)
|
||||
tmp_path.replace(self.path)
|
||||
|
||||
async def _threadsafe_save_json(self, data, settings=PRETTY):
|
||||
loop = asyncio.get_event_loop()
|
||||
func = functools.partial(self._save_json, data, settings)
|
||||
with await self._lock:
|
||||
await loop.run_in_executor(None, func)
|
||||
|
||||
# noinspection PyUnresolvedReferences
|
||||
def _load_json(self):
|
||||
log.debug("Reading file {}".format(self.path))
|
||||
with self.path.open(encoding='utf-8', mode="r") as f:
|
||||
data = json.load(f)
|
||||
return data
|
||||
|
||||
async def _threadsafe_load_json(self, path):
|
||||
loop = asyncio.get_event_loop()
|
||||
func = functools.partial(self._load_json, path)
|
||||
task = loop.run_in_executor(None, func)
|
||||
with await self._lock:
|
||||
return await asyncio.wait_for(task)
|
||||
0
redbot/core/locales/__init__.py
Normal file
0
redbot/core/locales/__init__.py
Normal file
245
redbot/core/locales/es.po
Normal file
245
redbot/core/locales/es.po
Normal file
@@ -0,0 +1,245 @@
|
||||
# SOME DESCRIPTIVE TITLE.
|
||||
# Copyright (C) YEAR ORGANIZATION
|
||||
# FIRST AUTHOR <EMAIL@ADDRESS>, YEAR.
|
||||
#
|
||||
msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: \n"
|
||||
"POT-Creation-Date: 2017-08-26 18:11+EDT\n"
|
||||
"PO-Revision-Date: 2017-08-27 09:02-0600\n"
|
||||
"Language-Team: \n"
|
||||
"MIME-Version: 1.0\n"
|
||||
"Content-Type: text/plain; charset=UTF-8\n"
|
||||
"Content-Transfer-Encoding: 8bit\n"
|
||||
"Generated-By: pygettext.py 1.5\n"
|
||||
"X-Generator: Poedit 2.0.3\n"
|
||||
"Last-Translator: UltimatePancake <pier.gaetani@gmail.com>\n"
|
||||
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
|
||||
"Language: es\n"
|
||||
|
||||
#: ../cog_manager.py:196
|
||||
msgid ""
|
||||
"Install Path: {}\n"
|
||||
"\n"
|
||||
msgstr ""
|
||||
"Ubicación de instalación: {}\n"
|
||||
"\n"
|
||||
|
||||
#: ../cog_manager.py:212
|
||||
msgid "That path is does not exist or does not point to a valid directory."
|
||||
msgstr "Esa ubicación no existe o no apunta a un directorio válido."
|
||||
|
||||
#: ../cog_manager.py:221
|
||||
msgid "Path successfully added."
|
||||
msgstr "Ubicación agregada exitósamente."
|
||||
|
||||
#: ../cog_manager.py:234
|
||||
msgid "That is an invalid path number."
|
||||
msgstr "Número de ubicación inválido."
|
||||
|
||||
#: ../cog_manager.py:238
|
||||
msgid "Path successfully removed."
|
||||
msgstr "Ubicación eliminada exitósamente."
|
||||
|
||||
#: ../cog_manager.py:254
|
||||
msgid "Invalid 'from' index."
|
||||
msgstr "Índice 'from' inválido."
|
||||
|
||||
#: ../cog_manager.py:260
|
||||
msgid "Invalid 'to' index."
|
||||
msgstr "Índice 'to' inválido."
|
||||
|
||||
#: ../cog_manager.py:264
|
||||
msgid "Paths reordered."
|
||||
msgstr "Ubicaciones reordenadas."
|
||||
|
||||
#: ../cog_manager.py:282
|
||||
msgid "That path does not exist."
|
||||
msgstr "Ubicación inexistente."
|
||||
|
||||
#: ../cog_manager.py:286
|
||||
msgid "The bot will install new cogs to the `{}` directory."
|
||||
msgstr "El bot instalará nuevos cogs en el directorio `{}`."
|
||||
|
||||
#: ../core_commands.py:35
|
||||
msgid "No module by that name was found in any cog path."
|
||||
msgstr "No existe módulo con ese nombre en ninguna ubicación."
|
||||
|
||||
#: ../core_commands.py:43
|
||||
msgid "Failed to load package. Check your console or logs for details."
|
||||
msgstr "Error cargando paquete. Revisar consola o bitácora para más detalles."
|
||||
|
||||
#: ../core_commands.py:47 ../core_commands.py:56 ../core_commands.py:76
|
||||
#: ../core_commands.py:151 ../core_commands.py:212 ../core_commands.py:226
|
||||
msgid "Done."
|
||||
msgstr "Terminado."
|
||||
|
||||
#: ../core_commands.py:58
|
||||
msgid "That extension is not loaded."
|
||||
msgstr "Esa extensión no está cargada."
|
||||
|
||||
#: ../core_commands.py:71
|
||||
msgid "Failed to reload package. Check your console or logs for details."
|
||||
msgstr ""
|
||||
"Error recargando paquete. Revisar consola o bitácora para más detalles."
|
||||
|
||||
#: ../core_commands.py:86
|
||||
msgid "Shutting down... "
|
||||
msgstr "Apagando..."
|
||||
|
||||
#: ../core_commands.py:123
|
||||
msgid "The admin role for this guild has been set."
|
||||
msgstr "El rol de administrador para este gremio ha sido configurado."
|
||||
|
||||
#: ../core_commands.py:131
|
||||
msgid "The mod role for this guild has been set."
|
||||
msgstr "El rol de moderador para este gremio ha sido configurado."
|
||||
|
||||
#: ../core_commands.py:145
|
||||
msgid ""
|
||||
"Failed. Remember that you can edit my avatar up to two times a hour. The URL "
|
||||
"must be a direct link to a JPG / PNG."
|
||||
msgstr ""
|
||||
"Error. Recordar que sólo se puede editar el avatar hasta dos veces por hora. "
|
||||
"La URL debe ser un enlace directo a un JPG o PNG."
|
||||
|
||||
#: ../core_commands.py:149
|
||||
msgid "JPG / PNG format only."
|
||||
msgstr "Únicamente formatos JPG o PNG."
|
||||
|
||||
#: ../core_commands.py:161
|
||||
msgid "Game set."
|
||||
msgstr "Juego configurado."
|
||||
|
||||
#: ../core_commands.py:190
|
||||
msgid "Status changed to %s."
|
||||
msgstr "Estado cambiado a %s."
|
||||
|
||||
#: ../core_commands.py:221
|
||||
msgid ""
|
||||
"Failed to change name. Remember that you can only do it up to 2 times an "
|
||||
"hour. Use nicknames if you need frequent changes. `{}set nickname`"
|
||||
msgstr ""
|
||||
"Error cambiando nombre. Recordar que sólo se puede hacer hasta dos veces por "
|
||||
"hora. Usar sobrenombres si cambios frecuentes son necesarios. `{}set "
|
||||
"nickname`"
|
||||
|
||||
#: ../core_commands.py:236
|
||||
msgid "I lack the permissions to change my own nickname."
|
||||
msgstr "Carezco de permisos para cambiar mi sobrenombre."
|
||||
|
||||
#: ../core_commands.py:250 ../core_commands.py:263
|
||||
msgid "Prefix set."
|
||||
msgstr "Prefijo configurado."
|
||||
|
||||
#: ../core_commands.py:259
|
||||
msgid "Guild prefixes have been reset."
|
||||
msgstr "Prefijos de gremio han sido restaurados."
|
||||
|
||||
#: ../core_commands.py:282
|
||||
msgid ""
|
||||
"\n"
|
||||
"Verification token:"
|
||||
msgstr ""
|
||||
"\n"
|
||||
"Ficha de verificación:"
|
||||
|
||||
#: ../core_commands.py:285
|
||||
msgid "Remember:\n"
|
||||
msgstr "Recordar:\n"
|
||||
|
||||
#: ../core_commands.py:288
|
||||
msgid ""
|
||||
"I have printed a one-time token in the console. Copy and paste it here to "
|
||||
"confirm you are the owner."
|
||||
msgstr ""
|
||||
"He impreso una ficha única en la consola. Copiar y pegarlo aca para "
|
||||
"confirmar propiedad."
|
||||
|
||||
#: ../core_commands.py:296
|
||||
msgid "The set owner request has timed out."
|
||||
msgstr "Tiempo de espera para configuración de dueño ha terminado."
|
||||
|
||||
#: ../core_commands.py:302
|
||||
msgid "You have been set as owner."
|
||||
msgstr "Has sido configurado como dueño."
|
||||
|
||||
#: ../core_commands.py:304
|
||||
msgid "Invalid token."
|
||||
msgstr "Ficha inválida."
|
||||
|
||||
#: ../core_commands.py:313
|
||||
msgid "Locale has been set."
|
||||
msgstr "Localización configurada."
|
||||
|
||||
#: ../core_commands.py:323
|
||||
msgid "User ID: %s"
|
||||
msgstr "ID de usuario: %s"
|
||||
|
||||
#: ../core_commands.py:326
|
||||
msgid "through DM"
|
||||
msgstr "a través de mensaje privado"
|
||||
|
||||
#: ../core_commands.py:328
|
||||
msgid "from {}"
|
||||
msgstr "de {}"
|
||||
|
||||
#: ../core_commands.py:329
|
||||
msgid " | Server ID: %s"
|
||||
msgstr " | ID de servidor: %s"
|
||||
|
||||
#: ../core_commands.py:337
|
||||
msgid "Use `{}dm {} <text>` to reply to this user"
|
||||
msgstr "Utilizar `{}dm {} <texto>` para responder a este usuario"
|
||||
|
||||
#: ../core_commands.py:345
|
||||
msgid "Sent by {} {}"
|
||||
msgstr "Enviado por {} {}"
|
||||
|
||||
#: ../core_commands.py:357
|
||||
msgid "I cannot send your message, I'm unable to find my owner... *sigh*"
|
||||
msgstr "Error enviando mensaje, no encuentro a mi dueño... *suspiro*"
|
||||
|
||||
#: ../core_commands.py:360
|
||||
msgid "I'm unable to deliver your message. Sorry."
|
||||
msgstr "No puedo enviar tu mensaje, perdón."
|
||||
|
||||
#: ../core_commands.py:362
|
||||
msgid "Your message has been sent."
|
||||
msgstr "Tu mensaje ha sido enviado."
|
||||
|
||||
#: ../core_commands.py:376
|
||||
msgid ""
|
||||
"Invalid ID or user not found. You can only send messages to people I share a "
|
||||
"server with."
|
||||
msgstr ""
|
||||
"ID inválido o usuario no encontrad. Solo puedes enviar mensajes a personas "
|
||||
"con quienes compartes un servidor."
|
||||
|
||||
#: ../core_commands.py:382
|
||||
msgid "Owner of %s"
|
||||
msgstr "Dueño de %s"
|
||||
|
||||
#: ../core_commands.py:385
|
||||
msgid "You can reply to this message with %scontact"
|
||||
msgstr "Puedes contestar este mensaje utilizando %scontact"
|
||||
|
||||
#: ../core_commands.py:395
|
||||
msgid "Sorry, I couldn't deliver your message to %s"
|
||||
msgstr "Perdón, no pude enviar tu mensaje para %s"
|
||||
|
||||
#: ../core_commands.py:398
|
||||
msgid "Message delivered to %s"
|
||||
msgstr "Mensaje enviado a %s"
|
||||
|
||||
#: ../dev_commands.py:165
|
||||
msgid "Already running a REPL session in this channel. Exit it with `quit`."
|
||||
msgstr "Una sesión de REPL ya esta activa en este canal. Salir con `quit`."
|
||||
|
||||
#: ../dev_commands.py:169
|
||||
msgid "Enter code to execute or evaluate. `exit()` or `quit` to exit."
|
||||
msgstr "Ingresar codigo a ejecutar o evaluar. `exit()` o `quit` para salir."
|
||||
|
||||
#: ../dev_commands.py:234
|
||||
msgid "Unexpected error: `{}`"
|
||||
msgstr "Error inesperado: `{}`"
|
||||
223
redbot/core/locales/messages.pot
Normal file
223
redbot/core/locales/messages.pot
Normal file
@@ -0,0 +1,223 @@
|
||||
# SOME DESCRIPTIVE TITLE.
|
||||
# Copyright (C) YEAR ORGANIZATION
|
||||
# FIRST AUTHOR <EMAIL@ADDRESS>, YEAR.
|
||||
#
|
||||
msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: PACKAGE VERSION\n"
|
||||
"POT-Creation-Date: 2017-08-26 18:11+EDT\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"
|
||||
|
||||
|
||||
#: ../cog_manager.py:196
|
||||
msgid ""
|
||||
"Install Path: {}\n"
|
||||
"\n"
|
||||
msgstr ""
|
||||
|
||||
#: ../cog_manager.py:212
|
||||
msgid "That path is does not exist or does not point to a valid directory."
|
||||
msgstr ""
|
||||
|
||||
#: ../cog_manager.py:221
|
||||
msgid "Path successfully added."
|
||||
msgstr ""
|
||||
|
||||
#: ../cog_manager.py:234
|
||||
msgid "That is an invalid path number."
|
||||
msgstr ""
|
||||
|
||||
#: ../cog_manager.py:238
|
||||
msgid "Path successfully removed."
|
||||
msgstr ""
|
||||
|
||||
#: ../cog_manager.py:254
|
||||
msgid "Invalid 'from' index."
|
||||
msgstr ""
|
||||
|
||||
#: ../cog_manager.py:260
|
||||
msgid "Invalid 'to' index."
|
||||
msgstr ""
|
||||
|
||||
#: ../cog_manager.py:264
|
||||
msgid "Paths reordered."
|
||||
msgstr ""
|
||||
|
||||
#: ../cog_manager.py:282
|
||||
msgid "That path does not exist."
|
||||
msgstr ""
|
||||
|
||||
#: ../cog_manager.py:286
|
||||
msgid "The bot will install new cogs to the `{}` directory."
|
||||
msgstr ""
|
||||
|
||||
#: ../core_commands.py:35
|
||||
msgid "No module by that name was found in any cog path."
|
||||
msgstr ""
|
||||
|
||||
#: ../core_commands.py:43
|
||||
msgid "Failed to load package. Check your console or logs for details."
|
||||
msgstr ""
|
||||
|
||||
#: ../core_commands.py:47 ../core_commands.py:56 ../core_commands.py:76
|
||||
#: ../core_commands.py:151 ../core_commands.py:212 ../core_commands.py:226
|
||||
msgid "Done."
|
||||
msgstr ""
|
||||
|
||||
#: ../core_commands.py:58
|
||||
msgid "That extension is not loaded."
|
||||
msgstr ""
|
||||
|
||||
#: ../core_commands.py:71
|
||||
msgid "Failed to reload package. Check your console or logs for details."
|
||||
msgstr ""
|
||||
|
||||
#: ../core_commands.py:86
|
||||
msgid "Shutting down... "
|
||||
msgstr ""
|
||||
|
||||
#: ../core_commands.py:123
|
||||
msgid "The admin role for this guild has been set."
|
||||
msgstr ""
|
||||
|
||||
#: ../core_commands.py:131
|
||||
msgid "The mod role for this guild has been set."
|
||||
msgstr ""
|
||||
|
||||
#: ../core_commands.py:145
|
||||
msgid "Failed. Remember that you can edit my avatar up to two times a hour. The URL must be a direct link to a JPG / PNG."
|
||||
msgstr ""
|
||||
|
||||
#: ../core_commands.py:149
|
||||
msgid "JPG / PNG format only."
|
||||
msgstr ""
|
||||
|
||||
#: ../core_commands.py:161
|
||||
msgid "Game set."
|
||||
msgstr ""
|
||||
|
||||
#: ../core_commands.py:190
|
||||
msgid "Status changed to %s."
|
||||
msgstr ""
|
||||
|
||||
#: ../core_commands.py:221
|
||||
msgid "Failed to change name. Remember that you can only do it up to 2 times an hour. Use nicknames if you need frequent changes. `{}set nickname`"
|
||||
msgstr ""
|
||||
|
||||
#: ../core_commands.py:236
|
||||
msgid "I lack the permissions to change my own nickname."
|
||||
msgstr ""
|
||||
|
||||
#: ../core_commands.py:250 ../core_commands.py:263
|
||||
msgid "Prefix set."
|
||||
msgstr ""
|
||||
|
||||
#: ../core_commands.py:259
|
||||
msgid "Guild prefixes have been reset."
|
||||
msgstr ""
|
||||
|
||||
#: ../core_commands.py:282
|
||||
msgid ""
|
||||
"\n"
|
||||
"Verification token:"
|
||||
msgstr ""
|
||||
|
||||
#: ../core_commands.py:285
|
||||
msgid ""
|
||||
"Remember:\n"
|
||||
msgstr ""
|
||||
|
||||
#: ../core_commands.py:288
|
||||
msgid "I have printed a one-time token in the console. Copy and paste it here to confirm you are the owner."
|
||||
msgstr ""
|
||||
|
||||
#: ../core_commands.py:296
|
||||
msgid "The set owner request has timed out."
|
||||
msgstr ""
|
||||
|
||||
#: ../core_commands.py:302
|
||||
msgid "You have been set as owner."
|
||||
msgstr ""
|
||||
|
||||
#: ../core_commands.py:304
|
||||
msgid "Invalid token."
|
||||
msgstr ""
|
||||
|
||||
#: ../core_commands.py:313
|
||||
msgid "Locale has been set."
|
||||
msgstr ""
|
||||
|
||||
#: ../core_commands.py:323
|
||||
msgid "User ID: %s"
|
||||
msgstr ""
|
||||
|
||||
#: ../core_commands.py:326
|
||||
msgid "through DM"
|
||||
msgstr ""
|
||||
|
||||
#: ../core_commands.py:328
|
||||
msgid "from {}"
|
||||
msgstr ""
|
||||
|
||||
#: ../core_commands.py:329
|
||||
msgid " | Server ID: %s"
|
||||
msgstr ""
|
||||
|
||||
#: ../core_commands.py:337
|
||||
msgid "Use `{}dm {} <text>` to reply to this user"
|
||||
msgstr ""
|
||||
|
||||
#: ../core_commands.py:345
|
||||
msgid "Sent by {} {}"
|
||||
msgstr ""
|
||||
|
||||
#: ../core_commands.py:357
|
||||
msgid "I cannot send your message, I'm unable to find my owner... *sigh*"
|
||||
msgstr ""
|
||||
|
||||
#: ../core_commands.py:360
|
||||
msgid "I'm unable to deliver your message. Sorry."
|
||||
msgstr ""
|
||||
|
||||
#: ../core_commands.py:362
|
||||
msgid "Your message has been sent."
|
||||
msgstr ""
|
||||
|
||||
#: ../core_commands.py:376
|
||||
msgid "Invalid ID or user not found. You can only send messages to people I share a server with."
|
||||
msgstr ""
|
||||
|
||||
#: ../core_commands.py:382
|
||||
msgid "Owner of %s"
|
||||
msgstr ""
|
||||
|
||||
#: ../core_commands.py:385
|
||||
msgid "You can reply to this message with %scontact"
|
||||
msgstr ""
|
||||
|
||||
#: ../core_commands.py:395
|
||||
msgid "Sorry, I couldn't deliver your message to %s"
|
||||
msgstr ""
|
||||
|
||||
#: ../core_commands.py:398
|
||||
msgid "Message delivered to %s"
|
||||
msgstr ""
|
||||
|
||||
#: ../dev_commands.py:165
|
||||
msgid "Already running a REPL session in this channel. Exit it with `quit`."
|
||||
msgstr ""
|
||||
|
||||
#: ../dev_commands.py:169
|
||||
msgid "Enter code to execute or evaluate. `exit()` or `quit` to exit."
|
||||
msgstr ""
|
||||
|
||||
#: ../dev_commands.py:234
|
||||
msgid "Unexpected error: `{}`"
|
||||
msgstr ""
|
||||
|
||||
17
redbot/core/locales/regen_messages.py
Normal file
17
redbot/core/locales/regen_messages.py
Normal file
@@ -0,0 +1,17 @@
|
||||
import subprocess
|
||||
|
||||
TO_TRANSLATE = [
|
||||
'../cog_manager.py',
|
||||
'../core_commands.py',
|
||||
'../dev_commands.py'
|
||||
]
|
||||
|
||||
|
||||
def regen_messages():
|
||||
subprocess.run(
|
||||
['pygettext', '-n'] + TO_TRANSLATE
|
||||
)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
regen_messages()
|
||||
43
redbot/core/sentry_setup.py
Normal file
43
redbot/core/sentry_setup.py
Normal file
@@ -0,0 +1,43 @@
|
||||
from raven import Client, breadcrumbs
|
||||
from raven.handlers.logging import SentryHandler
|
||||
|
||||
from redbot.core import __version__
|
||||
|
||||
__all__ = ("init_sentry_logging", "should_log")
|
||||
|
||||
|
||||
include_paths = (
|
||||
'core',
|
||||
'cogs.alias',
|
||||
'cogs.audio',
|
||||
'cogs.downloader',
|
||||
'cogs.economy',
|
||||
'cogs.general',
|
||||
'cogs.image',
|
||||
'cogs.streams',
|
||||
'cogs.trivia',
|
||||
'cogs.utils',
|
||||
'tests.core.test_sentry',
|
||||
'main',
|
||||
'launcher'
|
||||
)
|
||||
|
||||
client = None
|
||||
|
||||
|
||||
def init_sentry_logging(logger):
|
||||
global client
|
||||
client = Client(
|
||||
dsn=("https://27f3915ba0144725a53ea5a99c9ae6f3:87913fb5d0894251821dcf06e5e9cfe6@"
|
||||
"sentry.telemetry.red/19?verify_ssl=0"),
|
||||
release=__version__
|
||||
)
|
||||
|
||||
breadcrumbs.ignore_logger("websockets")
|
||||
breadcrumbs.ignore_logger("websockets.protocol")
|
||||
handler = SentryHandler(client)
|
||||
logger.addHandler(handler)
|
||||
|
||||
|
||||
def should_log(module_name: str) -> bool:
|
||||
return any(module_name.startswith(path) for path in include_paths)
|
||||
0
redbot/core/utils/__init__.py
Normal file
0
redbot/core/utils/__init__.py
Normal file
79
redbot/core/utils/chat_formatting.py
Normal file
79
redbot/core/utils/chat_formatting.py
Normal file
@@ -0,0 +1,79 @@
|
||||
def error(text):
|
||||
return "\N{NO ENTRY SIGN} {}".format(text)
|
||||
|
||||
|
||||
def warning(text):
|
||||
return "\N{WARNING SIGN} {}".format(text)
|
||||
|
||||
|
||||
def info(text):
|
||||
return "\N{INFORMATION SOURCE} {}".format(text)
|
||||
|
||||
|
||||
def question(text):
|
||||
return "\N{BLACK QUESTION MARK ORNAMENT} {}".format(text)
|
||||
|
||||
|
||||
def bold(text):
|
||||
return "**{}**".format(text)
|
||||
|
||||
|
||||
def box(text, lang=""):
|
||||
ret = "```{}\n{}\n```".format(lang, text)
|
||||
return ret
|
||||
|
||||
|
||||
def inline(text):
|
||||
return "`{}`".format(text)
|
||||
|
||||
|
||||
def italics(text):
|
||||
return "*{}*".format(text)
|
||||
|
||||
|
||||
def pagify(text, delims=["\n"], *, escape_mass_mentions=True, shorten_by=8,
|
||||
page_length=2000):
|
||||
"""DOES NOT RESPECT MARKDOWN BOXES OR INLINE CODE"""
|
||||
in_text = text
|
||||
page_length -= shorten_by
|
||||
while len(in_text) > page_length:
|
||||
this_page_len = page_length
|
||||
if escape_mass_mentions:
|
||||
this_page_len -= (in_text.count("@here", 0, page_length) +
|
||||
in_text.count("@everyone", 0, page_length))
|
||||
closest_delim = max([in_text.rfind(d, 1, this_page_len)
|
||||
for d in delims])
|
||||
closest_delim = closest_delim if closest_delim != -1 else this_page_length
|
||||
if escape_mass_mentions:
|
||||
to_send = escape(in_text[:closest_delim], mass_mentions=True)
|
||||
else:
|
||||
to_send = in_text[:closest_delim]
|
||||
if len(to_send.strip()) > 0:
|
||||
yield to_send
|
||||
in_text = in_text[closest_delim:]
|
||||
|
||||
if len(in_text.strip()) > 0:
|
||||
if escape_mass_mentions:
|
||||
yield escape(in_text, mass_mentions=True)
|
||||
else:
|
||||
yield in_text
|
||||
|
||||
|
||||
def strikethrough(text):
|
||||
return "~~{}~~".format(text)
|
||||
|
||||
|
||||
def underline(text):
|
||||
return "__{}__".format(text)
|
||||
|
||||
|
||||
def escape(text, *, mass_mentions=False, formatting=False):
|
||||
if mass_mentions:
|
||||
text = text.replace("@everyone", "@\u200beveryone")
|
||||
text = text.replace("@here", "@\u200bhere")
|
||||
if formatting:
|
||||
text = (text.replace("`", "\\`")
|
||||
.replace("*", "\\*")
|
||||
.replace("_", "\\_")
|
||||
.replace("~", "\\~"))
|
||||
return text
|
||||
107
redbot/setup.py
Normal file
107
redbot/setup.py
Normal file
@@ -0,0 +1,107 @@
|
||||
#!/usr/bin/env python
|
||||
|
||||
import sys
|
||||
from copy import deepcopy
|
||||
from pathlib import Path
|
||||
|
||||
import appdirs
|
||||
|
||||
from redbot.core.json_io import JsonIO
|
||||
from redbot.core.data_manager import basic_config_default
|
||||
from redbot.core.cli import confirm
|
||||
|
||||
appdir = appdirs.AppDirs("Red-DiscordBot")
|
||||
config_dir = Path(appdir.user_config_dir)
|
||||
config_dir.mkdir(parents=True, exist_ok=True)
|
||||
config_file = config_dir / 'config.json'
|
||||
|
||||
|
||||
def load_existing_config():
|
||||
if not config_file.exists():
|
||||
return {}
|
||||
|
||||
return JsonIO(config_file)._load_json()
|
||||
|
||||
|
||||
def save_config(name, data):
|
||||
config = load_existing_config()
|
||||
config[name] = data
|
||||
JsonIO(config_file)._save_json(config)
|
||||
|
||||
|
||||
def basic_setup():
|
||||
"""
|
||||
Creates the data storage folder.
|
||||
:return:
|
||||
"""
|
||||
|
||||
default_data_dir = Path(appdir.user_data_dir)
|
||||
|
||||
print("Hello! Before we begin the full configuration process we need to"
|
||||
" gather some initial information about where you'd like us"
|
||||
" to store your bot's data. We've attempted to figure out a"
|
||||
" sane default data location which is printed below. If you don't"
|
||||
" want to change this default please press [ENTER], otherwise"
|
||||
" input your desired data location.")
|
||||
print()
|
||||
print("Default: {}".format(default_data_dir))
|
||||
|
||||
new_path = input('> ')
|
||||
|
||||
if new_path != '':
|
||||
new_path = Path(new_path)
|
||||
default_data_dir = new_path
|
||||
|
||||
if not default_data_dir.exists():
|
||||
try:
|
||||
default_data_dir.mkdir(parents=True, exist_ok=True)
|
||||
except OSError:
|
||||
print("We were unable to create your chosen directory."
|
||||
" You may need to restart this process with admin"
|
||||
" privileges.")
|
||||
sys.exit(1)
|
||||
|
||||
print("You have chosen {} to be your data directory."
|
||||
"".format(default_data_dir))
|
||||
if not confirm("Please confirm (y/n):"):
|
||||
print("Please start the process over.")
|
||||
sys.exit(0)
|
||||
|
||||
default_dirs = deepcopy(basic_config_default)
|
||||
default_dirs['DATA_PATH'] = str(default_data_dir.resolve())
|
||||
|
||||
storage_dict = {
|
||||
1: "JSON",
|
||||
2: "MongoDB"
|
||||
}
|
||||
storage = None
|
||||
while storage is None:
|
||||
print()
|
||||
print("Please choose your storage backend (if you're unsure, choose 1).")
|
||||
print("1. JSON (file storage, requires no database).")
|
||||
print("2. MongoDB")
|
||||
storage = input("> ")
|
||||
try:
|
||||
storage = int(storage)
|
||||
except ValueError:
|
||||
storage = None
|
||||
else:
|
||||
if storage not in storage_dict:
|
||||
storage = None
|
||||
|
||||
default_dirs['STORAGE_TYPE'] = storage_dict[storage]
|
||||
|
||||
name = ""
|
||||
while len(name) == 0:
|
||||
print()
|
||||
print("Please enter a name for your instance, this name cannot include spaces"
|
||||
" and it will be used to run your bot from here on out.")
|
||||
name = input("> ")
|
||||
if " " in name:
|
||||
name = ""
|
||||
|
||||
save_config(name, default_dirs)
|
||||
|
||||
print()
|
||||
print("Your basic configuration has been saved. Please run `redbot <name>` to"
|
||||
" continue your setup process and to run the bot.")
|
||||
Reference in New Issue
Block a user