diff --git a/doc/images/admin_panel.png b/doc/images/admin_panel.png new file mode 100644 index 0000000..8ab7ac0 Binary files /dev/null and b/doc/images/admin_panel.png differ diff --git a/doc/images/chat_interface.png b/doc/images/chat_interface.png new file mode 100644 index 0000000..2900cf1 Binary files /dev/null and b/doc/images/chat_interface.png differ diff --git a/doc/images/login_screen.png b/doc/images/login_screen.png new file mode 100644 index 0000000..16d7808 Binary files /dev/null and b/doc/images/login_screen.png differ diff --git a/doc/images/register_screen.png b/doc/images/register_screen.png new file mode 100644 index 0000000..6444c07 Binary files /dev/null and b/doc/images/register_screen.png differ diff --git a/doc/main.pdf b/doc/main.pdf new file mode 100644 index 0000000..b55d2bf Binary files /dev/null and b/doc/main.pdf differ diff --git a/doc/main.tex b/doc/main.tex new file mode 100644 index 0000000..9e62859 --- /dev/null +++ b/doc/main.tex @@ -0,0 +1,399 @@ +\documentclass[12pt,a4paper]{article} + +% Polskie znaki i kodowanie +\usepackage[utf8]{inputenc} +\usepackage[T1]{fontenc} +\usepackage[polish]{babel} + +% Marginesy +\usepackage{geometry} +\geometry{a4paper, margin=2.5cm} + +% Zawijanie tekstu i mikrotypografia +\usepackage{microtype} +\usepackage[hyphens]{url} +\usepackage{xurl} % pozwala łamać URL-e w dowolnym miejscu +\emergencystretch=3em % zapobiega overfull hbox w uzasadnionych przypadkach +% \setlength{\parfillskip}{0pt plus 1fil} % naturalny fill na końcu akapitu +\setlength{\headheight}{13.59999pt} + +% Grafika, tabele i kolory +\usepackage{graphicx} +\usepackage{float} +\usepackage{xcolor} +\usepackage{booktabs} + +% Drzewo katalogów +\usepackage{dirtree} + +% Zaawansowane nagłówki i stopki +\usepackage{fancyhdr} +\pagestyle{fancy} +\fancyhf{} +\fancyhead[L]{\small\textbf{Technologie Webowe}} +\fancyhead[R]{\small\textit{Discord Clone - Dokumentacja}} +\fancyfoot[C]{\thepage} +\renewcommand{\headrulewidth}{0.4pt} +\renewcommand{\footrulewidth}{0.4pt} + +% Podświetlanie składni (Kod źródłowy) +\usepackage{listings} +\definecolor{codegreen}{rgb}{0,0.6,0} +\definecolor{codegray}{rgb}{0.5,0.5,0.5} +\definecolor{codepurple}{rgb}{0.58,0,0.82} +\definecolor{backcolour}{rgb}{0.95,0.95,0.92} +\lstdefinestyle{mystyle}{ + backgroundcolor=\color{backcolour}, + commentstyle=\color{codegreen}, + keywordstyle=\color{magenta}, + numberstyle=\tiny\color{codegray}, + stringstyle=\color{codepurple}, + basicstyle=\ttfamily\footnotesize, + breakatwhitespace=false, % łam linie również w środku tokenu (nie tylko przy białych znakach) + breaklines=true, % włącz automatyczne zawijanie długich linii + breakautoindent=true, % zachowaj wcięcie po złamaniu + postbreak=\mbox{\textcolor{codegray}{$\hookrightarrow$}\space}, % strzałka przy złamaniu + columns=flexible, % elastyczne kolumny zapobiegają overfull w listingach + captionpos=b, + keepspaces=true, + numbers=left, + numbersep=5pt, + showspaces=false, + showstringspaces=false, + showtabs=false, + tabsize=2, + extendedchars=true, + inputencoding=utf8, + literate={ą}{{\k{a}}}1 + {ć}{{\'c}}1 + {ę}{{\k{e}}}1 + {ł}{{\l{}}}1 + {ń}{{\'n}}1 + {ó}{{\'o}}1 + {ś}{{\'s}}1 + {ź}{{\'z}}1 + {ż}{{\.z}}1 + {Ą}{{\k{A}}}1 + {Ć}{{\'C}}1 + {Ę}{{\k{E}}}1 + {Ł}{{\L{}}}1 + {Ń}{{\'N}}1 + {Ó}{{\'O}}1 + {Ś}{{\'S}}1 + {Ź}{{\'Z}}1 + {Ż}{{\.Z}}1 +} +\lstset{style=mystyle} + +% Konfiguracja linków +\usepackage{hyperref} +\hypersetup{ + colorlinks=true, + linkcolor=blue!70!black, + filecolor=magenta, + urlcolor=blue!70!black, + pdftitle={Discord Clone - Dokumentacja}, + pdfpagemode=FullScreen, + breaklinks=true, % pozwól na zawijanie linków +} + +\begin{document} + +% Globalne ustawienie luźniejszego składania tekstu – zapobiega overfull hbox +\sloppy + +\begin{titlepage} + \centering + \vspace*{3cm} + + {\scshape\Large Przedmiot: Aplikacje internetowe w Django \par} + \vspace{1.5cm} + + {\huge\bfseries Dokumentacja Projektowa\par} + \vspace{0.5cm} + {\Large\itshape Discord Clone: Aplikacja do komunikacji w czasie rzeczywistym\par} + \vspace{2cm} + + \vfill + + {\large \today\par} +\end{titlepage} + +\newpage +\tableofcontents +\newpage + +\section{Wstęp i cel projektu} +Prezentowany projekt \textbf{,,Discord Clone''} to nowoczesna aplikacja webowa stworzona w celu dostarczenia funkcjonalności umożliwiających wieloosobową komunikację tekstową w czasie rzeczywistym, analogiczną do popularnej platformy Discord. + +Głównym celem edukacyjnym i technologicznym projektu jest zintegrowanie klasycznego frameworka opartego na protokole HTTP z zaawansowanymi mechanizmami asynchronicznymi i protokołem \textbf{WebSocket}, z zachowaniem optymalnej wydajności i uproszczonej infrastruktury. + +\subsection{Kluczowe funkcjonalności} +\begin{itemize} + \item \textbf{Autoryzacja:} Rejestracja i logowanie. Dostęp do głównej funkcjonalności jest chroniony. + \item \textbf{Zarządzanie Kanałami:} Użytkownicy z odpowiednimi uprawnieniami mogą tworzyć tematyczne kanały grupowe (tekstowe i głosowe). + \item \textbf{Role i uprawnienia:} Podział na Administratorów, Moderatorów oraz Użytkowników z dedykowanymi przywilejami (np. usuwanie obcych wiadomości przez moderatora). + \item \textbf{Czat Real-Time:} Natychmiastowe przesyłanie wiadomości bez konieczności odświeżania okna przeglądarki. + \item \textbf{Kanały Głosowe (Voice Channels):} Wieloużytkownikowe rozmowy audio w czasie rzeczywistym realizowane w architekturze peer-to-peer (P2P) z wykorzystaniem WebRTC, wspierane przez globalną usługe Cloudflare Realtime TURN do NAT-traversalu. +\end{itemize} + +\newpage +\section{Architektura systemu i wykorzystane technologie} +Aplikacja została zaprojektowana w oparciu o architekturę trójwarstwową, w której wyeliminowano konieczność stosowania osobnego serwera kolejkowego (np. Redis) na rzecz wbudowanych mechanizmów bazy PostgreSQL. + +\subsection{Warstwa Prezentacji (Klient / Frontend)} +Użytkownik komunikuje się z systemem za pośrednictwem przeglądarki internetowej. Warstwa ta renderowana jest przy pomocy \textbf{szablonów HTML/CSS (Bootstrap)}. Lekki skrypt \textbf{JavaScript} nawiązuje i utrzymuje ciągłe połączenie WebSocket z serwerem, asynchronicznie odbierając powiadomienia o nowych wiadomościach. W przypadku połączeń głosowych, frontend odpowiada za przechwytywanie dźwięku z mikrofonu (Web Audio API), realizację nawiązania połączeń WebRTC P2P (Full Mesh) oraz dynamiczną obsługę odtwarzania strumieni audio od innych użytkowników bez przerywania nawigacji dzięki technologii AJAX/Fetch (SPA Shell). + +\subsection{Warstwa Aplikacji (Backend)} +Główny trzon projektu oparty jest o język \textbf{Python} oraz framework \textbf{Django 4.2}. +Ruch sieciowy jest zarządzany przez serwer \textbf{Daphne (ASGI)}, który: +\begin{itemize} + \item \textbf{Zwykłe żądania HTTP} (np. logowanie, ładowanie widoków) przekazuje do synchronicznych widoków Django. + \item \textbf{Żądania WebSocket} routuje do asynchronicznych consumerów zdefiniowanych w \textbf{Django Channels}. +\end{itemize} +W kontekście kanałów głosowych backend pełni funkcję serwera sygnalizacyjnego (signaling server) za pośrednictwem protokołu WebSocket, pośrednicząc w wymianie ofert SDP (Session Description Protocol) i kandydatów ICE między klientami. Dodatkowo, backend komunikuje się z interfejsem API Cloudflare, pobierając krótkotrwałe, bezpieczne dane uwierzytelniające dla serwerów TURN (ważne przez 24 godziny). + +\subsection{Warstwa Danych} +Zastosowano zunifikowaną bazę danych \textbf{PostgreSQL}. Standardowe dane (użytkownicy, historia wiadomości) są obsługiwane przez mechanizm Django ORM. Dodatkowo, dzięki zastosowaniu pluginu \texttt{channels-postgres}, architektura w czasie rzeczywistym (pub/sub) wykorzystuje natywne zapytania \texttt{LISTEN/NOTIFY} z PostgreSQL, co pozwala rozgłaszać eventy WebSockets pomiędzy klientami. W bazie danych PostgreSQL przechowywany jest również stan aktywności użytkowników w kanałach głosowych za pomocą pola \texttt{current\_voice\_channel} w profilu użytkownika (\texttt{UserProfile}). Umożliwia to natychmiastowe renderowanie listy uczestników kanału głosowego bezpośrednio pod jego nazwą na liście kanałów podczas ładowania strony. + +\newpage +\section{Środowisko i instalacja} + +Do uruchomienia projektu w celach deweloperskich i testowych zastosowano konteneryzację, co eliminuje problem zależności systemowych. + +\subsection{Wymagania systemowe} +\begin{itemize} + \item \textbf{Docker} (Docker Engine) + \item \textbf{Docker Compose} + \item Połączenie z internetem (do pobrania obrazów oraz weryfikacji API Cloudflare) +\end{itemize} + +\subsection{Kroki instalacji} +\begin{enumerate} + \item \textbf{Pobranie kodu:} Sklonuj lub pobierz i rozpakuj repozytorium projektu w wybranym folderze. + \item \textbf{Konfiguracja zmiennych środowiskowych:} Utwórz plik \texttt{.env} w katalogu głównym projektu na wzór udostępnionych szablonów. Wprowadź tam wygenerowane w panelu Cloudflare dane uwierzytelniające dla usługi Cloudflare Realtime TURN: \texttt{CLOUDFLARE\_ACCOUNT\_ID}, \texttt{CLOUDFLARE\_API\_TOKEN} oraz \texttt{CLOUDFLARE\_TURN\_KEY\_ID}. Umożliwi to serwerowi dynamiczne generowanie tokenów ICE dla przeglądarek. + \item \textbf{Budowanie kontenerów:} Uruchom terminal w głównym katalogu (tam, gdzie znajduje się plik \texttt{docker-compose.yml}) i wykonaj polecenie: +\begin{lstlisting}[language=bash, style=mystyle, numbers=none] +docker-compose up --build +\end{lstlisting} + \item \textbf{Uruchomienie bazy:} Kontenery wykonają automatycznie proces migracji struktury danych (\texttt{manage.py migrate}). Zostanie także wygenerowane konto administratora. + \item Aplikacja docelowo jest dostępna pod adresem: \url{http://localhost:8000}. +\end{enumerate} + +\textbf{Domyślne dane dostępowe administratora:} +\begin{itemize} + \item Login: \texttt{admin} + \item Hasło: \texttt{admin} +\end{itemize} + +\newpage +\section{Struktura repozytorium} +Projekt zorganizowany jest wokół aplikacji modułowych typowych dla frameworka Django. Poniższe drzewo prezentuje kluczowe pliki z punktu widzenia architektonicznego. + +\vspace{0.5cm} +% Szerokość minipage dopasowana do pozostałej przestrzeni kolumny dirtree +\dirtree{% +.1 / (Katalog głowny projektu). +.2 core/. +.3 settings.py \dots\dots\dots\dots\begin{minipage}[t]{6cm}\raggedright Konfiguracja główna (baza danych, channels)\end{minipage}. +.3 asgi.py \dots\dots\dots\dots\dots\begin{minipage}[t]{6cm}\raggedright Punkt wejścia dla serwera Daphne i routing Channels\end{minipage}. +.3 urls.py \dots\dots\dots\dots\dots\begin{minipage}[t]{6cm}\raggedright Główny routing HTTP\end{minipage}. +.2 chat/. +.3 models.py \dots\dots\dots\dots\begin{minipage}[t]{6cm}\raggedright Definicje tabel DB (Użytkownicy, Kanały, Wiadomości)\end{minipage}. +.3 consumers.py \dots\dots\begin{minipage}[t]{6cm}\raggedright Asynchroniczna obsługa WebSocketów (czat, sygnalizacja WebRTC)\end{minipage}. +.3 routing.py \dots\dots\dots\dots\begin{minipage}[t]{6cm}\raggedright Definicje ścieżek URL dla połączeń ws://\end{minipage}. +.3 views.py \dots\dots\dots\dots\begin{minipage}[t]{6cm}\raggedright Widoki synchroniczne oraz endpoint generowania tokenów TURN\end{minipage}. +.2 docker-compose.yml \dots\dots\begin{minipage}[t]{6cm}\raggedright Orkiestracja kontenerów (PostgreSQL + aplikacja)\end{minipage}. +.2 Dockerfile \dots\dots\dots\dots\begin{minipage}[t]{6cm}\raggedright Instrukcja budowania środowiska bazowego Python\end{minipage}. +.2 requirements.txt \dots\dots\begin{minipage}[t]{6cm}\raggedright Zależności (Django, channels-postgres)\end{minipage}. +} + +\newpage +\section{Kluczowe fragmenty implementacji} +W tej sekcji przedstawiono wybrane fragmenty kodu źródłowego odpowiadające za najważniejsze mechanizmy realizujące komunikację w czasie rzeczywistym. + +\subsection{Consumer obsługujący WebSocket (chat/consumers.py)} +Poniższy kod ukazuje w jaki sposób framework Channels obsługuje podłączanie się użytkowników do specyficznej dla kanału ,,grupy'' oraz metodykę odbierania wiadomości tekstowych z przeglądarki i rozsyłania ich w czasie rzeczywistym. + +\begin{lstlisting}[language=Python, caption={Przykład implementacji klasy \texttt{AsyncWebsocketConsumer}}] +import json +from channels.generic.websocket import AsyncWebsocketConsumer + +class ChatConsumer(AsyncWebsocketConsumer): + async def connect(self): + self.room_name = self.scope['url_route']['kwargs']['room_name'] + self.room_group_name = f"chat_{self.room_name}" + + # Dodanie klienta do grupy nasluchujacej + await self.channel_layer.group_add( + self.room_group_name, + self.channel_name + ) + await self.accept() + + async def disconnect(self, close_code): + # Odlaczenie od grupy przy zamknieciu strony + await self.channel_layer.group_discard( + self.room_group_name, + self.channel_name + ) + + async def receive(self, text_data): + text_data_json = json.loads(text_data) + message = text_data_json['message'] + + # Rozeslanie otrzymanej wiadomosci do calej grupy + await self.channel_layer.group_send( + self.room_group_name, + { + 'type': 'chat_message', + 'message': message + } + ) + + # Funkcja wywolywana, gdy warstwa kanałów wysyla typ 'chat_message' + async def chat_message(self, event): + message = event['message'] + await self.send(text_data=json.dumps({ + 'message': message + })) +\end{lstlisting} + +\subsection{Generowanie tokenów TURN (chat/views.py)} +Poniższy kod prezentuje bezpieczne pobieranie danych uwierzytelniających (tokenów ICE) z API Cloudflare TURN za pomocą synchronicznego widoku Django. W przypadku braku konfiguracji kluczy Cloudflare zastosowano automatyczną ścieżkę awaryjną (fallback) do publicznych serwerów STUN. + +\begin{lstlisting}[language=Python, caption={Pobieranie tokenów ICE Cloudflare Realtime TURN}] +import urllib.request +import urllib.error +import json +from django.conf import settings +from django.http import JsonResponse +from django.contrib.auth.decorators import login_required + +@login_required +def voice_credentials_view(request): + api_token = getattr(settings, 'CLOUDFLARE_API_TOKEN', '') + turn_key_id = getattr(settings, 'CLOUDFLARE_TURN_KEY_ID', '') + + if not api_token or not turn_key_id: + return JsonResponse({ + "iceServers": [{"urls": ["stun:stun.cloudflare.com:3478"]}] + }) + + url = ( + f"https://rtc.live.cloudflare.com/v1/turn/keys/" + f"{turn_key_id}/credentials/generate-ice-servers" + ) + headers = { + "Authorization": f"Bearer {api_token}", + "Content-Type": "application/json" + } + body = json.dumps({"ttl": 86400}).encode("utf-8") + req = urllib.request.Request(url, data=body, headers=headers, method="POST") + try: + with urllib.request.urlopen(req, timeout=5) as response: + return JsonResponse(json.loads(response.read().decode())) + except urllib.error.URLError: + return JsonResponse({ + "iceServers": [{"urls": ["stun:stun.cloudflare.com:3478"]}] + }) +\end{lstlisting} + +\subsection{Sygnalizacja WebRTC (chat/consumers.py)} +Aby nawiązać połączenie P2P, klienci muszą wymienić oferty SDP i kandydatów ICE. Poniższy fragment klasy \texttt{VoiceSignalingConsumer} realizuje to zadanie, automatycznie aktualizując status obecności użytkownika w kanale głosowym w bazie danych oraz rozsyłając sygnały do właściwych odbiorców docelowych. + +\begin{lstlisting}[language=Python, caption={Klasa \texttt{VoiceSignalingConsumer} obsługująca sygnalizację WebRTC}] +from channels.generic.websocket import AsyncWebsocketConsumer + +class VoiceSignalingConsumer(AsyncWebsocketConsumer): + async def connect(self): + self.channel_id = self.scope['url_route']['kwargs']['channel_id'] + self.group_name = f'voice_{self.channel_id}' + self.user = self.scope['user'] + + await self.channel_layer.group_add(self.group_name, self.channel_name) + await self.accept() + + await self.set_voice_channel(self.channel_id) + await self.channel_layer.group_send( + 'global_updates', + { + 'type': 'voice_presence_change', + 'user_id': self.user.id, + 'username': self.user.username, + 'channel_id': int(self.channel_id), + 'action': 'joined', + } + ) + + async def receive(self, text_data): + data = json.loads(text_data) + if data.get('action') == 'signal': + await self.channel_layer.group_send( + self.group_name, + { + 'type': 'voice_signal_relay', + 'sender_id': self.user.id, + 'target_id': data.get('target_id'), + 'data': data.get('data'), + } + ) + + async def voice_signal_relay(self, event): + if event['target_id'] == self.user.id: + await self.send(text_data=json.dumps(event)) +\end{lstlisting} + +\newpage +\section{Prezentacja interfejsu graficznego} + +Kolejne ekrany prezentują wygląd aplikacji z perspektywy klienta webowego. Widoki zostały przygotowane w oparciu o framework Bootstrap, co zapewnia responsywność interfejsu na różnych urządzeniach. + +\subsection{Dostęp i Autoryzacja} + +\begin{figure}[H] + \centering + % Odkomentuj poniższą linię, aby wstawić prawdziwy plik graficzny + \includegraphics[width=0.7\textwidth]{images/login_screen.png} + \caption{Ekran logowania -- Wymagany dla wszystkich użytkowników przed przystąpieniem do konwersacji.} + \label{fig:login} +\end{figure} + +\begin{figure}[H] + \centering + \includegraphics[width=0.7\textwidth]{images/register_screen.png} + \caption{Ekran rejestracji -- Umożliwia utworzenie nowego profilu.} + \label{fig:register} +\end{figure} + +\subsection{Główny panel komunikacyjny} + +\begin{figure}[H] + \centering + \includegraphics[width=0.7\textwidth]{images/chat_interface.png} + \caption{Główne okno aplikacji przypominające interfejs platformy Discord. Po lewej stronie widoczna jest lista dostępnych kanałów tematycznych, z prawej okno wybranego czatu z historią wiadomości.} + \label{fig:main_chat} +\end{figure} + +\begin{figure}[H] + \centering + \includegraphics[width=0.7\textwidth]{images/admin_panel.png} + \caption{Panel Administracyjny Uprawnień -- Służy do moderowania i edycji uprawnień użytkowników.} + \label{fig:admin_panel} +\end{figure} + +\newpage +\section{Podsumowanie} +Projekt Discord Clone stanowi kompletną, chociaż z konieczności uproszczoną na potrzeby ćwiczeniowe, wersję nowoczesnego komunikatora grupowego. Użycie architektury ASGI (Daphne, Django Channels) przełamuje ograniczenia tradycyjnego, synchronicznego modelu HTTP/WSGI i pozwala na obsługę setek równoczesnych połączeń w czasie rzeczywistym. + +\vspace{0.3cm} +\textbf{Najważniejsze osiągnięcia technologiczne zastosowane w projekcie:} +\begin{enumerate} + \item \textbf{Integracja \texttt{channels-postgres}:} Rezygnacja z klasycznego wykorzystywania zewnętrznych rozwiązań cachingowych (takich jak np. Redis) pozwala znacznie uprościć infrastrukturę serwerową oraz obniżyć zasoby wykorzystywane przez kontenery Dockera. Komunikaty WebSocket skutecznie wykorzystują wewnętrzne wyzwalacze PostgreSQL. + \item \textbf{Podział uprawnień:} Prawidłowe zdefiniowanie ról wewnątrz systemu uniemożliwia użytkownikom ze standardowymi uprawnieniami modyfikowanie i kasowanie wiadomości innych użytkowników, chroniąc system przed nadużyciami. + \item \textbf{Prostota środowiska deweloperskiego:} Plik konfiguracyjny \texttt{docker-compose.yml} sprowadza proces instalacji środowiska uruchomieniowego do jednej komendy terminala, rozwiązując odwieczny problem niezgodności wersji paczek pomiędzy maszynami programistów. + \item \textbf{Komunikacja głosowa WebRTC z Cloudflare Realtime TURN:} Zaimplementowanie kanałów głosowych w modelu P2P Full Mesh, przy jednoczesnym wyeliminowaniu kosztów stałych dzięki darmowemu pakietowi Cloudflare Realtime (do 1000 GB egressu/miesiąc) i dynamicznemu generowaniu krótkoterminowych tokenów sesyjnych. +\end{enumerate} +Projekt daje doskonałą podstawę do wdrażania kolejnych funkcji w przyszłości, takich jak udostępnianie ekranu (screen sharing), grupowe rozmowy wideo czy natywne powiadomienia typu Push Notifications. + +\end{document} \ No newline at end of file