From 9274895226367c1695111b566f9777309700498e Mon Sep 17 00:00:00 2001
From: Romain J <romain.ordi@gmail.com>
Date: Thu, 19 Sep 2019 01:48:52 +0200
Subject: [PATCH] add(command|warn): start dev of command warn

---
 README.md                              |   15 +-
 bot.py                                 |    6 +-
 cogs/admin.py                          |   47 +-
 cogs/utils/db.py                       | 1081 +-----------------------
 extras/locales/en/LC_MESSAGES/admin.po |    3 +
 extras/locales/fr/LC_MESSAGES/admin.mo |  Bin 583 -> 634 bytes
 extras/locales/fr/LC_MESSAGES/admin.po |    5 +-
 launcher.py                            |    9 +-
 8 files changed, 80 insertions(+), 1086 deletions(-)

diff --git a/README.md b/README.md
index 17f6de3..db3d1bd 100644
--- a/README.md
+++ b/README.md
@@ -54,4 +54,17 @@
  - [x] info
  - [ ] help
  - [x] credits `new command`
- 
\ No newline at end of file
+ 
+ ---
+ 
+ # Cogs.ci commands
+ - [ ] ci (help?)
+ - [ ] ci show
+ - [ ] ci register
+ - [ ] ci delete
+ - [ ] ci update
+ - [ ] ci setconfig
+ - [ ] ci setos
+ - [ ] ci setcountry
+ - [ ] ci online_edit `renamed`, cause : `website down`
+ - [ ] ci list
\ No newline at end of file
diff --git a/bot.py b/bot.py
index f2cfb2c..3ea4eab 100755
--- a/bot.py
+++ b/bot.py
@@ -5,6 +5,7 @@ import traceback
 from collections import deque
 
 import aiohttp
+import asyncpg
 import discord
 import git
 from discord.ext import commands
@@ -37,9 +38,9 @@ async def _prefix_callable(bot, message: discord.message) -> list:
 
 
 class TuxBot(commands.AutoShardedBot):
-    __slots__ = ('uptime', 'config', 'session')
+    __slots__ = ('uptime', 'config', 'db', 'session')
 
-    def __init__(self, unload: list):
+    def __init__(self, unload: list, db: asyncpg.pool.Pool):
         super().__init__(command_prefix=_prefix_callable, pm_help=None,
                          help_command=None, description=description,
                          help_attrs=dict(hidden=True),
@@ -47,6 +48,7 @@ class TuxBot(commands.AutoShardedBot):
 
         self.uptime: datetime = datetime.datetime.utcnow()
         self.config = config
+        self.db = db
         self._prev_events = deque(maxlen=10)
         self.session = aiohttp.ClientSession(loop=self.loop)
 
diff --git a/cogs/admin.py b/cogs/admin.py
index 350d9b4..9e8a0f6 100644
--- a/cogs/admin.py
+++ b/cogs/admin.py
@@ -3,6 +3,7 @@ from typing import Union
 
 import discord
 from discord.ext import commands
+import humanize
 
 from bot import TuxBot
 from .utils.lang import Texts
@@ -12,8 +13,9 @@ class Admin(commands.Cog):
 
     def __init__(self, bot: TuxBot):
         self.bot = bot
+        self.db = bot.db
 
-    async def cog_check(self, ctx: commands.Context):
+    async def cog_check(self, ctx: commands.Context) -> bool:
         permissions: discord.Permissions = ctx.channel.permissions_for(
             ctx.author)
 
@@ -217,6 +219,49 @@ class Admin(commands.Cog):
             await ctx.send(Texts('utils').get("Unable to find the message"),
                            delete_after=5)
 
+    """---------------------------------------------------------------------"""
+
+    @commands.group(name='warn', aliases=['warns'])
+    async def _warn(self, ctx: commands.Context):
+        if ctx.invoked_subcommand is None:
+            query = """
+            SELECT user_id, reason, created_at FROM warns 
+            WHERE created_at >= $1 AND server_id = $2
+            ORDER BY created_at 
+            DESC LIMIT 10
+            """
+            week_ago = datetime.datetime.now() - datetime.timedelta(weeks=6)
+
+            async with self.bot.db.acquire() as con:
+                warns = await con.fetch(query, week_ago, ctx.guild.id)
+                warns_list = ''
+
+                for warn in warns:
+                    user_id = warn.get('user_id')
+                    user = await self.bot.fetch_user(user_id)
+                    reason = warn.get('reason')
+                    ago = humanize.naturaldelta(
+                        datetime.datetime.now() - warn.get('created_at')
+                    )
+
+                    warns_list += f"**{user}**: `{reason}` *({ago} ago)*\n"
+
+                e = discord.Embed(
+                    title=f"{len(warns)} {Texts('admin').get('last warns')}: ",
+                    description=warns_list
+                )
+
+                await ctx.send(embed=e)
+
+    @_warn.command(name='add', aliases=['new'])
+    async def _warn_new(self, ctx: commands.Context, member: discord.Member,
+                        *, reason):
+        """
+        todo: push in database
+        if warn > 2 for member:
+            todo: ask for confirmation to kick or ban
+        """
+
 
 def setup(bot: TuxBot):
     bot.add_cog(Admin(bot))
diff --git a/cogs/utils/db.py b/cogs/utils/db.py
index 90c7604..554a7e6 100755
--- a/cogs/utils/db.py
+++ b/cogs/utils/db.py
@@ -1,1085 +1,12 @@
-# -*- 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__)
 
 
-class SchemaError(Exception):
-    pass
-
-
-class SQLType:
-    python = None
-
-    def to_dict(self):
-        o = self.__dict__.copy()
-        cls = self.__class__
-        o['__meta__'] = cls.__module__ + '.' + cls.__qualname__
-        return o
-
+class Table:
     @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))
+    async def create_pool(cls, uri, **kwargs) -> asyncpg.pool.Pool:
+        cls._pool = db = await asyncpg.create_pool(uri, **kwargs)
+        return db
diff --git a/extras/locales/en/LC_MESSAGES/admin.po b/extras/locales/en/LC_MESSAGES/admin.po
index 12b07d3..46df0e2 100644
--- a/extras/locales/en/LC_MESSAGES/admin.po
+++ b/extras/locales/en/LC_MESSAGES/admin.po
@@ -22,4 +22,7 @@ msgid "Unable to ban this user"
 msgstr ""
 
 msgid "Unable to kick this user"
+msgstr ""
+
+msgid "last warns"
 msgstr ""
\ No newline at end of file
diff --git a/extras/locales/fr/LC_MESSAGES/admin.mo b/extras/locales/fr/LC_MESSAGES/admin.mo
index 139da3d412f105c10f9a7065cadff84a814a6a36..70fc67382a772c1d6f5286720541aec84e3ada85 100644
GIT binary patch
delta 164
zcmX@k@{6VZo)F7a1|VPsVi_QI0dbH(4v@_S#JNB$3d9vaEDpqdKr8{o%YfLHk%3`9
zkOqnW1+pc9v?vn;g9?z=2hyrQIs`~F_#_sWD3m7_<rOmo<fJARrz&hrsbds(NiE9D
bOf4!_NGwY&D#<J^PR&itD=D7*kTDGak)j<`

delta 105
zcmeyxa-1dRo)F7a1|VPoVi_Q|0dbH(43I4f#IZmu4#dSkECIybKy1s%z_0{JO9JsD
aAX^1U{{hmfKw6QBfnj5L8{=eArc?lE0t^cP

diff --git a/extras/locales/fr/LC_MESSAGES/admin.po b/extras/locales/fr/LC_MESSAGES/admin.po
index 338992d..e3601b4 100644
--- a/extras/locales/fr/LC_MESSAGES/admin.po
+++ b/extras/locales/fr/LC_MESSAGES/admin.po
@@ -22,4 +22,7 @@ msgid "Unable to ban this user"
 msgstr "Impossible de bannir cet utilisateur"
 
 msgid "Unable to kick this user"
-msgstr "Impossible d'expulser cet utilisateur"
\ No newline at end of file
+msgstr "Impossible d'expulser cet utilisateur"
+
+msgid "last warns"
+msgstr "derniers avertissements"
\ No newline at end of file
diff --git a/launcher.py b/launcher.py
index f13a703..e00fe11 100644
--- a/launcher.py
+++ b/launcher.py
@@ -4,7 +4,9 @@ import logging
 import socket
 import sys
 
+import asyncpg
 import click
+import git
 import requests
 
 from bot import TuxBot
@@ -50,17 +52,16 @@ def run_bot(unload: list = []):
     print(Texts().get('Starting...'))
 
     try:
-        pool = loop.run_until_complete(
+        db = loop.run_until_complete(
             Table.create_pool(config.postgresql, command_timeout=60)
         )
-    except socket.gaierror as e:
+    except socket.gaierror:
         click.echo(Texts().get("Could not set up PostgreSQL..."),
                    file=sys.stderr)
         log.exception(Texts().get("Could not set up PostgreSQL..."))
         return
 
-    bot = TuxBot(unload)
-    bot.pool = pool
+    bot = TuxBot(unload, db)
     bot.run()