Skip to content

Commit 0b2074a

Browse files
author
basti2342
committed
tweaked, traffic light + Mate-O-Meter implemented
1 parent 1773be0 commit 0b2074a

25 files changed

+816
-184
lines changed

README.md

+10-6
Original file line numberDiff line numberDiff line change
@@ -6,18 +6,22 @@ Overview
66
* room status (for SpaceAPI etc.)
77
* extended information about our hackspace
88
* audio announcement system via long polling
9-
* LED ticker via long polling
10-
* Mate-O-Meter (still to come)
9+
* LED ticker via long polling (see [ledticker](https://github.com/hickerspace/ledticker/))
10+
* traffic light via long polling and normal request (see [traffic light](https://hickerspace.org/wiki/Verkehrsampel))
11+
* Mate-O-Meter (measures our Club-Mate stock) (see [Mate-O-Meter](https://hickerspace.org/wiki/Mate-O-Meter))
1112
* MUC (xmpp) status
12-
* Wiki status (just redirects)
13+
* wiki status (just some redirects)
1314

1415
Dependencies
1516
============
1617

1718
* flask
18-
* [ledticker.py](https://github.com/hickerspace/ledticker/blob/master/ledticker.py)
19+
* gevent
20+
* simplejson
21+
* python-xmpp (see /helper)
22+
* bash, espeak, sox, lame (see /data/espeak/espeak.sh)
1923

2024
Notes
2125
=====
22-
* Enable WSGIPassAuthorization to pass through authorisation headers:
23-
http://code.google.com/p/modwsgi/wiki/ConfigurationDirectives#WSGIPassAuthorization
26+
* enable [WSGIPassAuthorization](http://code.google.com/p/modwsgi/wiki/ConfigurationDirectives#WSGIPassAuthorization) to pass through authorisation headers:
27+
* scripts in /helper update the API via http calls and must be called via cron or something similar (calls from other servers are supported)

announce.py

+54-5
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,65 @@
1-
from flask import jsonify
1+
from flask import jsonify, send_from_directory
22
from logger import log
33
import os
4+
import glob
5+
import time
6+
import ledticker, room
7+
from conf import *
8+
from subprocess import Popen, PIPE
9+
from misc import error504
410

11+
"""
12+
Call a bash script which creates a mp3 with espeak content
13+
and moves it into our restapi directory.
14+
"""
515
def announce(lang, text):
6-
if lang in ['de', 'en']:
7-
os.system('bash /var/www/hickerspace.org/espeak/espeak_%s.sh \'%s\'' % (lang, text))
8-
message = {'status': 'Ok'}
16+
if not room.isRoomOpen():
17+
message = { 'success': False, 'status': 'Room is not open. Announcements are forbidden at the moment.' }
18+
resp = jsonify(message)
19+
resp.status_code = 403
20+
elif lang in ['de', 'en']:
21+
espeak = Popen([ESPEAK_LOCATION, lang, text], stdout=PIPE, stderr=PIPE)
22+
returnMsg = espeak.communicate()[0]
23+
if espeak.returncode > 0:
24+
log('espeak returned "%s"' % returnMsg)
25+
message = { 'success': False, 'status': 'Unknown error. Error logged.' }
26+
else:
27+
message = { 'success': True }
28+
929
resp = jsonify(message)
1030
else:
11-
message = {'message': 'Language not found.'}
31+
message = { 'success': False, 'status': 'Language not found.' }
1232
resp = jsonify(message)
1333
resp.status_code = 403
1434

1535
return resp
1636

37+
"""
38+
When the mp3 announcement file is ready, call this method to deliver it.
39+
"""
40+
def serveAnnouncement(announcement):
41+
try:
42+
dlLocation = 'data/downloadable.mp3'
43+
os.rename(announcement, API_PATH+'/'+dlLocation)
44+
return send_from_directory(API_PATH, dlLocation, as_attachment=True)
45+
except OSError:
46+
log.exception('Could not move %s.' % announcement)
47+
message = { 'success': False, 'status': 'Announcement is not ready yet.' }
48+
resp = jsonify(message)
49+
resp.status_code = 403
50+
return resp
51+
52+
"""
53+
Determine oldest announcement and return it.
54+
"""
55+
def serveOldestAnnouncement():
56+
# get mp3 announces sorted by creation time, oldest first
57+
mtime = lambda f: os.stat(f).st_mtime
58+
announces = list(sorted(glob.glob(ANNOUNCE_LOCATION), key=mtime))
59+
60+
if len(announces) > 0:
61+
# return oldest announcement
62+
return serveAnnouncement(announces[0])
63+
else:
64+
return error504()
65+

auth.py

+25-24
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
from functools import wraps
1+
from functools import update_wrapper
22
from flask import request, jsonify
33
from conf import *
44

@@ -17,38 +17,39 @@ def check_auth(username, password):
1717
Source: "HTTP Basic Auth" by Armin Ronacher
1818
http://flask.pocoo.org/snippets/8/
1919
"""
20-
def authenticate(msg='Authenticate.'):
21-
resp = jsonify({'message': msg})
20+
def authenticate(msg='Please authenticate.'):
21+
resp = jsonify({'success': False, 'status': msg})
2222
resp.status_code = 401
2323
resp.headers['WWW-Authenticate'] = 'Basic realm="API credentials needed to access this resource."'
24-
2524
return resp
2625

2726
"""
2827
Basic authentication for protected resources and SSL enforcement.
2928
Source: "HTTP Basic Auth" by Armin Ronacher
3029
http://flask.pocoo.org/snippets/8/
30+
Changed to require ssl for authentication. Extra argument added to require auth only for POST requests.
3131
"""
3232

33-
def requires_auth(f):
34-
@wraps(f)
35-
def decorated(*args, **kwargs):
36-
# require ssl for api requests with auth
37-
if "https://" not in request.url:
38-
message = {'message': 'Resources requiring authentication also require ssl.'}
39-
resp = jsonify(message)
40-
resp.status_code = 403
41-
return resp
42-
43-
auth = request.authorization
44-
if not auth:
45-
return authenticate()
46-
47-
elif not check_auth(auth.username, auth.password):
48-
return authenticate("Authentication Failed.")
49-
50-
return f(*args, **kwargs)
51-
52-
return decorated
33+
def requires_auth(postAuthOnly=False):
34+
def decorator(f):
35+
def wrapped_function(*args, **kwargs):
36+
# require ssl for api requests with auth
37+
if "https://" not in request.url and not (postAuthOnly and request.method != 'POST'):
38+
message = { 'success': False, 'status': 'Resources requiring authentication also require ssl.'}
39+
resp = jsonify(message)
40+
resp.status_code = 403
41+
return resp
42+
43+
if not (request.method != 'POST' and postAuthOnly):
44+
auth = request.authorization
45+
if not auth:
46+
return authenticate()
47+
48+
elif not check_auth(auth.username, auth.password):
49+
return authenticate("Authentication Failed.")
50+
51+
return f(*args, **kwargs)
52+
return update_wrapper(wrapped_function, f)
53+
return decorator
5354

5455

conf.py

+36-7
Original file line numberDiff line numberDiff line change
@@ -1,29 +1,58 @@
1+
import os
2+
3+
# timeout per request in seconds (adjust WSGI/proxy timeout settings)
4+
REQUEST_TIMEOUT = 120
5+
16
# path to REST API files
27
API_PATH = os.path.abspath(os.path.dirname(__file__))
8+
39
# access tuples
410
API_ACCESS = [ ("api-user", "APIKEY") ]
11+
512
# after this number of minutes, the room will be automatically marked as "closed"
613
ROOM_TIMEOUT = 15
14+
715
# weekdays with events
816
EVENT_WEEKDAYS = [3, 4]
17+
918
# begin of meeting
1019
MEET_HOUR_BEGIN = 18
20+
1121
# end of meeting
1222
MEET_HOUR_END = 2
23+
1324
# people needed to open the room on events
1425
EVENT_PEOPLE_LIMIT = 1
26+
1527
# people needed to open the room on normal days
1628
PEOPLE_LIMIT = 3
29+
1730
# file to save room status in
18-
ROOM_STATUS_FILE = API_PATH + "/data/room_status.json"
31+
ROOM_STATUS_FILE = "%s/data/room.json" % API_PATH
32+
33+
# file to save MUC status in
34+
MUC_FILE = "%s/data/muc.json" % API_PATH
35+
36+
# store traffic light info in this file
37+
AMPEL_FILE = "%s/data/ampel.json" % API_PATH
38+
39+
# store mate info in this file
40+
MATE_FILE = "%s/data/mate.json" % API_PATH
41+
42+
# stores ledticker messages
43+
LEDTICKER_FILE = "%s/data/ledticker.txt" % API_PATH
44+
45+
# location of the espeak script
46+
ESPEAK_LOCATION = "%s/data/espeak/espeak.sh" % API_PATH
47+
48+
# temporary announce file location
49+
ANNOUNCE_LOCATION = "%s/data/announces/*.mp3" % API_PATH
50+
1951
# jabber info for status user
2052
JABBER_SERVER = "hickerspace.org"
21-
JABBER_USER = "checkMUCuser"
22-
JABBER_PASSWORD = "password"
53+
JABBER_USER = "JABBERUSER"
54+
JABBER_PASSWORD = "JABBERPASSWORD"
55+
2356
# muc info
2457
JABBER_MUC = "hick"
2558
JABBER_MUC_SERVER = "conference.hickerspace.org"
26-
# temprary announce file location
27-
ANNOUNCE_DL = "downloadable.mp3"
28-
# where to get announcements initally
29-
ESPEAK_FILE = "announce.mp3"

data/ampel.json

+1
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
{"lastUpdate": 1366758579, "green": false, "yellow": false, "red": false, "mode": "all off"}

data/espeak/espeak.sh

+25
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
#!/bin/bash
2+
3+
cd "$(dirname "$0")"
4+
5+
if [ "$1" != "en" ] && [ "$1" != "de" ]
6+
then
7+
echo "Please specify either 'en' or 'de' as your language of choice."
8+
exit 1
9+
fi
10+
11+
# create wav from text
12+
espeak "$2" -v$1 -s130 -w espeak-$$.wav
13+
14+
# prepend intro gong
15+
sox psa.wav espeak-$$.wav announce-$$.wav
16+
17+
# encode mp3
18+
lame -S -V0 -h -b 160 --vbr-new announce-$$.wav announce-$$.mp3
19+
20+
# move mp3 to final destination
21+
mv announce-$$.mp3 $(mktemp --tmpdir="$(dirname "$0")/../announces/ --suffix=.mp3)
22+
23+
# cleanup
24+
rm espeak-$$.wav
25+
rm announce-$$.wav

data/espeak/psa.wav

40.5 KB
Binary file not shown.

data/ledticker.txt

Whitespace-only changes.

data/mate.json

+1
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
{"lastUpdate": 1366753502, "bottles": 18}

data/muc.json

+1
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
{"lastUpdate": 1366844105, "mucUsers": 2, "botOnline": true}

data/room.json

+1
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
{"lastOpenSignal": 1366759202.5305691, "lastStatusSignal": 1366753802.6650469, "people": "1"}

data/room_status.json

-1
This file was deleted.

muc.py helper/muc_helper.py

+25-4
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,25 @@
11
import subprocess as sp
2-
import xmpp
3-
from conf import *
2+
import xmpp, urllib, urllib2
3+
4+
# api settings
5+
API_URL = "https://hickerspace.org/api/muc/"
6+
API_USER = "api-user"
7+
API_PASSWORD = "APIKEY"
8+
9+
# jabber settings for status user
10+
JABBER_SERVER = "hickerspace.org"
11+
JABBER_USER = "JABBERUSER"
12+
JABBER_PASSWORD = "JABBERPASSWORD"
13+
14+
# muc settings
15+
JABBER_MUC = "hick"
16+
JABBER_MUC_SERVER = "conference.hickerspace.org"
417

518
"""
619
Returns bot status and number of users online in our MUC.
720
"""
8-
def mucStatus():
9-
result = { 'mucUsers' : 0, 'botOnline' : False }
21+
def determineStatus():
22+
result = { "mucUsers": 0, "botOnline": False }
1023

1124
# connect to jabber server
1225
con = xmpp.Client(JABBER_SERVER)
@@ -27,3 +40,11 @@ def mucStatus():
2740
result['mucUsers'] = result['mucUsers'] - 1
2841

2942
return result
43+
44+
45+
passman = urllib2.HTTPPasswordMgrWithDefaultRealm()
46+
passman.add_password(None, API_URL, API_USER, API_PASSWORD)
47+
urllib2.install_opener(urllib2.build_opener(urllib2.HTTPBasicAuthHandler(passman)))
48+
req = urllib2.Request(API_URL)
49+
urllib2.urlopen(req, urllib.urlencode(determineStatus()))
50+

httpaccesscontrol.py

+50
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
from datetime import timedelta
2+
from flask import make_response, request, current_app
3+
from functools import update_wrapper
4+
5+
6+
"""
7+
Source: "Decorator for the HTTP Access Control" by Armin Ronacher
8+
http://flask.pocoo.org/snippets/56/
9+
"""
10+
11+
def crossdomain(origin=None, methods=None, headers=None,
12+
max_age=21600, attach_to_all=True,
13+
automatic_options=True):
14+
if methods is not None:
15+
methods = ', '.join(sorted(x.upper() for x in methods))
16+
if headers is not None and not isinstance(headers, basestring):
17+
headers = ', '.join(x.upper() for x in headers)
18+
if not isinstance(origin, basestring):
19+
origin = ', '.join(origin)
20+
if isinstance(max_age, timedelta):
21+
max_age = max_age.total_seconds()
22+
23+
def get_methods():
24+
if methods is not None:
25+
return methods
26+
27+
options_resp = current_app.make_default_options_response()
28+
return options_resp.headers['allow']
29+
30+
def decorator(f):
31+
def wrapped_function(*args, **kwargs):
32+
if automatic_options and request.method == 'OPTIONS':
33+
resp = current_app.make_default_options_response()
34+
else:
35+
resp = make_response(f(*args, **kwargs))
36+
if not attach_to_all and request.method != 'OPTIONS':
37+
return resp
38+
39+
h = resp.headers
40+
41+
h['Access-Control-Allow-Origin'] = origin
42+
h['Access-Control-Allow-Methods'] = get_methods()
43+
h['Access-Control-Max-Age'] = str(max_age)
44+
if headers is not None:
45+
h['Access-Control-Allow-Headers'] = headers
46+
return resp
47+
48+
f.provide_automatic_options = False
49+
return update_wrapper(wrapped_function, f)
50+
return decorator

0 commit comments

Comments
 (0)