✨(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.
This commit is contained in:
lebaudantoine
2025-01-12 23:37:25 +01:00
parent 668c692127
commit 9117bf3bef
4 changed files with 66 additions and 2 deletions
@@ -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'),
),
]
+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):