Skip to content

Add firebase-based tic-tac-toe sample. #560

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 4 commits into from
Oct 7, 2016
Merged

Add firebase-based tic-tac-toe sample. #560

merged 4 commits into from
Oct 7, 2016

Conversation

jerjou
Copy link
Contributor

@jerjou jerjou commented Oct 6, 2016

Mostly a refactor of stuff from @mogar1980 #558

@googlebot googlebot added the cla: yes This human has signed the Contributor License Agreement. label Oct 6, 2016
@jerjou jerjou force-pushed the firetactoe branch 3 times, most recently from 9503b8d to 175c133 Compare October 6, 2016 20:53
@jerjou jerjou mentioned this pull request Oct 6, 2016
@jerjou
Copy link
Contributor Author

jerjou commented Oct 6, 2016

FYI I still intend to write tests for this, but I figured I wanted some eyes on it first to make sure there're no objections.

#
# https://console.firebase.google.com/project/_/settings/database
#
FIRE_DB_KEY = 'REPLACE_WITH_YOUR_DB_KEY'
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

use env_variables in app.yaml?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is that very much better than having it inline here? We'll still need to say "open up a file and fill in that placeholder", and at least here it's more in-context?

app = flask.Flask(__name__)


def _get_firebase_config():
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is pretty gnarly, is there another way to get the info you need?

Copy link
Contributor Author

@jerjou jerjou Oct 6, 2016

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah - I agree.

The problem is that I need the info from _FIREBASE_CONFIG_TEMPLATE in two places -

  • the client needs it 'cuz it receives events from this endpoint
  • the server needs it because it sends events to this endpoint.

I could inject it into the template, but the way you get it from the firebase console is it gives you an html snippet to copy/paste, and I'd rather say "paste the entire snippet into a file" than "paste the snippet into a file and then look for the databaseURL and copy that value into this placeholder in firetactoe.py".

Alternatively, I could delete this function (it's only used once) and instead of doing config['databaseURL'], I could do:

with open(os.path.join(_CWD, 'templates', _FIREBASE_CONFIG_TEMPLATE)) as f:
    regex = re.compile(r'\bdatabaseURL\b.*?["']([^'"]+)['"]')
    match = next(regex.search(line) for line in f if regex.search(line))
databaseURL = match.group(1)
# etc...

which isn't as correct, but does the job. It's slightly prettier... okay, updated to use a form of that. LMKWYT

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What does this template look like? Can you paste an example?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

<script src="https://www.gstatic.com/firebasejs/3.4.1/firebase.js"></script>
<script>
  // Initialize Firebase
  var config = {
    apiKey: "ABACADABA",
    authDomain: "project-id.firebaseapp.com",
    databaseURL: "https://project-id.firebaseio.com",
    storageBucket: "project-id.appspot.com",
    messagingSenderId: "1234567889"
  };
  firebase.initializeApp(config);
</script>

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fine, regex it is.



def _send_firebase_message(u_id, message=None):
global _FIRE_URL
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Don't modify globals if you can help it, and definitely don't modify global constants. Can this just be pulled into a function?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Mostly I'm trying to cache the url, to avoid reading a file from disk on every request. I dunno - premature optimization? :-)

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That's fine, but can you pull all of this caching stuff into it's own function? _get_fire_url?

}
exp = datetime.timedelta(minutes=60)
return jwt.generate_jwt(
payload, RSA.importKey(credentials['private_key']), 'RS256', exp)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So it seems the only reason we require them to give the private key is so that pyjwt can sign the jwt. However, App engine can actually sign blob directly using app_identity. We'd have to forgo the jwt library, but do you think that's doable?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not too familiar with how jwts work, so I wasn't sure if the app_identity signing was the same as the tokens firebase expects.

I searched the app_identity page for the string 'jwt' and came up blank, so kept this :-/

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

encoding a jwt is pretty straightforward, just use this code: https://github.com/GoogleCloudPlatform/google-auth-library-python/blob/master/google/auth/jwt.py#L55

  • Don't worry about the kid
  • Replace signer.sign with app_identity.sign_blob.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just noticed that you've done this. 👍

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I ran into a problem with this which I fixed, but then realized that using the gcloud way of providing ADC doesn't provide app_identity with everything it needs; so modified the README to use a service account instead.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

SG.

payload, RSA.importKey(credentials['private_key']), 'RS256', exp)


class Wins():
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Does this need to be a class, or could it just be some constants?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I figured the class groups them together in a namespace, which is nice.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not a strong enough argument for class. :)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'd prefer that over having a slew of instance variables and initialization logic in the global namespace.
What would you suggest?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just

X_WIN_PATTERNS = ...

It's fine to have these as constants because, well, they are constants. There's no argument for having them in a class. Classes are for collective state or behavior. You could maybe argue for having them in a separate module.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm unconvinced, but want to get this in sooner rather than later.
Done.

o_wins = map(lambda s: re.compile(s), o_win_patterns)


class Game(db.Model):
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ndb instead of db?

return


def login_required(f):
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

s/f/view_func

*/
function updateGame() {
for (var i = 0; i < 9; i++) {
var square = document.getElementById(i);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

jquery could make a lot of this dom manipulation much easier to read.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I considered that too, but I don't think this script is doing anything complex enough to warrant pulling in jquery. The equivalent jquery would have a similar LOC count (I just tried), and then we're importing a relatively heavy-weight library.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

(not to say there might not be wins to be gained with a larger refactor... (/me eyes the manual style setting below) but it's non-obvious and the clock is ticking :-/)

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It won't decrease the line count, but it'll make things easier. (Also, jquery is hardly heavyweight).

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I disagree, but whatever. Refactored.

<link rel="stylesheet" type="text/css" href="static/main.css" />
<script src="static/main.js"></script>
<script>
initGame('{{ game_key }}', '{{ me }}', '{{ token }}', '{{ channel_id }}',
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would prefer you to store these as data- attributes on some well-known element (body would be fine).

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Any particular reason why that seems better? That feels a little global-variable-y to me, whereas passing in the parameters to init with as arguments seems like the Right Thing to do?

Copy link
Contributor

@theacodes theacodes Oct 7, 2016

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You can still invoke the function:

var $config = $('something),
    game_key = $config.data('game-key'),
    ...;

initGame(game_key, ...);

It just decouples the JS from the template engine - I really, really abhor mixing server-side template interpolation in javascript.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why?
I like it - it puts the information precisely where it's needed, and the templating system makes sure it's safe.

@jerjou jerjou force-pushed the firetactoe branch 2 times, most recently from d1f55ba to 8fffdf3 Compare October 7, 2016 00:40
@jerjou
Copy link
Contributor Author

jerjou commented Oct 7, 2016

Updated to eliminate need for @login_required decorator, as well as separate Firebase api key step

@jerjou
Copy link
Contributor Author

jerjou commented Oct 7, 2016

Removed dependency on service accounts, in favor of ADC all the way.



def _send_firebase_message(u_id, message=None, _memo={}):
if 'http' not in _memo:
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull memoization into it's own function, _get_http.

@jerjou
Copy link
Contributor Author

jerjou commented Oct 7, 2016

Updated. PTAL

Copy link
Contributor

@theacodes theacodes left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good enough for rock 'n' roll, I guess.

@jerjou jerjou merged commit 988acf5 into master Oct 7, 2016
@jerjou jerjou deleted the firetactoe branch October 7, 2016 21:40
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
cla: yes This human has signed the Contributor License Agreement.
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants