From 940f2414aeb27a12ea4d25d008d3e6436e5e1342 Mon Sep 17 00:00:00 2001 From: R1D3R175 Date: Fri, 10 Nov 2023 09:26:05 +0100 Subject: [PATCH 01/21] chore: add `SQLAlchemy` to `requirements.txt` --- requirements.txt | 1 + 1 file changed, 1 insertion(+) diff --git a/requirements.txt b/requirements.txt index 9a71bfb..bc578fa 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,2 +1,3 @@ python-telegram-bot==13.5 pyyaml +SQLAlchemy \ No newline at end of file From fa5248ea82c4698fff39cf3845b28ec17c5707f5 Mon Sep 17 00:00:00 2001 From: R1D3R175 Date: Fri, 10 Nov 2023 09:26:23 +0100 Subject: [PATCH 02/21] feat: initialize ORM components --- data/db/__init__.py | 9 +++++++++ 1 file changed, 9 insertions(+) create mode 100644 data/db/__init__.py diff --git a/data/db/__init__.py b/data/db/__init__.py new file mode 100644 index 0000000..7fc3e40 --- /dev/null +++ b/data/db/__init__.py @@ -0,0 +1,9 @@ +""" + Initialize the database engine + base model +""" +from sqlalchemy import create_engine +from sqlalchemy.orm import DeclarativeBase + +engine = create_engine("sqlite://db.sqlite3") +class Base(DeclarativeBase): + pass \ No newline at end of file From cc42bc2ef316728fa5e0a40969fce2ee883a99f1 Mon Sep 17 00:00:00 2001 From: R1D3R175 Date: Fri, 10 Nov 2023 09:26:45 +0100 Subject: [PATCH 03/21] feat: define `User` ORM class --- data/db/models.py | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) create mode 100644 data/db/models.py diff --git a/data/db/models.py b/data/db/models.py new file mode 100644 index 0000000..7907158 --- /dev/null +++ b/data/db/models.py @@ -0,0 +1,21 @@ +""" + Definition of database tables +""" +from sqlalchemy.orm import Mapped, mapped_column +from sqlalchemy import String +from . import Base + +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 + - salt (str): random string used to salt the user's email (8 bytes) + - 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(80)) + salt: Mapped[str] = mapped_column(String(16)) + chat_id: Mapped[int] \ No newline at end of file From cc363b40d21e962546922b42db6d3964c6dfc568 Mon Sep 17 00:00:00 2001 From: R1D3R175 Date: Fri, 10 Nov 2023 09:27:03 +0100 Subject: [PATCH 04/21] feat: create mock `/login` command --- module/commands/login.py | 25 +++++++++++++++++++++++++ module/data/__init__.py | 3 ++- module/data/constants.py | 2 ++ 3 files changed, 29 insertions(+), 1 deletion(-) create mode 100644 module/commands/login.py diff --git a/module/commands/login.py b/module/commands/login.py new file mode 100644 index 0000000..17e2086 --- /dev/null +++ b/module/commands/login.py @@ -0,0 +1,25 @@ +""" + /login mock command to test the login system +""" +from telegram import Update +from telegram.ext import CallbackContext + +from module.data import LOGIN_CMD_TEXT + +def login(update: Update, context: CallbackContext) -> None: + """ + Called by the /login command. + + Theoretically, it should send an OTP to the student's email address + that must be validated. + If this check is successfull the user is then logged in and registered + in the database. + + Args: + update: update event + context: context passed by the handler + """ + + context.bot.sendMessage( + chat_id=update.message.chat_id, text=LOGIN_CMD_TEXT + ) \ No newline at end of file diff --git a/module/data/__init__.py b/module/data/__init__.py index 549fdd1..9539f59 100644 --- a/module/data/__init__.py +++ b/module/data/__init__.py @@ -3,5 +3,6 @@ REPORT, HELP, HELP_CMD_TEXT, - START_CMD_TEXT + START_CMD_TEXT, + LOGIN_CMD_TEXT, ) diff --git a/module/data/constants.py b/module/data/constants.py index 9a2244c..440c734 100644 --- a/module/data/constants.py +++ b/module/data/constants.py @@ -4,5 +4,7 @@ HELP_CMD_TEXT = """📬 /report Fornisce la possibilità di poter inviare una segnalazione agli sviluppatori riguardante qualsiasi disservizio""" +LOGIN_CMD_TEXT = """🔑 /login Permette di effettuare il login al sistema""" + REPORT = "Segnalazioni Rappresentanti 📬" HELP = "Help ❔" From f11c8cdb5f8889534985c69fa619944b14d580f6 Mon Sep 17 00:00:00 2001 From: R1D3R175 Date: Sat, 11 Nov 2023 19:46:48 +0100 Subject: [PATCH 05/21] feat: update to `python-telegram-bot` latest version --- main.py | 71 +++++++++++++++++++++---------------- module/commands/__init__.py | 8 ++++- module/commands/help.py | 19 ++++++++++ module/commands/help_cmd.py | 16 --------- module/commands/login.py | 14 ++++---- module/commands/report.py | 14 ++++---- module/commands/start.py | 20 ++++++----- requirements.txt | 2 +- 8 files changed, 93 insertions(+), 71 deletions(-) create mode 100644 module/commands/help.py delete mode 100644 module/commands/help_cmd.py diff --git a/main.py b/main.py index 3c5c0c8..8e5f49f 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 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("login", "procedura di autenticazione") ] - 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), + ] -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").build() + add_commands(app) + add_handlers(app) + app.run_polling() if __name__ == "__main__": - main() + main() \ No newline at end of file diff --git a/module/commands/__init__.py b/module/commands/__init__.py index d688acf..6f1a909 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 .login import login 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/login.py b/module/commands/login.py index 17e2086..3a6030f 100644 --- a/module/commands/login.py +++ b/module/commands/login.py @@ -1,12 +1,12 @@ """ - /login mock command to test the login system + /login command """ from telegram import Update -from telegram.ext import CallbackContext +from telegram.ext import ContextTypes from module.data import LOGIN_CMD_TEXT -def login(update: Update, context: CallbackContext) -> None: +async def login(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None: """ Called by the /login command. @@ -19,7 +19,7 @@ def login(update: Update, context: CallbackContext) -> None: update: update event context: context passed by the handler """ - - context.bot.sendMessage( - chat_id=update.message.chat_id, text=LOGIN_CMD_TEXT - ) \ No newline at end of file + await context.bot.send_message( + chat_id=update.effective_chat.id, + text=LOGIN_CMD_TEXT + ) diff --git a/module/commands/report.py b/module/commands/report.py index 1f96fdc..5b2fe94 100644 --- a/module/commands/report.py +++ b/module/commands/report.py @@ -2,14 +2,14 @@ from telegram import Update from telegram.ext import CallbackContext +async def report(update: Update, context: CallbackContext) -> 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/requirements.txt b/requirements.txt index bc578fa..aa6f97e 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,3 +1,3 @@ -python-telegram-bot==13.5 +python-telegram-bot pyyaml SQLAlchemy \ No newline at end of file From f796bda77f08ae4e37e0170ae50f5e02bd35c278 Mon Sep 17 00:00:00 2001 From: R1D3R175 Date: Sat, 11 Nov 2023 19:50:58 +0100 Subject: [PATCH 06/21] style: address pylint errors --- data/__init__.py | 4 +++- data/db/__init__.py | 3 ++- data/db/models.py | 3 ++- main.py | 2 +- 4 files changed, 8 insertions(+), 4 deletions(-) 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 index 7fc3e40..492cae9 100644 --- a/data/db/__init__.py +++ b/data/db/__init__.py @@ -1,9 +1,10 @@ """ Initialize the database engine + base model """ +# pylint: disable=too-few-public-methods from sqlalchemy import create_engine from sqlalchemy.orm import DeclarativeBase engine = create_engine("sqlite://db.sqlite3") class Base(DeclarativeBase): - pass \ No newline at end of file + pass diff --git a/data/db/models.py b/data/db/models.py index 7907158..2588b06 100644 --- a/data/db/models.py +++ b/data/db/models.py @@ -1,6 +1,7 @@ """ Definition of database tables """ +# pylint: disable=too-few-public-methods from sqlalchemy.orm import Mapped, mapped_column from sqlalchemy import String from . import Base @@ -18,4 +19,4 @@ class User(Base): id: Mapped[int] = mapped_column(primary_key=True, autoincrement=True) email: Mapped[str] = mapped_column(String(80)) salt: Mapped[str] = mapped_column(String(16)) - chat_id: Mapped[int] \ No newline at end of file + chat_id: Mapped[int] diff --git a/main.py b/main.py index 8e5f49f..2329a8c 100644 --- a/main.py +++ b/main.py @@ -55,4 +55,4 @@ def main(): app.run_polling() if __name__ == "__main__": - main() \ No newline at end of file + main() From 09f79933508a120e06eb68f9bac7f238db9102c6 Mon Sep 17 00:00:00 2001 From: R1D3R175 Date: Sat, 11 Nov 2023 20:05:20 +0100 Subject: [PATCH 07/21] feat: automatically hash email when constructing object --- data/db/models.py | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/data/db/models.py b/data/db/models.py index 2588b06..a55e92e 100644 --- a/data/db/models.py +++ b/data/db/models.py @@ -2,6 +2,9 @@ Definition of database tables """ # pylint: disable=too-few-public-methods +import os +import hashlib + from sqlalchemy.orm import Mapped, mapped_column from sqlalchemy import String from . import Base @@ -17,6 +20,12 @@ class User(Base): __tablename__ = "user" id: Mapped[int] = mapped_column(primary_key=True, autoincrement=True) - email: Mapped[str] = mapped_column(String(80)) + email: Mapped[str] = mapped_column(String(64)) salt: Mapped[str] = mapped_column(String(16)) chat_id: Mapped[int] + + def __init__(self, email: str, chat_id: int): + salt = os.urandom(8) + self.email = hashlib.sha256(email.encode() + salt).hexdigest() + self.salt = salt.hex() + self.chat_id = chat_id From 4f7d84e412dde220d4debd9f78b9d3e35f4cece1 Mon Sep 17 00:00:00 2001 From: R1D3R175 Date: Sat, 11 Nov 2023 20:24:43 +0100 Subject: [PATCH 08/21] fix: add code to create table fix: use absolute path to address database position --- data/db/__init__.py | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/data/db/__init__.py b/data/db/__init__.py index 492cae9..853272b 100644 --- a/data/db/__init__.py +++ b/data/db/__init__.py @@ -2,9 +2,17 @@ Initialize the database engine + base model """ # pylint: disable=too-few-public-methods +import os + from sqlalchemy import create_engine from sqlalchemy.orm import DeclarativeBase -engine = create_engine("sqlite://db.sqlite3") +db_path = os.path.join(os.path.dirname(os.path.realpath(__file__)), "db.sqlite3") +engine = create_engine("sqlite:///" + db_path) class Base(DeclarativeBase): pass + +# pylint: disable=wrong-import-position +from .models import User + +Base.metadata.create_all(engine) From 08d3ace2e7ddd325b28870fcd8e4ad0ba6810045 Mon Sep 17 00:00:00 2001 From: R1D3R175 Date: Sat, 11 Nov 2023 21:17:46 +0100 Subject: [PATCH 09/21] fix: remove salt --- data/db/models.py | 10 +++------- 1 file changed, 3 insertions(+), 7 deletions(-) diff --git a/data/db/models.py b/data/db/models.py index a55e92e..bd451a9 100644 --- a/data/db/models.py +++ b/data/db/models.py @@ -14,18 +14,14 @@ 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 - - salt (str): random string used to salt the user's email (8 bytes) - 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)) - salt: Mapped[str] = mapped_column(String(16)) - chat_id: Mapped[int] + 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): - salt = os.urandom(8) - self.email = hashlib.sha256(email.encode() + salt).hexdigest() - self.salt = salt.hex() + self.email = hashlib.sha256(email.encode()).hexdigest() self.chat_id = chat_id From 90c6fde4d1a30f1442c7403b2629c4e68731c2aa Mon Sep 17 00:00:00 2001 From: R1D3R175 Date: Sat, 11 Nov 2023 22:35:45 +0100 Subject: [PATCH 10/21] feat: add login command with db interaction --- main.py | 3 ++- module/commands/login.py | 55 ++++++++++++++++++++++++++++++++++++++-- 2 files changed, 55 insertions(+), 3 deletions(-) diff --git a/main.py b/main.py index 2329a8c..16d6a1b 100644 --- a/main.py +++ b/main.py @@ -1,7 +1,7 @@ """ main module """ -from module.commands import start, report, help +from module.commands import start, report, help, login from module.data import HELP, REPORT from telegram import BotCommand, Update @@ -43,6 +43,7 @@ async def chatid(update: Update, context: ContextTypes.DEFAULT_TYPE): MessageHandler(filters.Regex(HELP) & filters.ChatType.PRIVATE, help), CommandHandler("report", report), MessageHandler(filters.Regex(REPORT) & filters.ChatType.PRIVATE, report), + CommandHandler("login", login) ] app.add_handlers(handlers) diff --git a/module/commands/login.py b/module/commands/login.py index 3a6030f..62a3899 100644 --- a/module/commands/login.py +++ b/module/commands/login.py @@ -4,7 +4,9 @@ from telegram import Update from telegram.ext import ContextTypes -from module.data import LOGIN_CMD_TEXT +from sqlalchemy import select +from data.db import Session +from data.db.models import User async def login(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None: """ @@ -19,7 +21,56 @@ async def login(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None: update: update event context: context passed by the handler """ + args = update.message.text.strip().split()[1:] + + if len(args) != 1: + await context.bot.send_message( + chat_id=update.effective_chat.id, + text="Utilizzo sbagliato. /login " + ) + return + + email = args[0] + if not email.endswith("@studium.unict.it"): + await context.bot.send_message( + chat_id=update.effective_chat.id, + text=( + "Questo bot e' solo per gli studenti di UNICT.\n" + "Se sei uno studente controlla di aver scritto bene l'email " + "(deve finire con @studium.unict.it)" + ) + ) + return + + session = Session() + + stmt = select(User).where(User.chat_id == update.effective_chat.id) + result = session.scalars(stmt).first() + if result is None: + await context.bot.send_message( + chat_id=update.effective_chat.id, + text="Non sei registrato, procedo alla registrazione.." + ) + + session.add(User(email=email, chat_id=update.effective_chat.id)) + session.commit() + + return + + await context.bot.send_message( + chat_id=update.effective_chat.id, + text="Sei gia' registrato! Controllo se il chat_id corrisponde..." + ) + + if result.chat_id != update.effective_chat.id: + await context.bot.send_message( + chat_id=update.effective_chat.id, + text="Il chat_id non corrisponde..." + ) + + return + await context.bot.send_message( chat_id=update.effective_chat.id, - text=LOGIN_CMD_TEXT + text="Bentornato!" ) From 51e7170a50ad17b987121f910253c8ea7fbff24c Mon Sep 17 00:00:00 2001 From: R1D3R175 Date: Sat, 11 Nov 2023 22:36:34 +0100 Subject: [PATCH 11/21] fix: move command description to `/help` --- module/data/__init__.py | 1 - module/data/constants.py | 7 ++++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/module/data/__init__.py b/module/data/__init__.py index 9539f59..d61fe1d 100644 --- a/module/data/__init__.py +++ b/module/data/__init__.py @@ -4,5 +4,4 @@ HELP, HELP_CMD_TEXT, START_CMD_TEXT, - LOGIN_CMD_TEXT, ) diff --git a/module/data/constants.py b/module/data/constants.py index 440c734..898baf5 100644 --- a/module/data/constants.py +++ b/module/data/constants.py @@ -2,9 +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""" - -LOGIN_CMD_TEXT = """🔑 /login Permette di effettuare il login al sistema""" +HELP_CMD_TEXT = '\n'.join(( + "📬 /report Fornisce la possibilità di poter inviare una segnalazione agli sviluppatori riguardante qualsiasi disservizio", + "🔑 /login Permette di effettuare il login al sistema" +)) REPORT = "Segnalazioni Rappresentanti 📬" HELP = "Help ❔" From 572f6dd867dfeddb38a12721a9aa6b02b780bf67 Mon Sep 17 00:00:00 2001 From: R1D3R175 Date: Sat, 11 Nov 2023 22:37:31 +0100 Subject: [PATCH 12/21] feat: add `sessionmaker` for an easier interaction style: move pylint comments near the line causing the issue --- data/db/__init__.py | 8 +++++--- data/db/models.py | 3 +-- 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/data/db/__init__.py b/data/db/__init__.py index 853272b..a497fcb 100644 --- a/data/db/__init__.py +++ b/data/db/__init__.py @@ -1,18 +1,20 @@ """ Initialize the database engine + base model """ -# pylint: disable=too-few-public-methods import os from sqlalchemy import create_engine -from sqlalchemy.orm import DeclarativeBase +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 +# pylint: disable=wrong-import-position,cyclic-import from .models import User Base.metadata.create_all(engine) diff --git a/data/db/models.py b/data/db/models.py index bd451a9..60640ec 100644 --- a/data/db/models.py +++ b/data/db/models.py @@ -1,14 +1,13 @@ """ Definition of database tables """ -# pylint: disable=too-few-public-methods -import os 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: From 3fa095fc0322d3231ac00cb3d92ef795a5937dd0 Mon Sep 17 00:00:00 2001 From: R1D3R175 Date: Sat, 11 Nov 2023 22:38:04 +0100 Subject: [PATCH 13/21] style: remove trailing whitespace --- module/commands/login.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/module/commands/login.py b/module/commands/login.py index 62a3899..fb8f87c 100644 --- a/module/commands/login.py +++ b/module/commands/login.py @@ -22,7 +22,7 @@ async def login(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None: context: context passed by the handler """ args = update.message.text.strip().split()[1:] - + if len(args) != 1: await context.bot.send_message( chat_id=update.effective_chat.id, From 7cb5af926620b4fbc6333b5fbcc82728fc8848ea Mon Sep 17 00:00:00 2001 From: R1D3R175 Date: Sat, 11 Nov 2023 23:00:24 +0100 Subject: [PATCH 14/21] feat: add missing check for email --- module/commands/login.py | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/module/commands/login.py b/module/commands/login.py index fb8f87c..be7e65f 100644 --- a/module/commands/login.py +++ b/module/commands/login.py @@ -1,6 +1,8 @@ """ /login command """ +import hashlib + from telegram import Update from telegram.ext import ContextTypes @@ -70,6 +72,15 @@ async def login(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None: return + email_digest = hashlib.sha256(email.encode()).hexdigest() + if result.email != email_digest: + await context.bot.send_message( + chat_id=update.effective_chat.id, + text="L'email non corrisponde..." + ) + + return + await context.bot.send_message( chat_id=update.effective_chat.id, text="Bentornato!" From 3817cc61989784d62f4e45ed57f88f691bafcb35 Mon Sep 17 00:00:00 2001 From: R1D3R175 Date: Sat, 11 Nov 2023 23:03:25 +0100 Subject: [PATCH 15/21] feat: add with block to correctly handle sessions --- module/commands/login.py | 25 +++++++++++++------------ 1 file changed, 13 insertions(+), 12 deletions(-) diff --git a/module/commands/login.py b/module/commands/login.py index be7e65f..2fa0983 100644 --- a/module/commands/login.py +++ b/module/commands/login.py @@ -30,6 +30,7 @@ async def login(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None: chat_id=update.effective_chat.id, text="Utilizzo sbagliato. /login " ) + return email = args[0] @@ -42,22 +43,22 @@ async def login(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None: "(deve finire con @studium.unict.it)" ) ) - return - session = Session() + return - stmt = select(User).where(User.chat_id == update.effective_chat.id) - result = session.scalars(stmt).first() - if result is None: - await context.bot.send_message( - chat_id=update.effective_chat.id, - text="Non sei registrato, procedo alla registrazione.." - ) + with Session() as session: + stmt = select(User).where(User.chat_id == update.effective_chat.id) + result = session.scalars(stmt).first() + if result is None: + await context.bot.send_message( + chat_id=update.effective_chat.id, + text="Non sei registrato, procedo alla registrazione.." + ) - session.add(User(email=email, chat_id=update.effective_chat.id)) - session.commit() + session.add(User(email=email, chat_id=update.effective_chat.id)) + session.commit() - return + return await context.bot.send_message( chat_id=update.effective_chat.id, From a8c7f9dcce118c6a88bec870806dbfaf2bb36663 Mon Sep 17 00:00:00 2001 From: R1D3R175 Date: Sat, 11 Nov 2023 23:05:10 +0100 Subject: [PATCH 16/21] fix: change data check message --- module/commands/login.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/module/commands/login.py b/module/commands/login.py index 2fa0983..e747998 100644 --- a/module/commands/login.py +++ b/module/commands/login.py @@ -30,7 +30,7 @@ async def login(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None: chat_id=update.effective_chat.id, text="Utilizzo sbagliato. /login " ) - + return email = args[0] @@ -62,7 +62,7 @@ async def login(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None: await context.bot.send_message( chat_id=update.effective_chat.id, - text="Sei gia' registrato! Controllo se il chat_id corrisponde..." + text="Sei gia' registrato! Controllo se i dati corrispondono..." ) if result.chat_id != update.effective_chat.id: From 661108d1ed8fcdc104680eb8a39c7ac676c5f6a5 Mon Sep 17 00:00:00 2001 From: R1D3R175 Date: Sat, 11 Nov 2023 23:08:46 +0100 Subject: [PATCH 17/21] fix: change context type hinting --- module/commands/report.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/module/commands/report.py b/module/commands/report.py index 5b2fe94..9441e5c 100644 --- a/module/commands/report.py +++ b/module/commands/report.py @@ -1,8 +1,10 @@ -"""/report command""" +""" + /report command +""" from telegram import Update -from telegram.ext import CallbackContext +from telegram.ext import ContextTypes -async def report(update: Update, context: CallbackContext) -> None: +async def report(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None: """ Called by the /report command Sends a report to the admin group From 096ea3f35682b789d527228aa14a9651a464ee3e Mon Sep 17 00:00:00 2001 From: R1D3R175 Date: Mon, 13 Nov 2023 14:51:01 +0100 Subject: [PATCH 18/21] chore: specify dependencies version --- requirements.txt | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/requirements.txt b/requirements.txt index aa6f97e..73dca0a 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,3 +1,3 @@ -python-telegram-bot -pyyaml -SQLAlchemy \ No newline at end of file +python-telegram-bot==20.6 +pyyaml==6.0.1 +SQLAlchemy==2.0 \ No newline at end of file From 70f5ba59c153e2bafeff19a169c27bea8cdc9a36 Mon Sep 17 00:00:00 2001 From: R1D3R175 Date: Mon, 13 Nov 2023 16:23:01 +0100 Subject: [PATCH 19/21] feat: make registration procedure a conversation fix: use ApplicationBuilder.post_init() to add_commands refactor: rename login to register --- main.py | 9 +- module/commands/__init__.py | 2 +- module/commands/login.py | 88 ------------------ module/commands/register.py | 174 ++++++++++++++++++++++++++++++++++++ module/data/constants.py | 2 +- 5 files changed, 180 insertions(+), 95 deletions(-) delete mode 100644 module/commands/login.py create mode 100644 module/commands/register.py diff --git a/main.py b/main.py index 16d6a1b..245a0ca 100644 --- a/main.py +++ b/main.py @@ -1,7 +1,7 @@ """ main module """ -from module.commands import start, report, help, login +from module.commands import start, report, help, register_conv_handler from module.data import HELP, REPORT from telegram import BotCommand, Update @@ -18,7 +18,7 @@ async def add_commands(app: Application) -> None: BotCommand("start", "messaggio di benvenuto"), BotCommand("help", "ricevi aiuto sui comandi"), BotCommand("report", "segnala un problema"), - BotCommand("login", "procedura di autenticazione") + BotCommand("register", "procedura di registrazione") ] await app.bot.set_my_commands(commands) @@ -43,14 +43,13 @@ async def chatid(update: Update, context: ContextTypes.DEFAULT_TYPE): MessageHandler(filters.Regex(HELP) & filters.ChatType.PRIVATE, help), CommandHandler("report", report), MessageHandler(filters.Regex(REPORT) & filters.ChatType.PRIVATE, report), - CommandHandler("login", login) + register_conv_handler() ] app.add_handlers(handlers) def main(): - app = ApplicationBuilder().token("TOKEN").build() - add_commands(app) + app = ApplicationBuilder().token("TOKEN").post_init(add_commands).build() add_handlers(app) app.run_polling() diff --git a/module/commands/__init__.py b/module/commands/__init__.py index 6f1a909..40e1434 100644 --- a/module/commands/__init__.py +++ b/module/commands/__init__.py @@ -4,4 +4,4 @@ from .start import start from .help import help from .report import report -from .login import login +from .register import register_conv_handler diff --git a/module/commands/login.py b/module/commands/login.py deleted file mode 100644 index e747998..0000000 --- a/module/commands/login.py +++ /dev/null @@ -1,88 +0,0 @@ -""" - /login command -""" -import hashlib - -from telegram import Update -from telegram.ext import ContextTypes - -from sqlalchemy import select -from data.db import Session -from data.db.models import User - -async def login(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None: - """ - Called by the /login command. - - Theoretically, it should send an OTP to the student's email address - that must be validated. - If this check is successfull the user is then logged in and registered - in the database. - - Args: - update: update event - context: context passed by the handler - """ - args = update.message.text.strip().split()[1:] - - if len(args) != 1: - await context.bot.send_message( - chat_id=update.effective_chat.id, - text="Utilizzo sbagliato. /login " - ) - - return - - email = args[0] - if not email.endswith("@studium.unict.it"): - await context.bot.send_message( - chat_id=update.effective_chat.id, - text=( - "Questo bot e' solo per gli studenti di UNICT.\n" - "Se sei uno studente controlla di aver scritto bene l'email " - "(deve finire con @studium.unict.it)" - ) - ) - - return - - with Session() as session: - stmt = select(User).where(User.chat_id == update.effective_chat.id) - result = session.scalars(stmt).first() - if result is None: - await context.bot.send_message( - chat_id=update.effective_chat.id, - text="Non sei registrato, procedo alla registrazione.." - ) - - session.add(User(email=email, chat_id=update.effective_chat.id)) - session.commit() - - return - - await context.bot.send_message( - chat_id=update.effective_chat.id, - text="Sei gia' registrato! Controllo se i dati corrispondono..." - ) - - if result.chat_id != update.effective_chat.id: - await context.bot.send_message( - chat_id=update.effective_chat.id, - text="Il chat_id non corrisponde..." - ) - - return - - email_digest = hashlib.sha256(email.encode()).hexdigest() - if result.email != email_digest: - await context.bot.send_message( - chat_id=update.effective_chat.id, - text="L'email non corrisponde..." - ) - - return - - await context.bot.send_message( - chat_id=update.effective_chat.id, - text="Bentornato!" - ) diff --git a/module/commands/register.py b/module/commands/register.py new file mode 100644 index 0000000..c21865d --- /dev/null +++ b/module/commands/register.py @@ -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" + 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: + context.bot.send_message( + chat_id=update.effective_chat.id, + text="Registrazione annullata!" + ) + + return ConversationHandler.END + + 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/data/constants.py b/module/data/constants.py index 898baf5..0efc49f 100644 --- a/module/data/constants.py +++ b/module/data/constants.py @@ -4,7 +4,7 @@ HELP_CMD_TEXT = '\n'.join(( "📬 /report Fornisce la possibilità di poter inviare una segnalazione agli sviluppatori riguardante qualsiasi disservizio", - "🔑 /login Permette di effettuare il login al sistema" + "🔑 /register Permette di effettuare la registrazione al sistema" )) REPORT = "Segnalazioni Rappresentanti 📬" From 96c4d25d2387a94a84601efae1f423c78a01701b Mon Sep 17 00:00:00 2001 From: R1D3R175 Date: Tue, 21 Nov 2023 17:11:35 +0100 Subject: [PATCH 20/21] refactor: change `Base` class position --- data/db/__init__.py | 12 ++++-------- data/db/base.py | 8 ++++++++ data/db/models.py | 2 +- 3 files changed, 13 insertions(+), 9 deletions(-) create mode 100644 data/db/base.py diff --git a/data/db/__init__.py b/data/db/__init__.py index a497fcb..ae2cd1e 100644 --- a/data/db/__init__.py +++ b/data/db/__init__.py @@ -4,17 +4,13 @@ import os from sqlalchemy import create_engine -from sqlalchemy.orm import DeclarativeBase, sessionmaker +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) -# pylint: disable=too-few-public-methods -class Base(DeclarativeBase): - pass - -# pylint: disable=wrong-import-position,cyclic-import -from .models import User - 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 index 60640ec..01471e6 100644 --- a/data/db/models.py +++ b/data/db/models.py @@ -5,7 +5,7 @@ from sqlalchemy.orm import Mapped, mapped_column from sqlalchemy import String -from . import Base +from .base import Base # pylint: disable=too-few-public-methods class User(Base): From 4105280d0025e08267a5f178f9437aa0c39357ab Mon Sep 17 00:00:00 2001 From: R1D3R175 Date: Tue, 21 Nov 2023 17:16:58 +0100 Subject: [PATCH 21/21] refactor: change docstrings, move out invalid data handlers --- module/commands/register.py | 101 +++++++++++++++++++++++------------- 1 file changed, 66 insertions(+), 35 deletions(-) diff --git a/module/commands/register.py b/module/commands/register.py index c21865d..97e8932 100644 --- a/module/commands/register.py +++ b/module/commands/register.py @@ -18,6 +18,7 @@ class State(Enum): """ EMAIL = 1 OTP = 2 + END = ConversationHandler.END async def register_entry(update: Update, context: ContextTypes.DEFAULT_TYPE) -> State: """ @@ -30,7 +31,7 @@ async def register_entry(update: Update, context: ContextTypes.DEFAULT_TYPE) -> context: context passed by the handler Returns: - State: the next state of the conversation + The next state of the conversation """ await context.bot.send_message( @@ -40,7 +41,7 @@ async def register_entry(update: Update, context: ContextTypes.DEFAULT_TYPE) -> return State.EMAIL -async def email_checker(update: Update, context: ContextTypes.DEFAULT_TYPE) -> State | int: +async def email_checker(update: Update, context: ContextTypes.DEFAULT_TYPE) -> State: """ Checks if the user isn't already registered. @@ -49,8 +50,7 @@ async def email_checker(update: Update, context: ContextTypes.DEFAULT_TYPE) -> S context: context passed by the handler Returns: - State: the next state of the conversation - int: constant ConversationHandler.END (if the user is already registered) + The next state of the conversation """ email = update.message.text.strip() email_digest = hashlib.sha256(email.encode()).hexdigest() @@ -65,7 +65,7 @@ async def email_checker(update: Update, context: ContextTypes.DEFAULT_TYPE) -> S text="Sei gia' registrato!" ) - return ConversationHandler.END + return State.END context.user_data["email"] = email context.user_data["otp"] = "123456" @@ -78,7 +78,7 @@ async def email_checker(update: Update, context: ContextTypes.DEFAULT_TYPE) -> S return State.OTP -async def otp_checker(update: Update, context: ContextTypes.DEFAULT_TYPE) -> State | int: +async def otp_checker(update: Update, context: ContextTypes.DEFAULT_TYPE) -> State: """ Checks if the OTP sent to the email is valid. @@ -87,8 +87,7 @@ async def otp_checker(update: Update, context: ContextTypes.DEFAULT_TYPE) -> Sta 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) + The next state of the conversation """ if context.user_data["tries"] >= 3: await context.bot.send_message( @@ -96,7 +95,7 @@ async def otp_checker(update: Update, context: ContextTypes.DEFAULT_TYPE) -> Sta text="Hai esaurito il numero di tentativi, riprova piu' tardi" ) - return ConversationHandler.END + return State.END otp = update.message.text.strip() if otp != context.user_data["otp"]: @@ -120,43 +119,75 @@ async def otp_checker(update: Update, context: ContextTypes.DEFAULT_TYPE) -> Sta return ConversationHandler.END -def register_conv_handler() -> ConversationHandler: +async def invalid_email(update: Update, context: ContextTypes.DEFAULT_TYPE) -> State: """ - Creates the /register ConversationHandler. + Handles invalid email - 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. + Args: + update: Update event + context: context passed by the handler Returns: - ConversationHandler: the created handler + The next state of the conversation """ - email_regex = re.compile(r"^[a-z]+\.[a-z]+@studium\.unict\.it$") - otp_regex = re.compile(r"^\d{6}$") + await context.bot.send_message( + chat_id=update.effective_chat.id, + text="Email non valida, riprova" + ) - 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 - return State.EMAIL +async def invalid_otp(update: Update, context: ContextTypes.DEFAULT_TYPE) -> State: + """ + Handles invalid OTP - 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" - ) + Args: + update: Update event + context: context passed by the handler - return State.OTP + Returns: + The next state of the conversation + """ + await context.bot.send_message( + chat_id=update.effective_chat.id, + text="OTP non valido, riprova" + ) - async def cancel(update: Update, context: ContextTypes.DEFAULT_TYPE) -> int: - context.bot.send_message( - chat_id=update.effective_chat.id, - text="Registrazione annullata!" - ) + 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 - return ConversationHandler.END + 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)],