[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:
Will
2017-09-08 23:14:32 -04:00
committed by GitHub
parent 6b1fc786ee
commit d69fd63da7
85 changed files with 451 additions and 255 deletions

0
redbot/__init__.py Normal file
View File

155
redbot/__main__.py Normal file
View 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
View File

View 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
View 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)

View 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

View 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."

View 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 ""

View 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
View 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'])

View 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
View 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

View 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

View 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 {}"

View 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 ""

View File

@@ -0,0 +1,6 @@
from redbot.core.bot import Red
from .downloader import Downloader
def setup(bot: Red):
bot.add_cog(Downloader(bot))

View 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)

View 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

View 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))

View 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

View 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)

View 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")

View 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."

View 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."

View 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"

View 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 ""

View 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 ""

View 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 ""

View File

@@ -0,0 +1,3 @@
import logging
log = logging.getLogger("red.downloader")

View 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)

View File

@@ -0,0 +1,2 @@
*
!.gitignore

View File

@@ -0,0 +1,6 @@
from redbot.core.bot import Red
from .economy import Economy
def setup(bot: Red):
bot.add_cog(Economy(bot))

View 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])

View 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"

View 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 ""

View File

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

View 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."))

View 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."

View 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 ""

View 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
View 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"))

View 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"

View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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)

View 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)

View 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
View 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

View File

View 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

View 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)

View 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
View 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

View 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
View 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
View 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)

View File

245
redbot/core/locales/es.po Normal file
View 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: `{}`"

View 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 ""

View 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()

View 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)

View File

View 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
View 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.")