diff --git a/bot.py b/bot.py
index bc5e67a..0f9486d 100755
--- a/bot.py
+++ b/bot.py
@@ -1,13 +1,5 @@
-#!/usr/bin/env python
-# -*- coding: utf-8 -*-
-
-__author__ = "Maël / Outout | Romain"
-__licence__ = "WTFPL Licence 2.0"
-
-
-import copy
import datetime
-import os
+import logging
import sys
import traceback
@@ -15,122 +7,95 @@ import aiohttp
import discord
from discord.ext import commands
-import cogs.utils.cli_colors as colors
import config
-from cogs.utils import checks
+from cogs.utils.lang import _
-if sys.version_info[1] < 7 or sys.version_info[0] < 3:
- print(f"{colors.text_colors.RED}[ERROR] Python 3.7 or + is required.{colors.ENDC}")
- exit()
+description = """
+Je suis TuxBot, le bot qui vit de l'OpenSource ! ;)
+"""
+
+log = logging.getLogger(__name__)
l_extensions = (
'cogs.admin',
- 'cogs.afk',
- 'cogs.atc',
- 'cogs.basics',
- 'cogs.ci',
- 'cogs.filter_messages',
- 'cogs.funs',
- 'cogs.role',
- 'cogs.search',
- 'cogs.send_logs',
- 'cogs.sondage',
- 'cogs.utility',
- 'cogs.vocal',
- 'cogs.private',
+ 'cogs.basaics',
)
-help_attrs = dict(hidden=True, in_help=True, name="DONOTUSE")
+
+async def _prefix_callable(bot, message):
+ base = [] if config.prefix is None else config.prefix
+
+ # if message.guild is not None:
+ # base.extend(bot.prefixes.get(message.guild.id))
+ return commands.when_mentioned_or(base)
-class TuxBot(commands.Bot):
- def __init__(self):
+class TuxBot(commands.AutoShardedBot):
+ __slots__ = ('uptime', 'config', 'session')
+
+ def __init__(self, unload):
+ super().__init__(command_prefix=_prefix_callable,
+ description=description, pm_help=None,
+ help_command=None, help_attrs=dict(hidden=True))
+
self.uptime = datetime.datetime.utcnow()
self.config = config
- super().__init__(command_prefix=self.config.prefix[0],
- description=self.config.description,
- pm_help=None,
- help_command=None)
-
- self.client_id = self.config.client_id
+ self.prefixes = {}
self.session = aiohttp.ClientSession(loop=self.loop)
- self._events = []
-
- self.add_command(self.do)
for extension in l_extensions:
- try:
- self.load_extension(extension)
- print(f"{colors.text_colors.GREEN}\"{extension}\""
- f" chargé !{colors.ENDC}")
- except Exception as e:
- print(f"{colors.text_colors.RED}"
- f"Impossible de charger l'extension {extension}\n"
- f"{type(e).__name__}: {e}{colors.ENDC}", file=sys.stderr)
+ if extension not in unload:
+ try:
+ self.load_extension(extension)
+ except Exception as e:
+ print(_("Failed to load extension : ") + extension,
+ file=sys.stderr)
+ traceback.print_exc()
async def on_command_error(self, ctx, error):
if isinstance(error, commands.NoPrivateMessage):
- await ctx.author.send('Cette commande ne peut pas être utilisee '
- 'en message privee.')
+ await ctx.author.send(
+ _('This command cannot be used in private messages.')
+ )
elif isinstance(error, commands.DisabledCommand):
- await ctx.author.send('Desoler mais cette commande est desactive, '
- 'elle ne peut donc pas être utilisée.')
+ await ctx.author.send(
+ _('Sorry. This command is disabled and cannot be used.')
+ )
elif isinstance(error, commands.CommandInvokeError):
- print(f'In {ctx.command.qualified_name}:', file=sys.stderr)
+ print(_('In ') + f'{ctx.command.qualified_name}:', file=sys.stderr)
traceback.print_tb(error.original.__traceback__)
print(f'{error.original.__class__.__name__}: {error.original}',
file=sys.stderr)
+ elif isinstance(error, commands.ArgumentParsingError):
+ await ctx.send(error)
async def on_ready(self):
- log_channel_id = await self.fetch_channel(self.config.log_channel_id)
+ if not hasattr(self, 'uptime'):
+ self.uptime = datetime.datetime.utcnow()
- print('\n\n---------------------')
- print('CONNECTÉ :')
- print(f'Nom d\'utilisateur: {self.user} {colors.text_style.DIM}'
- f'(ID: {self.user.id}){colors.ENDC}')
- print(f'Channel de log: {log_channel_id} {colors.text_style.DIM}'
- f'(ID: {log_channel_id.id}){colors.ENDC}')
- print(f'Prefix: {self.config.prefix[0]}')
- print('Merci d\'utiliser TuxBot')
- print('---------------------\n\n')
+ print(_('Ready:') + f' {self.user} (ID: {self.user.id})')
await self.change_presence(status=discord.Status.dnd,
activity=discord.Game(
- name=self.config.game)
- )
+ name=self.config.activity
+ ))
@staticmethod
async def on_resumed():
print('resumed...')
- async def on_message(self, message):
- if message.author.bot:
- return
+ @property
+ def logs_webhook(self):
+ logs_webhook = self.config.logs_webhook
+ webhook = discord.Webhook.partial(id=logs_webhook.get('id'),
+ token=logs_webhook.get('token'),
+ adapter=discord.AsyncWebhookAdapter(
+ self.session))
+ return webhook
- try:
- await self.process_commands(message)
- except Exception as e:
- print(f'{colors.text_colors.RED}Erreur rencontré : \n'
- f' {type(e).__name__}: {e}{colors.ENDC} \n \n')
+ async def close(self):
+ await super().close()
+ await self.session.close()
def run(self):
- super().run(self.config.token, reconnect=True)
-
- @checks.has_permissions(administrator=True)
- @commands.command(pass_context=True, hidden=True)
- async def do(self, ctx, times: int, *, command):
- """Repeats a command a specified number of times."""
- msg = copy.copy(ctx.message)
- msg.content = command
- for i in range(times):
- await self.process_commands(msg)
-
-
-if __name__ == '__main__':
- if os.path.exists('config.py') is not True:
- print(f"{colors.text_colors.RED}"
- f"Veuillez créer le fichier config.py{colors.ENDC}")
- exit()
-
- tuxbot = TuxBot()
- tuxbot.run()
+ super().run(config.token, reconnect=True)
diff --git a/cogs/utils/checks.py b/cogs/utils/checks.py
old mode 100755
new mode 100644
index b965267..4453006
--- a/cogs/utils/checks.py
+++ b/cogs/utils/checks.py
@@ -121,4 +121,4 @@ def check_date(date: str):
if len(date) == 1:
return f"0{date}"
else:
- return date
+ return date
\ No newline at end of file
diff --git a/cogs/utils/db.py b/cogs/utils/db.py
index 502d11a..90c7604 100755
--- a/cogs/utils/db.py
+++ b/cogs/utils/db.py
@@ -1,29 +1,1085 @@
-import pymysql
+# -*- coding: utf-8 -*-
+
+"""
+The MIT License (MIT)
+
+Copyright (c) 2017 Rapptz
+
+Permission is hereby granted, free of charge, to any person obtaining a
+copy of this software and associated documentation files (the "Software"),
+to deal in the Software without restriction, including without limitation
+the rights to use, copy, modify, merge, publish, distribute, sublicense,
+and/or sell copies of the Software, and to permit persons to whom the
+Software is furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in
+all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
+OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+DEALINGS IN THE SOFTWARE.
+"""
+
+# These are just things that allow me to make tables for PostgreSQL easier
+# This isn't exactly good. It's just good enough for my uses.
+# Also shoddy migration support.
+
+import asyncio
+import datetime
+import decimal
+import inspect
+import json
+import logging
+import pydoc
+import uuid
+from collections import OrderedDict
+from pathlib import Path
+
+import asyncpg
+
+log = logging.getLogger(__name__)
-def connect_to_db(self):
- mysqlHost = self.bot.config.mysql["host"]
- mysqlUser = self.bot.config.mysql["username"]
- mysqlPass = self.bot.config.mysql["password"]
- mysqlDB = self.bot.config.mysql["dbname"]
-
- try:
- return pymysql.connect(host=mysqlHost, user=mysqlUser,
- passwd=mysqlPass, db=mysqlDB, charset='utf8')
- except KeyError:
- print(
- "Rest in peperoni, Impossible de se connecter a la base de données.")
- print(str(KeyError))
- return
+class SchemaError(Exception):
+ pass
-def reconnect_to_db(self):
- if not self.conn:
- mysqlHost = self.bot.config.mysql["host"]
- mysqlUser = self.bot.config.mysql["username"]
- mysqlPass = self.bot.config.mysql["password"]
- mysqlDB = self.bot.config.mysql["dbname"]
+class SQLType:
+ python = None
- return pymysql.connect(host=mysqlHost, user=mysqlUser,
- passwd=mysqlPass, db=mysqlDB, charset='utf8')
- return self.conn
+ def to_dict(self):
+ o = self.__dict__.copy()
+ cls = self.__class__
+ o['__meta__'] = cls.__module__ + '.' + cls.__qualname__
+ return o
+
+ @classmethod
+ def from_dict(cls, data):
+ meta = data.pop('__meta__')
+ given = cls.__module__ + '.' + cls.__qualname__
+ if given != meta:
+ cls = pydoc.locate(meta)
+ if cls is None:
+ raise RuntimeError('Could not locate "%s".' % meta)
+
+ self = cls.__new__(cls)
+ self.__dict__.update(data)
+ return self
+
+ def __eq__(self, other):
+ return isinstance(other,
+ self.__class__) and self.__dict__ == other.__dict__
+
+ def __ne__(self, other):
+ return not self.__eq__(other)
+
+ def to_sql(self):
+ raise NotImplementedError()
+
+ def is_real_type(self):
+ return True
+
+
+class Binary(SQLType):
+ python = bytes
+
+ def to_sql(self):
+ return 'BYTEA'
+
+
+class Boolean(SQLType):
+ python = bool
+
+ def to_sql(self):
+ return 'BOOLEAN'
+
+
+class Date(SQLType):
+ python = datetime.date
+
+ def to_sql(self):
+ return 'DATE'
+
+
+class Datetime(SQLType):
+ python = datetime.datetime
+
+ def __init__(self, *, timezone=False):
+ self.timezone = timezone
+
+ def to_sql(self):
+ if self.timezone:
+ return 'TIMESTAMP WITH TIME ZONE'
+ return 'TIMESTAMP'
+
+
+class Double(SQLType):
+ python = float
+
+ def to_sql(self):
+ return 'REAL'
+
+
+class Float(SQLType):
+ python = float
+
+ def to_sql(self):
+ return 'FLOAT'
+
+
+class Integer(SQLType):
+ python = int
+
+ def __init__(self, *, big=False, small=False, auto_increment=False):
+ self.big = big
+ self.small = small
+ self.auto_increment = auto_increment
+
+ if big and small:
+ raise SchemaError(
+ 'Integer column type cannot be both big and small.')
+
+ def to_sql(self):
+ if self.auto_increment:
+ if self.big:
+ return 'BIGSERIAL'
+ if self.small:
+ return 'SMALLSERIAL'
+ return 'SERIAL'
+ if self.big:
+ return 'BIGINT'
+ if self.small:
+ return 'SMALLINT'
+ return 'INTEGER'
+
+ def is_real_type(self):
+ return not self.auto_increment
+
+
+class Interval(SQLType):
+ python = datetime.timedelta
+
+ def __init__(self, field=None):
+ if field:
+ field = field.upper()
+ if field not in (
+ 'YEAR', 'MONTH', 'DAY', 'HOUR', 'MINUTE', 'SECOND',
+ 'YEAR TO MONTH', 'DAY TO HOUR', 'DAY TO MINUTE', 'DAY TO SECOND',
+ 'HOUR TO MINUTE', 'HOUR TO SECOND', 'MINUTE TO SECOND'):
+ raise SchemaError('invalid interval specified')
+ self.field = field
+ else:
+ self.field = None
+
+ def to_sql(self):
+ if self.field:
+ return 'INTERVAL ' + self.field
+ return 'INTERVAL'
+
+
+class Numeric(SQLType):
+ python = decimal.Decimal
+
+ def __init__(self, *, precision=None, scale=None):
+ if precision is not None:
+ if precision < 0 or precision > 1000:
+ raise SchemaError(
+ 'precision must be greater than 0 and below 1000')
+ if scale is None:
+ scale = 0
+
+ self.precision = precision
+ self.scale = scale
+
+ def to_sql(self):
+ if self.precision is not None:
+ return 'NUMERIC({0.precision}, {0.scale})'.format(self)
+ return 'NUMERIC'
+
+
+class String(SQLType):
+ python = str
+
+ def __init__(self, *, length=None, fixed=False):
+ self.length = length
+ self.fixed = fixed
+
+ if fixed and length is None:
+ raise SchemaError('Cannot have fixed string with no length')
+
+ def to_sql(self):
+ if self.length is None:
+ return 'TEXT'
+ if self.fixed:
+ return 'CHAR({0.length})'.format(self)
+ return 'VARCHAR({0.length})'.format(self)
+
+
+class Time(SQLType):
+ python = datetime.time
+
+ def __init__(self, *, timezone=False):
+ self.timezone = timezone
+
+ def to_sql(self):
+ if self.timezone:
+ return 'TIME WITH TIME ZONE'
+ return 'TIME'
+
+
+class JSON(SQLType):
+ python = None
+
+ def to_sql(self):
+ return 'JSONB'
+
+
+class ForeignKey(SQLType):
+ def __init__(self, table, column, *, sql_type=None, on_delete='CASCADE',
+ on_update='NO ACTION'):
+ if not table or not isinstance(table, str):
+ raise SchemaError('missing table to reference (must be string)')
+
+ valid_actions = (
+ 'NO ACTION',
+ 'RESTRICT',
+ 'CASCADE',
+ 'SET NULL',
+ 'SET DEFAULT',
+ )
+
+ on_delete = on_delete.upper()
+ on_update = on_update.upper()
+
+ if on_delete not in valid_actions:
+ raise TypeError('on_delete must be one of %s.' % valid_actions)
+
+ if on_update not in valid_actions:
+ raise TypeError('on_update must be one of %s.' % valid_actions)
+
+ self.table = table
+ self.column = column
+ self.on_update = on_update
+ self.on_delete = on_delete
+
+ if sql_type is None:
+ sql_type = Integer
+
+ if inspect.isclass(sql_type):
+ sql_type = sql_type()
+
+ if not isinstance(sql_type, SQLType):
+ raise TypeError('Cannot have non-SQLType derived sql_type')
+
+ if not sql_type.is_real_type():
+ raise SchemaError('sql_type must be a "real" type')
+
+ self.sql_type = sql_type.to_sql()
+
+ def is_real_type(self):
+ return False
+
+ def to_sql(self):
+ fmt = '{0.sql_type} REFERENCES {0.table} ({0.column})' \
+ ' ON DELETE {0.on_delete} ON UPDATE {0.on_update}'
+ return fmt.format(self)
+
+
+class Array(SQLType):
+ python = list
+
+ def __init__(self, sql_type):
+ if inspect.isclass(sql_type):
+ sql_type = sql_type()
+
+ if not isinstance(sql_type, SQLType):
+ raise TypeError('Cannot have non-SQLType derived sql_type')
+
+ if not sql_type.is_real_type():
+ raise SchemaError('sql_type must be a "real" type')
+
+ self.sql_type = sql_type.to_sql()
+
+ def to_sql(self):
+ return '{0.sql_type} ARRAY'.format(self)
+
+ def is_real_type(self):
+ # technically, it is a real type
+ # however, it doesn't play very well with migrations
+ # so we're going to pretend that it isn't
+ return False
+
+
+class Column:
+ __slots__ = ('column_type', 'index', 'primary_key', 'nullable',
+ 'default', 'unique', 'name', 'index_name')
+
+ def __init__(self, column_type, *, index=False, primary_key=False,
+ nullable=True, unique=False, default=None, name=None):
+
+ if inspect.isclass(column_type):
+ column_type = column_type()
+
+ if not isinstance(column_type, SQLType):
+ raise TypeError('Cannot have a non-SQLType derived column_type')
+
+ self.column_type = column_type
+ self.index = index
+ self.unique = unique
+ self.primary_key = primary_key
+ self.nullable = nullable
+ self.default = default
+ self.name = name
+ self.index_name = None # to be filled later
+
+ if sum(map(bool, (unique, primary_key, default is not None))) > 1:
+ raise SchemaError(
+ "'unique', 'primary_key', and 'default' are mutually exclusive.")
+
+ @classmethod
+ def from_dict(cls, data):
+ index_name = data.pop('index_name', None)
+ column_type = data.pop('column_type')
+ column_type = SQLType.from_dict(column_type)
+ self = cls(column_type=column_type, **data)
+ self.index_name = index_name
+ return self
+
+ @property
+ def _comparable_id(self):
+ return '-'.join(
+ '%s:%s' % (attr, getattr(self, attr)) for attr in self.__slots__)
+
+ def _to_dict(self):
+ d = {
+ attr: getattr(self, attr)
+ for attr in self.__slots__
+ }
+ d['column_type'] = self.column_type.to_dict()
+ return d
+
+ def _qualifiers_dict(self):
+ return {attr: getattr(self, attr) for attr in ('nullable', 'default')}
+
+ def _is_rename(self, other):
+ if self.name == other.name:
+ return False
+
+ return self.unique == other.unique and self.primary_key == other.primary_key
+
+ def _create_table(self):
+ builder = []
+ builder.append(self.name)
+ builder.append(self.column_type.to_sql())
+
+ default = self.default
+ if default is not None:
+ builder.append('DEFAULT')
+ if isinstance(default, str) and isinstance(self.column_type,
+ String):
+ builder.append("'%s'" % default)
+ elif isinstance(default, bool):
+ builder.append(str(default).upper())
+ else:
+ builder.append("(%s)" % default)
+ elif self.unique:
+ builder.append('UNIQUE')
+ if not self.nullable:
+ builder.append('NOT NULL')
+
+ return ' '.join(builder)
+
+
+class PrimaryKeyColumn(Column):
+ """Shortcut for a SERIAL PRIMARY KEY column."""
+
+ def __init__(self):
+ super().__init__(Integer(auto_increment=True), primary_key=True)
+
+
+class SchemaDiff:
+ __slots__ = ('table', 'upgrade', 'downgrade')
+
+ def __init__(self, table, upgrade, downgrade):
+ self.table = table
+ self.upgrade = upgrade
+ self.downgrade = downgrade
+
+ def to_dict(self):
+ return {'upgrade': self.upgrade, 'downgrade': self.downgrade}
+
+ def is_empty(self):
+ return len(self.upgrade) == 0 and len(self.downgrade) == 0
+
+ def to_sql(self, *, downgrade=False):
+ statements = []
+ base = 'ALTER TABLE %s ' % self.table.__tablename__
+ path = self.upgrade if not downgrade else self.downgrade
+
+ for rename in path.get('rename_columns', []):
+ fmt = '{0}RENAME COLUMN {1[before]} TO {1[after]};'.format(base,
+ rename)
+ statements.append(fmt)
+
+ sub_statements = []
+ for dropped in path.get('remove_columns', []):
+ fmt = 'DROP COLUMN {0[name]} RESTRICT'.format(dropped)
+ sub_statements.append(fmt)
+
+ for changed_types in path.get('changed_column_types', []):
+ fmt = 'ALTER COLUMN {0[name]} SET DATA TYPE {0[type]}'.format(
+ changed_types)
+
+ using = changed_types.get('using')
+ if using is not None:
+ fmt = '%s USING %s' % (fmt, using)
+
+ sub_statements.append(fmt)
+
+ for constraints in path.get('changed_constraints', []):
+ before, after = constraints['before'], constraints['after']
+
+ before_default, after_default = before.get('default'), after.get(
+ 'default')
+ if before_default is None and after_default is not None:
+ fmt = 'ALTER COLUMN {0[name]} SET DEFAULT {1[default]}'.format(
+ constraints, after)
+ sub_statements.append(fmt)
+ elif before_default is not None and after_default is None:
+ fmt = 'ALTER COLUMN {0[name]} DROP DEFAULT'.format(constraints)
+ sub_statements.append(fmt)
+
+ before_nullable, after_nullable = before.get(
+ 'nullable'), after.get('nullable')
+ if not before_nullable and after_nullable:
+ fmt = 'ALTER COLUMN {0[name]} DROP NOT NULL'.format(
+ constraints)
+ sub_statements.append(fmt)
+ elif before_nullable and not after_nullable:
+ fmt = 'ALTER COLUMN {0[name]} SET NOT NULL'.format(constraints)
+ sub_statements.append(fmt)
+
+ for added in path.get('add_columns', []):
+ column = Column.from_dict(added)
+ sub_statements.append('ADD COLUMN ' + column._create_table())
+
+ if sub_statements:
+ statements.append(base + ', '.join(sub_statements) + ';')
+
+ # handle the index creation bits
+ for dropped in path.get('drop_index', []):
+ statements.append(
+ 'DROP INDEX IF EXISTS {0[index]};'.format(dropped))
+
+ for added in path.get('add_index', []):
+ fmt = 'CREATE INDEX IF NOT EXISTS {0[index]} ON {1.__tablename__} ({0[name]});'
+ statements.append(fmt.format(added, self.table))
+
+ return '\n'.join(statements)
+
+
+class MaybeAcquire:
+ def __init__(self, connection, *, pool):
+ self.connection = connection
+ self.pool = pool
+ self._cleanup = False
+
+ async def __aenter__(self):
+ if self.connection is None:
+ self._cleanup = True
+ self._connection = c = await self.pool.acquire()
+ return c
+ return self.connection
+
+ async def __aexit__(self, *args):
+ if self._cleanup:
+ await self.pool.release(self._connection)
+
+
+class TableMeta(type):
+ @classmethod
+ def __prepare__(cls, name, bases, **kwargs):
+ return OrderedDict()
+
+ def __new__(cls, name, parents, dct, **kwargs):
+ columns = []
+
+ try:
+ table_name = kwargs['table_name']
+ except KeyError:
+ table_name = name.lower()
+
+ dct['__tablename__'] = table_name
+
+ for elem, value in dct.items():
+ if isinstance(value, Column):
+ if value.name is None:
+ value.name = elem
+
+ if value.index:
+ value.index_name = '%s_%s_idx' % (table_name, value.name)
+
+ columns.append(value)
+
+ dct['columns'] = columns
+ return super().__new__(cls, name, parents, dct)
+
+ def __init__(self, name, parents, dct, **kwargs):
+ super().__init__(name, parents, dct)
+
+
+class Table(metaclass=TableMeta):
+ @classmethod
+ async def create_pool(cls, uri, **kwargs):
+ """Sets up and returns the PostgreSQL connection pool that is used.
+
+ .. note::
+
+ This must be called at least once before doing anything with the tables.
+ And must be called on the ``Table`` class.
+
+ Parameters
+ -----------
+ uri: str
+ The PostgreSQL URI to connect to.
+ \*\*kwargs
+ The arguments to forward to asyncpg.create_pool.
+ """
+
+ def _encode_jsonb(value):
+ return json.dumps(value)
+
+ def _decode_jsonb(value):
+ return json.loads(value)
+
+ old_init = kwargs.pop('init', None)
+
+ async def init(con):
+ await con.set_type_codec('jsonb', schema='pg_catalog',
+ encoder=_encode_jsonb,
+ decoder=_decode_jsonb, format='text')
+ if old_init is not None:
+ await old_init(con)
+
+ cls._pool = pool = await asyncpg.create_pool(uri, init=init, **kwargs)
+ return pool
+
+ @classmethod
+ def acquire_connection(cls, connection):
+ return MaybeAcquire(connection, pool=cls._pool)
+
+ @classmethod
+ def write_migration(cls, *, directory='migrations'):
+ """Writes the migration diff into the data file.
+
+ Note
+ ------
+ This doesn't actually commit/do the migration.
+ To do so, use :meth:`migrate`.
+
+ Returns
+ --------
+ bool
+ ``True`` if a migration was written, ``False`` otherwise.
+
+ Raises
+ -------
+ RuntimeError
+ Could not find the migration data necessary.
+ """
+
+ directory = Path(directory) / cls.__tablename__
+ p = directory.with_suffix('.json')
+
+ if not p.exists():
+ raise RuntimeError('Could not find migration file.')
+
+ current = directory.with_name('current-' + p.name)
+
+ if not current.exists():
+ raise RuntimeError('Could not find current data file.')
+
+ with current.open() as fp:
+ current_table = cls.from_dict(json.load(fp))
+
+ diff = cls().diff(current_table)
+
+ # the most common case, no difference
+ if diff.is_empty():
+ return None
+
+ # load the migration data
+ with p.open('r', encoding='utf-8') as fp:
+ data = json.load(fp)
+ migrations = data['migrations']
+
+ # check if we should add it
+ our_migrations = diff.to_dict()
+ if len(migrations) == 0 or migrations[-1] != our_migrations:
+ # we have a new migration, so add it
+ migrations.append(our_migrations)
+ temp_file = p.with_name('%s-%s.tmp' % (uuid.uuid4(), p.name))
+ with temp_file.open('w', encoding='utf-8') as tmp:
+ json.dump(data, tmp, ensure_ascii=True, indent=4)
+
+ temp_file.replace(p)
+ return True
+ return False
+
+ @classmethod
+ async def migrate(cls, *, directory='migrations', index=-1,
+ downgrade=False, verbose=False, connection=None):
+ """Actually run the latest migration pointed by the data file.
+
+ Parameters
+ -----------
+ directory: str
+ The directory of where the migration data file resides.
+ index: int
+ The index of the migration array to use.
+ downgrade: bool
+ Whether to run an upgrade or a downgrade.
+ verbose: bool
+ Whether to output some information to stdout.
+ connection: Optional[asyncpg.Connection]
+ The connection to use, if not provided will acquire one from
+ the internal pool.
+ """
+
+ directory = Path(directory) / cls.__tablename__
+ p = directory.with_suffix('.json')
+ if not p.exists():
+ raise RuntimeError('Could not find migration file.')
+
+ with p.open('r', encoding='utf-8') as fp:
+ data = json.load(fp)
+ migrations = data['migrations']
+
+ try:
+ migration = migrations[index]
+ except IndexError:
+ return False
+
+ diff = SchemaDiff(cls, migration['upgrade'], migration['downgrade'])
+ if diff.is_empty():
+ return False
+
+ async with MaybeAcquire(connection, pool=cls._pool) as con:
+ sql = diff.to_sql(downgrade=downgrade)
+ if verbose:
+ print(sql)
+ await con.execute(sql)
+
+ current = directory.with_name('current-' + p.name)
+ with current.open('w', encoding='utf-8') as fp:
+ json.dump(cls.to_dict(), fp, indent=4, ensure_ascii=True)
+
+ @classmethod
+ async def create(cls, *, directory='migrations', verbose=False,
+ connection=None, run_migrations=True):
+ """Creates the database and manages migrations, if any.
+
+ Parameters
+ -----------
+ directory: str
+ The migrations directory.
+ verbose: bool
+ Whether to output some information to stdout.
+ connection: Optional[asyncpg.Connection]
+ The connection to use, if not provided will acquire one from
+ the internal pool.
+ run_migrations: bool
+ Whether to run migrations at all.
+
+ Returns
+ --------
+ Optional[bool]
+ ``True`` if the table was successfully created or
+ ``False`` if the table was successfully migrated or
+ ``None`` if no migration took place.
+ """
+ directory = Path(directory) / cls.__tablename__
+ p = directory.with_suffix('.json')
+ current = directory.with_name('current-' + p.name)
+
+ table_data = cls.to_dict()
+
+ if not p.exists():
+ p.parent.mkdir(parents=True, exist_ok=True)
+
+ # we're creating this table for the first time,
+ # it's an uncommon case so let's get it out of the way
+ # first, try to actually create the table
+ async with MaybeAcquire(connection, pool=cls._pool) as con:
+ sql = cls.create_table(exists_ok=True)
+ if verbose:
+ print(sql)
+ await con.execute(sql)
+
+ # since that step passed, let's go ahead and make the migration
+ with p.open('w', encoding='utf-8') as fp:
+ data = {'table': table_data, 'migrations': []}
+ json.dump(data, fp, indent=4, ensure_ascii=True)
+
+ with current.open('w', encoding='utf-8') as fp:
+ json.dump(table_data, fp, indent=4, ensure_ascii=True)
+
+ return True
+
+ if not run_migrations:
+ return None
+
+ with current.open() as fp:
+ current_table = cls.from_dict(json.load(fp))
+
+ diff = cls().diff(current_table)
+
+ # the most common case, no difference
+ if diff.is_empty():
+ return None
+
+ # execute the upgrade SQL
+ async with MaybeAcquire(connection, pool=cls._pool) as con:
+ sql = diff.to_sql()
+ if verbose:
+ print(sql)
+ await con.execute(sql)
+
+ # load the migration data
+ with p.open('r', encoding='utf-8') as fp:
+ data = json.load(fp)
+ migrations = data['migrations']
+
+ # check if we should add it
+ our_migrations = diff.to_dict()
+ if len(migrations) == 0 or migrations[-1] != our_migrations:
+ # we have a new migration, so add it
+ migrations.append(our_migrations)
+ temp_file = p.with_name('%s-%s.tmp' % (uuid.uuid4(), p.name))
+ with temp_file.open('w', encoding='utf-8') as tmp:
+ json.dump(data, tmp, ensure_ascii=True, indent=4)
+
+ temp_file.replace(p)
+
+ # update our "current" data in the filesystem
+ with current.open('w', encoding='utf-8') as fp:
+ json.dump(table_data, fp, indent=4, ensure_ascii=True)
+
+ return False
+
+ @classmethod
+ async def drop(cls, *, directory='migrations', verbose=False,
+ connection=None):
+ """Drops the database and migrations, if any.
+
+ Parameters
+ -----------
+ directory: str
+ The migrations directory.
+ verbose: bool
+ Whether to output some information to stdout.
+ connection: Optional[asyncpg.Connection]
+ The connection to use, if not provided will acquire one from
+ the internal pool.
+ """
+
+ directory = Path(directory) / cls.__tablename__
+ p = directory.with_suffix('.json')
+ current = directory.with_name('current-' + p.name)
+
+ if not p.exists() or not current.exists():
+ raise RuntimeError('Could not find the appropriate data files.')
+
+ try:
+ p.unlink()
+ except:
+ raise RuntimeError('Could not delete migration file')
+
+ try:
+ current.unlink()
+ except:
+ raise RuntimeError('Could not delete current migration file')
+
+ async with MaybeAcquire(connection, pool=cls._pool) as con:
+ sql = 'DROP TABLE {0} CASCADE;'.format(cls.__tablename__)
+ if verbose:
+ print(sql)
+ await con.execute(sql)
+
+ @classmethod
+ def create_table(cls, *, exists_ok=True):
+ """Generates the CREATE TABLE stub."""
+ statements = []
+ builder = ['CREATE TABLE']
+
+ if exists_ok:
+ builder.append('IF NOT EXISTS')
+
+ builder.append(cls.__tablename__)
+ column_creations = []
+ primary_keys = []
+ for col in cls.columns:
+ column_creations.append(col._create_table())
+ if col.primary_key:
+ primary_keys.append(col.name)
+
+ column_creations.append('PRIMARY KEY (%s)' % ', '.join(primary_keys))
+ builder.append('(%s)' % ', '.join(column_creations))
+ statements.append(' '.join(builder) + ';')
+
+ # handle the index creations
+ for column in cls.columns:
+ if column.index:
+ fmt = 'CREATE INDEX IF NOT EXISTS {1.index_name} ON {0} ({1.name});'.format(
+ cls.__tablename__, column)
+ statements.append(fmt)
+
+ return '\n'.join(statements)
+
+ @classmethod
+ async def insert(cls, connection=None, **kwargs):
+ """Inserts an element to the table."""
+
+ # verify column names:
+ verified = {}
+ for column in cls.columns:
+ try:
+ value = kwargs[column.name]
+ except KeyError:
+ continue
+
+ check = column.column_type.python
+ if value is None and not column.nullable:
+ raise TypeError(
+ 'Cannot pass None to non-nullable column %s.' % column.name)
+ elif not check or not isinstance(value, check):
+ fmt = 'column {0.name} expected {1.__name__}, received {2.__class__.__name__}'
+ raise TypeError(fmt.format(column, check, value))
+
+ verified[column.name] = value
+
+ sql = 'INSERT INTO {0} ({1}) VALUES ({2});'.format(cls.__tablename__,
+ ', '.join(verified),
+ ', '.join(
+ '$' + str(i) for
+ i, _ in
+ enumerate(
+ verified,
+ 1)))
+
+ async with MaybeAcquire(connection, pool=cls._pool) as con:
+ await con.execute(sql, *verified.values())
+
+ @classmethod
+ def to_dict(cls):
+ x = {}
+ x['name'] = cls.__tablename__
+ x['__meta__'] = cls.__module__ + '.' + cls.__qualname__
+
+ # nb: columns is ordered due to the ordered dict usage
+ # this is used to help detect renames
+ x['columns'] = [a._to_dict() for a in cls.columns]
+ return x
+
+ @classmethod
+ def from_dict(cls, data):
+ meta = data['__meta__']
+ given = cls.__module__ + '.' + cls.__qualname__
+ if given != meta:
+ cls = pydoc.locate(meta)
+ if cls is None:
+ raise RuntimeError('Could not locate "%s".' % meta)
+
+ self = cls()
+ self.__tablename__ = data['name']
+ self.columns = [Column.from_dict(a) for a in data['columns']]
+ return self
+
+ @classmethod
+ def all_tables(cls):
+ return cls.__subclasses__()
+
+ def diff(self, before):
+ """Outputs the upgrade and downgrade path in JSON.
+
+ This isn't necessarily good, but it outputs it in a format
+ that allows the user to manually make edits if something is wrong.
+
+ The following JSON schema is used:
+
+ Note that every major key takes a list of objects as noted below.
+
+ Note that add_column and drop_column automatically create and drop
+ indices as necessary.
+
+ changed_column_types:
+ name: str [The column name]
+ type: str [The new column type]
+ using: Optional[str] [The USING expression to use, if applicable]
+ add_columns:
+ column: object
+ remove_columns:
+ column: object
+ rename_columns:
+ before: str [The previous column name]
+ after: str [The new column name]
+ drop_index:
+ name: str [The column name]
+ index: str [The index name]
+ add_index:
+ name: str [The column name]
+ index: str [The index name]
+ changed_constraints:
+ name: str [The column name]
+ before:
+ nullable: Optional[bool]
+ default: Optional[str]
+ after:
+ nullable: Optional[bool]
+ default: Optional[str]
+ """
+ upgrade = {}
+ downgrade = {}
+
+ def check_index_diff(a, b):
+ if a.index != b.index:
+ # Let's assume we have {name: thing, index: True}
+ # and we're going to { name: foo, index: False }
+ # This is a 'dropped' column when we upgrade with a rename
+ # care must be taken to use the old name when dropping
+
+ # check if we're dropping the index
+ if not a.index:
+ # we could also be renaming so make sure to use the old index name
+ upgrade.setdefault('drop_index', []).append(
+ {'name': a.name, 'index': b.index_name})
+ # if we want to roll back, we need to re-add the old index to the old column name
+ downgrade.setdefault('add_index', []).append(
+ {'name': b.name, 'index': b.index_name})
+ else:
+ # we're not dropping an index, instead we're adding one
+ upgrade.setdefault('add_index', []).append(
+ {'name': a.name, 'index': a.index_name})
+ downgrade.setdefault('drop_index', []).append(
+ {'name': a.name, 'index': a.index_name})
+
+ def insert_column_diff(a, b):
+ if a.column_type != b.column_type:
+ if a.name == b.name and a.column_type.is_real_type() and b.column_type.is_real_type():
+ upgrade.setdefault('changed_column_types', []).append(
+ {'name': a.name, 'type': a.column_type.to_sql()})
+ downgrade.setdefault('changed_column_types', []).append(
+ {'name': a.name, 'type': b.column_type.to_sql()})
+ else:
+ a_dict, b_dict = a._to_dict(), b._to_dict()
+ upgrade.setdefault('add_columns', []).append(a_dict)
+ upgrade.setdefault('remove_columns', []).append(b_dict)
+ downgrade.setdefault('remove_columns', []).append(a_dict)
+ downgrade.setdefault('add_columns', []).append(b_dict)
+ check_index_diff(a, b)
+ return
+
+ elif a._is_rename(b):
+ upgrade.setdefault('rename_columns', []).append(
+ {'before': b.name, 'after': a.name})
+ downgrade.setdefault('rename_columns', []).append(
+ {'before': a.name, 'after': b.name})
+
+ # technically, adding UNIQUE or PRIMARY KEY is rather simple and straight forward
+ # however, since the inverse is a little bit more complicated (you have to remove
+ # the index it maintains and you can't easily know what it is), it's not exactly
+ # worth supporting any sort of change to the uniqueness/primary_key as it stands.
+ # So.. just drop/add the column and call it a day.
+ if a.unique != b.unique or a.primary_key != b.primary_key:
+ a_dict, b_dict = a._to_dict(), b._to_dict()
+ upgrade.setdefault('add_columns', []).append(a_dict)
+ upgrade.setdefault('remove_columns', []).append(b_dict)
+ downgrade.setdefault('remove_columns', []).append(a_dict)
+ downgrade.setdefault('add_columns', []).append(b_dict)
+ check_index_diff(a, b)
+ return
+
+ check_index_diff(a, b)
+
+ b_qual, a_qual = b._qualifiers_dict(), a._qualifiers_dict()
+ if a_qual != b_qual:
+ upgrade.setdefault('changed_constraints', []).append(
+ {'name': a.name, 'before': b_qual, 'after': a_qual})
+ downgrade.setdefault('changed_constraints', []).append(
+ {'name': a.name, 'before': a_qual, 'after': b_qual})
+
+ if len(self.columns) == len(before.columns):
+ # check if we have any changes at all
+ for a, b in zip(self.columns, before.columns):
+ if a._comparable_id == b._comparable_id:
+ # no change
+ continue
+ insert_column_diff(a, b)
+
+ elif len(self.columns) > len(before.columns):
+ # check if we have more columns
+ # typically when we add columns we add them at the end of
+ # the table, this assumption makes this particularly bit easier.
+ # Breaking this assumption will probably break this portion and thus
+ # will require manual handling, sorry.
+
+ for a, b in zip(self.columns, before.columns):
+ if a._comparable_id == b._comparable_id:
+ # no change
+ continue
+ insert_column_diff(a, b)
+
+ new_columns = self.columns[len(before.columns):]
+ add, remove = upgrade.setdefault('add_columns',
+ []), downgrade.setdefault(
+ 'remove_columns', [])
+ for column in new_columns:
+ as_dict = column._to_dict()
+ add.append(as_dict)
+ remove.append(as_dict)
+ if column.index:
+ upgrade.setdefault('add_index', []).append(
+ {'name': column.name, 'index': column.index_name})
+ downgrade.setdefault('drop_index', []).append(
+ {'name': column.name, 'index': column.index_name})
+
+ elif len(self.columns) < len(before.columns):
+ # check if we have fewer columns
+ # this one is a little bit more complicated
+
+ # first we sort the columns by comparable IDs.
+ sorted_before = sorted(before.columns,
+ key=lambda c: c._comparable_id)
+ sorted_after = sorted(self.columns, key=lambda c: c._comparable_id)
+
+ # handle the column diffs:
+ for a, b in zip(sorted_after, sorted_before):
+ if a._comparable_id == b._comparable_id:
+ continue
+ insert_column_diff(a, b)
+
+ # check which columns are 'left over' and remove them
+ removed = [c._to_dict() for c in sorted_before[len(sorted_after):]]
+ upgrade.setdefault('remove_columns', []).extend(removed)
+ downgrade.setdefault('add_columns', []).extend(removed)
+
+ return SchemaDiff(self, upgrade, downgrade)
+
+
+async def _table_creator(tables, *, verbose=True):
+ for table in tables:
+ try:
+ await table.create(verbose=verbose)
+ except:
+ log.error('Failed to create table %s.', table.__tablename__)
+
+
+def create_tables(*tables, verbose=True, loop=None):
+ if loop is None:
+ loop = asyncio.get_event_loop()
+
+ loop.create_task(_table_creator(tables, verbose=verbose))
diff --git a/cogs/utils/formats.py b/cogs/utils/formats.py
deleted file mode 100755
index d483dac..0000000
--- a/cogs/utils/formats.py
+++ /dev/null
@@ -1,75 +0,0 @@
-async def entry_to_code(bot, entries):
- width = max(map(lambda t: len(t[0]), entries))
- output = ['```']
- fmt = '{0:<{width}}: {1}'
- for name, entry in entries:
- output.append(fmt.format(name, entry, width=width))
- output.append('```')
- await bot.say('\n'.join(output))
-
-import datetime
-
-async def indented_entry_to_code(bot, entries):
- width = max(map(lambda t: len(t[0]), entries))
- output = ['```']
- fmt = '\u200b{0:>{width}}: {1}'
- for name, entry in entries:
- output.append(fmt.format(name, entry, width=width))
- output.append('```')
- await bot.say('\n'.join(output))
-
-async def too_many_matches(bot, msg, matches, entry):
- check = lambda m: m.content.isdigit()
- await bot.say('There are too many matches... Which one did you mean? **Only say the number**.')
- await bot.say('\n'.join(map(entry, enumerate(matches, 1))))
-
- # only give them 3 tries.
- for i in range(3):
- message = await bot.wait_for_message(author=msg.author, channel=msg.channel, check=check)
- index = int(message.content)
- try:
- return matches[index - 1]
- except:
- await bot.say('Please give me a valid number. {} tries remaining...'.format(2 - i))
-
- raise ValueError('Too many tries. Goodbye.')
-
-class Plural:
- def __init__(self, **attr):
- iterator = attr.items()
- self.name, self.value = next(iter(iterator))
-
- def __str__(self):
- v = self.value
- if v > 1:
- return '%s %ss' % (v, self.name)
- return '%s %s' % (v, self.name)
-
-def human_timedelta(dt):
- now = datetime.datetime.utcnow()
- delta = now - dt
- hours, remainder = divmod(int(delta.total_seconds()), 3600)
- minutes, seconds = divmod(remainder, 60)
- days, hours = divmod(hours, 24)
- years, days = divmod(days, 365)
-
- if years:
- if days:
- return '%s and %s ago' % (Plural(year=years), Plural(day=days))
- return '%s ago' % Plural(year=years)
-
- if days:
- if hours:
- return '%s and %s ago' % (Plural(day=days), Plural(hour=hours))
- return '%s ago' % Plural(day=days)
-
- if hours:
- if minutes:
- return '%s and %s ago' % (Plural(hour=hours), Plural(minute=minutes))
- return '%s ago' % Plural(hour=hours)
-
- if minutes:
- if seconds:
- return '%s and %s ago' % (Plural(minute=minutes), Plural(second=seconds))
- return '%s ago' % Plural(minute=minutes)
- return '%s ago' % Plural(second=seconds)
diff --git a/cogs/utils/lang.py b/cogs/utils/lang.py
new file mode 100644
index 0000000..db41736
--- /dev/null
+++ b/cogs/utils/lang.py
@@ -0,0 +1,8 @@
+import gettext
+import config
+
+lang = gettext.translation('base', localedir='locales',
+ languages=[config.lang])
+lang.install()
+
+_ = lang.gettext
diff --git a/cogs/utils/maps.py b/cogs/utils/maps.py
deleted file mode 100755
index c062452..0000000
--- a/cogs/utils/maps.py
+++ /dev/null
@@ -1,147 +0,0 @@
-#!/bin/env python
-
-# With credit to DanielKO
-
-from lxml import etree
-import datetime, re
-import asyncio, aiohttp
-
-NINTENDO_LOGIN_PAGE = "https://id.nintendo.net/oauth/authorize"
-SPLATNET_CALLBACK_URL = "https://splatoon.nintendo.net/users/auth/nintendo/callback"
-SPLATNET_CLIENT_ID = "12af3d0a3a1f441eb900411bb50a835a"
-SPLATNET_SCHEDULE_URL = "https://splatoon.nintendo.net/schedule"
-
-class Rotation(object):
- def __init__(self):
- self.start = None
- self.end = None
- self.turf_maps = []
- self.ranked_mode = None
- self.ranked_maps = []
-
-
- @property
- def is_over(self):
- return self.end < datetime.datetime.utcnow()
-
- def __str__(self):
- now = datetime.datetime.utcnow()
- prefix = ''
- if self.start > now:
- minutes_delta = int((self.start - now) / datetime.timedelta(minutes=1))
- hours = int(minutes_delta / 60)
- minutes = minutes_delta % 60
- prefix = '**In {0} hours and {1} minutes**:\n'.format(hours, minutes)
- else:
- prefix = '**Current Rotation**:\n'
-
- fmt = 'Turf War is {0[0]} and {0[1]}\n{1} is {2[0]} and {2[1]}'
- return prefix + fmt.format(self.turf_maps, self.ranked_mode, self.ranked_maps)
-
-# based on https://github.com/Wiwiweb/SakuraiBot/blob/master/src/sakuraibot.py
-async def get_new_splatnet_cookie(username, password):
- parameters = {'client_id': SPLATNET_CLIENT_ID,
- 'response_type': 'code',
- 'redirect_uri': SPLATNET_CALLBACK_URL,
- 'username': username,
- 'password': password}
-
- async with aiohttp.post(NINTENDO_LOGIN_PAGE, data=parameters) as response:
- cookie = response.history[-1].cookies.get('_wag_session')
- if cookie is None:
- print(req)
- raise Exception("Couldn't retrieve cookie")
- return cookie
-
-def parse_splatnet_time(timestr):
- # time is given as "MM/DD at H:MM [p|a].m. (PDT|PST)"
- # there is a case where it goes over the year, e.g. 12/31 at ... and then 1/1 at ...
- # this case is kind of weird though and is currently unexpected
- # it could even end up being e.g. 12/31/2015 ... and then 1/1/2016 ...
- # we'll never know
-
- regex = r'(?P<month>\d+)\/(?P<day>\d+)\s*at\s*(?P<hour>\d+)\:(?P<minutes>\d+)\s*(?P<p>a\.m\.|p\.m\.)\s*\((?P<tz>.+)\)'
- m = re.match(regex, timestr.strip())
-
- if m is None:
- raise RuntimeError('Apparently the timestamp "{}" does not match the regex.'.format(timestr))
-
- matches = m.groupdict()
- tz = matches['tz'].strip().upper()
- offset = None
- if tz == 'PDT':
- # EDT is UTC - 4, PDT is UTC - 7, so you need +7 to make it UTC
- offset = +7
- elif tz == 'PST':
- # EST is UTC - 5, PST is UTC - 8, so you need +8 to make it UTC
- offset = +8
- else:
- raise RuntimeError('Unknown timezone found: {}'.format(tz))
-
- pm = matches['p'].replace('.', '') # a.m. -> am
-
- current_time = datetime.datetime.utcnow()
-
- # Kind of hacky.
- fmt = "{2}/{0[month]}/{0[day]} {0[hour]}:{0[minutes]} {1}".format(matches, pm, current_time.year)
- splatoon_time = datetime.datetime.strptime(fmt, '%Y/%m/%d %I:%M %p') + datetime.timedelta(hours=offset)
-
- # check for new year
- if current_time.month == 12 and splatoon_time.month == 1:
- splatoon_time.replace(current_time.year + 1)
-
- return splatoon_time
-
-
-async def get_splatnet_schedule(splatnet_cookie):
- cookies = {'_wag_session': splatnet_cookie}
-
-
- """
- This is repeated 3 times:
-
- <span class"stage-schedule"> ... </span> <--- figure out how to parse this
- <div class="stage-list">
- <div class="match-type">
- <span class="icon-regular-match"></span> <--- turf war
- </div>
- ... <span class="map-name"> ... </span>
- ... <span class="map-name"> ... </span>
- </div>
- <div class="stage-list">
- <div class="match-type">
- <span class="icon-earnest-match"></span> <--- ranked
- </div>
- ... <span class="rule-description"> ... </span> <--- Splat Zones, Rainmaker, Tower Control
- ... <span class="map-name"> ... </span>
- ... <span class="map-name"> ... </span>
- </div>
- """
-
- schedule = []
- async with aiohttp.get(SPLATNET_SCHEDULE_URL, cookies=cookies, data={'locale':"en"}) as response:
- text = await response.text()
- root = etree.fromstring(text, etree.HTMLParser())
- stage_schedule_nodes = root.xpath("//*[@class='stage-schedule']")
- stage_list_nodes = root.xpath("//*[@class='stage-list']")
-
- if len(stage_schedule_nodes)*2 != len(stage_list_nodes):
- raise RuntimeError("SplatNet changed, need to update the parsing!")
-
- for sched_node in stage_schedule_nodes:
- r = Rotation()
-
- start_time, end_time = sched_node.text.split("~")
- r.start = parse_splatnet_time(start_time)
- r.end = parse_splatnet_time(end_time)
-
- tw_list_node = stage_list_nodes.pop(0)
- r.turf_maps = tw_list_node.xpath(".//*[@class='map-name']/text()")
-
- ranked_list_node = stage_list_nodes.pop(0)
- r.ranked_maps = ranked_list_node.xpath(".//*[@class='map-name']/text()")
- r.ranked_mode = ranked_list_node.xpath(".//*[@class='rule-description']/text()")[0]
-
- schedule.append(r)
-
- return schedule
diff --git a/cogs/utils/menu.py b/cogs/utils/menu.py
deleted file mode 100755
index ea2c809..0000000
--- a/cogs/utils/menu.py
+++ /dev/null
@@ -1,140 +0,0 @@
-import asyncio
-
-class Menu:
- """An interactive menu class for Discord."""
-
-
- class Submenu:
- """A metaclass of the Menu class."""
- def __init__(self, name, content):
- self.content = content
- self.leads_to = []
- self.name = name
-
- def get_text(self):
- text = ""
- for idx, menu in enumerate(self.leads_to):
- text += "[{}] {}\n".format(idx+1, menu.name)
- return text
-
- def get_child(self, child_idx):
- try:
- return self.leads_to[child_idx]
- except IndexError:
- raise IndexError("child index out of range")
-
- def add_child(self, child):
- self.leads_to.append(child)
-
- class InputSubmenu:
- """A metaclass of the Menu class for submenu options that take input, instead of prompting the user to pick an option."""
- def __init__(self, name, content, input_function, leads_to):
- self.content = content
- self.name = name
- self.input_function = input_function
- self.leads_to = leads_to
-
- def next_child(self):
- return self.leads_to
-
- class ChoiceSubmenu:
- """A metaclass of the Menu class for submenu options for choosing an option from a list."""
- def __init__(self, name, content, options, input_function, leads_to):
- self.content = content
- self.name = name
- self.options = options
- self.input_function = input_function
- self.leads_to = leads_to
-
- def next_child(self):
- return self.leads_to
-
-
- def __init__(self, main_page):
- self.children = []
- self.main = self.Submenu("main", main_page)
-
- def add_child(self, child):
- self.main.add_child(child)
-
- async def start(self, ctx):
- current = self.main
- menu_msg = None
- while True:
- output = ""
-
- if type(current) == self.Submenu:
- if type(current.content) == str:
- output += current.content + "\n"
- elif callable(current.content):
- current.content()
- else:
- raise TypeError("submenu body is not a str or function")
-
- if not current.leads_to:
- if not menu_msg:
- menu_msg = await ctx.send("```" + output + "```")
- else:
- await menu_msg.edit(content="```" + output + "```")
- break
-
- output += "\n" + current.get_text() + "\n"
- output += "Enter a number."
-
- if not menu_msg:
- menu_msg = await ctx.send("```" + output + "```")
- else:
- await menu_msg.edit(content="```" + output + "```")
-
- reply = await ctx.bot.wait_for("message", check=lambda m: m.author == ctx.bot.user and m.content.isdigit() and m.channel == ctx.message.channel)
- await reply.delete()
-
- try:
- current = current.get_child(int(reply.content) - 1)
- except IndexError:
- print("Invalid number.")
- break
-
- elif type(current) == self.InputSubmenu:
- if type(current.content) == list:
- answers = []
- for question in current.content:
- await menu_msg.edit(content="```" + question + "\n\nEnter a value." + "```")
- reply = await ctx.bot.wait_for("message", check=lambda m: m.author == ctx.bot.user and m.channel == ctx.message.channel)
- await reply.delete()
- answers.append(reply)
- current.input_function(*answers)
- else:
- await menu_msg.edit(content="```" + current.content + "\n\nEnter a value." + "```")
- reply = await ctx.bot.wait_for("message", check=lambda m: m.author == ctx.bot.user and m.channel == ctx.message.channel)
- await reply.delete()
- current.input_function(reply)
-
- if not current.leads_to:
- break
-
- current = current.leads_to
-
- elif type(current) == self.ChoiceSubmenu:
- result = "```" + current.content + "\n\n"
- if type(current.options) == dict:
- indexes = {}
- for idx, option in enumerate(current.options):
- result += "[{}] {}: {}\n".format(idx+1, option, current.options[option])
- indexes[idx] = option
- else:
- for idx, option in current.options:
- result += "[{}] {}\n".format(idx+1, option)
- await menu_msg.edit(content=result + "\nPick an option.```")
- reply = await ctx.bot.wait_for("message", check=lambda m: m.author == ctx.bot.user and m.content.isdigit() and m.channel == ctx.message.channel)
- await reply.delete()
- if type(current.options) == dict:
- current.input_function(reply, indexes[int(reply.content)-1])
- else:
- current.input_function(reply, current.options[reply-1])
-
- if not current.leads_to:
- break
-
- current = current.leads_to
-
\ No newline at end of file
diff --git a/cogs/utils/paginator.py b/cogs/utils/paginator.py
deleted file mode 100755
index 8947bad..0000000
--- a/cogs/utils/paginator.py
+++ /dev/null
@@ -1,503 +0,0 @@
-# Help paginator by Rapptz
-# Edited by F4stZ4p
-
-import asyncio
-import discord
-
-class CannotPaginate(Exception):
- pass
-
-class Pages:
- """Implements a paginator that queries the user for the
- pagination interface.
- Pages are 1-index based, not 0-index based.
- If the user does not reply within 2 minutes then the pagination
- interface exits automatically.
- Parameters
- ------------
- ctx: Context
- The context of the command.
- entries: List[str]
- A list of entries to paginate.
- per_page: int
- How many entries show up per page.
- show_entry_count: bool
- Whether to show an entry count in the footer.
- Attributes
- -----------
- embed: discord.Embed
- The embed object that is being used to send pagination info.
- Feel free to modify this externally. Only the description,
- footer fields, and colour are internally modified.
- permissions: discord.Permissions
- Our permissions for the channel.
- """
- def __init__(self, ctx, *, entries, per_page=12, show_entry_count=True):
- self.bot = ctx.bot
- self.entries = entries
- self.message = ctx.message
- self.channel = ctx.channel
- self.author = ctx.author
- self.per_page = per_page
- pages, left_over = divmod(len(self.entries), self.per_page)
- if left_over:
- pages += 1
- self.maximum_pages = pages
- self.embed = discord.Embed(colour=discord.Color.green())
- self.paginating = len(entries) > per_page
- self.show_entry_count = show_entry_count
- self.reaction_emojis = [
- ('\N{BLACK LEFT-POINTING DOUBLE TRIANGLE WITH VERTICAL BAR}', self.first_page),
- ('\N{BLACK LEFT-POINTING TRIANGLE}', self.previous_page),
- ('\N{BLACK RIGHT-POINTING TRIANGLE}', self.next_page),
- ('\N{BLACK RIGHT-POINTING DOUBLE TRIANGLE WITH VERTICAL BAR}', self.last_page),
- ('\N{INPUT SYMBOL FOR NUMBERS}', self.numbered_page ),
- ('\N{BLACK SQUARE FOR STOP}', self.stop_pages),
- ('\N{INFORMATION SOURCE}', self.show_help),
- ]
-
- if ctx.guild is not None:
- self.permissions = self.channel.permissions_for(ctx.guild.me)
- else:
- self.permissions = self.channel.permissions_for(ctx.bot.user)
-
- if not self.permissions.embed_links:
- raise CannotPaginate('Bot does not have embed links permission.')
-
- if not self.permissions.send_messages:
- raise CannotPaginate('Bot cannot send messages.')
-
- if self.paginating:
- # verify we can actually use the pagination session
- if not self.permissions.add_reactions:
- raise CannotPaginate('Bot does not have add reactions permission.')
-
- if not self.permissions.read_message_history:
- raise CannotPaginate('Bot does not have Read Message History permission.')
-
- def get_page(self, page):
- base = (page - 1) * self.per_page
- return self.entries[base:base + self.per_page]
-
- async def show_page(self, page, *, first=False):
- self.current_page = page
- entries = self.get_page(page)
- p = []
- for index, entry in enumerate(entries, 1 + ((page - 1) * self.per_page)):
- p.append(f'{index}. {entry}')
-
- if self.maximum_pages > 1:
- if self.show_entry_count:
- text = f'Page {page}/{self.maximum_pages} ({len(self.entries)} entries)'
- else:
- text = f'Page {page}/{self.maximum_pages}'
-
- self.embed.set_footer(text=text)
-
- if not self.paginating:
- self.embed.description = '\n'.join(p)
- return await self.channel.send(embed=self.embed)
-
- if not first:
- self.embed.description = '\n'.join(p)
- await self.message.edit(embed=self.embed)
- return
-
- p.append('')
- p.append('Confused? React with \N{INFORMATION SOURCE} for more info.')
- self.embed.description = '\n'.join(p)
- self.message = await self.channel.send(embed=self.embed)
- for (reaction, _) in self.reaction_emojis:
- if self.maximum_pages == 2 and reaction in ('\u23ed', '\u23ee'):
- # no |<< or >>| buttons if we only have two pages
- # we can't forbid it if someone ends up using it but remove
- # it from the default set
- continue
-
- await self.message.add_reaction(reaction)
-
- async def checked_show_page(self, page):
- if page != 0 and page <= self.maximum_pages:
- await self.show_page(page)
-
- async def first_page(self):
- """goes to the first page"""
- await self.show_page(1)
-
- async def last_page(self):
- """goes to the last page"""
- await self.show_page(self.maximum_pages)
-
- async def next_page(self):
- """goes to the next page"""
- await self.checked_show_page(self.current_page + 1)
-
- async def previous_page(self):
- """goes to the previous page"""
- await self.checked_show_page(self.current_page - 1)
-
- async def show_current_page(self):
- if self.paginating:
- await self.show_page(self.current_page)
-
- async def numbered_page(self):
- """lets you type a page number to go to"""
- to_delete = []
- to_delete.append(await self.channel.send('What page do you want to go to?'))
-
- def message_check(m):
- return m.author == self.author and \
- self.channel == m.channel and \
- m.content.isdigit()
-
- try:
- msg = await self.bot.wait_for('message', check=message_check, timeout=30.0)
- except asyncio.TimeoutError:
- to_delete.append(await self.channel.send('Took too long.'))
- await asyncio.sleep(5)
- else:
- page = int(msg.content)
- to_delete.append(msg)
- if page != 0 and page <= self.maximum_pages:
- await self.show_page(page)
- else:
- to_delete.append(await self.channel.send(f'Invalid page given. ({page}/{self.maximum_pages})'))
- await asyncio.sleep(5)
-
- try:
- await self.channel.delete_messages(to_delete)
- except Exception:
- pass
-
- async def show_help(self):
- """shows this message"""
- messages = ['Welcome to the interactive paginator!\n']
- messages.append('This interactively allows you to see pages of text by navigating with ' \
- 'reactions. They are as follows:\n')
-
- for (emoji, func) in self.reaction_emojis:
- messages.append(f'{emoji} {func.__doc__}')
-
- self.embed.description = '\n'.join(messages)
- self.embed.clear_fields()
- self.embed.set_footer(text=f'We were on page {self.current_page} before this message.')
- await self.message.edit(embed=self.embed)
-
- async def go_back_to_current_page():
- await asyncio.sleep(60.0)
- await self.show_current_page()
-
- self.bot.loop.create_task(go_back_to_current_page())
-
- async def stop_pages(self):
- """stops the interactive pagination session"""
- await self.message.delete()
- self.paginating = False
-
- def react_check(self, reaction, user):
- if user is None or user.id != self.author.id:
- return False
-
- if reaction.message.id != self.message.id:
- return False
-
- for (emoji, func) in self.reaction_emojis:
- if reaction.emoji == emoji:
- self.match = func
- return True
- return False
-
- async def paginate(self):
- """Actually paginate the entries and run the interactive loop if necessary."""
- first_page = self.show_page(1, first=True)
- if not self.paginating:
- await first_page
- else:
- # allow us to react to reactions right away if we're paginating
- self.bot.loop.create_task(first_page)
-
- while self.paginating:
- try:
- reaction, user = await self.bot.wait_for('reaction_add', check=self.react_check, timeout=120.0)
- except asyncio.TimeoutError:
- self.paginating = False
- try:
- await self.message.clear_reactions()
- except:
- pass
- finally:
- break
-
- try:
- await self.message.remove_reaction(reaction, user)
- except:
- pass # can't remove it so don't bother doing so
-
- await self.match()
-
-class FieldPages(Pages):
- """Similar to Pages except entries should be a list of
- tuples having (key, value) to show as embed fields instead.
- """
- async def show_page(self, page, *, first=False):
- self.current_page = page
- entries = self.get_page(page)
-
- self.embed.clear_fields()
- self.embed.description = discord.Embed.Empty
-
- for key, value in entries:
- self.embed.add_field(name=key, value=value, inline=False)
-
- if self.maximum_pages > 1:
- if self.show_entry_count:
- text = f'Page {page}/{self.maximum_pages} ({len(self.entries)} entries)'
- else:
- text = f'Page {page}/{self.maximum_pages}'
-
- self.embed.set_footer(text=text)
-
- if not self.paginating:
- return await self.channel.send(embed=self.embed)
-
- if not first:
- await self.message.edit(embed=self.embed)
- return
-
- self.message = await self.channel.send(embed=self.embed)
- for (reaction, _) in self.reaction_emojis:
- if self.maximum_pages == 2 and reaction in ('\u23ed', '\u23ee'):
- # no |<< or >>| buttons if we only have two pages
- # we can't forbid it if someone ends up using it but remove
- # it from the default set
- continue
-
- await self.message.add_reaction(reaction)
-
-import itertools
-import inspect
-import re
-
-# ?help
-# ?help Cog
-# ?help command
-# -> could be a subcommand
-
-_mention = re.compile(r'<@\!?([0-9]{1,19})>')
-
-def cleanup_prefix(bot, prefix):
- m = _mention.match(prefix)
- if m:
- user = bot.get_user(int(m.group(1)))
- if user:
- return f'@{user.name} '
- return prefix
-
-async def _can_run(cmd, ctx):
- try:
- return await cmd.can_run(ctx)
- except:
- return False
-
-def _command_signature(cmd):
- # this is modified from discord.py source
- # which I wrote myself lmao
-
- result = [cmd.qualified_name]
- if cmd.usage:
- result.append(cmd.usage)
- return ' '.join(result)
-
- params = cmd.clean_params
- if not params:
- return ' '.join(result)
-
- for name, param in params.items():
- if param.default is not param.empty:
- # We don't want None or '' to trigger the [name=value] case and instead it should
- # do [name] since [name=None] or [name=] are not exactly useful for the user.
- should_print = param.default if isinstance(param.default, str) else param.default is not None
- if should_print:
- result.append(f'[{name}={param.default!r}]')
- else:
- result.append(f'[{name}]')
- elif param.kind == param.VAR_POSITIONAL:
- result.append(f'[{name}...]')
- else:
- result.append(f'<{name}>')
-
- return ' '.join(result)
-
-class HelpPaginator(Pages):
- def __init__(self, ctx, entries, *, per_page=4):
- super().__init__(ctx, entries=entries, per_page=per_page)
- self.reaction_emojis.append(('\N{WHITE QUESTION MARK ORNAMENT}', self.show_bot_help))
- self.total = len(entries)
-
- @classmethod
- async def from_cog(cls, ctx, cog):
- cog_name = cog.__class__.__name__
-
- # get the commands
- entries = sorted(ctx.bot.get_cog(cog_name).get_commands(), key=lambda c: c.name)
-
- # remove the ones we can't run
- entries = [cmd for cmd in entries if (await _can_run(cmd, ctx)) and not cmd.hidden]
-
- self = cls(ctx, entries)
- self.title = f'{cog_name} Commands'
- self.description = inspect.getdoc(cog)
- self.prefix = cleanup_prefix(ctx.bot, ctx.prefix)
-
- return self
-
- @classmethod
- async def from_command(cls, ctx, command):
- try:
- entries = sorted(command.commands, key=lambda c: c.name)
- except AttributeError:
- entries = []
- else:
- entries = [cmd for cmd in entries if (await _can_run(cmd, ctx)) and not cmd.hidden]
-
- self = cls(ctx, entries)
- self.title = command.signature
-
- if command.description:
- self.description = f'{command.description}\n\n{command.help}'
- else:
- self.description = command.help or 'No help given.'
-
- self.prefix = cleanup_prefix(ctx.bot, ctx.prefix)
- return self
-
- @classmethod
- async def from_bot(cls, ctx):
- def key(c):
- return c.cog_name or '\u200bMisc'
-
- entries = sorted(ctx.bot.commands, key=key)
- nested_pages = []
- per_page = 9
-
- # 0: (cog, desc, commands) (max len == 9)
- # 1: (cog, desc, commands) (max len == 9)
- # ...
-
- for cog, commands in itertools.groupby(entries, key=key):
- plausible = [cmd for cmd in commands if (await _can_run(cmd, ctx)) and not cmd.hidden]
- if len(plausible) == 0:
- continue
-
- description = ctx.bot.get_cog(cog)
- if description is None:
- description = discord.Embed.Empty
- else:
- description = inspect.getdoc(description) or discord.Embed.Empty
-
- nested_pages.extend((cog, description, plausible[i:i + per_page]) for i in range(0, len(plausible), per_page))
-
- self = cls(ctx, nested_pages, per_page=1) # this forces the pagination session
- self.prefix = cleanup_prefix(ctx.bot, ctx.prefix)
-
- # swap the get_page implementation with one that supports our style of pagination
- self.get_page = self.get_bot_page
- self._is_bot = True
-
- # replace the actual total
- self.total = sum(len(o) for _, _, o in nested_pages)
- return self
-
- def get_bot_page(self, page):
- cog, description, commands = self.entries[page - 1]
- self.title = f'{cog} Commands'
- self.description = description
- return commands
-
- async def show_page(self, page, *, first=False):
- self.current_page = page
- entries = self.get_page(page)
-
- self.embed.clear_fields()
- self.embed.description = self.description
- self.embed.title = self.title
-
- if hasattr(self, '_is_bot'):
- value ='Check the bot source: **[GitHub Link](https://github.com/F4stZ4p/DJ5n4k3/)**'
- self.embed.add_field(name='**GitHub**', value=value, inline=False)
-
- self.embed.set_footer(text=f'Use "{self.prefix}help command" for more info on a command.')
-
- signature = _command_signature
-
- for entry in entries:
- self.embed.add_field(name=signature(entry), value=entry.short_doc or "No help given", inline=False)
-
- if self.maximum_pages:
- self.embed.set_author(name=f'Page {page}/{self.maximum_pages} ({self.total} commands)')
-
- if not self.paginating:
- return await self.channel.send(embed=self.embed)
-
- if not first:
- await self.message.edit(embed=self.embed)
- return
-
- self.message = await self.channel.send(embed=self.embed)
- for (reaction, _) in self.reaction_emojis:
- if self.maximum_pages == 2 and reaction in ('\u23ed', '\u23ee'):
- # no |<< or >>| buttons if we only have two pages
- # we can't forbid it if someone ends up using it but remove
- # it from the default set
- continue
-
- await self.message.add_reaction(reaction)
-
- async def show_help(self):
- """shows this message"""
-
- self.embed.title = 'Paginator help'
- self.embed.description = 'Hello! Welcome to the help page.'
-
- messages = [f'{emoji} {func.__doc__}' for emoji, func in self.reaction_emojis]
- self.embed.clear_fields()
- self.embed.add_field(name='What are these reactions for?', value='\n'.join(messages), inline=False)
-
- self.embed.set_footer(text=f'We were on page {self.current_page} before this message.')
- await self.message.edit(embed=self.embed)
-
- async def go_back_to_current_page():
- await asyncio.sleep(30.0)
- await self.show_current_page()
-
- self.bot.loop.create_task(go_back_to_current_page())
-
- async def show_bot_help(self):
- """shows how to use the bot"""
-
- self.embed.title = 'Using the bot'
- self.embed.description = 'Hello! Welcome to the help page.'
- self.embed.clear_fields()
-
- entries = (
- ('<argument>', 'This means the argument is __**required**__.'),
- ('[argument]', 'This means the argument is __**optional**__.'),
- ('[A|B]', 'This means the it can be __**either A or B**__.'),
- ('[argument...]', 'This means you can have multiple arguments.\n' \
- 'Now that you know the basics, it should be noted that...\n' \
- '__**You do not type in the brackets!**__')
- )
-
- self.embed.add_field(name='How do I use this bot?', value='Reading the bot signature is pretty simple.')
-
- for name, value in entries:
- self.embed.add_field(name=name, value=value, inline=False)
-
- self.embed.set_footer(text=f'We were on page {self.current_page} before this message.')
- await self.message.edit(embed=self.embed)
-
- async def go_back_to_current_page():
- await asyncio.sleep(30.0)
- await self.show_current_page()
-
- self.bot.loop.create_task(go_back_to_current_page())
\ No newline at end of file
diff --git a/first_run/__init__.py b/first_run/__init__.py
index 669c547..e90bbc3 100644
--- a/first_run/__init__.py
+++ b/first_run/__init__.py
@@ -1,5 +1,10 @@
-from .config import Config
+from .initializer import Config
setup = Config()
+
+setup.install()
+
setup.ask()
setup.save()
+
+setup.clean()
\ No newline at end of file
diff --git a/first_run/config_generator.py b/first_run/initializer.py
similarity index 63%
rename from first_run/config_generator.py
rename to first_run/initializer.py
index 506c176..7ebd976 100644
--- a/first_run/config_generator.py
+++ b/first_run/initializer.py
@@ -1,3 +1,6 @@
+from pip._internal import main as pip
+import shutil
+
from .langs import locales, texts
@@ -10,6 +13,9 @@ class Config:
'unkickable_id': '[unkickable ids here (in int)]'
}
+ with open('requirements.txt', 'r') as f:
+ self.packages = f.read().split('\n')
+
def input(self, key, **kwargs):
lang = self.config.get('lang', 'multiple')
@@ -32,13 +38,30 @@ class Config:
self.config[key] = response
- def ask(self):
+ def install(self):
self.input('lang', valid=locales)
+ print('\n\n\033[4;36m'
+ + texts.get(self.config.get('lang')).get('install')
+ + '\033[0m\n')
+
+ for package in self.packages:
+ pip(['install', package])
+
+ def ask(self):
+ print('\n\n\033[4;36m' + texts.get(self.config.get('lang')).get('conf')
+ + '\033[0m\n')
+
self.input('token', empty=False)
self.input('postgresql_username', empty=False)
self.input('postgresql_password', empty=False)
self.input('postgresql_dbname', empty=False)
+ print('\n\n\033[4;36m' + texts.get(self.config.get('lang')).get('logs')
+ + '\033[0m\n')
+
+ self.input('wh_id', empty=True)
+ self.input('wh_token', empty=True)
+
print('\n\n\033[4;36m' + texts.get(self.config.get('lang')).get('misc')
+ '\033[0m\n')
@@ -49,13 +72,24 @@ class Config:
with open('config.py', 'w') as file:
postgresql = f"postgresql://" \
f"{self.config.get('postgresql_username')}:" \
- f"{self.config.get('postgresql_password')}@host/" \
- f"{self.config.get('postgresql_dbname')}"
+ f"{self.config.get('postgresql_password')}" \
+ f"@localhost/{self.config.get('postgresql_dbname')}"
file.write(f"postgresql = '{postgresql}'\n")
+ logs_webhook = dict(id=int(self.config.get('wh_id')),
+ token=self.config.get('wh_token'))
+ file.write(f"logs_webhook = '{logs_webhook}'\n")
+
for key, value in self.config.items():
- if not key.startswith('postgresql_'):
+ if not key.startswith('postgresql_') \
+ and not key.startswith('wh_'):
value = f"'{value}'" if type(value) is str else value
file.write(f"{key} = {value}\n")
print('\n\n\033[4;36m' + texts.get(self.config.get('lang')).get('end')
+ '\033[0m\n')
+
+ def clean(self):
+ print('\n\n\033[4;36m'
+ + texts.get(self.config.get('lang')).get('clean')
+ + '\033[0m\n')
+ shutil.rmtree('first_run')
diff --git a/first_run/langs.py b/first_run/langs.py
index 4d1ae7b..3048fcd 100644
--- a/first_run/langs.py
+++ b/first_run/langs.py
@@ -1,6 +1,10 @@
locales = ['fr', 'en']
texts = {
'fr': {
+ 'install': "Installation des modules...",
+
+ 'conf': "Configuration...",
+
'token': "Veuillez entrer le token",
'not_empty': "Cette valeur ne doit pas être vide",
@@ -8,15 +12,26 @@ texts = {
'postgresql_password': "Veuillez entrer le mot de passe de postgresql",
'postgresql_dbname': "Veuillez entrer le nom de la base de donnée",
+ 'logs': "Channel de logs (non obligatoire)",
+
+ 'wh_id': "L'id du webhook pour le channel de logs",
+ 'wh_token': "Le token du webhook pour le channel de logs",
+
'misc': 'Autre',
'activity': "Joue à ...",
'prefix': "Prefixe (par defaut : @tuxbot)",
- "end": "Configuration terminée, vous pouvez à tout moment la rectifier en modifiant le fichier config.py"
+ 'end': "Configuration terminée, vous pouvez à tout moment la rectifier en modifiant le fichier config.py",
+
+ 'clean': "Nettoyage..."
},
'en': {
+ 'install': "Installation of the modules....",
+
+ 'conf': "Configuration...",
+
'token': "Please enter the token",
'not_empty': "This value must not be empty",
@@ -24,12 +39,19 @@ texts = {
'postgresql_password': "Please enter the postgresql password",
'postgresql_dbname': "Please enter the database name",
+ 'logs': "Log channel (not required)",
+
+ 'wh_id': "Webhook id for log channel",
+ 'wh_token': "Webhook token for log channel",
+
'misc': 'Misc',
'activity': "Playing ...",
'prefix': "Prefix (default is @tuxbot)",
- "end": "Configuration completed, you can fix it at any time by modifying the config.py file"
+ 'end': "Configuration completed, you can fix it at any time by modifying the config.py file",
+
+ 'clean': "Cleaning..."
},
'multiple': {
diff --git a/launcher.py b/launcher.py
index 8cc5584..48f4542 100644
--- a/launcher.py
+++ b/launcher.py
@@ -1,9 +1,17 @@
-import logging
+import asyncio
import contextlib
+import logging
+import socket
+import sys
+import click
+
+from bot import TuxBot
+from cogs.utils.db import Table
try:
import config
+ from cogs.utils.lang import _
except ModuleNotFoundError:
import first_run
@@ -34,10 +42,34 @@ def setup_logging():
log.removeHandler(hdlr)
-def run_bot():
- pass # Todo: initialize bot, postgresql,...
+def run_bot(unload):
+ loop = asyncio.get_event_loop()
+ log = logging.getLogger()
+
+ try:
+ pool = loop.run_until_complete(
+ Table.create_pool(config.postgresql, command_timeout=60)
+ )
+ except socket.gaierror as e:
+ click.echo(_('Could not set up PostgreSQL...'), file=sys.stderr)
+ log.exception(_('Could not set up PostgreSQL...'))
+ return
+
+ bot = TuxBot(unload)
+ bot.pool = pool
+ bot.run()
+
+
+@click.group(invoke_without_command=True, options_metavar='[options]')
+@click.option('-u', '--unload',
+ multiple=True, type=str,
+ help=_('Launch without loading the <TEXT> module'))
+@click.pass_context
+def main(ctx, unload):
+ if ctx.invoked_subcommand is None:
+ with setup_logging():
+ run_bot(unload)
if __name__ == '__main__':
- with setup_logging():
- run_bot()
+ main()
diff --git a/locales/en/LC_MESSAGES/base.mo b/locales/en/LC_MESSAGES/base.mo
new file mode 100644
index 0000000..e56e9c9
Binary files /dev/null and b/locales/en/LC_MESSAGES/base.mo differ
diff --git a/locales/en/LC_MESSAGES/base.po b/locales/en/LC_MESSAGES/base.po
new file mode 100644
index 0000000..ca46dcc
--- /dev/null
+++ b/locales/en/LC_MESSAGES/base.po
@@ -0,0 +1,45 @@
+# SOME DESCRIPTIVE TITLE.
+# Copyright (C) YEAR ORGANIZATION
+# FIRST AUTHOR <EMAIL@ADDRESS>, YEAR.
+#
+msgid ""
+msgstr ""
+"Project-Id-Version: PACKAGE VERSION\n"
+"POT-Creation-Date: 2019-09-08 19:04+0200\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=UTF-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+"Generated-By: pygettext.py 1.5\n"
+
+
+#: launcher.py:56 launcher.py:57
+msgid "Could not set up PostgreSQL..."
+msgstr ""
+
+#: launcher.py:68
+msgid "Launch without loading the <TEXT> module"
+msgstr ""
+
+#: bot.py:52
+msgid "Failed to load extension : "
+msgstr ""
+
+#: bot.py:59
+msgid "This command cannot be used in private messages."
+msgstr ""
+
+#: bot.py:63
+msgid "Sorry. This command is disabled and cannot be used."
+msgstr ""
+
+#: bot.py:66
+msgid "In "
+msgstr ""
+
+#: bot.py:77
+msgid "Ready:"
+msgstr ""
+
diff --git a/locales/fr/LC_MESSAGES/base.mo b/locales/fr/LC_MESSAGES/base.mo
new file mode 100644
index 0000000..a24b97a
Binary files /dev/null and b/locales/fr/LC_MESSAGES/base.mo differ
diff --git a/locales/fr/LC_MESSAGES/base.po b/locales/fr/LC_MESSAGES/base.po
new file mode 100644
index 0000000..9bbd996
--- /dev/null
+++ b/locales/fr/LC_MESSAGES/base.po
@@ -0,0 +1,45 @@
+# SOME DESCRIPTIVE TITLE.
+# Copyright (C) YEAR ORGANIZATION
+# FIRST AUTHOR <EMAIL@ADDRESS>, YEAR.
+#
+msgid ""
+msgstr ""
+"Project-Id-Version: PACKAGE VERSION\n"
+"POT-Creation-Date: 2019-09-08 19:04+0200\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=UTF-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+"Generated-By: pygettext.py 1.5\n"
+
+
+#: launcher.py:56 launcher.py:57
+msgid "Could not set up PostgreSQL..."
+msgstr "Impossible de lancer PostgreSQL..."
+
+#: launcher.py:68
+msgid "Launch without loading the <TEXT> module"
+msgstr "Lancer sans charger le module <TEXT>"
+
+#: bot.py:52
+msgid "Failed to load extension : "
+msgstr "Impossible de charger l'extension : "
+
+#: bot.py:59
+msgid "This command cannot be used in private messages."
+msgstr "Cette commande ne peut pas être utilisée en message privé."
+
+#: bot.py:63
+msgid "Sorry. This command is disabled and cannot be used."
+msgstr "Désoler mais cette commande est désactivé."
+
+#: bot.py:66
+msgid "In "
+msgstr "Dans "
+
+#: bot.py:77
+msgid "Ready:"
+msgstr "Prêt:"
+
diff --git a/logs/tuxbot.log b/logs/tuxbot.log
index e69de29..72e6060 100644
--- a/logs/tuxbot.log
+++ b/logs/tuxbot.log
@@ -0,0 +1,12 @@
+[2019-09-08 22:58:46] [INFO ] discord.client: logging in using static token
+[2019-09-08 22:58:47] [INFO ] discord.gateway: Shard ID 0 has sent the IDENTIFY payload.
+[2019-09-08 22:58:47] [INFO ] discord.gateway: Shard ID 0 has connected to Gateway: ["gateway-prd-main-xfx5",{"micros":31802,"calls":["discord-sessions-prd-1-18",{"micros":26858,"calls":["start_session",{"micros":12548,"calls":["api-prd-main-6rfw",{"micros":8098,"calls":["get_user",{"micros":1942},"add_authorized_ip",{"micros":5},"get_guilds",{"micros":3036},"coros_wait",{"micros":3}]}]},"guilds_connect",{"micros":139,"calls":[]},"presence_connect",{"micros":1,"calls":[]}]}]}] (Session ID: 03fcb2e35ce477c42ae58e20259b5d68).
+[2019-09-08 22:58:53] [INFO ] discord.state: Processed a chunk for 463 members in guild ID 280805240977227776.
+[2019-09-08 22:58:54] [INFO ] discord.state: Processed a chunk for 807 members in guild ID 331981755177238530.
+[2019-09-08 22:58:55] [INFO ] discord.state: Processed a chunk for 1000 members in guild ID 296698073177128962.
+[2019-09-08 22:58:55] [INFO ] discord.state: Processed a chunk for 1000 members in guild ID 296698073177128962.
+[2019-09-08 22:58:55] [INFO ] discord.state: Processed a chunk for 662 members in guild ID 296698073177128962.
+[2019-09-08 23:03:12] [INFO ] discord.client: Cleaning up tasks.
+[2019-09-08 23:03:12] [INFO ] discord.client: Cleaning up after 5 tasks.
+[2019-09-08 23:03:12] [INFO ] discord.client: All tasks finished cancelling.
+[2019-09-08 23:03:12] [INFO ] discord.client: Closing the event loop.
diff --git a/requirements.txt b/requirements.txt
index 68f77bd..c94599b 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -1,4 +1,4 @@
-discord.py
+discord.py[voice]
lxml
click
asyncpg>=0.12.0
\ No newline at end of file