Skip to content
Snippets Groups Projects
Verified Commit d6728979 authored by Lukas Schauer's avatar Lukas Schauer :unlock:
Browse files

added jitsi moderation features

parent d54b3640
No related branches found
No related tags found
No related merge requests found
Showing
with 483 additions and 1 deletion
from django.contrib import admin
from .models import Room, Pattern
class RoomAdmin(admin.ModelAdmin):
def moderator_names(self):
return ", ".join(["{} {}".format(l.first_name, l.last_name) if (l.first_name or l.last_name) else l.username for l in self.moderators.all()])
list_display = ('name', moderator_names)
autocomplete_fields = ["moderators"]
admin.site.register(Room, RoomAdmin)
class PatternAdmin(admin.ModelAdmin):
def whitelisted_names(self):
return ", ".join(["{} {}".format(l.first_name, l.last_name) if (l.first_name or l.last_name) else l.username for l in self.whitelisted.all()])
list_display = ('pattern', whitelisted_names)
autocomplete_fields = ["whitelisted"]
admin.site.register(Pattern, PatternAdmin)
from django.apps import AppConfig
class JitsimodConfig(AppConfig):
name = 'jitsimod'
from django import forms
from django.contrib.auth import get_user_model
from .models import Room
class MultiUserField(forms.Field):
users = []
def get_user(self, username):
try:
return get_user_model().objects.get(username=username)
except:
return None
def to_python(self, value):
if not value:
return []
return list(value.replace(",", " ").split())
def get_users(self):
return self.users
def validate(self, usernames):
if len(usernames) > 10:
raise ValidationError("Only 10 moderators can be added to a room by yourself. If you need more contact an administrator at admin@fslab.de.")
failed = []
users = []
for username in usernames:
user = self.get_user(username)
if user is None:
failed.append(username)
else:
users.append(user)
if failed:
raise ValidationError('User%s not found: %s\nPlease note: FB02 user accounts have to sign in at least once to be created on the platform!' % ('s' if len(failed) > 1 else '', ", ".join(failed)))
else:
self.users = users
class RoomForm(forms.ModelForm):
other_moderators = MultiUserField(label='Other moderators (optional; usernames separated by whitespace and/or commata)', required=False, widget=forms.TextInput(attrs={'class': 'w3-input w3-border w3-light-grey'}))
class Meta:
model = Room
fields = ['name', 'reason', 'other_moderators']
widgets = {
'name': forms.TextInput(attrs={'class': 'w3-input w3-border w3-light-grey', 'autocomplete': 'off'}),
'reason': forms.Textarea(attrs={'class': 'w3-input w3-border w3-light-grey', 'autocomplete': 'off'}),
}
labels = {
'reason': 'Approval reason (leave empty if you have been given a whitelisting pattern)',
}
def __init__(self, *args, **kwargs):
initial = {}
if 'instance' in kwargs:
initial["other_moderators"] = ", ".join(list(l.username for l in kwargs['instance'].moderators.all() if l.username != kwargs['request'].user.username))
request = kwargs['request']
del kwargs['request']
forms.ModelForm.__init__(self, *args, **kwargs, initial = initial)
if 'instance' in kwargs:
del self.fields['name']
del self.fields['reason']
# Generated by Django 3.0.5 on 2020-04-29 17:50
from django.conf import settings
from django.db import migrations, models
class Migration(migrations.Migration):
initial = True
dependencies = [
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
]
operations = [
migrations.CreateModel(
name='Room',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('name', models.CharField(max_length=40, unique=True)),
('approved', models.BooleanField(default=False)),
('moderators', models.ManyToManyField(blank=True, related_name='jitsi_moderating', to=settings.AUTH_USER_MODEL)),
],
),
migrations.CreateModel(
name='Pattern',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('pattern', models.CharField(max_length=40, unique=True)),
('whitelisted', models.ManyToManyField(blank=True, related_name='jitsi_whitelisted', to=settings.AUTH_USER_MODEL)),
],
),
]
# Generated by Django 3.0.5 on 2020-04-29 18:16
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('jitsimod', '0001_initial'),
]
operations = [
migrations.AddField(
model_name='room',
name='reason',
field=models.TextField(blank=True, null=True, verbose_name='Approval Reason'),
),
]
from django.conf import settings
import json
import requests
from requests.auth import HTTPBasicAuth
auth = HTTPBasicAuth('modapi', settings.JITSI_MODAPI_SECRET)
def modapi_get(path):
return json.loads(requests.get("%s/%s" % (settings.JITSI_MODAPI_URL, path), auth=auth, timeout=3).text)
def modapi_post(path, data):
return requests.post("%s/%s" % (settings.JITSI_MODAPI_URL, path), auth=auth, data=data, timeout=3).text
def modapi_get_room(room):
return modapi_get("rooms/%s" % room)
def modapi_reset_password(room):
return modapi_post("rooms/%s" % room, {'resetpassword': 1})
def modapi_grant_moderator(room, nick):
return modapi_post("rooms/%s" % room, {'grant': nick})
def modapi_revoke_moderator(room, nick):
return modapi_post("rooms/%s" % room, {'revoke': nick})
from django.db import models
from django.contrib.auth import get_user_model
class Room(models.Model):
name = models.CharField(blank=False, null=False, max_length=40, unique=True)
moderators = models.ManyToManyField(get_user_model(), related_name='jitsi_moderating', blank=True)
reason = models.TextField('Approval Reason', null=True, blank=True)
approved = models.BooleanField(default=False)
class Pattern(models.Model):
pattern = models.CharField(blank=False, null=False, max_length=40, unique=True)
whitelisted = models.ManyToManyField(get_user_model(), related_name='jitsi_whitelisted', blank=True)
<div class="w3-card-4">
<div class="w3-container w3-hochschulblau">
<h2>{{ formtitle|default:'form' }}</h2><a name="roomform"></a>
</div>
{% if form.non_field_errors %}
<div class="w3-panel w3-pale-red w3-leftbar w3-border-red">
{% for error in form.non_field_errors %}{{ error|linebreaksbr }}<br />{% endfor %}
</div>
{% endif %}
<form action="{{ action|default:'?' }}" method="post" class="w3-container">
{% csrf_token %}
{% for field in form %}
<p>
<label for="{{ field.id_for_label }}"><b>{{ field.label }}</b></label>
{% if field.errors %}
<div class="w3-panel w3-pale-red w3-leftbar w3-border-red">
{% for error in field.errors %}{{ error|linebreaksbr }}<br />{% endfor %}
</div>
{% endif %}
{{ field }}
</p>
{% endfor %}
<p>
<input class="w3-input w3-green" type="submit" name="saveroom" value="Submit" />
</p>
</form>
</div>
{% extends "base.html" %}
{% block content %}
{% include "jitsi/forms/room.html" with formtitle="Create new room" %}
{% endblock content %}
{% extends "base.html" %}
{% block content %}
<h2>{{ room.name }}</h2>
{% if form.errors %}
<div class="w3-panel w3-pale-red w3-leftbar w3-border-red">
<p>There was an issue storing the room settings, see details on form below</p>
</div>
{% elif updated %}
<div class="w3-panel w3-pale-green w3-leftbar w3-border-green">
<p>Room updated successfully</p>
</div>
{% endif %}
{% if modapi_details.password %}
<div class="w3-card-4">
<div class="w3-container w3-hochschulblau">
<h2>Room Password</h2>
</div>
<form id="passwordreset-modform" action="?" method="post" class="w3-container">
<p>This room is password protected.</p>
{% csrf_token %}
<p><input class="w3-input w3-green" type="submit" name="resetpassword" value="Remove password" /></p>
</form>
</div>
<br/>
{% endif %}
<div class="w3-card-4">
<div class="w3-container w3-hochschulblau">
<h2>Room Occupants</h2>
</div>
<ul class="w3-ul w3-border">
{% if modapi_details.occupants %}
{% for occupant in modapi_details.occupants %}
<li class="w3-bar">
{% if occupant.role == "moderator" %}
<img src="/static/icons/star.svg" class="w3-bar-item" style="width:55px">
{% else %}
<img src="/static/icons/person.svg" class="w3-bar-item" style="width:55px">
{% endif %}
<div class="w3-bar-item"><span>{{ occupant.nick }}</span></div>
<div class="w3-bar-item w3-right w3-right-align">
<form id="roomoccupant-modform-{{ forloop.counter }}" action="?" method="post">{% csrf_token %}<input type="hidden" name="{% if occupant.role == "moderator" %}revoke{% else %}grant{% endif %}" value="{{ occupant.nick }}" /></form>
<span><a href="#" onclick='document.getElementById("roomoccupant-modform-{{ forloop.counter }}").submit();' style='color:red;'>{% if occupant.role == "moderator" %}revoke{% else %}grant{% endif %} moderator permissions</a></span>
</div>
</li>
{% endfor %}
{% else %}
<li>Room seems to be empty</li>
{% endif %}
</ul>
</div>
<br/>
{% if recordings %}
<div class="w3-card-4">
<div class="w3-container w3-hochschulblau">
<h2>Recordings</h2>
</div>
<ul class="w3-ul w3-border">
{% for video in recordings %}
<li class="w3-bar">
{% if video.masterfile %}<a href="/video/{{ video.key }}">{% endif %}
{% if video.thumbnail %}
<img src="{{ MEDIA_URL }}{{ video.thumbnail }}?key={{ video.key }}&timestamp={{ video.updated_at|date:'U' }}" class="w3-bar-item" style="width:128px" />
{% else %}
<img src="/static/icons/video.svg" class="w3-bar-item" style="width:128px;padding-left:30px;padding-right:30px;" />
{% endif %}
{% if video.masterfile %}</a>{% endif %}
<div class="w3-bar-item">
<span>{% if video.masterfile %}<a href="/video/{{ video.key }}">{% else %}<span class='w3-text-orange'>{% endif %}{{ video.name }}{% if video.masterfile %}</a>{% else %}</span>{% endif %}</span><br/>
<span class="w3-text-blue">State: {{ video.state }}{% if video.worker %} ({% if video.failed %}Processing Failed! {% endif %}Worker: {{ video.worker.name }}){% endif %}</span><br/>
</div>
<div class="w3-bar-item w3-right w3-right-align">
<span>Uploaded: {{ video.created_at|date:"d.m.Y" }}</span><br/>
<form id="video-deleteform-{{ video.id }}" action="?" method="post">{% csrf_token %}<input type="hidden" name="deleterecording" value="{{ video.id }}" /></form>
<span><a href="#" onclick='if(confirm("Do you really want to delete this file?")) document.getElementById("video-deleteform-{{ video.id }}").submit();' class='w3-text-red'>Delete</a> | <a href="{{ MEDIA_URL }}{{ video.original }}?key={{ video.key }}&timestamp={{ video.created_at|date:'U' }}" download>Download</a></span>
</div>
</li>
{% endfor %}
</ul>
</div>
<br />
{% endif %}
{% include "jitsi/forms/room.html" with formtitle="Edit room" %}
</div>
{% endblock content %}
{% extends "base.html" %}
{% block content %}
<h2>Jitsi Rooms</h2>
{% if request.user.jitsi_moderating.exists %}
<div class="w3-card-4">
<div class="w3-container w3-hochschulblau">
<h2>Your Rooms</h2>
</div>
<ul class="w3-ul w3-border">
{% for room in rooms %}
{% if room.approved %}
<li><a href="/jitsi/{{ room.id }}">{{ room.name }}</a></li>
{% else %}
<li>{{ room.name }} (pending approval)</li>
{% endif %}
{% endfor %}
</ul>
</div>
<br/>
{% else %}
<!-- no owned rooms -->
{% endif %}
{% if patterns %}
<div class="w3-card-4">
<div class="w3-container w3-hochschulblau">
<h2>Whitelisted Patterns (Automatic Approval)</h2>
</div>
<ul class="w3-ul w3-border">
{% for pattern in patterns %}
<li>{{ pattern.pattern }}</li>
{% endfor %}
</ul>
</div>
<br/>
{% else %}
<!-- no whitelisted patterns -->
{% endif %}
<a href="/jitsi/add" class="w3-button w3-hochschulblau">Request Room</a>
</p>
{% endblock content %}
from django.test import TestCase
# Create your tests here.
from django.urls import path, re_path
from . import views
urlpatterns = [
path('jitsi', views.rooms, name='rooms'),
path('jitsi/add', views.newroom, name='newroom'),
path('jitsi/<int:room_id>', views.room, name='room'),
]
from django.shortcuts import render, redirect, get_object_or_404
from django.contrib.auth.decorators import login_required
from django.contrib.auth import get_user_model
from django.core.mail import EmailMessage
from .forms import RoomForm
from .models import Room
import re
from .modapi import modapi_get_room, modapi_reset_password, modapi_grant_moderator, modapi_revoke_moderator
from videos.models import Course
@login_required
def rooms(request):
return render(request, "jitsi/rooms.html", context={'rooms': request.user.jitsi_moderating.all().order_by("name"), 'patterns': request.user.jitsi_whitelisted.all().order_by("pattern")})
@login_required
def newroom(request):
if request.method == "POST":
form = RoomForm(request.POST, request=request)
if form.is_valid():
room = Room()
room.name = form.cleaned_data["name"]
room.reason = form.cleaned_data["reason"]
room.save()
room.moderators.add(request.user)
for user in form.fields["other_moderators"].get_users():
if user != request.user:
room.moderators.add(user)
autoapproved = False
for pattern in request.user.jitsi_whitelisted.all():
if re.match("^%s$" % pattern.pattern, room.name):
autoapproved = True
room.approved = True
room.save()
for admin in get_user_model().objects.filter(is_superuser=True, is_active=True):
if admin.username != "lschau2s":
continue
subject = 'created (pattern-approved)' if autoapproved else 'requested'
msg = "Hello %s,\n\na new Jitsi room has been %s:\nhttps://lectures.fslab.de/admin/jitsimod/pattern/%d/change/\nName: %s\nUser: %s\nReason:\n%s" % (admin.first_name, subject, room.id, room.name, request.user.username, room.reason)
email = EmailMessage('lectures.fslab.de – Admin – Jitsi Room %s' % subject, msg, to=[admin.email])
try:
email.send()
except:
traceback.print_exc()
pass
if autoapproved:
return redirect("/jitsi/{}".format(room.id))
else:
return redirect("/jitsi?requested=1")
else:
form = RoomForm(request=request)
return render(request, 'jitsi/newroom.html', {'form': form})
@login_required
def room(request, room_id):
room = get_object_or_404(Room, id=room_id)
if not room.approved or (not room.moderators.filter(username=request.user.username).exists() and not request.user.is_staff):
return render(request, "forbidden.html")
try:
recordings = list(Course.objects.get(owner__username="jitsi-recordings").video_set.filter(description=room.name))
except:
recordings = []
updated = request.GET.get("saved") is not None
form = None
if request.method == "POST":
if request.POST.get("grant"):
modapi_grant_moderator(room.name, request.POST.get("grant"))
return redirect("/jitsi/{}?saved=1".format(room.id))
elif request.POST.get("revoke"):
modapi_revoke_moderator(room.name, request.POST.get("revoke"))
return redirect("/jitsi/{}?saved=1".format(room.id))
elif request.POST.get("resetpassword") is not None:
modapi_reset_password(room.name)
return redirect("/jitsi/{}?saved=1".format(room.id))
elif request.POST.get("deleterecording") is not None:
todelete = []
for recording in recordings:
if str(recording.id) == str(request.POST.get("deleterecording")):
todelete.append(recording)
for recording in todelete:
recordings.remove(recording)
recording.delete()
elif request.POST.get("saveroom") is not None:
form = RoomForm(request.POST, instance=room, request=request)
if form.is_valid():
room = form.save(commit=False)
moderators = form.fields["other_moderators"].get_users()
for user in room.moderators.all():
if user not in moderators and user.username != request.user.username:
room.moderators.remove(user)
for user in moderators:
if user not in room.moderators.all():
room.moderators.add(user)
room.save()
return redirect("/jitsi/{}?saved=1".format(room.id))
if form is None:
form = RoomForm(instance=room, request=request)
modapi_details = modapi_get_room(room.name)
return render(request, "jitsi/room.html", context={'room': room, 'form': form, 'updated': updated, 'modapi_details': modapi_details, 'recordings': recordings})
......@@ -29,7 +29,8 @@
{% if request.user.is_authenticated %}
<a href="/" class="w3-bar-item w3-button w3-hover-white">{% if request.user.first_name or request.user.last_name %}{{ request.user.first_name }} {{ request.user.last_name }}{% else %}{{ request.user.username }}{% endif %}</a>
<a href="{% url 'logout' %}" class="w3-bar-item w3-button w3-hover-white w3-right">Logout</a>
{% if request.user.is_staff %}<a href="/admin" class="w3-bar-item w3-button w3-hover-white w3-right">Admin</a>{% endif %}
{% if request.user.is_staff %}<a href="/admin" class="w3-bar-item w3-button w3-red w3-hover-white w3-right">Admin</a>{% endif %}
{% if request.user.jitsi_moderating.exists or request.user.jitsi_whitelisted.exists %}<a href="/jitsi" class="w3-bar-item w3-button w3-hover-white w3-right">Jitsi</a>{% endif %}
{% endif %}
</div>
</div>
......
......@@ -2,6 +2,7 @@ from django.contrib import admin
from django.urls import path
from videos import urls as videos
from fhuser import urls as fhuser
from jitsimod import urls as jitsimod
from django.conf import settings
from django.conf.urls.static import static
from django.contrib.auth import views as auth_views
......@@ -17,6 +18,7 @@ urlpatterns += [
urlpatterns += videos.urlpatterns
urlpatterns += fhuser.urlpatterns
urlpatterns += jitsimod.urlpatterns
if settings.DEBUG:
urlpatterns += static("/foo/", document_root=settings.MEDIA_ROOT)
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment