[schema] Add an API for sending/receiving messages.

(imported from commit 209d525dc5892fc4c392a8ced1588c838cbb17c4)
This commit is contained in:
Tim Abbott
2012-10-01 15:36:44 -04:00
parent 60cb2daab7
commit 18a3888373
8 changed files with 198 additions and 9 deletions

55
api/common.py Normal file
View File

@@ -0,0 +1,55 @@
#!/usr/bin/python
import mechanize
import urllib
import simplejson
from urllib2 import HTTPError
import time
class HumbugAPI():
def __init__(self, email, api_key, verbose=False, site="https://app.humbughq.com"):
self.browser = mechanize.Browser()
self.browser.set_handle_robots(False)
self.browser.add_password("https://app.humbughq.com/", "tabbott", "xxxxxxxxxxxxxxxxx", "wiki")
self.api_key = api_key
self.email = email
self.verbose = verbose
self.base_url = site
def send_message(self, submit_hash):
submit_hash["email"] = self.email
submit_hash["api-key"] = self.api_key
submit_data = urllib.urlencode([(k, v.encode('utf-8')) for k,v in submit_hash.items()])
res = self.browser.open(self.base_url + "/api/v1/send_message", submit_data)
return simplejson.loads(res.read())
def get_messages(self, last_received = None):
submit_hash = {}
submit_hash["email"] = self.email
submit_hash["api-key"] = self.api_key
if last_received is not None:
submit_hash["first"] = "0"
submit_hash["last"] = str(last_received)
submit_data = urllib.urlencode([(k, v.encode('utf-8')) for k,v in submit_hash.items()])
res = self.browser.open(self.base_url + "/api/v1/get_updates", submit_data)
return simplejson.loads(res.read())['zephyrs']
def call_on_each_message(self, callback):
max_message_id = None
while True:
try:
messages = self.get_messages(max_message_id)
except HTTPError, e:
# 502/503 typically means the server was restarted; sleep
# a bit, then try again
if self.verbose:
print "HTTP Error getting zephyrs; trying again soon."
print e
time.sleep(1)
except Exception, e:
# For other errors, just try again
print e
time.sleep(2)
continue
for message in sorted(messages, key=lambda x: x["id"]):
max_message_id = max(max_message_id, message["id"])
callback(message)

View File

@@ -0,0 +1,4 @@
#!/bin/sh
# Two quick API tests using curl
curl 127.0.0.1:8000/api/send_message -d "api-key=xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" -d "email=tabbott@humbughq.com" -d "type=personal" -d "new_zephyr=test" -d "recipient=tabbott@humbughq.com"
curl 127.0.0.1:8000/api/get_updates_new -d "api-key=xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" -d "email=tabbott@humbughq.com

38
api/examples/print-messages Executable file
View File

@@ -0,0 +1,38 @@
#!/usr/bin/python
import mechanize
import urllib
import sys
import os
import optparse
usage = """print-message --user=<email address> [options]
Prints out each message received by the indicated user.
Example: print-messages --user=tabbott@humbughq.com --site=http://127.0.0.1:8000
"""
parser = optparse.OptionParser(usage=usage)
parser.add_option('--site',
dest='site',
default="https://app.humbughq.com/",
action='store')
parser.add_option('--api-key',
dest='api_key',
default="xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx",
action='store')
parser.add_option('--user',
dest='user',
action='store')
(options, args) = parser.parse_args()
sys.path.append(os.path.dirname(os.path.dirname(os.path.dirname(__file__))))
import api.common
client = api.common.HumbugAPI(email=options.user,
api_key=options.api_key,
verbose=True,
site=options.site)
def print_message(message):
print message
client.call_on_each_message(print_message)

40
api/examples/send-message Executable file
View File

@@ -0,0 +1,40 @@
#!/usr/bin/python
import mechanize
import urllib
import sys
import os
import optparse
usage = """send-message --user=<email address> [options]
Sends a test message from by the provided user to tabbott@humbhughq.com.
Example: send-message --user=tabbott@humbughq.com --site=http://127.0.0.1:8000
"""
parser = optparse.OptionParser(usage=usage)
parser.add_option('--site',
dest='site',
default="https://app.humbughq.com/",
action='store')
parser.add_option('--api-key',
dest='api_key',
default="xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx",
action='store')
parser.add_option('--user',
dest='user',
action='store')
(options, args) = parser.parse_args()
sys.path.append(os.path.dirname(os.path.dirname(os.path.dirname(__file__))))
import api.common
client = api.common.HumbugAPI(email=options.user,
api_key=options.api_key,
verbose=True,
site=options.site)
message_data = {
"type": "personal",
"new_zephyr": "test",
"recipient": "tabbott@humbughq.com",
}
print client.send_message(message_data)

View File

@@ -11,6 +11,8 @@ urlpatterns = patterns('',
url(r'^update$', 'zephyr.views.update', name='update'), url(r'^update$', 'zephyr.views.update', name='update'),
url(r'^get_updates$', 'zephyr.views.get_updates', name='get_updates'), url(r'^get_updates$', 'zephyr.views.get_updates', name='get_updates'),
url(r'^api/get_updates$', 'zephyr.views.get_updates_api', name='get_updates_api'), url(r'^api/get_updates$', 'zephyr.views.get_updates_api', name='get_updates_api'),
url(r'^api/v1/get_updates$', 'zephyr.views.api_get_updates', name='api_get_updates'),
url(r'^api/v1/send_message$', 'zephyr.views.api_send_message', name='api_send_message'),
url(r'^zephyr/', 'zephyr.views.zephyr', name='zephyr'), url(r'^zephyr/', 'zephyr.views.zephyr', name='zephyr'),
url(r'^forge_zephyr/', 'zephyr.views.forge_zephyr', name='forge_zephyr'), url(r'^forge_zephyr/', 'zephyr.views.forge_zephyr', name='forge_zephyr'),
url(r'^accounts/home/', 'zephyr.views.accounts_home', name='accounts_home'), url(r'^accounts/home/', 'zephyr.views.accounts_home', name='accounts_home'),

View File

@@ -91,6 +91,7 @@ class Command(BaseCommand):
# Application is an instance of Django's standard wsgi handler. # Application is an instance of Django's standard wsgi handler.
application = web.Application([(r"/get_updates", AsyncDjangoHandler), application = web.Application([(r"/get_updates", AsyncDjangoHandler),
(r"/api/get_updates", AsyncDjangoHandler), (r"/api/get_updates", AsyncDjangoHandler),
(r"/api/v1/get_updates", AsyncDjangoHandler),
(r".*", FallbackHandler, dict(fallback=django_app)), (r".*", FallbackHandler, dict(fallback=django_app)),
], debug=django.conf.settings.DEBUG) ], debug=django.conf.settings.DEBUG)

View File

@@ -65,12 +65,22 @@ class Realm(models.Model):
def __str__(self): def __str__(self):
return self.__repr__() return self.__repr__()
def gen_api_key():
return 'xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx'
### TODO: For now, everyone has the same (fixed) API key to make
### testing easier. Uncomment the following to generate them randomly
### in a reasonable way. Long-term, we should use a real
### cryptographic random number generator.
# return hex(random.getrandbits(4*32))[2:34]
class UserProfile(models.Model): class UserProfile(models.Model):
user = models.OneToOneField(User) user = models.OneToOneField(User)
full_name = models.CharField(max_length=100) full_name = models.CharField(max_length=100)
short_name = models.CharField(max_length=100) short_name = models.CharField(max_length=100)
pointer = models.IntegerField() pointer = models.IntegerField()
realm = models.ForeignKey(Realm) realm = models.ForeignKey(Realm)
api_key = models.CharField(max_length=32)
# The user receives this message # The user receives this message
def receive(self, message): def receive(self, message):
@@ -100,6 +110,7 @@ class UserProfile(models.Model):
if not cls.objects.filter(user=user): if not cls.objects.filter(user=user):
profile = cls(user=user, pointer=-1, realm_id=realm.id, profile = cls(user=user, pointer=-1, realm_id=realm.id,
full_name=full_name, short_name=short_name) full_name=full_name, short_name=short_name)
profile.api_key = gen_api_key()
profile.save() profile.save()
# Auto-sub to the ability to receive personals. # Auto-sub to the ability to receive personals.
recipient = Recipient(type_id=profile.id, type=Recipient.PERSONAL) recipient = Recipient(type_id=profile.id, type=Recipient.PERSONAL)

View File

@@ -15,6 +15,7 @@ from zephyr.models import Zephyr, UserProfile, ZephyrClass, Subscription, \
create_user, do_send_zephyr, mit_sync_table, create_user_if_needed, \ create_user, do_send_zephyr, mit_sync_table, create_user_if_needed, \
create_class_if_needed, PreregistrationUser create_class_if_needed, PreregistrationUser
from zephyr.forms import RegistrationForm, HomepageForm, is_unique from zephyr.forms import RegistrationForm, HomepageForm, is_unique
from django.views.decorators.csrf import csrf_exempt
from zephyr.decorator import asynchronous from zephyr.decorator import asynchronous
from zephyr.lib.query import last_n from zephyr.lib.query import last_n
@@ -34,6 +35,22 @@ def require_post(view_func):
return view_func(request, *args, **kwargs) return view_func(request, *args, **kwargs)
return _wrapped_view_func return _wrapped_view_func
# api_key_required will add the authenticated user's user_profile to
# the view function's arguments list, since we have to look it up
# anyway.
def api_key_required(view_func):
def _wrapped_view_func(request, *args, **kwargs):
# Arguably @require_post should protect us from having to do
# this, but I don't want to count on us always getting the
# decorator ordering right.
if request.method != "POST":
return HttpResponseBadRequest('This form can only be submitted by POST.')
user_profile = UserProfile.objects.get(user__email=request.POST.get("email"))
if user_profile is None or request.POST.get("api-key") != user_profile.api_key:
return json_error('Invalid API user/key pair.')
return view_func(request, user_profile, *args, **kwargs)
return _wrapped_view_func
def json_response(res_type="success", msg="", data={}, status=200): def json_response(res_type="success", msg="", data={}, status=200):
content = {"result":res_type, "msg":msg} content = {"result":res_type, "msg":msg}
content.update(data) content.update(data)
@@ -216,8 +233,7 @@ def return_messages_immediately(request, handler, user_profile, **kwargs):
return False return False
def get_updates_backend(request, handler, **kwargs): def get_updates_backend(request, user_profile, handler, **kwargs):
user_profile = UserProfile.objects.get(user=request.user)
if return_messages_immediately(request, handler, user_profile, **kwargs): if return_messages_immediately(request, handler, user_profile, **kwargs):
return return
@@ -237,23 +253,44 @@ def get_updates_backend(request, handler, **kwargs):
def get_updates(request, handler): def get_updates(request, handler):
if not ('last' in request.POST and 'first' in request.POST): if not ('last' in request.POST and 'first' in request.POST):
return json_error("Missing message range") return json_error("Missing message range")
return get_updates_backend(request, handler, apply_markdown=True) user_profile = UserProfile.objects.get(user=request.user)
return get_updates_backend(request, user_profile, handler, apply_markdown=True)
@login_required @login_required
@asynchronous @asynchronous
@require_post @require_post
def get_updates_api(request, handler): def get_updates_api(request, handler):
user_profile = UserProfile.objects.get(user=request.user) user_profile = UserProfile.objects.get(user=request.user)
return get_updates_backend(request, handler, return get_updates_backend(request, user_profile, handler,
apply_markdown=(request.POST.get("apply_markdown") is not None), apply_markdown=(request.POST.get("apply_markdown") is not None),
mit_sync_bot=request.POST.get("mit_sync_bot")) mit_sync_bot=request.POST.get("mit_sync_bot"))
# Yes, this has a name similar to the previous function. I think this
# new name is better and expect the old function to be deleted and
# replaced by the new one soon, so I'm not going to worry about it.
@csrf_exempt
@asynchronous
@require_post
@api_key_required
def api_get_updates(request, user_profile, handler):
return get_updates_backend(request, user_profile, handler,
apply_markdown=(request.POST.get("apply_markdown") is not None),
mit_sync_bot=request.POST.get("mit_sync_bot"))
@csrf_exempt
@require_post
@api_key_required
def api_send_message(request, user_profile):
return zephyr_backend(request, user_profile, user_profile.user)
@login_required @login_required
@require_post @require_post
def zephyr(request): def zephyr(request):
user_profile = UserProfile.objects.get(user=request.user)
if 'time' in request.POST: if 'time' in request.POST:
return json_error("Invalid field 'time'") return json_error("Invalid field 'time'")
return zephyr_backend(request, request.user) return zephyr_backend(request, user_profile, request.user)
@login_required @login_required
@require_post @require_post
@@ -289,12 +326,13 @@ def forge_zephyr(request):
user_email.split('@')[0], user_email.split('@')[0],
user_email.split('@')[0]) user_email.split('@')[0])
return zephyr_backend(request, user) return zephyr_backend(request, user_profile, user)
@login_required # We do not @require_login for zephyr_backend, since it is used both
# from the API and the web service. Code calling zephyr_backend
# should either check the API key or check that the user is logged in.
@require_post @require_post
def zephyr_backend(request, sender): def zephyr_backend(request, user_profile, sender):
user_profile = UserProfile.objects.get(user=request.user)
if "type" not in request.POST: if "type" not in request.POST:
return json_error("Missing type") return json_error("Missing type")
if "new_zephyr" not in request.POST: if "new_zephyr" not in request.POST: