Skip to content

Commit 1be93d8

Browse files
committed
Google App Engine server added
Signed-off-by: Jonas Kalderstam <[email protected]>
1 parent f7889bf commit 1be93d8

20 files changed

+5084
-0
lines changed

app-engine-app/.gitignore

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
*.pyc
2+
index.yaml

app-engine-app/Makefile

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
GAE=/home/jonas/Downloads/google_appengine
2+
DEVSERVER=$(GAE)/dev_appserver.py
3+
APPCFG=$(GAE)/appcfg.py
4+
5+
# http://localhost:8080/_ah/api/explorer
6+
local:
7+
$(DEVSERVER) --host=0.0.0.0 ./
8+
9+
clear:
10+
$(DEVSERVER) --clear_datastore=yes --host=0.0.0.0 ./
11+
12+
deploy:
13+
$(APPCFG) update --oauth2 ./

app-engine-app/README.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
## Coming soon
2+
3+
Meanwhile, test the apk

app-engine-app/app.py

Lines changed: 228 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,228 @@
1+
import os, binascii
2+
from datetime import datetime
3+
4+
import endpoints
5+
#from google.appengine.ext import endpoints
6+
from google.appengine.ext import ndb
7+
from protorpc import messages
8+
from protorpc import message_types
9+
from protorpc import remote
10+
11+
from app_gcm import send_link, GCMRegIdModel
12+
13+
14+
def datetime_to_string(datetime_object):
15+
'''Converts a datetime object to a
16+
timestamp string in the format:
17+
18+
2013-09-23 23:23:12.123456'''
19+
return datetime_object.isoformat(sep=' ')
20+
21+
def parse_timestamp(timestamp):
22+
'''Parses a timestamp string.
23+
Supports two formats, examples:
24+
25+
In second precision
26+
>>> parse_timestamp("2013-09-29 13:21:42")
27+
datetime object
28+
29+
Or in fractional second precision (shown in microseconds)
30+
>>> parse_timestamp("2013-09-29 13:21:42.123456")
31+
datetime object
32+
33+
Returns None on failure to parse
34+
>>> parse_timestamp("2013-09-22")
35+
None
36+
'''
37+
result = None
38+
try:
39+
# Microseconds
40+
result = datetime.strptime(timestamp, '%Y-%m-%d %H:%M:%S.%f')
41+
except ValueError:
42+
pass
43+
44+
try:
45+
# Seconds
46+
result = datetime.strptime(timestamp, '%Y-%m-%d %H:%M:%S')
47+
except ValueError:
48+
pass
49+
50+
return result
51+
52+
class Link(messages.Message):
53+
url = messages.StringField(1, required=True)
54+
sha = messages.StringField(2)
55+
deleted = messages.BooleanField(3, default=False)
56+
timestamp = messages.StringField(4)
57+
58+
POST_REQUEST = endpoints.ResourceContainer(
59+
Link,
60+
regid=messages.StringField(2))
61+
62+
63+
class LinkModel(ndb.Model):
64+
sha = ndb.StringProperty(required=True)
65+
url = ndb.StringProperty(required=True)
66+
deleted = ndb.BooleanProperty(required=True, default=False)
67+
userid = ndb.UserProperty(required=True)
68+
timestamp = ndb.DateTimeProperty(required=True, auto_now=True)
69+
70+
# Used to request a link to be deleted.
71+
# Has no body, only URL parameter
72+
DELETE_REQUEST = endpoints.ResourceContainer(
73+
message_types.VoidMessage,
74+
sha=messages.StringField(2, required=True),
75+
regid=messages.StringField(3))
76+
77+
class LinkList(messages.Message):
78+
latestTimestamp = messages.StringField(2)
79+
links = messages.MessageField(Link, 1, repeated=True)
80+
81+
# Used to request the list with query parameters
82+
LIST_REQUEST = endpoints.ResourceContainer(
83+
message_types.VoidMessage,
84+
showDeleted=messages.BooleanField(2, default=False),
85+
timestampMin=messages.StringField(3))
86+
87+
# Add a device id to the user, database model in app_gcm.py
88+
class GCMRegId(messages.Message):
89+
regid = messages.StringField(1, required=True)
90+
91+
92+
# Client id for webapps
93+
CLIENT_ID = '86425096293.apps.googleusercontent.com'
94+
# Client id for devices (android apps)
95+
CLIENT_ID_ANDROID = '86425096293-v1er84h8bmp6c3pcsmdkgupr716u7jha.apps.googleusercontent.com'
96+
97+
@endpoints.api(name='links', version='v1',
98+
description='API for Link Management',
99+
allowed_client_ids=[CLIENT_ID,CLIENT_ID_ANDROID,
100+
endpoints.API_EXPLORER_CLIENT_ID]
101+
)
102+
class LinkApi(remote.Service):
103+
'''This is the REST API. Annotations
104+
specify address, HTTP method and expected
105+
messages.'''
106+
107+
@endpoints.method(POST_REQUEST, Link,
108+
name = 'link.insert',
109+
path = 'links',
110+
http_method = 'POST')
111+
def add_link(self, request):
112+
current_user = endpoints.get_current_user()
113+
if current_user is None:
114+
raise endpoints.UnauthorizedException('Invalid token.')
115+
116+
# Generate an ID if one wasn't included
117+
sha = request.sha
118+
if sha is None:
119+
sha = binascii.b2a_hex(os.urandom(15))
120+
# Construct object to save
121+
link = LinkModel(key=ndb.Key(LinkModel, sha),
122+
sha=sha,
123+
url=request.url,
124+
deleted=request.deleted,
125+
userid=current_user)
126+
# And save it
127+
link.put()
128+
129+
# Notify through GCM
130+
send_link(link, request.regid)
131+
132+
# Return a complete link
133+
return Link(url = link.url,
134+
sha = link.sha,
135+
timestamp = datetime_to_string(link.timestamp))
136+
137+
@endpoints.method(DELETE_REQUEST, message_types.VoidMessage,
138+
name = 'link.delete',
139+
path = 'links/{sha}',
140+
http_method = 'DELETE')
141+
def delete_link(self, request):
142+
current_user = endpoints.get_current_user()
143+
if current_user is None:
144+
raise endpoints.UnauthorizedException('Invalid token.')
145+
146+
link_key = ndb.Key(LinkModel, request.sha)
147+
link = link_key.get()
148+
if link is not None:
149+
link.deleted = True
150+
link.put()
151+
else:
152+
raise endpoints.NotFoundException('No such item')
153+
154+
# Notify through GCM
155+
send_link(link, request.regid)
156+
157+
return message_types.VoidMessage()
158+
159+
@endpoints.method(LIST_REQUEST, LinkList,
160+
name = 'link.list',
161+
path = 'links',
162+
http_method = 'GET')
163+
def list_links(self, request):
164+
current_user = endpoints.get_current_user()
165+
if current_user is None:
166+
raise endpoints.UnauthorizedException('Invalid token.')
167+
168+
# Build the query
169+
q = LinkModel.query(LinkModel.userid == current_user)
170+
q = q.order(LinkModel.timestamp)
171+
172+
# Filter on delete
173+
if not request.showDeleted:
174+
q = q.filter(LinkModel.deleted == False)
175+
176+
# Filter on timestamp
177+
if (request.timestampMin is not None and
178+
parse_timestamp(request.timestampMin) is not None):
179+
q = q.filter(LinkModel.timestamp >\
180+
parse_timestamp(request.timestampMin))
181+
182+
# Get the links
183+
links = []
184+
latest_time = None
185+
for link in q:
186+
ts = link.timestamp
187+
# Find the latest time
188+
if latest_time is None:
189+
latest_time = ts
190+
else:
191+
delta = ts - latest_time
192+
if delta.total_seconds() > 0:
193+
latest_time = ts
194+
195+
# Append to results
196+
links.append(Link(url=link.url, sha=link.sha,
197+
deleted=link.deleted,
198+
timestamp=datetime_to_string(ts)))
199+
200+
if latest_time is None:
201+
latest_time = datetime(1970, 1, 1, 0, 0)
202+
203+
return LinkList(links=links,
204+
latestTimestamp=datetime_to_string(latest_time))
205+
206+
@endpoints.method(GCMRegId, message_types.VoidMessage,
207+
name = 'gcm.register',
208+
path = 'registergcm',
209+
http_method = 'POST')
210+
def register_gcm(self, request):
211+
current_user = endpoints.get_current_user()
212+
if current_user is None:
213+
raise endpoints.UnauthorizedException('Invalid token.')
214+
215+
device = GCMRegIdModel(key=ndb.Key(GCMRegIdModel, request.regid),
216+
regid=request.regid,
217+
userid=current_user)
218+
# And save it
219+
device.put()
220+
221+
# Return nothing
222+
return message_types.VoidMessage()
223+
224+
225+
if __name__ != "__main__":
226+
# Set the application for GAE
227+
application = endpoints.api_server([LinkApi],
228+
restricted=False)

app-engine-app/app.yaml

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
application: esoteric-storm-343
2+
version: 1
3+
runtime: python27
4+
api_version: 1
5+
threadsafe: true
6+
7+
handlers:
8+
- url: /_ah/spi/.*
9+
script: app.application
10+
11+
libraries:
12+
- name: endpoints
13+
version: 1.0
14+
# Needed for endpoints/users_id_token.py.
15+
- name: pycrypto
16+
version: "2.6"

app-engine-app/app_gcm.py

Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
from __future__ import print_function, division
2+
from threading import Thread
3+
from functools import wraps
4+
from gcm import GCM
5+
6+
from google.appengine.ext import ndb
7+
8+
gcm = GCM('Your API key here')
9+
10+
class GCMRegIdModel(ndb.Model):
11+
regid = ndb.StringProperty(required=True)
12+
userid = ndb.UserProperty(required=True)
13+
14+
def to_dict(link):
15+
return dict(sha=link.sha,
16+
url=link.url,
17+
timestamp=link.timestamp.isoformat(sep=" "),
18+
deleted=link.deleted)
19+
20+
21+
def send_link(link, excludeid=None):
22+
'''Transmits the link specified by the sha to the users devices.
23+
24+
Does not run in a separate thread because App-Engine did not
25+
seem to support that.
26+
'''
27+
# Get devices
28+
reg_ids = []
29+
query = GCMRegIdModel.query(GCMRegIdModel.userid == link.userid)
30+
31+
for reg_model in query:
32+
reg_ids.append(reg_model.regid)
33+
34+
# Dont send to origin device, if specified
35+
try:
36+
reg_ids.remove(excludeid)
37+
except ValueError:
38+
pass # not in list, or None
39+
40+
if len(reg_ids) < 1:
41+
return
42+
43+
_send(link.userid, reg_ids, to_dict(link))
44+
45+
46+
def _remove_regid(regid):
47+
ndb.Key(GCMRegIdModel, regid).delete()
48+
49+
50+
def _replace_regid(userid, oldid, newid):
51+
_remove_regid(oldid)
52+
device = GCMRegIdModel(key=ndb.Key(GCMRegIdModel, newid),
53+
regid=newid,
54+
userid=userid)
55+
device.put()
56+
57+
58+
def _send(userid, rids, data):
59+
'''Send the data using GCM'''
60+
response = gcm.json_request(registration_ids=rids,
61+
data=data,
62+
delay_while_idle=True)
63+
64+
# A device has switched registration id
65+
if 'canonical' in response:
66+
for reg_id, canonical_id in response['canonical'].items():
67+
# Repace reg_id with canonical_id in your database
68+
_replace_regid(userid, reg_id, canonical_id)
69+
70+
# Handling errors
71+
if 'errors' in response:
72+
for error, reg_ids in response['errors'].items():
73+
# Check for errors and act accordingly
74+
if (error == 'NotRegistered' or
75+
error == 'InvalidRegistration'):
76+
# Remove reg_ids from database
77+
for regid in reg_ids:
78+
_remove_regid(regid)

app-engine-app/appengine_config.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
import os
2+
import sys
3+
4+
ENDPOINTS_PROJECT_DIR = os.path.join(os.path.dirname(__file__),
5+
'endpoints-proto-datastore')
6+
sys.path.append(ENDPOINTS_PROJECT_DIR)

0 commit comments

Comments
 (0)