Source

django-live / live / views.py

#!/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
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# 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 <http://www.gnu.org/licenses/>.

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

def _reconnect_stomp(stomp_connection):
    try:
        stomp_connection.start()
        stomp_connection.connect()
    ## orbited is not running or not responding properly
    except stomp.exception.ReconnectFailedException:
        print "Connection failed"

global stomp_connection
stomp_connection = stomp.Connection([('localhost', 61613)],
                                    reconnect_sleep_initial=1,
                                    reconnect_sleep_increase=0,
                                    reconnect_attempts_max=1)

## we set up a stomp connection for our Django process so we can send messages to subscribers.
_reconnect_stomp(stomp_connection)


# 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(pk=user.pk).exists()
    elif not channel.allow_guests:
        if user.is_authenticated():
            allowed = True
    else:
        allowed = True

    return allowed


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

    global stomp_connection
    ## if the django to stomp connection is for some reason down
    ## we try to reconnect
    if not stomp_connection.is_connected():
        _reconnect_stomp(stomp_connection)

    ## 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,
                                               slug=channel_slug)
    else:
        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:
            req_context.update(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:
        req_context.update(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):
    try:
        channel = Channel.objects.get(slug=channel_slug)
    except:
        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)
    else:
        channel = Channel(name=channel_slug)
        channel.save()
    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)
    else:
        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'
        else:
            body['message']['payload'] = 'registered'

    new_participant.save()
    channel.participants.add(new_participant)
    ## we return our modified message body
    return json.dumps({'allow': 'yes', 'body': json.dumps(body)})


def _leave(participant, channel):
    global stomp_connection
    if not stomp_connection.is_connected():
        _reconnect_stomp(stomp_connection)
    try:
        stomp_connection.send('{"message":{"action": "leave"}, "nickname": "%s"}' \
                                  % participant.name, destination=channel.slug)
    except: ## capture the correct exception here...
        pass

    channel.participants.remove(participant)
    participant.delete()
    if channel.participants.count() == 0 and not channel.persistant:
        channel.delete()


@login_required()
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)


@login_required()
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,
                                   })
    else:
        return HttpResponse(_('Not allowed'))


def one_on_one(request, role, channel_name):
    names = channel_name.split('..')[1:]
    if role == 'host':
        nickname = names[0]
    else:
        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((participant.name, 'guest'))
            else:
                if user.is_superuser:
                    json_data.append((participant.name, 'superuser'))
                elif user.is_staff:
                    json_data.append((participant.name, 'staff'))
                else:
                    json_data.append((participant.name, 'registered'))
    return json_response(json.dumps(json_data))


@csrf_exempt
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]
        try:
            session = Session.objects.get(session_key=session_key)
        except:
            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)})
                else:
                    participant.name = new_name
                    participant.save()

            ## TODO(nicoechaniz): this is an expensive validation to run on every message send, refactor.
            elif action == 'say':
                channel = _get_channel(destination)
                try:
                    participant = channel.participants.get(session=session)
                except:
                    print "no participant for session", session, "on", channel_slug
                    json_data=json.dumps({'allow':'no'})
                else:
                    if participant.name != body['nickname']:
                        ## someone has been fiddling with JS to impersonate a different nickname
                        body['nickname'] = participant.name
                        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)
        try:
            participant = channel.participants.get(session=session)
        except:
            print "no participant for session", session, "on", channel_slug
        else:
            print "disconnect", participant
            _leave(participant, channel)
        json_data = json.dumps({})

    return json_response(json_data)
Tip: Filter by directory path e.g. /media app.js to search for public/media/app.js.
Tip: Use camelCasing e.g. ProjME to search for ProjectModifiedEvent.java.
Tip: Filter by extension type e.g. /repo .js to search for all .js files in the /repo directory.
Tip: Separate your search with spaces e.g. /ssh pom.xml to search for src/ssh/pom.xml.
Tip: Use ↑ and ↓ arrow keys to navigate and return to view the file.
Tip: You can also navigate files with Ctrl+j (next) and Ctrl+k (previous) and view the file with Ctrl+o.
Tip: You can also navigate files with Alt+j (next) and Alt+k (previous) and view the file with Alt+o.