Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

make meetings bijective #129

Merged
merged 6 commits into from
May 29, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,12 @@
# ChangeLog

## 3.2.3 - 2024-05-28

Fixes:
- add possibility to use the same meeting ID for two different secrets.
PhilippKilian marked this conversation as resolved.
Show resolved Hide resolved
- return an error message if meeting id is out of range (2 <= id_length <= 100 chars)


## 3.2.2 - 2024-05-23

Fixes:
Expand Down
1 change: 1 addition & 0 deletions b3lb/rest/b3lb/constants.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@
RETURN_STRING_GET_RECORDING_TEXT_TRACKS_NOTHING_FOUND_JSON = '{"response":{"returncode":"FAILED","messageKey":"noRecordings","message":"No recording found"}}'
RETURN_STRING_GET_RECORDING_NO_RECORDINGS = '<response>\r\n<returncode>SUCCESS</returncode>\r\n<recordings></recordings>\r\n<messageKey>noRecordings</messageKey>\r\n<message>There are no recordings for the meeting(s).</message>\r\n</response>'
RETURN_STRING_MISSING_MEETING_ID = '<response>\r\n<returncode>FAILED</returncode>\r\n<messageKey>missingParamMeetingID</messageKey>\r\n<message>You must specify a meeting ID for the meeting.</message>\r\n</response>'
RETURN_STRING_MISSING_MEETING_ID_TO_LONG = '<response>\r\n<returncode>FAILED</returncode>\r\n<message>Meeting id must be between 2 and 100 characters</message>\r\n</response>'
RETURN_STRING_MISSING_RECORD_ID = '<response>\r\n<returncode>FAILED</returncode>\r\n<messageKey>missingParamRecordID</messageKey>\r\n<message>You must specify one or more a record IDs.</message>\r\n</response>'
RETURN_STRING_MISSING_RECORD_PUBLISH = '<response>\r\n<returncode>FAILED</returncode>\r\n<messageKey>missingParamPublish</messageKey>\r\n<message>You must specify one a publish value true or false.</message>\r\n</response>'
RETURN_STRING_RECORD_PUBLISHED = '<response>\r\n<returncode>SUCCESS</returncode>\r\n<published>{}</published>\r\n</response>'
Expand Down
103 changes: 54 additions & 49 deletions b3lb/rest/classes/api.py
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@
from rest.b3lb.metrics import incr_metric, update_create_metrics
from rest.b3lb.parameters import ALLOW_START_STOP_RECORDING, AUTO_START_RECORDING, BLOCK, LOGO, OVERRIDE, PARAMETERS_CREATE, PARAMETERS_JOIN, RECORD, SET, USERDATA_BBB_CUSTOM_STYLE_URL
from rest.b3lb.utils import get_checksum
from rest.models import is_meeting_name_length_fine, ClusterGroupRelation, Meeting, Metric, Node, Parameter, Record, RecordSet, Secret, SecretMeetingList, SecretMetricsList, Stats
from rest.models import ClusterGroupRelation, Meeting, Metric, Node, Parameter, Record, RecordSet, Secret, SecretMeetingList, SecretMetricsList, Stats
from typing import Any, Dict, List, Literal, Union
from uuid import UUID
from urllib.parse import urlencode
Expand All @@ -49,6 +49,7 @@ class ClientB3lbRequest:
request: HttpRequest
parameters: Dict[str, Any]
meeting_id: str
meeting_name: str
body: Union[str, bytes]
endpoint: str
checksum: str
Expand All @@ -60,6 +61,40 @@ class ClientB3lbRequest:
ENDPOINTS_PASS_THROUGH: List[str]
ENDPOINTS: Dict[str, Any]

## INIT ##
def __init__(self, request: HttpRequest, endpoint: str):
self.has_assets = False
self.request = request
self.endpoint = endpoint
self.parameters = {}
self.body = request.body
for parameter in request.GET.keys():
self.parameters[parameter] = request.GET.get(parameter)

self.meeting_id = self.parameters.get("meetingID", "")
self.meeting_name = self.parameters.get("name", "")
self.checksum = self.parameters.pop("checksum", "")
self.stats_token = self.request.headers.get("Authorization", "")

self.secret = None
self.node = None
self.state = self.parameters.get("state", "")
self.ENDPOINTS_PASS_THROUGH = ["end", "insertDocument", "setConfigXML", "getMeetingInfo"]
self.ENDPOINTS = {
"": self.version,
"create": self.create,
"join": self.join,
"isMeetingRunning": self.is_meeting_running,
"getMeetings": self.get_meetings,
"getRecordingTextTracks": self.get_recording_text_tracks,
"getRecordings": self.get_recordings,
"deleteRecordings": self.delete_recordings,
"publishRecordings": self.publish_recordings,
"updateRecordings": self.update_recordings,
"b3lb_metrics": self.metrics,
"b3lb_stats": self.stats
}

#### Class functions
async def _check_post_headers(self) -> Dict[str, Any]:
if not self.has_assets:
Expand All @@ -81,7 +116,10 @@ async def create(self) -> HttpResponse:
if not self.meeting_id:
return HttpResponse(cst.RETURN_STRING_MISSING_MEETING_ID, content_type=cst.CONTENT_TYPE)

if not is_meeting_name_length_fine(self.parameters.get("name", "")):
if not 2 <= len(self.meeting_id) <= cst.MEETING_ID_LENGTH:
return HttpResponse(cst.RETURN_STRING_MISSING_MEETING_ID_TO_LONG, content_type=cst.CONTENT_TYPE)

if not 2 <= len(self.meeting_name) <= cst.MEETING_NAME_LENGTH:
return HttpResponse(cst.RETURN_STRING_WRONG_MEETING_NAME_LENGTH, content_type=cst.CONTENT_TYPE)

if not await self.is_meeting():
Expand Down Expand Up @@ -449,7 +487,7 @@ def is_node_free(self) -> bool:

## Getter Routines ##
def get_meeting_defaults(self) -> Dict[str, Any]:
return {"id": self.meeting_id, "secret": self.secret, "node": self.node, "room_name": self.parameters.get("name", "Unknown"), "end_callback_url": self.parameters.get("meta_endCallbackUrl", "")}
return {"id": self.meeting_id, "secret": self.secret, "node": self.node, "room_name": self.meeting_name, "end_callback_url": self.parameters.get("meta_endCallbackUrl", "")}

def get_node_endpoint_url(self) -> str:
parameter_str = ""
Expand Down Expand Up @@ -540,39 +578,6 @@ async def set_secret_by_slug_and_slug_id(self, slug: str, sub_id: int):
except ObjectDoesNotExist:
pass

## INIT ##
def __init__(self, request: HttpRequest, endpoint: str):
self.has_assets = False
self.request = request
self.endpoint = endpoint
self.parameters = {}
self.body = request.body
for parameter in request.GET.keys():
self.parameters[parameter] = request.GET.get(parameter)

self.meeting_id = self.parameters.get("meetingID", "")
self.checksum = self.parameters.pop("checksum", "")
self.stats_token = self.request.headers.get("Authorization", "")

self.secret = None
self.node = None
self.state = self.parameters.get("state", "")
self.ENDPOINTS_PASS_THROUGH = ["end", "insertDocument", "setConfigXML", "getMeetingInfo"]
self.ENDPOINTS = {
"": self.version,
"create": self.create,
"join": self.join,
"isMeetingRunning": self.is_meeting_running,
"getMeetings": self.get_meetings,
"getRecordingTextTracks": self.get_recording_text_tracks,
"getRecordings": self.get_recordings,
"deleteRecordings": self.delete_recordings,
"publishRecordings": self.publish_recordings,
"updateRecordings": self.update_recordings,
"b3lb_metrics": self.metrics,
"b3lb_stats": self.stats
}


class NodeB3lbRequest:
"""
Expand All @@ -587,6 +592,19 @@ class NodeB3lbRequest:
nonce: str
recording_marks: str

def __init__(self, request: HttpRequest, backend: str, endpoint: str):
self.request = request
self.meeting = None
self.backend = backend
self.endpoint = endpoint
self.meeting_id = self.request.GET.get("meetingID", "")
self.nonce = self.request.GET.get("nonce", "")
self.recording_marks = self.request.GET.get("recordingmarks", "false")
self.BACKENDS = {
"meeting/end": {"methods": ["GET"], "function": self.end_meeting},
"record/upload": {"methods": ["POST"], "function": self.upload_record}
}

def is_allowed_endpoint(self) -> bool:
if self.full_endpoint() in self.BACKENDS:
return True
Expand Down Expand Up @@ -697,16 +715,3 @@ async def upload_record(self) -> HttpResponse:
await sync_to_async(record_set.save)()

return HttpResponse(status=204)

def __init__(self, request: HttpRequest, backend: str, endpoint: str):
self.request = request
self.meeting = None
self.backend = backend
self.endpoint = endpoint
self.meeting_id = self.request.GET.get("meetingID", "")
self.nonce = self.request.GET.get("nonce", "")
self.recording_marks = self.request.GET.get("recordingmarks", "false")
self.BACKENDS = {
"meeting/end": {"methods": ["GET"], "function": self.end_meeting},
"record/upload": {"methods": ["POST"], "function": self.upload_record}
}
17 changes: 17 additions & 0 deletions b3lb/rest/migrations/0017_rename_meeting_meetingold.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
# Generated by Django 3.2.23 on 2024-05-24 13:22

from django.db import migrations


class Migration(migrations.Migration):

dependencies = [
('rest', '0016_resize_meeting_name_length'),
]

operations = [
migrations.RenameModel(
old_name='Meeting',
new_name='MeetingOld',
),
]
44 changes: 44 additions & 0 deletions b3lb/rest/migrations/0018_meeting.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
# Generated by Django 3.2.23 on 2024-05-24 13:29

from django.db import migrations, models
import django.db.models.deletion
import django.utils.timezone
import rest.models
import uuid


class Migration(migrations.Migration):

dependencies = [
('rest', '0017_rename_meeting_meetingold'),
]

operations = [
migrations.CreateModel(
name='MeetingNew',
fields=[
('uuid', models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False, unique=True)),
('id', models.CharField(max_length=100)),
('room_name', models.CharField(max_length=256)),
('age', models.DateTimeField(default=django.utils.timezone.now)),
('attendees', models.SmallIntegerField(default=0)),
('end_callback_url', models.URLField(default='')),
('listenerCount', models.SmallIntegerField(default=0)),
('nonce', models.CharField(default=rest.models.get_nonce, editable=False, max_length=64, unique=True)),
('voiceParticipantCount', models.SmallIntegerField(default=0)),
('moderatorCount', models.SmallIntegerField(default=0)),
('videoCount', models.SmallIntegerField(default=0)),
('bbb_origin', models.CharField(default='', max_length=255)),
('bbb_origin_server_name', models.CharField(default='', max_length=255)),
('node', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='rest.node')),
('secret', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='rest.secret')),
],
options={
'ordering': ['secret__tenant', 'age'],
},
),
migrations.AddConstraint(
model_name='meetingnew',
constraint=models.UniqueConstraint(fields=('id', 'secret'), name='unique_meeting'),
),
]
35 changes: 35 additions & 0 deletions b3lb/rest/migrations/0019_migrate_meeting_data.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
# Generated by Django 3.2.23 on 2024-05-24 13:29

from django.db import migrations

def migrate_meetings(apps, schema_editor):
meeting_class = apps.get_model("rest", "MeetingNew")
meeting_old_class = apps.get_model("rest", "MeetingOld")
for meeting_old in meeting_old_class.objects.all():
meeting = meeting_class()
meeting.id = meeting_old.id
meeting.secret = meeting_old.secret
meeting.node = meeting_old.node
meeting.room_name = meeting_old.room_name
meeting.age = meeting_old.age
meeting.attendees = meeting_old.attendees
meeting.end_callback_url = meeting_old.end_callback_url
meeting.listenerCount = meeting_old.listenerCount
meeting.nonce = meeting_old.nonce
meeting.voiceParticipantCount = meeting_old.voiceParticipantCount
meeting.moderatorCount = meeting_old.moderatorCount
meeting.videoCount = meeting_old.videoCount
meeting.bbb_origin = meeting_old.bbb_origin
meeting.bbb_origin_server_name = meeting_old.bbb_origin_server_name
meeting.save()


class Migration(migrations.Migration):

dependencies = [
('rest', '0018_meeting'),
]

operations = [
migrations.RunPython(migrate_meetings)
]
39 changes: 39 additions & 0 deletions b3lb/rest/migrations/0020_remove_meetingold.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
# Generated by Django 3.2.23 on 2024-05-24 13:46

from django.db import migrations, models
import django.db.models.deletion

def migrate_record_set_meetings(apps, schema_editor):
record_set_class = apps.get_model('rest', 'RecordSet')
for record_set in record_set_class.objects.all():
if record_set.meeting:
record_set.meeting = None
record_set.save()


class Migration(migrations.Migration):

dependencies = [
('rest', '0019_migrate_meeting_data'),
]

operations = [
migrations.RunPython(migrate_record_set_meetings),
migrations.AlterField(
model_name='recordset',
name='meeting',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to='rest.meetingnew'),
),
migrations.DeleteModel(
name='MeetingOld',
),
migrations.RenameModel(
old_name='MeetingNew',
new_name='Meeting',
),
migrations.AlterField(
model_name='recordset',
name='meeting',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to='rest.meeting'),
),
]
23 changes: 23 additions & 0 deletions b3lb/rest/migrations/0021_refill_recordset.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
# Generated by Django 3.2.23 on 2024-05-24 14:14

from django.db import migrations

def refill_record_set(apps, schema_editor):
record_set_class = apps.get_model('rest', 'RecordSet')
meeting_class = apps.get_model('rest', 'Meeting')
for record_set in record_set_class.objects.all():
if record_set.status in ["UNKNOWN", "UPLOADED"] and record_set.meta_meeting_id:
meetings = meeting_class.objects.filter(id=record_set.meta_meeting_id, secret=record_set.secret)
if meetings.count() == 1:
record_set.meeting = meetings[0]
record_set.save()

class Migration(migrations.Migration):

dependencies = [
('rest', '0020_remove_meetingold'),
]

operations = [
migrations.RunPython(refill_record_set)
]
12 changes: 6 additions & 6 deletions b3lb/rest/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,12 +16,13 @@


from base64 import b32encode
from django.db import models
from django.core.exceptions import ValidationError
from django.core.files.storage import FileSystemStorage, default_storage
from django.core.validators import MaxValueValidator, MinValueValidator, RegexValidator
from django.conf import settings
from django.contrib import admin
from django.db import models
from django.db.models.constraints import UniqueConstraint
from django.utils import timezone
from django.utils.crypto import get_random_string
from django.utils.html import format_html
Expand Down Expand Up @@ -64,10 +65,6 @@ def get_storage():
return used_storage


def is_meeting_name_length_fine(name: str) -> bool:
return 2 <= len(name) < cst.MEETING_NAME_LENGTH


#
# ADMIN ACTIONS
#
Expand Down Expand Up @@ -575,7 +572,8 @@ class SecretMetricsListAdmin(admin.ModelAdmin):

# meeting - tenant - node relation class
class Meeting(models.Model):
id = models.CharField(max_length=cst.MEETING_ID_LENGTH, primary_key=True)
uuid = models.UUIDField(primary_key=True, editable=False, unique=True, default=uid.uuid4)
id = models.CharField(max_length=cst.MEETING_ID_LENGTH)
secret = models.ForeignKey(Secret, on_delete=models.CASCADE)
node = models.ForeignKey(Node, on_delete=models.CASCADE)
room_name = models.CharField(max_length=cst.MEETING_NAME_LENGTH)
Expand All @@ -592,11 +590,13 @@ class Meeting(models.Model):

class Meta(object):
ordering = ['secret__tenant', 'age']
constraints = [UniqueConstraint(fields=['id', 'secret'], name='unique_meeting')]

def __str__(self):
return "{} {}".format(self.secret.tenant.slug, self.room_name)


# meeting - tenant - node relation class
class MeetingAdmin(admin.ModelAdmin):
model = Meeting
list_display = ['__str__', 'bbb_origin_server_name', 'node', 'attendees', 'listenerCount', 'voiceParticipantCount', 'videoCount', 'age', 'id']
Expand Down
Loading