-
Notifications
You must be signed in to change notification settings - Fork 6.6k
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
Conversation
9503b8d
to
175c133
Compare
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' |
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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(): |
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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>
There was a problem hiding this comment.
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 |
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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? :-)
There was a problem hiding this comment.
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) |
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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 :-/
There was a problem hiding this comment.
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
withapp_identity.sign_blob
.
There was a problem hiding this comment.
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. 👍
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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(): |
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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. :)
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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): |
There was a problem hiding this comment.
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): |
There was a problem hiding this comment.
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); |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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 :-/)
There was a problem hiding this comment.
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).
There was a problem hiding this comment.
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 }}', |
There was a problem hiding this comment.
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).
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
d1f55ba
to
8fffdf3
Compare
Updated to eliminate need for |
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: |
There was a problem hiding this comment.
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
.
Updated. PTAL |
Mostly a refactor of stuff from @mogar1980
There was a problem hiding this 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.
Mostly a refactor of stuff from @mogar1980 #558