Files
django-discord/chat/views.py
T
biggy 88bdfc797b feat(voice): add WebRTC voice channels with Cloudflare TURN support
Add voice channel functionality with WebRTC peer-to-peer audio:
- Add VoiceSignalingConsumer for WebRTC signaling and peer coordination
- Add GlobalConsumer for real-time presence and voice status updates
- Add voice channel type to Channel model with current_voice_channel tracking on UserProfile
- Add voice controls UI (mute, deafen, disconnect) in status panel
- Add voice member list display in sidebar with avatar support
- Add
2026-05-23 20:56:37 +02:00

263 lines
8.8 KiB
Python

from django.shortcuts import render, redirect, get_object_or_404
from django.contrib.auth import authenticate, login, logout
from django.contrib.auth.decorators import login_required
from django.contrib.auth.models import User
from django.contrib import messages
from django.http import JsonResponse
from django.views.decorators.http import require_POST
from chat.models import UserProfile, Channel, Message, DirectMessage
from chat.forms import RegisterForm, ProfileForm, ChannelForm, MessageFileForm
from channels.layers import get_channel_layer
from asgiref.sync import async_to_sync
def _broadcast_message(group_name, message_type, msg, request_user):
channel_layer = get_channel_layer()
async_to_sync(channel_layer.group_send)(
group_name,
{
'type': message_type,
'message': msg.content,
'sender': request_user.username,
'sender_id': request_user.id,
'message_id': msg.id,
'timestamp': msg.timestamp.strftime('%H:%M'),
'avatar': request_user.profile.avatar.url if request_user.profile.avatar else None,
'role': request_user.profile.role,
'file_url': msg.file.url if msg.file else None,
'file_name': msg.file.name if msg.file else None,
}
)
def register_view(request):
form = RegisterForm(request.POST or None)
if request.method == 'POST' and form.is_valid():
user = User.objects.create_user(
username=form.cleaned_data['username'],
email=form.cleaned_data['email'],
password=form.cleaned_data['password'],
)
UserProfile.objects.create(user=user)
login(request, user)
return redirect('home')
return render(request, 'chat/register.html', {'form': form})
def login_view(request):
error = None
if request.method == 'POST':
user = authenticate(request,
username=request.POST.get('username'),
password=request.POST.get('password'))
if user:
login(request, user)
return redirect('home')
error = 'Invalid credentials.'
return render(request, 'chat/login.html', {'error': error})
def logout_view(request):
logout(request)
return redirect('login')
@login_required
def home_view(request):
channels = Channel.objects.all().prefetch_related('members')
users = User.objects.exclude(id=request.user.id).select_related('profile')
return render(request, 'chat/home.html', {'channels': channels, 'users': users})
@login_required
def channel_view(request, channel_id):
channel = get_object_or_404(Channel, id=channel_id)
# Join if not member
if request.user not in channel.members.all():
channel.members.add(request.user)
# File upload via POST
if request.method == 'POST':
f = request.FILES.get('file')
if f:
msg = Message.objects.create(
channel=channel,
sender=request.user,
content=request.POST.get('content', ''),
file=f,
)
_broadcast_message(f'channel_{channel.id}', 'chat_message', msg, request.user)
return redirect('channel', channel_id=channel_id)
messages_qs = channel.messages.filter(deleted=False).select_related('sender', 'sender__profile')
return render(request, 'chat/channel.html', {
'channel': channel,
'messages': messages_qs,
'all_channels': Channel.objects.all(),
})
@login_required
def create_channel_view(request):
profile = request.user.profile
if not profile.is_moderator():
messages.error(request, 'No permission.')
return redirect('home')
form = ChannelForm(request.POST or None)
if form.is_valid():
ch = form.save(commit=False)
ch.created_by = request.user
ch.save()
ch.members.add(request.user)
return redirect('channel', channel_id=ch.id)
return render(request, 'chat/create_channel.html', {'form': form})
@login_required
def delete_channel_view(request, channel_id):
channel = get_object_or_404(Channel, id=channel_id)
if request.user.profile.is_admin():
channel.delete()
return redirect('home')
@login_required
def dm_view(request, username):
other = get_object_or_404(User, username=username)
if request.method == 'POST':
f = request.FILES.get('file')
if f:
msg = DirectMessage.objects.create(
sender=request.user,
receiver=other,
content=request.POST.get('content', ''),
file=f,
)
ids = sorted([request.user.id, other.id])
_broadcast_message(f'dm_{ids[0]}_{ids[1]}', 'dm_message', msg, request.user)
return redirect('dm', username=username)
dms = DirectMessage.objects.filter(
sender__in=[request.user, other],
receiver__in=[request.user, other],
).select_related('sender', 'sender__profile').order_by('timestamp')
return render(request, 'chat/dm.html', {
'other': other,
'messages': dms,
'all_users': User.objects.exclude(id=request.user.id).select_related('profile'),
})
@login_required
def profile_view(request):
profile = request.user.profile
form = ProfileForm(request.POST or None, request.FILES or None, instance=profile)
if form.is_valid():
form.save()
messages.success(request, 'Profile updated.')
return redirect('profile')
return render(request, 'chat/profile.html', {'form': form})
def _handle_admin_action(request, action, target, role):
if action == 'set_role' and role:
target.profile.role = role
target.profile.save()
messages.success(request, f'Role updated for {target.username}.')
elif action == 'block':
request.user.profile.blocked_users.add(target)
messages.success(request, f'{target.username} blocked.')
elif action == 'unblock':
request.user.profile.blocked_users.remove(target)
messages.success(request, f'{target.username} unblocked.')
@login_required
def admin_panel_view(request):
if not request.user.profile.is_admin():
messages.error(request, 'No permission.')
return redirect('home')
users = User.objects.all().select_related('profile').exclude(id=request.user.id)
if request.method == 'POST':
user_id = request.POST.get('user_id')
action = request.POST.get('action')
role = request.POST.get('role')
target = get_object_or_404(User, id=user_id)
_handle_admin_action(request, action, target, role)
return redirect('admin_panel')
blocked_ids = list(request.user.profile.blocked_users.values_list('id', flat=True))
return render(request, 'chat/admin_panel.html', {
'users': users,
'blocked_ids': blocked_ids,
})
@login_required
def delete_message_view(request, message_id):
msg = get_object_or_404(Message, id=message_id)
if request.user.profile.is_moderator() or msg.sender == request.user:
msg.deleted = True
msg.save()
return redirect('channel', channel_id=msg.channel_id)
@login_required
def block_user_view(request, user_id):
if request.user.profile.is_moderator():
target = get_object_or_404(User, id=user_id)
request.user.profile.blocked_users.add(target)
return redirect('home')
def handler404(request, exception):
return render(request, 'chat/404.html', status=404)
def handler500(request):
return render(request, 'chat/500.html', status=500)
import urllib.request
import urllib.error
import json
from django.conf import settings
@login_required
def voice_credentials_view(request):
account_id = getattr(settings, 'CLOUDFLARE_ACCOUNT_ID', '')
api_token = getattr(settings, 'CLOUDFLARE_API_TOKEN', '')
turn_key_id = getattr(settings, 'CLOUDFLARE_TURN_KEY_ID', '')
if not all([account_id, api_token, turn_key_id]):
return JsonResponse({
"iceServers": [
{"urls": ["stun:stun.cloudflare.com:3478", "stun:stun.cloudflare.com:53"]}
]
})
url = f"https://rtc.live.cloudflare.com/v1/turn/keys/{turn_key_id}/credentials/generate-ice-servers"
headers = {
"Authorization": f"Bearer {api_token}",
"Content-Type": "application/json"
}
body = json.dumps({"ttl": 86400}).encode("utf-8")
req = urllib.request.Request(url, data=body, headers=headers, method="POST")
try:
with urllib.request.urlopen(req, timeout=5) as response:
res_data = json.loads(response.read().decode())
return JsonResponse(res_data)
except urllib.error.URLError as e:
return JsonResponse({
"error": str(e),
"iceServers": [
{"urls": ["stun:stun.cloudflare.com:3478", "stun:stun.cloudflare.com:53"]}
]
})