Skip to content

Commit 8f8206e

Browse files
mogar1980Jon Wayne Parrott
authored and
Jon Wayne Parrott
committed
Update firetactoe (GoogleCloudPlatform#590)
* fixed template bugs, added regions for docs writers, and moved credentials into json to make code more obvious to readers * responded to comments and added back html snippet based config * reverting snippet to instructions * minor tweaks and revert to older config file
1 parent a5e17b5 commit 8f8206e

File tree

3 files changed

+154
-13
lines changed

3 files changed

+154
-13
lines changed

appengine/standard/firebase/firetactoe/firetactoe.py

+36-13
Original file line numberDiff line numberDiff line change
@@ -52,17 +52,19 @@
5252

5353

5454
def _get_firebase_db_url(_memo={}):
55-
"""Grabs the databaseURL from the Firebase config snippet."""
56-
# Memoize the value, to avoid parsing the code snippet every time
55+
"""Grabs the databaseURL from the Firebase config snippet. Regex looks
56+
scary, but all it is doing is pulling the 'databaseURL' field from the
57+
Firebase javascript snippet"""
5758
if 'dburl' not in _memo:
59+
# Memoize the value, to avoid parsing the code snippet every time
5860
regex = re.compile(r'\bdatabaseURL\b.*?["\']([^"\']+)')
5961
cwd = os.path.dirname(__file__)
6062
with open(os.path.join(cwd, 'templates', _FIREBASE_CONFIG)) as f:
6163
url = next(regex.search(line) for line in f if regex.search(line))
6264
_memo['dburl'] = url.group(1)
6365
return _memo['dburl']
6466

65-
67+
# [START authed_http]
6668
def _get_http(_memo={}):
6769
"""Provides an authed http object."""
6870
if 'http' not in _memo:
@@ -75,17 +77,24 @@ def _get_http(_memo={}):
7577
creds.authorize(http)
7678
_memo['http'] = http
7779
return _memo['http']
80+
# [END authed_http]
7881

79-
82+
# [START send_msg]
8083
def _send_firebase_message(u_id, message=None):
84+
"""Updates data in firebase. If a message is provided, then it updates
85+
the data at /channels/<channel_id> with the message using the PATCH
86+
http method. If no message is provided, then the data at this location
87+
is deleted using the DELETE http method
88+
"""
8189
url = '{}/channels/{}.json'.format(_get_firebase_db_url(), u_id)
8290

8391
if message:
8492
return _get_http().request(url, 'PATCH', body=message)
8593
else:
8694
return _get_http().request(url, 'DELETE')
95+
# [END send_msg]
8796

88-
97+
# [START create_token]
8998
def create_custom_token(uid, valid_minutes=60):
9099
"""Create a secure token for the given id.
91100
@@ -94,25 +103,29 @@ def create_custom_token(uid, valid_minutes=60):
94103
security rules to prevent unauthorized access. In this case, the uid will
95104
be the channel id which is a combination of user_id and game_key
96105
"""
97-
header = base64.b64encode(json.dumps({'typ': 'JWT', 'alg': 'RS256'}))
98106

107+
# use the app_identity service from google.appengine.api to get the
108+
# project's service account email automatically
99109
client_email = app_identity.get_service_account_name()
110+
100111
now = int(time.time())
112+
# encode the required claims
113+
# per https://firebase.google.com/docs/auth/server/create-custom-tokens
101114
payload = base64.b64encode(json.dumps({
102115
'iss': client_email,
103116
'sub': client_email,
104117
'aud': _IDENTITY_ENDPOINT,
105-
'uid': uid,
118+
'uid': uid, # this is the important parameter as it will be the channel id
106119
'iat': now,
107120
'exp': now + (valid_minutes * 60),
108121
}))
109-
122+
# add standard header to identify this as a JWT
123+
header = base64.b64encode(json.dumps({'typ': 'JWT', 'alg': 'RS256'}))
110124
to_sign = '{}.{}'.format(header, payload)
111-
112-
# Sign the jwt
125+
# Sign the jwt using the built in app_identity service
113126
return '{}.{}'.format(to_sign, base64.b64encode(
114127
app_identity.sign_blob(to_sign)[1]))
115-
128+
# [END create_token]
116129

117130
class Game(ndb.Model):
118131
"""All the data we store for a game"""
@@ -128,6 +141,7 @@ def to_json(self):
128141
d['winningBoard'] = d.pop('winning_board')
129142
return json.dumps(d, default=lambda user: user.user_id())
130143

144+
# [START send_update]
131145
def send_update(self):
132146
"""Updates Firebase's copy of the board."""
133147
message = self.to_json()
@@ -140,6 +154,7 @@ def send_update(self):
140154
_send_firebase_message(
141155
self.userO.user_id() + self.key.id(),
142156
message=message)
157+
# [END send_update]
143158

144159
def _check_win(self):
145160
if self.moveX:
@@ -161,6 +176,7 @@ def _check_win(self):
161176
if ' ' not in self.board:
162177
self.winner = 'Noone'
163178

179+
# [START make_move]
164180
def make_move(self, position, user):
165181
# If the user is a player, and it's their move
166182
if (user in (self.userX, self.userO)) and (
@@ -175,8 +191,9 @@ def make_move(self, position, user):
175191
self.put()
176192
self.send_update()
177193
return
194+
# [END make_move]
178195

179-
196+
# [START move_route]
180197
@app.route('/move', methods=['POST'])
181198
def move():
182199
game = Game.get_by_id(request.args.get('g'))
@@ -185,8 +202,9 @@ def move():
185202
return 'Game not found, or invalid position', 400
186203
game.make_move(position, users.get_current_user())
187204
return ''
205+
# [END move_route]
188206

189-
207+
# [START route_delete]
190208
@app.route('/delete', methods=['POST'])
191209
def delete():
192210
game = Game.get_by_id(request.args.get('g'))
@@ -196,6 +214,7 @@ def delete():
196214
_send_firebase_message(
197215
user.user_id() + game.key.id(), message=None)
198216
return ''
217+
# [END route_delete]
199218

200219

201220
@app.route('/opened', methods=['POST'])
@@ -226,6 +245,7 @@ def main_page():
226245
game.userO = user
227246
game.put()
228247

248+
# [START pass_token]
229249
# choose a unique identifier for channel_id
230250
channel_id = user.user_id() + game_key
231251
# encrypt the channel_id and send it as a custom token to the
@@ -236,6 +256,8 @@ def main_page():
236256
_send_firebase_message(
237257
channel_id, message=game.to_json())
238258

259+
# game_link is a url that you can open in another browser to play
260+
# against this player
239261
game_link = '{}?g={}'.format(request.base_url, game_key)
240262

241263
# push all the data to the html template so the client will
@@ -250,3 +272,4 @@ def main_page():
250272
}
251273

252274
return flask.render_template('fire_index.html', **template_values)
275+
# [END pass_token]
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,108 @@
1+
# Copyright 2016 Google Inc. All Rights Reserved.
2+
#
3+
# Licensed under the Apache License, Version 2.0 (the "License");
4+
# you may not use this file except in compliance with the License.
5+
# You may obtain a copy of the License at
6+
#
7+
# http://www.apache.org/licenses/LICENSE-2.0
8+
#
9+
# Unless required by applicable law or agreed to in writing, software
10+
# distributed under the License is distributed on an "AS IS" BASIS,
11+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
# See the License for the specific language governing permissions and
13+
# limitations under the License.
14+
15+
"""Demonstration of the Firebase REST API in Python"""
16+
17+
# [START rest_writing_data]
18+
import base64
19+
import json
20+
import os
21+
import re
22+
import time
23+
import urllib
24+
25+
26+
from flask import request
27+
import httplib2
28+
from oauth2client.client import GoogleCredentials
29+
30+
31+
_FIREBASE_SCOPES = [
32+
'https://www.googleapis.com/auth/firebase.database',
33+
'https://www.googleapis.com/auth/userinfo.email']
34+
35+
36+
def _get_http(_memo={}):
37+
"""Provides an authed http object."""
38+
if 'http' not in _memo:
39+
# Memoize the authorized http, to avoid fetching new access tokens
40+
http = httplib2.Http()
41+
# Use application default credentials to make the Firebase calls
42+
# https://firebase.google.com/docs/reference/rest/database/user-auth
43+
creds = GoogleCredentials.get_application_default().create_scoped(
44+
_FIREBASE_SCOPES)
45+
creds.authorize(http)
46+
_memo['http'] = http
47+
return _memo['http']
48+
49+
def firebase_put(path, value=None):
50+
"""Writes data to Firebase. Value should be a valid json object.
51+
Put writes an entire object at the given database path. Updates to
52+
fields cannot be performed without overwriting the entire object
53+
"""
54+
response, content = _get_http().request(path, method='PUT', body=value)
55+
if content != "null":
56+
return json.loads(content)
57+
else:
58+
return None
59+
60+
def firebase_patch(path, value=None):
61+
"""Allows specific children or fields to be updated without overwriting
62+
the entire object. Value should again be a valid json object
63+
"""
64+
response, content = _get_http().request(path, method='PATCH', body=value)
65+
if content != "null":
66+
return json.loads(content)
67+
else:
68+
return None
69+
70+
def firebase_post(path, value=None):
71+
"""Post allows an object to be added to an existing list of data.
72+
Value should once again be a valid json object. A successful request
73+
will be indicated by a 200 OK HTTP status code. The response will
74+
contain a new attribute "name" which is the key for the child added
75+
"""
76+
response, content = _get_http().request(path, method='POST', body=value)
77+
if content != "null":
78+
return json.loads(content)
79+
else:
80+
return None
81+
82+
# [END rest_writing_data]
83+
# [START rest_reading_data]
84+
def firebase_get(path):
85+
"""Get request allows reading of data at a particular path
86+
A successful request will be indicated by a 200 OK HTTP status code.
87+
The response will contain the data being retrieved
88+
"""
89+
response, content = _get_http().request(path, method='GET')
90+
if content != "null":
91+
return json.loads(content)
92+
else:
93+
return None
94+
# [END rest_reading_data]
95+
# [START rest_deleting_data]
96+
97+
def firebase_delete(path):
98+
"""Delete removes the data at a particular path
99+
A successful request will be indicated by a 200 OK HTTP status code
100+
with a response containing JSON null.
101+
"""
102+
response, content = _get_http().request(path, method='DELETE')
103+
if content != "null":
104+
return json.loads(content)
105+
else:
106+
return None
107+
108+
# [END rest_deleting_data]

appengine/standard/firebase/firetactoe/static/main.js

+10
Original file line numberDiff line numberDiff line change
@@ -83,6 +83,7 @@ function initGame(gameKey, me, token, channelId, initialMessage) {
8383
return state.userX === state.me ? 'X' : 'O';
8484
}
8585

86+
// [START move_in_square]
8687
/**
8788
* Send the user's latest move back to the server
8889
*/
@@ -92,6 +93,7 @@ function initGame(gameKey, me, token, channelId, initialMessage) {
9293
$.post('/move', {i: id});
9394
}
9495
}
96+
// [END move_in_square]
9597

9698
/**
9799
* This method lets the server know that the user has opened the channel
@@ -109,6 +111,7 @@ function initGame(gameKey, me, token, channelId, initialMessage) {
109111
$.post('/delete');
110112
}
111113

114+
// [START remove_listener]
112115
/**
113116
* This method is called every time an event is fired from Firebase
114117
* it updates the entire game state and checks for a winner
@@ -124,29 +127,36 @@ function initGame(gameKey, me, token, channelId, initialMessage) {
124127
deleteChannel(); //delete the data we wrote
125128
}
126129
}
130+
// [END remove_listener]
127131

132+
// [START open_channel]
128133
/**
129134
* This function opens a realtime communication channel with Firebase
130135
* It logs in securely using the client token passed from the server
131136
* then it sets up a listener on the proper database path (also passed by server)
132137
* finally, it calls onOpened() to let the server know it is ready to receive messages
133138
*/
134139
function openChannel() {
140+
// [START auth_login]
135141
// sign into Firebase with the token passed from the server
136142
firebase.auth().signInWithCustomToken(token).catch(function(error) {
137143
console.log('Login Failed!', error.code);
138144
console.log('Error message: ', error.message);
139145
});
146+
// [END auth_login]
140147

148+
// [START add_listener]
141149
// setup a database reference at path /channels/channelId
142150
channel = firebase.database().ref('channels/' + channelId);
143151
// add a listener to the path that fires any time the value of the data changes
144152
channel.on('value', function(data) {
145153
onMessage(data.val());
146154
});
155+
// [END add_listener]
147156
onOpened();
148157
// let the server know that the channel is open
149158
}
159+
// [END open_channel]
150160

151161
/**
152162
* This function opens a communication channel with the server

0 commit comments

Comments
 (0)