diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..7a74596 --- /dev/null +++ b/.gitignore @@ -0,0 +1,30 @@ +# Build related +*.pyc +*.egg +*.[oa] +pip-log.txt +docs/.build +docs/_build +build/ +log/ +src/ +tmp/ +db/ + +# Backup files +*~.nib +.*.swp +*~ +*.tmp +*.bak +.metadata +Thumbs.db +Desktop.ini + +# Other repositories +.hg +.svn +CVS + +# Mac OS X Finder and whatnot +.DS_Store diff --git a/bbb_django/__init__.py b/bbb_django/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/bbb_django/bbb/__init__.py b/bbb_django/bbb/__init__.py new file mode 100755 index 0000000..e69de29 diff --git a/bbb_django/bbb/models.py b/bbb_django/bbb/models.py new file mode 100755 index 0000000..b0e51ea --- /dev/null +++ b/bbb_django/bbb/models.py @@ -0,0 +1,170 @@ +from django.db import models +from django import forms +from django.conf import settings +from django.core.urlresolvers import reverse + +from urllib2 import urlopen +from urllib import urlencode +from hashlib import sha1 +import xml.etree.ElementTree as ET +import random + +def parse(response): + try: + xml = ET.XML(response) + code = xml.find('returncode').text + if code == 'SUCCESS': + return xml + else: + raise + except: + return None + +class Meeting(models.Model): + + name = models.CharField(max_length=100, unique=True) + meeting_id = models.CharField(max_length=100, unique=True) + attendee_password = models.CharField(max_length=50) + moderator_password = models.CharField(max_length=50) + + @classmethod + def api_call(self, query, call): + prepared = "%s%s%s" % (call, query, settings.SALT) + checksum = sha1(prepared).hexdigest() + result = "%s&checksum=%s" % (query, checksum) + return result + + def is_running(self): + call = 'isMeetingRunning' + query = urlencode(( + ('meetingID', self.meeting_id), + )) + hashed = self.api_call(query, call) + url = settings.BBB_API_URL + call + '?' + hashed + result = parse(urlopen(url).read()) + if result: + return result.find('running').text + else: + return 'error' + + @classmethod + def end_meeting(self, meeting_id, password): + call = 'end' + query = urlencode(( + ('meetingID', meeting_id), + ('password', password), + )) + hashed = self.api_call(query, call) + url = settings.BBB_API_URL + call + '?' + hashed + result = parse(urlopen(url).read()) + if result: + pass + else: + return 'error' + + @classmethod + def meeting_info(self, meeting_id, password): + call = 'getMeetingInfo' + query = urlencode(( + ('meetingID', meeting_id), + ('password', password), + )) + hashed = self.api_call(query, call) + url = settings.BBB_API_URL + call + '?' + hashed + r = parse(urlopen(url).read()) + if r: + # Create dict of values for easy use in template + d = { + 'start_time': r.find('startTime').text, + 'end_time': r.find('endTime').text, + 'participant_count': r.find('participantCount').text, + 'moderator_count': r.find('moderatorCount').text, + 'moderator_pw': r.find('moderatorPW').text, + 'attendee_pw': r.find('attendeePW').text, + 'invite_url': reverse('join', args=[meeting_id]), + } + return d + else: + return None + + @classmethod + def get_meetings(self): + call = 'getMeetings' + query = urlencode(( + ('random', 'random'), + )) + hashed = self.api_call(query, call) + url = settings.BBB_API_URL + call + '?' + hashed + result = parse(urlopen(url).read()) + if result: + # Create dict of values for easy use in template + d = [] + r = result[1].findall('meeting') + for m in r: + meeting_id = m.find('meetingID').text + password = m.find('moderatorPW').text + d.append({ + 'name': meeting_id, + 'running': m.find('running').text, + 'moderator_pw': password, + 'attendee_pw': m.find('attendeePW').text, + 'info': Meeting.meeting_info( + meeting_id, + password) + }) + return d + else: + return 'error' + + def start(self): + call = 'create' + voicebridge = 70000 + random.randint(0,9999) + query = urlencode(( + ('name', self.name), + ('meetingID', self.meeting_id), + ('attendeePW', self.attendee_password), + ('moderatorPW', self.moderator_password), + ('voiceBridge', voicebridge), + ('welcome', "Welcome!"), + )) + hashed = self.api_call(query, call) + url = settings.BBB_API_URL + call + '?' + hashed + result = parse(urlopen(url).read()) + if result: + return result + else: + raise + + @classmethod + def join_url(self, meeting_id, name, password): + call = 'join' + query = urlencode(( + ('fullName', name), + ('meetingID', meeting_id), + ('password', password), + )) + hashed = self.api_call(query, call) + url = settings.BBB_API_URL + call + '?' + hashed + return url + + class CreateForm(forms.Form): + name = forms.SlugField() + attendee_password = forms.CharField( + widget=forms.PasswordInput(render_value=False)) + moderator_password= forms.CharField( + widget=forms.PasswordInput(render_value=False)) + + def clean(self): + data = self.cleaned_data + + # TODO: should check for errors before modifying + data['meeting_id'] = data.get('name') + + if Meeting.objects.filter(name = data.get('name')): + raise forms.ValidationError("That meeting name is already in use") + return data + + class JoinForm(forms.Form): + name = forms.CharField(label="Your name") + password = forms.CharField( + widget=forms.PasswordInput(render_value=False)) diff --git a/bbb_django/bbb/static/logo.png b/bbb_django/bbb/static/logo.png new file mode 100644 index 0000000..a7873a5 Binary files /dev/null and b/bbb_django/bbb/static/logo.png differ diff --git a/bbb_django/bbb/static/reset.css b/bbb_django/bbb/static/reset.css new file mode 100644 index 0000000..edee770 --- /dev/null +++ b/bbb_django/bbb/static/reset.css @@ -0,0 +1,7 @@ +/* +Copyright (c) 2010, Yahoo! Inc. All rights reserved. +Code licensed under the BSD License: +http://developer.yahoo.com/yui/license.html +version: 2.8.2r1 +*/ +html{color:#000;background:#FFF;}body,div,dl,dt,dd,ul,ol,li,h1,h2,h3,h4,h5,h6,pre,code,form,fieldset,legend,input,button,textarea,p,blockquote,th,td{margin:0;padding:0;}table{border-collapse:collapse;border-spacing:0;}fieldset,img{border:0;}address,caption,cite,code,dfn,em,strong,th,var,optgroup{font-style:inherit;font-weight:inherit;}del,ins{text-decoration:none;}li{list-style:none;}caption,th{text-align:left;}h1,h2,h3,h4,h5,h6{font-size:100%;font-weight:normal;}q:before,q:after{content:'';}abbr,acronym{border:0;font-variant:normal;}sup{vertical-align:baseline;}sub{vertical-align:baseline;}legend{color:#000;}input,button,textarea,select,optgroup,option{font-family:inherit;font-size:inherit;font-style:inherit;font-weight:inherit;}input,button,textarea,select{*font-size:100%;} \ No newline at end of file diff --git a/bbb_django/bbb/templates/base.html b/bbb_django/bbb/templates/base.html new file mode 100644 index 0000000..0ecf107 --- /dev/null +++ b/bbb_django/bbb/templates/base.html @@ -0,0 +1,241 @@ + + + + {% block title %}{% endblock %} + {% block extrahead %}{% endblock%} + + + + + + +
+ +
+

BBB Django

+ + +
+ + +
+ {% if messages %} + + {% endif %} + {% block content %}{% endblock %} +
+
+ + + diff --git a/bbb_django/bbb/templates/begin.html b/bbb_django/bbb/templates/begin.html new file mode 100644 index 0000000..ab14961 --- /dev/null +++ b/bbb_django/bbb/templates/begin.html @@ -0,0 +1,14 @@ +{% extends "base.html" %} + +{% block title %}Begin meeting{% endblock %} + +{% block content %} +

Begin Meeting

+< meeting invite url here > + +
+{% csrf_token %} + +
+ +{% endblock %} diff --git a/bbb_django/bbb/templates/create.html b/bbb_django/bbb/templates/create.html new file mode 100644 index 0000000..e93aa3f --- /dev/null +++ b/bbb_django/bbb/templates/create.html @@ -0,0 +1,17 @@ +{% extends "base.html" %} + +{% block title %}Create a meeting{% endblock %} + +{% block content %} +

Create Meeting

+
+ + {% csrf_token %} + {{form}} + + + + +
+
+{% endblock %} diff --git a/bbb_django/bbb/templates/home.html b/bbb_django/bbb/templates/home.html new file mode 100644 index 0000000..d33a7c7 --- /dev/null +++ b/bbb_django/bbb/templates/home.html @@ -0,0 +1,14 @@ +{% extends "base.html" %} + +{% block title %}Home{% endblock %} + +{% block content %} +{% if user.is_authenticated %} + +{% else %} +

Welcome to the Django Big Blue Button web conferencing server. If you are here to attend a webinar then you should have been issued a URL to log into the meeting.

+{% endif %} +{% endblock %} diff --git a/bbb_django/bbb/templates/join.html b/bbb_django/bbb/templates/join.html new file mode 100644 index 0000000..ea8715c --- /dev/null +++ b/bbb_django/bbb/templates/join.html @@ -0,0 +1,20 @@ +{% extends "base.html" %} + +{% block title %}Join meeting{% endblock %} + +{% block content %} +
+{% csrf_token %} +

Join meeting '{{ meeting_name }}'

+

Invite others to this meeting with this URL

+ + {{form}} + + + + +
+ +
+
+{% endblock %} diff --git a/bbb_django/bbb/templates/login.html b/bbb_django/bbb/templates/login.html new file mode 100644 index 0000000..64ea333 --- /dev/null +++ b/bbb_django/bbb/templates/login.html @@ -0,0 +1,20 @@ +{% extends "base.html" %} + +{% block title %}Log In{% endblock %} + +{% block content %} +
+{% csrf_token %} + +

Log In

+ {{form}} + + + + +
+ + +
+
+{% endblock %} diff --git a/bbb_django/bbb/templates/meetings.html b/bbb_django/bbb/templates/meetings.html new file mode 100644 index 0000000..e628c07 --- /dev/null +++ b/bbb_django/bbb/templates/meetings.html @@ -0,0 +1,37 @@ +{% extends "base.html" %} + +{% block title %}Meetings{% endblock %} + +{% block content %} +

Meetings

+{% if meetings %} + +{% else %} +

There are no meetings at the moment.

+{% endif %} +{% endblock %} diff --git a/bbb_django/bbb/tests.py b/bbb_django/bbb/tests.py new file mode 100755 index 0000000..2247054 --- /dev/null +++ b/bbb_django/bbb/tests.py @@ -0,0 +1,23 @@ +""" +This file demonstrates two different styles of tests (one doctest and one +unittest). These will both pass when you run "manage.py test". + +Replace these with more appropriate tests for your application. +""" + +from django.test import TestCase + +class SimpleTest(TestCase): + def test_basic_addition(self): + """ + Tests that 1 + 1 always equals 2. + """ + self.failUnlessEqual(1 + 1, 2) + +__test__ = {"doctest": """ +Another way to test that 1 + 1 is equal to 2. + +>>> 1 + 1 == 2 +True +"""} + diff --git a/bbb_django/bbb/urls.py b/bbb_django/bbb/urls.py new file mode 100644 index 0000000..e62c1b2 --- /dev/null +++ b/bbb_django/bbb/urls.py @@ -0,0 +1,26 @@ +from django.conf.urls.defaults import * +from bbb.views.core import (home_page, create_meeting, begin_meeting, meetings, + join_meeting, delete_meeting) + +# Uncomment the next two lines to enable the admin: +# from django.contrib import admin +# admin.autodiscover() + +urlpatterns = patterns('', + url('^$', home_page, name='home'), + url(r'^login/$', 'django.contrib.auth.views.login', { + 'template_name': 'login.html', + }, name='login'), + url(r'^logoff/$', 'django.contrib.auth.views.logout', {'next_page': '/'}, + name='logout'), + url('^create/$', create_meeting, name='create'), + url('^begin/$', begin_meeting, name='begin'), + url('^meetings/$', meetings, name='meetings'), + url('^meeting/(?P[a-zA-Z0-9 _-]+)/join$', join_meeting, + name='join'), + url('^meeting/(?P[a-zA-Z0-9 _-]+)/(?P.*)/delete$', delete_meeting, + name='delete'), + url('^help.html$', 'django.views.generic.simple.redirect_to', { + 'url': 'http://www.bigbluebutton.org/content/videos' , + }, name='help'), +) diff --git a/bbb_django/bbb/views/__init__.py b/bbb_django/bbb/views/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/bbb_django/bbb/views/core.py b/bbb_django/bbb/views/core.py new file mode 100644 index 0000000..b7b4588 --- /dev/null +++ b/bbb_django/bbb/views/core.py @@ -0,0 +1,114 @@ +from django.http import (Http404, HttpResponseRedirect, HttpResponseNotFound, + HttpResponse) +from django.shortcuts import render_to_response +from django.contrib.auth.decorators import login_required, permission_required +from django.contrib.auth.views import login as django_login +from django.template import RequestContext +from django.core.urlresolvers import reverse +from django.conf import settings +from django.contrib import messages +import hashlib + +from bbb.models import Meeting + +def home_page(request): + context = RequestContext(request, { + }) + return render_to_response('home.html', context) + + +@login_required +def begin_meeting(request): + + if request.method == "POST": + begin_url = "http://bigbluebutton.org" + return HttpResponseRedirect(begin_url) + + context = RequestContext(request, { + }) + + return render_to_response('begin.html', context) + +@login_required +def meetings(request): + + #meetings = Meeting.objects.all() + meetings = Meeting.get_meetings() + + context = RequestContext(request, { + 'meetings': meetings, + }) + + return render_to_response('meetings.html', context) + +def join_meeting(request, meeting_id): + form_class = Meeting.JoinForm + + if request.method == "POST": + # Get post data from form + form = form_class(request.POST) + if form.is_valid(): + data = form.cleaned_data + name = data.get('name') + password = data.get('password') + + return HttpResponseRedirect(Meeting.join_url(meeting_id, name, password)) + else: + form = form_class() + + context = RequestContext(request, { + 'form': form, + 'meeting_name': meeting_id, + }) + + return render_to_response('join.html', context) + +@login_required +def delete_meeting(request, meeting_id, password): + if request.method == "POST": + #meeting = Meeting.objects.filter(meeting_id=meeting_id) + #meeting.delete() + Meeting.end_meeting(meeting_id, password) + + msg = 'Successfully ended meeting %s' % meeting_id + messages.success(request, msg) + return HttpResponseRedirect(reverse('meetings')) + else: + msg = 'Unable to end meeting %s' % meeting_id + messages.error(request, msg) + return HttpResponseRedirect(reverse('meetings')) + +@login_required +def create_meeting(request): + form_class = Meeting.CreateForm + + if request.method == "POST": + # Get post data from form + form = form_class(request.POST) + if form.is_valid(): + data = form.cleaned_data + meeting = Meeting() + meeting.name = data.get('name') + #password = hashlib.sha1(data.get('password')).hexdigest() + meeting.attendee_password = data.get('attendee_password') + meeting.moderator_password = data.get('moderator_password') + meeting.meeting_id = data.get('meeting_id') + try: + url = meeting.start() + meeting.save() + msg = 'Successfully created meeting %s' % meeting.meeting_id + messages.success(request, msg) + return HttpResponseRedirect(reverse('meetings')) + except: + return HttpResponse("An error occureed whilst creating the " \ + "meeting. The meeting has probably been " + "deleted recently but is still running.") + + else: + form = form_class() + + context = RequestContext(request, { + 'form': form, + }) + + return render_to_response('create.html', context) diff --git a/bbb_django/manage.py b/bbb_django/manage.py new file mode 100755 index 0000000..5e78ea9 --- /dev/null +++ b/bbb_django/manage.py @@ -0,0 +1,11 @@ +#!/usr/bin/env python +from django.core.management import execute_manager +try: + import settings # Assumed to be in the same directory. +except ImportError: + import sys + sys.stderr.write("Error: Can't find the file 'settings.py' in the directory containing %r. It appears you've customized things.\nYou'll have to run django-admin.py, passing it your settings module.\n(If the file settings.py does indeed exist, it's causing an ImportError somehow.)\n" % __file__) + sys.exit(1) + +if __name__ == "__main__": + execute_manager(settings) diff --git a/bbb_django/settings.py b/bbb_django/settings.py new file mode 100644 index 0000000..e4974bd --- /dev/null +++ b/bbb_django/settings.py @@ -0,0 +1,112 @@ +# Django settings for bbb project. +import os + +DEBUG = True +TEMPLATE_DEBUG = DEBUG + +ADMINS = ( +) + +MANAGERS = ADMINS + +DATABASES = { + 'default': { + 'ENGINE': 'django.db.backends.mysql', # Add 'postgresql_psycopg2', 'postgresql', 'mysql', 'sqlite3' or 'oracle'. + 'NAME': 'bbb_django', # Or path to database file if using sqlite3. + 'USER': 'bbb_django', # Not used with sqlite3. + 'PASSWORD': '', # Not used with sqlite3. + 'HOST': '', # Set to empty string for localhost. Not used with sqlite3. + 'PORT': '', # Set to empty string for default. Not used with sqlite3. + } +} + +# Local time zone for this installation. Choices can be found here: +# http://en.wikipedia.org/wiki/List_of_tz_zones_by_name +# although not all choices may be available on all operating systems. +# On Unix systems, a value of None will cause Django to use the same +# timezone as the operating system. +# If running in a Windows environment this must be set to the same as your +# system time zone. +TIME_ZONE = 'Europe/London' + +# Language code for this installation. All choices can be found here: +# http://www.i18nguy.com/unicode/language-identifiers.html +LANGUAGE_CODE = 'en-gb' + +SITE_ID = 1 + +# If you set this to False, Django will make some optimizations so as not +# to load the internationalization machinery. +USE_I18N = True + +# If you set this to False, Django will not format dates, numbers and +# calendars according to the current locale +USE_L10N = True + +# Absolute path to the directory that holds media. +# Example: "/home/media/media.lawrence.com/" +MEDIA_ROOT = '' + +# URL that handles the media served from MEDIA_ROOT. Make sure to use a +# trailing slash if there is a path component (optional in other cases). +# Examples: "http://media.lawrence.com", "http://example.com/media/" +MEDIA_URL = '' + +# URL prefix for admin media -- CSS, JavaScript and images. Make sure to use a +# trailing slash. +# Examples: "http://foo.com/media/", "/media/". +ADMIN_MEDIA_PREFIX = '/media/' + +# Make this unique, and don't share it with anybody. +SECRET_KEY = '' + +# List of callables that know how to import templates from various sources. +TEMPLATE_LOADERS = ( + 'django.template.loaders.filesystem.Loader', + 'django.template.loaders.app_directories.Loader', +) + +MIDDLEWARE_CLASSES = ( + 'django.middleware.common.CommonMiddleware', + 'django.contrib.sessions.middleware.SessionMiddleware', + 'django.middleware.csrf.CsrfViewMiddleware', + 'django.contrib.auth.middleware.AuthenticationMiddleware', + 'django.contrib.messages.middleware.MessageMiddleware', +) + +AUTHENTICATION_BACKENDS = ( + 'django_auth_ldap.backend.LDAPBackend', + 'django.contrib.auth.backends.ModelBackend', +) + +LOGIN_URL = '/login' +LOGIN_REDIRECT_URL = '/' + +SALT = "" +BBB_API_URL = "http://yourdomain.com/bigbluebutton/api/" + +ROOT_URLCONF = 'bbb.urls' + +PROJECT_PATH = os.path.abspath(os.path.dirname(__file__)) + +TEMPLATE_DIRS = ( + # Put strings here, like "/home/html/django_templates" or "C:/www/django/templates". + # Always use forward slashes, even on Windows. + # Don't forget to use absolute paths, not relative paths. + os.path.join(PROJECT_PATH, 'templates'), + os.path.join(PROJECT_PATH, 'bbb', 'templates'), +) + +INSTALLED_APPS = ( + 'django.contrib.auth', + 'django.contrib.contenttypes', + 'django.contrib.sessions', + 'django.contrib.sites', + 'django.contrib.messages', + 'bbb', + 'gunicorn', + # Uncomment the next line to enable the admin: + # 'django.contrib.admin', + # Uncomment the next line to enable admin documentation: + # 'django.contrib.admindocs', +) diff --git a/bbb_django/urls.py b/bbb_django/urls.py new file mode 100644 index 0000000..94c5ce4 --- /dev/null +++ b/bbb_django/urls.py @@ -0,0 +1,5 @@ +from django.conf.urls.defaults import * + +urlpatterns = patterns('', + (r'^/', include('bbb.urls')), +) diff --git a/readme.rst b/readme.rst new file mode 100644 index 0000000..ea8d853 --- /dev/null +++ b/readme.rst @@ -0,0 +1,42 @@ +==================== +django-bigbluebutton +==================== +:Info: A Django project for interacting with BigBlueButton +:Author: Steve Challis (http://schallis.com) +:Requirements: BigBlueButton >= 0.71, Django >= 1.0 + +This is a simple Django project and application that interacts with the +`BigBlueButton `_ API to allow you to create and +interact with web conference meetings It currently supports: + +* Password protected administration +* Meeting creation/ending +* Meeting joining +* List all currently running meetings + + +Setup +===== +You'll first need to edit settings.py in the bbb_django project or your own +project. The following custom variables must be added/set: + +* SALT = "[your_salt]" +* BBB_API_URL = "http://yourdomain.com/bigbluebutton/api/" + +The `bbb` application is where all the controllers and views are contained so +you should be able to drop this into any Django project. + +You can quickly test the project with the Django default webserver but you'll +probably want to have it running permenantly. `Gunicorn +`_ has already been added in as a dependancy so +you should be able to use `gunicorn_django` once gunicorn is installed. + +It is assumed you are using FreeSWITCH for the voice calling but it is easy +enough to change the extension to that required by Asterisk. + +Screenshots +=========== +.. image:: +https://github.com/schallis/django-bigbluebutton/raw/master/screenshots/screenshot-create.png + +.. image:: https://github.com/schallis/django-bigbluebutton/raw/master/screenshots/screenshot-meetings.png diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..514e259 --- /dev/null +++ b/requirements.txt @@ -0,0 +1 @@ +django >= 1.0 diff --git a/screenshots/screenshot-create.png b/screenshots/screenshot-create.png new file mode 100644 index 0000000..fbbcb5d Binary files /dev/null and b/screenshots/screenshot-create.png differ diff --git a/screenshots/screenshot-home.png b/screenshots/screenshot-home.png new file mode 100644 index 0000000..7c50d12 Binary files /dev/null and b/screenshots/screenshot-home.png differ diff --git a/screenshots/screenshot-join.png b/screenshots/screenshot-join.png new file mode 100644 index 0000000..c6b35a8 Binary files /dev/null and b/screenshots/screenshot-join.png differ diff --git a/screenshots/screenshot-meetings-detail.png b/screenshots/screenshot-meetings-detail.png new file mode 100644 index 0000000..71b0aea Binary files /dev/null and b/screenshots/screenshot-meetings-detail.png differ diff --git a/screenshots/screenshot-meetings.png b/screenshots/screenshot-meetings.png new file mode 100644 index 0000000..e224b42 Binary files /dev/null and b/screenshots/screenshot-meetings.png differ