79 Commits

Author SHA1 Message Date
4fd6616786 add block option 2023-12-20 23:48:52 +01:00
57ed6165ea bug issue; rewrite all messages 2023-12-19 12:42:46 +01:00
acba77e228 Réarrangement du code; correction css; écriture uniquement du nouveau message 2023-12-19 11:27:18 +01:00
fb0f9be103 merge with Chatte; patch css 2023-12-18 22:37:05 +01:00
41a7a6dfb8 css patch 2023-12-18 22:35:42 +01:00
a10fdefd23 css patch 2023-12-18 22:29:40 +01:00
kdx
b139652fc5 fix css 2023-12-18 22:29:38 +01:00
c6f6e216f2 css patch 2023-12-18 22:27:17 +01:00
kdx
7479088254 fix css 2023-12-18 22:27:02 +01:00
651fbbd67f css patch 2023-12-18 22:18:49 +01:00
778d10d22f fix: client.me 2 2023-12-18 21:41:00 +01:00
1961047703 merge with Chatte 2023-12-18 21:29:35 +01:00
929c1fdeb3 merge with Chatte 2023-12-18 21:27:52 +01:00
980a66fb47 fix: client.me 2023-12-18 21:27:10 +01:00
609b045315 better css, add debug 2023-12-18 21:26:18 +01:00
ceed7c2c4a fix: profile page 2 2023-12-17 20:29:04 +01:00
6a537a9b68 fix: profile work 2023-12-17 20:27:13 +01:00
26a9152756 fix: register view 2023-12-17 20:17:52 +01:00
f32d38287a tentative de merge 2023-12-16 18:02:07 +01:00
51354d9922 tentative de merge 2023-12-16 18:00:38 +01:00
b5d73e59fd core: simplified login and register, fix: me 2023-12-16 17:41:31 +01:00
86e2528d04 merge with Chatte 2023-12-16 17:01:03 +01:00
3be50e747d bug issue, null users_id creation channel 2023-12-16 17:00:38 +01:00
1f41e62a86 core: refonte profile 2023-12-16 16:42:30 +01:00
51059e6bf3 merge with Chatte 2023-12-16 16:15:13 +01:00
92221926a1 Better Css 2023-12-16 16:14:46 +01:00
115ae9357a fix: profile not found GET return error 2023-12-16 11:06:24 +01:00
1b44d5c94f Merge remote-tracking branch 'refs/remotes/origin/server' into server 2023-12-16 10:58:11 +01:00
468a3917a9 fix css file to /login /register 2023-12-16 10:39:07 +01:00
9be65a2bab Update README.md 2023-12-16 10:35:52 +01:00
d03f90367f bug issue 2023-12-15 20:39:31 +01:00
6171528371 merge with Chatte 2023-12-15 20:34:55 +01:00
bcb072f7d9 chat functional 2023-12-15 20:32:43 +01:00
c303042588 add: 404 not found 2023-12-14 13:55:31 +01:00
072c97d1c0 Update README.md 2023-12-13 14:55:17 +01:00
8e375c4805 Update README.md 2023-12-13 13:45:29 +01:00
4c3dd1be4a update: requirement 2023-12-13 13:01:51 +01:00
5d46ff5123 add: offline button 2023-12-13 12:40:47 +01:00
0ce8645770 Merge branch 'jspong' into server 2023-12-13 12:35:06 +01:00
a15c59fbf7 clean: remove trash file 2023-12-12 15:21:06 +01:00
243c5f266a clean: remove bozo print 2023-12-12 15:20:53 +01:00
926ac0dd54 clean: print 2023-12-12 15:20:26 +01:00
f0aaf0f29e clean: remove print 2023-12-12 14:43:57 +01:00
ae20be25fb add: matchmaking 2023-12-12 12:06:13 +01:00
7e2c29e78b merge with server 2023-12-12 10:37:44 +01:00
0e3b19fcd9 Advance don't merge 2023-12-12 10:05:13 +01:00
bc892bc157 Advance don't merge 2023-12-12 10:04:46 +01:00
ad6cfdf08a Merge branch 'server' into feat/matchmaking 2023-12-12 10:01:12 +01:00
08ce682980 rename id to user_id 2023-12-12 09:53:28 +01:00
9b523f082f merge with server 2023-12-11 16:15:21 +01:00
78379aea1b Bug fix with username in chat 2023-12-11 16:14:27 +01:00
982130a02f 🤓 2023-12-11 15:59:15 +01:00
326724930a Merge branch 'server' into feat/matchmaking 2023-12-11 15:55:12 +01:00
85787760b9 fix: back button is back 2023-12-11 15:54:56 +01:00
fb1b71ade6 Merge branch 'server' into feat/matchmaking 2023-12-11 15:24:12 +01:00
2ccfc5464a fix: fix: + ratio 2023-12-11 15:23:42 +01:00
624fb47e04 merge with server 2023-12-11 14:55:17 +01:00
c178556a2e Add functional research bar 2023-12-11 14:54:39 +01:00
d8a279f4d8 fix: profiles list display new profiles 2023-12-11 14:36:36 +01:00
336257d1d0 init view 2023-12-11 13:40:01 +01:00
f66b3883c1 init app 2023-12-11 13:39:53 +01:00
9c1dd30db7 add: css to me 2023-12-11 13:25:00 +01:00
cb5affab48 merge with server 2023-12-11 13:10:17 +01:00
08093627c9 Camille à trop les cramptés, en plus il va voir francis éboué qui est passé chez noz avec son daron qui se dore la biscotte en guadeloupe 2023-12-11 13:09:20 +01:00
079be0bb46 delete accounts auto logout 2023-12-11 12:43:49 +01:00
df436e0b88 fix: user can change avatar 2023-12-11 12:43:36 +01:00
208dd206ce merge with server 2023-12-11 10:57:51 +01:00
cee188145d no idea 2023-12-11 10:53:34 +01:00
c3c83b3168 add: profiles and profile page 2023-12-09 21:51:32 +01:00
dd19e15e7d fix: add return to profiles.all() 2023-12-09 15:13:40 +01:00
0edcd97f94 add: profiles.all() 2023-12-08 17:36:41 +01:00
54afa8aae5 move image path 2023-12-06 16:48:21 +01:00
2ce1356412 remove: bozo print 2023-12-06 15:33:40 +01:00
d1c75f7033 clean: remove bozo commnet 2023-12-06 15:33:18 +01:00
3833c647aa ignore avatars 2023-12-06 15:33:04 +01:00
32a8cbfda6 clean: rm trash 2023-12-06 15:20:37 +01:00
9b6c5547f0 add: profile avatar 2023-12-06 15:19:41 +01:00
910644a804 fix, add auto login when register 2023-12-04 12:32:15 +01:00
af9595c447 Don't merge, it's prototypal 2023-11-30 16:36:21 +01:00
78 changed files with 1498 additions and 301 deletions

4
.gitignore vendored
View File

@ -1,4 +1,6 @@
.env .env
*.pyc *.pyc
db.sqlite3 db.sqlite3
**/migrations/** **/migrations/**
/profiles/static/avatars/*
!/profiles/static/avatars/default.env

View File

@ -22,10 +22,17 @@ pip install -r requirements.txt
``` ```
- Setup database - Setup database
``` ```
python manage.py runserver makemigrations profiles python manage.py makemigrations games
python manage.py migrate profiles python manage.py makemigrations profiles
python manage.py makemigrations chat
python manage.py migrate
``` ```
- Start the developpement server - Start the developpement server
``` ```
python manage.py runserver 0.0.0.0:8000 python manage.py runserver 0.0.0.0:8000
``` ```
coc nvim
```
pip install django-stubs
```

View File

@ -1,6 +1,7 @@
from rest_framework.views import APIView from rest_framework.views import APIView
from rest_framework import permissions, status from rest_framework import permissions, status
from rest_framework.response import Response from rest_framework.response import Response
from django.contrib.auth import logout
from django.http import HttpRequest from django.http import HttpRequest
from rest_framework.authentication import SessionAuthentication from rest_framework.authentication import SessionAuthentication
@ -16,4 +17,5 @@ class DeleteView(APIView):
if (request.user.check_password(password) == False): if (request.user.check_password(password) == False):
return Response({"password": ["Password wrong."]}) return Response({"password": ["Password wrong."]})
request.user.delete() request.user.delete()
logout(request)
return Response("user deleted", status=status.HTTP_200_OK) return Response("user deleted", status=status.HTTP_200_OK)

View File

@ -13,7 +13,7 @@ class EditView(APIView):
authentication_classes = (SessionAuthentication,) authentication_classes = (SessionAuthentication,)
def get(self, request: HttpRequest): def get(self, request: HttpRequest):
return Response({"username": request.user.username}) return Response({"username": request.user.username, "id": request.user.pk})
def patch(self, request: HttpRequest): def patch(self, request: HttpRequest):
data: dict = request.data data: dict = request.data

View File

@ -13,4 +13,6 @@ class LoggedView(APIView):
authentication_classes = (SessionAuthentication,) authentication_classes = (SessionAuthentication,)
def get(self, request: HttpRequest): def get(self, request: HttpRequest):
return Response(str(request.user.is_authenticated), status=status.HTTP_200_OK) if (request.user.is_authenticated):
return Response({'id': request.user.pk}, status=status.HTTP_200_OK)
return Response('false', status=status.HTTP_200_OK)

View File

@ -20,4 +20,4 @@ class LoginView(APIView):
if user is None: if user is None:
return Response({'user': ['Username or password wrong.']}, status.HTTP_200_OK) return Response({'user': ['Username or password wrong.']}, status.HTTP_200_OK)
login(request, user) login(request, user)
return Response('user connected', status=status.HTTP_200_OK) return Response({'id': user.pk}, status=status.HTTP_200_OK)

View File

@ -3,6 +3,7 @@ from ..serializers.register import RegisterSerialiser
from rest_framework.views import APIView from rest_framework.views import APIView
from rest_framework.response import Response from rest_framework.response import Response
from django.http import HttpRequest from django.http import HttpRequest
from django.contrib.auth import login
class RegisterView(APIView): class RegisterView(APIView):
permission_classes = (permissions.AllowAny,) permission_classes = (permissions.AllowAny,)
@ -12,5 +13,6 @@ class RegisterView(APIView):
if serializer.is_valid(raise_exception=True): if serializer.is_valid(raise_exception=True):
user = serializer.create(data) user = serializer.create(data)
if user: if user:
login(request, user)
return Response("user created", status=status.HTTP_201_CREATED) return Response("user created", status=status.HTTP_201_CREATED)
return Response(status=status.HTTP_400_BAD_REQUEST) return Response(status=status.HTTP_400_BAD_REQUEST)

View File

@ -1,3 +1,6 @@
from django.contrib import admin from django.contrib import admin
from .models import ChannelModel, MemberModel, MessageModel
# Register your models here. admin.site.register(ChannelModel)
admin.site.register(MemberModel)
admin.site.register(MessageModel)

View File

@ -1,10 +1,25 @@
import json import json
from channels.generic.websocket import WebsocketConsumer from channels.generic.websocket import WebsocketConsumer
from asgiref.sync import async_to_sync from asgiref.sync import async_to_sync
from .models import MemberModel, MessageModel
from profiles.models import BlockModel
import time
class ChatConsumer(WebsocketConsumer): class ChatConsumer(WebsocketConsumer):
def connect(self): def connect(self):
self.room_group_name = 'test' channel_id : str = self.scope['path'].split('/')[3]
self.room_group_name = 'chat' + channel_id
user = self.scope["user"]
if (user.is_anonymous or not user.is_authenticated):
return
if MemberModel.objects.filter(member_id=user.pk, channel_id=int(channel_id)).count() != 1:
return
if (self.channel_layer == None):
return
async_to_sync(self.channel_layer.group_add)( async_to_sync(self.channel_layer.group_add)(
self.room_group_name, self.room_group_name,
@ -14,23 +29,66 @@ class ChatConsumer(WebsocketConsumer):
self.accept() self.accept()
def receive(self, text_data): def receive(self, text_data=None, bytes_data=None):
if text_data == None:
return
text_data_json = json.loads(text_data) text_data_json = json.loads(text_data)
message = text_data_json['message'] message = text_data_json['message']
receivers_id = text_data_json['receivers_id']
print(text_data)
channel_id : int = int(self.scope['path'].split('/')[3])
user = self.scope["user"]
if (user.is_anonymous or not user.is_authenticated):
return
if MemberModel.objects.filter(member_id=user.pk, channel_id=channel_id).count() != 1:
return
if (self.channel_layer == None):
return
message_time : int = int(time.time() * 1000)
if (len(receivers_id) == 1 and
BlockModel.objects.filter(blocker=user.pk, blocked=receivers_id[0]) or
BlockModel.objects.filter(blocker=receivers_id[0], blocked=user.pk)
):
return
async_to_sync(self.channel_layer.group_send)( async_to_sync(self.channel_layer.group_send)(
self.room_group_name, self.room_group_name,
{ {
'type':'chat_message', 'type':'chat_message',
'message':message 'author_id':user.pk,
'content':message,
'time':message_time,
} }
) )
new_message = MessageModel()
new_message.channel_id = channel_id
new_message.author_id = user.pk
new_message.content = message
new_message.time = message_time
new_message.save()
def chat_message(self, event): def chat_message(self, event):
message = event['message']
channel_id : int = int(self.scope['path'].split('/')[3])
user = self.scope["user"]
if (user.is_anonymous or not user.is_authenticated):
return
if MemberModel.objects.filter(member_id=user.pk, channel_id=channel_id).count() != 1:
return
self.send(text_data=json.dumps({ self.send(text_data=json.dumps({
'type':'chat', 'type':'chat',
'username':self.scope["user"].username, 'author_id':event['author_id'],
'message':message 'content':event['content'],
'time': event['time'],
})) }))

View File

@ -1,3 +1,24 @@
from django.db import models from django.db import models
from django.db.models import IntegerField
from django.contrib.auth.models import User
from django.contrib import admin
# Create your models here. # Create your models here.
class ChannelModel(models.Model):
pass
class MemberModel(models.Model):
member_id = IntegerField(primary_key=False)
channel_id = IntegerField(primary_key=False)
def __str__(self):
return "member_id: " + str(self.member_id) + ", channel_id: " + str(self.channel_id)
class MessageModel(models.Model):
channel_id = IntegerField(primary_key=False)
author_id = IntegerField(primary_key=False)
content = models.CharField(max_length=255)
time = IntegerField(primary_key=False)
def __str__(self):
return "author_id: " + str(self.author_id) + ", channel_id: " + str(self.channel_id) + ", content: " + self.content

View File

@ -2,5 +2,5 @@ from django.urls import re_path
from . import consumers from . import consumers
websocket_urlpatterns = [ websocket_urlpatterns = [
re_path(r'ws/socket-server/', consumers.ChatConsumer.as_asgi()) re_path(r'ws/chat/(?P<chat_id>\d+)/$', consumers.ChatConsumer.as_asgi())
] ]

8
chat/serializers.py Normal file
View File

@ -0,0 +1,8 @@
from rest_framework import serializers
from .models import ChannelModel
class ChannelSerializer(serializers.ModelSerializer):
class Meta:
model = ChannelModel
fields = []

10
chat/urls.py Normal file
View File

@ -0,0 +1,10 @@
from django.urls import path
from django.conf import settings
from django.conf.urls.static import static
from . import views
urlpatterns = [
path("<int:pk>", views.ChatView.as_view(), name="chat_page"),
path("", views.ChatsView.as_view(), name="chats_page"),
]

View File

@ -1,3 +1,79 @@
from django.shortcuts import render from rest_framework.views import APIView
from rest_framework.response import Response
from rest_framework import authentication, permissions, status
from rest_framework.authentication import SessionAuthentication
from .models import ChannelModel, MemberModel, MessageModel
from django.core import serializers
# Create your views here. class ChatView(APIView):
permission_classes = (permissions.IsAuthenticated,)
authentication_classes = (SessionAuthentication,)
def get(self, request, pk):
if (ChannelModel.objects.filter(pk=pk)):
return Response({'channel_id': pk}, status=status.HTTP_200_OK)
else:
return Response("Channel doesn't exist", status=status.HTTP_404_NOT_FOUND)
def delete(self, request, pk):
ChannelModel.objects.filter(pk=pk).delete()
MessageModel.objects.filter(pk=pk).delete()
MemberModel.objects.filter(pk=pk).delete()
return Response({'channel_id': pk}, status=status.HTTP_200_OK)
class ChatsView(APIView):
def post(self, request):
data: dict = request.data
users_id = request.data.get("users_id", [])
if len(users_id) < 2:
return Response('Not enought members to create the channel', status=status.HTTP_400_BAD_REQUEST)
if users_id[0] == users_id[1]:
return Response('Same member', status=status.HTTP_400_BAD_REQUEST)
for user_id1 in users_id:
for member1 in MemberModel.objects.filter(member_id=user_id1):
for user_id2 in users_id:
if user_id1 == user_id2:
continue
for member2 in MemberModel.objects.filter(member_id=user_id2):
if (member1.channel_id == member2.channel_id):
messages = MessageModel.objects.filter(channel_id=member1.channel_id).order_by("time")
messages = serializers.serialize("json", messages)
return Response({'channel_id': member1.channel_id, 'messages':messages}, status=status.HTTP_200_OK)
new_channel = ChannelModel()
new_channel.save()
for user_id in users_id:
new_member = MemberModel()
new_member.channel_id = new_channel.pk
new_member.member_id = user_id
new_member.save()
return Response({'channel_id': new_channel.pk}, status=status.HTTP_201_CREATED)
def delete(self, request):
data: dict = request.data
users_id = request.data.get("users_id", [])
#print(list(MemberModel.objects.all()))
for user_id1 in users_id:
for member1 in MemberModel.objects.filter(member_id=user_id1):
for user_id2 in users_id:
if user_id1 == user_id2:
break
for member2 in MemberModel.objects.filter(member_id=user_id2):
if (member1.channel_id == member2.channel_id):
MessageModel.objects.filter(channel_id=member1.channel_id).delete()
member1.delete()
member2.delete()
ChannelModel.objects.get(pk=member1.channel_id).delete()
return Response("Channel removed", status=status.HTTP_200_OK)
return Response("Channel doesn't exist", status=status.HTTP_404_NOT_FOUND)

View File

@ -9,4 +9,4 @@
margin-right: auto; margin-right: auto;
margin-top: 90px; margin-top: 90px;
border: 15px black solid; border: 15px black solid;
} }

View File

@ -9,4 +9,4 @@
margin-right: auto; margin-right: auto;
margin-top: 90px; margin-top: 90px;
border: 15px black solid; border: 15px black solid;
} }

View File

@ -1,7 +1,8 @@
body { body {
margin: 10; margin: 0.5em;
font-family: 'Quicksand', sans-serif; font-family: 'Quicksand', sans-serif;
font-size: 18px; font-size: 30px;
} }
a { a {

View File

@ -0,0 +1,18 @@
#app #main .account
{
background-color: red;
}
#app #main
{
width: 60%;
display: flex;
margin-left: auto;
margin-right: auto;
flex-direction: column;
}
#app #main .profile
{
background-color: green;
}

View File

@ -0,0 +1,20 @@
#app #avatar
{
height: 100px;
width: 100px;
}
#app #username
{
font-size: 0.8em;
}
#app #block {
cursor: pointer;
font-size: 0.7em;
text-decoration: underline;
}
#app {
margin-top: 20px;
}

View File

@ -0,0 +1,104 @@
#app img
{
max-height: 3em;
max-width: 3em;
}
#app ul
{
font-size: 0.75em;
margin: 0.25em 0 0 0;
padding: 0 0 0 0;
list-style-type: none;
max-height: 80vh;
overflow: auto;
}
#app li
{
margin: 0.25em 0.25em 0 0;
}
#app #chats {
overflow: hidden;
display: flex;
text-align: left;
}
#app #users {
margin: 0em 1.0em 0em 0.05em;
}
#app #chat {
position: relative;
max-height: 100vh;
width: 100vh;
/*border: 2px solid green;*/
overflow: hidden;
}
#app #members {
font-size: 1em;
}
#app #add_chat_off {
text-decoration: underline;
cursor: pointer;
}
#app #add_chat_on {
cursor: pointer;
}
#app #messages {
max-height: 60vh;
overflow: scroll;
overflow-y: scroll;
overflow-x: hidden;
font-size: 0.75em;
}
#app #input_user{
color: green;
width: 8.5em;
height: 1.1em;
font-size: 0.65em;
border: none;
outline: none;
border-bottom: 0.15em solid green;
}
#app #input_chat{
position: sticky;
bottom: 0;
/*width: calc(100% - 8px);*/
width: 100%;
border: none;
outline: none;
border-bottom: 0.2em solid green;
caret-color: green;
color: green;
font-size: 16px;
}
#app #you {
text-align: left;
position: relative;
max-width: 48%;
left: 0.5em;
margin: 0.5em 0 0 0;
color: green;
word-wrap: break-word;
}
#app #other {
text-align: right;
position: relative;
max-width: 48%;
margin: 0.5em 0 0 auto;
right: 0.5em;
color: red;
/* permet le retour à la ligne à la place de dépasser*/
word-wrap: break-word;
}

View File

@ -0,0 +1,19 @@
import { Profile } from "./profile.js";
class MyProfile extends Profile
{
async change_avatar(form_data)
{
let response = await this.client._patch_file(`/api/profiles/me`, form_data);
let response_data = await response.json()
return response_data;
}
async init()
{
super.init("me");
}
}
export {MyProfile}

View File

@ -1,7 +1,15 @@
import { Client } from "./client.js";
class Account class Account
{ {
/**
* @param {Client} client
*/
constructor (client) constructor (client)
{ {
/**
* @type {Client} client
*/
this.client = client; this.client = client;
} }
@ -23,7 +31,12 @@ class Account
let response = await this.client._delete("/api/accounts/delete", {password: password}); let response = await this.client._delete("/api/accounts/delete", {password: password});
let response_data = await response.json(); let response_data = await response.json();
if (response_data === "user deleted") if (JSON.stringify(response_data) == JSON.stringify({'detail': 'Authentication credentials were not provided.'}))
{
this.client._logged = false;
return null;
}
if (response_data == "user deleted")
this.client._logged = false; this.client._logged = false;
return response_data; return response_data;
} }
@ -35,7 +48,7 @@ class Account
if (JSON.stringify(response_data) == JSON.stringify({'detail': 'Authentication credentials were not provided.'})) if (JSON.stringify(response_data) == JSON.stringify({'detail': 'Authentication credentials were not provided.'}))
{ {
console.log("error, client is not logged"); this.client._logged = false;
return null; return null;
} }
return response_data; return response_data;
@ -43,13 +56,13 @@ class Account
async update(data, password) async update(data, password)
{ {
data.password = password; data.current_password = password;
let response = await this.client._patch_json("/api/accounts/edit", data); let response = await this.client._patch_json("/api/accounts/edit", data);
let response_data = await response.json(); let response_data = await response.json();
if (JSON.stringify(response_data) == JSON.stringify({'detail': 'Authentication credentials were not provided.'})) if (JSON.stringify(response_data) == JSON.stringify({'detail': 'Authentication credentials were not provided.'}))
{ {
console.log("error, client is not logged"); this.client._;
return null; return null;
} }
return response_data; return response_data;

View File

@ -0,0 +1,78 @@
import {Message} from "./message.js"
class Channel {
constructor(client, channel_id, members_id, messages, reload) {
this.client = client;
this.channel_id = channel_id;
this.members_id = members_id;
this.messages = [];
if (messages != undefined)
this.updateMessages(messages);
this.connect(reload);
}
// reload = function to use when we receive a message
async connect(reload) {
let url = `ws://${window.location.host}/ws/chat/${this.channel_id}/`;
this.chatSocket = new WebSocket(url);
this.chatSocket.onmessage = (event) =>{
let data = JSON.parse(event.data)
this.messages.push(new Message(
this.channel_id,
data.author_id,
data.content,
data.time,
));
reload();
};
}
async disconnect() {
this.chatSocket.close();
}
updateMessages(messages) {
messages = JSON.parse(messages);
let new_messages = [];
messages.forEach((message) => {
message = message["fields"];
new_messages.push(new Message(
message["channel_id"],
message["author_id"],
message["content"],
message["time"],
));
});
//console.log(new_messages);
this.messages = new_messages;
return new_messages;
}
async sendMessageChannel(message, receivers_id) {
if (this.chatSocket == undefined)
return;
this.chatSocket.send(JSON.stringify({
'message':message,
'receivers_id':receivers_id,
}));
}
async deleteChannel() {
let response = await this.client._delete("/api/chat/" + this.channel_id, {
});
let data = await response.json();
return data;
}
}
export {Channel}

View File

@ -0,0 +1,46 @@
import {Channel} from "./channel.js"
import {Message} from "./message.js"
class Channels {
constructor(client) {
this.client = client;
}
async createChannel(users_id, reload) {
let null_id = false;
users_id.forEach(user_id => {
if (user_id == null)
null_id = true;
});
if (null_id)
return console.log(users_id, "createChannel error, null id;");
let response = await this.client._post("/api/chat/", {
users_id:users_id
});
let data = await response.json();
let exit_code = await response.status;
if (exit_code >= 300)
return undefined;
let messages = undefined;
if (exit_code == 200)
messages = data.messages;
return new Channel(this.client, data.channel_id, users_id, messages, reload);
}
async deleteChannel(users_id) {
let response = await this.client._delete("/api/chat/", {
users_id:users_id
});
let data = await response.json();
console.log(response.status)
return data;
}
}
export {Channels}

View File

@ -0,0 +1,10 @@
class Message {
constructor(channel_id, author_id, content, time) {
this.channel_id = channel_id;
this.author_id = author_id;
this.content = content;
this.time = time;
}
}
export {Message}

View File

@ -1,11 +1,15 @@
import { Account } from "./account.js"; import { Account } from "./account.js";
import { MatchMaking } from "./matchmaking.js";
import { Profiles } from "./profiles.js";
import { Channels } from './chat/channels.js';
import { MyProfile } from "./MyProfile.js";
function getCookie(name) function getCookie(name)
{ {
let cookie = {}; let cookie = {};
document.cookie.split(';').forEach(function(el) { document.cookie.split(';').forEach(function(el) {
let split = el.split('='); let split = el.split('=');
cookie[split[0].trim()] = split.slice(1).join("="); cookie[split[0].trim()] = split.slice(1).join("=");
}) })
return cookie[name]; return cookie[name];
} }
@ -16,7 +20,12 @@ class Client
{ {
this._url = url; this._url = url;
this.account = new Account(this); this.account = new Account(this);
this.profiles = new Profiles(this);
this.matchmaking = new MatchMaking(this);
this._logged = undefined; this._logged = undefined;
this.channels = new Channels(this);
this.channel = undefined;
} }
async isAuthentificate() async isAuthentificate()
@ -26,10 +35,11 @@ class Client
return this.logged; return this.logged;
} }
async _get(uri) async _get(uri, data)
{ {
let response = await fetch(this._url + uri, { let response = await fetch(this._url + uri, {
method: "GET", method: "GET",
body: JSON.stringify(data),
}); });
return response; return response;
} }
@ -73,13 +83,35 @@ class Client
return response; return response;
} }
async _patch_file(uri, file)
{
let response = await fetch(this._url + uri, {
method: "PATCH",
headers: {
"X-CSRFToken": getCookie("csrftoken"),
},
body: file,
});
return response;
}
async _update_logged(state)
{
if (!this.logged && state)
{
this.me = new MyProfile(this);
await this.me.init();
}
this.logged = state;
}
async login(username, password) async login(username, password)
{ {
let response = await this._post("/api/accounts/login", {username: username, password: password}) let response = await this._post("/api/accounts/login", {username: username, password: password})
let data = await response.json(); let data = await response.json();
if (data == "user connected") if (data.id != undefined)
{ {
this.logged = true; await this._update_logged(true);
return null; return null;
} }
return data; return data;
@ -88,15 +120,18 @@ class Client
async logout() async logout()
{ {
await this._get("/api/accounts/logout"); await this._get("/api/accounts/logout");
this.logged = false; await this._update_logged(false);
} }
async _test_logged() async _test_logged()
{ {
let response = await this._get("/api/accounts/logged"); let response = await this._get("/api/accounts/logged");
let data = await response.json(); let data = await response.json();
return data === "True";
if (data.id !== undefined)
await this._update_logged(true);
return data.id !== undefined;
} }
} }
export {Client} export {Client}

View File

@ -0,0 +1,37 @@
import { Client } from "./client.js";
class MatchMaking
{
/**
* @param {Client} client
*/
constructor(client)
{
/**
* @type {client}
*/
this.client = client
}
async start(func)
{
if (!await this.client.isAuthentificate())
return null;
let url = `wss://${window.location.host}/ws/matchmaking/`;
this._chatSocket = new WebSocket(url);
this._chatSocket.onmessage = function (event) {
const data = JSON.parse(event.data);
func(data.game_id)
};
}
async stop()
{
this._chatSocket.close()
}
}
export {MatchMaking}

View File

@ -0,0 +1,45 @@
import { Client } from "./client.js";
class Profile
{
/**
* @param {Client} client
*/
constructor (client, username = undefined, avatar_url = undefined, user_id = undefined)
{
/**
* @type {Client} client
*/
this.client = client;
this.username = username;
this.avatar_url = avatar_url;
this.user_id = user_id;
this.isBlocked = false;
}
async init(user_id)
{
let response = await this.client._get(`/api/profiles/${user_id}`);
let response_data = await response.json();
this.user_id = response_data.user_id;
this.username = response_data.username;
this.avatar_url = response_data.avatar_url;
let block_response = await this.client._get("/api/profiles/block");
if (block_response.status == 404)
return
let block_data = await block_response.json();
let block_list = JSON.parse(block_data);
block_list.forEach(block => {
let blocker = block.fields.blocker;
let blocked = block.fields.blocked;
if (blocker == this.client.me.user_id && blocked == user_id)
return this.isBlocked = true;
});
}
}
export {Profile}

View File

@ -0,0 +1,64 @@
import { Profile } from "./profile.js";
class Profiles
{
/**
* @param {Client} client
*/
constructor (client)
{
/**
* @type {Client} client
*/
this.client = client
}
async all()
{
let response = await this.client._get("/api/profiles/");
let response_data = await response.json();
let profiles = []
response_data.forEach((profile) => {
profiles.push(new Profile(this.client, profile.username, profile.avatar_url, profile.user_id))
});
return profiles;
}
async getProfile(user_id)
{
let profile = new Profile(this.client);
await profile.init(user_id);
return profile;
}
async block(user_id) {
// blocker & blocked
let response = await this.client._post("/api/profiles/block", {
users_id:[this.client.me.user_id, user_id],
});
let data = await response.json();
console.log(response.status);
console.log(data);
return data;
}
async deblock(user_id) {
// blocker & blocked
let response = await this.client._delete("/api/profiles/block", {
users_id:[this.client.me.user_id, user_id],
});
let data = await response.json();
console.log(response.status);
console.log(data);
return data;
}
}
export {Profiles}

View File

@ -1,18 +1,19 @@
import { Client } from "./api/client.js";
import LoginView from "./views/accounts/LoginView.js"; import LoginView from "./views/accounts/LoginView.js";
import Dashboard from "./views/Dashboard.js"; import Dashboard from "./views/Dashboard.js";
import Posts from "./views/Posts.js"; import Search from "./views/Search.js";
import PostView from "./views/PostView.js";
import Settings from "./views/Settings.js";
import Chat from "./views/Chat.js";
import HomeView from "./views/HomeView.js"; import HomeView from "./views/HomeView.js";
import RegisterView from "./views/accounts/RegisterView.js"; import RegisterView from "./views/accounts/RegisterView.js";
import LogoutView from "./views/accounts/LogoutView.js"; import LogoutView from "./views/accounts/LogoutView.js";
import GameView from "./views/Game.js" import GameView from "./views/Game.js"
import { Client } from "./api/client.js"; import PageNotFoundView from './views/PageNotFoundView.js'
import AbstractRedirectView from "./views/AbstractRedirectView.js";
import AbstractRedirectView from "./views/abstracts/AbstractRedirectView.js";
import MeView from "./views/MeView.js"; import MeView from "./views/MeView.js";
import ProfilePageView from "./views/ProfilePageView.js";
import MatchMakingView from "./views/MatchMakingView.js";
let client = new Client(location.protocol + "//" + location.host) let client = new Client(location.protocol + "//" + location.host)
@ -34,19 +35,18 @@ const navigateTo = async (uri) => {
history.pushState(null, null, uri); history.pushState(null, null, uri);
}; };
const router = async (uri = "") => { const router = async (uri) => {
const routes = [ const routes = [
{ path: "/", view: Dashboard }, { path: "/", view: Dashboard },
{ path: "/posts", view: Posts }, { path: "/profiles/:id", view: ProfilePageView },
{ path: "/posts/:id", view: PostView },
{ path: "/settings", view: Settings },
{ path: "/login", view: LoginView }, { path: "/login", view: LoginView },
{ path: "/logout", view: LogoutView }, { path: "/logout", view: LogoutView },
{ path: "/register", view: RegisterView }, { path: "/register", view: RegisterView },
{ path: "/chat", view: Chat }, { path: "/search", view: Search },
{ path: "/home", view: HomeView }, { path: "/home", view: HomeView },
{ path: "/me", view: MeView }, { path: "/me", view: MeView },
{ path: "/game", view: GameView }, { path: "/matchmaking", view: MatchMakingView },
{ path: "/game/offline", view: GameView },
]; ];
// Test each route for potential match // Test each route for potential match
@ -61,7 +61,10 @@ const router = async (uri = "") => {
if (!match) { if (!match) {
match = { match = {
route: routes[0], route: {
path: uri,
view: PageNotFoundView
},
result: [uri] result: [uri]
}; };
} }
@ -76,6 +79,7 @@ const router = async (uri = "") => {
lastView = view; lastView = view;
await client.isAuthentificate();
let content = await view.getHtml(); let content = await view.getHtml();
if (content == null) if (content == null)
return 1; return 1;
@ -87,7 +91,7 @@ const router = async (uri = "") => {
return 0; return 0;
}; };
window.addEventListener("popstate", router); window.addEventListener("popstate", function() {router(location.pathname)});
document.addEventListener("DOMContentLoaded", () => { document.addEventListener("DOMContentLoaded", () => {
document.body.addEventListener("click", e => { document.body.addEventListener("click", e => {

View File

@ -0,0 +1,18 @@
function clear(property_name, elements_id)
{
elements_id.forEach(element_id => {
let element = document.getElementById(element_id)
element[property_name] = ""
});
}
function fill_errors(errors, property_name)
{
Object.keys(errors).forEach(error_field =>
{
let element = document.getElementById(error_field);
element[property_name] = errors[error_field];
});
}
export {fill_errors, clear}

View File

@ -1,56 +0,0 @@
import AbstractAuthentifiedView from "./AbstractAuthentifiedView.js";
export default class extends AbstractAuthentifiedView {
constructor(params) {
super(params, "Chat");
let url = `wss://${window.location.host}/ws/socket-server/`
this.chatSocket = new WebSocket(url)
this.chatSocket.onmessage = function(e){
let data = JSON.parse(e.data)
console.log('Data:', data)
if (data.type === 'chat') {
let messages = document.getElementById('messages')
let username = data.username === null || data.username.length <= 0 ? "NoName" : data.username;
messages.insertAdjacentHTML('beforeend', `
<div><p>${username}: ${data.message}</p></div>
`)
}
}
}
async postInit() {
let form = document.getElementById('form')
form.addEventListener('submit', (e)=> {
e.preventDefault()
let message = e.target.message.value
this.chatSocket.send(JSON.stringify({
'message':message
}))
form.reset()
})
}
async leavePage() {
this.chatSocket.close();
}
async getHtml() {
return `
<h1>Chat</h1>
<form id="form">
<input type="text" name="message" />
</form>
<div id="messages">
</div>
`;
}
}

View File

@ -1,4 +1,4 @@
import AbstractView from "./AbstractView.js"; import AbstractView from "./abstracts/AbstractView.js";
export default class extends AbstractView { export default class extends AbstractView {
constructor(params) { constructor(params) {
@ -16,4 +16,4 @@ export default class extends AbstractView {
</p> </p>
`; `;
} }
} }

View File

@ -1,4 +1,4 @@
import AbstractView from './AbstractView.js' import AbstractView from "./abstracts/AbstractView.js";
export default class extends AbstractView { export default class extends AbstractView {
constructor(params) { constructor(params) {

View File

@ -1,4 +1,4 @@
import AbstractAuthentificateView from "./AbstractAuthentifiedView.js"; import AbstractAuthentificateView from "./abstracts/AbstractAuthentifiedView.js";
export default class extends AbstractAuthentificateView { export default class extends AbstractAuthentificateView {
constructor(params) { constructor(params) {
@ -9,8 +9,10 @@ export default class extends AbstractAuthentificateView {
async getHtml() { async getHtml() {
return ` return `
<h1>HOME</h1> <h1>HOME</h1>
<a href="/matchmaking" class="nav__link" data-link>Play a game</a>
<a href="/game/offline" class="nav__link" data-link>Play offline</a>
<a href="/me" class="nav__link" data-link>Me</a> <a href="/me" class="nav__link" data-link>Me</a>
<a href="/logout" class="nav__link" data-link>Logout</a> <a href="/logout" class="nav__link" data-link>Logout</a>
`; `;
} }
} }

View File

@ -0,0 +1,29 @@
import { client, navigateTo } from "../index.js";
import AbstractView from "./abstracts/AbstractView.js";
function game_found(game_id)
{
navigateTo(`/games/${game_id}`)
}
export default class extends AbstractView {
constructor(params) {
super(params, "Dashboard");
}
async postInit()
{
await client.matchmaking.start(game_found)
}
async getHtml() {
return `
<h1>finding<h1>
`;
}
async leavePage()
{
await client.matchmaking.stop();
}
}

View File

@ -1,5 +1,6 @@
import { client, navigateTo } from "../index.js"; import { client, navigateTo } from "../index.js";
import AbstractAuthentificateView from "./AbstractAuthentifiedView.js"; import { clear, fill_errors } from "../utils/formUtils.js";
import AbstractAuthentificateView from "./abstracts/AbstractAuthentifiedView.js";
export default class extends AbstractAuthentificateView export default class extends AbstractAuthentificateView
{ {
@ -10,94 +11,95 @@ export default class extends AbstractAuthentificateView
async postInit() async postInit()
{ {
if (this.fill() === null) document.getElementById("save-account-button").onclick = this.save_account;
return; document.getElementById("delete-account-button").onclick = this.delete_account;
document.getElementById("save-button").onclick = this.save; document.getElementById("save-profile-button").onclick = this.save_profile;
document.getElementById("delete-button").onclick = this.delete_accounts;
} }
async fill() async delete_account()
{ {
let data = await client.account.get(); let current_password = document.getElementById("current_password-input").value;
document.getElementById("username").value = data.username;
}
async delete_accounts()
{
let current_password = document.getElementById("current_password").value;
let response_data = await client.account.delete(current_password); let response_data = await client.account.delete(current_password);
console.log(await client.isAuthentificate())
if (response_data === null || response_data === "user deleted")
{
navigateTo("/login");
return;
}
clear("innerHTML", ["current_password-input"])
fill_errors({"current_password-input": response_data["password"]}, "innerHTML")
}
async save_account()
{
let username = document.getElementById("username-input").value;
let new_password = document.getElementById("new_password-input").value;
let current_password = document.getElementById("current_password-input").value;
let data = {};
data.username = username;
if (new_password.length != 0)
data.new_password = new_password;
let response_data = await client.account.update(data, current_password);
if (response_data === null) if (response_data === null)
{ {
navigateTo("/login"); navigateTo("/login");
return; return;
} }
["delete", "current_password"].forEach(error_field => { if (response_data === "data has been alterate")
let error_display = document.getElementById(`error_${error_field}`); response_data = {"save-account": "saved"}
if (error_display != null)
error_display.innerHTML = "";
});
if (response_data === "user deleted") clear("innerHTML", ["username", "new_password", "current_password", "save-account", "delete-account"])
{ fill_errors(response_data, "innerHTML")
document.getElementById(`error_delete`).innerHTML = "OK";
navigateTo("/login")
return;
}
document.getElementById("error_current_password").innerHTML = response_data["password"]
} }
async save() async save_profile()
{ {
let username = document.getElementById("username").value; let avatar = document.getElementById("avatar-input");
let new_password = document.getElementById("new_password").value;
let current_password = document.getElementById("current_password").value; if (avatar.files[0] !== undefined)
let data = {};
data.username = username;
if (new_password.length != 0)
data.new_password = new_password;
let response_data = await client.account.update(data, current_password);
if (response_data === null)
{ {
navigateTo(super.redirect_url); let form_data = new FormData();
return; form_data.append("file", avatar.files[0]);
await client.me.change_avatar(form_data);
} }
document.getElementById("save-profile").innerHTML = "Saved";
["username", "new_password", "current_password"].forEach(error_field => {
let error_display = document.getElementById(`error_${error_field}`);
if (error_display != null)
error_display.innerHTML = "";
});
Object.keys(response_data).forEach(error_field => {
let error_display = document.getElementById(`error_${error_field}`);
if (error_display != null)
error_display.innerHTML = response_data[error_field];
});
} }
async getHtml() async getHtml()
{ {
return ` return `
<link rel="stylesheet" href="/static/css/me.css">
<h1>ME</h1> <h1>ME</h1>
<input type=text placeholder="username" id="username"> <div id="main">
<span id="error_username"></span> <div class="account">
<input type=password placeholder="new password" id="new_password"> <h3>Account</h3>
<span id="error_new_password"></span> <input type="text" placeholder="username" id="username-input" text=${client.me.username}>
<input type=password placeholder="current password" id="current_password"> <span id="username"></span>
<span id="error_current_password"></span> <input type=password placeholder="new_password" id="new_password-input">
<input type="button" value="Save" id="save-button"> <span id="new_password"></span>
<span id="error_save"></span> <input type=password placeholder="current_password" id="current_password-input">
<input type="button" value="Delete" id="delete-button"> <span id="current_password"></span>
<span id="error_delete"></span>
<a href="/logout" class="nav__link" data-link>Logout</a> <input type="button" value="Save Credentials" id="save-account-button">
<span id="save-account"></span>
<input type="button" value="Delete Account" id="delete-account-button">
<span id="delete-account"></span>
</div>
<div class="profile">
<h3>Profile</h3>
<input type="file" id="avatar-input" accept="image/png, image/jpeg">
<input type="button" value="Save profile" id="save-profile-button">
<span id="save-profile"></span>
</div>
<a href="/logout" class="nav__link" data-link>Logout</a>
</div>
`; `;
} }
} }

View File

@ -0,0 +1,14 @@
import AbstractView from "./abstracts/AbstractView.js";
export default class extends AbstractView {
constructor(params) {
super(params, "Dashboard");
}
async getHtml() {
return `
<h1>404 Bozo</h1>
<p>Git gud</p>
`;
}
}

View File

@ -1,15 +0,0 @@
import AbstractView from "./AbstractView.js";
export default class extends AbstractView {
constructor(params) {
super(params, "Viewing Post");
this.postId = params.id;
}
async getHtml() {
return `
<h1>Post</h1>
<p>You are viewing post #${this.postId}.</p>
`;
}
}

View File

@ -1,14 +0,0 @@
import AbstractView from "./AbstractView.js";
export default class extends AbstractView {
constructor(params) {
super(params, "Posts");
}
async getHtml() {
return `
<h1>Posts</h1>
<p>You are viewing the posts!</p>
`;
}
}

View File

@ -0,0 +1,62 @@
import AbstractView from "./abstracts/AbstractView.js";
import { client } from "../index.js"
export default class extends AbstractView {
constructor(params) {
super(params, "Profile ");
this.user_id = params.id;
}
async postInit()
{
this.profile = await client.profiles.getProfile(this.user_id);
this.info = document.getElementById("info");
// Username
let username = document.createElement("a");
username.id = "username";
username.appendChild(document.createTextNode(this.profile.username));
this.info.appendChild(username);
this.info.appendChild(document.createElement("br"));
// Avatar
let avatar = document.createElement("img");
avatar.id = "avatar";
avatar.src = this.profile.avatar_url;
this.info.appendChild(avatar);
await this.blockButton();
}
async blockButton() {
// Block option
if (client.me.user_id != this.user_id) {
let block = document.getElementById("block") || document.createElement("a");
block.id = "block";
block.innerText = "";
block.onclick = async () => {
if (!this.profile.isBlocked)
await client.profiles.block(this.user_id);
else
await client.profiles.deblock(this.user_id);
this.profile = await client.profiles.getProfile(this.user_id);
this.blockButton();
};
if (this.profile.isBlocked)
block.appendChild(document.createTextNode("Deblock"));
else
block.appendChild(document.createTextNode("Block"));
this.info.appendChild(block);
}
}
async getHtml() {
return `
<link rel="stylesheet" href="/static/css/profile.css">
<div id="info">
</div>
`;
}
}

View File

@ -0,0 +1,227 @@
import AbstractView from "./abstracts/AbstractView.js";
import {client} from "../index.js";
import {Message} from "../api/chat/message.js"
export default class extends AbstractView {
constructor(params) {
super(params, "Search");
}
async postInit() {
let search = document.getElementById("input_user");
search.oninput = this.users;
let chat_input = document.getElementById("input_chat");
//chat_input.addEventListener("keydown", this.chat_manager)
this.last_add_chat = undefined;
this.users();
this.chat();
}
async users() {
let search = document.getElementById("input_user").value.toLowerCase();
let logged = await client.isAuthentificate();
let users = await client.profiles.all();
let list_users = document.getElementById('list_users');
list_users.innerHTML = "";
users.filter(user => user.username.toLowerCase().startsWith(search) == true).forEach((user) => {
if (user.user_id == null) {
console.log("list User one with id null;");
return;
}
var new_user = document.createElement("li");
// username
let username = document.createElement("a");
username.href = `/profiles/${user.user_id}`;
username.appendChild(document.createTextNode(user.username));
new_user.appendChild(username);
// space
new_user.appendChild(document.createTextNode(" "));
// chat
if (logged && client.me.user_id != user.user_id) {
let add_chat = document.createElement("a");
add_chat.id = "add_chat_off";
add_chat.onclick = async () => {
if (client.channel != undefined) {
client.channel.members_id.forEach((member_id) => {
if (member_id == user.user_id)
client.channel = undefined;
});
if (client.channel == undefined) {
add_chat.id = "add_chat_off";
this.last_add_chat = undefined;
return this.hideChat();
}
client.channel.disconnect();
}
client.channel = await client.channels.createChannel([client.me.user_id , user.user_id], this.chat);
this.chat();
if (this.last_add_chat != undefined)
this.last_add_chat.id = "add_chat_off";
this.last_add_chat = add_chat;
add_chat.id = "add_chat_on";
};
add_chat.appendChild(document.createTextNode("Chat"));
new_user.appendChild(add_chat);
new_user.appendChild(document.createTextNode(" "));
}
// break line
new_user.appendChild(document.createElement("br"));
// avatar
var img = document.createElement("img");
img.src = user.avatar_url;
new_user.appendChild(img);
list_users.appendChild(new_user);
});
//console.log(list_users);
}
async chat() {
let users = await client.profiles.all();
let logged = await client.isAuthentificate();
/*let reload = document.getElementById("messages");
if (reload != null)
reload.remove();*/
let reload = document.getElementById("members");
if (reload != null)
reload.remove();
if (client.channel == undefined || !logged)
return ;
let chats = document.getElementById("chats");
if (document.getElementById("chat") == null) {
let chat = document.createElement("div");
chat.id = "chat";
chats.appendChild(chat);
}
// div des messages
let messages = document.getElementById("messages");
if (messages == null) {
messages = document.createElement("div");
messages.id = "messages";
if (document.getElementById("input_chat") == null)
chat.appendChild(messages);
else
document.getElementById("input_chat").before(messages);
}
// les messages, réecriture seulement du dernier
let i = 0;
client.channel.messages.forEach((message) => {
if (messages.children[i] == null || message.content != messages.children[i].innerText) {
let text = document.createElement("p");
text.appendChild(document.createTextNode(message.content));
if (message.author_id == client.me.user_id)
text.id = "you";
else
text.id = "other";
messages.appendChild(text);
}
i++;
});
// Input pour rentrer un message
if (document.getElementById("input_chat") == null) {
let chat_input = document.createElement("input");
chat_input.id="input_chat";
chat_input.type="text";
chat_input.name="message";
chat_input.placeholder="message bozo";
chat_input.maxLength=255;
chat.appendChild(chat_input);
chat_input.onkeydown = async () => {
if (event.keyCode == 13 && client.channel != undefined) {
//let chat_input = document.getElementById("input_chat");
let chat_text = chat_input.value;
let receivers_id = [];
client.channel.members_id.forEach((member_id) => {
if (member_id != client.me.user_id)
receivers_id.push(users.filter(user => user.user_id == member_id)[0].user_id);
});
await client.channel.sendMessageChannel(chat_text, receivers_id)
// Reset
chat_input.value = "";
}
};
}
// nom des membres du chat
let members = document.createElement("h2");
members.id = "members";
let usernames = "";
client.channel.members_id.forEach((member_id) => {
if (member_id != client.me.user_id) {
if (usernames.length > 0)
usernames += ", ";
usernames += (users.filter(user => user.user_id == member_id)[0].username);
}
});
members.appendChild(document.createTextNode(usernames));
messages.before(members);
// Scroll to the bottom of messages
messages.scrollTop = messages.scrollHeight;
}
async hideChat() {
let close = document.getElementById("chat");
if (close != null)
close.remove();
}
async leavePage() {
if (client.channel != undefined)
client.channel.disconnect();
client.channel = undefined
}
async getHtml() {
return `
<link rel="stylesheet" href="/static/css/search.css">
<div id="chats">
<div id="users">
<input id="input_user" type="text" name="message" placeholder="userbozo"/>
<ul id="list_users">
</ul>
</div>
</div>
`;
}
}

View File

@ -1,14 +0,0 @@
import AbstractView from "./AbstractView.js";
export default class extends AbstractView {
constructor(params) {
super(params, "Settings");
}
async getHtml() {
return `
<h1>Settings</h1>
<p>Manage your privacy and configuration.</p>
`;
}
}

View File

@ -1,4 +1,4 @@
import { client, navigateTo } from "../index.js"; import { client, navigateTo } from "../../index.js";
import AbstractRedirectView from "./AbstractRedirectView.js"; import AbstractRedirectView from "./AbstractRedirectView.js";
export default class extends AbstractRedirectView{ export default class extends AbstractRedirectView{

View File

@ -1,4 +1,4 @@
import { client, navigateTo } from "../index.js"; import { client, navigateTo } from "../../index.js";
import AbstractRedirectView from "./AbstractRedirectView.js"; import AbstractRedirectView from "./AbstractRedirectView.js";
export default class extends AbstractRedirectView{ export default class extends AbstractRedirectView{

View File

@ -1,4 +1,4 @@
import { navigateTo } from "../index.js"; import { navigateTo } from "../../index.js";
import AbstractView from "./AbstractView.js"; import AbstractView from "./AbstractView.js";
export default class extends AbstractView{ export default class extends AbstractView{

View File

@ -1,10 +1,11 @@
import { client, navigateTo } from "../../index.js"; import { client, navigateTo } from "../../index.js";
import AbstractNonAuthentifiedView from "../AbstractNonAuthentified.js"; import { clear, fill_errors } from "../../utils/formUtils.js";
import AbstractNonAuthentifiedView from "../abstracts/AbstractNonAuthentified.js";
async function login() async function login()
{ {
let username = document.getElementById("username").value; let username = document.getElementById("username-input").value;
let password = document.getElementById("password").value; let password = document.getElementById("password-input").value;
let response_data = await client.login(username, password); let response_data = await client.login(username, password);
@ -14,17 +15,8 @@ async function login()
return; return;
} }
["username", "user", "password"].forEach(error_field => { clear("innerHTML", ["username", "user", "password"]);
let error_display = document.getElementById(`error_${error_field}`); fill_errors(response_data, "innerHTML");
if (error_display != null)
error_display.innerHTML = "";
});
Object.keys(response_data).forEach(error_field => {
let error_display = document.getElementById(`error_${error_field}`);
if (error_display != null)
error_display.innerHTML = response_data[error_field];
});
} }
export default class extends AbstractNonAuthentifiedView { export default class extends AbstractNonAuthentifiedView {
@ -34,22 +26,22 @@ export default class extends AbstractNonAuthentifiedView {
async postInit() async postInit()
{ {
document.getElementById("button").onclick = login; document.getElementById("login-button").onclick = login;
} }
async getHtml() { async getHtml() {
return ` return `
<div class=form> <div class=form>
<label>Login</label> <label>Login</label>
<link rel="stylesheet" href="static/css/accounts/login.css"> <link rel="stylesheet" href="/static/css/accounts/login.css">
<input type="text" id="username" placeholder="username"> <input type="text" id="username-input" placeholder="username">
<span id="error_username"></span> <span id="username"></span>
<input type="password" id="password" placeholder="password"> <input type="password" id="password-input" placeholder="password">
<span id="error_password"></span> <span id="password"></span>
<input type="button" value="login" id="button"> <input type="button" value="Login" id="login-button">
<span id="error_user"></span> <span id="user"></span>
<a href="/register" class="nav__link" data-link>Register</a> <a href="/register" class="nav__link" data-link>Register</a>
</div> </div>
`; `;
} }
} }

View File

@ -1,5 +1,5 @@
import { client, navigateTo } from "../../index.js"; import { client, navigateTo } from "../../index.js";
import AbstractAuthentifiedView from "../AbstractAuthentifiedView.js"; import AbstractAuthentifiedView from "../abstracts/AbstractAuthentifiedView.js";
export default class extends AbstractAuthentifiedView export default class extends AbstractAuthentifiedView
{ {
@ -8,4 +8,4 @@ export default class extends AbstractAuthentifiedView
client.logout(); client.logout();
navigateTo("/login") navigateTo("/login")
} }
} }

View File

@ -1,10 +1,11 @@
import { client, navigateTo } from "../../index.js"; import { client, navigateTo } from "../../index.js";
import AbstractAuthentifiedView from "../AbstractNonAuthentified.js"; import { clear, fill_errors } from "../../utils/formUtils.js";
import AbstractNonAuthentifiedView from "../abstracts/AbstractNonAuthentified.js";
async function register() async function register()
{ {
let username = document.getElementById("username").value; let username = document.getElementById("username-input").value;
let password = document.getElementById("password").value; let password = document.getElementById("password-input").value;
let response_data = await client.account.create(username, password); let response_data = await client.account.create(username, password);
@ -14,43 +15,33 @@ async function register()
return; return;
} }
clear("innerHTML", ["username", "user", "password"]);
["username", "user", "password"].forEach(error_field => { fill_errors(response_data, "innerHTML");
let error_display = document.getElementById(`error_${error_field}`);
if (error_display != null)
error_display.innerHTML = "";
});
Object.keys(response_data).forEach(error_field => {
let error_display = document.getElementById(`error_${error_field}`);
if (error_display != null)
error_display.innerHTML = response_data[error_field];
});
} }
export default class extends AbstractAuthentifiedView { export default class extends AbstractNonAuthentifiedView {
constructor(params) { constructor(params) {
super(params, "Register", "/home"); super(params, "Register", "/home");
} }
async postInit() async postInit()
{ {
document.getElementById("button").onclick = register; document.getElementById("register-button").onclick = register;
} }
async getHtml() { async getHtml() {
return ` return `
<div class=form> <div class=form>
<label>Register</label> <label>Register</label>
<link rel="stylesheet" href="static/css/accounts/register.css"> <link rel="stylesheet" href="/static/css/accounts/register.css">
<input type="text" id="username" placeholder="username"> <input type="text" id="username-input" placeholder="username">
<span id="error_username"></span> <span id="username"></span>
<input type="password" id="password" placeholder="password"> <input type="password" id="password-input" placeholder="password">
<span id="error_password"></span> <span id="password"></span>
<input type="button" value="register" id="button"> <input type="button" value="Register" id="register-button">
<span id="error_user"></span> <span id="user"></span>
<a href="/login" class="nav__link" data-link>Login</a> <a href="/login" class="nav__link" data-link>Login</a>
</div> </div>
`; `;
} }
} }

View File

@ -10,11 +10,10 @@
<body> <body>
<nav class="nav"> <nav class="nav">
<a href="/" class="nav__link" data-link>Dashboard</a> <a href="/" class="nav__link" data-link>Dashboard</a>
<a href="/posts" class="nav__link" data-link>Posts</a> <a href="/search" class="nav__link" data-link>Search</a>
<a href="/settings" class="nav__link" data-link>Settings</a> <a href="/home" class="nav__link" data-link>Home</a>
<a href="/login" class="nav__link" data-link>Login</a> <a href="/login" class="nav__link" data-link>Login</a>
<a href="/register" class="nav__link" data-link>Register</a> <a href="/register" class="nav__link" data-link>Register</a>
<a href="/chat" class="nav__link" data-link>Chat</a>
</nav> </nav>
<div id="app"></div> <div id="app"></div>
<script type="module" src="{% static 'js/index.js' %}"></script> <script type="module" src="{% static 'js/index.js' %}"></script>

0
games/__init__.py Normal file
View File

3
games/admin.py Normal file
View File

@ -0,0 +1,3 @@
from django.contrib import admin
# Register your models here.

6
games/apps.py Normal file
View File

@ -0,0 +1,6 @@
from django.apps import AppConfig
class GamesConfig(AppConfig):
default_auto_field = 'django.db.models.BigAutoField'
name = 'games'

14
games/models.py Normal file
View File

@ -0,0 +1,14 @@
from django.db import models
# Create your models here.
class GameModel(models.Model):
def create(self, users_id: [int]):
self.save()
for user_id in users_id:
GameMembersModel(game_id=self.pk, member_id=user_id)
return self.pk
class GameMembersModel(models.Model):
game_id = models.IntegerField()
member_id = models.IntegerField()

3
games/tests.py Normal file
View File

@ -0,0 +1,3 @@
from django.test import TestCase
# Create your tests here.

3
games/views.py Normal file
View File

@ -0,0 +1,3 @@
from django.shortcuts import render
# Create your views here.

0
matchmaking/__init__.py Normal file
View File

3
matchmaking/admin.py Normal file
View File

@ -0,0 +1,3 @@
from django.contrib import admin
# Register your models here.

6
matchmaking/apps.py Normal file
View File

@ -0,0 +1,6 @@
from django.apps import AppConfig
class MatchmakingConfig(AppConfig):
default_auto_field = 'django.db.models.BigAutoField'
name = 'matchmaking'

48
matchmaking/consumers.py Normal file
View File

@ -0,0 +1,48 @@
from channels.generic.websocket import WebsocketConsumer
from django.contrib.auth.models import User
from games.models import GameModel
import json
queue_id: [int] = []
queue_ws: [WebsocketConsumer] = []
class MatchMaking(WebsocketConsumer):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.channel_name = "matchmaking"
self.group_name = "matchmaking"
def connect(self):
user: User = self.scope["user"]
if (user.is_anonymous or not user.is_authenticated):
return
self.channel_layer.group_add(self.group_name, self.channel_name)
self.accept()
global queue_id, queue_ws
queue_id.append(user.pk)
queue_ws.append(self)
if len(set(queue_id)) == 2:
game_id: int = GameModel().create(set(queue_id))
event = {"game_id": game_id}
for ws in queue_ws:
ws.send(text_data=json.dumps({'game_id': game_id}))
queue_id.clear()
queue_ws.clear()
def disconnect(self, close_code):
user: User = self.scope["user"]
global queue_id, queue_ws
if (user.pk in queue_id):
queue_ws.pop(queue_id.index(user.pk))
queue_id.remove(user.pk)
self.channel_layer.group_discard(self.group_name, self.channel_name)

3
matchmaking/models.py Normal file
View File

@ -0,0 +1,3 @@
from django.db import models
# Create your models here.

6
matchmaking/routing.py Normal file
View File

@ -0,0 +1,6 @@
from django.urls import re_path
from . import consumers
websocket_urlpatterns = [
re_path(r'ws/matchmaking/', consumers.MatchMaking.as_asgi())
]

3
matchmaking/tests.py Normal file
View File

@ -0,0 +1,3 @@
from django.test import TestCase
# Create your tests here.

3
matchmaking/views.py Normal file
View File

@ -0,0 +1,3 @@
from django.shortcuts import render
# Create your views here.

6
package-lock.json generated Normal file
View File

@ -0,0 +1,6 @@
{
"name": "ft_transcendence",
"lockfileVersion": 3,
"requires": true,
"packages": {}
}

View File

@ -1,6 +1,7 @@
from django.contrib import admin from django.contrib import admin
from .models import ProfileModel from .models import ProfileModel, BlockModel
# Register your models here. # Register your models here.
admin.site.register(ProfileModel) admin.site.register(ProfileModel)
admin.site.register(BlockModel)

View File

@ -3,14 +3,26 @@ from django.db import models
from django.contrib.auth.models import User from django.contrib.auth.models import User
from django.db.models.signals import post_save from django.db.models.signals import post_save
from django.dispatch import receiver from django.dispatch import receiver
from django.conf import settings
from django.db.models import IntegerField
def upload_to(instance, filename: str):
return f"./profiles/static/avatars/{instance.pk}.{filename.split('.')[1]}"
# Create your models here. # Create your models here.
class ProfileModel(models.Model): class ProfileModel(models.Model):
user = models.ForeignKey(User, on_delete=models.CASCADE) user = models.ForeignKey(User, on_delete=models.CASCADE)
title = models.CharField(max_length=40) avatar_url = models.ImageField(upload_to=upload_to, default="./profiles/static/avatars/default.avif") #blank=True, null=True)
@receiver(post_save, sender=User) @receiver(post_save, sender=User)
def on_user_created(sender, instance, created, **kwargs): def on_user_created(sender, instance, created, **kwargs):
if created: if created:
profile: ProfileModel = ProfileModel.objects.create(pk = instance.pk, user = instance) profile: ProfileModel = ProfileModel.objects.create(pk = instance.pk, user = instance)
profile.save() profile.save()
class BlockModel(models.Model):
blocker = IntegerField(primary_key=False)
blocked = IntegerField(primary_key=False)
def __str__(self):
return "blocker_id: " + str(self.blocker) + ", blocked_id: " + str(self.blocked)

11
profiles/serializers.py Normal file
View File

@ -0,0 +1,11 @@
from rest_framework import serializers
from .models import ProfileModel
class ProfileSerializer(serializers.ModelSerializer):
username = serializers.ReadOnlyField(source='user.username')
avatar_url = serializers.ImageField(required=False)
class Meta:
model = ProfileModel
fields = ["username", "avatar_url", "user_id"]

Binary file not shown.

After

Width:  |  Height:  |  Size: 35 KiB

View File

@ -1 +0,0 @@
PROFILE_NOT_FOUND = "Profile Not Found"

View File

@ -1,7 +1,15 @@
from django.urls import path from django.urls import path
from django.conf import settings
from django.conf.urls.static import static
from . import viewsets
from . import views from . import views
urlpatterns = [ urlpatterns = [
path("<int:pk>", views.ProfileView.as_view(), name="profile_page"), path("me", viewsets.MyProfileViewSet.as_view({'patch': 'partial_update', 'get': 'retrieve'}), name="my_profile_page"),
] path("<int:pk>", viewsets.ProfileViewSet.as_view({'get': 'retrieve'}), name="profile_page"),
path("", viewsets.ProfileViewSet.as_view({'get': 'list'}), name="profiles_list"),
path("block", views.BlocksView.as_view(), name="block_page"),
path("block/<int:pk>", views.BlockView.as_view(), name="block_page"),
] + static("/static/avatars/", document_root="./avatars")

View File

@ -1,19 +1,66 @@
from django.http import HttpRequest
from rest_framework.views import APIView from rest_framework.views import APIView
from rest_framework.response import Response from rest_framework.response import Response
from rest_framework import permissions, status from rest_framework import authentication, permissions, status
from rest_framework.authentication import SessionAuthentication
from rest_framework.renderers import JSONRenderer
from django.core import serializers
from .models import BlockModel
from .models import ProfileModel class BlockView(APIView):
permission_classes = (permissions.IsAuthenticated,)
authentication_classes = (SessionAuthentication,)
# Create your views here. def get(self, request, pk):
class ProfileView(APIView): block = BlockModel.objects.filter(pk=pk)
permission_classes = (permissions.AllowAny,) if (block):
return Response(serializers.serialize("json", block), status=status.HTTP_200_OK)
else:
return Response("Not Found", status=status.HTTP_404_NOT_FOUND)
def get(self, request: HttpRequest, pk: int):
class BlocksView(APIView):
profile: ProfileModel = ProfileModel.objects.get(pk=pk) permission_classes = (permissions.IsAuthenticated,)
if (profile is None): authentication_classes = (SessionAuthentication,)
return Response(status=status.HTTP_404_NOT_FOUND)
def get(self, request):
return Response(status=status.HTTP_200_OK, data={'name': profile.user.username, blocks = BlockModel.objects.filter(blocker=request.user.pk)
'title': profile.title}) if (blocks):
return Response(serializers.serialize("json", BlockModel.objects.filter(blocker=request.user.pk)), status=status.HTTP_200_OK)
else:
return Response({}, status=status.HTTP_404_NOT_FOUND)
def post(self, request):
data: dict = request.data
users_id = request.data.get("users_id", None)
if (users_id == None):
return Response({"Error"}, status=status.HTTP_400_BAD_REQUEST)
if (BlockModel.objects.filter(blocker=users_id[0], blocked=users_id[1])):
return Response({"Already Exist"}, status=status.HTTP_409_CONFLICT)
new_block = BlockModel()
new_block.blocker = users_id[0]
new_block.blocked = users_id[1]
new_block.save()
return Response({"block_id": new_block.pk}, status=status.HTTP_201_CREATED)
def delete(self, request):
data: dict = request.data
users_id = request.data.get("users_id", None)
if (users_id == None):
return Response({"Error"}, status=status.HTTP_400_BAD_REQUEST)
block = BlockModel.objects.filter(blocker=users_id[0], blocked=users_id[1])
print(list(block))
if (block.count() > 1):
return Response("Not normal >:[", status=status.HTTP_500_INTERNAL_SERVER_ERROR)
if (not block):
return Response("Don't exist", status=status.HTTP_404_NOT_FOUND)
block.delete()
return Response("Deleted", status=status.HTTP_200_OK)

61
profiles/viewsets.py Normal file
View File

@ -0,0 +1,61 @@
from rest_framework import permissions
from rest_framework.parsers import MultiPartParser, FormParser
from rest_framework import permissions, status
from rest_framework import viewsets
from rest_framework.response import Response
from rest_framework.authentication import SessionAuthentication
from django.http import HttpRequest
from django.db.models import QuerySet
from .serializers import ProfileSerializer
from .models import ProfileModel
class ProfileViewSet(viewsets.ModelViewSet):
queryset = ProfileModel.objects.all
serializer_class = ProfileSerializer
parser_classes = (MultiPartParser, FormParser)
permission_classes = (permissions.IsAuthenticatedOrReadOnly,)
def retrieve(self, request: HttpRequest, pk=None):
if (not self.queryset().filter(pk=pk).exists()):
return Response({"detail": "Profile not found."}, status=status.HTTP_404_NOT_FOUND)
instance = self.queryset().get(pk=pk)
instance.avatar_url.name = instance.avatar_url.name[instance.avatar_url.name.find("static") - 1:]
return Response(self.serializer_class(instance).data,
status=status.HTTP_200_OK)
def list(self, request: HttpRequest):
serializer = ProfileSerializer(self.queryset(), many=True)
for profile in serializer.data:
profile["avatar_url"] = profile["avatar_url"][profile["avatar_url"].find("static") - 1:]
return Response(serializer.data)
def perform_create(self, serializer):
serializer.save(user=self.request.user)
class MyProfileViewSet(viewsets.ModelViewSet):
permission_classes = (permissions.IsAuthenticated,)
authentication_classes = (SessionAuthentication,)
serializer_class = ProfileSerializer
queryset = ProfileModel.objects.all
def get_object(self):
obj = self.queryset().get(pk=self.request.user.pk)
return obj
def perform_update(self, serializer, pk=None):
profile: ProfileModel = self.get_object()
avatar = self.request.data.get("file", None)
if (avatar is not None):
if (profile.avatar_url.name != "./profiles/static/avatars/default.avif"):
profile.avatar_url.storage.delete(profile.avatar_url.name)
profile.avatar_url = avatar
profile.save()
def retrieve(self, request: HttpRequest, pk=None):
instance: ProfileModel = self.get_object()
instance.avatar_url.name = instance.avatar_url.name[instance.avatar_url.name.find("static") - 1:]
return Response(self.serializer_class(instance).data,
status=status.HTTP_200_OK)

View File

@ -1,10 +1,30 @@
asgiref==3.7.2 asgiref==3.7.2
attrs==23.1.0
autobahn==23.6.2
Automat==22.10.0
cffi==1.16.0
channels==4.0.0
constantly==23.10.4
cryptography==41.0.7
daphne==4.0.0
Django==4.2.6 Django==4.2.6
django-cors-headers==4.3.0 django-cors-headers==4.3.0
django-restapi==0.1.3 django-restapi==0.1.3
djangorestframework==3.14.0 djangorestframework==3.14.0
hyperlink==21.0.0
idna==3.6
incremental==22.10.0
install==1.3.5 install==1.3.5
Pillow==10.1.0
pyasn1==0.5.1
pyasn1-modules==0.3.0
pycparser==2.21
pyOpenSSL==23.3.0
pytz==2023.3.post1 pytz==2023.3.post1
service-identity==23.1.0
six==1.16.0
sqlparse==0.4.4 sqlparse==0.4.4
channels==4.0.0 Twisted==23.10.0
daphne txaio==23.1.1
typing_extensions==4.8.0
zope.interface==6.1

View File

@ -10,17 +10,20 @@ https://docs.djangoproject.com/en/4.2/howto/deployment/asgi/
import os import os
from channels.routing import ProtocolTypeRouter, URLRouter from channels.routing import ProtocolTypeRouter, URLRouter
from channels.auth import AuthMiddlewareStack from channels.auth import AuthMiddlewareStack
import chat.routing import chat.routing
import matchmaking.routing
from django.core.asgi import get_asgi_application from django.core.asgi import get_asgi_application
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'trancendence.settings') os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'trancendence.settings')
application = ProtocolTypeRouter({ application = ProtocolTypeRouter({
'http':get_asgi_application(), 'http':get_asgi_application(),
'websocket':AuthMiddlewareStack( 'websocket':AuthMiddlewareStack(
URLRouter( URLRouter(
chat.routing.websocket_urlpatterns chat.routing.websocket_urlpatterns +
matchmaking.routing.websocket_urlpatterns
) )
) )
}) })

View File

@ -10,6 +10,8 @@ For the full list of settings and their values, see
https://docs.djangoproject.com/en/4.2/ref/settings/ https://docs.djangoproject.com/en/4.2/ref/settings/
""" """
import os
from pathlib import Path from pathlib import Path
# Build paths inside the project like this: BASE_DIR / 'subdir'. # Build paths inside the project like this: BASE_DIR / 'subdir'.
@ -41,9 +43,12 @@ INSTALLED_APPS = [
'channels', 'channels',
'daphne', 'daphne',
'matchmaking.apps.MatchmakingConfig',
'games.apps.GamesConfig',
'accounts.apps.AccountsConfig', 'accounts.apps.AccountsConfig',
'profiles.apps.ProfilesConfig', 'profiles.apps.ProfilesConfig',
'frontend.apps.FrontendConfig', 'frontend.apps.FrontendConfig',
'chat.apps.ChatConfig',
'corsheaders', 'corsheaders',
'rest_framework', 'rest_framework',

View File

@ -21,5 +21,6 @@ urlpatterns = [
path('admin/', admin.site.urls), path('admin/', admin.site.urls),
path('api/profiles/', include('profiles.urls')), path('api/profiles/', include('profiles.urls')),
path('api/accounts/', include('accounts.urls')), path('api/accounts/', include('accounts.urls')),
path('api/chat/', include('chat.urls')),
path('', include('frontend.urls')), path('', include('frontend.urls')),
] ]