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