\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}