class Message(models.Model):
    TYPE = Choices(
        ('s', 'signalling', 'signalling'),
        ('e', 'event', 'event'),
        ('j', 'join', 'join'),
        ('a', 'announce', 'announce'),
        ('A', 'action', 'action'),
        ('l', 'leave', 'leave'),
    )
    ENCODING_KEY_MAP = {
        't': 'type',
        'T': 'timestamp',
        'p': 'payload',
        'P': 'peer_id',
        'u': 'participant_id',
        'i': 'id'
    }
    room = models.ForeignKey('Room', related_name='messages')
    participant = models.ForeignKey('Participant', blank=True, null=True)
    peer_id = models.UUIDField(blank=True, null=True)
    type = models.CharField(max_length=1, choices=TYPE)
    payload = JSONField()
    timestamp = models.DateTimeField(default=timezone.now)

    class Meta:
        ordering = ['-timestamp']
        get_latest_by = 'timestamp'

    @classmethod
    def encode_message_dict(cls, message_dict):
        out = {
            k: message_dict[v]
            for k, v in cls.ENCODING_KEY_MAP.items()
            if v in message_dict
        }
        if not out.get('t') or not out.get('p'):
            raise ValueError("Invalid message")
        return json.dumps(out)

    def encode(self):
        from ..serializers import MessageSerializer
        data = MessageSerializer(self).data
        if 'uid' in data:
            # rename.
            # TODO: do in a nicer way?
            data['participant_id'] = data['uid']
            del data['uid']
        return self.encode_message_dict(data)


    @classmethod
    def decode(cls, message_dict):
        return cls(**cls.decode_message_dict(message_dict))

    @classmethod
    def decode_message_dict(cls, message_dict):
        try:
            decoded = {
                v: message_dict[k]
                for k, v in Message.ENCODING_KEY_MAP.items()
                if k in message_dict
            }
        except TypeError:
            raise ValueError("Invalid message")
        if (
            'type' not in decoded or
            'payload' not in decoded or
            decoded['type'] not in Message.TYPE
        ):
            print(message_dict)
            raise ValueError("Invalid message")
        return decoded class Participant(models.Model):
    user = models.OneToOneField('accounts.User', blank=True, null=True,
                                related_name='participant')
    session_key = models.CharField(max_length=32, blank=True, null=True)
    name = models.CharField(max_length=64, blank=True, null=True)

    def get_display_name(self):
        if self.name is None:
            if self.user_id:
                return self.user.get_short_name()
        return self.name

    objects = ParticipantManager() class Room(models.Model):
    ACTION_TYPES = [
        'start_recording',
        'stop_recording',
        'pause_recording',
        'kick',
        'update_config',
    ]

    ROOM_TYPES = ['audio', 'video']

    event_handlers = {}

    #: For now, all room IDs are 6-character randomly generated strings.
    id = models.CharField(
        max_length=6,
        primary_key=True,
        default=RoomManager.generate_id
    )

    #: The owner is the Participant who created the room.
    owner = models.ForeignKey('Participant', related_name='owned_rooms')

    members = models.ManyToManyField('Participant',
        through='RoomMembership',
        related_name='rooms'
    )
    created = models.DateTimeField(auto_now_add=True)

    #: Field for storing the room config.
    #: See RoomConfigSerializer for docs.
    config = JSONField(blank=True, null=True, default=dict)

    objects = RoomManager()

    # def get_config_json(self):
    #     return json.dumps(serializers.RoomConfigSerializer(self.config).data)

    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)
        self.peers = PeerManager(redis_conn, self)

    @property
    def owner_membership(self):
        """
        The membership belonging to the owner, or None if it doesn't exist yet
        """
        return self.memberships.filter(participant=self.owner).first()

    @property
    def connected_memberships(self):
        return self.memberships.filter(participant__in=self.peers.participant_ids)

    @property
    def group_name(self):
        return 'room.' + self.id

    def is_admin(self, participant):
        """Checks whether `participant` has admin rights for this room."""
        return participant == self.owner

    def is_member(self, participant):
        """Checks whether `participant` is a member of this room."""
        return self.memberships.filter(participant=participant).exists()

    def is_valid_action(self, action_name):
        """Checks whether `action_name` is a valid action."""
        return action_name in self.ACTION_TYPES

    def message(self, type, payload, timestamp=None, participant_id=None,
                peer_id=None, id=None):
        """Convenience method to initialize a message for this room."""
        return Message(
            type=type,
            payload=payload,
            timestamp=timestamp or now(),
            participant_id=participant_id,
            peer_id=peer_id,
            id=id,
            room=self
        )

    def receive_event(self, message):
        """
        Receive an event, represented by `message`, and perform action
        on it if necessary, before sending it to the room.
        """
        # TODO add a test
        assert message.type == Message.TYPE.event
        event = message.payload

        if event['type'] in self.event_handlers:
            self.event_handlers[event['type']](
                event=event,
                message=message,
                room=self,
            )

        self.send(message)

    def get_initial_data(self):
        return serializers.InitialRoomDataSerializer(self).data

    def send(self, message, to_peer_id=None, save=None):
        """
        Send `message` to the room (or, if `to_peer_id` is set, then just
        to that peer).
        By default, the message is saved depending on its type. This can
        be manually overridden by setting `save` to either True or
        False.
        """
        if to_peer_id is None:
            to = Group(self.group_name)
        else:
            to = self.peers[to_peer_id].channel
        if save is None and to_peer_id is None:
            save = self.should_save_message(message)
        if save:
            self.add_message(message)
        to.send({'text': message.encode()})

    def set_config(self, new_config):
        """Update the room config, and send an update_config event"""
        self.config = new_config
        self.save()
        self.send(self.message(
            type=Message.TYPE.event,
            payload={
                'type': 'update_config',
                'data': new_config
            }
        ))

    def get_config(self):
        """Get the room config."""
        self.refresh_from_db(fields=['config'])
        return serializers.RoomConfigSerializer(self.config).data

    def _send_action_event(self, name, target_peer_id, from_peer_id,
                          from_participant, data=None):
        if data is None:
            data = {}
        self.send(self.message(
            type=Message.TYPE.event,
            payload={
                'type': name,
                'data': {
                    'target': target_peer_id,
                    **data
                },
            },
            peer_id=from_peer_id,
            participant_id=from_participant.id
        ))

    def start_recording(self, *args, **kwargs):
        # FIXME: probably should check if peer is recording before actually
        # transmitting
        return self._send_action_event('request_start_recording',
            *args, **kwargs
        )

    def stop_recording(self, *args, **kwargs):
        # FIXME: probably should check if peer is recording before actually
        # transmitting
        return self._send_action_event('request_stop_recording',
            *args, **kwargs
        )

    def kick(self, peer_id, from_peer_id):
        # TODO: implement kick
        raise NotImplementedError

    def change_member_name(self, participant, name):
        self.memberships.filter(participant=participant).update(name=name)
        self.send(self.message(
            type=Message.TYPE.event,
            payload={
                'type': 'update_status',
                'data': {
                    'name': name,
                },
            },
            participant_id=participant.id,
        ))

    def announce(self, peer_id, participant):
        mem = self.memberships.get(participant=participant)
        mem._peer_id = peer_id
        self.send(self.message(
            type=Message.TYPE.announce,
            payload={
                'peer': serializers.MembershipSerializer(mem).data
            },
            participant_id=participant.id,
            peer_id=peer_id
        ))

    def leave(self, peer_id, participant):
        self.peers[peer_id].disconnect()
        self.send(self.message(
            type=Message.TYPE.leave,
            payload={
                'id': peer_id
            },
            participant_id=participant.id,
            peer_id=peer_id
        ))

    def connect(self, participant, channel_name):
        """
        Connect an existing participant `participant`, connected on
        channel `channel_name`, to the room.
        """
        # TODO fix tests
        if not self.members.filter(id=participant.id).exists():
            raise RoomMembership.DoesNotExist

        peer = self.peers.for_participant(participant)
        if peer is None:
            return self.peers.connect(participant, channel_name).id
        else:
            return peer.id

    def create_recording(self, **kwargs):
        """
        Create a recording for this room, with data set by **kwargs, and
        send an event.
        """
        rec = self.recordings.create(**kwargs)
        self.send(self.message(
            type=Message.TYPE.event,
            payload={
                'type': 'start_recording',
                'data': recordings.serializers.RecordingSerializer(rec).data,
            },
            participant_id=rec.participant.id,
            peer_id=self.peers.for_participant(rec.participant).id
        ))
        return rec

    def update_recordings(self, recordings, participant):
        from recordings.models import Recording
        from recordings.serializers import RecordingSerializer
        existing = {
            r.id: r
            for r in self.recordings.filter(participant=participant)
        }
        for recording_data in recordings:
            rec = existing.get(recording_data['id'])
            if rec is not None:
                for field, val in recording_data.items():
                    if field not in ('room_id', 'participant_id'):
                        setattr(rec, field, val)
                rec.save()
            else:
                rec = Recording(**recording_data)
                rec.room = self
                rec.participant = participant
                rec.save()

            self.send(self.message(
                type=Message.TYPE.event,
                payload={
                    'type': 'update_recording',
                    'data': RecordingSerializer(rec).data,
                },
                participant_id=rec.participant_id,
                peer_id=self.peers.for_participant(rec.participant).id
            ))

    def add_message(self, message):
        if message.id is not None:
            raise ValueError("This message was already added.")
        message.room = self
        message.save()

    def should_save_message(self, message):
        if message.type in (Message.TYPE.leave, Message.TYPE.announce):
            return True
        if message.type in (
            Message.TYPE.join,
            Message.TYPE.signalling,
            Message.TYPE.action
        ):
            return False
        if message.type == Message.TYPE.event:
            event_type = message.payload['type']
            if event_type in (
                'update_recording',
                'update_meter',
                'update_upload_progress',
            ):
                return False
            else:
                if event_type == 'update_status':
                    if 'disk_usage' in message.payload['data']:
                        return False
                    if 'resources' in message.payload['data']:
                        return False
            return True

    def get_socket_url(self):
        return self.get_absolute_url() + "socket"

    def get_full_socket_url(self):
        return "wss://" + settings.HOSTNAME + self.get_socket_url()

    def get_absolute_url(self):
        return reverse('rooms:room', kwargs={'room_id': self.id})

    def get_join_url(self):
        return reverse('rooms:join', kwargs={'room_id': self.id})

    def get_messages_url(self):
        return reverse('rooms:messages', kwargs={'room_id': self.id})

    def get_recordings_url(self):
        return reverse('rooms:recordings', kwargs={'room_id': self.id}) class RoomMembership(models.Model):
    ROLE = Choices(
        ('g', 'guest', 'Guest'),
        ('o', 'owner', 'Owner'),
    )
    STATUS = Choices(
        (0, 'disconnected', 'Disconnected'),
        (1, 'connected', 'Connected'),
    )
    participant = models.ForeignKey('Participant', related_name='memberships')
    room = models.ForeignKey('Room', related_name='memberships')
    name = models.CharField(max_length=64, blank=True, null=True)
    role = models.CharField(max_length=1, choices=ROLE, default=ROLE.guest)
    joined = models.DateTimeField(auto_now_add=True)
    onboarding_complete = models.BooleanField(default=False)

    class Meta:
        unique_together = ('room', 'participant')

    @property
    def status(self):
        if self.peer_id:
            return self.STATUS.connected
        else:
            return self.STATUS.disconnected

    @property
    def peer_id(self):
        if hasattr(self, '_peer_id'):
            return self._peer_id
        else:
            peer = self.room.peers.for_participant(self.participant)
            return peer.id if peer else None

    @property
    def current_recording_id(self):
        return (
            self.recordings.latest().values_list('id', flat=True) or
            [None]
        )[0]

    @property
    def current_recording(self):
        return self.recordings.latest()

    @property
    def disk_usage(self):
        if self.peer_id:
            return self.room.peers[self.peer_id]['disk_usage']
        else:
            return None

    @property
    def resources(self):
        if self.peer_id:
            return self.room.peers[self.peer_id]['resources']
        else:
            return None

    @property
    def recorder_status(self):
        if self.peer_id:
            return self.room.peers[self.peer_id]['recorder_status']
        else:
            return None

    @property
    def recordings(self):
        return self.participant.recordings.all().filter(room=self.room)

    def get_display_name(self):
        if self.name is not None:
            return self.name
        else:
            return self.participant.get_display_name() class MembershipSerializer(serializers.Serializer):
    peer_id = serializers.CharField()
    uid = serializers.IntegerField(source='participant_id')
    info = PeerInfoSerializer(source='*')
    status = serializers.IntegerField()
    onboarding_complete = serializers.BooleanField() class MessageSerializer(serializers.ModelSerializer):
    timestamp = TimestampField(read_only=True)
    uid = serializers.IntegerField(source='participant_id', required=False)

    def validate_type(self, value):
        if value != models.Message.TYPE.event:
            raise serializers.ValidationError('You can only send event messages.')
        return value

    class Meta:
        model = models.Message
        fields = ('id', 'uid', 'type', 'payload', 'timestamp', 'peer_id') class PeerActionSerializer(serializers.Serializer):
    peer_id = serializers.UUIDField(format='hex')

    def validate_peer_id(self, value):
        if value.hex not in self.context['room'].peers.ids:
            raise serializers.ValidationError('Peer is not connected.')
        return value class JoinRoomSerializer(serializers.Serializer):
    name = serializers.CharField(max_length=64, default=None) class RoomConfigSerializer(serializers.Serializer):
    UPLOAD_MODE_CHOICES = (
        ['p2p'] +
        (['http'] if settings.FIRESIDE_HTTP_UPLOAD_ENABLED else [])
    )
    DEFAULT_UPLOAD_MODE = 'http' if 'http' in UPLOAD_MODE_CHOICES else 'p2p'
    mode = serializers.ChoiceField(choices=['audio', 'video'], default='audio')
    debug_mode = serializers.BooleanField(default=False)
    video_bitrate = serializers.IntegerField(allow_null=True, default=None)
    upload_mode = serializers.ChoiceField(
        choices=UPLOAD_MODE_CHOICES,
        default=DEFAULT_UPLOAD_MODE
    )
    upload_mode_choices = serializers.ListField(
        default=UPLOAD_MODE_CHOICES,
        read_only=True) class EditNameSerializer(serializers.Serializer):
    name = serializers.CharField(max_length=64, default=None)
from datetime import datetime, timezone
from django.views.generic import DetailView, View
from django.http import HttpResponseRedirect
from django.shortcuts import get_object_or_404
from django.db import IntegrityError
from rest_framework import status
from rest_framework.response import Response
from rest_framework.views import APIView
from rest_framework.generics import ListCreateAPIView, ListAPIView
from rest_framework.permissions import BasePermission
from .models import Room, Participant, RoomMembership, Message
from .serializers import (
    MembershipSerializer,
    MessageSerializer,
    PeerActionSerializer,
    JoinRoomSerializer,
    RoomConfigSerializer,
    EditNameSerializer
)
from recordings.serializers import RecordingSerializer
import json
        return HttpResponseRedirect(room.get_absolute_url())


class JoinRoomView(APIView):
    def post(self, request, room_id):
        room = get_object_or_404(Room, id=room_id)
        participant = Participant.objects.from_request(request, create=True)
        serializer = JoinRoomSerializer(data=request.data,
            context={'participant': participant}
        )
        if serializer.is_valid():
            try:
                room.memberships.create(
                    participant=participant,
                    name=serializer.validated_data['name'],
                    role='o' if participant == room.owner else 'g',
                )
            except IntegrityError:
                return Response(
                    data={'error': 'Already joined!'},
                    status=status.HTTP_400_BAD_REQUEST
                )
        else:
            return Response(
                data=serializer.errors,
                status=status.HTTP_400_BAD_REQUEST
            )
        return Response({'uid': participant.id}, status=status.HTTP_200_OK)


class ChangeNameView(APIView):
    permission_classes = (HasRoomAccess,)

    def post(self, request, room_id, participant_id):
        participant = get_object_or_404(Participant, id=participant_id)
        if (
            participant != request.participant and not
            request.room.is_admin(request.participant)
        ):
            return Response(
                data="Must be admin to change other member's name",
                status=status.HTTP_403_FORBIDDEN
            )

        serializer = EditNameSerializer(data=request.data,
            context={'participant': participant}
        )
        if serializer.is_valid():
            request.room.change_member_name(participant, serializer.validated_data['name'])
        else:
            return Response(
                data=serializer.errors,
                status=status.HTTP_400_BAD_REQUEST
            )
        return Response({}, status=status.HTTP_200_OK)

class RoomMessagesView(ListAPIView):
    permission_classes = (HasRoomAccess,)
    serializer_class = MessageSerializer

    def post(self, request, **kwargs):
        serializer = self.serializer_class(data=request.data)
        if serializer.is_valid():
            message = Message(**serializer.validated_data)
            message.participant = request.participant
            message.peer_id = request.room.peers.for_participant(request.participant).id
            print(message.__dict__)
            request.room.receive_event(message)
            return Response({
                'id': message.id,
                'timestamp': message.timestamp.timestamp() * 1000
            }, status=status.HTTP_200_OK)
        else:
            return Response(
                data=serializer.errors,
                status=status.HTTP_400_BAD_REQUEST
            )

    def get_queryset(self):
        qs = self.request.room.messages.all()
        if 'until' in self.request.query_params:
            qs = qs.filter(
                timestamp__lt=datetime.fromtimestamp(
                    int(self.request.query_params['until']) / 1000,
                    timezone.utc,
                ),
            )
        return qs


class RoomRecordingsView(ListCreateAPIView):
    permission_classes = (HasRoomAccess,)
    serializer_class = RecordingSerializer

    def get_queryset(self):
        return self.request.room.recordings.all()

    def perform_create(self, serializer):
        self.request.room.create_recording(**serializer.validated_data)

    def put(self, request, **kwargs):
        serializer = RecordingSerializer(data=request.data, many=True)
        if serializer.is_valid():
            self.request.room.update_recordings(
                serializer.validated_data,
                participant=request.participant
            )
            return Response(data="OK", status=status.HTTP_200_OK)
        else:
            return Response(
                data=serializer.errors,
                status=status.HTTP_400_BAD_REQUEST
            )


class RoomParticipantsView(ListAPIView):
    permission_classes = (HasRoomAccess,)
    serializer_class = MembershipSerializer

    def get_queryset(self):

        return RoomMembership.objects.filter(room_id=self.kwargs['room_id'])