-
Notifications
You must be signed in to change notification settings - Fork 2
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
feat: implement database to store users email #8
Open
R1D3R175
wants to merge
21
commits into
UNICT-DMI:main
Choose a base branch
from
R1D3R175:main
base: main
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Open
Changes from 19 commits
Commits
Show all changes
21 commits
Select commit
Hold shift + click to select a range
940f241
chore: add `SQLAlchemy` to `requirements.txt`
R1D3R175 fa5248e
feat: initialize ORM components
R1D3R175 cc42bc2
feat: define `User` ORM class
R1D3R175 cc363b4
feat: create mock `/login` command
R1D3R175 f11c8cd
feat: update to `python-telegram-bot` latest version
R1D3R175 f796bda
style: address pylint errors
R1D3R175 09f7993
feat: automatically hash email when constructing object
R1D3R175 4f7d84e
fix: add code to create table
R1D3R175 08d3ace
fix: remove salt
R1D3R175 90c6fde
feat: add login command with db interaction
R1D3R175 51e7170
fix: move command description to `/help`
R1D3R175 572f6dd
feat: add `sessionmaker` for an easier interaction
R1D3R175 3fa095f
style: remove trailing whitespace
R1D3R175 7cb5af9
feat: add missing check for email
R1D3R175 3817cc6
feat: add with block to correctly handle sessions
R1D3R175 a8c7f9d
fix: change data check message
R1D3R175 661108d
fix: change context type hinting
R1D3R175 096ea3f
chore: specify dependencies version
R1D3R175 70f5ba5
feat: make registration procedure a conversation
R1D3R175 96c4d25
refactor: change `Base` class position
R1D3R175 4105280
refactor: change docstrings, move out invalid data handlers
R1D3R175 File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1 +1,3 @@ | ||
"""Data""" | ||
""" | ||
Data | ||
""" |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,20 @@ | ||
""" | ||
Initialize the database engine + base model | ||
""" | ||
import os | ||
|
||
from sqlalchemy import create_engine | ||
from sqlalchemy.orm import DeclarativeBase, sessionmaker | ||
|
||
db_path = os.path.join(os.path.dirname(os.path.realpath(__file__)), "db.sqlite3") | ||
engine = create_engine("sqlite:///" + db_path) | ||
Session = sessionmaker(engine) | ||
|
||
# pylint: disable=too-few-public-methods | ||
class Base(DeclarativeBase): | ||
pass | ||
|
||
# pylint: disable=wrong-import-position,cyclic-import | ||
from .models import User | ||
R1D3R175 marked this conversation as resolved.
Show resolved
Hide resolved
R1D3R175 marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
||
Base.metadata.create_all(engine) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,26 @@ | ||
""" | ||
Definition of database tables | ||
""" | ||
import hashlib | ||
|
||
from sqlalchemy.orm import Mapped, mapped_column | ||
from sqlalchemy import String | ||
from . import Base | ||
|
||
# pylint: disable=too-few-public-methods | ||
class User(Base): | ||
""" | ||
User table, maps the following fields: | ||
- id (int): primary key, autoincrement | ||
- email (str): hexdigest of salted user's email hashed with sha256 | ||
- chat_id (int): id of the chat the user is in | ||
""" | ||
__tablename__ = "user" | ||
|
||
id: Mapped[int] = mapped_column(primary_key=True, autoincrement=True) | ||
email: Mapped[str] = mapped_column(String(64), unique=True) | ||
chat_id: Mapped[int] = mapped_column(unique=True) | ||
|
||
def __init__(self, email: str, chat_id: int): | ||
self.email = hashlib.sha256(email.encode()).hexdigest() | ||
self.chat_id = chat_id |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,47 +1,58 @@ | ||
"""main module""" | ||
from telegram import BotCommand | ||
from telegram.ext import CommandHandler, MessageHandler, Updater, Dispatcher, Filters | ||
|
||
from module.commands import start, report, help_cmd | ||
""" | ||
main module | ||
""" | ||
from module.commands import start, report, help, register_conv_handler | ||
from module.data import HELP, REPORT | ||
|
||
def add_commands(up: Updater) -> None: | ||
"""Adds list of commands with their description to the boy | ||
from telegram import BotCommand, Update | ||
from telegram.ext import filters, Application, ApplicationBuilder, CommandHandler, MessageHandler, ContextTypes | ||
|
||
async def add_commands(app: Application) -> None: | ||
""" | ||
Adds a list of commands with their description to the bot | ||
|
||
Args: | ||
up(Updater): supplied Updater | ||
Args: | ||
app (Application): the built application | ||
""" | ||
commands = [ | ||
BotCommand("start", "messaggio di benvenuto"), | ||
BotCommand("help", "ricevi aiuto sui comandi"), | ||
BotCommand("report", "segnala un problema") | ||
BotCommand("report", "segnala un problema"), | ||
BotCommand("register", "procedura di registrazione") | ||
] | ||
up.bot.set_my_commands(commands=commands) | ||
|
||
def add_handlers(dp:Dispatcher) -> None: | ||
"""Adds all the handlers the bot will react to | ||
await app.bot.set_my_commands(commands) | ||
|
||
Args: | ||
dp:suppplied Dispatcher | ||
def add_handlers(app: Application) -> None: | ||
""" | ||
Adds all the handlers to the bot | ||
|
||
dp.add_handler(CommandHandler("start", start, Filters.chat_type.private)) | ||
dp.add_handler(CommandHandler("chatid", lambda u, c: u.message.reply_text(str(u.message.chat_id)))) | ||
dp.add_handler(CommandHandler("help", help_cmd, Filters.chat_type.private)) | ||
dp.add_handler(MessageHandler(Filters.regex(HELP) & Filters.chat_type.private, help_cmd)) | ||
dp.add_handler(CommandHandler("report", report)) | ||
dp.add_handler(MessageHandler(Filters.regex(REPORT) & Filters.chat_type.private, report)) | ||
dp.add_handler(CommandHandler("chatid", lambda u, c: u.message.reply_text(str(u.message.chat_id)))) | ||
Args: | ||
app (Application): the built application | ||
""" | ||
async def chatid(update: Update, context: ContextTypes.DEFAULT_TYPE): | ||
await context.bot.send_message( | ||
chat_id=update.effective_chat.id, | ||
text=str(update.effective_chat.id) | ||
) | ||
|
||
handlers = [ | ||
CommandHandler("start", start, filters.ChatType.PRIVATE), | ||
CommandHandler("chatid", chatid), | ||
CommandHandler("help", help, filters.ChatType.PRIVATE), | ||
MessageHandler(filters.Regex(HELP) & filters.ChatType.PRIVATE, help), | ||
CommandHandler("report", report), | ||
MessageHandler(filters.Regex(REPORT) & filters.ChatType.PRIVATE, report), | ||
register_conv_handler() | ||
] | ||
|
||
def main() -> None: | ||
"""Main function""" | ||
updater = Updater() | ||
add_commands(updater) | ||
add_handlers(updater.dispatcher) | ||
app.add_handlers(handlers) | ||
|
||
updater.start_polling() | ||
updater.idle() | ||
def main(): | ||
app = ApplicationBuilder().token("TOKEN").post_init(add_commands).build() | ||
add_handlers(app) | ||
|
||
app.run_polling() | ||
|
||
if __name__ == "__main__": | ||
main() |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1 +1,7 @@ | ||
"""Commands""" | ||
""" | ||
Commands | ||
""" | ||
from .start import start | ||
from .help import help | ||
from .report import report | ||
from .register import register_conv_handler |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,19 @@ | ||
"""/help command""" | ||
from telegram import Update | ||
from telegram.ext import ContextTypes | ||
|
||
from module.data.constants import HELP_CMD_TEXT | ||
|
||
async def help(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None: | ||
""" | ||
Called by the /help command | ||
Sends a list of the avaible bot's commands | ||
|
||
Args: | ||
update: update event | ||
context: context passed by the handler | ||
""" | ||
await context.bot.send_message( | ||
chat_id=update.effective_chat.id, | ||
text=HELP_CMD_TEXT | ||
) |
This file was deleted.
Oops, something went wrong.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,174 @@ | ||
""" | ||
/register command | ||
""" | ||
import re | ||
import hashlib | ||
from enum import Enum | ||
|
||
from telegram import Update | ||
from telegram.ext import ContextTypes, MessageHandler, CommandHandler, ConversationHandler, filters | ||
|
||
from sqlalchemy import select | ||
from data.db import Session | ||
from data.db.models import User | ||
|
||
class State(Enum): | ||
""" | ||
States of the register procedure | ||
""" | ||
EMAIL = 1 | ||
OTP = 2 | ||
|
||
async def register_entry(update: Update, context: ContextTypes.DEFAULT_TYPE) -> State: | ||
""" | ||
Called by the /register command. | ||
|
||
Starts the registration procedure. | ||
|
||
Args: | ||
update: Update event | ||
context: context passed by the handler | ||
|
||
Returns: | ||
State: the next state of the conversation | ||
""" | ||
|
||
await context.bot.send_message( | ||
chat_id=update.effective_chat.id, | ||
text="Invia la tua email studium" | ||
) | ||
|
||
return State.EMAIL | ||
|
||
async def email_checker(update: Update, context: ContextTypes.DEFAULT_TYPE) -> State | int: | ||
""" | ||
Checks if the user isn't already registered. | ||
|
||
Args: | ||
update: Update event | ||
context: context passed by the handler | ||
|
||
Returns: | ||
State: the next state of the conversation | ||
int: constant ConversationHandler.END (if the user is already registered) | ||
""" | ||
email = update.message.text.strip() | ||
email_digest = hashlib.sha256(email.encode()).hexdigest() | ||
|
||
with Session() as session: | ||
stmt = select(User).where((User.chat_id == update.effective_chat.id) | (User.email == email_digest)) | ||
result = session.scalars(stmt).first() | ||
|
||
if result is not None: | ||
await context.bot.send_message( | ||
chat_id=update.effective_chat.id, | ||
text="Sei gia' registrato!" | ||
) | ||
|
||
return ConversationHandler.END | ||
|
||
context.user_data["email"] = email | ||
context.user_data["otp"] = "123456" | ||
R1D3R175 marked this conversation as resolved.
Show resolved
Hide resolved
|
||
context.user_data["tries"] = 0 | ||
|
||
await context.bot.send_message( | ||
chat_id=update.effective_chat.id, | ||
text="Invia l'OTP che ti e' stato inviato all'email da te indicata" | ||
) | ||
|
||
return State.OTP | ||
|
||
async def otp_checker(update: Update, context: ContextTypes.DEFAULT_TYPE) -> State | int: | ||
""" | ||
Checks if the OTP sent to the email is valid. | ||
|
||
Args: | ||
update: Update event | ||
context: context passed by the handler | ||
|
||
Returns: | ||
State: returns State.OTP if the OTP wasn't correct. | ||
int: constant ConversationHandler.END (if the OTP was correct or too many wrong tries) | ||
""" | ||
if context.user_data["tries"] >= 3: | ||
await context.bot.send_message( | ||
chat_id=update.effective_chat.id, | ||
text="Hai esaurito il numero di tentativi, riprova piu' tardi" | ||
) | ||
|
||
return ConversationHandler.END | ||
|
||
otp = update.message.text.strip() | ||
if otp != context.user_data["otp"]: | ||
context.user_data["tries"] += 1 | ||
|
||
await context.bot.send_message( | ||
chat_id=update.effective_chat.id, | ||
text="OTP non corretto, controlla la tua mail" | ||
) | ||
|
||
return State.OTP | ||
|
||
with Session() as session: | ||
session.add(User=context.user_data["email"], chat_id=update.effective_chat.id) | ||
session.commit() | ||
|
||
await context.bot.send_message( | ||
chat_id=update.effective_chat.id, | ||
text="Registrazione completata!" | ||
) | ||
|
||
return ConversationHandler.END | ||
|
||
def register_conv_handler() -> ConversationHandler: | ||
""" | ||
Creates the /register ConversationHandler. | ||
|
||
States of the command: | ||
- State.EMAIL: Waits for a text message containing the email (should match the regex) | ||
- State.OTP: Waits for a text message containing the OTP sent to the email address. | ||
|
||
Returns: | ||
ConversationHandler: the created handler | ||
""" | ||
email_regex = re.compile(r"^[a-z]+\.[a-z]+@studium\.unict\.it$") | ||
otp_regex = re.compile(r"^\d{6}$") | ||
|
||
async def invalid_email(update: Update, context: ContextTypes.DEFAULT_TYPE) -> State: | ||
await context.bot.send_message( | ||
chat_id=update.effective_chat.id, | ||
text="Email non valida, riprova" | ||
) | ||
|
||
return State.EMAIL | ||
|
||
async def invalid_otp(update: Update, context: ContextTypes.DEFAULT_TYPE) -> State: | ||
await context.bot.send_message( | ||
chat_id=update.effective_chat.id, | ||
text="OTP non valido, riprova" | ||
) | ||
|
||
return State.OTP | ||
|
||
async def cancel(update: Update, context: ContextTypes.DEFAULT_TYPE) -> int: | ||
R1D3R175 marked this conversation as resolved.
Show resolved
Hide resolved
|
||
context.bot.send_message( | ||
chat_id=update.effective_chat.id, | ||
text="Registrazione annullata!" | ||
) | ||
|
||
return ConversationHandler.END | ||
R1D3R175 marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
||
return ConversationHandler( | ||
entry_points=[CommandHandler("register", register_entry)], | ||
states={ | ||
State.EMAIL: [ | ||
MessageHandler(filters.Regex(email_regex), email_checker), | ||
MessageHandler(filters.TEXT & ~filters.Regex(email_regex), invalid_email) | ||
], | ||
State.OTP: [ | ||
MessageHandler(filters.Regex(otp_regex), otp_checker), | ||
MessageHandler(filters.TEXT & ~filters.Regex(otp_regex), invalid_otp) | ||
] | ||
}, | ||
fallbacks=[CommandHandler("cancel", cancel)] | ||
) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Evita sempre di mettere direttive a questo livello, in quanto verranno eseguite nel momento in cui il modulo viene importato. Senza saperlo sto provocando un side-effect (creando un file).
Inoltre rendi piu' delicati i test (cosa succede se voglio usare un altro path invece di db.sqlite?)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Per cui come dovrei fare?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Essenzialmente l'ideale sarebbe rinchiudere tutto dentro una funzione da chiamare al momento del bisogno, possibilmente anche dal main.
Non c'è bisogno di avere una sessione globale: puoi crearne una nuova ogni volta che ti serve, è praticamente equivalente. Leggi questa risposta da uno degli sviluppatori per approfondire.
Se proprio pensi di aver bisogno di uno stato globale, un Singleton potrebbe fare al caso tuo. Almeno puoi controllare il momento in cui viene inizializzato, piuttosto che fare tutto nel momento il cui il modulo viene importato