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
149
bot.py
149
bot.py
|
@ -1,13 +1,5 @@
|
|||
#!/usr/bin/env python
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
__author__ = "Maël / Outout | Romain"
|
||||
__licence__ = "WTFPL Licence 2.0"
|
||||
|
||||
|
||||
import copy
|
||||
import datetime
|
||||
import os
|
||||
import logging
|
||||
import sys
|
||||
import traceback
|
||||
|
||||
|
@ -15,122 +7,95 @@ import aiohttp
|
|||
import discord
|
||||
from discord.ext import commands
|
||||
|
||||
import cogs.utils.cli_colors as colors
|
||||
import config
|
||||
from cogs.utils import checks
|
||||
from cogs.utils.lang import _
|
||||
|
||||
if sys.version_info[1] < 7 or sys.version_info[0] < 3:
|
||||
print(f"{colors.text_colors.RED}[ERROR] Python 3.7 or + is required.{colors.ENDC}")
|
||||
exit()
|
||||
description = """
|
||||
Je suis TuxBot, le bot qui vit de l'OpenSource ! ;)
|
||||
"""
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
l_extensions = (
|
||||
'cogs.admin',
|
||||
'cogs.afk',
|
||||
'cogs.atc',
|
||||
'cogs.basics',
|
||||
'cogs.ci',
|
||||
'cogs.filter_messages',
|
||||
'cogs.funs',
|
||||
'cogs.role',
|
||||
'cogs.search',
|
||||
'cogs.send_logs',
|
||||
'cogs.sondage',
|
||||
'cogs.utility',
|
||||
'cogs.vocal',
|
||||
'cogs.private',
|
||||
'cogs.basaics',
|
||||
)
|
||||
|
||||
help_attrs = dict(hidden=True, in_help=True, name="DONOTUSE")
|
||||
|
||||
async def _prefix_callable(bot, message):
|
||||
base = [] if config.prefix is None else config.prefix
|
||||
|
||||
# if message.guild is not None:
|
||||
# base.extend(bot.prefixes.get(message.guild.id))
|
||||
return commands.when_mentioned_or(base)
|
||||
|
||||
|
||||
class TuxBot(commands.Bot):
|
||||
def __init__(self):
|
||||
class TuxBot(commands.AutoShardedBot):
|
||||
__slots__ = ('uptime', 'config', 'session')
|
||||
|
||||
def __init__(self, unload):
|
||||
super().__init__(command_prefix=_prefix_callable,
|
||||
description=description, pm_help=None,
|
||||
help_command=None, help_attrs=dict(hidden=True))
|
||||
|
||||
self.uptime = datetime.datetime.utcnow()
|
||||
self.config = config
|
||||
super().__init__(command_prefix=self.config.prefix[0],
|
||||
description=self.config.description,
|
||||
pm_help=None,
|
||||
help_command=None)
|
||||
|
||||
self.client_id = self.config.client_id
|
||||
self.prefixes = {}
|
||||
self.session = aiohttp.ClientSession(loop=self.loop)
|
||||
self._events = []
|
||||
|
||||
self.add_command(self.do)
|
||||
|
||||
for extension in l_extensions:
|
||||
try:
|
||||
self.load_extension(extension)
|
||||
print(f"{colors.text_colors.GREEN}\"{extension}\""
|
||||
f" chargé !{colors.ENDC}")
|
||||
except Exception as e:
|
||||
print(f"{colors.text_colors.RED}"
|
||||
f"Impossible de charger l'extension {extension}\n"
|
||||
f"{type(e).__name__}: {e}{colors.ENDC}", file=sys.stderr)
|
||||
if extension not in unload:
|
||||
try:
|
||||
self.load_extension(extension)
|
||||
except Exception as e:
|
||||
print(_("Failed to load extension : ") + extension,
|
||||
file=sys.stderr)
|
||||
traceback.print_exc()
|
||||
|
||||
async def on_command_error(self, ctx, error):
|
||||
if isinstance(error, commands.NoPrivateMessage):
|
||||
await ctx.author.send('Cette commande ne peut pas être utilisee '
|
||||
'en message privee.')
|
||||
await ctx.author.send(
|
||||
_('This command cannot be used in private messages.')
|
||||
)
|
||||
elif isinstance(error, commands.DisabledCommand):
|
||||
await ctx.author.send('Desoler mais cette commande est desactive, '
|
||||
'elle ne peut donc pas être utilisée.')
|
||||
await ctx.author.send(
|
||||
_('Sorry. This command is disabled and cannot be used.')
|
||||
)
|
||||
elif isinstance(error, commands.CommandInvokeError):
|
||||
print(f'In {ctx.command.qualified_name}:', file=sys.stderr)
|
||||
print(_('In ') + f'{ctx.command.qualified_name}:', file=sys.stderr)
|
||||
traceback.print_tb(error.original.__traceback__)
|
||||
print(f'{error.original.__class__.__name__}: {error.original}',
|
||||
file=sys.stderr)
|
||||
elif isinstance(error, commands.ArgumentParsingError):
|
||||
await ctx.send(error)
|
||||
|
||||
async def on_ready(self):
|
||||
log_channel_id = await self.fetch_channel(self.config.log_channel_id)
|
||||
if not hasattr(self, 'uptime'):
|
||||
self.uptime = datetime.datetime.utcnow()
|
||||
|
||||
print('\n\n---------------------')
|
||||
print('CONNECTÉ :')
|
||||
print(f'Nom d\'utilisateur: {self.user} {colors.text_style.DIM}'
|
||||
f'(ID: {self.user.id}){colors.ENDC}')
|
||||
print(f'Channel de log: {log_channel_id} {colors.text_style.DIM}'
|
||||
f'(ID: {log_channel_id.id}){colors.ENDC}')
|
||||
print(f'Prefix: {self.config.prefix[0]}')
|
||||
print('Merci d\'utiliser TuxBot')
|
||||
print('---------------------\n\n')
|
||||
print(_('Ready:') + f' {self.user} (ID: {self.user.id})')
|
||||
|
||||
await self.change_presence(status=discord.Status.dnd,
|
||||
activity=discord.Game(
|
||||
name=self.config.game)
|
||||
)
|
||||
name=self.config.activity
|
||||
))
|
||||
|
||||
@staticmethod
|
||||
async def on_resumed():
|
||||
print('resumed...')
|
||||
|
||||
async def on_message(self, message):
|
||||
if message.author.bot:
|
||||
return
|
||||
@property
|
||||
def logs_webhook(self):
|
||||
logs_webhook = self.config.logs_webhook
|
||||
webhook = discord.Webhook.partial(id=logs_webhook.get('id'),
|
||||
token=logs_webhook.get('token'),
|
||||
adapter=discord.AsyncWebhookAdapter(
|
||||
self.session))
|
||||
return webhook
|
||||
|
||||
try:
|
||||
await self.process_commands(message)
|
||||
except Exception as e:
|
||||
print(f'{colors.text_colors.RED}Erreur rencontré : \n'
|
||||
f' {type(e).__name__}: {e}{colors.ENDC} \n \n')
|
||||
async def close(self):
|
||||
await super().close()
|
||||
await self.session.close()
|
||||
|
||||
def run(self):
|
||||
super().run(self.config.token, reconnect=True)
|
||||
|
||||
@checks.has_permissions(administrator=True)
|
||||
@commands.command(pass_context=True, hidden=True)
|
||||
async def do(self, ctx, times: int, *, command):
|
||||
"""Repeats a command a specified number of times."""
|
||||
msg = copy.copy(ctx.message)
|
||||
msg.content = command
|
||||
for i in range(times):
|
||||
await self.process_commands(msg)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
if os.path.exists('config.py') is not True:
|
||||
print(f"{colors.text_colors.RED}"
|
||||
f"Veuillez créer le fichier config.py{colors.ENDC}")
|
||||
exit()
|
||||
|
||||
tuxbot = TuxBot()
|
||||
tuxbot.run()
|
||||
super().run(config.token, reconnect=True)
|
||||
|
|
0
cogs/utils/checks.py
Executable file → Normal file
0
cogs/utils/checks.py
Executable file → Normal file
1104
cogs/utils/db.py
1104
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.install()
|
||||
|
||||
setup.ask()
|
||||
setup.save()
|
||||
|
||||
setup.clean()
|
|
@ -1,3 +1,6 @@
|
|||
from pip._internal import main as pip
|
||||
import shutil
|
||||
|
||||
from .langs import locales, texts
|
||||
|
||||
|
||||
|
@ -10,6 +13,9 @@ class Config:
|
|||
'unkickable_id': '[unkickable ids here (in int)]'
|
||||
}
|
||||
|
||||
with open('requirements.txt', 'r') as f:
|
||||
self.packages = f.read().split('\n')
|
||||
|
||||
def input(self, key, **kwargs):
|
||||
lang = self.config.get('lang', 'multiple')
|
||||
|
||||
|
@ -32,13 +38,30 @@ class Config:
|
|||
|
||||
self.config[key] = response
|
||||
|
||||
def ask(self):
|
||||
def install(self):
|
||||
self.input('lang', valid=locales)
|
||||
print('\n\n\033[4;36m'
|
||||
+ texts.get(self.config.get('lang')).get('install')
|
||||
+ '\033[0m\n')
|
||||
|
||||
for package in self.packages:
|
||||
pip(['install', package])
|
||||
|
||||
def ask(self):
|
||||
print('\n\n\033[4;36m' + texts.get(self.config.get('lang')).get('conf')
|
||||
+ '\033[0m\n')
|
||||
|
||||
self.input('token', empty=False)
|
||||
self.input('postgresql_username', empty=False)
|
||||
self.input('postgresql_password', empty=False)
|
||||
self.input('postgresql_dbname', empty=False)
|
||||
|
||||
print('\n\n\033[4;36m' + texts.get(self.config.get('lang')).get('logs')
|
||||
+ '\033[0m\n')
|
||||
|
||||
self.input('wh_id', empty=True)
|
||||
self.input('wh_token', empty=True)
|
||||
|
||||
print('\n\n\033[4;36m' + texts.get(self.config.get('lang')).get('misc')
|
||||
+ '\033[0m\n')
|
||||
|
||||
|
@ -49,13 +72,24 @@ class Config:
|
|||
with open('config.py', 'w') as file:
|
||||
postgresql = f"postgresql://" \
|
||||
f"{self.config.get('postgresql_username')}:" \
|
||||
f"{self.config.get('postgresql_password')}@host/" \
|
||||
f"{self.config.get('postgresql_dbname')}"
|
||||
f"{self.config.get('postgresql_password')}" \
|
||||
f"@localhost/{self.config.get('postgresql_dbname')}"
|
||||
file.write(f"postgresql = '{postgresql}'\n")
|
||||
|
||||
logs_webhook = dict(id=int(self.config.get('wh_id')),
|
||||
token=self.config.get('wh_token'))
|
||||
file.write(f"logs_webhook = '{logs_webhook}'\n")
|
||||
|
||||
for key, value in self.config.items():
|
||||
if not key.startswith('postgresql_'):
|
||||
if not key.startswith('postgresql_') \
|
||||
and not key.startswith('wh_'):
|
||||
value = f"'{value}'" if type(value) is str else value
|
||||
file.write(f"{key} = {value}\n")
|
||||
print('\n\n\033[4;36m' + texts.get(self.config.get('lang')).get('end')
|
||||
+ '\033[0m\n')
|
||||
|
||||
def clean(self):
|
||||
print('\n\n\033[4;36m'
|
||||
+ texts.get(self.config.get('lang')).get('clean')
|
||||
+ '\033[0m\n')
|
||||
shutil.rmtree('first_run')
|
|
@ -1,6 +1,10 @@
|
|||
locales = ['fr', 'en']
|
||||
texts = {
|
||||
'fr': {
|
||||
'install': "Installation des modules...",
|
||||
|
||||
'conf': "Configuration...",
|
||||
|
||||
'token': "Veuillez entrer le token",
|
||||
'not_empty': "Cette valeur ne doit pas être vide",
|
||||
|
||||
|
@ -8,15 +12,26 @@ texts = {
|
|||
'postgresql_password': "Veuillez entrer le mot de passe de postgresql",
|
||||
'postgresql_dbname': "Veuillez entrer le nom de la base de donnée",
|
||||
|
||||
'logs': "Channel de logs (non obligatoire)",
|
||||
|
||||
'wh_id': "L'id du webhook pour le channel de logs",
|
||||
'wh_token': "Le token du webhook pour le channel de logs",
|
||||
|
||||
'misc': 'Autre',
|
||||
|
||||
'activity': "Joue à ...",
|
||||
'prefix': "Prefixe (par defaut : @tuxbot)",
|
||||
|
||||
"end": "Configuration terminée, vous pouvez à tout moment la rectifier en modifiant le fichier config.py"
|
||||
'end': "Configuration terminée, vous pouvez à tout moment la rectifier en modifiant le fichier config.py",
|
||||
|
||||
'clean': "Nettoyage..."
|
||||
},
|
||||
|
||||
'en': {
|
||||
'install': "Installation of the modules....",
|
||||
|
||||
'conf': "Configuration...",
|
||||
|
||||
'token': "Please enter the token",
|
||||
'not_empty': "This value must not be empty",
|
||||
|
||||
|
@ -24,12 +39,19 @@ texts = {
|
|||
'postgresql_password': "Please enter the postgresql password",
|
||||
'postgresql_dbname': "Please enter the database name",
|
||||
|
||||
'logs': "Log channel (not required)",
|
||||
|
||||
'wh_id': "Webhook id for log channel",
|
||||
'wh_token': "Webhook token for log channel",
|
||||
|
||||
'misc': 'Misc',
|
||||
|
||||
'activity': "Playing ...",
|
||||
'prefix': "Prefix (default is @tuxbot)",
|
||||
|
||||
"end": "Configuration completed, you can fix it at any time by modifying the config.py file"
|
||||
'end': "Configuration completed, you can fix it at any time by modifying the config.py file",
|
||||
|
||||
'clean': "Cleaning..."
|
||||
},
|
||||
|
||||
'multiple': {
|
||||
|
|
42
launcher.py
42
launcher.py
|
@ -1,9 +1,17 @@
|
|||
import logging
|
||||
import asyncio
|
||||
import contextlib
|
||||
import logging
|
||||
import socket
|
||||
import sys
|
||||
|
||||
import click
|
||||
|
||||
from bot import TuxBot
|
||||
from cogs.utils.db import Table
|
||||
|
||||
try:
|
||||
import config
|
||||
from cogs.utils.lang import _
|
||||
except ModuleNotFoundError:
|
||||
import first_run
|
||||
|
||||
|
@ -34,10 +42,34 @@ def setup_logging():
|
|||
log.removeHandler(hdlr)
|
||||
|
||||
|
||||
def run_bot():
|
||||
pass # Todo: initialize bot, postgresql,...
|
||||
def run_bot(unload):
|
||||
loop = asyncio.get_event_loop()
|
||||
log = logging.getLogger()
|
||||
|
||||
try:
|
||||
pool = loop.run_until_complete(
|
||||
Table.create_pool(config.postgresql, command_timeout=60)
|
||||
)
|
||||
except socket.gaierror as e:
|
||||
click.echo(_('Could not set up PostgreSQL...'), file=sys.stderr)
|
||||
log.exception(_('Could not set up PostgreSQL...'))
|
||||
return
|
||||
|
||||
bot = TuxBot(unload)
|
||||
bot.pool = pool
|
||||
bot.run()
|
||||
|
||||
|
||||
@click.group(invoke_without_command=True, options_metavar='[options]')
|
||||
@click.option('-u', '--unload',
|
||||
multiple=True, type=str,
|
||||
help=_('Launch without loading the <TEXT> module'))
|
||||
@click.pass_context
|
||||
def main(ctx, unload):
|
||||
if ctx.invoked_subcommand is None:
|
||||
with setup_logging():
|
||||
run_bot(unload)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
with setup_logging():
|
||||
run_bot()
|
||||
main()
|
||||
|
|
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
|
||||
click
|
||||
asyncpg>=0.12.0
|
Loading…
Reference in a new issue