10 Commits

Author SHA1 Message Date
ae20be25fb add: matchmaking 2023-12-12 12:06:13 +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
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
336257d1d0 init view 2023-12-11 13:40:01 +01:00
f66b3883c1 init app 2023-12-11 13:39:53 +01:00
32 changed files with 204 additions and 120 deletions

2
.gitignore vendored
View File

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

View File

@ -22,8 +22,8 @@ pip install -r requirements.txt
``` ```
- Setup database - Setup database
``` ```
python manage.py runserver makemigrations profiles python manage.py runserver makemigrations profiles games
python manage.py migrate profiles python manage.py migrate profiles games
``` ```
- Start the developpement server - Start the developpement server
``` ```

View File

@ -22,15 +22,15 @@ class ChatConsumer(WebsocketConsumer):
self.room_group_name, self.room_group_name,
{ {
'type':'chat_message', 'type':'chat_message',
'username':self.scope["user"].username,
'message':message 'message':message
} }
) )
def chat_message(self, event): def chat_message(self, event):
message = event['message']
self.send(text_data=json.dumps({ self.send(text_data=json.dumps({
'type':'chat', 'type':'chat',
'username':event['username'], 'username':self.scope["user"].username,
'message':event['message'] 'message':message
})) }))

View File

@ -1,17 +0,0 @@
#app img
{
max-height: 100px;
max-width: 100px;
}
#app ul
{
margin: 5px 0 0 0;
padding: 0 0 0 0;
list-style-type: none;
}
#app li
{
margin: 10px 10px 0 0;
}

View File

@ -1,4 +1,5 @@
import { Account } from "./account.js"; import { Account } from "./account.js";
import { MatchMaking } from "./matchmaking.js";
import { Profile } from "./profile.js"; import { Profile } from "./profile.js";
import { Profiles } from "./profiles.js"; import { Profiles } from "./profiles.js";
@ -19,6 +20,7 @@ class Client
this._url = url; this._url = url;
this.account = new Account(this); this.account = new Account(this);
this.profiles = new Profiles(this); this.profiles = new Profiles(this);
this.matchmaking = new MatchMaking(this);
this._logged = undefined; this._logged = undefined;
} }
@ -122,4 +124,4 @@ class Client
} }
} }
export {Client} export {Client}

View File

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

View File

@ -8,19 +8,19 @@ class Profile
this.user_id = user_id this.user_id = user_id
} }
async init(id) async init(user_id)
{ {
let response = await this.client._get(`/api/profiles/${id}`); let response = await this.client._get(`/api/profiles/${user_id}`);
let response_data = await response.json(); let response_data = await response.json();
this.id = id; this.user_id = user_id;
this.username = response_data.username; this.username = response_data.username;
this.avatar_url = response_data.avatar_url; this.avatar_url = response_data.avatar_url;
} }
async change_avatar(form_data) async change_avatar(form_data)
{ {
let response = await this.client._patch_file(`/api/profiles/${this.id}`, form_data); let response = await this.client._patch_file(`/api/profiles/${this.user_id}`, form_data);
let response_data = await response.json() let response_data = await response.json()
return response_data; return response_data;

View File

@ -1,7 +1,6 @@
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 Settings from "./views/Settings.js"; import Settings from "./views/Settings.js";
import Search from "./views/Search.js";
import Chat from "./views/Chat.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";
@ -12,6 +11,7 @@ import AbstractRedirectView from "./views/AbstractRedirectView.js";
import MeView from "./views/MeView.js"; import MeView from "./views/MeView.js";
import ProfilePageView from "./views/profiles/ProfilePageView.js"; import ProfilePageView from "./views/profiles/ProfilePageView.js";
import ProfilesView from "./views/profiles/ProfilesView.js"; import ProfilesView from "./views/profiles/ProfilesView.js";
import MatchMakingView from "./views/MatchMakingView.js";
let client = new Client(location.protocol + "//" + location.host) let client = new Client(location.protocol + "//" + location.host)
@ -33,7 +33,7 @@ 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: "/profiles", view: ProfilesView}, { path: "/profiles", view: ProfilesView},
@ -42,10 +42,10 @@ const router = async (uri = "") => {
{ 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: "/search", view: Search },
{ path: "/chat", view: Chat }, { path: "/chat", view: Chat },
{ path: "/home", view: HomeView }, { path: "/home", view: HomeView },
{ path: "/me", view: MeView }, { path: "/me", view: MeView },
{ path: "/matchmaking", view: MatchMakingView },
]; ];
// Test each route for potential match // Test each route for potential match
@ -86,7 +86,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 => {
@ -98,4 +98,4 @@ document.addEventListener("DOMContentLoaded", () => {
router(location.pathname); router(location.pathname);
}); });
export { client, navigateTo } export { client, navigateTo }

View File

@ -4,7 +4,7 @@ export default class extends AbstractAuthentifiedView {
constructor(params) { constructor(params) {
super(params, "Chat"); super(params, "Chat");
let url = `ws://${window.location.host}/ws/socket-server/` let url = `wss://${window.location.host}/ws/socket-server/`
this.chatSocket = new WebSocket(url) this.chatSocket = new WebSocket(url)
this.chatSocket.onmessage = function(e){ this.chatSocket.onmessage = function(e){
@ -44,7 +44,7 @@ export default class extends AbstractAuthentifiedView {
<h1>Chat</h1> <h1>Chat</h1>
<form id="form"> <form id="form">
<input type="text" name="message" placeholder="message"/> <input type="text" name="message" />
</form> </form>
<div id="messages"> <div id="messages">

View File

@ -9,6 +9,7 @@ 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="/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,30 @@
import { client, navigateTo } from "../index.js";
import AbstractView from "./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)
console.log("start matchmaking")
}
async getHtml() {
return `
<h1>finding<h1>
`;
}
async leavePage()
{
await client.matchmaking.stop();
}
}

View File

@ -1,74 +0,0 @@
import AbstractView from "./AbstractView.js";
import {client} from "../index.js";
export default class extends AbstractView {
constructor(params) {
super(params, "Search");
}
async postInit() {
let search = document.getElementById("form");
search.addEventListener("input", this.users)
this.users();
}
async users() {
let search = document.getElementById("form").value;
let logged = client.isAuthentificate();
let users = await client.profiles.all();
let list_users = document.getElementById('list_users');
list_users.innerHTML = "";
users.filter(user => user.username.startsWith(search) == true).forEach((user) => {
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
let chat = document.createElement("a");
chat.href = `/chat`
chat.appendChild(document.createTextNode("Chat"));
new_user.appendChild(chat);
// 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 getHtml() {
return `
<link rel="stylesheet" href="/static/css/search.css">
<input id="form" type="text" name="message" placeholder="userbozo"/>
<div id="users">
<ul id="list_users">
</ul>
</div>
`;
}
}

View File

@ -52,4 +52,4 @@ export default class extends AbstractNonAuthentifiedView {
</div> </div>
`; `;
} }
} }

View File

@ -13,7 +13,7 @@
<a href="/profiles" class="nav__link" data-link>Profiles</a> <a href="/profiles" class="nav__link" data-link>Profiles</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="/search" class="nav__link" data-link>Search</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
View File

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

View File

@ -17,7 +17,7 @@ class ProfileViewSet(viewsets.ModelViewSet):
permission_classes = (permissions.IsAuthenticatedOrReadOnly,) permission_classes = (permissions.IsAuthenticatedOrReadOnly,)
def retrieve(self, request: HttpRequest, pk=None): def retrieve(self, request: HttpRequest, pk=None):
instance = self.get_object() instance = ProfileModel.objects.get(pk=pk)
instance.avatar_url.name = instance.avatar_url.name[instance.avatar_url.name.find("static") - 1:] instance.avatar_url.name = instance.avatar_url.name[instance.avatar_url.name.find("static") - 1:]
return Response(self.serializer_class(instance).data, return Response(self.serializer_class(instance).data,
status=status.HTTP_200_OK) status=status.HTTP_200_OK)

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

@ -43,6 +43,8 @@ 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',