refactor(command|sondage): continue rewrite of sondage

known issues: datas are not commited in database on reaction on
This commit is contained in:
Romain J 2019-10-06 01:49:30 +02:00
parent d5f1f71a0a
commit 76e845e5be
18 changed files with 313 additions and 101 deletions

3
.gitignore vendored
View file

@ -3,7 +3,6 @@ __pycache__/
*.pyc *.pyc
.env .env
config.py config.py
!cogs/utils/*
.DS_Store .DS_Store
private.py private.py
@ -11,4 +10,4 @@ private.py
.idea/ .idea/
#other #other
logs/* *.log

View file

@ -6,12 +6,12 @@
- [ ] Alias system for commands (e.g. `.alias .ci show .cs`) - [ ] Alias system for commands (e.g. `.alias .ci show .cs`)
- [ ] Migrate MySQL to postgresql - [ ] Migrate MySQL to postgresql
- [x] Prepare bot for python 3.8 and discord.py 1.3.0 - [x] Prepare bot for python 3.8 and discord.py 1.3.0
- [x] Create launcher - [ ] Create launcher
- [ ] Create documentation - [ ] Create documentation
## Launcher requirements : ## Launcher requirements :
- [x] Can install the bot - [ ] Can install the bot
- [x] Can launch the bot - [x] Can launch the bot
- [x] Can propose updates - [x] Can propose updates
@ -82,5 +82,5 @@
--- ---
# Cogs.sondage commands `(renamed as cogs.poll)` # Cogs.sondage commands `(renamed as cogs.poll)` `canceled until the frontend development`
- [ ] sondage (help?) - [ ] sondage (help?)

7
bot.py
View file

@ -26,6 +26,7 @@ l_extensions = (
'cogs.basics', 'cogs.basics',
'cogs.utility', 'cogs.utility',
'cogs.logs', 'cogs.logs',
'cogs.poll',
'jishaku', 'jishaku',
) )
@ -127,10 +128,12 @@ class TuxBot(commands.AutoShardedBot):
@property @property
def logs_webhook(self) -> discord.Webhook: def logs_webhook(self) -> discord.Webhook:
logs_webhook = self.config.logs_webhook logs_webhook = self.config.logs_webhook
webhook = discord.Webhook.partial(id=logs_webhook.get('id'), webhook = discord.Webhook.partial(
id=logs_webhook.get('id'),
token=logs_webhook.get('token'), token=logs_webhook.get('token'),
adapter=discord.AsyncWebhookAdapter( adapter=discord.AsyncWebhookAdapter(
self.session) self.session
)
) )
return webhook return webhook

View file

@ -9,9 +9,8 @@ import humanize
from discord.ext import commands from discord.ext import commands
from bot import TuxBot from bot import TuxBot
from .utils.models.lang import Lang
from .utils.lang import Texts from .utils.lang import Texts
from .utils.models.warn import Warn from .utils.models import Warn, Lang
log = logging.getLogger(__name__) log = logging.getLogger(__name__)
@ -87,7 +86,8 @@ class Admin(commands.Cog):
message_id) message_id)
await message.edit(content=content) await message.edit(content=content)
except (discord.errors.NotFound, discord.errors.Forbidden): except (discord.errors.NotFound, discord.errors.Forbidden):
await ctx.send(Texts('utils', ctx).get("Unable to find the message"), await ctx.send(
Texts('utils', ctx).get("Unable to find the message"),
delete_after=5) delete_after=5)
@_say.command(name='to') @_say.command(name='to')
@ -120,10 +120,12 @@ class Admin(commands.Cog):
await ctx.send(embed=e) await ctx.send(embed=e)
except discord.Forbidden: except discord.Forbidden:
await ctx.send(Texts('admin', ctx).get("Unable to ban this user"), await ctx.send(
Texts('admin', ctx).get("Unable to ban this user"),
delete_after=5) delete_after=5)
except discord.errors.NotFound: except discord.errors.NotFound:
await ctx.send(Texts('utils', ctx).get("Unable to find the user..."), await ctx.send(
Texts('utils', ctx).get("Unable to find the user..."),
delete_after=5) delete_after=5)
"""---------------------------------------------------------------------""" """---------------------------------------------------------------------"""
@ -145,10 +147,12 @@ class Admin(commands.Cog):
await ctx.send(embed=e) await ctx.send(embed=e)
except discord.Forbidden: except discord.Forbidden:
await ctx.send(Texts('admin', ctx).get("Unable to kick this user"), await ctx.send(
Texts('admin', ctx).get("Unable to kick this user"),
delete_after=5) delete_after=5)
except discord.errors.NotFound: except discord.errors.NotFound:
await ctx.send(Texts('utils', ctx).get("Unable to find the user..."), await ctx.send(
Texts('utils', ctx).get("Unable to find the user..."),
delete_after=5) delete_after=5)
"""---------------------------------------------------------------------""" """---------------------------------------------------------------------"""
@ -180,7 +184,8 @@ class Admin(commands.Cog):
for emoji in emojis: for emoji in emojis:
await message.add_reaction(emoji) await message.add_reaction(emoji)
except discord.errors.NotFound: except discord.errors.NotFound:
await ctx.send(Texts('utils', ctx).get("Unable to find the message"), await ctx.send(
Texts('utils', ctx).get("Unable to find the message"),
delete_after=5) delete_after=5)
@_react.command(name='clear') @_react.command(name='clear')
@ -190,7 +195,8 @@ class Admin(commands.Cog):
message_id) message_id)
await message.clear_reactions() await message.clear_reactions()
except discord.errors.NotFound: except discord.errors.NotFound:
await ctx.send(Texts('utils', ctx).get("Unable to find the message"), await ctx.send(
Texts('utils', ctx).get("Unable to find the message"),
delete_after=5) delete_after=5)
"""---------------------------------------------------------------------""" """---------------------------------------------------------------------"""
@ -207,7 +213,8 @@ class Admin(commands.Cog):
message_id) message_id)
await message.delete() await message.delete()
except (discord.errors.NotFound, discord.errors.Forbidden): except (discord.errors.NotFound, discord.errors.Forbidden):
await ctx.send(Texts('utils', ctx).get("Unable to find the message"), await ctx.send(
Texts('utils', ctx).get("Unable to find the message"),
delete_after=5) delete_after=5)
@_delete.command(name='from', aliases=['to', 'in']) @_delete.command(name='from', aliases=['to', 'in'])
@ -223,7 +230,8 @@ class Admin(commands.Cog):
message_id) message_id)
await message.delete() await message.delete()
except (discord.errors.NotFound, discord.errors.Forbidden): except (discord.errors.NotFound, discord.errors.Forbidden):
await ctx.send(Texts('utils', ctx).get("Unable to find the message"), await ctx.send(
Texts('utils', ctx).get("Unable to find the message"),
delete_after=5) delete_after=5)
"""---------------------------------------------------------------------""" """---------------------------------------------------------------------"""
@ -394,17 +402,18 @@ class Admin(commands.Cog):
@commands.command(name='language', aliases=['lang', 'langue', 'langage']) @commands.command(name='language', aliases=['lang', 'langue', 'langage'])
async def _language(self, ctx: commands.Context, locale: str): async def _language(self, ctx: commands.Context, locale: str):
available = self.bot.engine\ available = self.bot.engine \
.query(Lang.value)\ .query(Lang.value) \
.filter(Lang.key == 'available')\ .filter(Lang.key == 'available') \
.one()[0]\ .one()[0] \
.split(', ') .split(', ')
if locale.lower() not in available: if locale.lower() not in available:
await ctx.send(Texts('admin', ctx).get('Unable to find this language')) await ctx.send(
Texts('admin', ctx).get('Unable to find this language'))
else: else:
current = self.bot.engine\ current = self.bot.engine \
.query(Lang)\ .query(Lang) \
.filter(Lang.key == str(ctx.guild.id)) .filter(Lang.key == str(ctx.guild.id))
if current.count() > 0: if current.count() > 0:
@ -416,7 +425,8 @@ class Admin(commands.Cog):
self.bot.engine.add(new_row) self.bot.engine.add(new_row)
self.bot.engine.commit() self.bot.engine.commit()
await ctx.send(Texts('admin', ctx).get('Language changed successfully')) await ctx.send(
Texts('admin', ctx).get('Language changed successfully'))
def setup(bot: TuxBot): def setup(bot: TuxBot):

View file

@ -55,7 +55,8 @@ class Basics(commands.Cog):
file_amount += 1 file_amount += 1
with open(file_dir, "r", encoding="utf-8") as file: with open(file_dir, "r", encoding="utf-8") as file:
for line in file: for line in file:
if not line.strip().startswith("#") or not line.strip(): if not line.strip().startswith("#") \
or not line.strip():
total += 1 total += 1
return total, file_amount return total, file_amount
@ -68,7 +69,7 @@ class Basics(commands.Cog):
with proc.oneshot(): with proc.oneshot():
mem = proc.memory_full_info() mem = proc.memory_full_info()
e = discord.Embed( e = discord.Embed(
title=f"{Texts('basics', ctx).get('Information about TuxBot')}", title=Texts('basics', ctx).get('Information about TuxBot'),
color=0x89C4F9) color=0x89C4F9)
e.add_field( e.add_field(
@ -129,7 +130,7 @@ class Basics(commands.Cog):
name=f"__:link: {Texts('basics', ctx).get('Links')}__", name=f"__:link: {Texts('basics', ctx).get('Links')}__",
value="[tuxbot.gnous.eu](https://tuxbot.gnous.eu/) " value="[tuxbot.gnous.eu](https://tuxbot.gnous.eu/) "
"| [gnous.eu](https://gnous.eu/) " "| [gnous.eu](https://gnous.eu/) "
f"| [{Texts('basics').get('Invite')}](https://discordapp.com/oauth2/authorize?client_id=301062143942590465&scope=bot&permissions=268749888)", f"| [{Texts('basics', ctx).get('Invite')}](https://discordapp.com/oauth2/authorize?client_id=301062143942590465&scope=bot&permissions=268749888)",
inline=False inline=False
) )

View file

@ -1,22 +1,133 @@
from typing import Union
import discord
import bcrypt
from discord.ext import commands from discord.ext import commands
from bot import TuxBot from bot import TuxBot
from .utils.lang import Texts from .utils.lang import Texts
from .utils.models import Poll
from .utils import emotes as utils_emotes
class Poll(commands.Cog): class Polls(commands.Cog):
def __init__(self, bot: TuxBot): def __init__(self, bot: TuxBot):
self.bot = bot self.bot = bot
def get_poll(self, pld) -> Union[bool, Poll]:
if pld.user_id != self.bot.user.id:
poll = self.bot.engine \
.query(Poll) \
.filter(Poll.message_id == pld.message_id) \
.one_or_none()
if poll is not None:
emotes = utils_emotes.get(len(poll.responses))
if pld.emoji.name in emotes:
return poll
return False
async def remove_reaction(self, pld):
channel: discord.TextChannel = self.bot.get_channel(
pld.channel_id
)
message: discord.Message = await channel.fetch_message(
pld.message_id
)
user: discord.User = await self.bot.fetch_user(pld.user_id)
await message.remove_reaction(pld.emoji.name, user)
@commands.Cog.listener()
async def on_raw_reaction_add(self, pld: discord.RawReactionActionEvent):
poll = self.get_poll(pld)
if poll:
if poll.is_anonymous:
await self.remove_reaction(pld)
user_id = str(pld.user_id).encode()
responses = poll.responses
choice = utils_emotes.get_index(pld.emoji.name) + 1
responders = responses.get(str(choice))
if not responders:
print(responders, 'before0')
user_id_hash = bcrypt.hashpw(user_id, bcrypt.gensalt())
responders.append(user_id_hash)
print(responders, 'after0')
else:
for i, responder in enumerate(responders):
if bcrypt.checkpw(user_id, responder.encode()):
print(responders, 'before1')
responders.pop(i)
print(responders, 'after1')
else:
print(responders, 'before2')
user_id_hash = bcrypt.hashpw(user_id, bcrypt.gensalt())
responders.append(user_id_hash)
print(responders, 'after2')
poll.responses = responses
print(poll.responses)
self.bot.engine.commit()
return 1
"""---------------------------------------------------------------------""" """---------------------------------------------------------------------"""
async def make_poll(self, ctx: commands.Context, poll: str, anonymous):
question = (poll.split('|')[0]).strip()
responses = [response.strip() for response in poll.split('|')[1:]]
responses_row = {}
emotes = utils_emotes.get(len(responses))
stmt = await ctx.send(Texts('poll', ctx).get('**Preparation...**'))
poll_row = Poll()
self.bot.engine.add(poll_row)
self.bot.engine.flush()
e = discord.Embed(description=f"**{question}**")
e.set_author(
name=ctx.author,
icon_url='https://cdn.pixabay.com/photo/2017/05/15/23/48/survey-2316468_960_720.png'
)
for i, response in enumerate(responses):
responses_row[str(i+1)] = []
e.add_field(
name=f"{emotes[i]} __{response.capitalize()}__",
value="**0** vote"
)
e.set_footer(text=f"ID: {poll_row.id}")
poll_row.message_id = stmt.id
poll_row.poll = e.to_dict()
poll_row.is_anonymous = anonymous
poll_row.responses = responses_row
self.bot.engine.commit()
await stmt.edit(content='', embed=e)
for emote in range(len(responses)):
await stmt.add_reaction(emotes[emote])
@commands.group(name='sondage', aliases=['poll']) @commands.group(name='sondage', aliases=['poll'])
async def _poll(self, ctx): async def _poll(self, ctx: commands.Context):
""" if ctx.invoked_subcommand is None:
todo: refer to readme.md ...
"""
@_poll.group(name='create', aliases=['new', 'nouveau'])
async def _poll_create(self, ctx: commands.Context, *, poll: str):
is_anonymous = '--anonyme' in poll
poll = poll.replace('--anonyme', '')
await self.make_poll(ctx, poll, anonymous=is_anonymous)
def setup(bot: TuxBot): def setup(bot: TuxBot):
bot.add_cog(Poll(bot)) bot.add_cog(Polls(bot))

View file

@ -5,6 +5,7 @@ import discord
from discord.ext import commands from discord.ext import commands
from bot import TuxBot from bot import TuxBot
import socket import socket
from socket import AF_INET6
from .utils.lang import Texts from .utils.lang import Texts
@ -23,22 +24,24 @@ class Utility(commands.Cog):
await ctx.trigger_typing() await ctx.trigger_typing()
try:
if ip_type in ('v6', 'ipv6'): if ip_type in ('v6', 'ipv6'):
try: try:
ip = socket.getaddrinfo(addr, None, socket.AF_INET6)[1][4][0] ip = socket.getaddrinfo(addr, None, AF_INET6)[1][4][0]
except socket.gaierror: except socket.gaierror:
return await ctx.send( return await ctx.send(
Texts('utility', ctx).get('ipv6 not available')) Texts('utility', ctx).get('ipv6 not available'))
else: else:
ip = socket.gethostbyname(addr) ip = socket.gethostbyname(addr)
async with self.bot.session.get(f"http://ip-api.com/json/{ip}") as s: async with self.bot.session.get(f"http://ip-api.com/json/{ip}") \
as s:
response: dict = await s.json() response: dict = await s.json()
if response.get('status') == 'success': if response.get('status') == 'success':
e = discord.Embed( e = discord.Embed(
title=f"{Texts('utility', ctx).get('Information for')} " title=f"{Texts('utility', ctx).get('Information for')}"
f"``{addr}`` *`({response.get('query')})`*", f" ``{addr}`` *`({response.get('query')})`*",
color=0x5858d7 color=0x5858d7
) )
@ -71,6 +74,11 @@ class Utility(commands.Cog):
content=f"{Texts('utility', ctx).get('info not available')}" content=f"{Texts('utility', ctx).get('info not available')}"
f"``{response.get('query')}``") f"``{response.get('query')}``")
except Exception:
await ctx.send(
f"{Texts('utility', ctx).get('Cannot connect to host')} {addr}"
)
"""---------------------------------------------------------------------""" """---------------------------------------------------------------------"""
@commands.command(name='getheaders') @commands.command(name='getheaders')
@ -78,9 +86,10 @@ class Utility(commands.Cog):
if (addr.startswith('http') or addr.startswith('ftp')) is not True: if (addr.startswith('http') or addr.startswith('ftp')) is not True:
addr = f"http://{addr}" addr = f"http://{addr}"
await ctx.trigger_typing()
try: try:
async with self.bot.session.get(addr) as s: async with self.bot.session.get(addr) as s:
await ctx.trigger_typing()
e = discord.Embed( e = discord.Embed(
title=f"{Texts('utility', ctx).get('Headers of')} {addr}", title=f"{Texts('utility', ctx).get('Headers of')} {addr}",
color=0xd75858 color=0xd75858
@ -95,9 +104,10 @@ class Utility(commands.Cog):
e.add_field(name=key, value=value, inline=True) e.add_field(name=key, value=value, inline=True)
await ctx.send(embed=e) await ctx.send(embed=e)
except aiohttp.client_exceptions.ClientConnectorError: except aiohttp.client_exceptions.ClientError:
await ctx.send(f"{Texts('utility', ctx).get('Cannot connect to host')} " await ctx.send(
f"{addr}") f"{Texts('utility', ctx).get('Cannot connect to host')} {addr}"
)
"""---------------------------------------------------------------------""" """---------------------------------------------------------------------"""

10
cogs/utils/emotes.py Normal file
View file

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

View file

@ -0,0 +1,3 @@
from .lang import Lang
from .warn import Warn
from .poll import Poll

View file

@ -4,7 +4,7 @@ Base = declarative_base()
class Lang(Base): class Lang(Base):
__tablename__ = 'lang' __tablename__ = 'langs'
key = Column(String, primary_key=True) key = Column(String, primary_key=True)
value = Column(String) value = Column(String)

20
cogs/utils/models/poll.py Normal file
View file

@ -0,0 +1,20 @@
from sqlalchemy.ext.declarative import declarative_base
from sqlalchemy import Column, Integer, Boolean, BigInteger, JSON
Base = declarative_base()
class Poll(Base):
__tablename__ = 'polls'
id = Column(Integer, primary_key=True)
message_id = Column(BigInteger)
poll = Column(JSON)
is_anonymous = Column(Boolean)
responses = Column(JSON, nullable=True)
def __repr__(self):
return "<Poll(id='%s', message_id='%s', poll='%s', " \
"is_anonymous='%s', responses='%s')>" % \
(self.id, self.message_id, self.poll,
self.is_anonymous, self.responses)

View file

@ -2,6 +2,7 @@ import datetime
from sqlalchemy.ext.declarative import declarative_base from sqlalchemy.ext.declarative import declarative_base
from sqlalchemy import Column, Integer, String, BIGINT, TIMESTAMP from sqlalchemy import Column, Integer, String, BIGINT, TIMESTAMP
Base = declarative_base() Base = declarative_base()
@ -16,5 +17,5 @@ class Warn(Base):
def __repr__(self): def __repr__(self):
return "<Warn(server_id='%s', user_id='%s', reason='%s', " \ return "<Warn(server_id='%s', user_id='%s', reason='%s', " \
"created_at='%s')>"\ "created_at='%s')>" \
% (self.server_id, self.user_id, self.reason, self.created_at) % (self.server_id, self.user_id, self.reason, self.created_at)

Binary file not shown.

View file

@ -0,0 +1,20 @@
# 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:51
msgid "**Preparation...**"
msgstr ""

Binary file not shown.

View file

@ -0,0 +1,20 @@
# 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:51
msgid "**Preparation...**"
msgstr "**Préparation...**"

View file

@ -1,5 +1,8 @@
{ {
"280805240977227776": [ "280805240977227776": [
"b." "b!"
],
"303633056944881686": [
"b! "
] ]
} }

View file

@ -8,3 +8,4 @@ sqlalchemy
gitpython gitpython
requests requests
psutil psutil
bcrypt