Skip to content

Commit 435b8c3

Browse files
committed
Add online presence
1 parent e95b405 commit 435b8c3

25 files changed

+440
-12
lines changed

backend/Dockerfile

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ RUN mkdir -p /usr/src/app && chown celery:celery /usr/src/app
1919
FROM base AS builder
2020
COPY backend/requirements.txt .
2121
RUN pip install --no-cache-dir -r requirements.txt
22-
RUN pip install --no-cache-dir celery redis django-celery-beat
22+
RUN pip install --no-cache-dir celery redis django-celery-beat daphne
2323

2424
# Development stage
2525
FROM base AS development
@@ -28,7 +28,7 @@ COPY backend/ .
2828
RUN python manage.py collectstatic --noinput
2929
RUN chown -R celery:celery /usr/src/app
3030
USER celery
31-
CMD ["python", "manage.py", "runserver", "0.0.0.0:8000"]
31+
CMD ["daphne", "-b", "0.0.0.0", "-p", "8000", "backend.asgi:application"]
3232

3333
# Production stage
3434
FROM base AS production
@@ -37,4 +37,4 @@ COPY backend/ .
3737
RUN python manage.py collectstatic --noinput
3838
RUN chown -R celery:celery /usr/src/app
3939
USER celery
40-
CMD ["gunicorn", "tibiknini.wsgi:application", "--workers", "3", "--bind", "0.0.0.0:8000"]
40+
CMD ["daphne", "-b", "0.0.0.0", "-p", "8000", "backend.asgi:application"]

backend/api/urls.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@
2323
path("privacy_policy/", PrivacyPolicyView.as_view(), name="privacy_policy"),
2424
path("terms_of_service/", TermsOfServiceView.as_view(), name="terms_of_service"),
2525
path("users/", include("users.urls")),
26+
path("presence/", include("presence.urls")),
2627
path("set-csrf-token/", set_csrf_token, name="set_csrf_token"),
2728
path("auth/login/", ReCaptchaLoginView.as_view(), name="login"),
2829
path("auth/logout/", auth_views.LogoutView.as_view(), name="logout"),

backend/backend/asgi.py

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -8,9 +8,18 @@
88
"""
99

1010
import os
11-
1211
from django.core.asgi import get_asgi_application
1312

14-
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "tibiknini.settings")
13+
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "backend.settings")
14+
django_asgi_app = get_asgi_application()
15+
16+
from channels.routing import ProtocolTypeRouter, URLRouter
17+
from channels.auth import AuthMiddlewareStack
18+
from presence.routing import websocket_urlpatterns
1519

16-
application = get_asgi_application()
20+
application = ProtocolTypeRouter({
21+
"http": django_asgi_app,
22+
"websocket": AuthMiddlewareStack(
23+
URLRouter(websocket_urlpatterns)
24+
),
25+
})

backend/backend/settings.py

Lines changed: 20 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,8 @@ def get_secret(secret_name, default=None):
7070
"moderation",
7171
"navbar",
7272
"users",
73+
"presence",
74+
"channels",
7375
"corsheaders",
7476
"rest_framework",
7577
"django.contrib.admin",
@@ -95,6 +97,17 @@ def get_secret(secret_name, default=None):
9597

9698
ROOT_URLCONF = "backend.urls"
9799

100+
# Channels Configuration
101+
ASGI_APPLICATION = "backend.asgi.application"
102+
CHANNEL_LAYERS = {
103+
"default": {
104+
"BACKEND": "channels_redis.core.RedisChannelLayer",
105+
"CONFIG": {
106+
"hosts": [(os.getenv("REDIS_HOST", "redis"), 6379)],
107+
},
108+
},
109+
}
110+
98111
TEMPLATES = [
99112
{
100113
"BACKEND": "django.template.backends.django.DjangoTemplates",
@@ -165,6 +178,11 @@ def get_secret(secret_name, default=None):
165178
"http://localhost",
166179
"http://127.0.0.1",
167180
]
181+
182+
CORS_ALLOW_CREDENTIALS = True
183+
CORS_ALLOW_METHODS = ['GET', 'POST', 'PUT', 'PATCH', 'DELETE', 'OPTIONS']
184+
CORS_ALLOW_HEADERS = ['*']
185+
168186
CSRF_TRUSTED_ORIGINS = [
169187
"http://127.0.0.1",
170188
"https://127.0.0.1",
@@ -173,8 +191,8 @@ def get_secret(secret_name, default=None):
173191
f"https://*.{os.getenv('DOMAIN_NAME')}",
174192
]
175193

176-
SESSION_COOKIE_HTTPONLY = True
177-
SECURE_PROXY_SSL_HEADER = ("HTTP_X_FORWARDED_PROTO", "https")
194+
# Allow WebSocket connections
195+
CORS_ALLOW_ALL_ORIGINS = True if DEBUG else False
178196

179197
# Celery Configuration
180198
CELERY_BROKER_URL = os.environ.get('CELERY_BROKER_URL', 'redis://localhost:6379/0')

backend/presence/__init__.py

Whitespace-only changes.

backend/presence/admin.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
from django.contrib import admin
2+
3+
# Register your models here.

backend/presence/apps.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
from django.apps import AppConfig
2+
3+
4+
class PresenceConfig(AppConfig):
5+
default_auto_field = 'django.db.models.BigAutoField'
6+
name = 'presence'

backend/presence/consumers.py

Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
import json
2+
from channels.generic.websocket import AsyncWebsocketConsumer
3+
from channels.db import database_sync_to_async
4+
from django.utils import timezone
5+
from django.apps import apps
6+
7+
class PresenceConsumer(AsyncWebsocketConsumer):
8+
async def connect(self):
9+
# Accept the WebSocket connection
10+
await self.accept()
11+
12+
# Store the channel name and user
13+
self.user = self.scope["user"]
14+
15+
# Handle authentication
16+
if not self.user.is_authenticated:
17+
# Close connection if user is not authenticated
18+
await self.close(code=4001)
19+
return
20+
21+
# Update user presence
22+
await self.update_user_presence(True, self.channel_name)
23+
24+
# Add this channel to the group for broadcasting presence updates
25+
await self.channel_layer.group_add("presence", self.channel_name)
26+
27+
# Broadcast the user's online status to all connected clients
28+
await self.channel_layer.group_send(
29+
"presence",
30+
{
31+
"type": "presence_update",
32+
"user_id": self.user.id,
33+
"status": "online"
34+
}
35+
)
36+
37+
async def disconnect(self, close_code):
38+
if hasattr(self, 'user') and self.user.is_authenticated:
39+
await self.update_user_presence(False, "")
40+
41+
# Remove this channel from the group
42+
await self.channel_layer.group_discard("presence", self.channel_name)
43+
44+
# Broadcast the user's offline status
45+
await self.channel_layer.group_send(
46+
"presence",
47+
{
48+
"type": "presence_update",
49+
"user_id": self.user.id,
50+
"status": "offline"
51+
}
52+
)
53+
54+
async def receive(self, text_data):
55+
try:
56+
data = json.loads(text_data)
57+
if data.get('type') == 'authenticate':
58+
# Handle authentication if needed
59+
# The token validation is already handled by Django Channels authentication
60+
pass
61+
# Handle other message types if needed
62+
except json.JSONDecodeError:
63+
await self.close(code=4000)
64+
65+
async def presence_update(self, event):
66+
# Send presence update to WebSocket
67+
await self.send(text_data=json.dumps(event))
68+
69+
@database_sync_to_async
70+
def update_user_presence(self, is_online, channel_name):
71+
UserPresence = apps.get_model('presence', 'UserPresence')
72+
presence, _ = UserPresence.objects.get_or_create(user=self.user)
73+
presence.is_online = is_online
74+
presence.channel_name = channel_name
75+
presence.last_seen = timezone.now()
76+
presence.save()
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
# Generated by Django 5.1.3 on 2024-11-25 14:52
2+
3+
import django.db.models.deletion
4+
import django.utils.timezone
5+
from django.conf import settings
6+
from django.db import migrations, models
7+
8+
9+
class Migration(migrations.Migration):
10+
11+
initial = True
12+
13+
dependencies = [
14+
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
15+
]
16+
17+
operations = [
18+
migrations.CreateModel(
19+
name='UserPresence',
20+
fields=[
21+
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
22+
('last_seen', models.DateTimeField(default=django.utils.timezone.now)),
23+
('is_online', models.BooleanField(default=False)),
24+
('channel_name', models.CharField(blank=True, max_length=255)),
25+
('user', models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)),
26+
],
27+
options={
28+
'verbose_name': 'User Presence',
29+
'verbose_name_plural': 'User Presences',
30+
},
31+
),
32+
]

backend/presence/migrations/__init__.py

Whitespace-only changes.

0 commit comments

Comments
 (0)