sessions, or something
This commit is contained in:
+12
-2
@@ -8,9 +8,19 @@ https://docs.djangoproject.com/en/4.2/howto/deployment/asgi/
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
import os
|
import os
|
||||||
|
|
||||||
from django.core.asgi import get_asgi_application
|
from django.core.asgi import get_asgi_application
|
||||||
|
from channels.routing import ProtocolTypeRouter, URLRouter
|
||||||
|
from channels.auth import AuthMiddlewareStack
|
||||||
|
from muzak.auth import QueryAuthMiddleware
|
||||||
|
import voting.routing
|
||||||
|
|
||||||
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'muzak.settings')
|
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'muzak.settings')
|
||||||
|
|
||||||
application = get_asgi_application()
|
application = ProtocolTypeRouter({
|
||||||
|
"http": get_asgi_application(),
|
||||||
|
"websocket": QueryAuthMiddleware(
|
||||||
|
URLRouter(
|
||||||
|
voting.routing.websocket_urlpatterns
|
||||||
|
)
|
||||||
|
),
|
||||||
|
})
|
||||||
|
|||||||
+46
-11
@@ -1,7 +1,19 @@
|
|||||||
from rest_framework.authentication import BaseAuthentication
|
from rest_framework.authentication import BaseAuthentication
|
||||||
from rest_framework.exceptions import AuthenticationFailed
|
from rest_framework.exceptions import AuthenticationFailed
|
||||||
from mozilla_django_oidc.auth import OIDCAuthenticationBackend
|
from mozilla_django_oidc.auth import OIDCAuthenticationBackend
|
||||||
from django.contrib.auth.models import User
|
from django.contrib.auth.models import User, AnonymousUser
|
||||||
|
from channels.db import database_sync_to_async
|
||||||
|
|
||||||
|
def get_user_from_token(token, log=False):
|
||||||
|
if log:
|
||||||
|
print("verifying token:", token)
|
||||||
|
backend = OIDCAuthenticationBackend()
|
||||||
|
try:
|
||||||
|
claims = backend.verify_token(token)
|
||||||
|
except Exception as e:
|
||||||
|
raise AuthenticationFailed(f'Invalid token: {e}')
|
||||||
|
return backend.filter_users_by_claims(claims).first()
|
||||||
|
|
||||||
|
|
||||||
class OIDCBearerTokenAuthentication(BaseAuthentication):
|
class OIDCBearerTokenAuthentication(BaseAuthentication):
|
||||||
def authenticate(self, request):
|
def authenticate(self, request):
|
||||||
@@ -9,15 +21,38 @@ class OIDCBearerTokenAuthentication(BaseAuthentication):
|
|||||||
if not auth.startswith('Bearer '):
|
if not auth.startswith('Bearer '):
|
||||||
return None
|
return None
|
||||||
token = auth.split(' ')[1]
|
token = auth.split(' ')[1]
|
||||||
|
user = get_user_from_token(token)
|
||||||
backend = OIDCAuthenticationBackend()
|
|
||||||
|
|
||||||
try:
|
|
||||||
claims = backend.verify_token(token)
|
|
||||||
except Exception as e:
|
|
||||||
raise AuthenticationFailed(f'Invalid token: {e}')
|
|
||||||
|
|
||||||
user = backend.filter_users_by_claims(claims).first()
|
|
||||||
if not user:
|
if not user:
|
||||||
raise AuthenticationFailed(f'Unknown user: {claims}')
|
raise AuthenticationFailed(f'Unknown user: {token}')
|
||||||
return (user, None)
|
return (user, None)
|
||||||
|
|
||||||
|
# @database_sync_to_async
|
||||||
|
# def get_user(user_id):
|
||||||
|
# try:
|
||||||
|
# return User.objects.get(id=user_id)
|
||||||
|
# except User.DoesNotExist:
|
||||||
|
# return AnonymousUser()
|
||||||
|
|
||||||
|
class QueryAuthMiddleware:
|
||||||
|
"""
|
||||||
|
Custom middleware that takes user from passed token.
|
||||||
|
"""
|
||||||
|
def __init__(self, app):
|
||||||
|
# Store the ASGI application we were passed
|
||||||
|
self.app = app
|
||||||
|
|
||||||
|
async def __call__(self, scope, receive, send):
|
||||||
|
scope['user'] = AnonymousUser()
|
||||||
|
# print("Authing", scope)
|
||||||
|
# data = await receive()
|
||||||
|
# print("data: ", data)
|
||||||
|
# token = data.get('token')
|
||||||
|
# if token:
|
||||||
|
# user = database_sync_to_async(get_user_from_token(token, print=True))
|
||||||
|
# print("USER?", user)
|
||||||
|
# if user:
|
||||||
|
# scope['user'] = user
|
||||||
|
# else:
|
||||||
|
# pass
|
||||||
|
|
||||||
|
return await self.app(scope, receive, send)
|
||||||
|
|||||||
@@ -18,7 +18,9 @@ CORS_ALLOW_ALL_ORIGINS = True
|
|||||||
CURRENT_HOST = 'http://127.0.0.1:8000' # Needs to be set for spotify callback to work.
|
CURRENT_HOST = 'http://127.0.0.1:8000' # Needs to be set for spotify callback to work.
|
||||||
|
|
||||||
INSTALLED_APPS = [
|
INSTALLED_APPS = [
|
||||||
|
'daphne',
|
||||||
'playlist.apps.PlaylistConfig',
|
'playlist.apps.PlaylistConfig',
|
||||||
|
'voting.apps.VotingConfig',
|
||||||
'django.contrib.admin',
|
'django.contrib.admin',
|
||||||
'django.contrib.auth',
|
'django.contrib.auth',
|
||||||
'mozilla_django_oidc',
|
'mozilla_django_oidc',
|
||||||
@@ -30,6 +32,7 @@ INSTALLED_APPS = [
|
|||||||
'rest_framework',
|
'rest_framework',
|
||||||
'corsheaders',
|
'corsheaders',
|
||||||
'drf_spectacular',
|
'drf_spectacular',
|
||||||
|
'channels',
|
||||||
]
|
]
|
||||||
|
|
||||||
MIDDLEWARE = [
|
MIDDLEWARE = [
|
||||||
@@ -48,7 +51,7 @@ ROOT_URLCONF = 'muzak.urls'
|
|||||||
TEMPLATES = [
|
TEMPLATES = [
|
||||||
{
|
{
|
||||||
'BACKEND': 'django.template.backends.django.DjangoTemplates',
|
'BACKEND': 'django.template.backends.django.DjangoTemplates',
|
||||||
'DIRS': [],
|
'DIRS': [BASE_DIR / 'templates'],
|
||||||
'APP_DIRS': True,
|
'APP_DIRS': True,
|
||||||
'OPTIONS': {
|
'OPTIONS': {
|
||||||
'context_processors': [
|
'context_processors': [
|
||||||
@@ -62,6 +65,16 @@ TEMPLATES = [
|
|||||||
]
|
]
|
||||||
|
|
||||||
WSGI_APPLICATION = 'muzak.wsgi.application'
|
WSGI_APPLICATION = 'muzak.wsgi.application'
|
||||||
|
ASGI_APPLICATION = 'muzak.asgi.application'
|
||||||
|
|
||||||
|
CHANNEL_LAYERS = {
|
||||||
|
"default": {
|
||||||
|
"BACKEND": "channels_redis.core.RedisChannelLayer",
|
||||||
|
"CONFIG": {
|
||||||
|
"hosts": [("valkey", 6379)],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
DATABASES = {
|
DATABASES = {
|
||||||
'default': {
|
'default': {
|
||||||
|
|||||||
@@ -20,5 +20,6 @@ from django.urls import path, include
|
|||||||
urlpatterns = [
|
urlpatterns = [
|
||||||
path('admin/', admin.site.urls),
|
path('admin/', admin.site.urls),
|
||||||
path('oidc/', include('mozilla_django_oidc.urls')),
|
path('oidc/', include('mozilla_django_oidc.urls')),
|
||||||
|
path('voting/', include('voting.urls')),
|
||||||
path('', include('playlist.urls')),
|
path('', include('playlist.urls')),
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -1,8 +1,10 @@
|
|||||||
from django.contrib import admin
|
from django.contrib import admin
|
||||||
from django.utils.http import urlencode
|
from django.utils.http import urlencode
|
||||||
from django.shortcuts import redirect
|
from django.shortcuts import redirect
|
||||||
from .models import Track, Artist, Album, Background, Vote, Profile
|
from .models import Track, Artist, Album, Background, Vote, Profile, Session
|
||||||
|
|
||||||
|
class SessionAdmin(admin.ModelAdmin):
|
||||||
|
pass
|
||||||
class TrackAdmin(admin.ModelAdmin):
|
class TrackAdmin(admin.ModelAdmin):
|
||||||
pass
|
pass
|
||||||
class ArtistAdmin(admin.ModelAdmin):
|
class ArtistAdmin(admin.ModelAdmin):
|
||||||
@@ -34,3 +36,4 @@ admin.site.register(Album, AlbumAdmin)
|
|||||||
admin.site.register(Background, BackgroundAdmin)
|
admin.site.register(Background, BackgroundAdmin)
|
||||||
admin.site.register(Vote, VoteAdmin)
|
admin.site.register(Vote, VoteAdmin)
|
||||||
admin.site.register(Profile, ProfileAdmin)
|
admin.site.register(Profile, ProfileAdmin)
|
||||||
|
admin.site.register(Session, SessionAdmin)
|
||||||
|
|||||||
@@ -0,0 +1,98 @@
|
|||||||
|
# Generated by Django 5.2.4 on 2025-08-07 17:36
|
||||||
|
|
||||||
|
import django.core.validators
|
||||||
|
import django.db.models.deletion
|
||||||
|
import uuid
|
||||||
|
from django.conf import settings
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
("playlist", "0022_background_cover"),
|
||||||
|
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name="vote",
|
||||||
|
name="points",
|
||||||
|
field=models.IntegerField(
|
||||||
|
validators=[
|
||||||
|
django.core.validators.MinValueValidator(1),
|
||||||
|
django.core.validators.MaxValueValidator(5),
|
||||||
|
]
|
||||||
|
),
|
||||||
|
),
|
||||||
|
migrations.CreateModel(
|
||||||
|
name="Session",
|
||||||
|
fields=[
|
||||||
|
(
|
||||||
|
"id",
|
||||||
|
models.BigAutoField(
|
||||||
|
auto_created=True,
|
||||||
|
primary_key=True,
|
||||||
|
serialize=False,
|
||||||
|
verbose_name="ID",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
("opens", models.DateTimeField(default=None, null=True)),
|
||||||
|
("session_id", models.UUIDField(default=uuid.uuid4, editable=False)),
|
||||||
|
("seed", models.IntegerField(default=892933)),
|
||||||
|
(
|
||||||
|
"status",
|
||||||
|
models.CharField(
|
||||||
|
choices=[
|
||||||
|
("lobby", "Lobby"),
|
||||||
|
("voting", "Voting"),
|
||||||
|
("waiting_for_next_track", "Waiting for Next Track"),
|
||||||
|
("paused", "Paused"),
|
||||||
|
("voting_done", "Voting Done"),
|
||||||
|
],
|
||||||
|
default="lobby",
|
||||||
|
max_length=30,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
("voting_open", models.BooleanField(default=False)),
|
||||||
|
(
|
||||||
|
"current_track",
|
||||||
|
models.ForeignKey(
|
||||||
|
blank=True,
|
||||||
|
null=True,
|
||||||
|
on_delete=django.db.models.deletion.SET_NULL,
|
||||||
|
to="playlist.track",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"host",
|
||||||
|
models.ForeignKey(
|
||||||
|
on_delete=django.db.models.deletion.CASCADE,
|
||||||
|
to=settings.AUTH_USER_MODEL,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"players_connected",
|
||||||
|
models.ManyToManyField(
|
||||||
|
related_name="sessions_joined", to=settings.AUTH_USER_MODEL
|
||||||
|
),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"players_wanted",
|
||||||
|
models.ManyToManyField(
|
||||||
|
related_name="players_wanted", to=settings.AUTH_USER_MODEL
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name="vote",
|
||||||
|
name="session",
|
||||||
|
field=models.ForeignKey(
|
||||||
|
default=None,
|
||||||
|
null=True,
|
||||||
|
on_delete=django.db.models.deletion.CASCADE,
|
||||||
|
to="playlist.session",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
]
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
# Generated by Django 5.2.4 on 2025-08-09 06:41
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
("playlist", "0023_alter_vote_points_session_vote_session"),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name="session",
|
||||||
|
name="seed",
|
||||||
|
field=models.IntegerField(default=689663),
|
||||||
|
),
|
||||||
|
]
|
||||||
+27
@@ -0,0 +1,27 @@
|
|||||||
|
# Generated by Django 5.2.4 on 2025-08-09 08:42
|
||||||
|
|
||||||
|
from django.conf import settings
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
("playlist", "0024_alter_session_seed"),
|
||||||
|
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name="session",
|
||||||
|
name="players_connected",
|
||||||
|
field=models.ManyToManyField(
|
||||||
|
null=True, related_name="players_joined", to=settings.AUTH_USER_MODEL
|
||||||
|
),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name="session",
|
||||||
|
name="seed",
|
||||||
|
field=models.IntegerField(default=804599),
|
||||||
|
),
|
||||||
|
]
|
||||||
+27
@@ -0,0 +1,27 @@
|
|||||||
|
# Generated by Django 5.2.4 on 2025-08-09 08:42
|
||||||
|
|
||||||
|
from django.conf import settings
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
("playlist", "0025_alter_session_players_connected_alter_session_seed"),
|
||||||
|
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name="session",
|
||||||
|
name="players_connected",
|
||||||
|
field=models.ManyToManyField(
|
||||||
|
blank=True, related_name="players_joined", to=settings.AUTH_USER_MODEL
|
||||||
|
),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name="session",
|
||||||
|
name="seed",
|
||||||
|
field=models.IntegerField(default=456463),
|
||||||
|
),
|
||||||
|
]
|
||||||
@@ -4,3 +4,4 @@ from .album import Album
|
|||||||
from .vote import Vote
|
from .vote import Vote
|
||||||
from .profile import Profile, Background
|
from .profile import Profile, Background
|
||||||
from .playlist import Playlist
|
from .playlist import Playlist
|
||||||
|
from .session import Session
|
||||||
|
|||||||
@@ -0,0 +1,33 @@
|
|||||||
|
from django.db import models
|
||||||
|
from django.contrib.auth.models import User
|
||||||
|
import uuid
|
||||||
|
import random
|
||||||
|
from datetime import datetime
|
||||||
|
|
||||||
|
class Session(models.Model):
|
||||||
|
def __str__(self):
|
||||||
|
return f"Session {self.session_id} - Host: {self.host.username}"
|
||||||
|
|
||||||
|
@property
|
||||||
|
def is_open(self):
|
||||||
|
return datetime.now() < self.opens
|
||||||
|
|
||||||
|
opens = models.DateTimeField(null=True, default=None)
|
||||||
|
session_id = models.UUIDField(default=uuid.uuid4, editable=False)
|
||||||
|
host = models.ForeignKey(User, on_delete=models.CASCADE)
|
||||||
|
players_wanted = models.ManyToManyField(User, related_name='players_wanted')
|
||||||
|
players_connected= models.ManyToManyField(User, related_name='players_joined', blank=True)
|
||||||
|
seed = models.IntegerField(default=random.randint(1, 1000000))
|
||||||
|
status = models.CharField(
|
||||||
|
max_length=30,
|
||||||
|
choices=[
|
||||||
|
('lobby', 'Lobby'),
|
||||||
|
('voting', 'Voting'),
|
||||||
|
('waiting_for_next_track', 'Waiting for Next Track'),
|
||||||
|
('paused', 'Paused'),
|
||||||
|
('voting_done', 'Voting Done'),
|
||||||
|
],
|
||||||
|
default='lobby'
|
||||||
|
)
|
||||||
|
current_track = models.ForeignKey('Track', on_delete=models.SET_NULL, null=True, blank=True)
|
||||||
|
voting_open = models.BooleanField(default=False)
|
||||||
@@ -1,6 +1,7 @@
|
|||||||
from django.db import models
|
from django.db import models
|
||||||
from django.contrib.auth.models import User
|
from django.contrib.auth.models import User
|
||||||
from django.core.validators import MinValueValidator, MaxValueValidator
|
from django.core.validators import MinValueValidator, MaxValueValidator
|
||||||
|
from .session import Session
|
||||||
|
|
||||||
class Vote(models.Model):
|
class Vote(models.Model):
|
||||||
class Meta:
|
class Meta:
|
||||||
@@ -9,3 +10,4 @@ class Vote(models.Model):
|
|||||||
user = models.ForeignKey(User, on_delete=models.CASCADE)
|
user = models.ForeignKey(User, on_delete=models.CASCADE)
|
||||||
points = models.IntegerField(validators=[MinValueValidator(1), MaxValueValidator(5)])
|
points = models.IntegerField(validators=[MinValueValidator(1), MaxValueValidator(5)])
|
||||||
skipped = models.BooleanField(default=False)
|
skipped = models.BooleanField(default=False)
|
||||||
|
session = models.ForeignKey(Session, on_delete=models.CASCADE, null=True, default=None)
|
||||||
|
|||||||
@@ -4,3 +4,4 @@ from .user import *
|
|||||||
from .album import *
|
from .album import *
|
||||||
from .profile import *
|
from .profile import *
|
||||||
from .vote import *
|
from .vote import *
|
||||||
|
from .session import *
|
||||||
|
|||||||
@@ -0,0 +1,9 @@
|
|||||||
|
from rest_framework import serializers
|
||||||
|
from playlist.models import Session
|
||||||
|
|
||||||
|
class SessionSerializer(serializers.ModelSerializer):
|
||||||
|
host = serializers.EmailField(source='host.email', read_only=True)
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
model = Session
|
||||||
|
fields = '__all__'
|
||||||
@@ -11,3 +11,6 @@ markdown
|
|||||||
PyYAML
|
PyYAML
|
||||||
Pygments
|
Pygments
|
||||||
drf-spectacular
|
drf-spectacular
|
||||||
|
channels
|
||||||
|
daphne
|
||||||
|
channels-redis
|
||||||
|
|||||||
@@ -0,0 +1,5 @@
|
|||||||
|
from django.apps import AppConfig
|
||||||
|
|
||||||
|
class VotingConfig(AppConfig):
|
||||||
|
default_auto_field = 'django.db.models.BigAutoField'
|
||||||
|
name = 'voting'
|
||||||
@@ -0,0 +1,71 @@
|
|||||||
|
import json
|
||||||
|
|
||||||
|
from asgiref.sync import async_to_sync
|
||||||
|
from channels.generic.websocket import WebsocketConsumer
|
||||||
|
from django.contrib.auth.models import User
|
||||||
|
from muzak.auth import get_user_from_token
|
||||||
|
from channels.db import database_sync_to_async
|
||||||
|
|
||||||
|
|
||||||
|
class SessionConsumer(WebsocketConsumer):
|
||||||
|
def connect(self):
|
||||||
|
self.session_id = self.scope["url_route"]["kwargs"]["session_id"]
|
||||||
|
self.session_name = f"session_{self.session_id}"
|
||||||
|
# print("---")
|
||||||
|
# print(self.scope)
|
||||||
|
|
||||||
|
# Join room group
|
||||||
|
async_to_sync(self.channel_layer.group_add)(
|
||||||
|
self.session_name, self.channel_name
|
||||||
|
)
|
||||||
|
|
||||||
|
self.accept()
|
||||||
|
|
||||||
|
def disconnect(self, close_code):
|
||||||
|
# Leave room group
|
||||||
|
async_to_sync(self.channel_layer.group_discard)(
|
||||||
|
self.session_name, self.channel_name
|
||||||
|
)
|
||||||
|
|
||||||
|
# Receive message from WebSocket
|
||||||
|
def receive(self, text_data):
|
||||||
|
user = self.scope["user"]
|
||||||
|
text_data_json = json.loads(text_data)
|
||||||
|
action = text_data_json["action"]
|
||||||
|
|
||||||
|
# Niet lachen, middleware is moeilijk oké?
|
||||||
|
username = text_data_json.get("username")
|
||||||
|
token = text_data_json.get("token")
|
||||||
|
if token:
|
||||||
|
user = get_user_from_token(token, log=True)
|
||||||
|
print(user)
|
||||||
|
elif username:
|
||||||
|
pass
|
||||||
|
#user = User.objects.get(username=username)
|
||||||
|
|
||||||
|
|
||||||
|
if action == "message":
|
||||||
|
message = text_data_json["message"]
|
||||||
|
# Send message to room group
|
||||||
|
async_to_sync(self.channel_layer.group_send)(
|
||||||
|
self.session_name, {"type": "chat.message", "message": str(user) + ": " + message}
|
||||||
|
)
|
||||||
|
if action == "login":
|
||||||
|
print("LOGIN", str(user))
|
||||||
|
async_to_sync(self.channel_layer.group_send)(
|
||||||
|
self.session_name, {"type": "announce", "user": str(user)}
|
||||||
|
)
|
||||||
|
|
||||||
|
def announce(self, event):
|
||||||
|
user = event["user"]
|
||||||
|
self.send(text_data=json.dumps({"joined": user}))
|
||||||
|
|
||||||
|
|
||||||
|
# Receive message from room group
|
||||||
|
def chat_message(self, event):
|
||||||
|
message = event["message"]
|
||||||
|
|
||||||
|
print(self.scope["user"])
|
||||||
|
|
||||||
|
# Send message to WebSocket
|
||||||
|
self.send(text_data=json.dumps({"message": message}))
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
from django.urls import re_path
|
||||||
|
|
||||||
|
from . import consumers
|
||||||
|
|
||||||
|
websocket_urlpatterns = [
|
||||||
|
re_path(r"voting/session/(?P<session_id>[\w\-]+)/$", consumers.SessionConsumer.as_asgi()),
|
||||||
|
]
|
||||||
@@ -0,0 +1,25 @@
|
|||||||
|
<!doctype html>
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8" />
|
||||||
|
<title>Sessions</title>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<input id="room-name-input" type="text" size="100" /><br />
|
||||||
|
<input id="room-name-submit" type="button" value="Enter" />
|
||||||
|
<script>
|
||||||
|
document.querySelector("#room-name-input").focus();
|
||||||
|
document.querySelector("#room-name-input").onkeyup = function (e) {
|
||||||
|
if (e.key === "Enter") {
|
||||||
|
// enter, return
|
||||||
|
document.querySelector("#room-name-submit").click();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
document.querySelector("#room-name-submit").onclick = function (e) {
|
||||||
|
var roomName = document.querySelector("#room-name-input").value;
|
||||||
|
window.location.pathname =
|
||||||
|
"/voting/session/" + roomName + "/test";
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
@@ -0,0 +1,63 @@
|
|||||||
|
<!doctype html>
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8" />
|
||||||
|
<title>Votign Session</title>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<textarea id="chat-log" cols="100" rows="20"></textarea><br />
|
||||||
|
<input id="chat-message-input" type="text" size="100" /><br />
|
||||||
|
<input id="chat-message-submit" type="button" value="Send" />
|
||||||
|
{{ room_name|json_script:"room-name" }}
|
||||||
|
<script>
|
||||||
|
const roomName = JSON.parse(
|
||||||
|
document.getElementById("room-name").textContent,
|
||||||
|
);
|
||||||
|
|
||||||
|
const host =
|
||||||
|
"ws://" +
|
||||||
|
window.location.host +
|
||||||
|
"/voting/session/" +
|
||||||
|
roomName +
|
||||||
|
"/";
|
||||||
|
const chatSocket = new WebSocket(host);
|
||||||
|
console.log(host);
|
||||||
|
|
||||||
|
chatSocket.onmessage = function (e) {
|
||||||
|
const data = JSON.parse(e.data);
|
||||||
|
document.querySelector("#chat-log").value +=
|
||||||
|
data.message + "\n";
|
||||||
|
};
|
||||||
|
|
||||||
|
chatSocket.onclose = function (e) {
|
||||||
|
console.error("Chat socket closed unexpectedly");
|
||||||
|
};
|
||||||
|
|
||||||
|
document.querySelector("#chat-message-input").focus();
|
||||||
|
document.querySelector("#chat-message-input").onkeyup = function (
|
||||||
|
e,
|
||||||
|
) {
|
||||||
|
if (e.key === "Enter") {
|
||||||
|
// enter, return
|
||||||
|
document.querySelector("#chat-message-submit").click();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
document.querySelector("#chat-message-submit").onclick = function (
|
||||||
|
e,
|
||||||
|
) {
|
||||||
|
const messageInputDom = document.querySelector(
|
||||||
|
"#chat-message-input",
|
||||||
|
);
|
||||||
|
const message = messageInputDom.value;
|
||||||
|
chatSocket.send(
|
||||||
|
JSON.stringify({
|
||||||
|
action: "message",
|
||||||
|
message: message,
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
messageInputDom.value = "";
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
from django.urls import path
|
||||||
|
|
||||||
|
from . import views
|
||||||
|
|
||||||
|
urlpatterns = [
|
||||||
|
path("", views.index, name="index"),
|
||||||
|
path("session/<str:session_id>/test", views.session, name="session"),
|
||||||
|
path("session/", views.SessionListView.as_view(), name="api-session-list"),
|
||||||
|
]
|
||||||
@@ -0,0 +1,24 @@
|
|||||||
|
from django.shortcuts import render
|
||||||
|
from rest_framework.views import APIView
|
||||||
|
from rest_framework.response import Response
|
||||||
|
from playlist.models import Session, Profile, Track, Vote
|
||||||
|
from playlist.serializers import TrackSerializer, SessionSerializer
|
||||||
|
from drf_spectacular.utils import extend_schema
|
||||||
|
|
||||||
|
def index(request):
|
||||||
|
return render(request, 'voting/index.html')
|
||||||
|
|
||||||
|
def session(request, session_id):
|
||||||
|
return render(request, 'voting/session.html', {'room_name': session_id})
|
||||||
|
|
||||||
|
@extend_schema(
|
||||||
|
responses={200: SessionSerializer(many=True)},
|
||||||
|
description="Retrieve a list of all available sessions."
|
||||||
|
)
|
||||||
|
class SessionListView(APIView):
|
||||||
|
def get(self, request):
|
||||||
|
if request.user.is_superuser:
|
||||||
|
sessions = Session.objects.all()
|
||||||
|
else:
|
||||||
|
sessions = Session.objects.filter(voting_open=True)
|
||||||
|
return Response(SessionSerializer(sessions, many=True).data)
|
||||||
@@ -30,6 +30,12 @@ services:
|
|||||||
- "3003:3000"
|
- "3003:3000"
|
||||||
networks:
|
networks:
|
||||||
- app_network
|
- app_network
|
||||||
|
valkey:
|
||||||
|
image: valkey/valkey
|
||||||
|
ports:
|
||||||
|
- "6379:6379"
|
||||||
|
networks:
|
||||||
|
- app_network
|
||||||
|
|
||||||
networks:
|
networks:
|
||||||
app_network:
|
app_network:
|
||||||
|
|||||||
@@ -7,8 +7,10 @@ import { Source_Sans_3, Nunito } from "next/font/google";
|
|||||||
import AuthStatus from "muzak/components/AuthStatus";
|
import AuthStatus from "muzak/components/AuthStatus";
|
||||||
import NavigationBar from "muzak/components/NavigationBar";
|
import NavigationBar from "muzak/components/NavigationBar";
|
||||||
import Quota from "muzak/components/Quota";
|
import Quota from "muzak/components/Quota";
|
||||||
|
import SessionChecker from "muzak/components/SessionChecker";
|
||||||
import { AnimationContext } from "muzak/contexts/AnimationContext";
|
import { AnimationContext } from "muzak/contexts/AnimationContext";
|
||||||
import { QuotaContext } from "muzak/contexts/QuotaContext";
|
import { QuotaContext } from "muzak/contexts/QuotaContext";
|
||||||
|
import { SessionContext } from "muzak/contexts/SessionContext";
|
||||||
|
|
||||||
const nunito = Nunito({
|
const nunito = Nunito({
|
||||||
subsets: ["latin"],
|
subsets: ["latin"],
|
||||||
@@ -17,6 +19,7 @@ const nunito = Nunito({
|
|||||||
function LayoutContent({ children }: { children: React.ReactNode }) {
|
function LayoutContent({ children }: { children: React.ReactNode }) {
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
|
<SessionChecker />
|
||||||
<AuthStatus />
|
<AuthStatus />
|
||||||
{children}
|
{children}
|
||||||
{/* <NavigationBar /> */}
|
{/* <NavigationBar /> */}
|
||||||
@@ -32,6 +35,7 @@ export default function RootLayout({
|
|||||||
}) {
|
}) {
|
||||||
const [isAnimate, setIsAnimate] = useState(false);
|
const [isAnimate, setIsAnimate] = useState(false);
|
||||||
const [quota, setQuota] = useState<number | null>(null);
|
const [quota, setQuota] = useState<number | null>(null);
|
||||||
|
const [sessionId, setSessionId] = useState<string | null>(null);
|
||||||
|
|
||||||
const toggleAnimation = () => {
|
const toggleAnimation = () => {
|
||||||
setIsAnimate(!isAnimate);
|
setIsAnimate(!isAnimate);
|
||||||
@@ -43,7 +47,9 @@ export default function RootLayout({
|
|||||||
<AuthProvider {...oidcConfig}>
|
<AuthProvider {...oidcConfig}>
|
||||||
<AnimationContext.Provider value={{ isAnimate, setIsAnimate }}>
|
<AnimationContext.Provider value={{ isAnimate, setIsAnimate }}>
|
||||||
<QuotaContext.Provider value={{ quota, setQuota }}>
|
<QuotaContext.Provider value={{ quota, setQuota }}>
|
||||||
<LayoutContent children={children} />
|
<SessionContext.Provider value={{ sessionId, setSessionId }}>
|
||||||
|
<LayoutContent children={children} />
|
||||||
|
</SessionContext.Provider>
|
||||||
</QuotaContext.Provider>
|
</QuotaContext.Provider>
|
||||||
</AnimationContext.Provider>
|
</AnimationContext.Provider>
|
||||||
</AuthProvider>
|
</AuthProvider>
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import { Markazi_Text, Nunito } from "next/font/google";
|
|||||||
import users from "muzak/data/users.json";
|
import users from "muzak/data/users.json";
|
||||||
import Image from "next/image";
|
import Image from "next/image";
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
|
import { useSession } from "muzak/contexts/SessionContext";
|
||||||
|
|
||||||
const nunito = Nunito({
|
const nunito = Nunito({
|
||||||
weight: "900",
|
weight: "900",
|
||||||
@@ -13,29 +14,44 @@ const nunito = Nunito({
|
|||||||
const MAX_PARTY_SIZE = 8;
|
const MAX_PARTY_SIZE = 8;
|
||||||
|
|
||||||
export default function Lobby() {
|
export default function Lobby() {
|
||||||
const [partySize, setPartySize] = useState(0);
|
const [userList, setUserList] = useState<any[]>([]);
|
||||||
const [userList, setUserList] = useState([]);
|
const [sessionSocket, setSessionSocket] = useState<WebSocket | null>(null);
|
||||||
|
const { sessionId, setSessionId } = useSession();
|
||||||
|
|
||||||
|
function onMessage(e) {
|
||||||
|
setTimeout(() => {
|
||||||
|
console.log(e);
|
||||||
|
let data = JSON.parse(e.data);
|
||||||
|
for (let key in data) {
|
||||||
|
if (key === "joined") {
|
||||||
|
addUser(data[key]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
console.log(data);
|
||||||
|
}, 1000);
|
||||||
|
}
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
populateUserList();
|
if (sessionId) {
|
||||||
}, [partySize]);
|
const socket = new WebSocket(
|
||||||
|
`${process.env.NEXT_PUBLIC_WS_HOST}/voting/session/${sessionId}/`,
|
||||||
|
);
|
||||||
|
socket.onmessage = onMessage;
|
||||||
|
setSessionSocket(socket);
|
||||||
|
}
|
||||||
|
}, [sessionId]);
|
||||||
|
|
||||||
function populateUserList() {
|
function addUser(userName: string) {
|
||||||
const mappedUsers = users.slice(0, partySize).map((user) => ({
|
if (userList.find((user) => user.name === userName)) {
|
||||||
id: user.id,
|
return;
|
||||||
name: user.name,
|
}
|
||||||
avatar: user.avatar,
|
const newUser = {
|
||||||
host: user.host,
|
id: userList.length + 1,
|
||||||
}));
|
name: userName,
|
||||||
setUserList(mappedUsers);
|
avatar: `https://i.pravatar.cc/150?img=${userList.length + 1}`,
|
||||||
}
|
host: userList.length === 0,
|
||||||
|
};
|
||||||
function addUserToParty() {
|
setUserList((prevUserList) => [...prevUserList, newUser]);
|
||||||
setPartySize((prev) => Math.min(prev + 1, MAX_PARTY_SIZE));
|
|
||||||
}
|
|
||||||
|
|
||||||
function deleteUserFromParty() {
|
|
||||||
setPartySize((prev) => Math.max(prev - 1, 0));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -51,7 +67,7 @@ export default function Lobby() {
|
|||||||
<div className="flex min-h-[532px] min-w-[500px] flex-col items-center justify-start gap-4 rounded-md bg-blue-950/80 p-8">
|
<div className="flex min-h-[532px] min-w-[500px] flex-col items-center justify-start gap-4 rounded-md bg-blue-950/80 p-8">
|
||||||
<div className="flex w-full flex-row items-center justify-between px-1">
|
<div className="flex w-full flex-row items-center justify-between px-1">
|
||||||
<div className="text-3xl font-bold text-yellow-500">
|
<div className="text-3xl font-bold text-yellow-500">
|
||||||
Players: {partySize}
|
Players: {userList.length}
|
||||||
</div>
|
</div>
|
||||||
<Link
|
<Link
|
||||||
href="/voting"
|
href="/voting"
|
||||||
@@ -61,7 +77,7 @@ export default function Lobby() {
|
|||||||
</Link>
|
</Link>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{partySize === 0 ? (
|
{userList.length === 0 ? (
|
||||||
<div className="mt-31 min-w-52 text-3xl font-semibold text-white drop-shadow-sm drop-shadow-gray-900">
|
<div className="mt-31 min-w-52 text-3xl font-semibold text-white drop-shadow-sm drop-shadow-gray-900">
|
||||||
Waiting for players...
|
Waiting for players...
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -0,0 +1,62 @@
|
|||||||
|
"use client";
|
||||||
|
import { useSession } from "muzak/contexts/SessionContext";
|
||||||
|
import { useState, useEffect } from "react";
|
||||||
|
import { useAuth } from "react-oidc-context";
|
||||||
|
import { getUser } from "muzak/data/fetcher";
|
||||||
|
import { Nunito } from "next/font/google";
|
||||||
|
|
||||||
|
const nunito = Nunito({
|
||||||
|
weight: "900",
|
||||||
|
subsets: ["latin"],
|
||||||
|
});
|
||||||
|
|
||||||
|
export default function Session() {
|
||||||
|
const auth = useAuth();
|
||||||
|
const { sessionId, setSessionId } = useSession();
|
||||||
|
const user = getUser();
|
||||||
|
const [sessionSocket, setSessionSocket] = useState<WebSocket | null>(null);
|
||||||
|
|
||||||
|
function connect() {
|
||||||
|
console.log(`ik ben ${user.profile.nickname}`);
|
||||||
|
console.log(sessionSocket?.readyState);
|
||||||
|
if (sessionSocket?.readyState === WebSocket.OPEN) {
|
||||||
|
console.log("socket is open");
|
||||||
|
sessionSocket.send(
|
||||||
|
JSON.stringify({
|
||||||
|
action: "login",
|
||||||
|
userr: user.profile.nickname,
|
||||||
|
token: user.access_token,
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (sessionId) {
|
||||||
|
const socket = new WebSocket(
|
||||||
|
`${process.env.NEXT_PUBLIC_WS_HOST}/voting/session/${sessionId}/`,
|
||||||
|
);
|
||||||
|
setSessionSocket(socket);
|
||||||
|
}
|
||||||
|
}, [sessionId]);
|
||||||
|
return (
|
||||||
|
<main className="flex min-h-screen flex-col items-center justify-between p-24">
|
||||||
|
<div>
|
||||||
|
<h1
|
||||||
|
className={`${nunito.className} text-outline text-[42px] tracking-tighter text-yellow-500`}
|
||||||
|
>
|
||||||
|
LOGIN
|
||||||
|
</h1>
|
||||||
|
</div>
|
||||||
|
<div className="mb-12 flex flex-col items-center justify-between gap-4 rounded-md bg-blue-950/80 p-8">
|
||||||
|
<h1 className="text-xl text-white">{user.profile.nickname}</h1>
|
||||||
|
<button
|
||||||
|
className="rounded-md bg-blue-500 px-4 py-2 text-white hover:bg-blue-600"
|
||||||
|
onClick={connect}
|
||||||
|
>
|
||||||
|
Connect
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</main>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,52 @@
|
|||||||
|
"use client";
|
||||||
|
import Link from "next/link";
|
||||||
|
import { useAuth } from "react-oidc-context";
|
||||||
|
import { useState, useEffect, use } from "react";
|
||||||
|
import { useRouter, usePathname } from "next/navigation";
|
||||||
|
import { api, Session } from "muzak/data/fetcher";
|
||||||
|
import { getUser } from "muzak/data/fetcher";
|
||||||
|
import { useSession } from "muzak/contexts/SessionContext";
|
||||||
|
|
||||||
|
export default function SessionChecker() {
|
||||||
|
const auth = useAuth();
|
||||||
|
const router = useRouter();
|
||||||
|
|
||||||
|
const [isSession, setIsSession] = useState(false);
|
||||||
|
const [link, setLink] = useState("/lobby");
|
||||||
|
const { sessionId, setSessionId } = useSession();
|
||||||
|
|
||||||
|
async function getSessions() {
|
||||||
|
if (!auth.isLoading && !auth.error && auth.isAuthenticated) {
|
||||||
|
const { data: sessions } = await api?.sessions?.list({});
|
||||||
|
console.log(sessions);
|
||||||
|
if (sessions?.length > 0) {
|
||||||
|
setIsSession(true);
|
||||||
|
const user = getUser();
|
||||||
|
setSessionId(sessions[0].session_id);
|
||||||
|
if (sessions[0].host === user?.profile?.email) {
|
||||||
|
setLink("/lobby");
|
||||||
|
} else {
|
||||||
|
setLink("/session");
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
setIsSession(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
getSessions();
|
||||||
|
}, [auth]);
|
||||||
|
|
||||||
|
if (isSession) {
|
||||||
|
return (
|
||||||
|
<Link
|
||||||
|
href={link}
|
||||||
|
className="bold text-l absolute top-2 left-2 rounded-[50%] bg-red-500 p-3 font-bold text-white"
|
||||||
|
>
|
||||||
|
Session available!
|
||||||
|
</Link>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return <> </>;
|
||||||
|
}
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
"use client";
|
||||||
|
import { createContext, useContext } from "react";
|
||||||
|
|
||||||
|
interface SessionContextType {
|
||||||
|
sessionId: string;
|
||||||
|
setSessionId: React.Dispatch<React.SetStateAction<string>>;
|
||||||
|
}
|
||||||
|
const SessionContext = createContext<SessionContextType>(
|
||||||
|
{} as SessionContextType,
|
||||||
|
);
|
||||||
|
|
||||||
|
export const useSession = () => useContext(SessionContext);
|
||||||
|
|
||||||
|
export { SessionContext };
|
||||||
@@ -83,11 +83,15 @@ export const api = {
|
|||||||
profiles: {
|
profiles: {
|
||||||
me: fetcher.path("/api/profile/me").method("get").create(),
|
me: fetcher.path("/api/profile/me").method("get").create(),
|
||||||
},
|
},
|
||||||
|
sessions: {
|
||||||
|
list: fetcher.path("/voting/session/").method("get").create(),
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
export type User = components["schemas"]["User"];
|
export type User = components["schemas"]["User"];
|
||||||
export type Track = components["schemas"]["Track"];
|
export type Track = components["schemas"]["Track"];
|
||||||
export type Album = components["schemas"]["Album"];
|
export type Album = components["schemas"]["Album"];
|
||||||
export type Profile = components["schemas"]["Profile"];
|
export type Profile = components["schemas"]["Profile"];
|
||||||
|
export type Session = components["schemas"]["Session"];
|
||||||
|
|
||||||
export default api;
|
export default api;
|
||||||
|
|||||||
Reference in New Issue
Block a user