fix(security): fix high security breach

This commit is contained in:
Romain J 2019-06-27 23:40:25 +02:00
parent 5aadb8e057
commit ff32a7721c
52 changed files with 235 additions and 914 deletions

7
.gitignore vendored
View file

@ -1,9 +1,10 @@
#Don't track user data
data/users/
#Python
__pycache__/
*.pyc
.env
config.py
.DS_Store
private.py
#jetbrains
.idea/

17
bot.py
View file

@ -24,10 +24,8 @@ l_extensions = (
'cogs.atc',
'cogs.basics',
'cogs.ci',
'cogs.cog_manager',
'cogs.filter_messages',
'cogs.funs',
'cogs.passport',
'cogs.role',
'cogs.search',
'cogs.send_logs',
@ -46,8 +44,7 @@ class TuxBot(commands.Bot):
super().__init__(command_prefix=self.config.prefix[0],
description=self.config.description,
pm_help=None,
help_command = None
)
help_command=None)
self.client_id = self.config.client_id
self.session = aiohttp.ClientSession(loop=self.loop)
@ -67,10 +64,10 @@ class TuxBot(commands.Bot):
async def on_command_error(self, ctx, error):
if isinstance(error, commands.NoPrivateMessage):
await ctx.author.send('Cette commande ne peut pas être utilisée '
'en message privé.')
await ctx.author.send('Cette commande ne peut pas être utilisee '
'en message privee.')
elif isinstance(error, commands.DisabledCommand):
await ctx.author.send('Desolé mais cette commande est desactivée, '
await ctx.author.send('Desoler mais cette commande est desactive, '
'elle ne peut donc pas être utilisée.')
elif isinstance(error, commands.CommandInvokeError):
print(f'In {ctx.command.qualified_name}:', file=sys.stderr)
@ -79,13 +76,13 @@ class TuxBot(commands.Bot):
file=sys.stderr)
async def on_ready(self):
log_channel_id = self.get_channel(int(self.config.log_channel_id))
log_channel_id = await self.fetch_channel(self.config.log_channel_id)
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'Salon de journalisation: {log_channel_id} {colors.text_style.DIM}'
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')
@ -93,7 +90,7 @@ class TuxBot(commands.Bot):
await self.change_presence(status=discord.Status.dnd,
activity=discord.Game(
name=self.config.game),
name=self.config.game)
)
@staticmethod

View file

@ -1,16 +1,10 @@
import aiohttp
from discord.ext import commands
import discord
import asyncio
from .utils import checks
from .utils.checks import get_user
import traceback
import textwrap
from contextlib import redirect_stdout
import inspect
import io
class Admin(commands.Cog):
"""Commandes secrètes d'administration."""
@ -20,20 +14,37 @@ class Admin(commands.Cog):
self._last_result = None
self.sessions = set()
@staticmethod
def cleanup_code(content):
if content.startswith('```') and content.endswith('```'):
return '\n'.join(content.split('\n')[1:-1])
return content.strip('` \n')
"""---------------------------------------------------------------------"""
@checks.has_permissions(administrator=True)
@commands.command(pass_context=True)
async def ban(self, ctx, user, *, reason=""):
@commands.command(name="upload", pass_context=True)
async def _upload(self, ctx, *, url=""):
if len(ctx.message.attachments) >= 1:
file = ctx.message.attachments[0].url
elif url != "":
file = url
else:
em = discord.Embed(title='Une erreur est survenue',
description="Fichier introuvable.",
colour=0xDC3546)
await ctx.send(embed=em)
return
async with aiohttp.ClientSession() as session:
async with session.get(file) as r:
image = await r.content.read()
with open(f"data/tmp/{str(ctx.author.id)}.png", 'wb') as f:
f.write(image)
f.close()
await ctx.send(file=discord.File(f"data/tmp/{str(ctx.author.id)}.png"))
@checks.has_permissions(administrator=True)
@commands.command(name="ban", pass_context=True)
async def _ban(self, ctx, user, *, reason=""):
"""Ban user"""
user = get_user(ctx.message, user)
if user:
if user and str(user.id) not in self.bot.config.unkickable_id:
try:
await user.ban(reason=reason)
return_msg = f"`{user.mention}` a été banni\n"
@ -50,11 +61,11 @@ class Admin(commands.Cog):
"""---------------------------------------------------------------------"""
@checks.has_permissions(administrator=True)
@commands.command(pass_context=True)
async def kick(self, ctx, user, *, reason=""):
@commands.command(name="kick", pass_context=True)
async def _kick(self, ctx, user, *, reason=""):
"""Kick a user"""
user = get_user(ctx.message, user)
if user:
if user and str(user.id) not in self.bot.config.unkickable_id:
try:
await user.kick(reason=reason)
return_msg = f"`{user.mention}` a été kické\n"
@ -155,7 +166,7 @@ class Admin(commands.Cog):
@checks.has_permissions(administrator=True)
@commands.command(name='editsay', pass_context=True)
async def _editsay(self, ctx, id: int, *, new_content: str):
async def _editsay(self, ctx, message_id: int, *, new_content: str):
"""Edit a bot's message"""
try:
try:
@ -163,10 +174,10 @@ class Admin(commands.Cog):
except Exception:
print(f"Impossible de supprimer le message "
f"\"{str(ctx.message.content)}\"")
toedit = await ctx.channel.get_message(id)
toedit = await ctx.channel.get_message(message_id)
except discord.errors.NotFound:
await ctx.send(f"Impossible de trouver le message avec l'id "
f"`{id}` sur ce salon")
f"`{message_id}` sur ce salon")
return
try:
await toedit.edit(content=str(new_content))
@ -177,7 +188,7 @@ class Admin(commands.Cog):
@checks.has_permissions(administrator=True)
@commands.command(name='addreaction', pass_context=True)
async def _addreaction(self, ctx, id: int, reaction: str):
async def _addreaction(self, ctx, message_id: int, reaction: str):
"""Add reactions to a message"""
try:
try:
@ -185,10 +196,10 @@ class Admin(commands.Cog):
except Exception:
print(f"Impossible de supprimer le message "
f"\"{str(ctx.message.content)}\"")
toadd = await ctx.channel.get_message(id)
toadd = await ctx.channel.get_message(message_id)
except discord.errors.NotFound:
await ctx.send(f"Impossible de trouver le message avec l'id "
f"`{id}` sur ce salon")
f"`{message_id}` sur ce salon")
return
try:
await toadd.add_reaction(reaction)
@ -199,7 +210,7 @@ class Admin(commands.Cog):
@checks.has_permissions(administrator=True)
@commands.command(name='delete', pass_context=True)
async def _delete(self, ctx, id: int):
async def _delete(self, ctx, message_id: int):
"""Delete message in current channel"""
try:
try:
@ -207,10 +218,10 @@ class Admin(commands.Cog):
except Exception:
print(f"Impossible de supprimer le message "
f"\"{str(ctx.message.content)}\"")
todelete = await ctx.channel.get_message(id)
todelete = await ctx.channel.get_message(message_id)
except discord.errors.NotFound:
await ctx.send(f"Impossible de trouver le message avec l'id "
f"`{id}` sur ce salon")
f"`{message_id}` sur ce salon")
return
try:
await todelete.delete()
@ -221,7 +232,7 @@ class Admin(commands.Cog):
@checks.has_permissions(administrator=True)
@commands.command(name='deletefrom', pass_context=True)
async def _deletefrom(self, ctx, chan_id: int, *, message_id: str):
async def _deletefrom(self, ctx, chan_id: int, *, message_id: int):
"""Delete message in <chan_id> channel"""
try:
chan = self.bot.get_channel(chan_id)
@ -391,151 +402,6 @@ class Admin(commands.Cog):
await ctx.send(embed=embed)
"""---------------------------------------------------------------------"""
@checks.has_permissions(administrator=True)
@commands.command(pass_context=True, hidden=True)
async def repl(self, ctx):
"""Launches an interactive REPL session."""
variables = {
'ctx': ctx,
'bot': self.bot,
'message': ctx.message,
'guild': ctx.guild,
'channel': ctx.channel,
'author': ctx.author,
'_': None,
}
if ctx.channel.id in self.sessions:
await ctx.send('Already running a REPL session in this channel.'
' Exit it with `quit`.')
return
self.sessions.add(ctx.channel.id)
await ctx.send(
'Enter code to execute or evaluate. `exit()` or `quit` to exit.')
def check(m):
return m.author.id == ctx.author.id and \
m.channel.id == ctx.channel.id and \
m.content.startswith('`')
while True:
try:
response = await self.bot.wait_for('message', check=check,
timeout=10.0 * 60.0)
except asyncio.TimeoutError:
await ctx.send('Exiting REPL session.')
self.sessions.remove(ctx.channel.id)
break
cleaned = self.cleanup_code(response.content)
if cleaned in ('quit', 'exit', 'exit()'):
await ctx.send('Exiting.')
self.sessions.remove(ctx.channel.id)
return
executor = exec
if cleaned.count('\n') == 0:
# single statement, potentially 'eval'
try:
code = compile(cleaned, '<repl session>', 'eval')
except SyntaxError:
pass
else:
executor = eval
if executor is exec:
try:
code = compile(cleaned, '<repl session>', 'exec')
except SyntaxError as e:
await ctx.send(self.get_syntax_error(e))
continue
variables['message'] = response
fmt = None
stdout = io.StringIO()
try:
with redirect_stdout(stdout):
result = executor(code, variables)
if inspect.isawaitable(result):
result = await result
except Exception as e:
value = stdout.getvalue()
fmt = f'```py\n{value}{traceback.format_exc()}\n```'
else:
value = stdout.getvalue()
if result is not None:
fmt = f'```py\n{value}{result}\n```'
variables['_'] = result
elif value:
fmt = f'```py\n{value}\n```'
try:
if fmt is not None:
if len(fmt) > 2000:
await ctx.send('Content too big to be printed.')
else:
await ctx.send(fmt)
except discord.Forbidden:
pass
except discord.HTTPException as e:
await ctx.send(f'Unexpected error: `{e}`')
"""---------------------------------------------------------------------"""
@checks.has_permissions(administrator=True)
@commands.command(pass_context=True, hidden=True, name='eval')
async def _eval(self, ctx, *, body: str):
"""Evaluates a code"""
env = {
'bot': self.bot,
'ctx': ctx,
'channel': ctx.channel,
'author': ctx.author,
'guild': ctx.guild,
'message': ctx.message,
'_': self._last_result
}
env.update(globals())
body = self.cleanup_code(body)
stdout = io.StringIO()
to_compile = f'async def func():\n{textwrap.indent(body, " ")}'
try:
exec(to_compile, env)
except Exception as e:
return await ctx.send(f'```py\n{e.__class__.__name__}: {e}\n```')
func = env['func']
try:
with redirect_stdout(stdout):
ret = await func()
except Exception:
value = stdout.getvalue()
await ctx.send(f'```py\n{value}{traceback.format_exc()}\n```')
else:
value = stdout.getvalue()
try:
await ctx.message.add_reaction('\u2705')
except Exception:
pass
if ret is None:
if value:
await ctx.send(f'```py\n{value}\n```')
else:
self._last_result = ret
await ctx.send(f'```py\n{value}{ret}\n```')
def setup(bot):
bot.add_cog(Admin(bot))

View file

@ -61,10 +61,10 @@ class Basics(commands.Cog):
async def help(self, ctx):
"""Affiches l'aide du bot"""
text = open('texts/help.md').read()
text = text.split("[split]")
for text_result in text:
em = discord.Embed(title='Commandes de TuxBot', description=text_result,colour=0x89C4F9)
await ctx.send(embed=em)
em = discord.Embed(title='Commandes de TuxBot', description=text,
colour=0x89C4F9)
await ctx.send(embed=em)
def setup(bot):
bot.add_cog(Basics(bot))

View file

@ -1,116 +0,0 @@
from discord.ext import commands
import discord
from .utils import checks
from .utils.paginator import HelpPaginator
class CogManager(commands.Cog):
"""Gestionnaire des cogs"""
def __init__(self, bot):
self.bot = bot
self.help = discord.Embed(title='Tuxbot - Commandes cogs', description="Tuxbot - Commandes cogs\n"
"-> .cogs <load/unload/reload/info> *{cog}* : <load/unload/reload/info> *{cog}*\n"
"-> .cogs <list> : donne la liste de tous les cogs\n"
"-> .cogs <null/!(load/unload/reload)>: affiche cette aide", colour=0x89C4F9)
"""---------------------------------------------------------------------"""
@checks.has_permissions(administrator=True)
@commands.group(name="cogs", no_pm=True, pass_context=True,
case_insensitive=True)
async def _cogs(self, ctx):
"""show help about 'cogs' command"""
if ctx.invoked_subcommand is None:
await ctx.send(embed=self.help)
"""---------------------------------------------------------------------"""
@_cogs.command(name="load", pass_context=True)
async def cogs_load(self, ctx, cog: str = ""):
"""load a cog"""
if cog != "":
try:
self.bot.load_extension(cog)
await ctx.send('\N{OK HAND SIGN}')
print("cog : " + str(cog) + " chargé")
except Exception as e:
await ctx.send('\N{PISTOL}')
await ctx.send(f'{type(e).__name__}: {e}')
else:
await ctx.send(embed=self.help)
"""---------------------------------------------------------------------"""
@_cogs.command(name="unload", pass_context=True)
async def cogs_unload(self, ctx, cog: str = ""):
"""unload a cog"""
if cog != "":
try:
self.bot.unload_extension(cog)
await ctx.send('\N{OK HAND SIGN}')
print("cog : " + str(cog) + " déchargé")
except Exception as e:
await ctx.send('\N{PISTOL}')
await ctx.send(f'{type(e).__name__}: {e}')
else:
await ctx.send(embed=self.help)
"""---------------------------------------------------------------------"""
@_cogs.command(name="reload", pass_context=True)
async def cogs_reload(self, ctx, cog: str = ""):
"""reload a cog"""
if cog != "":
try:
self.bot.unload_extension(cog)
self.bot.load_extension(cog)
await ctx.send('\N{OK HAND SIGN}')
print("cog : " + str(cog) + " rechargé")
except Exception as e:
await ctx.send('\N{PISTOL}')
await ctx.send(f'{type(e).__name__}: {e}')
else:
await ctx.send(embed=self.help)
"""---------------------------------------------------------------------"""
@_cogs.command(name="info", pass_context=True)
async def cogs_info(self, ctx, cog: str = ""):
"""show info about a cog"""
if cog != "":
try:
entity = self.bot.get_cog(cog)
if entity is None:
clean = cog.replace('@', '@\u200b')
await ctx.send(f'Command or category "{clean}" not found.')
else:
p = await HelpPaginator.from_cog(ctx, entity)
await p.paginate()
except Exception as e:
await ctx.send('\N{PISTOL}')
await ctx.send(f'{type(e).__name__}: {e}')
else:
await ctx.send(embed=self.help)
"""---------------------------------------------------------------------"""
@_cogs.command(name="list", pass_context=True)
async def cogs_list(self, ctx):
"""list all cogs"""
cogs = ""
for cog in self.bot.cogs:
cogs += f"{cog} *`({self.bot.cogs[cog].__module__})`*:" \
f" {len(self.bot.get_cog_commands(cog))} commandes\n"
await ctx.send(cogs)
def setup(bot):
bot.add_cog(CogManager(bot))

View file

@ -1,224 +0,0 @@
import aiohttp
import datetime
import discord
import imghdr
import os
import shutil
from PIL import Image
from PIL import ImageOps
from discord.ext import commands
from .utils import db
from .utils.checks import get_user
from .utils.passport_generator import generate_passport
class Passport(commands.Cog):
"""Commandes des passeports ."""
def __init__(self, bot):
self.bot = bot
self.conn = db.connect_to_db(self)
self.cursor = self.conn.cursor()
self.cursor.execute("""SHOW TABLES LIKE 'passport'""")
result = self.cursor.fetchone()
if not result:
# Creation table Passport si premiere fois
sql = "CREATE TABLE passport ( `id` INT NOT NULL AUTO_INCREMENT PRIMARY KEY, userid TEXT null, os TEXT null, config TEXT null, languages TEXT null, pays TEXT null, passportdate TEXT null, theme CHAR(5) DEFAULT 'dark');"
self.cursor.execute(sql)
@commands.group(pass_context=True)
async def passeport(self, ctx):
"""Passeport"""
if ctx.invoked_subcommand is None:
text = open('texts/passport-info.md').read()
em = discord.Embed(title='Commandes de carte de passeport de TuxBot', description=text, colour=0x89C4F9)
await ctx.send(embed=em)
@passeport.command(pass_context=True)
async def show(self, ctx, user: str = None):
self.conn = db.connect_to_db(self)
self.cursor = self.conn.cursor()
if user == None:
user = get_user(ctx.message, ctx.message.author.name)
else:
user = get_user(ctx.message, user)
wait_message = await ctx.send(f"Je vais chercher le passeport de {user.name} dans les archives, je vous prie de bien vouloir patienter...")
card = await generate_passport(self, user)
s = 'data/users/cards/{0}.png'.format(user.id)
card.save(s, 'png')
with open('data/users/cards/{0}.png'.format(user.id), 'rb') as g:
await ctx.message.channel.send(file=discord.File(g))
await wait_message.delete()
@passeport.command(name="config", pass_context=True)
async def passeport_config(self, ctx):
self.conn = db.connect_to_db(self)
self.cursor = self.conn.cursor()
await ctx.send('Un message privé vous a été envoyé pour configurer votre passeport.')
questions = ["Système(s) d'exploitation :", "Configuration Système :", "Langages de programmation préférés :", "Pays :"]
answers = {}
user_dm = await ctx.author.create_dm()
try:
await user_dm.send("Salut ! Je vais vous posez quelques questions afin de configurer votre passeport, si vous ne voulez pas répondre à une question, envoyez `skip` pour passer à la question suivante.")
except discord.HTTPException:
await ctx.send(f"{str(ctx.message.author.mention)}> il m'est impossible de vous envoyer les messages nécessaire a la configuration de votre passeport :sob:")
return
for x, question in enumerate(questions):
await user_dm.send(question)
def check(m):
return m.channel.id == user_dm.id and m.author.id == user_dm.recipient.id
answer = await self.bot.wait_for('message', check=check)
if answer.content.lower() == 'skip':
answers[x] = 'n/a'
else:
answers[x] = answer.content
try:
self.cursor.execute("""SELECT id, userid FROM passport WHERE userid = %s""", str(user_dm.recipient.id))
result = self.cursor.fetchone()
if result:
self.cursor.execute("""UPDATE passport SET os = %s, config = %s, languages = %s, pays = %s WHERE userid = %s""", (str(answers[0]), str(answers[1]), str(answers[2]), str(answers[3]), str(ctx.message.author.id)))
self.conn.commit()
else:
now = datetime.datetime.now()
self.cursor.execute("""INSERT INTO passport(userid, os, config, languages, pays, passportdate, theme) VALUES(%s, %s, %s, %s, %s, %s, %s)""", (str(ctx.message.author.id), str(answers[0]), str(answers[1]), str(answers[2]), str(answers[3]), now, "dark"))
self.conn.commit()
await user_dm.send('Configuration de votre passeport terminée avec succès, vous pouvez désormais la voir en faisant `.passeport show`.')
except Exception as e:
await user_dm.send(f':sob: Une erreur est survenue : \n {type(e).__name__}: {e}')
@passeport.command(name="background", pass_context=True)
async def passeport_background(self, ctx, *, url=""):
try:
background = ctx.message.attachments[0].url
except:
if url != "":
background = url
else:
em = discord.Embed(title='Une erreur est survenue', description="Image ou URL introuvable.", colour=0xDC3546)
await ctx.send(embed=em)
return
user = ctx.message.author
try:
async with aiohttp.ClientSession() as session:
async with session.get(background) as r:
image = await r.content.read()
except:
em = discord.Embed(title='Une erreur est survenue', description="Image ou URL introuvable.", colour=0xDC3546)
await ctx.send(embed=em)
return
with open(f"data/users/backgrounds/{str(user.id)}.png",'wb') as f:
f.write(image)
isImage = imghdr.what(f"data/users/backgrounds/{str(user.id)}.png")
if isImage == 'png' or isImage == 'jpeg' or isImage == 'jpg' or isImage == 'gif':
f.close()
em = discord.Embed(title='Configuration terminée', description="Fond d'écran enregistré et configuré avec succes", colour=0x28a745)
await ctx.send(embed=em)
else:
f.close()
os.remove(f"data/users/backgrounds/{str(user.id)}.png")
em = discord.Embed(title='Une erreur est survenue', description="Est-ce bien une image que vous avez envoyé ? :thinking:", colour=0xDC3546)
await ctx.send(embed=em)
@passeport.command(name="theme", aliases=["thème"], pass_context=True)
async def passeport_theme(self, ctx, theme: str = ""):
self.conn = db.connect_to_db(self)
self.cursor = self.conn.cursor()
possible_theme = ["dark", "light", "preview"]
if theme.lower() in possible_theme:
if theme.lower() == "dark":
self.cursor.execute("""UPDATE passport SET theme = %s WHERE userid = %s""", ("dark", str(ctx.message.author.id)))
self.conn.commit()
em = discord.Embed(title='Configuration terminée', description="Thème enregistré avec succes", colour=0x28a745)
await ctx.send(embed=em)
elif theme.lower() == "light":
self.cursor.execute("""UPDATE passport SET theme = %s WHERE userid = %s""", ("light", str(ctx.message.author.id)))
self.conn.commit()
em = discord.Embed(title='Configuration terminée', description="Thème enregistré avec succes", colour=0x28a745)
await ctx.send(embed=em)
else:
wait_message = await ctx.send(f"Laissez moi juste le temps de superposer les 2 passeports, je vous prie de bien vouloir patienter...")
cardbg = Image.new('RGBA', (1600, 500), (0, 0, 0, 255))
card_dark = await generate_passport(self, ctx.author, "dark")
card_dark.save(f'data/tmp/{ctx.author.id}_dark.png', 'png')
card_light = await generate_passport(self, ctx.author, "light")
card_light.save(f'data/tmp/{ctx.author.id}_light.png', 'png')
saved_card_dark = Image.open(f'data/tmp/{ctx.author.id}_dark.png')
saved_card_light = Image.open(f'data/tmp/{ctx.author.id}_light.png')
saved_card_dark = ImageOps.fit(saved_card_dark, (800, 500))
saved_card_light = ImageOps.fit(saved_card_light, (800, 500))
cardbg.paste(saved_card_dark, (0, 0))
cardbg.paste(saved_card_light, (800, 0))
cardbg.save(f'data/tmp/{ctx.author.id}.png', 'png')
with open(f'data/tmp/{ctx.author.id}.png', 'rb') as g:
await ctx.send(file=discord.File(g))
await wait_message.delete()
await ctx.send(f"Et voila {ctx.author.mention} ! à gauche votre passeport avec le thème \"dark\" et à droite avec le thème \"light\" :wink:")
shutil.rmtree("data/tmp")
os.mkdir("data/tmp")
else:
em = discord.Embed(title='Une erreur est survenue', description="Les choix possible pour cette commande sont : `dark`, `light`, `preview`", colour=0xDC3546)
await ctx.send(embed=em)
@passeport.command(name="delete", pass_context=True)
async def passeport_delete(self, ctx):
self.conn = db.connect_to_db(self)
self.cursor = self.conn.cursor()
self.cursor.execute("""SELECT id, userid FROM passport WHERE userid = %s""", str(ctx.author.id))
result = self.cursor.fetchone()
if result:
def check(m):
return m.author.id == ctx.author.id and \
m.channel.id == ctx.channel.id
await ctx.send(f"{str(ctx.message.author.mention)}> envoyez `CONFIRMER` afin de supprimer vos données conformément à l'article 17 du `règlement général sur la protection des données` sur le `Droit à l'effacement`")
response = await self.bot.wait_for('message', check=check, timeout=10.0 * 60.0)
if response.content == "CONFIRMER":
os.remove(f"data/users/backgrounds/{str(ctx.author.id)}.png")
self.cursor.execute("""DELETE FROM passport WHERE userid =%s""", (str(ctx.message.author.id)))
self.conn.commit()
em = discord.Embed(title='Suppression confirmée', description="Vos données ont été supprimées avec succès", colour=0x28a745)
await ctx.send(embed=em)
else:
await ctx.send("Déja configure ton passeport avant de la supprimer u_u (après c'est pas logique...)")
def setup(bot):
bot.add_cog(Passport(bot))

View file

@ -23,7 +23,7 @@ class SendLogs(commands.Cog):
@commands.Cog.listener()
async def on_ready(self):
self.log_channel = self.bot.get_channel(int(self.bot.config.log_channel_id))
self.log_channel = await self.bot.fetch_channel(int(self.bot.config.log_channel_id))
em = discord.Embed(title="Je suis opérationnel 😃",
description=f"*Instance lancée sur "
f"{socket.gethostname()}*", colour=0x5cb85c)

View file

@ -15,76 +15,63 @@ class Sondage(commands.Cog):
if msg != "help":
await ctx.message.delete()
options = msg.split(" | ")
time = [x for x in options if x.startswith("time=")]
if time:
time = time[0]
if time:
options.remove(time)
times = [x for x in options if x.startswith("time=")]
if times:
time = int(times[0].strip("time="))
options.remove(times[0])
else:
time = 0
if len(options) <= 1:
raise commands.errors.MissingRequiredArgument
if len(options) >= 22:
return await ctx.send(f"{ctx.message.author.mention}> "
f":octagonal_sign: Vous ne pouvez pas "
f"mettre plus de 20 réponses !")
if time:
time = int(time.strip("time="))
else:
time = 0
emoji = ['1⃣',
'2⃣',
'3⃣',
'4⃣',
'5⃣',
'6⃣',
'7⃣',
'8⃣',
'9⃣',
'🔟',
'0⃣',
'🇦',
'🇧',
'🇨',
'🇩',
'🇪',
'🇫',
'🇬',
'🇭',
'🇮'
]
emoji = ['1⃣', '2⃣', '3⃣', '4⃣', '5⃣', '6⃣', '7⃣', '8⃣', '9⃣', '🔟', '0⃣',
'🇦', '🇧', '🇨', '🇩', '🇪', '🇫', '🇬', '🇭', '🇮']
to_react = []
confirmation_msg = f"**{options[0].rstrip('?')}?**:\n\n"
for idx, option in enumerate(options[1:]):
confirmation_msg += "{} - {}\n".format(emoji[idx], option)
confirmation_msg += f"{emoji[idx]} - {option}\n"
to_react.append(emoji[idx])
confirmation_msg += "*Sondage proposé par* " + \
str(ctx.message.author.mention)
if time == 0:
confirmation_msg += ""
else:
confirmation_msg += "\n\nVous avez {} secondes pour voter!". \
format(time)
confirmation_msg += f"\n\nVous avez {time} secondes pour voter!"
poll_msg = await ctx.send(confirmation_msg)
for emote in to_react:
await poll_msg.add_reaction(emote)
if time != 0:
await asyncio.sleep(time)
if time != 0:
async for message in ctx.message.channel.history():
if message.id == poll_msg.id:
poll_msg = message
results = {}
for reaction in poll_msg.reactions:
if reaction.emoji in to_react:
results[reaction.emoji] = reaction.count - 1
end_msg = "Le sondage est términé. Les résultats sont:\n\n"
for result in results:
end_msg += "{} {} - {} votes\n". \
format(result,
options[emoji.index(result)+1],
results[result])
top_result = max(results, key=lambda key: results[key])
if len([x for x in results
if results[x] == results[top_result]]) > 1:
top_results = []

View file

@ -3,7 +3,6 @@ import json
import pytz
import random
import urllib
import socket
import discord
import requests
@ -178,7 +177,6 @@ class Utility(commands.Cog):
em.set_thumbnail(url = img)
await ctx.send(embed=em)
"""---------------------------------------------------------------------"""
@commands.command()
@ -213,8 +211,7 @@ class Utility(commands.Cog):
"""---------------------------------------------------------------------"""
@commands.command(name='iplocalise', pass_context=True)
async def _iplocalise(self, ctx, ipaddress, iptype=""):
realipaddress = ipaddress
async def _iplocalise(self, ctx, ipaddress):
"""Recup headers."""
if ipaddress.startswith("http://"):
if ipaddress[-1:] == '/':
@ -225,39 +222,44 @@ class Utility(commands.Cog):
ipaddress = ipaddress[:-1]
ipaddress = ipaddress.split("https://")[1]
if(iptype=="ipv6" or iptype=="v6"):
try:
ipaddress = socket.getaddrinfo(ipaddress, None, socket.AF_INET6)[1][4][0]
except:
await ctx.send("Erreur, cette adresse n'est pas disponible en IPv6.")
return
iploading = await ctx.send("_réfléchis..._")
ipapi = urllib.request.urlopen("http://ip-api.com/json/" + ipaddress)
ipinfo = json.loads(ipapi.read().decode())
if ipinfo["status"] != "fail":
if ipinfo['query']:
embed = discord.Embed(title=f"Informations pour ``{realipaddress}`` *`({ipinfo['query']})`*", color=0x5858d7)
if ipinfo['org']:
embed.add_field(name="Appartient à :", value=ipinfo['org'], inline = False)
org = ipinfo['org']
else:
org = 'n/a'
if ipinfo['query']:
ip = ipinfo['query']
else:
ip = 'n/a'
if ipinfo['city']:
embed.add_field(name="Se situe à :", value=ipinfo['city'], inline = True)
city = ipinfo['city']
else:
city = 'n/a'
if ipinfo['regionName']:
regionName = ipinfo['regionName']
else:
regionName = 'n/a'
if ipinfo['country']:
if ipinfo['regionName']:
regionName = ipinfo['regionName']
else:
regionName = "N/A"
embed.add_field(name="Region :", value=f"{regionName} ({ipinfo['country']})", inline = True)
country = ipinfo['country']
else:
country = 'n/a'
embed = discord.Embed(title=f"Informations pour {ipaddress} *`({ip})`*", color=0x5858d7)
embed.add_field(name="Appartient à :", value=org, inline = False)
embed.add_field(name="Se situe à :", value=city, inline = True)
embed.add_field(name="Region :", value=f"{regionName} ({country})", inline = True)
embed.set_thumbnail(url=f"https://www.countryflags.io/{ipinfo['countryCode']}/flat/64.png")
await ctx.send(embed=embed)
else:
await ctx.send(content=f"Erreur, impossible d'avoir des informations sur l'adresse IP ``{ipinfo['query']}``")
await ctx.send(content=f"Erreur, impossible d'avoir des informations sur l'adresse IP {ipinfo['query']}")
await iploading.delete()
"""---------------------------------------------------------------------"""
@ -310,7 +312,7 @@ class Utility(commands.Cog):
"""Pour voir mon code"""
text = "How tu veux voir mon repos Gitea pour me disséquer ? " \
"Pas de soucis ! Je suis un Bot, je ne ressens pas la " \
"douleur !\n https://git.gnous.eu/gnous/tuxbot"
"douleur !\n https://git.gnous.eu/gnouseu/tuxbot-bot"
em = discord.Embed(title='Repos TuxBot-Bot', description=text, colour=0xE9D460)
em.set_author(name='Gnous', icon_url="https://cdn.discordapp.com/"
"icons/280805240977227776/"
@ -318,6 +320,41 @@ class Utility(commands.Cog):
".png")
await ctx.send(embed=em)
"""---------------------------------------------------------------------"""
@commands.command(name='quote', pass_context=True)
async def _quote(self, ctx, quote_id):
global quoted_message
async def get_message(message_id: int):
for channel in ctx.message.guild.channels:
if isinstance(channel, discord.TextChannel):
test_chan = await self.bot.fetch_channel(channel.id)
try:
return await test_chan.fetch_message(message_id)
except discord.NotFound:
pass
return None
quoted_message = await get_message(int(quote_id))
if quoted_message is not None:
embed = discord.Embed(colour=quoted_message.author.colour,
description=quoted_message.clean_content,
timestamp=quoted_message.created_at)
embed.set_author(name=quoted_message.author.display_name,
icon_url=quoted_message.author.avatar_url_as(
format="jpg"))
if len(quoted_message.attachments) >= 1:
embed.set_image(url=quoted_message.attachments[0].url)
embed.add_field(name="**Original**",
value=f"[Go!]({quoted_message.jump_url})")
embed.set_footer(text="#" + quoted_message.channel.name)
await ctx.send(embed=embed)
else:
await ctx.send("Impossible de trouver le message.")
def setup(bot):
bot.add_cog(Utility(bot))

View file

@ -2,100 +2,123 @@ from discord.ext import commands
def is_owner_check(message):
return str(message.author.id) in ['171685542553976832', '269156684155453451']
return str(message.author.id) in ['171685542553976832',
'269156684155453451']
def is_owner(warn=True):
def check(ctx, warn):
owner = is_owner_check(ctx.message)
if not owner and warn:
print(ctx.message.author.name + " à essayer d'executer " + ctx.message.content + " sur le serveur " + ctx.message.guild.name)
return owner
def check(ctx, log):
owner = is_owner_check(ctx.message)
if not owner and log:
print(ctx.message.author.name + " à essayer d'executer " + ctx.message.content + " sur le serveur " + ctx.message.guild.name)
return owner
owner = commands.check(lambda ctx: check(ctx, warn))
return owner
owner = commands.check(lambda ctx: check(ctx, warn))
return owner
"""-------------------------------------------------------------------------"""
"""--------------------------------------------------------------------------------------------------------------------------"""
async def check_permissions(ctx, perms, *, check=all):
is_owner = await ctx.bot.is_owner(ctx.author)
if is_owner or is_owner_check(ctx.message) == True:
return True
is_owner = await ctx.bot.is_owner(ctx.author)
if is_owner or is_owner_check(ctx.message) is True:
return True
resolved = ctx.channel.permissions_for(ctx.author)
return check(getattr(resolved, name, None) == value for name, value in
perms.items())
resolved = ctx.channel.permissions_for(ctx.author)
return check(getattr(resolved, name, None) == value for name, value in perms.items())
def has_permissions(*, check=all, **perms):
async def pred(ctx):
return await check_permissions(ctx, perms, check=check)
return commands.check(pred)
async def pred(ctx):
return await check_permissions(ctx, perms, check=check)
return commands.check(pred)
async def check_guild_permissions(ctx, perms, *, check=all):
is_owner = await ctx.bot.is_owner(ctx.author)
if is_owner:
return True
is_owner = await ctx.bot.is_owner(ctx.author)
if is_owner:
return True
if ctx.guild is None:
return False
if ctx.guild is None:
return False
resolved = ctx.author.guild_permissions
return check(getattr(resolved, name, None) == value for name, value in
perms.items())
resolved = ctx.author.guild_permissions
return check(getattr(resolved, name, None) == value for name, value in perms.items())
def has_guild_permissions(*, check=all, **perms):
async def pred(ctx):
return await check_guild_permissions(ctx, perms, check=check)
return commands.check(pred)
async def pred(ctx):
return await check_guild_permissions(ctx, perms, check=check)
return commands.check(pred)
# These do not take channel overrides into account
def is_mod():
async def pred(ctx):
return await check_guild_permissions(ctx, {'manage_guild': True})
return commands.check(pred)
async def pred(ctx):
return await check_guild_permissions(ctx, {'manage_guild': True})
return commands.check(pred)
def is_admin():
async def pred(ctx):
return await check_guild_permissions(ctx, {'administrator': True})
return commands.check(pred)
async def pred(ctx):
return await check_guild_permissions(ctx, {'administrator': True})
return commands.check(pred)
def mod_or_permissions(**perms):
perms['manage_guild'] = True
async def predicate(ctx):
return await check_guild_permissions(ctx, perms, check=any)
return commands.check(predicate)
perms['manage_guild'] = True
async def predicate(ctx):
return await check_guild_permissions(ctx, perms, check=any)
return commands.check(predicate)
def admin_or_permissions(**perms):
perms['administrator'] = True
async def predicate(ctx):
return await check_guild_permissions(ctx, perms, check=any)
return commands.check(predicate)
perms['administrator'] = True
async def predicate(ctx):
return await check_guild_permissions(ctx, perms, check=any)
return commands.check(predicate)
def is_in_guilds(*guild_ids):
def predicate(ctx):
guild = ctx.guild
if guild is None:
return False
return guild.id in guild_ids
return commands.check(predicate)
def predicate(ctx):
guild = ctx.guild
if guild is None:
return False
return guild.id in guild_ids
return commands.check(predicate)
def is_lounge_cpp():
return is_in_guilds(145079846832308224)
def get_user(message, user):
try:
member = message.mentions[0]
except:
member = message.guild.get_member_named(user)
if not member:
try:
member = message.guild.get_member(int(user))
except ValueError:
pass
if not member:
return None
return member
try:
member = message.mentions[0]
except:
member = message.guild.get_member_named(user)
if not member:
try:
member = message.guild.get_member(int(user))
except ValueError:
pass
if not member:
return None
return member
def check_date(date: str):
if len(date) == 1:
return f"0{date}"
else:
return date
if len(date) == 1:
return f"0{date}"
else:
return date

View file

@ -16,6 +16,7 @@ class text_colors:
LIGHT_CYAN = '\033[96m'
WHITE = '\033[97m'
class bg_colors:
BLACK = '\033[40m'
RED = '\033[41m'
@ -34,10 +35,12 @@ class bg_colors:
LIGHT_CYAN = '\033[106m'
WHITE = '\033[107m'
class text_style:
BOLD = '\033[1m'
DIM = '\033[2m'
UNDERLINE = '\033[4m'
BLINK = '\033[5m'
ENDC = '\033[0m'

View file

@ -1,245 +0,0 @@
from PIL import Image
from PIL import ImageOps
from PIL import ImageDraw
from PIL import ImageFont
from .checks import check_date
import aiohttp, imghdr, textwrap, math, datetime
async def generate_passport(self, user, theme: str = None):
name = user.name
userid = user.id
avatar = user.avatar_url
def_avatar = user.default_avatar_url
created = datetime.datetime.fromisoformat(str(user.created_at))
nick = user.display_name
discr = user.discriminator
roles = user.roles
top_role_color = str(user.top_role.color)[1:]
user_color = tuple(int(top_role_color[i:i+2], 16) for i in (0, 2 ,4))
user_color += (255,)
color = {
"dark": {
"background": 39,
"question": 220,
"answer": 150
},
"light": {
"background": 216,
"question": 35,
"answer": 61
}
}
user_birth_day = check_date(str(created.day))
user_birth_month = check_date(str(created.month))
formated_user_birth = str(user_birth_day) + "/" + str(user_birth_month) + "/" + str(created.year)
formated_passportdate = "n/a"
roleImages = {}
def draw_underlined_text(draw, pos, text, font, **options):
twidth, theight = draw.textsize(text, font=font)
lx, ly = pos[0], pos[1] + theight
draw.text(pos, text, font=font, **options)
draw.line((lx, ly, lx + twidth, ly), **options)
def break_line(draw, pos, text, font, **options):
lines = text.split("\n")
current_y = pos[1]
for line in lines:
twidth, theight = draw.textsize(line, font=font)
lx, ly = pos[0], current_y
if textwrap.fill(line, 60) == line:
draw.text((lx, ly), textwrap.fill(line, 60), font=font, **options)
current_y += math.floor(theight)
else:
draw.text((lx, ly), textwrap.fill(line, 60), font=font, **options)
current_y += math.floor(theight*2)
for x, role in enumerate(roles):
try:
roleImages[role.name] = Image.open(f"data/images/roles/small/{role.name.lower().replace(' user', '')}.png")
except Exception as e:
next
if avatar == '':
async with aiohttp.ClientSession() as session:
async with session.get(def_avatar) as r:
image = await r.content.read()
else:
async with aiohttp.ClientSession() as session:
async with session.get(avatar) as r:
image = await r.content.read()
with open('data/users/avatars/{}.png'.format(user.id), 'wb') as f:
f.write(image)
checked = False
while checked == False:
checks = 0
isImage = imghdr.what('data/users/avatars/{}.png'.format(user.id))
if checks > 4:
checked = True
if isImage != 'None':
checked = True
else:
checks += 1
av = Image.open('data/users/avatars/{}.png'.format(user.id))
userAvatar = av.resize((128, 128), resample=Image.BILINEAR).convert('RGBA')
maxsize = ( 800, 500)
try:
bg = Image.open('data/users/backgrounds/{0}.png'.format(user.id))
bg_width, bg_height = bg.size
bg = ImageOps.fit(bg,maxsize)
except:
bg = Image.open('data/images/background_default.png')
fontFace = 'data/fonts/{}'.format(self.bot.config.fonts['normal'])
fontFace_bold = 'data/fonts/{}'.format(self.bot.config.fonts['bold'])
fontSize = 18
fontSizeVeryTiny = 16
descSizeQuestion = 10
descSizeAnswer = 10
headerSize = 32
headerSizeTiny = 24
headerSizeVeryTiny = 16
header_font = ImageFont.truetype(fontFace_bold, headerSize)
header_font_tiny = ImageFont.truetype(fontFace_bold, headerSizeTiny)
header_font_very_tiny = ImageFont.truetype(fontFace_bold, headerSizeVeryTiny)
font = ImageFont.truetype(fontFace, fontSize)
font_very_tiny = ImageFont.truetype(fontFace, fontSizeVeryTiny)
desc_font_question = ImageFont.truetype(fontFace_bold, descSizeQuestion)
desc_font_answer = ImageFont.truetype(fontFace, descSizeAnswer)
font_bold = font = ImageFont.truetype(fontFace_bold, fontSize)
answers = None
self.cursor.execute("SELECT os, config, languages, pays, passportdate, theme FROM passport WHERE userid=%s", str(user.id))
answers = self.cursor.fetchone()
if not theme:
if answers:
theme = str(answers[5])
else:
theme = "dark"
cardbg = Image.new('RGBA', (800, 500), (0, 0, 0, 255))
d = ImageDraw.Draw(cardbg)
d.rectangle([(0, 0), 800, 500], fill=(255, 255, 255, 255))
cardbg.paste(bg, (0, 0))
cardfg = Image.new('RGBA', (800, 500), (255, 255, 255, 0))
dd = ImageDraw.Draw(cardfg)
# Info Box Top
dd.rectangle([(60, 60), (600, 191)], fill=(color[theme]["background"], color[theme]["background"], color[theme]["background"], 200))
dd.rectangle([(60, 60), (600, 134)], fill=(color[theme]["background"], color[theme]["background"], color[theme]["background"], 255))
# Avatar box
if user_color == (0, 0, 0, 255):
user_color = (color[theme]["background"], color[theme]["background"], color[theme]["background"], 255)
dd.rectangle([(609, 60), (740, 191)], fill=user_color)
cardfg.paste(userAvatar, (611, 62))
# Profile Information
if textwrap.fill(nick, 25) != nick:
dd.text((70, 70), nick, fill=(color[theme]["question"], color[theme]["question"], color[theme]["question"], 220), font=header_font_very_tiny)
dd.text((70, 106), '@' + name + '#' + discr, fill=(color[theme]["answer"], color[theme]["answer"], color[theme]["answer"], 225), font=font_very_tiny)
elif textwrap.fill(nick, 15) != nick:
dd.text((70, 70), nick, fill=(color[theme]["question"], color[theme]["question"], color[theme]["question"], 220), font=header_font_tiny)
dd.text((70, 106), '@' + name + '#' + discr, fill=(color[theme]["answer"], color[theme]["answer"], color[theme]["answer"], 225), font=font)
else:
dd.text((70, 64), nick, fill=(color[theme]["question"], color[theme]["question"], color[theme]["question"], 220), font=header_font)
dd.text((70, 106), '@' + name + '#' + discr, fill=(color[theme]["answer"], color[theme]["answer"], color[theme]["answer"], 225), font=font)
draw_underlined_text(dd, (380, 75), "Date de parution sur discord :", fill=(color[theme]["question"], color[theme]["question"], color[theme]["question"], 220), font=desc_font_question)
dd.text((542, 75), formated_user_birth, fill=(color[theme]["answer"], color[theme]["answer"], color[theme]["answer"], 225), font=desc_font_answer)
# Roles
for idy, ii in enumerate(roleImages):
startx = int((270 - (30 * len(roleImages))) / 2)
cardfg.paste(roleImages[ii], (197 + startx + (30 * idy),152), roleImages[ii])
#Info Box Bottom
dd.rectangle([(60, 200), (740, 450)], fill=(color[theme]["background"], color[theme]["background"], color[theme]["background"], 200))
if answers:
passportdate = datetime.datetime.fromisoformat(answers[4])
passportdate_day = check_date(str(passportdate.day))
passportdate_month = check_date(str(passportdate.month))
formated_passportdate = str(passportdate_day) + "/" + str(passportdate_month) + "/" + str(passportdate.year)
draw_underlined_text(dd, (80, 220), "Système(s) d'exploitation :", desc_font_question, fill=(color[theme]["question"], color[theme]["question"], color[theme]["question"], 255))
break_line(
dd,
(80, 240),
answers[0],
fill=(color[theme]["answer"], color[theme]["answer"], color[theme]["answer"], 255),
font=desc_font_answer
)
draw_underlined_text(dd, (80, 300), "Langages de programmation préférés :", desc_font_question, fill=(color[theme]["question"], color[theme]["question"], color[theme]["question"], 255))
break_line(
dd,
(80, 320),
answers[2],
fill=(color[theme]["answer"], color[theme]["answer"], color[theme]["answer"], 255),
font=desc_font_answer
)
draw_underlined_text(dd, (80, 380), "Pays :", desc_font_question, fill=(color[theme]["question"], color[theme]["question"], color[theme]["question"], 255))
break_line(
dd,
(80, 400),
answers[3],
fill=(color[theme]["answer"], color[theme]["answer"], color[theme]["answer"], 255),
font=desc_font_answer
)
dd.line((400, 220, 400, 430), fill=(color[theme]["answer"], color[theme]["answer"], color[theme]["answer"], 255))
draw_underlined_text(dd, (410, 220), "Configuration Système :", desc_font_question, fill=(color[theme]["question"], color[theme]["question"], color[theme]["question"], 255))
break_line(
dd,
(410, 240),
answers[1],
fill=(color[theme]["answer"], color[theme]["answer"], color[theme]["answer"], 255),
font=desc_font_answer
)
else:
dd.text(
(370, 300),
"Non renseigné.",
fill=(color[theme]["question"], color[theme]["question"], color[theme]["question"], 255),
font=desc_font_question
)
draw_underlined_text(dd, (380, 100), "Date de création du passeport :", fill=(color[theme]["question"], color[theme]["question"], color[theme]["question"], 220), font=desc_font_question)
dd.text((542, 100), formated_passportdate, fill=(color[theme]["answer"], color[theme]["answer"], color[theme]["answer"], 225), font=desc_font_answer)
card = Image.new('RGBA', (800, 500), (255, 255, 255, 255))
card = Image.alpha_composite(card, cardbg)
card = Image.alpha_composite(card, cardfg)
return card

View file

@ -1,7 +1,7 @@
token = "INSERT TOKEN HERE"
client_id = "INSERT_CLIENT_ID_HERE"
log_channel_id = "INSERT_LOG_CHANNEL_HERE"
main_server_id = "INSERT_MAIN_CHANNEL_ID_HERE"
client_id = <INSERT_CLIENT_ID_HERE (in int)>
log_channel_id = <INSERT_LOG_CHANNEL_HERE (in int)>
main_server_id = <INSERT_MAIN_CHANNEL_ID_HERE (in int)>
game = "PLAYING_GAME_HERE"
prefix = ["."]
@ -17,10 +17,4 @@ mysql = {
}
authorized_id = ['admin ids here']
## Passport settings
fonts = {
"normal": "NotoSansCJK-Regular.ttc",
"bold": "NotoSansCJK-Bold.ttc"
}
unkickable_id = ['unkickable ids here']

BIN
data/.DS_Store vendored

Binary file not shown.

Binary file not shown.

Binary file not shown.

BIN
data/images/.DS_Store vendored

Binary file not shown.

Binary file not shown.

Before

Width:  |  Height:  |  Size: 627 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.3 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1,012 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 763 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 845 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 962 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 239 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 518 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 478 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 610 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 917 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 261 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 911 B

View file

@ -1,2 +0,0 @@
-- This file is only for keeping the folder alive in GitHub. --
-- Ce fichier ne sert qu'à garder ce dossier en vie dans GitHub. --