diff --git a/appengine/standard/firebase/firetactoe/firetactoe.py b/appengine/standard/firebase/firetactoe/firetactoe.py index 9ccd4070b58..e719867539c 100644 --- a/appengine/standard/firebase/firetactoe/firetactoe.py +++ b/appengine/standard/firebase/firetactoe/firetactoe.py @@ -52,9 +52,11 @@ def _get_firebase_db_url(_memo={}): - """Grabs the databaseURL from the Firebase config snippet.""" - # Memoize the value, to avoid parsing the code snippet every time + """Grabs the databaseURL from the Firebase config snippet. Regex looks + scary, but all it is doing is pulling the 'databaseURL' field from the + Firebase javascript snippet""" if 'dburl' not in _memo: + # Memoize the value, to avoid parsing the code snippet every time regex = re.compile(r'\bdatabaseURL\b.*?["\']([^"\']+)') cwd = os.path.dirname(__file__) with open(os.path.join(cwd, 'templates', _FIREBASE_CONFIG)) as f: @@ -62,7 +64,7 @@ def _get_firebase_db_url(_memo={}): _memo['dburl'] = url.group(1) return _memo['dburl'] - +# [START authed_http] def _get_http(_memo={}): """Provides an authed http object.""" if 'http' not in _memo: @@ -75,17 +77,24 @@ def _get_http(_memo={}): creds.authorize(http) _memo['http'] = http return _memo['http'] +# [END authed_http] - +# [START send_msg] def _send_firebase_message(u_id, message=None): + """Updates data in firebase. If a message is provided, then it updates + the data at /channels/ with the message using the PATCH + http method. If no message is provided, then the data at this location + is deleted using the DELETE http method + """ url = '{}/channels/{}.json'.format(_get_firebase_db_url(), u_id) if message: return _get_http().request(url, 'PATCH', body=message) else: return _get_http().request(url, 'DELETE') +# [END send_msg] - +# [START create_token] def create_custom_token(uid, valid_minutes=60): """Create a secure token for the given id. @@ -94,25 +103,29 @@ def create_custom_token(uid, valid_minutes=60): security rules to prevent unauthorized access. In this case, the uid will be the channel id which is a combination of user_id and game_key """ - header = base64.b64encode(json.dumps({'typ': 'JWT', 'alg': 'RS256'})) + # use the app_identity service from google.appengine.api to get the + # project's service account email automatically client_email = app_identity.get_service_account_name() + now = int(time.time()) + # encode the required claims + # per https://firebase.google.com/docs/auth/server/create-custom-tokens payload = base64.b64encode(json.dumps({ 'iss': client_email, 'sub': client_email, 'aud': _IDENTITY_ENDPOINT, - 'uid': uid, + 'uid': uid, # this is the important parameter as it will be the channel id 'iat': now, 'exp': now + (valid_minutes * 60), })) - + # add standard header to identify this as a JWT + header = base64.b64encode(json.dumps({'typ': 'JWT', 'alg': 'RS256'})) to_sign = '{}.{}'.format(header, payload) - - # Sign the jwt + # Sign the jwt using the built in app_identity service return '{}.{}'.format(to_sign, base64.b64encode( app_identity.sign_blob(to_sign)[1])) - +# [END create_token] class Game(ndb.Model): """All the data we store for a game""" @@ -128,6 +141,7 @@ def to_json(self): d['winningBoard'] = d.pop('winning_board') return json.dumps(d, default=lambda user: user.user_id()) + # [START send_update] def send_update(self): """Updates Firebase's copy of the board.""" message = self.to_json() @@ -140,6 +154,7 @@ def send_update(self): _send_firebase_message( self.userO.user_id() + self.key.id(), message=message) + # [END send_update] def _check_win(self): if self.moveX: @@ -161,6 +176,7 @@ def _check_win(self): if ' ' not in self.board: self.winner = 'Noone' + # [START make_move] def make_move(self, position, user): # If the user is a player, and it's their move if (user in (self.userX, self.userO)) and ( @@ -175,8 +191,9 @@ def make_move(self, position, user): self.put() self.send_update() return + # [END make_move] - +# [START move_route] @app.route('/move', methods=['POST']) def move(): game = Game.get_by_id(request.args.get('g')) @@ -185,8 +202,9 @@ def move(): return 'Game not found, or invalid position', 400 game.make_move(position, users.get_current_user()) return '' +# [END move_route] - +# [START route_delete] @app.route('/delete', methods=['POST']) def delete(): game = Game.get_by_id(request.args.get('g')) @@ -196,6 +214,7 @@ def delete(): _send_firebase_message( user.user_id() + game.key.id(), message=None) return '' +# [END route_delete] @app.route('/opened', methods=['POST']) @@ -226,6 +245,7 @@ def main_page(): game.userO = user game.put() + # [START pass_token] # choose a unique identifier for channel_id channel_id = user.user_id() + game_key # encrypt the channel_id and send it as a custom token to the @@ -236,6 +256,8 @@ def main_page(): _send_firebase_message( channel_id, message=game.to_json()) + # game_link is a url that you can open in another browser to play + # against this player game_link = '{}?g={}'.format(request.base_url, game_key) # push all the data to the html template so the client will @@ -250,3 +272,4 @@ def main_page(): } return flask.render_template('fire_index.html', **template_values) + # [END pass_token] diff --git a/appengine/standard/firebase/firetactoe/rest_api.py b/appengine/standard/firebase/firetactoe/rest_api.py new file mode 100644 index 00000000000..00c6de0a7e2 --- /dev/null +++ b/appengine/standard/firebase/firetactoe/rest_api.py @@ -0,0 +1,108 @@ +# Copyright 2016 Google Inc. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""Demonstration of the Firebase REST API in Python""" + +# [START rest_writing_data] +import base64 +import json +import os +import re +import time +import urllib + + +from flask import request +import httplib2 +from oauth2client.client import GoogleCredentials + + +_FIREBASE_SCOPES = [ + 'https://www.googleapis.com/auth/firebase.database', + 'https://www.googleapis.com/auth/userinfo.email'] + + +def _get_http(_memo={}): + """Provides an authed http object.""" + if 'http' not in _memo: + # Memoize the authorized http, to avoid fetching new access tokens + http = httplib2.Http() + # Use application default credentials to make the Firebase calls + # https://firebase.google.com/docs/reference/rest/database/user-auth + creds = GoogleCredentials.get_application_default().create_scoped( + _FIREBASE_SCOPES) + creds.authorize(http) + _memo['http'] = http + return _memo['http'] + +def firebase_put(path, value=None): + """Writes data to Firebase. Value should be a valid json object. + Put writes an entire object at the given database path. Updates to + fields cannot be performed without overwriting the entire object + """ + response, content = _get_http().request(path, method='PUT', body=value) + if content != "null": + return json.loads(content) + else: + return None + +def firebase_patch(path, value=None): + """Allows specific children or fields to be updated without overwriting + the entire object. Value should again be a valid json object + """ + response, content = _get_http().request(path, method='PATCH', body=value) + if content != "null": + return json.loads(content) + else: + return None + +def firebase_post(path, value=None): + """Post allows an object to be added to an existing list of data. + Value should once again be a valid json object. A successful request + will be indicated by a 200 OK HTTP status code. The response will + contain a new attribute "name" which is the key for the child added + """ + response, content = _get_http().request(path, method='POST', body=value) + if content != "null": + return json.loads(content) + else: + return None + +# [END rest_writing_data] +# [START rest_reading_data] +def firebase_get(path): + """Get request allows reading of data at a particular path + A successful request will be indicated by a 200 OK HTTP status code. + The response will contain the data being retrieved + """ + response, content = _get_http().request(path, method='GET') + if content != "null": + return json.loads(content) + else: + return None +# [END rest_reading_data] +# [START rest_deleting_data] + +def firebase_delete(path): + """Delete removes the data at a particular path + A successful request will be indicated by a 200 OK HTTP status code + with a response containing JSON null. + """ + response, content = _get_http().request(path, method='DELETE') + if content != "null": + return json.loads(content) + else: + return None + +# [END rest_deleting_data] \ No newline at end of file diff --git a/appengine/standard/firebase/firetactoe/static/main.js b/appengine/standard/firebase/firetactoe/static/main.js index 618eada4bd9..f938a55fd01 100644 --- a/appengine/standard/firebase/firetactoe/static/main.js +++ b/appengine/standard/firebase/firetactoe/static/main.js @@ -83,6 +83,7 @@ function initGame(gameKey, me, token, channelId, initialMessage) { return state.userX === state.me ? 'X' : 'O'; } + // [START move_in_square] /** * Send the user's latest move back to the server */ @@ -92,6 +93,7 @@ function initGame(gameKey, me, token, channelId, initialMessage) { $.post('/move', {i: id}); } } + // [END move_in_square] /** * This method lets the server know that the user has opened the channel @@ -109,6 +111,7 @@ function initGame(gameKey, me, token, channelId, initialMessage) { $.post('/delete'); } + // [START remove_listener] /** * This method is called every time an event is fired from Firebase * it updates the entire game state and checks for a winner @@ -124,7 +127,9 @@ function initGame(gameKey, me, token, channelId, initialMessage) { deleteChannel(); //delete the data we wrote } } + // [END remove_listener] + // [START open_channel] /** * This function opens a realtime communication channel with Firebase * It logs in securely using the client token passed from the server @@ -132,21 +137,26 @@ function initGame(gameKey, me, token, channelId, initialMessage) { * finally, it calls onOpened() to let the server know it is ready to receive messages */ function openChannel() { + // [START auth_login] // sign into Firebase with the token passed from the server firebase.auth().signInWithCustomToken(token).catch(function(error) { console.log('Login Failed!', error.code); console.log('Error message: ', error.message); }); + // [END auth_login] + // [START add_listener] // setup a database reference at path /channels/channelId channel = firebase.database().ref('channels/' + channelId); // add a listener to the path that fires any time the value of the data changes channel.on('value', function(data) { onMessage(data.val()); }); + // [END add_listener] onOpened(); // let the server know that the channel is open } + // [END open_channel] /** * This function opens a communication channel with the server