add(i18n): start i18n development
refactor(application|bot): ...
This commit is contained in:
parent
b03dc30c6c
commit
efc05f816e
18 changed files with 1354 additions and 995 deletions
143
bot.py
143
bot.py
|
@ -1,13 +1,5 @@
|
||||||
#!/usr/bin/env python
|
|
||||||
# -*- coding: utf-8 -*-
|
|
||||||
|
|
||||||
__author__ = "Maël / Outout | Romain"
|
|
||||||
__licence__ = "WTFPL Licence 2.0"
|
|
||||||
|
|
||||||
|
|
||||||
import copy
|
|
||||||
import datetime
|
import datetime
|
||||||
import os
|
import logging
|
||||||
import sys
|
import sys
|
||||||
import traceback
|
import traceback
|
||||||
|
|
||||||
|
@ -15,122 +7,95 @@ import aiohttp
|
||||||
import discord
|
import discord
|
||||||
from discord.ext import commands
|
from discord.ext import commands
|
||||||
|
|
||||||
import cogs.utils.cli_colors as colors
|
|
||||||
import config
|
import config
|
||||||
from cogs.utils import checks
|
from cogs.utils.lang import _
|
||||||
|
|
||||||
if sys.version_info[1] < 7 or sys.version_info[0] < 3:
|
description = """
|
||||||
print(f"{colors.text_colors.RED}[ERROR] Python 3.7 or + is required.{colors.ENDC}")
|
Je suis TuxBot, le bot qui vit de l'OpenSource ! ;)
|
||||||
exit()
|
"""
|
||||||
|
|
||||||
|
log = logging.getLogger(__name__)
|
||||||
|
|
||||||
l_extensions = (
|
l_extensions = (
|
||||||
'cogs.admin',
|
'cogs.admin',
|
||||||
'cogs.afk',
|
'cogs.basaics',
|
||||||
'cogs.atc',
|
|
||||||
'cogs.basics',
|
|
||||||
'cogs.ci',
|
|
||||||
'cogs.filter_messages',
|
|
||||||
'cogs.funs',
|
|
||||||
'cogs.role',
|
|
||||||
'cogs.search',
|
|
||||||
'cogs.send_logs',
|
|
||||||
'cogs.sondage',
|
|
||||||
'cogs.utility',
|
|
||||||
'cogs.vocal',
|
|
||||||
'cogs.private',
|
|
||||||
)
|
)
|
||||||
|
|
||||||
help_attrs = dict(hidden=True, in_help=True, name="DONOTUSE")
|
|
||||||
|
async def _prefix_callable(bot, message):
|
||||||
|
base = [] if config.prefix is None else config.prefix
|
||||||
|
|
||||||
|
# if message.guild is not None:
|
||||||
|
# base.extend(bot.prefixes.get(message.guild.id))
|
||||||
|
return commands.when_mentioned_or(base)
|
||||||
|
|
||||||
|
|
||||||
class TuxBot(commands.Bot):
|
class TuxBot(commands.AutoShardedBot):
|
||||||
def __init__(self):
|
__slots__ = ('uptime', 'config', 'session')
|
||||||
|
|
||||||
|
def __init__(self, unload):
|
||||||
|
super().__init__(command_prefix=_prefix_callable,
|
||||||
|
description=description, pm_help=None,
|
||||||
|
help_command=None, help_attrs=dict(hidden=True))
|
||||||
|
|
||||||
self.uptime = datetime.datetime.utcnow()
|
self.uptime = datetime.datetime.utcnow()
|
||||||
self.config = config
|
self.config = config
|
||||||
super().__init__(command_prefix=self.config.prefix[0],
|
self.prefixes = {}
|
||||||
description=self.config.description,
|
|
||||||
pm_help=None,
|
|
||||||
help_command=None)
|
|
||||||
|
|
||||||
self.client_id = self.config.client_id
|
|
||||||
self.session = aiohttp.ClientSession(loop=self.loop)
|
self.session = aiohttp.ClientSession(loop=self.loop)
|
||||||
self._events = []
|
|
||||||
|
|
||||||
self.add_command(self.do)
|
|
||||||
|
|
||||||
for extension in l_extensions:
|
for extension in l_extensions:
|
||||||
|
if extension not in unload:
|
||||||
try:
|
try:
|
||||||
self.load_extension(extension)
|
self.load_extension(extension)
|
||||||
print(f"{colors.text_colors.GREEN}\"{extension}\""
|
|
||||||
f" chargé !{colors.ENDC}")
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
print(f"{colors.text_colors.RED}"
|
print(_("Failed to load extension : ") + extension,
|
||||||
f"Impossible de charger l'extension {extension}\n"
|
file=sys.stderr)
|
||||||
f"{type(e).__name__}: {e}{colors.ENDC}", file=sys.stderr)
|
traceback.print_exc()
|
||||||
|
|
||||||
async def on_command_error(self, ctx, error):
|
async def on_command_error(self, ctx, error):
|
||||||
if isinstance(error, commands.NoPrivateMessage):
|
if isinstance(error, commands.NoPrivateMessage):
|
||||||
await ctx.author.send('Cette commande ne peut pas être utilisee '
|
await ctx.author.send(
|
||||||
'en message privee.')
|
_('This command cannot be used in private messages.')
|
||||||
|
)
|
||||||
elif isinstance(error, commands.DisabledCommand):
|
elif isinstance(error, commands.DisabledCommand):
|
||||||
await ctx.author.send('Desoler mais cette commande est desactive, '
|
await ctx.author.send(
|
||||||
'elle ne peut donc pas être utilisée.')
|
_('Sorry. This command is disabled and cannot be used.')
|
||||||
|
)
|
||||||
elif isinstance(error, commands.CommandInvokeError):
|
elif isinstance(error, commands.CommandInvokeError):
|
||||||
print(f'In {ctx.command.qualified_name}:', file=sys.stderr)
|
print(_('In ') + f'{ctx.command.qualified_name}:', file=sys.stderr)
|
||||||
traceback.print_tb(error.original.__traceback__)
|
traceback.print_tb(error.original.__traceback__)
|
||||||
print(f'{error.original.__class__.__name__}: {error.original}',
|
print(f'{error.original.__class__.__name__}: {error.original}',
|
||||||
file=sys.stderr)
|
file=sys.stderr)
|
||||||
|
elif isinstance(error, commands.ArgumentParsingError):
|
||||||
|
await ctx.send(error)
|
||||||
|
|
||||||
async def on_ready(self):
|
async def on_ready(self):
|
||||||
log_channel_id = await self.fetch_channel(self.config.log_channel_id)
|
if not hasattr(self, 'uptime'):
|
||||||
|
self.uptime = datetime.datetime.utcnow()
|
||||||
|
|
||||||
print('\n\n---------------------')
|
print(_('Ready:') + f' {self.user} (ID: {self.user.id})')
|
||||||
print('CONNECTÉ :')
|
|
||||||
print(f'Nom d\'utilisateur: {self.user} {colors.text_style.DIM}'
|
|
||||||
f'(ID: {self.user.id}){colors.ENDC}')
|
|
||||||
print(f'Channel de log: {log_channel_id} {colors.text_style.DIM}'
|
|
||||||
f'(ID: {log_channel_id.id}){colors.ENDC}')
|
|
||||||
print(f'Prefix: {self.config.prefix[0]}')
|
|
||||||
print('Merci d\'utiliser TuxBot')
|
|
||||||
print('---------------------\n\n')
|
|
||||||
|
|
||||||
await self.change_presence(status=discord.Status.dnd,
|
await self.change_presence(status=discord.Status.dnd,
|
||||||
activity=discord.Game(
|
activity=discord.Game(
|
||||||
name=self.config.game)
|
name=self.config.activity
|
||||||
)
|
))
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
async def on_resumed():
|
async def on_resumed():
|
||||||
print('resumed...')
|
print('resumed...')
|
||||||
|
|
||||||
async def on_message(self, message):
|
@property
|
||||||
if message.author.bot:
|
def logs_webhook(self):
|
||||||
return
|
logs_webhook = self.config.logs_webhook
|
||||||
|
webhook = discord.Webhook.partial(id=logs_webhook.get('id'),
|
||||||
|
token=logs_webhook.get('token'),
|
||||||
|
adapter=discord.AsyncWebhookAdapter(
|
||||||
|
self.session))
|
||||||
|
return webhook
|
||||||
|
|
||||||
try:
|
async def close(self):
|
||||||
await self.process_commands(message)
|
await super().close()
|
||||||
except Exception as e:
|
await self.session.close()
|
||||||
print(f'{colors.text_colors.RED}Erreur rencontré : \n'
|
|
||||||
f' {type(e).__name__}: {e}{colors.ENDC} \n \n')
|
|
||||||
|
|
||||||
def run(self):
|
def run(self):
|
||||||
super().run(self.config.token, reconnect=True)
|
super().run(config.token, reconnect=True)
|
||||||
|
|
||||||
@checks.has_permissions(administrator=True)
|
|
||||||
@commands.command(pass_context=True, hidden=True)
|
|
||||||
async def do(self, ctx, times: int, *, command):
|
|
||||||
"""Repeats a command a specified number of times."""
|
|
||||||
msg = copy.copy(ctx.message)
|
|
||||||
msg.content = command
|
|
||||||
for i in range(times):
|
|
||||||
await self.process_commands(msg)
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == '__main__':
|
|
||||||
if os.path.exists('config.py') is not True:
|
|
||||||
print(f"{colors.text_colors.RED}"
|
|
||||||
f"Veuillez créer le fichier config.py{colors.ENDC}")
|
|
||||||
exit()
|
|
||||||
|
|
||||||
tuxbot = TuxBot()
|
|
||||||
tuxbot.run()
|
|
||||||
|
|
0
cogs/utils/checks.py
Executable file → Normal file
0
cogs/utils/checks.py
Executable file → Normal file
1096
cogs/utils/db.py
1096
cogs/utils/db.py
File diff suppressed because it is too large
Load diff
|
@ -1,75 +0,0 @@
|
||||||
async def entry_to_code(bot, entries):
|
|
||||||
width = max(map(lambda t: len(t[0]), entries))
|
|
||||||
output = ['```']
|
|
||||||
fmt = '{0:<{width}}: {1}'
|
|
||||||
for name, entry in entries:
|
|
||||||
output.append(fmt.format(name, entry, width=width))
|
|
||||||
output.append('```')
|
|
||||||
await bot.say('\n'.join(output))
|
|
||||||
|
|
||||||
import datetime
|
|
||||||
|
|
||||||
async def indented_entry_to_code(bot, entries):
|
|
||||||
width = max(map(lambda t: len(t[0]), entries))
|
|
||||||
output = ['```']
|
|
||||||
fmt = '\u200b{0:>{width}}: {1}'
|
|
||||||
for name, entry in entries:
|
|
||||||
output.append(fmt.format(name, entry, width=width))
|
|
||||||
output.append('```')
|
|
||||||
await bot.say('\n'.join(output))
|
|
||||||
|
|
||||||
async def too_many_matches(bot, msg, matches, entry):
|
|
||||||
check = lambda m: m.content.isdigit()
|
|
||||||
await bot.say('There are too many matches... Which one did you mean? **Only say the number**.')
|
|
||||||
await bot.say('\n'.join(map(entry, enumerate(matches, 1))))
|
|
||||||
|
|
||||||
# only give them 3 tries.
|
|
||||||
for i in range(3):
|
|
||||||
message = await bot.wait_for_message(author=msg.author, channel=msg.channel, check=check)
|
|
||||||
index = int(message.content)
|
|
||||||
try:
|
|
||||||
return matches[index - 1]
|
|
||||||
except:
|
|
||||||
await bot.say('Please give me a valid number. {} tries remaining...'.format(2 - i))
|
|
||||||
|
|
||||||
raise ValueError('Too many tries. Goodbye.')
|
|
||||||
|
|
||||||
class Plural:
|
|
||||||
def __init__(self, **attr):
|
|
||||||
iterator = attr.items()
|
|
||||||
self.name, self.value = next(iter(iterator))
|
|
||||||
|
|
||||||
def __str__(self):
|
|
||||||
v = self.value
|
|
||||||
if v > 1:
|
|
||||||
return '%s %ss' % (v, self.name)
|
|
||||||
return '%s %s' % (v, self.name)
|
|
||||||
|
|
||||||
def human_timedelta(dt):
|
|
||||||
now = datetime.datetime.utcnow()
|
|
||||||
delta = now - dt
|
|
||||||
hours, remainder = divmod(int(delta.total_seconds()), 3600)
|
|
||||||
minutes, seconds = divmod(remainder, 60)
|
|
||||||
days, hours = divmod(hours, 24)
|
|
||||||
years, days = divmod(days, 365)
|
|
||||||
|
|
||||||
if years:
|
|
||||||
if days:
|
|
||||||
return '%s and %s ago' % (Plural(year=years), Plural(day=days))
|
|
||||||
return '%s ago' % Plural(year=years)
|
|
||||||
|
|
||||||
if days:
|
|
||||||
if hours:
|
|
||||||
return '%s and %s ago' % (Plural(day=days), Plural(hour=hours))
|
|
||||||
return '%s ago' % Plural(day=days)
|
|
||||||
|
|
||||||
if hours:
|
|
||||||
if minutes:
|
|
||||||
return '%s and %s ago' % (Plural(hour=hours), Plural(minute=minutes))
|
|
||||||
return '%s ago' % Plural(hour=hours)
|
|
||||||
|
|
||||||
if minutes:
|
|
||||||
if seconds:
|
|
||||||
return '%s and %s ago' % (Plural(minute=minutes), Plural(second=seconds))
|
|
||||||
return '%s ago' % Plural(minute=minutes)
|
|
||||||
return '%s ago' % Plural(second=seconds)
|
|
8
cogs/utils/lang.py
Normal file
8
cogs/utils/lang.py
Normal file
|
@ -0,0 +1,8 @@
|
||||||
|
import gettext
|
||||||
|
import config
|
||||||
|
|
||||||
|
lang = gettext.translation('base', localedir='locales',
|
||||||
|
languages=[config.lang])
|
||||||
|
lang.install()
|
||||||
|
|
||||||
|
_ = lang.gettext
|
|
@ -1,147 +0,0 @@
|
||||||
#!/bin/env python
|
|
||||||
|
|
||||||
# With credit to DanielKO
|
|
||||||
|
|
||||||
from lxml import etree
|
|
||||||
import datetime, re
|
|
||||||
import asyncio, aiohttp
|
|
||||||
|
|
||||||
NINTENDO_LOGIN_PAGE = "https://id.nintendo.net/oauth/authorize"
|
|
||||||
SPLATNET_CALLBACK_URL = "https://splatoon.nintendo.net/users/auth/nintendo/callback"
|
|
||||||
SPLATNET_CLIENT_ID = "12af3d0a3a1f441eb900411bb50a835a"
|
|
||||||
SPLATNET_SCHEDULE_URL = "https://splatoon.nintendo.net/schedule"
|
|
||||||
|
|
||||||
class Rotation(object):
|
|
||||||
def __init__(self):
|
|
||||||
self.start = None
|
|
||||||
self.end = None
|
|
||||||
self.turf_maps = []
|
|
||||||
self.ranked_mode = None
|
|
||||||
self.ranked_maps = []
|
|
||||||
|
|
||||||
|
|
||||||
@property
|
|
||||||
def is_over(self):
|
|
||||||
return self.end < datetime.datetime.utcnow()
|
|
||||||
|
|
||||||
def __str__(self):
|
|
||||||
now = datetime.datetime.utcnow()
|
|
||||||
prefix = ''
|
|
||||||
if self.start > now:
|
|
||||||
minutes_delta = int((self.start - now) / datetime.timedelta(minutes=1))
|
|
||||||
hours = int(minutes_delta / 60)
|
|
||||||
minutes = minutes_delta % 60
|
|
||||||
prefix = '**In {0} hours and {1} minutes**:\n'.format(hours, minutes)
|
|
||||||
else:
|
|
||||||
prefix = '**Current Rotation**:\n'
|
|
||||||
|
|
||||||
fmt = 'Turf War is {0[0]} and {0[1]}\n{1} is {2[0]} and {2[1]}'
|
|
||||||
return prefix + fmt.format(self.turf_maps, self.ranked_mode, self.ranked_maps)
|
|
||||||
|
|
||||||
# based on https://github.com/Wiwiweb/SakuraiBot/blob/master/src/sakuraibot.py
|
|
||||||
async def get_new_splatnet_cookie(username, password):
|
|
||||||
parameters = {'client_id': SPLATNET_CLIENT_ID,
|
|
||||||
'response_type': 'code',
|
|
||||||
'redirect_uri': SPLATNET_CALLBACK_URL,
|
|
||||||
'username': username,
|
|
||||||
'password': password}
|
|
||||||
|
|
||||||
async with aiohttp.post(NINTENDO_LOGIN_PAGE, data=parameters) as response:
|
|
||||||
cookie = response.history[-1].cookies.get('_wag_session')
|
|
||||||
if cookie is None:
|
|
||||||
print(req)
|
|
||||||
raise Exception("Couldn't retrieve cookie")
|
|
||||||
return cookie
|
|
||||||
|
|
||||||
def parse_splatnet_time(timestr):
|
|
||||||
# time is given as "MM/DD at H:MM [p|a].m. (PDT|PST)"
|
|
||||||
# there is a case where it goes over the year, e.g. 12/31 at ... and then 1/1 at ...
|
|
||||||
# this case is kind of weird though and is currently unexpected
|
|
||||||
# it could even end up being e.g. 12/31/2015 ... and then 1/1/2016 ...
|
|
||||||
# we'll never know
|
|
||||||
|
|
||||||
regex = r'(?P<month>\d+)\/(?P<day>\d+)\s*at\s*(?P<hour>\d+)\:(?P<minutes>\d+)\s*(?P<p>a\.m\.|p\.m\.)\s*\((?P<tz>.+)\)'
|
|
||||||
m = re.match(regex, timestr.strip())
|
|
||||||
|
|
||||||
if m is None:
|
|
||||||
raise RuntimeError('Apparently the timestamp "{}" does not match the regex.'.format(timestr))
|
|
||||||
|
|
||||||
matches = m.groupdict()
|
|
||||||
tz = matches['tz'].strip().upper()
|
|
||||||
offset = None
|
|
||||||
if tz == 'PDT':
|
|
||||||
# EDT is UTC - 4, PDT is UTC - 7, so you need +7 to make it UTC
|
|
||||||
offset = +7
|
|
||||||
elif tz == 'PST':
|
|
||||||
# EST is UTC - 5, PST is UTC - 8, so you need +8 to make it UTC
|
|
||||||
offset = +8
|
|
||||||
else:
|
|
||||||
raise RuntimeError('Unknown timezone found: {}'.format(tz))
|
|
||||||
|
|
||||||
pm = matches['p'].replace('.', '') # a.m. -> am
|
|
||||||
|
|
||||||
current_time = datetime.datetime.utcnow()
|
|
||||||
|
|
||||||
# Kind of hacky.
|
|
||||||
fmt = "{2}/{0[month]}/{0[day]} {0[hour]}:{0[minutes]} {1}".format(matches, pm, current_time.year)
|
|
||||||
splatoon_time = datetime.datetime.strptime(fmt, '%Y/%m/%d %I:%M %p') + datetime.timedelta(hours=offset)
|
|
||||||
|
|
||||||
# check for new year
|
|
||||||
if current_time.month == 12 and splatoon_time.month == 1:
|
|
||||||
splatoon_time.replace(current_time.year + 1)
|
|
||||||
|
|
||||||
return splatoon_time
|
|
||||||
|
|
||||||
|
|
||||||
async def get_splatnet_schedule(splatnet_cookie):
|
|
||||||
cookies = {'_wag_session': splatnet_cookie}
|
|
||||||
|
|
||||||
|
|
||||||
"""
|
|
||||||
This is repeated 3 times:
|
|
||||||
|
|
||||||
<span class"stage-schedule"> ... </span> <--- figure out how to parse this
|
|
||||||
<div class="stage-list">
|
|
||||||
<div class="match-type">
|
|
||||||
<span class="icon-regular-match"></span> <--- turf war
|
|
||||||
</div>
|
|
||||||
... <span class="map-name"> ... </span>
|
|
||||||
... <span class="map-name"> ... </span>
|
|
||||||
</div>
|
|
||||||
<div class="stage-list">
|
|
||||||
<div class="match-type">
|
|
||||||
<span class="icon-earnest-match"></span> <--- ranked
|
|
||||||
</div>
|
|
||||||
... <span class="rule-description"> ... </span> <--- Splat Zones, Rainmaker, Tower Control
|
|
||||||
... <span class="map-name"> ... </span>
|
|
||||||
... <span class="map-name"> ... </span>
|
|
||||||
</div>
|
|
||||||
"""
|
|
||||||
|
|
||||||
schedule = []
|
|
||||||
async with aiohttp.get(SPLATNET_SCHEDULE_URL, cookies=cookies, data={'locale':"en"}) as response:
|
|
||||||
text = await response.text()
|
|
||||||
root = etree.fromstring(text, etree.HTMLParser())
|
|
||||||
stage_schedule_nodes = root.xpath("//*[@class='stage-schedule']")
|
|
||||||
stage_list_nodes = root.xpath("//*[@class='stage-list']")
|
|
||||||
|
|
||||||
if len(stage_schedule_nodes)*2 != len(stage_list_nodes):
|
|
||||||
raise RuntimeError("SplatNet changed, need to update the parsing!")
|
|
||||||
|
|
||||||
for sched_node in stage_schedule_nodes:
|
|
||||||
r = Rotation()
|
|
||||||
|
|
||||||
start_time, end_time = sched_node.text.split("~")
|
|
||||||
r.start = parse_splatnet_time(start_time)
|
|
||||||
r.end = parse_splatnet_time(end_time)
|
|
||||||
|
|
||||||
tw_list_node = stage_list_nodes.pop(0)
|
|
||||||
r.turf_maps = tw_list_node.xpath(".//*[@class='map-name']/text()")
|
|
||||||
|
|
||||||
ranked_list_node = stage_list_nodes.pop(0)
|
|
||||||
r.ranked_maps = ranked_list_node.xpath(".//*[@class='map-name']/text()")
|
|
||||||
r.ranked_mode = ranked_list_node.xpath(".//*[@class='rule-description']/text()")[0]
|
|
||||||
|
|
||||||
schedule.append(r)
|
|
||||||
|
|
||||||
return schedule
|
|
|
@ -1,140 +0,0 @@
|
||||||
import asyncio
|
|
||||||
|
|
||||||
class Menu:
|
|
||||||
"""An interactive menu class for Discord."""
|
|
||||||
|
|
||||||
|
|
||||||
class Submenu:
|
|
||||||
"""A metaclass of the Menu class."""
|
|
||||||
def __init__(self, name, content):
|
|
||||||
self.content = content
|
|
||||||
self.leads_to = []
|
|
||||||
self.name = name
|
|
||||||
|
|
||||||
def get_text(self):
|
|
||||||
text = ""
|
|
||||||
for idx, menu in enumerate(self.leads_to):
|
|
||||||
text += "[{}] {}\n".format(idx+1, menu.name)
|
|
||||||
return text
|
|
||||||
|
|
||||||
def get_child(self, child_idx):
|
|
||||||
try:
|
|
||||||
return self.leads_to[child_idx]
|
|
||||||
except IndexError:
|
|
||||||
raise IndexError("child index out of range")
|
|
||||||
|
|
||||||
def add_child(self, child):
|
|
||||||
self.leads_to.append(child)
|
|
||||||
|
|
||||||
class InputSubmenu:
|
|
||||||
"""A metaclass of the Menu class for submenu options that take input, instead of prompting the user to pick an option."""
|
|
||||||
def __init__(self, name, content, input_function, leads_to):
|
|
||||||
self.content = content
|
|
||||||
self.name = name
|
|
||||||
self.input_function = input_function
|
|
||||||
self.leads_to = leads_to
|
|
||||||
|
|
||||||
def next_child(self):
|
|
||||||
return self.leads_to
|
|
||||||
|
|
||||||
class ChoiceSubmenu:
|
|
||||||
"""A metaclass of the Menu class for submenu options for choosing an option from a list."""
|
|
||||||
def __init__(self, name, content, options, input_function, leads_to):
|
|
||||||
self.content = content
|
|
||||||
self.name = name
|
|
||||||
self.options = options
|
|
||||||
self.input_function = input_function
|
|
||||||
self.leads_to = leads_to
|
|
||||||
|
|
||||||
def next_child(self):
|
|
||||||
return self.leads_to
|
|
||||||
|
|
||||||
|
|
||||||
def __init__(self, main_page):
|
|
||||||
self.children = []
|
|
||||||
self.main = self.Submenu("main", main_page)
|
|
||||||
|
|
||||||
def add_child(self, child):
|
|
||||||
self.main.add_child(child)
|
|
||||||
|
|
||||||
async def start(self, ctx):
|
|
||||||
current = self.main
|
|
||||||
menu_msg = None
|
|
||||||
while True:
|
|
||||||
output = ""
|
|
||||||
|
|
||||||
if type(current) == self.Submenu:
|
|
||||||
if type(current.content) == str:
|
|
||||||
output += current.content + "\n"
|
|
||||||
elif callable(current.content):
|
|
||||||
current.content()
|
|
||||||
else:
|
|
||||||
raise TypeError("submenu body is not a str or function")
|
|
||||||
|
|
||||||
if not current.leads_to:
|
|
||||||
if not menu_msg:
|
|
||||||
menu_msg = await ctx.send("```" + output + "```")
|
|
||||||
else:
|
|
||||||
await menu_msg.edit(content="```" + output + "```")
|
|
||||||
break
|
|
||||||
|
|
||||||
output += "\n" + current.get_text() + "\n"
|
|
||||||
output += "Enter a number."
|
|
||||||
|
|
||||||
if not menu_msg:
|
|
||||||
menu_msg = await ctx.send("```" + output + "```")
|
|
||||||
else:
|
|
||||||
await menu_msg.edit(content="```" + output + "```")
|
|
||||||
|
|
||||||
reply = await ctx.bot.wait_for("message", check=lambda m: m.author == ctx.bot.user and m.content.isdigit() and m.channel == ctx.message.channel)
|
|
||||||
await reply.delete()
|
|
||||||
|
|
||||||
try:
|
|
||||||
current = current.get_child(int(reply.content) - 1)
|
|
||||||
except IndexError:
|
|
||||||
print("Invalid number.")
|
|
||||||
break
|
|
||||||
|
|
||||||
elif type(current) == self.InputSubmenu:
|
|
||||||
if type(current.content) == list:
|
|
||||||
answers = []
|
|
||||||
for question in current.content:
|
|
||||||
await menu_msg.edit(content="```" + question + "\n\nEnter a value." + "```")
|
|
||||||
reply = await ctx.bot.wait_for("message", check=lambda m: m.author == ctx.bot.user and m.channel == ctx.message.channel)
|
|
||||||
await reply.delete()
|
|
||||||
answers.append(reply)
|
|
||||||
current.input_function(*answers)
|
|
||||||
else:
|
|
||||||
await menu_msg.edit(content="```" + current.content + "\n\nEnter a value." + "```")
|
|
||||||
reply = await ctx.bot.wait_for("message", check=lambda m: m.author == ctx.bot.user and m.channel == ctx.message.channel)
|
|
||||||
await reply.delete()
|
|
||||||
current.input_function(reply)
|
|
||||||
|
|
||||||
if not current.leads_to:
|
|
||||||
break
|
|
||||||
|
|
||||||
current = current.leads_to
|
|
||||||
|
|
||||||
elif type(current) == self.ChoiceSubmenu:
|
|
||||||
result = "```" + current.content + "\n\n"
|
|
||||||
if type(current.options) == dict:
|
|
||||||
indexes = {}
|
|
||||||
for idx, option in enumerate(current.options):
|
|
||||||
result += "[{}] {}: {}\n".format(idx+1, option, current.options[option])
|
|
||||||
indexes[idx] = option
|
|
||||||
else:
|
|
||||||
for idx, option in current.options:
|
|
||||||
result += "[{}] {}\n".format(idx+1, option)
|
|
||||||
await menu_msg.edit(content=result + "\nPick an option.```")
|
|
||||||
reply = await ctx.bot.wait_for("message", check=lambda m: m.author == ctx.bot.user and m.content.isdigit() and m.channel == ctx.message.channel)
|
|
||||||
await reply.delete()
|
|
||||||
if type(current.options) == dict:
|
|
||||||
current.input_function(reply, indexes[int(reply.content)-1])
|
|
||||||
else:
|
|
||||||
current.input_function(reply, current.options[reply-1])
|
|
||||||
|
|
||||||
if not current.leads_to:
|
|
||||||
break
|
|
||||||
|
|
||||||
current = current.leads_to
|
|
||||||
|
|
|
@ -1,503 +0,0 @@
|
||||||
# Help paginator by Rapptz
|
|
||||||
# Edited by F4stZ4p
|
|
||||||
|
|
||||||
import asyncio
|
|
||||||
import discord
|
|
||||||
|
|
||||||
class CannotPaginate(Exception):
|
|
||||||
pass
|
|
||||||
|
|
||||||
class Pages:
|
|
||||||
"""Implements a paginator that queries the user for the
|
|
||||||
pagination interface.
|
|
||||||
Pages are 1-index based, not 0-index based.
|
|
||||||
If the user does not reply within 2 minutes then the pagination
|
|
||||||
interface exits automatically.
|
|
||||||
Parameters
|
|
||||||
------------
|
|
||||||
ctx: Context
|
|
||||||
The context of the command.
|
|
||||||
entries: List[str]
|
|
||||||
A list of entries to paginate.
|
|
||||||
per_page: int
|
|
||||||
How many entries show up per page.
|
|
||||||
show_entry_count: bool
|
|
||||||
Whether to show an entry count in the footer.
|
|
||||||
Attributes
|
|
||||||
-----------
|
|
||||||
embed: discord.Embed
|
|
||||||
The embed object that is being used to send pagination info.
|
|
||||||
Feel free to modify this externally. Only the description,
|
|
||||||
footer fields, and colour are internally modified.
|
|
||||||
permissions: discord.Permissions
|
|
||||||
Our permissions for the channel.
|
|
||||||
"""
|
|
||||||
def __init__(self, ctx, *, entries, per_page=12, show_entry_count=True):
|
|
||||||
self.bot = ctx.bot
|
|
||||||
self.entries = entries
|
|
||||||
self.message = ctx.message
|
|
||||||
self.channel = ctx.channel
|
|
||||||
self.author = ctx.author
|
|
||||||
self.per_page = per_page
|
|
||||||
pages, left_over = divmod(len(self.entries), self.per_page)
|
|
||||||
if left_over:
|
|
||||||
pages += 1
|
|
||||||
self.maximum_pages = pages
|
|
||||||
self.embed = discord.Embed(colour=discord.Color.green())
|
|
||||||
self.paginating = len(entries) > per_page
|
|
||||||
self.show_entry_count = show_entry_count
|
|
||||||
self.reaction_emojis = [
|
|
||||||
('\N{BLACK LEFT-POINTING DOUBLE TRIANGLE WITH VERTICAL BAR}', self.first_page),
|
|
||||||
('\N{BLACK LEFT-POINTING TRIANGLE}', self.previous_page),
|
|
||||||
('\N{BLACK RIGHT-POINTING TRIANGLE}', self.next_page),
|
|
||||||
('\N{BLACK RIGHT-POINTING DOUBLE TRIANGLE WITH VERTICAL BAR}', self.last_page),
|
|
||||||
('\N{INPUT SYMBOL FOR NUMBERS}', self.numbered_page ),
|
|
||||||
('\N{BLACK SQUARE FOR STOP}', self.stop_pages),
|
|
||||||
('\N{INFORMATION SOURCE}', self.show_help),
|
|
||||||
]
|
|
||||||
|
|
||||||
if ctx.guild is not None:
|
|
||||||
self.permissions = self.channel.permissions_for(ctx.guild.me)
|
|
||||||
else:
|
|
||||||
self.permissions = self.channel.permissions_for(ctx.bot.user)
|
|
||||||
|
|
||||||
if not self.permissions.embed_links:
|
|
||||||
raise CannotPaginate('Bot does not have embed links permission.')
|
|
||||||
|
|
||||||
if not self.permissions.send_messages:
|
|
||||||
raise CannotPaginate('Bot cannot send messages.')
|
|
||||||
|
|
||||||
if self.paginating:
|
|
||||||
# verify we can actually use the pagination session
|
|
||||||
if not self.permissions.add_reactions:
|
|
||||||
raise CannotPaginate('Bot does not have add reactions permission.')
|
|
||||||
|
|
||||||
if not self.permissions.read_message_history:
|
|
||||||
raise CannotPaginate('Bot does not have Read Message History permission.')
|
|
||||||
|
|
||||||
def get_page(self, page):
|
|
||||||
base = (page - 1) * self.per_page
|
|
||||||
return self.entries[base:base + self.per_page]
|
|
||||||
|
|
||||||
async def show_page(self, page, *, first=False):
|
|
||||||
self.current_page = page
|
|
||||||
entries = self.get_page(page)
|
|
||||||
p = []
|
|
||||||
for index, entry in enumerate(entries, 1 + ((page - 1) * self.per_page)):
|
|
||||||
p.append(f'{index}. {entry}')
|
|
||||||
|
|
||||||
if self.maximum_pages > 1:
|
|
||||||
if self.show_entry_count:
|
|
||||||
text = f'Page {page}/{self.maximum_pages} ({len(self.entries)} entries)'
|
|
||||||
else:
|
|
||||||
text = f'Page {page}/{self.maximum_pages}'
|
|
||||||
|
|
||||||
self.embed.set_footer(text=text)
|
|
||||||
|
|
||||||
if not self.paginating:
|
|
||||||
self.embed.description = '\n'.join(p)
|
|
||||||
return await self.channel.send(embed=self.embed)
|
|
||||||
|
|
||||||
if not first:
|
|
||||||
self.embed.description = '\n'.join(p)
|
|
||||||
await self.message.edit(embed=self.embed)
|
|
||||||
return
|
|
||||||
|
|
||||||
p.append('')
|
|
||||||
p.append('Confused? React with \N{INFORMATION SOURCE} for more info.')
|
|
||||||
self.embed.description = '\n'.join(p)
|
|
||||||
self.message = await self.channel.send(embed=self.embed)
|
|
||||||
for (reaction, _) in self.reaction_emojis:
|
|
||||||
if self.maximum_pages == 2 and reaction in ('\u23ed', '\u23ee'):
|
|
||||||
# no |<< or >>| buttons if we only have two pages
|
|
||||||
# we can't forbid it if someone ends up using it but remove
|
|
||||||
# it from the default set
|
|
||||||
continue
|
|
||||||
|
|
||||||
await self.message.add_reaction(reaction)
|
|
||||||
|
|
||||||
async def checked_show_page(self, page):
|
|
||||||
if page != 0 and page <= self.maximum_pages:
|
|
||||||
await self.show_page(page)
|
|
||||||
|
|
||||||
async def first_page(self):
|
|
||||||
"""goes to the first page"""
|
|
||||||
await self.show_page(1)
|
|
||||||
|
|
||||||
async def last_page(self):
|
|
||||||
"""goes to the last page"""
|
|
||||||
await self.show_page(self.maximum_pages)
|
|
||||||
|
|
||||||
async def next_page(self):
|
|
||||||
"""goes to the next page"""
|
|
||||||
await self.checked_show_page(self.current_page + 1)
|
|
||||||
|
|
||||||
async def previous_page(self):
|
|
||||||
"""goes to the previous page"""
|
|
||||||
await self.checked_show_page(self.current_page - 1)
|
|
||||||
|
|
||||||
async def show_current_page(self):
|
|
||||||
if self.paginating:
|
|
||||||
await self.show_page(self.current_page)
|
|
||||||
|
|
||||||
async def numbered_page(self):
|
|
||||||
"""lets you type a page number to go to"""
|
|
||||||
to_delete = []
|
|
||||||
to_delete.append(await self.channel.send('What page do you want to go to?'))
|
|
||||||
|
|
||||||
def message_check(m):
|
|
||||||
return m.author == self.author and \
|
|
||||||
self.channel == m.channel and \
|
|
||||||
m.content.isdigit()
|
|
||||||
|
|
||||||
try:
|
|
||||||
msg = await self.bot.wait_for('message', check=message_check, timeout=30.0)
|
|
||||||
except asyncio.TimeoutError:
|
|
||||||
to_delete.append(await self.channel.send('Took too long.'))
|
|
||||||
await asyncio.sleep(5)
|
|
||||||
else:
|
|
||||||
page = int(msg.content)
|
|
||||||
to_delete.append(msg)
|
|
||||||
if page != 0 and page <= self.maximum_pages:
|
|
||||||
await self.show_page(page)
|
|
||||||
else:
|
|
||||||
to_delete.append(await self.channel.send(f'Invalid page given. ({page}/{self.maximum_pages})'))
|
|
||||||
await asyncio.sleep(5)
|
|
||||||
|
|
||||||
try:
|
|
||||||
await self.channel.delete_messages(to_delete)
|
|
||||||
except Exception:
|
|
||||||
pass
|
|
||||||
|
|
||||||
async def show_help(self):
|
|
||||||
"""shows this message"""
|
|
||||||
messages = ['Welcome to the interactive paginator!\n']
|
|
||||||
messages.append('This interactively allows you to see pages of text by navigating with ' \
|
|
||||||
'reactions. They are as follows:\n')
|
|
||||||
|
|
||||||
for (emoji, func) in self.reaction_emojis:
|
|
||||||
messages.append(f'{emoji} {func.__doc__}')
|
|
||||||
|
|
||||||
self.embed.description = '\n'.join(messages)
|
|
||||||
self.embed.clear_fields()
|
|
||||||
self.embed.set_footer(text=f'We were on page {self.current_page} before this message.')
|
|
||||||
await self.message.edit(embed=self.embed)
|
|
||||||
|
|
||||||
async def go_back_to_current_page():
|
|
||||||
await asyncio.sleep(60.0)
|
|
||||||
await self.show_current_page()
|
|
||||||
|
|
||||||
self.bot.loop.create_task(go_back_to_current_page())
|
|
||||||
|
|
||||||
async def stop_pages(self):
|
|
||||||
"""stops the interactive pagination session"""
|
|
||||||
await self.message.delete()
|
|
||||||
self.paginating = False
|
|
||||||
|
|
||||||
def react_check(self, reaction, user):
|
|
||||||
if user is None or user.id != self.author.id:
|
|
||||||
return False
|
|
||||||
|
|
||||||
if reaction.message.id != self.message.id:
|
|
||||||
return False
|
|
||||||
|
|
||||||
for (emoji, func) in self.reaction_emojis:
|
|
||||||
if reaction.emoji == emoji:
|
|
||||||
self.match = func
|
|
||||||
return True
|
|
||||||
return False
|
|
||||||
|
|
||||||
async def paginate(self):
|
|
||||||
"""Actually paginate the entries and run the interactive loop if necessary."""
|
|
||||||
first_page = self.show_page(1, first=True)
|
|
||||||
if not self.paginating:
|
|
||||||
await first_page
|
|
||||||
else:
|
|
||||||
# allow us to react to reactions right away if we're paginating
|
|
||||||
self.bot.loop.create_task(first_page)
|
|
||||||
|
|
||||||
while self.paginating:
|
|
||||||
try:
|
|
||||||
reaction, user = await self.bot.wait_for('reaction_add', check=self.react_check, timeout=120.0)
|
|
||||||
except asyncio.TimeoutError:
|
|
||||||
self.paginating = False
|
|
||||||
try:
|
|
||||||
await self.message.clear_reactions()
|
|
||||||
except:
|
|
||||||
pass
|
|
||||||
finally:
|
|
||||||
break
|
|
||||||
|
|
||||||
try:
|
|
||||||
await self.message.remove_reaction(reaction, user)
|
|
||||||
except:
|
|
||||||
pass # can't remove it so don't bother doing so
|
|
||||||
|
|
||||||
await self.match()
|
|
||||||
|
|
||||||
class FieldPages(Pages):
|
|
||||||
"""Similar to Pages except entries should be a list of
|
|
||||||
tuples having (key, value) to show as embed fields instead.
|
|
||||||
"""
|
|
||||||
async def show_page(self, page, *, first=False):
|
|
||||||
self.current_page = page
|
|
||||||
entries = self.get_page(page)
|
|
||||||
|
|
||||||
self.embed.clear_fields()
|
|
||||||
self.embed.description = discord.Embed.Empty
|
|
||||||
|
|
||||||
for key, value in entries:
|
|
||||||
self.embed.add_field(name=key, value=value, inline=False)
|
|
||||||
|
|
||||||
if self.maximum_pages > 1:
|
|
||||||
if self.show_entry_count:
|
|
||||||
text = f'Page {page}/{self.maximum_pages} ({len(self.entries)} entries)'
|
|
||||||
else:
|
|
||||||
text = f'Page {page}/{self.maximum_pages}'
|
|
||||||
|
|
||||||
self.embed.set_footer(text=text)
|
|
||||||
|
|
||||||
if not self.paginating:
|
|
||||||
return await self.channel.send(embed=self.embed)
|
|
||||||
|
|
||||||
if not first:
|
|
||||||
await self.message.edit(embed=self.embed)
|
|
||||||
return
|
|
||||||
|
|
||||||
self.message = await self.channel.send(embed=self.embed)
|
|
||||||
for (reaction, _) in self.reaction_emojis:
|
|
||||||
if self.maximum_pages == 2 and reaction in ('\u23ed', '\u23ee'):
|
|
||||||
# no |<< or >>| buttons if we only have two pages
|
|
||||||
# we can't forbid it if someone ends up using it but remove
|
|
||||||
# it from the default set
|
|
||||||
continue
|
|
||||||
|
|
||||||
await self.message.add_reaction(reaction)
|
|
||||||
|
|
||||||
import itertools
|
|
||||||
import inspect
|
|
||||||
import re
|
|
||||||
|
|
||||||
# ?help
|
|
||||||
# ?help Cog
|
|
||||||
# ?help command
|
|
||||||
# -> could be a subcommand
|
|
||||||
|
|
||||||
_mention = re.compile(r'<@\!?([0-9]{1,19})>')
|
|
||||||
|
|
||||||
def cleanup_prefix(bot, prefix):
|
|
||||||
m = _mention.match(prefix)
|
|
||||||
if m:
|
|
||||||
user = bot.get_user(int(m.group(1)))
|
|
||||||
if user:
|
|
||||||
return f'@{user.name} '
|
|
||||||
return prefix
|
|
||||||
|
|
||||||
async def _can_run(cmd, ctx):
|
|
||||||
try:
|
|
||||||
return await cmd.can_run(ctx)
|
|
||||||
except:
|
|
||||||
return False
|
|
||||||
|
|
||||||
def _command_signature(cmd):
|
|
||||||
# this is modified from discord.py source
|
|
||||||
# which I wrote myself lmao
|
|
||||||
|
|
||||||
result = [cmd.qualified_name]
|
|
||||||
if cmd.usage:
|
|
||||||
result.append(cmd.usage)
|
|
||||||
return ' '.join(result)
|
|
||||||
|
|
||||||
params = cmd.clean_params
|
|
||||||
if not params:
|
|
||||||
return ' '.join(result)
|
|
||||||
|
|
||||||
for name, param in params.items():
|
|
||||||
if param.default is not param.empty:
|
|
||||||
# We don't want None or '' to trigger the [name=value] case and instead it should
|
|
||||||
# do [name] since [name=None] or [name=] are not exactly useful for the user.
|
|
||||||
should_print = param.default if isinstance(param.default, str) else param.default is not None
|
|
||||||
if should_print:
|
|
||||||
result.append(f'[{name}={param.default!r}]')
|
|
||||||
else:
|
|
||||||
result.append(f'[{name}]')
|
|
||||||
elif param.kind == param.VAR_POSITIONAL:
|
|
||||||
result.append(f'[{name}...]')
|
|
||||||
else:
|
|
||||||
result.append(f'<{name}>')
|
|
||||||
|
|
||||||
return ' '.join(result)
|
|
||||||
|
|
||||||
class HelpPaginator(Pages):
|
|
||||||
def __init__(self, ctx, entries, *, per_page=4):
|
|
||||||
super().__init__(ctx, entries=entries, per_page=per_page)
|
|
||||||
self.reaction_emojis.append(('\N{WHITE QUESTION MARK ORNAMENT}', self.show_bot_help))
|
|
||||||
self.total = len(entries)
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
async def from_cog(cls, ctx, cog):
|
|
||||||
cog_name = cog.__class__.__name__
|
|
||||||
|
|
||||||
# get the commands
|
|
||||||
entries = sorted(ctx.bot.get_cog(cog_name).get_commands(), key=lambda c: c.name)
|
|
||||||
|
|
||||||
# remove the ones we can't run
|
|
||||||
entries = [cmd for cmd in entries if (await _can_run(cmd, ctx)) and not cmd.hidden]
|
|
||||||
|
|
||||||
self = cls(ctx, entries)
|
|
||||||
self.title = f'{cog_name} Commands'
|
|
||||||
self.description = inspect.getdoc(cog)
|
|
||||||
self.prefix = cleanup_prefix(ctx.bot, ctx.prefix)
|
|
||||||
|
|
||||||
return self
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
async def from_command(cls, ctx, command):
|
|
||||||
try:
|
|
||||||
entries = sorted(command.commands, key=lambda c: c.name)
|
|
||||||
except AttributeError:
|
|
||||||
entries = []
|
|
||||||
else:
|
|
||||||
entries = [cmd for cmd in entries if (await _can_run(cmd, ctx)) and not cmd.hidden]
|
|
||||||
|
|
||||||
self = cls(ctx, entries)
|
|
||||||
self.title = command.signature
|
|
||||||
|
|
||||||
if command.description:
|
|
||||||
self.description = f'{command.description}\n\n{command.help}'
|
|
||||||
else:
|
|
||||||
self.description = command.help or 'No help given.'
|
|
||||||
|
|
||||||
self.prefix = cleanup_prefix(ctx.bot, ctx.prefix)
|
|
||||||
return self
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
async def from_bot(cls, ctx):
|
|
||||||
def key(c):
|
|
||||||
return c.cog_name or '\u200bMisc'
|
|
||||||
|
|
||||||
entries = sorted(ctx.bot.commands, key=key)
|
|
||||||
nested_pages = []
|
|
||||||
per_page = 9
|
|
||||||
|
|
||||||
# 0: (cog, desc, commands) (max len == 9)
|
|
||||||
# 1: (cog, desc, commands) (max len == 9)
|
|
||||||
# ...
|
|
||||||
|
|
||||||
for cog, commands in itertools.groupby(entries, key=key):
|
|
||||||
plausible = [cmd for cmd in commands if (await _can_run(cmd, ctx)) and not cmd.hidden]
|
|
||||||
if len(plausible) == 0:
|
|
||||||
continue
|
|
||||||
|
|
||||||
description = ctx.bot.get_cog(cog)
|
|
||||||
if description is None:
|
|
||||||
description = discord.Embed.Empty
|
|
||||||
else:
|
|
||||||
description = inspect.getdoc(description) or discord.Embed.Empty
|
|
||||||
|
|
||||||
nested_pages.extend((cog, description, plausible[i:i + per_page]) for i in range(0, len(plausible), per_page))
|
|
||||||
|
|
||||||
self = cls(ctx, nested_pages, per_page=1) # this forces the pagination session
|
|
||||||
self.prefix = cleanup_prefix(ctx.bot, ctx.prefix)
|
|
||||||
|
|
||||||
# swap the get_page implementation with one that supports our style of pagination
|
|
||||||
self.get_page = self.get_bot_page
|
|
||||||
self._is_bot = True
|
|
||||||
|
|
||||||
# replace the actual total
|
|
||||||
self.total = sum(len(o) for _, _, o in nested_pages)
|
|
||||||
return self
|
|
||||||
|
|
||||||
def get_bot_page(self, page):
|
|
||||||
cog, description, commands = self.entries[page - 1]
|
|
||||||
self.title = f'{cog} Commands'
|
|
||||||
self.description = description
|
|
||||||
return commands
|
|
||||||
|
|
||||||
async def show_page(self, page, *, first=False):
|
|
||||||
self.current_page = page
|
|
||||||
entries = self.get_page(page)
|
|
||||||
|
|
||||||
self.embed.clear_fields()
|
|
||||||
self.embed.description = self.description
|
|
||||||
self.embed.title = self.title
|
|
||||||
|
|
||||||
if hasattr(self, '_is_bot'):
|
|
||||||
value ='Check the bot source: **[GitHub Link](https://github.com/F4stZ4p/DJ5n4k3/)**'
|
|
||||||
self.embed.add_field(name='**GitHub**', value=value, inline=False)
|
|
||||||
|
|
||||||
self.embed.set_footer(text=f'Use "{self.prefix}help command" for more info on a command.')
|
|
||||||
|
|
||||||
signature = _command_signature
|
|
||||||
|
|
||||||
for entry in entries:
|
|
||||||
self.embed.add_field(name=signature(entry), value=entry.short_doc or "No help given", inline=False)
|
|
||||||
|
|
||||||
if self.maximum_pages:
|
|
||||||
self.embed.set_author(name=f'Page {page}/{self.maximum_pages} ({self.total} commands)')
|
|
||||||
|
|
||||||
if not self.paginating:
|
|
||||||
return await self.channel.send(embed=self.embed)
|
|
||||||
|
|
||||||
if not first:
|
|
||||||
await self.message.edit(embed=self.embed)
|
|
||||||
return
|
|
||||||
|
|
||||||
self.message = await self.channel.send(embed=self.embed)
|
|
||||||
for (reaction, _) in self.reaction_emojis:
|
|
||||||
if self.maximum_pages == 2 and reaction in ('\u23ed', '\u23ee'):
|
|
||||||
# no |<< or >>| buttons if we only have two pages
|
|
||||||
# we can't forbid it if someone ends up using it but remove
|
|
||||||
# it from the default set
|
|
||||||
continue
|
|
||||||
|
|
||||||
await self.message.add_reaction(reaction)
|
|
||||||
|
|
||||||
async def show_help(self):
|
|
||||||
"""shows this message"""
|
|
||||||
|
|
||||||
self.embed.title = 'Paginator help'
|
|
||||||
self.embed.description = 'Hello! Welcome to the help page.'
|
|
||||||
|
|
||||||
messages = [f'{emoji} {func.__doc__}' for emoji, func in self.reaction_emojis]
|
|
||||||
self.embed.clear_fields()
|
|
||||||
self.embed.add_field(name='What are these reactions for?', value='\n'.join(messages), inline=False)
|
|
||||||
|
|
||||||
self.embed.set_footer(text=f'We were on page {self.current_page} before this message.')
|
|
||||||
await self.message.edit(embed=self.embed)
|
|
||||||
|
|
||||||
async def go_back_to_current_page():
|
|
||||||
await asyncio.sleep(30.0)
|
|
||||||
await self.show_current_page()
|
|
||||||
|
|
||||||
self.bot.loop.create_task(go_back_to_current_page())
|
|
||||||
|
|
||||||
async def show_bot_help(self):
|
|
||||||
"""shows how to use the bot"""
|
|
||||||
|
|
||||||
self.embed.title = 'Using the bot'
|
|
||||||
self.embed.description = 'Hello! Welcome to the help page.'
|
|
||||||
self.embed.clear_fields()
|
|
||||||
|
|
||||||
entries = (
|
|
||||||
('<argument>', 'This means the argument is __**required**__.'),
|
|
||||||
('[argument]', 'This means the argument is __**optional**__.'),
|
|
||||||
('[A|B]', 'This means the it can be __**either A or B**__.'),
|
|
||||||
('[argument...]', 'This means you can have multiple arguments.\n' \
|
|
||||||
'Now that you know the basics, it should be noted that...\n' \
|
|
||||||
'__**You do not type in the brackets!**__')
|
|
||||||
)
|
|
||||||
|
|
||||||
self.embed.add_field(name='How do I use this bot?', value='Reading the bot signature is pretty simple.')
|
|
||||||
|
|
||||||
for name, value in entries:
|
|
||||||
self.embed.add_field(name=name, value=value, inline=False)
|
|
||||||
|
|
||||||
self.embed.set_footer(text=f'We were on page {self.current_page} before this message.')
|
|
||||||
await self.message.edit(embed=self.embed)
|
|
||||||
|
|
||||||
async def go_back_to_current_page():
|
|
||||||
await asyncio.sleep(30.0)
|
|
||||||
await self.show_current_page()
|
|
||||||
|
|
||||||
self.bot.loop.create_task(go_back_to_current_page())
|
|
|
@ -1,5 +1,10 @@
|
||||||
from .config import Config
|
from .initializer import Config
|
||||||
|
|
||||||
setup = Config()
|
setup = Config()
|
||||||
|
|
||||||
|
setup.install()
|
||||||
|
|
||||||
setup.ask()
|
setup.ask()
|
||||||
setup.save()
|
setup.save()
|
||||||
|
|
||||||
|
setup.clean()
|
|
@ -1,3 +1,6 @@
|
||||||
|
from pip._internal import main as pip
|
||||||
|
import shutil
|
||||||
|
|
||||||
from .langs import locales, texts
|
from .langs import locales, texts
|
||||||
|
|
||||||
|
|
||||||
|
@ -10,6 +13,9 @@ class Config:
|
||||||
'unkickable_id': '[unkickable ids here (in int)]'
|
'unkickable_id': '[unkickable ids here (in int)]'
|
||||||
}
|
}
|
||||||
|
|
||||||
|
with open('requirements.txt', 'r') as f:
|
||||||
|
self.packages = f.read().split('\n')
|
||||||
|
|
||||||
def input(self, key, **kwargs):
|
def input(self, key, **kwargs):
|
||||||
lang = self.config.get('lang', 'multiple')
|
lang = self.config.get('lang', 'multiple')
|
||||||
|
|
||||||
|
@ -32,13 +38,30 @@ class Config:
|
||||||
|
|
||||||
self.config[key] = response
|
self.config[key] = response
|
||||||
|
|
||||||
def ask(self):
|
def install(self):
|
||||||
self.input('lang', valid=locales)
|
self.input('lang', valid=locales)
|
||||||
|
print('\n\n\033[4;36m'
|
||||||
|
+ texts.get(self.config.get('lang')).get('install')
|
||||||
|
+ '\033[0m\n')
|
||||||
|
|
||||||
|
for package in self.packages:
|
||||||
|
pip(['install', package])
|
||||||
|
|
||||||
|
def ask(self):
|
||||||
|
print('\n\n\033[4;36m' + texts.get(self.config.get('lang')).get('conf')
|
||||||
|
+ '\033[0m\n')
|
||||||
|
|
||||||
self.input('token', empty=False)
|
self.input('token', empty=False)
|
||||||
self.input('postgresql_username', empty=False)
|
self.input('postgresql_username', empty=False)
|
||||||
self.input('postgresql_password', empty=False)
|
self.input('postgresql_password', empty=False)
|
||||||
self.input('postgresql_dbname', empty=False)
|
self.input('postgresql_dbname', empty=False)
|
||||||
|
|
||||||
|
print('\n\n\033[4;36m' + texts.get(self.config.get('lang')).get('logs')
|
||||||
|
+ '\033[0m\n')
|
||||||
|
|
||||||
|
self.input('wh_id', empty=True)
|
||||||
|
self.input('wh_token', empty=True)
|
||||||
|
|
||||||
print('\n\n\033[4;36m' + texts.get(self.config.get('lang')).get('misc')
|
print('\n\n\033[4;36m' + texts.get(self.config.get('lang')).get('misc')
|
||||||
+ '\033[0m\n')
|
+ '\033[0m\n')
|
||||||
|
|
||||||
|
@ -49,13 +72,24 @@ class Config:
|
||||||
with open('config.py', 'w') as file:
|
with open('config.py', 'w') as file:
|
||||||
postgresql = f"postgresql://" \
|
postgresql = f"postgresql://" \
|
||||||
f"{self.config.get('postgresql_username')}:" \
|
f"{self.config.get('postgresql_username')}:" \
|
||||||
f"{self.config.get('postgresql_password')}@host/" \
|
f"{self.config.get('postgresql_password')}" \
|
||||||
f"{self.config.get('postgresql_dbname')}"
|
f"@localhost/{self.config.get('postgresql_dbname')}"
|
||||||
file.write(f"postgresql = '{postgresql}'\n")
|
file.write(f"postgresql = '{postgresql}'\n")
|
||||||
|
|
||||||
|
logs_webhook = dict(id=int(self.config.get('wh_id')),
|
||||||
|
token=self.config.get('wh_token'))
|
||||||
|
file.write(f"logs_webhook = '{logs_webhook}'\n")
|
||||||
|
|
||||||
for key, value in self.config.items():
|
for key, value in self.config.items():
|
||||||
if not key.startswith('postgresql_'):
|
if not key.startswith('postgresql_') \
|
||||||
|
and not key.startswith('wh_'):
|
||||||
value = f"'{value}'" if type(value) is str else value
|
value = f"'{value}'" if type(value) is str else value
|
||||||
file.write(f"{key} = {value}\n")
|
file.write(f"{key} = {value}\n")
|
||||||
print('\n\n\033[4;36m' + texts.get(self.config.get('lang')).get('end')
|
print('\n\n\033[4;36m' + texts.get(self.config.get('lang')).get('end')
|
||||||
+ '\033[0m\n')
|
+ '\033[0m\n')
|
||||||
|
|
||||||
|
def clean(self):
|
||||||
|
print('\n\n\033[4;36m'
|
||||||
|
+ texts.get(self.config.get('lang')).get('clean')
|
||||||
|
+ '\033[0m\n')
|
||||||
|
shutil.rmtree('first_run')
|
|
@ -1,6 +1,10 @@
|
||||||
locales = ['fr', 'en']
|
locales = ['fr', 'en']
|
||||||
texts = {
|
texts = {
|
||||||
'fr': {
|
'fr': {
|
||||||
|
'install': "Installation des modules...",
|
||||||
|
|
||||||
|
'conf': "Configuration...",
|
||||||
|
|
||||||
'token': "Veuillez entrer le token",
|
'token': "Veuillez entrer le token",
|
||||||
'not_empty': "Cette valeur ne doit pas être vide",
|
'not_empty': "Cette valeur ne doit pas être vide",
|
||||||
|
|
||||||
|
@ -8,15 +12,26 @@ texts = {
|
||||||
'postgresql_password': "Veuillez entrer le mot de passe de postgresql",
|
'postgresql_password': "Veuillez entrer le mot de passe de postgresql",
|
||||||
'postgresql_dbname': "Veuillez entrer le nom de la base de donnée",
|
'postgresql_dbname': "Veuillez entrer le nom de la base de donnée",
|
||||||
|
|
||||||
|
'logs': "Channel de logs (non obligatoire)",
|
||||||
|
|
||||||
|
'wh_id': "L'id du webhook pour le channel de logs",
|
||||||
|
'wh_token': "Le token du webhook pour le channel de logs",
|
||||||
|
|
||||||
'misc': 'Autre',
|
'misc': 'Autre',
|
||||||
|
|
||||||
'activity': "Joue à ...",
|
'activity': "Joue à ...",
|
||||||
'prefix': "Prefixe (par defaut : @tuxbot)",
|
'prefix': "Prefixe (par defaut : @tuxbot)",
|
||||||
|
|
||||||
"end": "Configuration terminée, vous pouvez à tout moment la rectifier en modifiant le fichier config.py"
|
'end': "Configuration terminée, vous pouvez à tout moment la rectifier en modifiant le fichier config.py",
|
||||||
|
|
||||||
|
'clean': "Nettoyage..."
|
||||||
},
|
},
|
||||||
|
|
||||||
'en': {
|
'en': {
|
||||||
|
'install': "Installation of the modules....",
|
||||||
|
|
||||||
|
'conf': "Configuration...",
|
||||||
|
|
||||||
'token': "Please enter the token",
|
'token': "Please enter the token",
|
||||||
'not_empty': "This value must not be empty",
|
'not_empty': "This value must not be empty",
|
||||||
|
|
||||||
|
@ -24,12 +39,19 @@ texts = {
|
||||||
'postgresql_password': "Please enter the postgresql password",
|
'postgresql_password': "Please enter the postgresql password",
|
||||||
'postgresql_dbname': "Please enter the database name",
|
'postgresql_dbname': "Please enter the database name",
|
||||||
|
|
||||||
|
'logs': "Log channel (not required)",
|
||||||
|
|
||||||
|
'wh_id': "Webhook id for log channel",
|
||||||
|
'wh_token': "Webhook token for log channel",
|
||||||
|
|
||||||
'misc': 'Misc',
|
'misc': 'Misc',
|
||||||
|
|
||||||
'activity': "Playing ...",
|
'activity': "Playing ...",
|
||||||
'prefix': "Prefix (default is @tuxbot)",
|
'prefix': "Prefix (default is @tuxbot)",
|
||||||
|
|
||||||
"end": "Configuration completed, you can fix it at any time by modifying the config.py file"
|
'end': "Configuration completed, you can fix it at any time by modifying the config.py file",
|
||||||
|
|
||||||
|
'clean': "Cleaning..."
|
||||||
},
|
},
|
||||||
|
|
||||||
'multiple': {
|
'multiple': {
|
||||||
|
|
42
launcher.py
42
launcher.py
|
@ -1,9 +1,17 @@
|
||||||
import logging
|
import asyncio
|
||||||
import contextlib
|
import contextlib
|
||||||
|
import logging
|
||||||
|
import socket
|
||||||
|
import sys
|
||||||
|
|
||||||
|
import click
|
||||||
|
|
||||||
|
from bot import TuxBot
|
||||||
|
from cogs.utils.db import Table
|
||||||
|
|
||||||
try:
|
try:
|
||||||
import config
|
import config
|
||||||
|
from cogs.utils.lang import _
|
||||||
except ModuleNotFoundError:
|
except ModuleNotFoundError:
|
||||||
import first_run
|
import first_run
|
||||||
|
|
||||||
|
@ -34,10 +42,34 @@ def setup_logging():
|
||||||
log.removeHandler(hdlr)
|
log.removeHandler(hdlr)
|
||||||
|
|
||||||
|
|
||||||
def run_bot():
|
def run_bot(unload):
|
||||||
pass # Todo: initialize bot, postgresql,...
|
loop = asyncio.get_event_loop()
|
||||||
|
log = logging.getLogger()
|
||||||
|
|
||||||
|
try:
|
||||||
|
pool = loop.run_until_complete(
|
||||||
|
Table.create_pool(config.postgresql, command_timeout=60)
|
||||||
|
)
|
||||||
|
except socket.gaierror as e:
|
||||||
|
click.echo(_('Could not set up PostgreSQL...'), file=sys.stderr)
|
||||||
|
log.exception(_('Could not set up PostgreSQL...'))
|
||||||
|
return
|
||||||
|
|
||||||
|
bot = TuxBot(unload)
|
||||||
|
bot.pool = pool
|
||||||
|
bot.run()
|
||||||
|
|
||||||
|
|
||||||
|
@click.group(invoke_without_command=True, options_metavar='[options]')
|
||||||
|
@click.option('-u', '--unload',
|
||||||
|
multiple=True, type=str,
|
||||||
|
help=_('Launch without loading the <TEXT> module'))
|
||||||
|
@click.pass_context
|
||||||
|
def main(ctx, unload):
|
||||||
|
if ctx.invoked_subcommand is None:
|
||||||
|
with setup_logging():
|
||||||
|
run_bot(unload)
|
||||||
|
|
||||||
|
|
||||||
if __name__ == '__main__':
|
if __name__ == '__main__':
|
||||||
with setup_logging():
|
main()
|
||||||
run_bot()
|
|
||||||
|
|
BIN
locales/en/LC_MESSAGES/base.mo
Normal file
BIN
locales/en/LC_MESSAGES/base.mo
Normal file
Binary file not shown.
45
locales/en/LC_MESSAGES/base.po
Normal file
45
locales/en/LC_MESSAGES/base.po
Normal file
|
@ -0,0 +1,45 @@
|
||||||
|
# SOME DESCRIPTIVE TITLE.
|
||||||
|
# Copyright (C) YEAR ORGANIZATION
|
||||||
|
# FIRST AUTHOR <EMAIL@ADDRESS>, YEAR.
|
||||||
|
#
|
||||||
|
msgid ""
|
||||||
|
msgstr ""
|
||||||
|
"Project-Id-Version: PACKAGE VERSION\n"
|
||||||
|
"POT-Creation-Date: 2019-09-08 19:04+0200\n"
|
||||||
|
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
|
||||||
|
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
|
||||||
|
"Language-Team: LANGUAGE <LL@li.org>\n"
|
||||||
|
"MIME-Version: 1.0\n"
|
||||||
|
"Content-Type: text/plain; charset=UTF-8\n"
|
||||||
|
"Content-Transfer-Encoding: 8bit\n"
|
||||||
|
"Generated-By: pygettext.py 1.5\n"
|
||||||
|
|
||||||
|
|
||||||
|
#: launcher.py:56 launcher.py:57
|
||||||
|
msgid "Could not set up PostgreSQL..."
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: launcher.py:68
|
||||||
|
msgid "Launch without loading the <TEXT> module"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: bot.py:52
|
||||||
|
msgid "Failed to load extension : "
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: bot.py:59
|
||||||
|
msgid "This command cannot be used in private messages."
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: bot.py:63
|
||||||
|
msgid "Sorry. This command is disabled and cannot be used."
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: bot.py:66
|
||||||
|
msgid "In "
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: bot.py:77
|
||||||
|
msgid "Ready:"
|
||||||
|
msgstr ""
|
||||||
|
|
BIN
locales/fr/LC_MESSAGES/base.mo
Normal file
BIN
locales/fr/LC_MESSAGES/base.mo
Normal file
Binary file not shown.
45
locales/fr/LC_MESSAGES/base.po
Normal file
45
locales/fr/LC_MESSAGES/base.po
Normal file
|
@ -0,0 +1,45 @@
|
||||||
|
# SOME DESCRIPTIVE TITLE.
|
||||||
|
# Copyright (C) YEAR ORGANIZATION
|
||||||
|
# FIRST AUTHOR <EMAIL@ADDRESS>, YEAR.
|
||||||
|
#
|
||||||
|
msgid ""
|
||||||
|
msgstr ""
|
||||||
|
"Project-Id-Version: PACKAGE VERSION\n"
|
||||||
|
"POT-Creation-Date: 2019-09-08 19:04+0200\n"
|
||||||
|
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
|
||||||
|
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
|
||||||
|
"Language-Team: LANGUAGE <LL@li.org>\n"
|
||||||
|
"MIME-Version: 1.0\n"
|
||||||
|
"Content-Type: text/plain; charset=UTF-8\n"
|
||||||
|
"Content-Transfer-Encoding: 8bit\n"
|
||||||
|
"Generated-By: pygettext.py 1.5\n"
|
||||||
|
|
||||||
|
|
||||||
|
#: launcher.py:56 launcher.py:57
|
||||||
|
msgid "Could not set up PostgreSQL..."
|
||||||
|
msgstr "Impossible de lancer PostgreSQL..."
|
||||||
|
|
||||||
|
#: launcher.py:68
|
||||||
|
msgid "Launch without loading the <TEXT> module"
|
||||||
|
msgstr "Lancer sans charger le module <TEXT>"
|
||||||
|
|
||||||
|
#: bot.py:52
|
||||||
|
msgid "Failed to load extension : "
|
||||||
|
msgstr "Impossible de charger l'extension : "
|
||||||
|
|
||||||
|
#: bot.py:59
|
||||||
|
msgid "This command cannot be used in private messages."
|
||||||
|
msgstr "Cette commande ne peut pas être utilisée en message privé."
|
||||||
|
|
||||||
|
#: bot.py:63
|
||||||
|
msgid "Sorry. This command is disabled and cannot be used."
|
||||||
|
msgstr "Désoler mais cette commande est désactivé."
|
||||||
|
|
||||||
|
#: bot.py:66
|
||||||
|
msgid "In "
|
||||||
|
msgstr "Dans "
|
||||||
|
|
||||||
|
#: bot.py:77
|
||||||
|
msgid "Ready:"
|
||||||
|
msgstr "Prêt:"
|
||||||
|
|
|
@ -0,0 +1,12 @@
|
||||||
|
[2019-09-08 22:58:46] [INFO ] discord.client: logging in using static token
|
||||||
|
[2019-09-08 22:58:47] [INFO ] discord.gateway: Shard ID 0 has sent the IDENTIFY payload.
|
||||||
|
[2019-09-08 22:58:47] [INFO ] discord.gateway: Shard ID 0 has connected to Gateway: ["gateway-prd-main-xfx5",{"micros":31802,"calls":["discord-sessions-prd-1-18",{"micros":26858,"calls":["start_session",{"micros":12548,"calls":["api-prd-main-6rfw",{"micros":8098,"calls":["get_user",{"micros":1942},"add_authorized_ip",{"micros":5},"get_guilds",{"micros":3036},"coros_wait",{"micros":3}]}]},"guilds_connect",{"micros":139,"calls":[]},"presence_connect",{"micros":1,"calls":[]}]}]}] (Session ID: 03fcb2e35ce477c42ae58e20259b5d68).
|
||||||
|
[2019-09-08 22:58:53] [INFO ] discord.state: Processed a chunk for 463 members in guild ID 280805240977227776.
|
||||||
|
[2019-09-08 22:58:54] [INFO ] discord.state: Processed a chunk for 807 members in guild ID 331981755177238530.
|
||||||
|
[2019-09-08 22:58:55] [INFO ] discord.state: Processed a chunk for 1000 members in guild ID 296698073177128962.
|
||||||
|
[2019-09-08 22:58:55] [INFO ] discord.state: Processed a chunk for 1000 members in guild ID 296698073177128962.
|
||||||
|
[2019-09-08 22:58:55] [INFO ] discord.state: Processed a chunk for 662 members in guild ID 296698073177128962.
|
||||||
|
[2019-09-08 23:03:12] [INFO ] discord.client: Cleaning up tasks.
|
||||||
|
[2019-09-08 23:03:12] [INFO ] discord.client: Cleaning up after 5 tasks.
|
||||||
|
[2019-09-08 23:03:12] [INFO ] discord.client: All tasks finished cancelling.
|
||||||
|
[2019-09-08 23:03:12] [INFO ] discord.client: Closing the event loop.
|
|
@ -1,4 +1,4 @@
|
||||||
discord.py
|
discord.py[voice]
|
||||||
lxml
|
lxml
|
||||||
click
|
click
|
||||||
asyncpg>=0.12.0
|
asyncpg>=0.12.0
|
Loading…
Reference in a new issue