refactor(cogs): prepare the env for help command

This commit is contained in:
Romain J 2020-01-04 02:46:12 +01:00
commit fdf220cdfa
27 changed files with 452 additions and 261 deletions

9
utils/__init__.py Executable file
View file

@ -0,0 +1,9 @@
from .database import Database
from .models import *
from .config import *
from .lang import *
from .version import *
from .extra import *
from .paginator import *

36
utils/config.py Normal file
View file

@ -0,0 +1,36 @@
from typing import List, Union
import configparser
class Config(configparser.ConfigParser):
__slots__ = ('name', '_db')
def __init__(self, name):
super().__init__()
self._db = super()
self._db.read(name)
def find(self, value: str, **kwargs) \
-> Union[
List[configparser.SectionProxy], configparser.SectionProxy
]:
key = kwargs.get('key', None)
first = kwargs.get('first', False)
results = []
for name, section in self._db.items():
if key is None:
for k in section.keys():
if section.get(k) == value:
results.append(section)
if first and len(results) == 1:
return results[0]
else:
if section.get(key) == value:
results.append(section)
if first and len(results) == 1:
return results[0]
return results

17
utils/database.py Normal file
View file

@ -0,0 +1,17 @@
from .config import Config
from sqlalchemy import create_engine
from sqlalchemy.orm import sessionmaker, session
class Database:
def __init__(self, config: Config):
conf_postgresql = config["postgresql"]
postgresql = 'postgresql://{}:{}@{}/{}'.format(
conf_postgresql.get("Username"), conf_postgresql.get("Password"),
conf_postgresql.get("Host"), conf_postgresql.get("DBName"))
self.engine = create_engine(postgresql, echo=False)
Session = sessionmaker()
Session.configure(bind=self.engine)
self.session: session = Session()

10
utils/emotes.py Normal file
View file

@ -0,0 +1,10 @@
emotes = ['1⃣', '2⃣', '3⃣', '4⃣', '5⃣', '6⃣', '7⃣', '8⃣', '9⃣', '🔟', '0⃣',
'🇦', '🇧', '🇨', '🇩', '🇪', '🇫', '🇬', '🇭', '🇮']
def get(count):
return emotes[:count]
def get_index(emote):
return emotes.index(emote)

21
utils/extra.py Normal file
View file

@ -0,0 +1,21 @@
from discord.ext import commands
class commandsPlus(commands.Command):
def __init__(self, func, **kwargs):
super().__init__(func, **kwargs)
self.category = kwargs.get("category", 'other')
def commandExtra(*args, **kwargs):
return commands.command(*args, **kwargs, cls=commandsPlus)
class GroupPlus(commands.Group):
def __init__(self, func, **kwargs):
super().__init__(func, **kwargs)
self.category = kwargs.get("category", 'other')
def groupExtra(*args, **kwargs):
return commands.group(*args, **kwargs, cls=GroupPlus)

38
utils/lang.py Normal file
View file

@ -0,0 +1,38 @@
import gettext
from .config import Config
from utils import Database
from .models.lang import LangModel
from discord.ext import commands
class Texts:
def __init__(self, base: str = 'base', ctx: commands.Context = None):
self.locale = self.get_locale(ctx)
self.base = base
def get(self, text: str) -> str:
texts = gettext.translation(self.base, localedir='extras/locales',
languages=[self.locale])
texts.install()
return texts.gettext(text)
def set(self, lang: str):
self.locale = lang
@staticmethod
def get_locale(ctx):
database = Database(Config("./configs/config.cfg"))
if ctx is not None:
current = database.session\
.query(LangModel.value)\
.filter(LangModel.key == str(ctx.guild.id))
if current.count() > 0:
return current.one()[0]
default = database.session\
.query(LangModel.value)\
.filter(LangModel.key == 'default')\
.one()[0]
return default

7
utils/models/__init__.py Normal file
View file

@ -0,0 +1,7 @@
from sqlalchemy.ext.declarative import declarative_base
Base = declarative_base()
from .lang import LangModel
from .warn import WarnModel
from .poll import PollModel, ResponsesModel
from .alias import AliasesModel

28
utils/models/alias.py Normal file
View file

@ -0,0 +1,28 @@
from sqlalchemy import Column, String, BigInteger, Integer
from . import Base
class AliasesModel(Base):
__tablename__ = 'aliases'
id = Column(Integer, primary_key=True)
user_id = Column(BigInteger)
alias = Column(String)
command = Column(String)
guild = Column(String)
def __repr__(self):
return "<AliasesModel(" \
"id='%s', " \
"user_id='%s', " \
"alias='%s', " \
"command='%s', " \
"guild='%s', " \
")>" % (
self.id,
self.user_id,
self.alias,
self.command,
self.guild
)

12
utils/models/lang.py Normal file
View file

@ -0,0 +1,12 @@
from . import Base
from sqlalchemy import Column, String
class LangModel(Base):
__tablename__ = 'langs'
key = Column(String, primary_key=True)
value = Column(String)
def __repr__(self):
return "<LangModel(key='%s', locale='%s')>" % (self.key, self.value)

27
utils/models/poll.py Normal file
View file

@ -0,0 +1,27 @@
from . import Base
from sqlalchemy import Column, Integer, BigInteger, JSON, ForeignKey, Boolean
from sqlalchemy.orm import relationship
class PollModel(Base):
__tablename__ = 'polls'
id = Column(Integer, primary_key=True, autoincrement=True)
channel_id = Column(BigInteger)
message_id = Column(BigInteger)
content = Column(JSON)
is_anonymous = Column(Boolean)
available_choices = Column(Integer)
choice = relationship("ResponsesModel")
class ResponsesModel(Base):
__tablename__ = 'responses'
id = Column(Integer, primary_key=True, autoincrement=True)
user = Column(BigInteger)
poll_id = Column(Integer, ForeignKey('polls.id'))
choice = Column(Integer)

19
utils/models/warn.py Normal file
View file

@ -0,0 +1,19 @@
import datetime
from . import Base
from sqlalchemy import Column, Integer, String, BIGINT, TIMESTAMP
class WarnModel(Base):
__tablename__ = 'warns'
id = Column(Integer, primary_key=True)
server_id = Column(BIGINT)
user_id = Column(BIGINT)
reason = Column(String)
created_at = Column(TIMESTAMP, default=datetime.datetime.now())
def __repr__(self):
return "<WarnModel(server_id='%s', user_id='%s', reason='%s', " \
"created_at='%s')>" \
% (self.server_id, self.user_id, self.reason, self.created_at)

289
utils/paginator.py Normal file
View file

@ -0,0 +1,289 @@
import asyncio
import discord
from discord.ext.commands import Paginator as CommandPaginator
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.Colour.blurple())
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]
def get_content(self, entries, page, *, first=False):
return None
def get_embed(self, entries, page, *, first=False):
self.prepare_embed(entries, page, first=first)
return self.embed
def prepare_embed(self, entries, page, *, first=False):
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 self.paginating and first:
p.append('')
p.append('Confused? React with \N{INFORMATION SOURCE} for more info.')
self.embed.description = '\n'.join(p)
async def show_page(self, page, *, first=False):
self.current_page = page
entries = self.get_page(page)
content = self.get_content(entries, page, first=first)
embed = self.get_embed(entries, page, first=first)
if not self.paginating:
return await self.channel.send(content=content, embed=embed)
if not first:
await self.message.edit(content=content, embed=embed)
return
self.message = await self.channel.send(content=content, embed=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__}')
embed = self.embed.copy()
embed.clear_fields()
embed.description = '\n'.join(messages)
embed.set_footer(text=f'We were on page {self.current_page} before this message.')
await self.message.edit(content=None, embed=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, payload):
if payload.user_id != self.author.id:
return False
if payload.message_id != self.message.id:
return False
to_check = str(payload.emoji)
for (emoji, func) in self.reaction_emojis:
if to_check == 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:
payload = await self.bot.wait_for('raw_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(payload.emoji, discord.Object(id=payload.user_id))
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.
"""
def prepare_embed(self, entries, page, *, first=False):
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)
class TextPages(Pages):
"""Uses a commands.Paginator internally to paginate some text."""
def __init__(self, ctx, text, *, prefix='```', suffix='```', max_size=2000):
paginator = CommandPaginator(prefix=prefix, suffix=suffix, max_size=max_size - 200)
for line in text.split('\n'):
paginator.add_line(line)
super().__init__(ctx, entries=paginator.pages, per_page=1, show_entry_count=False)
def get_page(self, page):
return self.entries[page - 1]
def get_embed(self, entries, page, *, first=False):
return None
def get_content(self, entry, page, *, first=False):
if self.maximum_pages > 1:
return f'{entry}\nPage {page}/{self.maximum_pages}'
return entry

12
utils/version.py Normal file
View file

@ -0,0 +1,12 @@
class Version:
def __init__(self, major: int, minor: int, patch: int, **kwargs):
self.major: int = major
self.minor: int = minor
self.patch: int = patch
self.pre_release = kwargs.get('pre_release', '')
self.build = kwargs.get('build', '')
def __str__(self) -> str:
build = self.build[:10]
return f'v{self.major}.{self.minor}.{self.patch}{self.pre_release}+{build}'