Compare commits

...

4 Commits

Author SHA1 Message Date
lebaudantoine d0fef03555 🚧(backend) generate pin code for existing rooms
To ensure consistency, generate a pin code for existing
rooms in db.

Not sure of this approach.
2025-01-12 23:42:17 +01:00
lebaudantoine 2a32330c0d 🚧(backend) serialize SIP-related information
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.
2025-01-12 23:37:33 +01:00
lebaudantoine 9117bf3bef (backend) implement 9-digit room PIN codes for SIP access
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.
2025-01-12 23:37:25 +01:00
lebaudantoine 668c692127 🚧(backend) fix missing migrations
Oopsie. Forgot to run migrations while fixing Django warnings.
2025-01-12 23:37:25 +01:00
7 changed files with 128 additions and 2 deletions
+7
View File
@@ -141,6 +141,13 @@ class RoomSerializer(serializers.ModelSerializer):
),
}
# 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
@@ -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'},
),
]
@@ -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'),
),
]
@@ -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
),
]
+31 -2
View File
@@ -11,13 +11,15 @@ from django.contrib.auth import models as auth_models
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 Room(Resource):
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.
+5
View File
@@ -15,6 +15,11 @@ from django.conf import settings
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.
+12
View File
@@ -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):