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
263 lines
8.8 KiB
Python
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"]}
|
|
]
|
|
})
|