django-live / live /

#!/usr/bin/env python
# -*- coding: utf-8 -*-
# Copyright 2010 Nicolás Echániz
# All rights reserved.
# This file is part of django-live.
# Django-Live is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
# Django-Live is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# GNU General Public License for more details.
# You should have received a copy of the GNU General Public License
# along with this program.  If not, see <>.

import time

from django.conf import settings
from django.http import HttpResponse, HttpResponseNotFound, HttpResponseRedirect
from django.shortcuts import render_to_response, get_object_or_404, render_to_response
from django.template import RequestContext, loader
from django.contrib.auth.decorators import login_required, permission_required
from django.utils import simplejson as json
from django.views.decorators.csrf import csrf_exempt
from django.utils.translation import ugettext as _
from django.contrib.sites.models import get_current_site

from django.contrib.sessions.models import Session

from django.contrib.auth.models import User

import live.settings as s
from models import Participant, Channel

import stomp

## we set up a stomp connection from Django.
## this fails if we do it where it's later used (inside the restq method)
## it's wrapped in a try/except because otherwise the whole site is unusable unless orbited is running
    conn = stomp.Connection([('localhost', 61613)])
##conn.subscribe(destination='PUBLIC', ack='auto')
except: #ReconnectFailedException

# copied from django.templates.defaultfilters
def slugify(value):
    Normalizes string, converts to lowercase, removes non-alpha characters,
    and converts spaces to hyphens.
    import unicodedata, re
    value = unicode(value)
    value = unicodedata.normalize('NFKD', value).encode('ascii', 'ignore')
    value = unicode(re.sub('[^\w\s-]', '', value).strip().lower())
    return re.sub('[-\s]+', '-', value)

def json_response(json_data):
    return HttpResponse(json_data, mimetype='application/json')

def check_allowed(user, channel):
    allowed = False
    if channel.members.exists():
        ## check if the authenticated user is in the members list
        allowed = user.is_authenticated() and channel.members.filter(
    elif not channel.allow_guests:
        if user.is_authenticated():
            allowed = True
        allowed = True

    return allowed

def chat_html(request, channel_name=None, nickname=None,
               manager_channel="MANAGE", extra_context=None):

    ## we will be using the slug and not the name so we slugify in case we received
    ## the name from an arbitrary URL
    channel_slug = slugify(channel_name)

    session = Session.objects.filter(session_key=request.session.session_key)
    if session:
        session = session[0]
        is_subscribed = Channel.objects.filter(participants__session=session,
        is_subscribed = False

    ## if a user was participating in a channel and joins again
    ## we show a page to let him kick himself before re-joining
    if is_subscribed:
        req_context = RequestContext(request, {'channel_slug': channel_slug})
        if extra_context:
        return render_to_response('live/already_participating.html', req_context)

    if not nickname:
        timestamp = str(time.time()).replace('.', '')[5:]
        nickname = 'guest' + timestamp
    channel_session = "%s__%s" % (request.session.session_key, channel_slug)

    ## we make sure the session gets saved
    request.session.modified = True

    req_context = RequestContext(request,
                                 {'ORBITED_HOST': s.ORBITED_HOST,
                                  'ORBITED_PORT': s.ORBITED_PORT,
                                  'STOMP_PORT': s.STOMP_PORT,
                                  'channel_name': channel_name,
                                  'manager_channel': manager_channel,
                                  'nickname': nickname,
                                  'username': channel_session,
                                  'channel_slug': channel_slug,
    if extra_context:
    t = loader.get_template('live/chat.html')
    return t.render(req_context)

def _show_chat(request, channel_name=None, nickname=None,
               manager_channel="MANAGE", extra_context=None):
    channel = _get_channel(slugify(channel_name))
    if channel:
        allowed = check_allowed(request.user, channel)
        if not allowed:
            return HttpResponseRedirect(settings.LOGIN_URL)

    return HttpResponse(chat_html(request, channel_name, nickname,
                                   manager_channel, extra_context))
def _get_channel(channel_slug):
        channel = Channel.objects.get(slug=channel_slug)
        channel = None
    return channel

def _get_or_create_channel(channel_slug):
    if Channel.objects.filter(slug=channel_slug).exists():
        channel = Channel.objects.get(slug=channel_slug)
        channel = Channel(name=channel_slug)
    return channel

def _join(user, destination, session, body):
    channel = _get_or_create_channel(destination)
    nickname = body['nickname']
    ## set payload to the type of user so it can be CSS styled
    if not user:
        body['message']['payload'] = 'guest'
        new_participant = Participant(name=nickname, session=session)
        new_participant = Participant(name=nickname, user=user, session=session)
        if user.is_superuser:
            body['message']['payload'] = 'superuser'
        elif user.is_staff:
            body['message']['payload'] = 'staff'
            body['message']['payload'] = 'registered'
    ## we return our modified message body
    return json.dumps({'allow': 'yes', 'body': json.dumps(body)})

def _leave(participant, channel):
    conn.send('{"message":{"action": "leave"}, "nickname": "%s"}' \
              %, destination=channel.slug)
    if channel.participants.count() == 0 and not channel.persistant:

def chat(request, channel_name=None, nickname=None, manager_channel="MANAGE"):
    if channel_name is None:
        channel_name = str(time.time())
    nickname = request.user.username
    return _show_chat(request, channel_name, nickname, manager_channel)

def kickme(request, channel_slug):
    """"Kick the participant/s corresponding to this session (cleanup)"""

    channel = _get_channel(channel_slug)
    if channel:
        session = Session.objects.get(session_key=request.session.session_key)
        participants = channel.participants.filter(session=session)
        for participant in participants:
            _leave(participant, channel)
    req_context = RequestContext(request, {'channel_slug': channel_slug})
    return render_to_response('live/can_join.html', req_context)

def manage(request):
    ## TODO(nicoechaniz): implement proper permissions
    if request.user.is_superuser:
        return render_to_response('live/manage.html',
                                  {'ORBITED_HOST': s.ORBITED_HOST,
                                   'ORBITED_PORT': s.ORBITED_PORT,
                                   'STOMP_PORT': s.STOMP_PORT,
                                   'STOMP_BROKER': s.STOMP_BROKER.lower(),
                                   'manage_channel': "MANAGE",
                                   'nickname': request.user.username,
                                   'session_key': request.session.session_key,
        return HttpResponse(_('Not allowed'))

def one_on_one(request, role, channel_name):
    names = channel_name.split('..')[1:]
    if role == 'host':
        nickname = names[0]
        nickname = names[1]
    return _show_chat(request, channel_name=channel_name, nickname=nickname)

def public(request, channel_name="PUBLIC"):
    if request.user:
        nickname = request.user.username
    return _show_chat(request, channel_name=channel_name, nickname=nickname)

def channel_participants(request, channel_slug):
    """Return a JSON list of current participants for a given channel"""
    channel = _get_channel(channel_slug)
    json_data = []
    if channel:
        participants = channel.participants.all()
        for participant in participants:
            user = participant.user
            if not user:
                json_data.append((, 'guest'))
                if user.is_superuser:
                    json_data.append((, 'superuser'))
                elif user.is_staff:
                    json_data.append((, 'staff'))
                    json_data.append((, 'registered'))
    return json_response(json.dumps(json_data))

def restq(request, command=None, *args, **kwargs):
    """Morbid's RestQ view"""

    ## Morbid is asking for the command URLs
    if not command:
        site =  get_current_site(request)
        base_url = "http://"+site.domain
        rest_q_url = base_url + s.STOMP_RESTQ_URL
        json_data = json.dumps({'unsubscribe': rest_q_url+'unsubscribe/',
                                'subscribe': rest_q_url+'subscribe/',
                                'disconnect': rest_q_url+'disconnect/',
                                'connect': rest_q_url+'connect/',
                                'send': rest_q_url+'send/'})
        return json_response(json_data)

    incoming_data = json.loads(request.raw_post_data)
    destination = incoming_data.get('destination', None)
    stomp_username = incoming_data.get('username', None)
    ## we "encoded" the session_key and channel_slug in the STOMP username
    ## so here we extract that info in order to determine the real user
    session = None
    user = None
    if stomp_username:
        session_key, channel_slug = stomp_username.split('__')[:2]
            session = Session.objects.get(session_key=session_key)
            print "Invalid session"
            return json_response(json.dumps({'allow': 'no'}))

        uid = session.get_decoded().get('_auth_user_id')

        if uid:
            user = User.objects.get(pk=uid)

   ## only super_users are allowed to use the manage feature
    if command == "subscribe":
        if destination == 'MANAGE':
            if not user or not user.is_superuser:
                return json_response(json.dumps({'allow': 'no'}))

    json_data = json.dumps({'allow': 'yes'})
    if command == "send":
        body = json.loads(incoming_data['body'])
        if 'message' in body:
            action = body['message'].get('action', None)
            ## we only accept messages that contain an action
            if not action:
                json_data = json.dumps({'allow': 'no'})
            elif action == 'join':
                json_data = _join(user, destination, session, body)

            elif action == 'rename':
                channel = _get_channel(destination)
                participant = channel.participants.get(session=session)
                new_name = body['message']['payload']
                invalid_name = channel.participants.filter(name=new_name).exists()
                if not invalid_name:
                    existing_user = User.objects.filter(username=new_name)
                    if existing_user:
                        invalid_name = not user == existing_user[0]
                if invalid_name:
                    ## the name is invalid, so we change the action to 'failed_rename
                    ## which will inform about the invalid change and reset the nickname
                     body['message']['action'] = 'failed_rename'
                     json_data = json.dumps({'allow': 'yes', 'body': json.dumps(body)})
           = new_name

            ## TODO(nicoechaniz): this is an expensive validation to run on every message send, refactor.
            elif action == 'say':
                channel = _get_channel(destination)
                    participant = channel.participants.get(session=session)
                    print "no participant for session", session, "on", channel_slug
                    if != body['nickname']:
                        ## someone has been fiddling with JS to impersonate a different nickname
                        body['nickname'] =
                        json_data = json.dumps({'allow': 'yes', 'body': json.dumps(body)})

    elif command == "disconnect":
        ## Morbid sends no destination data for disconnect command
        ## so we use the channel_name we got earlier
        channel = _get_channel(channel_slug)
            participant = channel.participants.get(session=session)
            print "no participant for session", session, "on", channel_slug
            print "disconnect", participant
            _leave(participant, channel)
        json_data = json.dumps({})

    return json_response(json_data)