Last active
December 31, 2015 16:39
-
-
Save smarx/8014858 to your computer and use it in GitHub Desktop.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
from flask import Flask, redirect, url_for, session, request, render_template | |
from dropbox.client import DropboxClient, DropboxOAuth2Flow | |
import dropbox | |
import redis | |
from datetime import datetime, timedelta | |
import os | |
# Token for the account holding the images | |
my_token = os.environ['TOKEN'] | |
redis_url = os.environ['REDISTOGO_URL'] | |
redis_client = redis.from_url(redis_url) | |
# App key and secret from the App console (dropbox.com/developers/apps) | |
APP_KEY = os.environ['APP_KEY'] | |
APP_SECRET = os.environ['APP_SECRET'] | |
app = Flask(__name__) | |
app.debug = True | |
# A random secret used by Flask to encrypt session data cookies | |
app.secret_key = os.environ['FLASK_SECRET_KEY'] | |
def get_callback_url(): | |
'''Generate a proper callback URL, forcing HTTPS if not running locally''' | |
url = url_for( | |
'callback', | |
_external=True, | |
_scheme='http' if request.host.startswith('127.0.0.1') else 'https' | |
) | |
return url | |
def get_flow(): | |
return DropboxOAuth2Flow( | |
APP_KEY, | |
APP_SECRET, | |
get_callback_url(), | |
session, | |
'dropbox-csrf-token') | |
def get_answer_url(): | |
'''Based on the current date, generate a media link for the current photo (<date>.jpg).''' | |
pacific_now = datetime.utcnow() + timedelta(hours=-8) | |
return DropboxClient(my_token).media(pacific_now.strftime('%Y-%m-%d.jpg'))['url'] | |
@app.route('/') | |
def index(): | |
# Pass a URL for today's image into the view. | |
return render_template('index.html', url=get_answer_url()) | |
@app.route('/login') | |
def login(): | |
# A bit of JavaScript on the client passes a 'tz' parameter specifying the | |
# user's time zone. Pass that through the OAuth flow in the state parameter. | |
tz = request.args.get('tz', '0') | |
return redirect(get_flow().start(tz)) | |
cache = {} | |
def get_copy_ref(token, date): | |
'''For today's photo, return a copy_ref that will be used to copy the file | |
into users' accounts. Cache these for efficiency when processing a lot of | |
accounts on the same day (which happens in the cron job).''' | |
if date not in cache: | |
cache[date] = DropboxClient(token).create_copy_ref(date + '.jpg')['copy_ref'] | |
return cache[date] | |
def update_if_needed(uid, r, my_token): | |
'''For a given user, copy a new photo into their Dropbox if needed.''' | |
# Get the user's access token from Redis. | |
access_token = redis_client.hget('tokens', uid) | |
# Get the time zone, or use Pacific time by default. | |
tz = redis_client.hget('timezones', uid) or '-8' | |
date = (datetime.utcnow() + timedelta(hours=int(tz))).strftime('%Y-%m-%d') | |
# If the user hasn't been updated yet today, copy in a new photo. | |
if redis_client.hget('last_update', uid) != date: | |
# Get a copy ref from the master account | |
copy_ref = get_copy_ref(my_token, date) | |
client = DropboxClient(access_token) | |
try: | |
# Add the photo | |
client.add_copy_ref(copy_ref, 'Is %s Christmas.jpg' % date) | |
except: | |
# Ignore all errors! Probably a bad idea, but the most common | |
# error is that there's a conflict because the user actually | |
# already has this file. TODO: Catch that specifically. :-) | |
pass | |
# Track the last update so we don't revisit this user until tomorrow. | |
redis_client.hset('last_update', uid, date) | |
@app.route('/callback') | |
def callback(): | |
'''Callback function for when the user returns from OAuth.''' | |
# Extract and store the access token, user ID, and time zone. | |
access_token, uid, tz = get_flow().finish(request.args) | |
redis_client.hset('tokens', uid, access_token) | |
redis_client.hset('timezones', uid, tz) | |
# For new users, this should give them today's photo. | |
update_if_needed(uid, r, my_token) | |
return render_template('done.html', url=get_answer_url()) | |
@app.route('/cron') | |
def cron(): | |
'''Cron job, triggered remotely on a regular basis by hitting this URL.''' | |
# Update any users that need it. | |
for uid in redis_client.hkeys('tokens'): | |
update_if_needed(uid, r, my_token) | |
return 'Okay.' | |
if __name__=='__main__': | |
app.run(debug=True) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<!-- To be placed in templates/done.html --> | |
{% extends 'layout.html' %} | |
{% block body %} | |
<p>That's it! You should now see an image in the folder "<code>Dropbox/Apps/Is it Christmas</code>" each day.</p> | |
{% endblock %} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<!-- To be placed in templates/index.html --> | |
{% extends 'layout.html' %} | |
{% block body %} | |
<a class="button" href="login">Connect to Dropbox for daily updates!</a> | |
<script> | |
var links = document.getElementsByTagName('a'); | |
var tz = -new Date().getTimezoneOffset()/60; | |
for (var i = 0; i < links.length; i++) { | |
links[i].href += '?tz=' + tz; | |
} | |
</script> | |
{% endblock %} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<!-- To be placed in templates/layout.html --> | |
<!doctype html> | |
<html> | |
<head> | |
<title>Is it Christmas?</title> | |
<link rel="stylesheet" href="/static/style.css"> | |
</head> | |
<body> | |
<h1>Is it Christmas?</h1> | |
<a class="image" href="/"><img src="{{url}}"></a> | |
{% block body %}{% endblock %} | |
</body> | |
</html> |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/* To be placed in static/style.css */ | |
body { font-family: Helvetica, 'Helvetica Neue', 'Segoe UI', Arial; } | |
h1 { text-align: center; margin: 50px; padding-top: 0; font-weight: normal; } | |
code { font-family: Monaco, Menlo, Consolas, monospace; } | |
a.image { | |
width: 612px; | |
display: block; | |
margin: 0 auto; | |
} | |
a.image img { border: none; outline: none; } | |
a.button { | |
text-decoration: none; | |
text-align: center; | |
width: 300px; | |
outline: none; | |
color: white; | |
display: block; | |
margin: 50px auto; | |
background-color: #007ee5; | |
border: none; | |
font-size: 16px; | |
line-height: 24px; | |
padding: 14px; | |
-webkit-box-shadow: inset 0px -3px 1px rgba(0, 0, 0, 0.45); | |
-moz-box-shadow: inset 0px -3px 1px rgba(0, 0, 0, 0.45); | |
box-shadow: inset 0px -3px 1px rgba(0, 0, 0, 0.45); | |
-webkit-border-radius: 3px; | |
-moz-border-radius: 3px; | |
border-radius: 3px; | |
} | |
a.button:active { | |
position: relative; top: 3px; | |
-webkit-box-shadow: inset 0px -3px 1px rgba(255, 255, 255, 1), inset 0 0px 3px rgba(0, 0, 0, 0.9); | |
-moz-box-shadow: inset 0px -3px 1px rgba(255, 255, 255, 1), inset 0 0px 3px rgba(0, 0, 0, 0.9); | |
box-shadow: inset 0px -3px 1px rgba(255, 255, 255, 1), inset 0 0px 3px rgba(0, 0, 0, 0.9); | |
} | |
a.button:active:after { content: ""; width: 100%; height: 3px; background: #fff; position: absolute; bottom: -1px; left: 0; } | |
p { text-align: center; font-size: 18px; margin-top: 50px; } |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment