Skip to content

Commit

Permalink
Google App Engine server added
Browse files Browse the repository at this point in the history
Signed-off-by: Jonas Kalderstam <[email protected]>
  • Loading branch information
spacecowboy committed Sep 28, 2013
1 parent f7889bf commit 1be93d8
Show file tree
Hide file tree
Showing 20 changed files with 5,084 additions and 0 deletions.
2 changes: 2 additions & 0 deletions app-engine-app/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
*.pyc
index.yaml
13 changes: 13 additions & 0 deletions app-engine-app/Makefile
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
GAE=/home/jonas/Downloads/google_appengine
DEVSERVER=$(GAE)/dev_appserver.py
APPCFG=$(GAE)/appcfg.py

# http://localhost:8080/_ah/api/explorer
local:
$(DEVSERVER) --host=0.0.0.0 ./

clear:
$(DEVSERVER) --clear_datastore=yes --host=0.0.0.0 ./

deploy:
$(APPCFG) update --oauth2 ./
3 changes: 3 additions & 0 deletions app-engine-app/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
## Coming soon

Meanwhile, test the apk
228 changes: 228 additions & 0 deletions app-engine-app/app.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,228 @@
import os, binascii
from datetime import datetime

import endpoints
#from google.appengine.ext import endpoints
from google.appengine.ext import ndb
from protorpc import messages
from protorpc import message_types
from protorpc import remote

from app_gcm import send_link, GCMRegIdModel


def datetime_to_string(datetime_object):
'''Converts a datetime object to a
timestamp string in the format:
2013-09-23 23:23:12.123456'''
return datetime_object.isoformat(sep=' ')

def parse_timestamp(timestamp):
'''Parses a timestamp string.
Supports two formats, examples:
In second precision
>>> parse_timestamp("2013-09-29 13:21:42")
datetime object
Or in fractional second precision (shown in microseconds)
>>> parse_timestamp("2013-09-29 13:21:42.123456")
datetime object
Returns None on failure to parse
>>> parse_timestamp("2013-09-22")
None
'''
result = None
try:
# Microseconds
result = datetime.strptime(timestamp, '%Y-%m-%d %H:%M:%S.%f')
except ValueError:
pass

try:
# Seconds
result = datetime.strptime(timestamp, '%Y-%m-%d %H:%M:%S')
except ValueError:
pass

return result

class Link(messages.Message):
url = messages.StringField(1, required=True)
sha = messages.StringField(2)
deleted = messages.BooleanField(3, default=False)
timestamp = messages.StringField(4)

POST_REQUEST = endpoints.ResourceContainer(
Link,
regid=messages.StringField(2))


class LinkModel(ndb.Model):
sha = ndb.StringProperty(required=True)
url = ndb.StringProperty(required=True)
deleted = ndb.BooleanProperty(required=True, default=False)
userid = ndb.UserProperty(required=True)
timestamp = ndb.DateTimeProperty(required=True, auto_now=True)

# Used to request a link to be deleted.
# Has no body, only URL parameter
DELETE_REQUEST = endpoints.ResourceContainer(
message_types.VoidMessage,
sha=messages.StringField(2, required=True),
regid=messages.StringField(3))

class LinkList(messages.Message):
latestTimestamp = messages.StringField(2)
links = messages.MessageField(Link, 1, repeated=True)

# Used to request the list with query parameters
LIST_REQUEST = endpoints.ResourceContainer(
message_types.VoidMessage,
showDeleted=messages.BooleanField(2, default=False),
timestampMin=messages.StringField(3))

# Add a device id to the user, database model in app_gcm.py
class GCMRegId(messages.Message):
regid = messages.StringField(1, required=True)


# Client id for webapps
CLIENT_ID = '86425096293.apps.googleusercontent.com'
# Client id for devices (android apps)
CLIENT_ID_ANDROID = '86425096293-v1er84h8bmp6c3pcsmdkgupr716u7jha.apps.googleusercontent.com'

@endpoints.api(name='links', version='v1',
description='API for Link Management',
allowed_client_ids=[CLIENT_ID,CLIENT_ID_ANDROID,
endpoints.API_EXPLORER_CLIENT_ID]
)
class LinkApi(remote.Service):
'''This is the REST API. Annotations
specify address, HTTP method and expected
messages.'''

@endpoints.method(POST_REQUEST, Link,
name = 'link.insert',
path = 'links',
http_method = 'POST')
def add_link(self, request):
current_user = endpoints.get_current_user()
if current_user is None:
raise endpoints.UnauthorizedException('Invalid token.')

# Generate an ID if one wasn't included
sha = request.sha
if sha is None:
sha = binascii.b2a_hex(os.urandom(15))
# Construct object to save
link = LinkModel(key=ndb.Key(LinkModel, sha),
sha=sha,
url=request.url,
deleted=request.deleted,
userid=current_user)
# And save it
link.put()

# Notify through GCM
send_link(link, request.regid)

# Return a complete link
return Link(url = link.url,
sha = link.sha,
timestamp = datetime_to_string(link.timestamp))

@endpoints.method(DELETE_REQUEST, message_types.VoidMessage,
name = 'link.delete',
path = 'links/{sha}',
http_method = 'DELETE')
def delete_link(self, request):
current_user = endpoints.get_current_user()
if current_user is None:
raise endpoints.UnauthorizedException('Invalid token.')

link_key = ndb.Key(LinkModel, request.sha)
link = link_key.get()
if link is not None:
link.deleted = True
link.put()
else:
raise endpoints.NotFoundException('No such item')

# Notify through GCM
send_link(link, request.regid)

return message_types.VoidMessage()

@endpoints.method(LIST_REQUEST, LinkList,
name = 'link.list',
path = 'links',
http_method = 'GET')
def list_links(self, request):
current_user = endpoints.get_current_user()
if current_user is None:
raise endpoints.UnauthorizedException('Invalid token.')

# Build the query
q = LinkModel.query(LinkModel.userid == current_user)
q = q.order(LinkModel.timestamp)

# Filter on delete
if not request.showDeleted:
q = q.filter(LinkModel.deleted == False)

# Filter on timestamp
if (request.timestampMin is not None and
parse_timestamp(request.timestampMin) is not None):
q = q.filter(LinkModel.timestamp >\
parse_timestamp(request.timestampMin))

# Get the links
links = []
latest_time = None
for link in q:
ts = link.timestamp
# Find the latest time
if latest_time is None:
latest_time = ts
else:
delta = ts - latest_time
if delta.total_seconds() > 0:
latest_time = ts

# Append to results
links.append(Link(url=link.url, sha=link.sha,
deleted=link.deleted,
timestamp=datetime_to_string(ts)))

if latest_time is None:
latest_time = datetime(1970, 1, 1, 0, 0)

return LinkList(links=links,
latestTimestamp=datetime_to_string(latest_time))

@endpoints.method(GCMRegId, message_types.VoidMessage,
name = 'gcm.register',
path = 'registergcm',
http_method = 'POST')
def register_gcm(self, request):
current_user = endpoints.get_current_user()
if current_user is None:
raise endpoints.UnauthorizedException('Invalid token.')

device = GCMRegIdModel(key=ndb.Key(GCMRegIdModel, request.regid),
regid=request.regid,
userid=current_user)
# And save it
device.put()

# Return nothing
return message_types.VoidMessage()


if __name__ != "__main__":
# Set the application for GAE
application = endpoints.api_server([LinkApi],
restricted=False)
16 changes: 16 additions & 0 deletions app-engine-app/app.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
application: esoteric-storm-343
version: 1
runtime: python27
api_version: 1
threadsafe: true

handlers:
- url: /_ah/spi/.*
script: app.application

libraries:
- name: endpoints
version: 1.0
# Needed for endpoints/users_id_token.py.
- name: pycrypto
version: "2.6"
78 changes: 78 additions & 0 deletions app-engine-app/app_gcm.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
from __future__ import print_function, division
from threading import Thread
from functools import wraps
from gcm import GCM

from google.appengine.ext import ndb

gcm = GCM('Your API key here')

class GCMRegIdModel(ndb.Model):
regid = ndb.StringProperty(required=True)
userid = ndb.UserProperty(required=True)

def to_dict(link):
return dict(sha=link.sha,
url=link.url,
timestamp=link.timestamp.isoformat(sep=" "),
deleted=link.deleted)


def send_link(link, excludeid=None):
'''Transmits the link specified by the sha to the users devices.
Does not run in a separate thread because App-Engine did not
seem to support that.
'''
# Get devices
reg_ids = []
query = GCMRegIdModel.query(GCMRegIdModel.userid == link.userid)

for reg_model in query:
reg_ids.append(reg_model.regid)

# Dont send to origin device, if specified
try:
reg_ids.remove(excludeid)
except ValueError:
pass # not in list, or None

if len(reg_ids) < 1:
return

_send(link.userid, reg_ids, to_dict(link))


def _remove_regid(regid):
ndb.Key(GCMRegIdModel, regid).delete()


def _replace_regid(userid, oldid, newid):
_remove_regid(oldid)
device = GCMRegIdModel(key=ndb.Key(GCMRegIdModel, newid),
regid=newid,
userid=userid)
device.put()


def _send(userid, rids, data):
'''Send the data using GCM'''
response = gcm.json_request(registration_ids=rids,
data=data,
delay_while_idle=True)

# A device has switched registration id
if 'canonical' in response:
for reg_id, canonical_id in response['canonical'].items():
# Repace reg_id with canonical_id in your database
_replace_regid(userid, reg_id, canonical_id)

# Handling errors
if 'errors' in response:
for error, reg_ids in response['errors'].items():
# Check for errors and act accordingly
if (error == 'NotRegistered' or
error == 'InvalidRegistration'):
# Remove reg_ids from database
for regid in reg_ids:
_remove_regid(regid)
6 changes: 6 additions & 0 deletions app-engine-app/appengine_config.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
import os
import sys

ENDPOINTS_PROJECT_DIR = os.path.join(os.path.dirname(__file__),
'endpoints-proto-datastore')
sys.path.append(ENDPOINTS_PROJECT_DIR)
Loading

0 comments on commit 1be93d8

Please sign in to comment.