diff --git a/data/__init__.py b/data/__init__.py index f7424a4..6b94bc3 100644 --- a/data/__init__.py +++ b/data/__init__.py @@ -1 +1,3 @@ -"""Data""" \ No newline at end of file +""" + Data +""" diff --git a/data/db/__init__.py b/data/db/__init__.py new file mode 100644 index 0000000..ae2cd1e --- /dev/null +++ b/data/db/__init__.py @@ -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) + +Base.metadata.create_all(engine) diff --git a/data/db/base.py b/data/db/base.py new file mode 100644 index 0000000..b8d0a38 --- /dev/null +++ b/data/db/base.py @@ -0,0 +1,8 @@ +""" + Models declarative base +""" +from sqlalchemy.orm import DeclarativeBase + +# pylint: disable=too-few-public-methods +class Base(DeclarativeBase): + pass diff --git a/data/db/models.py b/data/db/models.py new file mode 100644 index 0000000..01471e6 --- /dev/null +++ b/data/db/models.py @@ -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 diff --git a/main.py b/main.py index 3c5c0c8..245a0ca 100644 --- a/main.py +++ b/main.py @@ -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() diff --git a/module/commands/__init__.py b/module/commands/__init__.py index d688acf..40e1434 100644 --- a/module/commands/__init__.py +++ b/module/commands/__init__.py @@ -1 +1,7 @@ -"""Commands""" +""" + Commands +""" +from .start import start +from .help import help +from .report import report +from .register import register_conv_handler diff --git a/module/commands/help.py b/module/commands/help.py new file mode 100644 index 0000000..f127720 --- /dev/null +++ b/module/commands/help.py @@ -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 + ) diff --git a/module/commands/help_cmd.py b/module/commands/help_cmd.py deleted file mode 100644 index efeeab7..0000000 --- a/module/commands/help_cmd.py +++ /dev/null @@ -1,16 +0,0 @@ -"""/help command""" -from telegram import Update -from telegram.ext import CallbackContext - -from module.data.constants import HELP_CMD_TEXT - -def help_cmd(update: Update, context: CallbackContext) -> 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 - """ - context.bot.sendMessage( - chat_id=update.message.chat_id, text=HELP_CMD_TEXT) diff --git a/module/commands/register.py b/module/commands/register.py new file mode 100644 index 0000000..97e8932 --- /dev/null +++ b/module/commands/register.py @@ -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" + 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)] + ) diff --git a/module/commands/report.py b/module/commands/report.py index 1f96fdc..9441e5c 100644 --- a/module/commands/report.py +++ b/module/commands/report.py @@ -1,15 +1,17 @@ -"""/report command""" +""" + /report command +""" from telegram import Update -from telegram.ext import CallbackContext +from telegram.ext import ContextTypes +async def report(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None: + """ + Called by the /report command + Sends a report to the admin group -def report(update: Update, context: CallbackContext) -> None: - """Called by the /report command - Sends a report to the admin group - - Args: - update: update event - context: context passed by the handler + Args: + update: update event + context: context passed by the handler """ # post(update, context) print(update, context) diff --git a/module/commands/start.py b/module/commands/start.py index cc4bce7..32de20a 100644 --- a/module/commands/start.py +++ b/module/commands/start.py @@ -1,17 +1,19 @@ """/start command""" from telegram import Update -from telegram.ext import CallbackContext +from telegram.ext import ContextTypes from module.data import START_CMD_TEXT -def start(update: Update, context: CallbackContext) -> None: - """Called by the /start command - Sends a welcome message +async def start(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None: + """ + Called by the /start command + Sends a welcome message - Args: - update: update event - context: context passed by the handler + Args: + update: update event + context: context passed by the handler """ - context.bot.sendMessage( - chat_id=update.message.chat_id, text=START_CMD_TEXT + await context.bot.send_message( + chat_id=update.effective_chat.id, + text=START_CMD_TEXT ) diff --git a/module/data/__init__.py b/module/data/__init__.py index 549fdd1..d61fe1d 100644 --- a/module/data/__init__.py +++ b/module/data/__init__.py @@ -3,5 +3,5 @@ REPORT, HELP, HELP_CMD_TEXT, - START_CMD_TEXT + START_CMD_TEXT, ) diff --git a/module/data/constants.py b/module/data/constants.py index 9a2244c..0efc49f 100644 --- a/module/data/constants.py +++ b/module/data/constants.py @@ -2,7 +2,10 @@ START_CMD_TEXT = "Benvenuto! Questo bot è stato realizzato dagli studenti del Corso di Laurea in Informatica" -HELP_CMD_TEXT = """📬 /report Fornisce la possibilità di poter inviare una segnalazione agli sviluppatori riguardante qualsiasi disservizio""" +HELP_CMD_TEXT = '\n'.join(( + "📬 /report Fornisce la possibilità di poter inviare una segnalazione agli sviluppatori riguardante qualsiasi disservizio", + "🔑 /register Permette di effettuare la registrazione al sistema" +)) REPORT = "Segnalazioni Rappresentanti 📬" HELP = "Help ❔" diff --git a/requirements.txt b/requirements.txt index 9a71bfb..73dca0a 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,2 +1,3 @@ -python-telegram-bot==13.5 -pyyaml +python-telegram-bot==20.6 +pyyaml==6.0.1 +SQLAlchemy==2.0 \ No newline at end of file