From 76e845e5be7a495de05384d32677d639fc4f1151 Mon Sep 17 00:00:00 2001 From: Romain J Date: Sun, 6 Oct 2019 01:49:30 +0200 Subject: [PATCH] refactor(command|sondage): continue rewrite of sondage known issues: datas are not commited in database on reaction on --- .gitignore | 3 +- README.md | 6 +- bot.py | 13 +-- cogs/admin.py | 66 ++++++++------ cogs/basics.py | 7 +- cogs/poll.py | 123 ++++++++++++++++++++++++-- cogs/utility.py | 110 ++++++++++++----------- cogs/utils/emotes.py | 10 +++ cogs/utils/models/__init__.py | 3 + cogs/utils/models/lang.py | 2 +- cogs/utils/models/poll.py | 20 +++++ cogs/utils/models/warn.py | 3 +- extras/locales/en/LC_MESSAGES/poll.mo | Bin 0 -> 363 bytes extras/locales/en/LC_MESSAGES/poll.po | 20 +++++ extras/locales/fr/LC_MESSAGES/poll.mo | Bin 0 -> 418 bytes extras/locales/fr/LC_MESSAGES/poll.po | 20 +++++ prefixes.json | 5 +- requirements.txt | 3 +- 18 files changed, 313 insertions(+), 101 deletions(-) create mode 100644 cogs/utils/emotes.py create mode 100644 cogs/utils/models/__init__.py create mode 100644 cogs/utils/models/poll.py create mode 100644 extras/locales/en/LC_MESSAGES/poll.mo create mode 100644 extras/locales/en/LC_MESSAGES/poll.po create mode 100644 extras/locales/fr/LC_MESSAGES/poll.mo create mode 100644 extras/locales/fr/LC_MESSAGES/poll.po diff --git a/.gitignore b/.gitignore index 28290b4..8eef5b1 100644 --- a/.gitignore +++ b/.gitignore @@ -3,7 +3,6 @@ __pycache__/ *.pyc .env config.py -!cogs/utils/* .DS_Store private.py @@ -11,4 +10,4 @@ private.py .idea/ #other -logs/* \ No newline at end of file +*.log \ No newline at end of file diff --git a/README.md b/README.md index e6bb595..37224ee 100644 --- a/README.md +++ b/README.md @@ -6,12 +6,12 @@ - [ ] Alias system for commands (e.g. `.alias .ci show .cs`) - [ ] Migrate MySQL to postgresql - [x] Prepare bot for python 3.8 and discord.py 1.3.0 - - [x] Create launcher + - [ ] Create launcher - [ ] Create documentation ## Launcher requirements : - - [x] Can install the bot + - [ ] Can install the bot - [x] Can launch the bot - [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?) diff --git a/bot.py b/bot.py index b1c06cd..5ca163b 100755 --- a/bot.py +++ b/bot.py @@ -26,6 +26,7 @@ l_extensions = ( 'cogs.basics', 'cogs.utility', 'cogs.logs', + 'cogs.poll', 'jishaku', ) @@ -127,11 +128,13 @@ class TuxBot(commands.AutoShardedBot): @property def logs_webhook(self) -> discord.Webhook: 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) - ) + webhook = discord.Webhook.partial( + id=logs_webhook.get('id'), + token=logs_webhook.get('token'), + adapter=discord.AsyncWebhookAdapter( + self.session + ) + ) return webhook diff --git a/cogs/admin.py b/cogs/admin.py index c637203..7ad57f1 100644 --- a/cogs/admin.py +++ b/cogs/admin.py @@ -9,9 +9,8 @@ import humanize from discord.ext import commands from bot import TuxBot -from .utils.models.lang import Lang from .utils.lang import Texts -from .utils.models.warn import Warn +from .utils.models import Warn, Lang log = logging.getLogger(__name__) @@ -87,8 +86,9 @@ class Admin(commands.Cog): message_id) await message.edit(content=content) except (discord.errors.NotFound, discord.errors.Forbidden): - await ctx.send(Texts('utils', ctx).get("Unable to find the message"), - delete_after=5) + await ctx.send( + Texts('utils', ctx).get("Unable to find the message"), + delete_after=5) @_say.command(name='to') async def _say_to(self, ctx: commands.Context, @@ -120,11 +120,13 @@ class Admin(commands.Cog): await ctx.send(embed=e) except discord.Forbidden: - await ctx.send(Texts('admin', ctx).get("Unable to ban this user"), - delete_after=5) + await ctx.send( + Texts('admin', ctx).get("Unable to ban this user"), + delete_after=5) except discord.errors.NotFound: - await ctx.send(Texts('utils', ctx).get("Unable to find the user..."), - delete_after=5) + await ctx.send( + Texts('utils', ctx).get("Unable to find the user..."), + delete_after=5) """---------------------------------------------------------------------""" @@ -145,11 +147,13 @@ class Admin(commands.Cog): await ctx.send(embed=e) except discord.Forbidden: - await ctx.send(Texts('admin', ctx).get("Unable to kick this user"), - delete_after=5) + await ctx.send( + Texts('admin', ctx).get("Unable to kick this user"), + delete_after=5) except discord.errors.NotFound: - await ctx.send(Texts('utils', ctx).get("Unable to find the user..."), - delete_after=5) + await ctx.send( + Texts('utils', ctx).get("Unable to find the user..."), + delete_after=5) """---------------------------------------------------------------------""" @@ -180,8 +184,9 @@ class Admin(commands.Cog): for emoji in emojis: await message.add_reaction(emoji) except discord.errors.NotFound: - await ctx.send(Texts('utils', ctx).get("Unable to find the message"), - delete_after=5) + await ctx.send( + Texts('utils', ctx).get("Unable to find the message"), + delete_after=5) @_react.command(name='clear') async def _react_remove(self, ctx: commands.Context, message_id: int): @@ -190,8 +195,9 @@ class Admin(commands.Cog): message_id) await message.clear_reactions() except discord.errors.NotFound: - await ctx.send(Texts('utils', ctx).get("Unable to find the message"), - delete_after=5) + await ctx.send( + Texts('utils', ctx).get("Unable to find the message"), + delete_after=5) """---------------------------------------------------------------------""" @@ -207,8 +213,9 @@ class Admin(commands.Cog): message_id) await message.delete() except (discord.errors.NotFound, discord.errors.Forbidden): - await ctx.send(Texts('utils', ctx).get("Unable to find the message"), - delete_after=5) + await ctx.send( + Texts('utils', ctx).get("Unable to find the message"), + delete_after=5) @_delete.command(name='from', aliases=['to', 'in']) async def _delete_from(self, ctx: commands.Context, @@ -223,8 +230,9 @@ class Admin(commands.Cog): message_id) await message.delete() except (discord.errors.NotFound, discord.errors.Forbidden): - await ctx.send(Texts('utils', ctx).get("Unable to find the message"), - delete_after=5) + await ctx.send( + Texts('utils', ctx).get("Unable to find the message"), + delete_after=5) """---------------------------------------------------------------------""" @@ -394,17 +402,18 @@ class Admin(commands.Cog): @commands.command(name='language', aliases=['lang', 'langue', 'langage']) async def _language(self, ctx: commands.Context, locale: str): - available = self.bot.engine\ - .query(Lang.value)\ - .filter(Lang.key == 'available')\ - .one()[0]\ + available = self.bot.engine \ + .query(Lang.value) \ + .filter(Lang.key == 'available') \ + .one()[0] \ .split(', ') 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: - current = self.bot.engine\ - .query(Lang)\ + current = self.bot.engine \ + .query(Lang) \ .filter(Lang.key == str(ctx.guild.id)) if current.count() > 0: @@ -416,7 +425,8 @@ class Admin(commands.Cog): self.bot.engine.add(new_row) 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): diff --git a/cogs/basics.py b/cogs/basics.py index ce1263b..78cb5c3 100644 --- a/cogs/basics.py +++ b/cogs/basics.py @@ -55,7 +55,8 @@ class Basics(commands.Cog): file_amount += 1 with open(file_dir, "r", encoding="utf-8") as 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 return total, file_amount @@ -68,7 +69,7 @@ class Basics(commands.Cog): with proc.oneshot(): mem = proc.memory_full_info() e = discord.Embed( - title=f"{Texts('basics', ctx).get('Information about TuxBot')}", + title=Texts('basics', ctx).get('Information about TuxBot'), color=0x89C4F9) e.add_field( @@ -129,7 +130,7 @@ class Basics(commands.Cog): name=f"__:link: {Texts('basics', ctx).get('Links')}__", value="[tuxbot.gnous.eu](https://tuxbot.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 ) diff --git a/cogs/poll.py b/cogs/poll.py index ac13044..3006dc3 100644 --- a/cogs/poll.py +++ b/cogs/poll.py @@ -1,22 +1,133 @@ +from typing import Union + +import discord +import bcrypt from discord.ext import commands from bot import TuxBot 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): 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']) - async def _poll(self, ctx): - """ - todo: refer to readme.md - """ + async def _poll(self, ctx: commands.Context): + if ctx.invoked_subcommand is None: + ... + + @_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): - bot.add_cog(Poll(bot)) + bot.add_cog(Polls(bot)) diff --git a/cogs/utility.py b/cogs/utility.py index 03bcc65..c090675 100644 --- a/cogs/utility.py +++ b/cogs/utility.py @@ -5,6 +5,7 @@ import discord from discord.ext import commands from bot import TuxBot import socket +from socket import AF_INET6 from .utils.lang import Texts @@ -23,53 +24,60 @@ class Utility(commands.Cog): await ctx.trigger_typing() - if ip_type in ('v6', 'ipv6'): - try: - ip = socket.getaddrinfo(addr, None, socket.AF_INET6)[1][4][0] - except socket.gaierror: - return await ctx.send( - Texts('utility', ctx).get('ipv6 not available')) - else: - ip = socket.gethostbyname(addr) - - async with self.bot.session.get(f"http://ip-api.com/json/{ip}") as s: - response: dict = await s.json() - - if response.get('status') == 'success': - e = discord.Embed( - title=f"{Texts('utility', ctx).get('Information for')} " - f"``{addr}`` *`({response.get('query')})`*", - color=0x5858d7 - ) - - e.add_field( - name=Texts('utility', ctx).get('Belongs to :'), - value=response.get('org', 'N/A'), - inline=False - ) - - e.add_field( - name=Texts('utility', ctx).get('Is located at :'), - value=response.get('city', 'N/A'), - inline=True - ) - - e.add_field( - name="Region :", - value=f"{response.get('regionName', 'N/A')} " - f"({response.get('country', 'N/A')})", - inline=True - ) - - e.set_thumbnail( - url=f"https://www.countryflags.io/" - f"{response.get('countryCode')}/flat/64.png") - - await ctx.send(embed=e) + try: + if ip_type in ('v6', 'ipv6'): + try: + ip = socket.getaddrinfo(addr, None, AF_INET6)[1][4][0] + except socket.gaierror: + return await ctx.send( + Texts('utility', ctx).get('ipv6 not available')) else: - await ctx.send( - content=f"{Texts('utility', ctx).get('info not available')}" - f"``{response.get('query')}``") + ip = socket.gethostbyname(addr) + + async with self.bot.session.get(f"http://ip-api.com/json/{ip}") \ + as s: + response: dict = await s.json() + + if response.get('status') == 'success': + e = discord.Embed( + title=f"{Texts('utility', ctx).get('Information for')}" + f" ``{addr}`` *`({response.get('query')})`*", + color=0x5858d7 + ) + + e.add_field( + name=Texts('utility', ctx).get('Belongs to :'), + value=response.get('org', 'N/A'), + inline=False + ) + + e.add_field( + name=Texts('utility', ctx).get('Is located at :'), + value=response.get('city', 'N/A'), + inline=True + ) + + e.add_field( + name="Region :", + value=f"{response.get('regionName', 'N/A')} " + f"({response.get('country', 'N/A')})", + inline=True + ) + + e.set_thumbnail( + url=f"https://www.countryflags.io/" + f"{response.get('countryCode')}/flat/64.png") + + await ctx.send(embed=e) + else: + await ctx.send( + content=f"{Texts('utility', ctx).get('info not available')}" + f"``{response.get('query')}``") + + except Exception: + await ctx.send( + f"{Texts('utility', ctx).get('Cannot connect to host')} {addr}" + ) """---------------------------------------------------------------------""" @@ -78,9 +86,10 @@ class Utility(commands.Cog): if (addr.startswith('http') or addr.startswith('ftp')) is not True: addr = f"http://{addr}" + await ctx.trigger_typing() + try: async with self.bot.session.get(addr) as s: - await ctx.trigger_typing() e = discord.Embed( title=f"{Texts('utility', ctx).get('Headers of')} {addr}", color=0xd75858 @@ -95,9 +104,10 @@ class Utility(commands.Cog): e.add_field(name=key, value=value, inline=True) await ctx.send(embed=e) - except aiohttp.client_exceptions.ClientConnectorError: - await ctx.send(f"{Texts('utility', ctx).get('Cannot connect to host')} " - f"{addr}") + except aiohttp.client_exceptions.ClientError: + await ctx.send( + f"{Texts('utility', ctx).get('Cannot connect to host')} {addr}" + ) """---------------------------------------------------------------------""" diff --git a/cogs/utils/emotes.py b/cogs/utils/emotes.py new file mode 100644 index 0000000..c8febda --- /dev/null +++ b/cogs/utils/emotes.py @@ -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) \ No newline at end of file diff --git a/cogs/utils/models/__init__.py b/cogs/utils/models/__init__.py new file mode 100644 index 0000000..2527050 --- /dev/null +++ b/cogs/utils/models/__init__.py @@ -0,0 +1,3 @@ +from .lang import Lang +from .warn import Warn +from .poll import Poll \ No newline at end of file diff --git a/cogs/utils/models/lang.py b/cogs/utils/models/lang.py index bec56be..52aa037 100644 --- a/cogs/utils/models/lang.py +++ b/cogs/utils/models/lang.py @@ -4,7 +4,7 @@ Base = declarative_base() class Lang(Base): - __tablename__ = 'lang' + __tablename__ = 'langs' key = Column(String, primary_key=True) value = Column(String) diff --git a/cogs/utils/models/poll.py b/cogs/utils/models/poll.py new file mode 100644 index 0000000..aa30ece --- /dev/null +++ b/cogs/utils/models/poll.py @@ -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 "" % \ + (self.id, self.message_id, self.poll, + self.is_anonymous, self.responses) diff --git a/cogs/utils/models/warn.py b/cogs/utils/models/warn.py index 7cf3ef5..171a378 100644 --- a/cogs/utils/models/warn.py +++ b/cogs/utils/models/warn.py @@ -2,6 +2,7 @@ import datetime from sqlalchemy.ext.declarative import declarative_base from sqlalchemy import Column, Integer, String, BIGINT, TIMESTAMP + Base = declarative_base() @@ -16,5 +17,5 @@ class Warn(Base): def __repr__(self): return ""\ + "created_at='%s')>" \ % (self.server_id, self.user_id, self.reason, self.created_at) diff --git a/extras/locales/en/LC_MESSAGES/poll.mo b/extras/locales/en/LC_MESSAGES/poll.mo new file mode 100644 index 0000000000000000000000000000000000000000..e56e9c9c4f698196d1c9f23390bf6da85a3ddd45 GIT binary patch literal 363 zcmYjM!A`?43>^}u9yxQ!f!lCNCy=(VO$%!_D$;i9x=C;gn<+&jt0ZI9WJ`&UtGF+;z;ObD^tDU&O)?@Et;mRG$SiMf8a|&wjx_c zY2vNY9OedK>b)OC>bwbGSv+TC~=^h2oAWk9Vf%Ec2sx^EMkG&`o6g%<%Gsgx zD^pCpIEuy5ec)4XUTKTk-DqoP*5a4Fst>Bvw`BLKOoMi^$rms@>N+nkXh7&)wJ&wS f$nd$e-V!w$_wXL>T1M)&K-(?z>y`xmjo14HFE3?c literal 0 HcmV?d00001 diff --git a/extras/locales/en/LC_MESSAGES/poll.po b/extras/locales/en/LC_MESSAGES/poll.po new file mode 100644 index 0000000..320f5d4 --- /dev/null +++ b/extras/locales/en/LC_MESSAGES/poll.po @@ -0,0 +1,20 @@ +# SOME DESCRIPTIVE TITLE. +# Copyright (C) YEAR ORGANIZATION +# FIRST AUTHOR , 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 \n" +"Language-Team: LANGUAGE \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 "" diff --git a/extras/locales/fr/LC_MESSAGES/poll.mo b/extras/locales/fr/LC_MESSAGES/poll.mo new file mode 100644 index 0000000000000000000000000000000000000000..41187c1536ab92e558c70501ec254c1ac4482402 GIT binary patch literal 418 zcmY+AO-{ow5QPI`m1Wto@H)jIsX%H?Th%mfiO8SQv=Z!0JA|O(M2v{b2zB@-BhOR@+iRQFD{HdYtJDO3;fng+iKi0hK-mt7hLd7LlMJa6C zwk#u&z} zr{_3Yh*Nki+9|)fTSikH#@M6iIwqmty^AAe2E0}{lU&qWu1ZPJY#9V7q9H>=7E(W$ zP|r)*VlmcIk-zX9a0dLDpnyj6W$SGi1e0xHmogulp&zond)Ic%X(<#4{YO)2vkKnS zS+(VbxJ2tmE^APuWj4e8y{`5001`8?E;oh93F_Y$%AA7$ofd5HRYOqK, 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 \n" +"Language-Team: LANGUAGE \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...**" diff --git a/prefixes.json b/prefixes.json index e046253..212e423 100644 --- a/prefixes.json +++ b/prefixes.json @@ -1,5 +1,8 @@ { "280805240977227776": [ - "b." + "b!" + ], + "303633056944881686": [ + "b! " ] } \ No newline at end of file diff --git a/requirements.txt b/requirements.txt index 4b42423..aeebb77 100644 --- a/requirements.txt +++ b/requirements.txt @@ -7,4 +7,5 @@ asyncpg>=0.12.0 sqlalchemy gitpython requests -psutil \ No newline at end of file +psutil +bcrypt \ No newline at end of file