Mashup Award6で小飼弾さんの 404 API Not Found賞を授賞しました

codetype.org

先日作ったPythonのFlask製アプリ、オンラインコード共有ツール codetype.orgが審査員特別賞を授賞しました。
授賞作品の中では一番地味だと言えるサービスです。
TwitterのOAuthを利用してログインをし、プログラミングのコードを貼り付けてオンライン上で実行結果がわかる・共有できるというものです。

もともと9月に女性エンジニアとしてMashup Caravan Girls Talkでプレゼンをさせて頂いたのですが
そのデモアプリとして動く物を1週間くらいでざっくり作ったのがきっかけです。

その当時は弾さんのAPIは提供企業として正式なものではありませんでした。
それがプレゼンから1ヶ月後くらいの10月半ばに、弾さんのAPIが特別審査員として正式に提供することになったようです。
真面目に授賞できるだなんて思っていなくて、私自身でもすごくビックリしていますし、嬉しいのと恥ずかしいのが入り交じっている感覚です。


弾さんから弾さんの著書 & 弾言に弾さんのサインも頂きました。あリがとうございます!!


授賞式の様子。恐縮しすぎて笑顔がおかしい。
ma6
source: http://weekly.ascii.jp/elem/000/000/029/29236/


システム的にも雑な部分がたくさんあります。
さくらの500円サーバーでマルチドメインとして動いているため、全然費用がかかっていないです。
弾さんのllevalのAPIから返ってくるレスポンスをごそっとjson形式のままDBに保存しています。

制作/開発時間

今回のサービスは学習込みで全部で30時間くらいかけて作りました。

  • ざっくりとしたサンプルベースの雛形5時間
  • OAuth対応、lleval対応、ユーザ登録、syntax highlight対応、 10時間
  • テンプレート化 10時間
  • fork機能、レイアウト微調整、Twitterにpost機能、バナー作成、その他細かいところ 5時間

開発・公開環境

  • さくらの共有サーバー (月500円のライトプラン)
  • Python 2.6.2
  • Flask 0.6
  • Flask OAuth 0.9
  • SQLite 3.6.14.2

codetypeの主な機能

  • Twitterのアカウントでログインができる
  • コードが貼りつけて公開できる
  • 実行した結果がオンライン上で確認できる
  • http://codetype.org/aNfJJREpTu などオリジナルのURLでシェアできる
  • コメントができる
  • 既存のコードをベースにフォークできる
  • ユーザ名は非公開

あたらしいことにチャレンジしたこと

  1. Pythonでなにか作りたい
  2. OAuthの仕組みを利用してなにか作りたい
  3. 個人ではじめてきちんとサービスとして公開した
  4. コンテスト的なものに初めて応募

コードも晒します

create_table.sql

CREATE TABLE users (
  id integer primary key autoincrement,
  twitter_id int not null,
  name string not null,
  oauth_token string not null,
  oauth_secret string not null,
  date string null
);

CREATE TABLE entries (
  id integer primary key autoincrement,
  title string null,
  text string not null,
  author string not null,
  hash string not null,
  tag string null,
  lang string null,
  date string null,
  dan string null
);

CREATE TABLE comments (
  id integer primary key autoincrement,
  text string not null,
  entry_id int null,
  user_id int null,
  date string null
);


~/src/flaskr/flaskr.py

# -*- coding: utf-8 -*-
"""
    Flaskr
    ~~~~~~

    A microblog example application written as Flask tutorial with
    Flask and sqlite3.

    :copyright: (c) 2010 by Armin Ronacher.
    :license: BSD, see LICENSE for more details.
"""
from __future__ import with_statement
import sqlite3
from contextlib import closing
from flask import Flask, request, session, g, redirect, url_for, abort, render_template, flash, escape
from flaskext.oauth import OAuth
from markdown import markdown
import random
import string
import datetime
import sys
import os
from pprint import pprint
import urllib
from flask import json

oauth = OAuth()
twitter = oauth.remote_app('twitter',
                           base_url='http://api.twitter.com/1/',
                           request_token_url='http://api.twitter.com/oauth/request_token',
                           access_token_url='http://api.twitter.com/oauth/access_token',
                           authorize_url='http://api.twitter.com/oauth/authorize',
                           consumer_key='[consumer_key]',
                           consumer_secret='[consumer_secret]'
)

# configuration
DATABASE = '/home/acotie/src/flaskr/flaskr.db'
DEBUG = True
SECRET_KEY = '[SECRET_KEY]'
USERNAME = ''
PASSWORD = ''

# create our little application :)
app = Flask(__name__)
app.config.from_object(__name__)
app.config.from_envvar('FLASKR_SETTINGS', silent=True)


default_lang = {
'perl': 'pl',
'perl6': 'p6',
'awk': 'awk',
'basic': 'bas',
'brainfuck': 'bf',
'c': 'c',
'elisp': 'el',
'lisp': 'lsp',
'scheme': 'scm',
'haskell': 'hs',
'io': 'io',
'javascript': 'js',
'lua': 'lua',
'm4': 'm4',
'ocaml': 'ml',
'php': 'php',
'python': 'py',
'python3': 'py3',
'ruby': 'rb',
'ruby19': 'rb19',
'postscript': 'ps',
'tcl': 'tcl',
}

def query_db(query, args=(), one=False):
    cur = g.db.execute(query, args)
    rv = [dict((cur.description[idx][0], value)
               for idx, value in enumerate(row)) for row in cur.fetchall()]
    return (rv[0] if rv else None) if one else rv

def random_str(self, length = 10):
    ret = ""
    for i in range(length):
        ret += random.choice(string.ascii_letters)
    return ret


def connect_db():
    """Returns a new connection to the database."""
    return sqlite3.connect(app.config['DATABASE'])


def init_db():
    """Creates the database tables."""
    with closing(connect_db()) as db:
        with app.open_resource('schema.sql') as f:
            db.cursor().executescript(f.read())
        db.commit()

@app.before_request
def before_request():
    """Make sure we are connected to the database each request."""
    g.db = connect_db()
    g.user = None
    if 'user_id' in session:
        g.user = query_db('select * from users where twitter_id = ?',
                          [session['user_id']], one=True)

@app.after_request
def after_request(response):
    """Closes the database again at the end of the request."""
    g.db.close()
    return response

@twitter.tokengetter
def get_twitter_token():
    #return session.get('twitter_token')
    user = g.user
    if user is not None:
        return user.oauth_token, user.oauth_secret


@app.route('/')
def show_entries():
    cur = g.db.execute('select title, text, author, tag, lang, hash, date from entries order by id desc limit 10')
    entries = [dict(title=row[0], text=row[1], author=row[2], tag=row[3], lang=row[4], hash=row[5], date=row[6]) 
            for row in cur.fetchall()]
    return render_template('index.html', entries=entries, default_lang=default_lang, )


@app.route('/recent/<int:page>')
def show_entries_pager(page):
    if page is None:
        page = 1
    of_value = 1
    of_value = page * 10
    print of_value
    entries = query_db('select title, text, author, tag, lang, hash, date from entries order by id desc LIMIT 10 OFFSET ?', [of_value], one=False)
    if entries is None:
        print 'No such entry'
        abort(404)
    return render_template('recent.html', entries=entries, default_lang=default_lang, )


@app.route('/<hash>')
def entry(hash):
    dan = ''
    entry = query_db('select * from entries where hash = ?', [hash], one=True)
    if entry is None:
        print 'No such entry'
        abort(404)

    comment = query_db('select * from comments where entry_id = ?', [entry['id']], one=False)
    if comment is None:
        print 'No such comment'

    if entry['dan']:
        dan = json.loads( entry['dan'] )
        print dan

    return render_template('entry.html', entry=entry, comment=comment, dan=dan, default_lang=default_lang, )


@app.route('/<hash>/fork')
def entry_fork(hash):
    dan = ''
    entry = query_db('select * from entries where hash = ?', [hash], one=True)
    if entry is None:
        print 'No such entry'
        abort(404)

    comment = query_db('select * from comments where entry_id = ?', [entry['id']], one=False)
    if comment is None:
        print 'No such comment'

    if entry['dan']:
        dan = json.loads( entry['dan'] )
        print dan

    return render_template('entry_fork.html', entry=entry, comment=comment, dan=dan, default_lang=default_lang)


@app.route('/tag/<tag>')
def entry_tag(tag):
    entry = query_db('select * from entries where tag = ?',
                [tag], one=False)
    if entry is None:
        print 'No such entry'
        abort(404)
    return render_template('entry_tag.html', entry=entry, tags=tag )


@app.route('/about')
def about_entry():
    return render_template('about.html')


@app.route('/blog')
def blog_entry():
    return render_template('blog.html')

@app.route('/contact')
def contact_entry():
    return render_template('contact.html')


@app.route('/add', methods=['POST'])
def add_entry():
    if not session.get('logged_in'):
        abort(401)

    if not g.user:
        abort(401)

    name = session.get('username',None)
    hash = random_str(20)

    # dan the api
    code = request.form['text']
    lang = request.form['lang'] 
    url = 'http://api.dan.co.jp/lleval.cgi'
    params = urllib.urlencode({'c':'', 's':code, 'l':lang})
    f = urllib.urlopen(url + '?' + params) # paramsはc=&s=code&l=lang
    dan = f.read()

    g.db.execute("insert into entries (title, text, author, tag, lang, hash, date, dan) values (?, ?, ?, ?, ?, ?, ?, ?)",
            [request.form['title'], request.form['text'], name, request.form['tag'], request.form['lang'], hash, datetime.datetime.now().strftime("%Y/%m/%d %H:%M:%S"), dan ])
    g.db.commit()
 
    flash('New entry was successfully posted')
    return redirect( url_for('entry', hash=hash) )


@app.route('/add_c/<hash>', methods=['POST'])
def add_comment(hash):
    if not session.get('logged_in'):
        abort(401)

    if not g.user:
        abort(401)

    entry = query_db('select * from entries where hash = ?',
                [hash], one=True)
    if entry is None:
        print 'No such entry'
        abort(404)

    g.db.execute("insert into comments (text, entry_id, user_id, date) values (?, ?, ?, ?)",
            [request.form['comment'], entry['id'], g.user['id'], datetime.datetime.now().strftime("%Y/%m/%d %H:%M:%S")])
    g.db.commit()
    flash('New entry was successfully posted')
    return redirect( url_for('entry', hash=hash) )



@app.route('/login', methods=['GET', 'POST'])
def login():
    if g.user:
        print "OK"
        session['logged_in'] = True
        session['user_id']  = g.user['twitter_id']
        session['username'] = g.user['name']
        flash('You were logged in')
        return redirect(url_for('show_entries'))
    else:
        print "NO"
        print g.user
        print(url_for("oauth_authorized"))
        return twitter.authorize(callback=url_for("oauth_authorized"))


@app.route('/logout')
def logout():
    session.pop('logged_in', None)
    session.pop('username', None)
    flash('You were logged out')
    return redirect(url_for('show_entries'))


@app.route('/oauth-authorized')
@twitter.authorized_handler
def oauth_authorized(resp):
    next_url = url_for('show_entries')
    if resp is None:
        flash(u'You denied the request to sign in.')
        return redirect(next_url)
    else: #success
        user = g.db.execute('select name from users where twitter_id = ?', [int(resp['user_id'])]).fetchone()

        if user is None:
            g.db.execute("insert into users (twitter_id, name, oauth_token, oauth_secret, date) values (?, ?, ?, ?, ?)",
                         [int(resp['user_id']), resp['screen_name'], resp['oauth_token'], resp['oauth_token_secret'], datetime.datetime.now()])
            g.db.commit()

        session['twitter_token'] = (
            resp['oauth_token'],
            resp['oauth_token_secret']
        )
            
        session['logged_in'] = True
        session['username']  = resp['screen_name']
        session['user_id']   = int(resp['user_id'])

        flash(resp['screen_name'] + ' were signed in')
        return redirect(next_url)


if __name__ == '__main__':
    app.run(host='codetype.org', port=5001)


さくらのマルチドメインの公開ディレクトリの~/www/codetype/以下に.htaccessを置きます。
~/www/codetype/.htaccess

#AddHandler cgi-script-debug .cgi
DirectoryIndex index.html .ht

RewriteEngine on
RewriteBase /
RewriteCond %{REQUEST_URI} ^/
RewriteRule ^(.*)$ http://codetype.org:5001/$1 [L,P]

さくらのサーバーは定期的にpythonperlなど立ち上げっぱなしだとサーバー側のプロセスをkillしてしまう為
プロセスが落ちたら立ち上げるように無限ループさせるスクリプトを書いています。
問題はシェルのプロセス自体やscreenなどもkillされる恐れもあります。必要であれば別途、死活監視用スクリプトが必要かと思います。

~/codetype.sh

#!/usr/local/bin/bash

while true
do
    echo 'starting Flaskr server...'
    python ~/src/flaskr/flaskr.py
done

構造やレイアウトなどはこちらにまとめています。
https://gist.github.com/730666

謝辞

今回アプリを作る上で参考にさせて頂いたサイト

以下の4サイトにお世話になりました。本当にありがとうございます!!

  1. Welcome | Flask (A Python Microframework)*1
  2. https://github.com/mitsuhiko/flask/blob/master/examples/minitwit/minitwit.py*2
  3. Bitbucket | The Git solution for professional teams*3
  4. Flaskrの認証をOAuthで*4
MA運営事務局の方々、弾さん、知り合いの方々

今回のきっかけを作っていただいた、プレゼンしませんか?と声を掛けてくださったMashup Award運営事務局の山本さん、山田さん、
仕事終わってからカフェなどで資料thonに付き合ってくださった方々、温かい声を掛けてくださった方々。
本当に感謝の気持ちでいっぱいです。心からお礼を申し上げます。
授賞式でしか味わえないような感動や刺激を沢山受けました。

MA7への抱負

また一から新しいことを勉強しようと思います。
そして個人でも仕事でも、もっとスピード感のある開発ができるように努力します。
できればデザイナーさんと一緒にサービス作って、もっとデザインにもシステム的にも凝ったサービスを作りたいと思います。
あとは個人的に勉強してるFlex/Air系のアプリでガジェット、デスクトップアプリや、ゆるくiPhoneアプリ作りたいです!
なんかよくわからないけどデザインとかディレクションとかしてあげてもいいよ!っていう方はぜひ声掛けてください。

*1:わかりやすい本家サイト

*2:作者の方のコード。Flaskrだけでなく、OAuthのログインまわりも作者のexampleアプリが参考になりました。WAF本体のコード読んでても本当に勉強になります。

*3:id:a2cさん、id:ymotongpooさんによる日本語翻訳ドキュメント。毎日ページ開いて読んでました :D

*4:超貴重な日本語でのサンプル。全体的な流れも超参考になりました