Skip to content

Instantly share code, notes, and snippets.

@smarx
Last active December 31, 2015 16:39
Show Gist options
  • Star 2 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save smarx/8014858 to your computer and use it in GitHub Desktop.
Save smarx/8014858 to your computer and use it in GitHub Desktop.
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)
<!-- 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 %}
<!-- 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 %}
<!-- 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>
/* 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