proiect finalizat

This commit is contained in:
andrei-mihnea-cerbu
2024-01-13 12:59:19 +02:00
parent 54542da299
commit 67b7a241d7
53 changed files with 321 additions and 19 deletions
+5
View File
@@ -0,0 +1,5 @@
#!/bin/bash
source venv/bin/activate
cd messenger_app
uvicorn app.main:app --reload
+3
View File
@@ -0,0 +1,3 @@
"""
The 'api' package contains the logic behind the endpoints of the backend. The main version of the api is 'v1'.
"""
@@ -6,6 +6,10 @@ from .signup import signup
router = APIRouter() router = APIRouter()
"""
The 'auth' packages contains the router configuration for the 'Authentication' endpoints.
"""
@router.post("/login") @router.post("/login")
async def login_router(login_info: Login): async def login_router(login_info: Login):
@@ -4,6 +4,15 @@ from database.queries.user import get_user_by_email
async def login(login_info: Login): async def login(login_info: Login):
"""
Verify user credentials
Args:
login_info (Login)
Returns:
dict : full user information besides password
"""
result = get_user_by_email(login_info.email) result = get_user_by_email(login_info.email)
if not result: if not result:
raise HTTPException(status_code=404, detail='User not in system.') raise HTTPException(status_code=404, detail='User not in system.')
@@ -4,6 +4,15 @@ from database.queries.user import get_user_by_email, create_user_db
async def signup(user: User): async def signup(user: User):
"""
Verify user credentials
Args:
user (User)
Returns:
dict : full user information besides password
"""
result = get_user_by_email(user.email) result = get_user_by_email(user.email)
if result: if result:
raise HTTPException(status_code=409, detail='User already exists.') raise HTTPException(status_code=409, detail='User already exists.')
@@ -4,6 +4,10 @@ from .get_conversation import get_conversation
from .get_conversation_url import get_conversation_url from .get_conversation_url import get_conversation_url
from app.models import Message from app.models import Message
'''
The 'messages' packages contains the router configuration for the 'Messages' endpoints.
'''
router = APIRouter() router = APIRouter()
@@ -3,6 +3,15 @@ from database.queries.messages import find_conversation_db
async def get_conversation(conversation_url: str): async def get_conversation(conversation_url: str):
"""
Retrieves the full context of the conversation
Args:
conversation_url (str)
Returns:
HTTP Status
"""
conversation = find_conversation_db(conversation_url) conversation = find_conversation_db(conversation_url)
if conversation is None: if conversation is None:
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Conversation not found") raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Conversation not found")
@@ -4,6 +4,16 @@ from fastapi import HTTPException
async def get_conversation_url(id_user1: str, id_user2: str): async def get_conversation_url(id_user1: str, id_user2: str):
"""
Retrieves the conversation identifier
Args:
id_user1 (str)
id_user2 (str)
Returns:
(str) : conversation_url
"""
if get_user_by_id(id_user1) is None or get_user_by_id(id_user2) is None: if get_user_by_id(id_user1) is None or get_user_by_id(id_user2) is None:
raise HTTPException(status_code=404, detail="Users not in the system.") raise HTTPException(status_code=404, detail="Users not in the system.")
@@ -4,6 +4,15 @@ from database.queries.messages import send_message_db
async def send(message: Message): async def send(message: Message):
"""
Stores message in MongoDB
Args:
message (Message)
Returns:
HTTP Status
"""
success = send_message_db(message) success = send_message_db(message)
if success: if success:
@@ -5,6 +5,10 @@ from .update import update_user
from .delete import delete_user from .delete import delete_user
from .get_all import get_all_users from .get_all import get_all_users
'''
The 'messages' packages contains the router configuration for the 'Messages' endpoints.
'''
router = APIRouter() router = APIRouter()
@@ -4,6 +4,15 @@ from database.queries.user import get_user_by_email, create_user_db
async def create_user(user: User): async def create_user(user: User):
"""
Creates a user
Args:
user (User)
Returns:
complete user information, including id, if successful
"""
result = get_user_by_email(user.email) result = get_user_by_email(user.email)
if result: if result:
raise HTTPException(status_code=409, detail='User already exists.') raise HTTPException(status_code=409, detail='User already exists.')
+10 -1
View File
@@ -2,7 +2,16 @@ from fastapi import HTTPException
from database.queries.user import get_user_by_id, delete_user_db from database.queries.user import get_user_by_id, delete_user_db
async def delete_user(user_id: int): async def delete_user(user_id):
"""
Deletes a user
Args:
user_id (str)
Returns:
complete user information, including id, if successful
"""
user = get_user_by_id(user_id) user = get_user_by_id(user_id)
if user is None: if user is None:
raise HTTPException(status_code=404, detail='User not found') raise HTTPException(status_code=404, detail='User not found')
@@ -3,4 +3,13 @@ from database.queries.user import get_all_users_db
async def get_all_users(): async def get_all_users():
users = get_all_users_db() users = get_all_users_db()
"""
Gets all users from database
Args:
Nan
Returns:
A list of users, if exists
"""
return users return users
+3
View File
@@ -0,0 +1,3 @@
"""
The 'app' package contains de main API object plus all the routing. It also contains the 'models' for the API requests.
"""
+7
View File
@@ -1,3 +1,10 @@
from .user import User from .user import User
from .message import Message from .message import Message
from .login import Login from .login import Login
'''
The 'models' package contains the schematics for the API body structures. In more detail:
- 'Login' model is used for the auth/login endpoint
- 'User' model is used for the auto/signup and users endpoints
- 'Message' model is used for the messages/send endpoint
'''
-1
View File
@@ -1,5 +1,4 @@
from pydantic import BaseModel from pydantic import BaseModel
from datetime import datetime
class Message(BaseModel): class Message(BaseModel):
@@ -1,4 +1,11 @@
from .connection import MONGODB_CONFIG, MongoDbHandler from .connection import MONGODB_CONFIG, MongoDbHandler
mongo_db_handler = MongoDbHandler(**MONGODB_CONFIG) mongo_db_handler = MongoDbHandler(**MONGODB_CONFIG)
mongo_db_handler.connect() mongo_db_handler.connect()
'''
The 'mongodb_database' package contains the definition of the 'MongoDbHandler' class which connects to the MongoDB
database used for storing the messages between the clients.
The object 'mongo_db_handler' is created inside this file and can be used throughout the lifetime of the application
'''
@@ -1,4 +1,12 @@
from .connection import POSTGRESQL_CONFIG, PostgreSQLHandler from .connection import POSTGRESQL_CONFIG, PostgreSQLHandler
postgresql_db_handler = PostgreSQLHandler(**POSTGRESQL_CONFIG) postgresql_db_handler = PostgreSQLHandler(**POSTGRESQL_CONFIG)
postgresql_db_handler.connect() postgresql_db_handler.connect()
'''
The 'postgresql_database' package contains the definition of the 'PostgreSQlHandler' class which connects to the MongoDB
database used for storing information about the users and the existing chats
The object 'postgresql_db_handler' is created inside this file and can be used throughout the lifetime
of the application
'''
@@ -0,0 +1,8 @@
'''
The 'queries' package contains functions which are used by the API to execute database operations. This package contains
implementations for 'messages' and 'users' requirements. The list of the functions is:
- create_conversation_db()
- find_conversation_db()
- get_conversation_url_db()
- send_message_db
'''
@@ -5,6 +5,16 @@ from datetime import datetime
def create_conversation_db(user1_id, user2_id): def create_conversation_db(user1_id, user2_id):
"""
Creates the conversation identifier for two users.
Args:
user1_id (str): ID of first user.
user1_id (str): ID of second user.
Returns:
str: Conversation identifier/URL
"""
current_datetime = datetime.now().strftime("%Y%m%d%H%M%S") current_datetime = datetime.now().strftime("%Y%m%d%H%M%S")
unique_id = str(uuid.uuid4()).replace('-', '') unique_id = str(uuid.uuid4()).replace('-', '')
conversation_url = f"{current_datetime}_{unique_id}" conversation_url = f"{current_datetime}_{unique_id}"
@@ -20,6 +30,15 @@ def create_conversation_db(user1_id, user2_id):
def find_conversation_db(conversation_url): def find_conversation_db(conversation_url):
"""
Retrieves the conversation from MongoDB
Args:
conversation_url (str): conversation identifier
Returns:
dic: conversation
"""
collection = mongo_db_handler.get_collection() collection = mongo_db_handler.get_collection()
query = {'name': conversation_url} query = {'name': conversation_url}
result_set = collection.find_one(query) result_set = collection.find_one(query)
+51
View File
@@ -2,6 +2,15 @@ from database import postgresql_db_handler
def get_user_by_id(user_id): def get_user_by_id(user_id):
"""
Creates the conversation identifier for two users.
Args:
user_id (str): user id
Returns:
User: information about the user if exists
"""
query = """ query = """
SELECT * FROM users WHERE id = %s; SELECT * FROM users WHERE id = %s;
""" """
@@ -15,6 +24,15 @@ def get_user_by_id(user_id):
def get_user_by_email(email): def get_user_by_email(email):
"""
Creates the conversation identifier for two users.
Args:
email (str): email of the user
Returns:
User: information about the user if exists
"""
query = """ query = """
SELECT * FROM users WHERE email = %s; SELECT * FROM users WHERE email = %s;
""" """
@@ -28,6 +46,17 @@ def get_user_by_email(email):
def create_user_db(username, password, email): def create_user_db(username, password, email):
"""
Creates used based on provided information
Args:
username (str)
password (str)
email (str)
Returns:
Nothing
"""
query = """ query = """
INSERT INTO users (username, password, email) INSERT INTO users (username, password, email)
VALUES (%s, %s, %s); VALUES (%s, %s, %s);
@@ -37,6 +66,19 @@ def create_user_db(username, password, email):
def update_user_db(user_id, new_username, new_password, new_email): def update_user_db(user_id, new_username, new_password, new_email):
"""
Updates user based on the new information
Args:
user_id (int)
new_username (str)
new_password (str)
new_email (str)
Returns:
Nothing
"""
query = """ query = """
UPDATE users UPDATE users
SET username = %s, password = %s, email = %s SET username = %s, password = %s, email = %s
@@ -48,6 +90,15 @@ def update_user_db(user_id, new_username, new_password, new_email):
def delete_user_db(user_id): def delete_user_db(user_id):
"""
Deletes a user
Args:
user_id: int
Returns:
Nothing
"""
query = """ query = """
DELETE FROM users WHERE id = %s; DELETE FROM users WHERE id = %s;
""" """
+5
View File
@@ -0,0 +1,5 @@
#!/bin/bash
source venv/bin/activate
cd messenger_gui
python main.py
+10 -2
View File
@@ -1,2 +1,10 @@
from .main import MainApplication from .main_application import MainApplication
app = MainApplication() app = MainApplication()
'''
The 'app' package contains all the UI and network components to run the application'. This package contains the
following packages:
- http (tools to make HTTP requests to the API)
- models (schematics for the HTTP requests and user info validation)
- views (UI design)
'''
+6
View File
@@ -1,3 +1,9 @@
from .request_builder import RequestBuilder from .request_builder import RequestBuilder
request_builder = RequestBuilder() request_builder = RequestBuilder()
"""
The 'http' package contains the tools for making HTTP requests to the backend. This package contains:
- request_builder: class through which the request is build
- request_handler: object resulted from the builder and can be used to make requests.
"""
+18 -5
View File
@@ -2,6 +2,18 @@ from .request_handler import _HttpRequestHandler
class RequestBuilder: class RequestBuilder:
"""
A Builder for HTTP Request class
Attributes:
__url (str): URL at which the request is made
__headers (dict):
__body (dict):
__params (dict): Query params
__method (str): HTTP method (GET, POST, ...)
"""
def __init__(self): def __init__(self):
self.__url = "" self.__url = ""
self.__headers = {} self.__headers = {}
@@ -9,26 +21,27 @@ class RequestBuilder:
self.__params = None self.__params = None
self.__method = "GET" self.__method = "GET"
def set_url(self, url=str): def set_url(self, url):
self.__url = url self.__url = url
return self return self
def set_headers(self, headers=dict): def set_headers(self, headers):
self.__headers = headers self.__headers = headers
return self return self
def set_body(self, body=dict): def set_body(self, body):
self.__body = body self.__body = body
return self return self
def set_params(self, params=dict): def set_params(self, params):
self.__params = params self.__params = params
return self return self
def set_method(self, method=str): def set_method(self, method):
self.__method = method self.__method = method
return self return self
def build(self): def build(self):
"""Based on the set parameters it returns a Request Handler object"""
return _HttpRequestHandler(url=self.__url, method=self.__method, body=self.__body, return _HttpRequestHandler(url=self.__url, method=self.__method, body=self.__body,
params=self.__params, headers=self.__headers) params=self.__params, headers=self.__headers)
+12 -2
View File
@@ -3,16 +3,26 @@ import requests
class _HttpRequestHandler: class _HttpRequestHandler:
"""
A Builder for HTTP Request class
Attributes:
url (str): URL at which the request is made
headers (dict):
body (dict):
params (dict): Query params
method (str): HTTP method (GET, POST, ...)
"""
def __init__(self, url, method="GET", params=None, headers=None, body=None): def __init__(self, url, method="GET", params=None, headers=None, body=None):
self.url = url self.url = url
self.method = method self.method = method
self.headers = headers self.headers = headers
print(body)
self.body = json.dumps(body) self.body = json.dumps(body)
self.params = params self.params = params
def make_request(self): def make_request(self):
print(self.body) """Make HTTP request and returns the answer"""
response = requests.request(method=self.method, url=self.url, response = requests.request(method=self.method, url=self.url,
params=self.params, data=self.body, headers=self.headers) params=self.params, data=self.body, headers=self.headers)
return response return response
@@ -1,6 +1,5 @@
import json import json
import tkinter as tk import tkinter as tk
from PIL import Image, ImageTk
from app.views.login import LoginView from app.views.login import LoginView
from app.views.initial import InitialView from app.views.initial import InitialView
from app.views.signup import SignupView from app.views.signup import SignupView
@@ -52,6 +51,3 @@ class MainApplication(tk.Tk):
self.current_logged_user['id'] = data['info']['id'] self.current_logged_user['id'] = data['info']['id']
self.current_logged_user['username'] = data['info']['username'] self.current_logged_user['username'] = data['info']['username']
self.current_logged_user['email'] = data['info']['email'] self.current_logged_user['email'] = data['info']['email']
+6
View File
@@ -0,0 +1,6 @@
"""
The 'models' package contains schematics for the HTTP requests and validations for the information provided by the
user during the authentication. The package contains:
- http_models
- view_models
"""
@@ -1,2 +1,6 @@
from .login import LoginRequestModel from .login import LoginRequestModel
from .signup import SignupRequestModel from .signup import SignupRequestModel
"""
The 'http_models' package contains the schematics for the body data when making HTTP requests
"""
@@ -1,2 +1,7 @@
from .login import LoginModel from .login import LoginModel
from .signup import SignupModel from .signup import SignupModel
"""
The 'view models' package contains the validations for the information the user provides during the authentication
(login/sign up)
"""
+8
View File
@@ -0,0 +1,8 @@
"""
The 'views' package contains all the UI implementation of the app. The app contains the following views:
- InitialView
- LoginView
- SingUpView
- MainPageView
- ChatView
"""
+13
View File
@@ -5,6 +5,10 @@ from app.views.message import MessageView
class ChatWindow(tk.Toplevel): class ChatWindow(tk.Toplevel):
"""
This view is opened in a separate window and takes care of the communication between the two users. It receives
both the users' information and retrieves the conversation_url.
"""
def __init__(self, master=None, user_data=None, current_logged_user=None): def __init__(self, master=None, user_data=None, current_logged_user=None):
super().__init__(master) super().__init__(master)
@@ -48,6 +52,7 @@ class ChatWindow(tk.Toplevel):
} }
def create_widgets(self, conversation): def create_widgets(self, conversation):
"""Creates the UI of this view"""
# Create a frame for displaying messages # Create a frame for displaying messages
self.messages_frame = tk.Frame(self) self.messages_frame = tk.Frame(self)
self.messages_frame.pack() self.messages_frame.pack()
@@ -70,6 +75,10 @@ class ChatWindow(tk.Toplevel):
self.send_button.pack(side=tk.RIGHT) self.send_button.pack(side=tk.RIGHT)
def update_chat(self): def update_chat(self):
"""
This function is scheduled each second to make a request to the API to retrieve the latest chat between the
users
"""
self.messages_frame.destroy() self.messages_frame.destroy()
self.messages_frame = tk.Frame(self) self.messages_frame = tk.Frame(self)
self.messages_frame.pack() self.messages_frame.pack()
@@ -85,7 +94,9 @@ class ChatWindow(tk.Toplevel):
pass pass
def send_message(self): def send_message(self):
"""At the submit button, the text is sent to the database"""
message = self.entry_widget.get() message = self.entry_widget.get()
self.define_send_message_params(message, "text") self.define_send_message_params(message, "text")
if not message: if not message:
return return
@@ -101,6 +112,7 @@ class ChatWindow(tk.Toplevel):
self.update_chat() self.update_chat()
def get_conversation_url(self): def get_conversation_url(self):
"""Fetches the conversation_url"""
request_builder.set_url(self.url) request_builder.set_url(self.url)
request_builder.set_body(self.body) request_builder.set_body(self.body)
request_builder.set_headers(self.headers) request_builder.set_headers(self.headers)
@@ -113,6 +125,7 @@ class ChatWindow(tk.Toplevel):
return data['conversation_url'] return data['conversation_url']
def get_conversation(self): def get_conversation(self):
"""Fetches the conversation between the users"""
request_builder.set_url(self.url) request_builder.set_url(self.url)
request_builder.set_body(self.body) request_builder.set_body(self.body)
request_builder.set_headers(self.headers) request_builder.set_headers(self.headers)
+3
View File
@@ -2,6 +2,9 @@ import tkinter as tk
class InitialView(tk.Frame): class InitialView(tk.Frame):
"""
This class is the first rendered view of the application. This offers the options to Login or sign Up
"""
def __init__(self, master=None): def __init__(self, master=None):
super().__init__(master) super().__init__(master)
self.create_widgets() self.create_widgets()
+11
View File
@@ -14,6 +14,10 @@ def hash_password(password):
class LoginView(tk.Frame): class LoginView(tk.Frame):
"""
This view takes care of the validation of the user in the system. This class receives the login information and
validates them. The response is shown to the user.
"""
def __init__(self, master=None): def __init__(self, master=None):
super().__init__(master) super().__init__(master)
self.form_frame = None self.form_frame = None
@@ -27,6 +31,9 @@ class LoginView(tk.Frame):
self.params = None self.params = None
def create_widgets(self): def create_widgets(self):
"""
Creates the layout of the view
"""
# Create a frame to hold the entry fields and labels # Create a frame to hold the entry fields and labels
self.form_frame = tk.Frame(self) self.form_frame = tk.Frame(self)
@@ -71,6 +78,10 @@ class LoginView(tk.Frame):
self.master.change_to_initial_view() self.master.change_to_initial_view()
def on_login(self): def on_login(self):
"""
After login button pressed, the validation of the provided information starts and will redirect the
user to next view if everything is accepted
"""
# Retrieve values from entry fields # Retrieve values from entry fields
email = self.email_entry.get() email = self.email_entry.get()
password = self.password_entry.get() password = self.password_entry.get()
+6
View File
@@ -6,6 +6,10 @@ from app.views.user_frame import UserFrame
class MainPageView(tk.Frame): class MainPageView(tk.Frame):
"""
This is the view from which the user will open a chat with anyone which is registered in the system. Each chat is
shown as a box with a photo, the username and the email.
"""
def __init__(self, master=None): def __init__(self, master=None):
super().__init__(master) super().__init__(master)
self.form_frame = None self.form_frame = None
@@ -20,6 +24,7 @@ class MainPageView(tk.Frame):
self.body = None self.body = None
def create_widgets(self): def create_widgets(self):
"""Takes care of the creation of the UI"""
# Create a main frame to center the user frame and button frame # Create a main frame to center the user frame and button frame
main_frame = tk.Frame(self.master) main_frame = tk.Frame(self.master)
main_frame.pack(expand=True) main_frame.pack(expand=True)
@@ -57,6 +62,7 @@ class MainPageView(tk.Frame):
self.master.change_to_initial_view() self.master.change_to_initial_view()
def open_chat(self, user_data): def open_chat(self, user_data):
"""Opens a chat in a different window"""
# Pass both the clicked user data and the current logged user info to ChatWindow # Pass both the clicked user data and the current logged user info to ChatWindow
chat_window = ChatWindow(self, user_data, self.master.current_logged_user) chat_window = ChatWindow(self, user_data, self.master.current_logged_user)
chat_window.mainloop() # Main loop for the new window chat_window.mainloop() # Main loop for the new window
+3
View File
@@ -7,6 +7,9 @@ import base64
class MessageView(tk.Frame): class MessageView(tk.Frame):
"""
This view is the message that appears in main windows of the chat. This is configured to hold text and images.
"""
def __init__(self, master, message, current_logged_user, user_data, *args, **kwargs): def __init__(self, master, message, current_logged_user, user_data, *args, **kwargs):
super().__init__(master, *args, **kwargs) super().__init__(master, *args, **kwargs)
self.current_logged_user = current_logged_user self.current_logged_user = current_logged_user
+9
View File
@@ -14,6 +14,10 @@ def hash_password(password):
class SignupView(tk.Frame): class SignupView(tk.Frame):
"""
This view takes care of the registration of the user in the system. This class receives the sing up
information and validates them. The response is shown to the user.
"""
def __init__(self, master=None): def __init__(self, master=None):
super().__init__(master) super().__init__(master)
self.form_frame = None self.form_frame = None
@@ -27,6 +31,7 @@ class SignupView(tk.Frame):
self.params = None self.params = None
def create_widgets(self): def create_widgets(self):
"""Creates the layout of the view"""
# Create a frame to hold the entry fields and labels # Create a frame to hold the entry fields and labels
self.form_frame = tk.Frame(self) self.form_frame = tk.Frame(self)
@@ -78,6 +83,10 @@ class SignupView(tk.Frame):
self.master.change_to_initial_view() self.master.change_to_initial_view()
def on_signup(self): def on_signup(self):
"""
After sign up button pressed, the validation of the provided information starts and will redirect the
user to next view if everything is accepted
"""
# Retrieve values from entry fields # Retrieve values from entry fields
username = self.username_entry.get() username = self.username_entry.get()
email = self.email_entry.get() email = self.email_entry.get()
+3 -2
View File
@@ -3,11 +3,13 @@ from PIL import Image, ImageTk
class UserFrame(tk.Frame): class UserFrame(tk.Frame):
"""
This class takes care of the aspect of the user blocks from the MainPageView.
"""
def __init__(self, master, user_data, current_logged_user, click_callback, *args, **kwargs): def __init__(self, master, user_data, current_logged_user, click_callback, *args, **kwargs):
super().__init__(master, *args, **kwargs) super().__init__(master, *args, **kwargs)
self.current_logged_user = current_logged_user self.current_logged_user = current_logged_user
print(user_data)
self.user_data = { self.user_data = {
'id': user_data[0], 'id': user_data[0],
@@ -45,7 +47,6 @@ class UserFrame(tk.Frame):
# Bind the click event to the user frame # Bind the click event to the user frame
user_frame.bind("<Button-1>", lambda event, user_data=self.user_data, current_logged_user=self.current_logged_user: self.on_user_click(user_data, current_logged_user)) user_frame.bind("<Button-1>", lambda event, user_data=self.user_data, current_logged_user=self.current_logged_user: self.on_user_click(user_data, current_logged_user))
# Pack the user frame # Pack the user frame
user_frame.pack(side=tk.LEFT, padx=10, pady=10) user_frame.pack(side=tk.LEFT, padx=10, pady=10)