Skip to content
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
wants to merge 21 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion data/__init__.py
Original file line number Diff line number Diff line change
@@ -1 +1,3 @@
"""Data"""
"""
Data
"""
16 changes: 16 additions & 0 deletions data/db/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
"""
Initialize the database engine + base model
"""
import os

from sqlalchemy import create_engine
from sqlalchemy.orm import sessionmaker
#: Base has to be imported from models to create tables, otherwise no tables
#: will be created since the models don't exist at the time of creation (line 16)
from .models import Base

db_path = os.path.join(os.path.dirname(os.path.realpath(__file__)), "db.sqlite3")
engine = create_engine("sqlite:///" + db_path)
Session = sessionmaker(engine)
Comment on lines +12 to +14
Copy link
Member

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?)

Copy link
Author

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?

Copy link
Member

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


Base.metadata.create_all(engine)
8 changes: 8 additions & 0 deletions data/db/base.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
"""
Models declarative base
"""
from sqlalchemy.orm import DeclarativeBase

# pylint: disable=too-few-public-methods
class Base(DeclarativeBase):
pass
26 changes: 26 additions & 0 deletions data/db/models.py
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 .base 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
69 changes: 40 additions & 29 deletions main.py
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()
8 changes: 7 additions & 1 deletion module/commands/__init__.py
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
19 changes: 19 additions & 0 deletions module/commands/help.py
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
)
16 changes: 0 additions & 16 deletions module/commands/help_cmd.py

This file was deleted.

205 changes: 205 additions & 0 deletions module/commands/register.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,205 @@
"""
/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
END = ConversationHandler.END

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:
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:
"""
Checks if the user isn't already registered.

Args:
update: Update event
context: context passed by the handler

Returns:
The next state of the conversation
"""
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 State.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:
"""
Checks if the OTP sent to the email is valid.

Args:
update: Update event
context: context passed by the handler

Returns:
The next state of the conversation
"""
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 State.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

async def invalid_email(update: Update, context: ContextTypes.DEFAULT_TYPE) -> State:
"""
Handles invalid email

Args:
update: Update event
context: context passed by the handler

Returns:
The next state of the conversation
"""
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:
"""
Handles invalid OTP

Args:
update: Update event
context: context passed by the handler

Returns:
The next state of the conversation
"""
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) -> State:
"""
Handles invalid email

Args:
update: Update event
context: context passed by the handler

Returns:
The next state of the conversation
"""
context.bot.send_message(
chat_id=update.effective_chat.id,
text="Registrazione annullata!"
)

return State.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.
(should match the regex)

Returns:
ConversationHandler: the created handler
"""
email_regex = re.compile(r"^[a-z]+\.[a-z]+@studium\.unict\.it$")
otp_regex = re.compile(r"^\d{6}$")

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)]
)
Loading