From 668c692127519ddb9a44dc290c0ae2c61109b8b3 Mon Sep 17 00:00:00 2001 From: lebaudantoine Date: Sun, 12 Jan 2025 23:32:31 +0100 Subject: [PATCH 1/4] =?UTF-8?q?=F0=9F=9A=A7(backend)=20fix=20missing=20mig?= =?UTF-8?q?rations?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Oopsie. Forgot to run migrations while fixing Django warnings. --- ...sourceaccess_options_alter_user_options.py | 21 +++++++++++++++++++ 1 file changed, 21 insertions(+) create mode 100644 src/backend/core/migrations/0010_alter_resourceaccess_options_alter_user_options.py diff --git a/src/backend/core/migrations/0010_alter_resourceaccess_options_alter_user_options.py b/src/backend/core/migrations/0010_alter_resourceaccess_options_alter_user_options.py new file mode 100644 index 000000000..c99265b82 --- /dev/null +++ b/src/backend/core/migrations/0010_alter_resourceaccess_options_alter_user_options.py @@ -0,0 +1,21 @@ +# Generated by Django 5.1.4 on 2025-01-12 22:30 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('core', '0009_alter_recording_status'), + ] + + operations = [ + migrations.AlterModelOptions( + name='resourceaccess', + options={'ordering': ('-created_at',), 'verbose_name': 'Resource access', 'verbose_name_plural': 'Resource accesses'}, + ), + migrations.AlterModelOptions( + name='user', + options={'ordering': ('-created_at',), 'verbose_name': 'user', 'verbose_name_plural': 'users'}, + ), + ] From 9117bf3bef7c8836d3837c8aa835f4feaf54eab7 Mon Sep 17 00:00:00 2001 From: lebaudantoine Date: Sun, 12 Jan 2025 23:06:04 +0100 Subject: [PATCH 2/4] =?UTF-8?q?=E2=9C=A8(backend)=20implement=209-digit=20?= =?UTF-8?q?room=20PIN=20codes=20for=20SIP=20access?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Enable users to join rooms via SIP telephony by: - Dialing the SIP trunk number - Entering the room's PIN followed by '#' The PIN code needs to be generated before the LiveKit room is created, allowing the owner to send invites to participants in advance. With 9-digit PINs (10^9 combinations) and a large number of rooms (e.g., 1M), collisions are inevitable. However, a retry mechanism reduces the likelihood of multiple consecutive collisions. Discussion points: - The `while` loop should be reviewed. Should we add rate limiting for failed attempts? - A systematic existence check before `INSERT` is more costly for a rare event and doesn't prevent race conditions, whereas retrying on integrity errors is more efficient overall. - Should we add logging or monitoring to track and analyze collisions? I tried balances performance and simplicity while ensuring the robustness of the PIN generation process. --- .../core/migrations/0011_room_pin_code.py | 18 ++++++++++ src/backend/core/models.py | 33 +++++++++++++++++-- src/backend/core/utils.py | 5 +++ src/backend/meet/settings.py | 12 +++++++ 4 files changed, 66 insertions(+), 2 deletions(-) create mode 100644 src/backend/core/migrations/0011_room_pin_code.py diff --git a/src/backend/core/migrations/0011_room_pin_code.py b/src/backend/core/migrations/0011_room_pin_code.py new file mode 100644 index 000000000..d13155a30 --- /dev/null +++ b/src/backend/core/migrations/0011_room_pin_code.py @@ -0,0 +1,18 @@ +# Generated by Django 5.1.4 on 2025-01-12 22:34 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('core', '0010_alter_resourceaccess_options_alter_user_options'), + ] + + operations = [ + migrations.AddField( + model_name='room', + name='pin_code', + field=models.CharField(blank=True, help_text="Unique n-digit code that identifies this room. Automatically generated on creation. Displayed with '#' suffix.", max_length=100, null=True, unique=True, verbose_name='Room PIN code'), + ), + ] diff --git a/src/backend/core/models.py b/src/backend/core/models.py index 536531432..135dc3c0b 100644 --- a/src/backend/core/models.py +++ b/src/backend/core/models.py @@ -11,13 +11,15 @@ from django.contrib.auth.base_user import AbstractBaseUser from django.core import mail, validators from django.core.exceptions import PermissionDenied, ValidationError -from django.db import models +from django.db import IntegrityError, models from django.utils.functional import lazy from django.utils.text import capfirst, slugify from django.utils.translation import gettext_lazy as _ from timezone_field import TimeZoneField +from core import utils + logger = getLogger(__name__) @@ -361,13 +363,23 @@ class Room(Resource): primary_key=True, ) slug = models.SlugField(max_length=100, blank=True, null=True, unique=True) - configuration = models.JSONField( blank=True, default=dict, verbose_name=_("Visio room configuration"), help_text=_("Values for Visio parameters to configure the room."), ) + pin_code = models.CharField( + max_length=100, + unique=True, + blank=True, + null=True, + verbose_name=_("Room PIN code"), + help_text=_( + "Unique n-digit code that identifies this room. Automatically generated on creation." + " Displayed with '#' suffix." + ), + ) class Meta: db_table = "meet_room" @@ -378,6 +390,23 @@ class Meta: def __str__(self): return capfirst(self.name) + def save(self, *args, **kwargs): + """Generate a unique n-digit pin code for new rooms. + + This uses the DB to ensure uniqueness of the code. Better than checking separately + with the DB and then saving, which introduces a race condition. + """ + + if self.pk or self.pin_code: + return super().save(*args, **kwargs) + + while True: + try: + self.pin_code = utils.generate_pin_code(n=settings.ROOM_PIN_CODE_LENGTH) + return super().save(*args, **kwargs) + except IntegrityError: + continue + def clean_fields(self, exclude=None): """ Automatically generate the slug from the name and make sure it does not look like a UUID. diff --git a/src/backend/core/utils.py b/src/backend/core/utils.py index 304b5de7d..169d5c7c3 100644 --- a/src/backend/core/utils.py +++ b/src/backend/core/utils.py @@ -15,6 +15,11 @@ from livekit.api import AccessToken, VideoGrants +def generate_pin_code(n: int) -> str: + """Generate a n-digit pin code""" + return f"{''.join([str(random.randint(0, 9)) for _ in range(n)])}" + + def generate_color(identity: str) -> str: """Generates a consistent HSL color based on a given identity string. diff --git a/src/backend/meet/settings.py b/src/backend/meet/settings.py index 9b30d2de8..51f202ebd 100755 --- a/src/backend/meet/settings.py +++ b/src/backend/meet/settings.py @@ -483,6 +483,18 @@ class Base(Configuration): ) BREVO_API_CONTACT_ATTRIBUTES = values.DictValue({"VISIO_USER": True}) + # SIP Telephony + ROOM_PIN_CODE_LENGTH = values.PositiveIntegerValue( + 9, # this value cannot exceed 100 digits due to database constraints + environ_name="ROOM_PIN_CODE_LENGTH", + environ_prefix=None, + ) + ROOM_PIN_CODE_GENERATION_MAX_RETRY = values.PositiveIntegerValue( + 3, + environ_name="ROOM_PIN_CODE_GENERATION_MAX_RETRY", + environ_prefix=None, + ) + # pylint: disable=invalid-name @property def ENVIRONMENT(self): From 2a32330c0de13cb5aa763b67b0d230c6a23613b1 Mon Sep 17 00:00:00 2001 From: lebaudantoine Date: Sun, 12 Jan 2025 23:22:49 +0100 Subject: [PATCH 3/4] =?UTF-8?q?=F0=9F=9A=A7(backend)=20serialize=20SIP-rel?= =?UTF-8?q?ated=20information?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Users require a phone number and PIN code to join a meeting by phone. This code is an initial brainstorming effort: - Phone numbers must be purchased from a SIP Trunk provider, which converts phone calls into Voice over IP (VoIP) and initiates a SIP connection with our LiveKit SIP server. - The Trunk provider needs to be configured with the LiveKit SIP server, either via CLI commands or API calls. Open questions for design: - Should a phone number be persisted in Django settings? This approach risks the serialized number becoming out of sync with the actual configuration. - Alternatively, should we list configured numbers from the LiveKit SIP server? This ensures accuracy but comes with the performance cost of querying the LiveKit API. Could a caching strategy mitigate this issue? Currently, the right design strategy remains unclear. Data responsibility seems to naturally belong on the LiveKit side, but further discussion and exploration are needed. --- src/backend/core/api/serializers.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/src/backend/core/api/serializers.py b/src/backend/core/api/serializers.py index 51b37fbb0..b27b05a39 100644 --- a/src/backend/core/api/serializers.py +++ b/src/backend/core/api/serializers.py @@ -141,6 +141,13 @@ def to_representation(self, instance): ), } + # Todo - discuss this part, retrieve phone number from a setting? Dynamically? + # Todo - is it the right place? + # Todo - discuss the method `to_representation` which is quite dirty IMO + pin_code = self.instance.pin_code + if pin_code is not None: + output["livekit"]["sip"] = {"pin_code": pin_code, "phone_number": "wip"} + output["is_administrable"] = is_admin return output From d0fef0355595da0782fe9793f4774476fe15e75b Mon Sep 17 00:00:00 2001 From: lebaudantoine Date: Sun, 12 Jan 2025 23:42:17 +0100 Subject: [PATCH 4/4] =?UTF-8?q?=F0=9F=9A=A7(backend)=20generate=20pin=20co?= =?UTF-8?q?de=20for=20existing=20rooms?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit To ensure consistency, generate a pin code for existing rooms in db. Not sure of this approach. --- .../migrations/0012_generate_room_pin_code.py | 34 +++++++++++++++++++ 1 file changed, 34 insertions(+) create mode 100644 src/backend/core/migrations/0012_generate_room_pin_code.py diff --git a/src/backend/core/migrations/0012_generate_room_pin_code.py b/src/backend/core/migrations/0012_generate_room_pin_code.py new file mode 100644 index 000000000..f118dfb26 --- /dev/null +++ b/src/backend/core/migrations/0012_generate_room_pin_code.py @@ -0,0 +1,34 @@ +from django.db import migrations +import random + + +def generate_pin_for_rooms(apps, schema_editor): + """Generate unique 9-digit PIN codes for existing rooms. + + The PIN code is required for upcoming SIP telephony features. + """ + Room = apps.get_model('core', 'Room') + rooms_without_pin = Room.objects.filter(pin_code__isnull=True) + + def generate_pin(): + while True: + pin = str(random.randint(0, 999999999)).zfill(9) + if not Room.objects.filter(pin_code=pin).exists(): + return pin + + for room in rooms_without_pin: + room.pin_code = generate_pin() + room.save() + + +class Migration(migrations.Migration): + dependencies = [ + ('core', '0011_room_pin_code'), + ] + + operations = [ + migrations.RunPython( + generate_pin_for_rooms, + reverse_code=migrations.RunPython.noop + ), + ]