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

Configure video settings for talks #288

Closed
135 changes: 135 additions & 0 deletions server/venueless/api/task.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,135 @@
import datetime
import datetime as dt
import logging
import uuid

import jwt
import requests
from celery import shared_task
from django.conf import settings

from venueless.core.models.auth import ShortToken
from venueless.core.models.world import World

logger = logging.getLogger(__name__)


def generate_video_token(world, days, number, traits, long=False):
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

issue (complexity): Consider introducing a TokenService class to encapsulate token generation and management functionality.

The token generation logic can be simplified by introducing a dedicated TokenService class to handle token creation and reduce parameter passing. This separates concerns and makes the code more maintainable. Here's a suggested refactor:

class TokenService:
    def __init__(self, jwt_config):
        self.secret = jwt_config['secret']
        self.audience = jwt_config['audience']
        self.issuer = jwt_config['issuer']
        self.now = datetime.datetime.utcnow()

    def create_video_token(self, traits, expiry_days):
        return {
            "iss": self.issuer,
            "aud": self.audience,
            "exp": self.now + datetime.timedelta(days=expiry_days),
            "iat": self.now,
            "uid": str(uuid.uuid4()),
            "traits": traits,
        }

    def create_talk_token(self, video_tokens, event_slug):
        return {
            "exp": self.now + datetime.timedelta(days=30),
            "iat": self.now,
            "video_tokens": video_tokens,
            "slug": event_slug,
        }

    def encode_token(self, payload):
        return jwt.encode(payload, self.secret, algorithm="HS256")

Usage in the task:

@shared_task(bind=True, max_retries=5, default_retry_delay=60)
def configure_video_settings_for_talks(self, world_id, days, number, traits, long=False):
    world = World.objects.get(id=world_id)
    jwt_config = world.config.get("JWT_secrets", [])[0]

    token_service = TokenService(jwt_config)
    video_tokens = []
    bulk_tokens = []

    for _ in range(number):
        payload = token_service.create_video_token(traits, days)
        token = token_service.encode_token(payload)

        if long:
            video_tokens.append(token)
        else:
            st = ShortToken(world=world, long_token=token, 
                          expires=payload['exp'])
            video_tokens.append(st.short_token)
            bulk_tokens.append(st)

    if bulk_tokens:
        ShortToken.objects.bulk_create(bulk_tokens)

    talk_payload = token_service.create_talk_token(video_tokens, world_id)
    talk_token = token_service.encode_token(talk_payload)
    # ... rest of the API call code

This refactor:

  1. Consolidates token generation logic in one place
  2. Eliminates duplicate datetime handling
  3. Reduces parameter passing
  4. Makes the code more testable
  5. Keeps the important error handling intact

"""
Generate video token
:param world: World object
:param days: A integer representing the number of days the token is valid
:param number: A integer representing the number of tokens to generate
:param traits: A dictionary representing the traits of the token
:param long: A boolean representing if the token is long or short
:return: A list of tokens
"""
jwt_secrets = world.config.get("JWT_secrets", [])
if not jwt_secrets:
logger.error("JWT_secrets is missing or empty in the configuration")
return
jwt_config = jwt_secrets[0]
secret = jwt_config.get("secret")
audience = jwt_config.get("audience")
issuer = jwt_config.get("issuer")
iat = datetime.datetime.utcnow()
exp = iat + datetime.timedelta(days=days)
result = []
bulk_create = []
for _ in range(number):
payload = {
"iss": issuer,
"aud": audience,
"exp": exp,
"iat": iat,
"uid": str(uuid.uuid4()),
"traits": traits,
}
token = jwt.encode(payload, secret, algorithm="HS256")
if long:
result.append(token)
else:
st = ShortToken(world=world, long_token=token, expires=exp)
result.append(st.short_token)
bulk_create.append(st)

if not long:
ShortToken.objects.bulk_create(bulk_create)
return result


def generate_talk_token(video_settings, video_tokens, event_slug):
"""
Generate talk token
:param video_settings: A dictionary representing the video settings
:param video_tokens: A list of video tokens
:param event_slug: A string representing the event slug
:return: A token
"""
iat = dt.datetime.utcnow()
exp = iat + dt.timedelta(days=30)
payload = {
"exp": exp,
"iat": iat,
"video_tokens": video_tokens,
"slug": event_slug,
}
token = jwt.encode(payload, video_settings.get("secret"), algorithm="HS256")
return token


@shared_task(bind=True, max_retries=5, default_retry_delay=60)
def configure_video_settings_for_talks(
self, world_id, days, number, traits, long=False
):
"""
Configure video settings for talks
:param self: instance of the task
:param world_id: A integer representing the world id
:param days: A integer representing the number of days the token is valid
:param number: A integer representing the number of tokens to generate
:param traits: A dictionary representing the traits of the token
:param long: A boolean representing if the token is long or short
"""
world = World.objects.get(id=world_id)
event_slug = world_id
odkhang marked this conversation as resolved.
Show resolved Hide resolved
jwt_secrets = world.config.get("JWT_secrets", [])
if not jwt_secrets:
logger.error("JWT_secrets is missing or empty in the configuration")
return
jwt_config = jwt_secrets[0]
video_tokens = generate_video_token(world, days, number, traits, long)
talk_token = generate_talk_token(jwt_config, video_tokens, event_slug)
header = {
"Content-Type": "application/json",
"Authorization": f"Bearer {talk_token}",
}
payload = {
"video_settings": {
"audience": jwt_config.get("audience"),
"issuer": jwt_config.get("issuer"),
"secret": jwt_config.get("secret"),
}
}
try:
requests.post(
"{}/api/configure-video-settings/".format(settings.EVENTYAY_TALK_BASE_PATH),
json=payload,
headers=header,
)
world.config["pretalx"] = {
"event": event_slug,
"domain": "{}".format(settings.EVENTYAY_TALK_BASE_PATH),
"pushed": datetime.datetime.now().isoformat(),
"connected": True,
}
world.save()
except requests.exceptions.ConnectionError as e:
logger.error("Connection error: %s", str(e))
self.retry(exc=e)
except requests.exceptions.Timeout as e:
logger.error("Request timed out: %s", str(e))
self.retry(exc=e)
except requests.exceptions.RequestException as e:
odkhang marked this conversation as resolved.
Show resolved Hide resolved
logger.error("Request failed: %s", str(e))
self.retry(exc=e)
16 changes: 15 additions & 1 deletion server/venueless/api/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@
from venueless.core.services.world import notify_schedule_change, notify_world_change

from ..core.models import Room, World
from .task import configure_video_settings_for_talks
from .utils import get_protocol

logger = logging.getLogger(__name__)
Expand Down Expand Up @@ -149,6 +150,15 @@ def post(request, *args, **kwargs) -> JsonResponse:

title = titles.get(locale) or titles.get("en") or title_default

attendee_trait_grants = request.data.get("traits", {}).get("attendee", "")
trait_grants = {
"admin": ["admin"],
"attendee": (
[attendee_trait_grants] if attendee_trait_grants else ["attendee"]
),
"scheduleuser": ["schedule-update"],
}

# if world already exists, update it, otherwise create a new world
world_id = request.data.get("id")
domain_path = "{}{}/{}".format(
Expand All @@ -165,6 +175,7 @@ def post(request, *args, **kwargs) -> JsonResponse:
world.domain = domain_path
world.locale = request.data.get("locale") or "en"
world.timezone = request.data.get("timezone") or "UTC"
world.trait_grants = trait_grants
world.save()
else:
world = World.objects.create(
Expand All @@ -174,8 +185,11 @@ def post(request, *args, **kwargs) -> JsonResponse:
locale=request.data.get("locale") or "en",
timezone=request.data.get("timezone") or "UTC",
config=config,
trait_grants=trait_grants,
)

configure_video_settings_for_talks.delay(
world_id, days=30, number=1, traits=["schedule-update"], long=True
)
site_url = settings.SITE_URL
protocol = get_protocol(site_url)
world.domain = "{}://{}".format(protocol, domain_path)
Expand Down
3 changes: 3 additions & 0 deletions server/venueless/settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -202,6 +202,9 @@
MEDIA_URL = os.getenv(
"VENUELESS_MEDIA_URL", config.get("urls", "media", fallback="/media/")
)
EVENTYAY_TALK_BASE_PATH = config.get(
"urls", "eventyay-talk", fallback="https://app-test.eventyay.com/talk"
)

WEBSOCKET_PROTOCOL = os.getenv(
"VENUELESS_WEBSOCKET_PROTOCOL",
Expand Down
Loading