[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/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 ""