Fork me on GitHub

2010/02/17

はてな
TwitterのBasic認証APIは6月で廃止される予定なのですが、OAuthという認証方法はブラウザを起動してユーザに認証して貰わなければなりません。一見flickrアプリケーションの様な認証方法を想定しますが、OAuthはflickr認証の様にサーバから貰ったトークンをブラウザから渡して認証させる様な物ではありません。
今回OAuthの問題を解決すべくOAuthを拡張した認証方式であるxAuthが取り入れられました。
詳しくはAPIドキュメントか以下のサイトが分かりやすいかと思います。
s-take Blog.: Twitterによる簡易版OAuth: "xAuth"

従来のOAuth認証ではまずアプリケーション(OAuthコンシューマ)がTwitterに接続してRequest Tokenを取得し、認証画面を開いてRequest Tokenを承認させ、承認されたRequest Tokenを使ってAccess TokenとToken Secretを取得することによって各APIにアクセスできるようになります。しかしこれはアプリケーション側の実装が複雑になる上、デスクトップアプリケーションの場合はわざわざWebブラウザへ切り替えなければならず(ブラウザを内包するものもありますが)、ユーザにとっても面倒なものです。

http://s-take.blogspot.com/2010/02/twitteroauth-xauth.html
the.hackerConundrum: Sneak peek at Twitter's browserless OAuth credentials exchange method

Over the past couple of months the Twitter API Google Group has been overflowing with more and more disgruntled developers complaining about lack of bug fixes, slow rollout of promised features, no mobile interface for OAuth, etc. (The list goes on and on) Well I'm happy to say Twitter appears to be almost done with one much requested feature: browserless OAuth credentials exchange. It was hinted that Seesmic Look was using said exchange so today I took a peek at how Look worked behind the scenes.

http://the.hackerconundrum.com/2010/02/sneak-peek-at-twitters-browserless.html
実装だと
Maraigue風。:[Ruby][Twitter] OAuthのアクセストークンを、ブラウザなしで、Twitterのユーザ名およびパスワードのみを用いて取得する(通称:xAuth)ためのRubyのコード
http://blog.livedoor.jp/maraigue/archives/1109122.html
とか
pastebin - 誰か - post number 1796209
http://ja.pastebin.ca/1796209
あとOAuthな話ですが
あまやどり: OAuthで認証してTwitterでつぶやいてみた
http://petitbanca.blogspot.com/2009/11/oauthtwitter.html
あたりが参考になります。今日はpythonを使ってxAuthするサンプルを書いてみました。pythonにも元々oauthライブラリはあるのですが、今回は分かりやすく使わず書いてみました。
以下ソース。
from pit import Pit
from random import getrandbits
from time import time
import hmac, hashlib
import sys
import urllib
import urllib2
import urlparse

consumer_key = 'YOUR-CONSUMER-KEY'
consumer_secret = 'YOUR-CONSUMER-SECRET'
user = Pit.get('twitter.com', {'require' : {
  'username' : 'your username in twitter.com',
  'password' : 'your password in twitter.com',
}})
message = sys.argv[1]
access_url = 'https://api.twitter.com/oauth/access_token'
post_url = 'http://twitter.com/statuses/update.json'

# build parameter to get access token
params = {
  'oauth_consumer_key' : consumer_key,
  'oauth_signature_method' : 'HMAC-SHA1',
  'oauth_timestamp' : str(int(time())),
  'oauth_nonce' : str(getrandbits(64)),
  'oauth_version' : '1.0',
  'x_auth_mode' : 'client_auth',
  'x_auth_username' : user['username'],
  'x_auth_password' : user['password'],
}
params['oauth_signature'] = hmac.new(
  '%s&%s' % (consumer_secret, ''),
  '&'.join([
      'POST',
      urllib.quote(access_url, ''),
      urllib.quote('&'.join(['%s=%s' % (x, params[x])
          for x in sorted(params)]), '')
  ]),
  hashlib.sha1).digest().encode('base64').strip()

# get access token
req = urllib2.Request(access_url, data = urllib.urlencode(params))
res = urllib2.urlopen(req)
token = urlparse.parse_qs(res.read())
token_key = token['oauth_token'][0]
token_secret = token['oauth_token_secret'][0]

# build parameters to post
params = {
  'oauth_consumer_key' : consumer_key,
  'oauth_signature_method' : 'HMAC-SHA1',
  'oauth_timestamp' : str(int(time())),
  'oauth_nonce' : str(getrandbits(64)),
  'oauth_version' : '1.0',
  'oauth_token' : token_key,
}
params['status'] = urllib.quote(message, '')
params['oauth_signature'] = hmac.new(
  '%s&%s' % (consumer_secret, token_secret),
  '&'.join([
      'POST',
      urllib.quote(post_url, ''),
      urllib.quote('&'.join(['%s=%s' % (x, params[x])
          for x in sorted(params)]), '')
  ]),
  hashlib.sha1).digest().encode('base64').strip()
del params['status']

# post with oauth token
req = urllib2.Request(post_url, data = urllib.urlencode(params))
req.add_data(urllib.urlencode({'status' : message}))
req.add_header('Authorization', 'OAuth %s' % ', '.join(
  ['%s="%s"' % (x, urllib.quote(params[x], '')) for x in params]))

# done!
print urllib2.urlopen(req).read()
生の処理で書いてあるので、oauthライブラリに依存させたくない様な移植には参考になるかもしれません。
ところで今回GtkTwitterというC言語で書いたTwitterクライアントのBasic認証を止めようと思っていてこの件を調べ始めたのですが, どうやらTwitterに登録するOAuthアプリケーションには「Twitter」という文言を使ってはいけない事が今日分かりました。まぁGtkTwitterはクライアントアプリが名前登録出来た頃に書いた物なので、あの頃はOKだったのかも知れません。
しかしまぁ...どうせぃっちゅうねん!

どうしましょ。Gtkほにゃらら...何がいいやろ。困った。

2009/06/18

はてな
WindowsではGrowl For Windowsとそれが使っているプロトコルGNTPにより、Windowsでもアイコンを使ったGrowlアプリケーションの開発が可能になりました。その一つにmiyagawaさんが作ったgithub growlerのGNTP版でもあるyet another github growlerというのも作りました。
これでMac, Windowsでのgithub growlerがある事になるのですが、Linuxにありません。Linuxにはアイコンが表示できてGNTPプロトコルを喋るGrowlシステムがありません。
そこで以前から使っていた、Growlネットワークプロトコル(Growlネットワークプロトコルはアイコンが出せません)をサポートしているmumblesというGrowlシステムを調べて見たところ、内部ではpythonでDBusによるプロセス間通信を行っている事が分かりました。
mumbles-project.org

a plugin driven, modern notification system for Gnome

http://www.mumbles-project.org/
さらにそのDBusインタフェース上ではアイコン表示をサポートしていた為、これは!と思いGitHubのGrowlアプリケーションを作ってみました。
まず、DBusで通信する為のプラグインを作成します。
DBusでメソッドが呼ばれると、MumblesPluginクラスに渡されるMumblesNotifyオブジェクトのalertメソッドを呼び出します。
全体のソースは以下の様になります。
from MumblesPlugin import *
import dbus
import gnomevfs
import os
import urllib

class GithubMumbles(MumblesPlugin):
  plugin_name = 'GithubMumbles'
  dbus_interface = 'com.github.DBus'
  dbus_path = '/com/github/DBus'
  icons = {'github' : 'github.png'}
  __url = None

  def __init__(self, mumbles_notify, session_bus):
    self.signal_config = {
      'Notify': self.Notify,
      'NotifyNum': self.NotifyNum
    }
    MumblesPlugin.__init__(self, mumbles_notify, session_bus)
    self.add_click_handler(self.onClick)

  def NotifyNum(self, num):
    self.__url = 'http://github.com/'
    icon = self.get_icon('github')
    title = 'Github'
    msg = str(num)+' new messages!'
    self.mumbles_notify.alert(self.plugin_name, title, msg, icon)

  def Notify(self, link, author, text):
    self.__url = link
    path = os.path.join(PLUGIN_DIR_USER, 'icons', 'github-%s' % author)
    if os.path.exists(path):
      self.icons[author] = 'github-%s' % author
      icon = self.get_icon(author)
    else:
      icon = self.get_icon('github')
    self.mumbles_notify.alert(self.plugin_name, author, text, icon)

  def onClick(self, widget, event, plugin_name):
    if event.button == 3:
      self.mumbles_notify.close(widget.window)
    else:
      self.open_url(self.__url)

  def open_url(self, url):
    mime_type = gnomevfs.get_mime_type(url)
    application = gnomevfs.mime_get_default_application(mime_type)
    os.system(application[2] + ' "' + url + '" &')
インタフェースはリンク、作者、本文のみとしました。これをegg形式にビルドしてmumblesのpluginフォルダに置くと、上記のインタフェース呼び出しによりGrowlが表示されます。
MumblesPluginにはget_iconメソッドが用意されており、これにはアイコン名称を渡す事になります。実際にはplugin/iconsというフォルダにある名称のファイルが使用されるので、今回の仕組としてはgithubフィードのチェッカースクリプトでアイコンをplugin/iconsフォルダに格納させ、それを使用してプラグイン側が使用するという形になっています。プラグイン側でアイコンを取って来ても良いのですがアイコンをダウンロードしている最中はGrowlが固まってしまう為、今回の様な作りとなっています。
次にチェッカースクリプトですが以下の様なコードになります。
#!/usr/bin/env python

UPDATE_INTERVAL=1000 # 10 minutes
MAX_NOTIFICATIONS = 40
DEBUG = True
##################################################

import os
import sys
import time
import getopt
import rfc822
import calendar
import urllib
import feedparser
import dbus
import dbus.service
from dbus.mainloop.glib import DBusGMainLoop
import gobject
from BeautifulSoup import BeautifulSoup
from pit import Pit

GITHUB_DBUS_INTERFACE = 'com.github.DBus'
GITHUB_DBUS_PATH = '/com/github/DBus'

config = Pit.get('github.com', {'require': {
    'user'  : 'user id on github.com',
    'token' : 'user token on github.com'
    }})

class Usage(Exception):
  def __init__(self, msg=None):
    app = sys.argv[0]
    if msg != 'help':
        self.msg = app+': Invalid options. Try --help for usage details.'
    else:
      self.msg = app+": DBus notifications on new github messages.\n"

class GithubCheck(dbus.service.Object):
  def __init__(self):
    session_bus = dbus.SessionBus()
    bus_name = dbus.service.BusName(GITHUB_DBUS_INTERFACE, bus=session_bus)
    dbus.service.Object.__init__(self, bus_name, GITHUB_DBUS_PATH)

    self.interval = UPDATE_INTERVAL
    self.notifyLimit = MAX_NOTIFICATIONS
    self.debug = DEBUG

    self.lastCheck = None
    self.minInterval = 60000 # 1 minute min refresh interval

    if self.interval < self.minInterval:
      print "Warning: Cannot check github more often than once a minute! Using default of 1 minute."
      self.interval = self.minInterval
    self._check()

  @dbus.service.signal(dbus_interface=GITHUB_DBUS_INTERFACE, signature='sss')
  def Notify(self, link, author, text):
    pass

  @dbus.service.signal(dbus_interface=GITHUB_DBUS_INTERFACE, signature='i')
  def NotifyNum(self, num):
    pass

  def _check(self):
    if self.debug:
      if self.lastCheck:
        print "checking feed (newer than %s):" %(self.lastCheck)
      else:
        print "checking feed:"
    try:
      items = feedparser.parse("http://github.com/%s.private.atom/?token=%s" % (config['user'], config['token']))['entries']
    except Exception, e:
      items = []

    if self.lastCheck:
      lastCheck = calendar.timegm(time.localtime(calendar.timegm(rfc822.parsedate(self.lastCheck))))
      for item in items:
        if calendar.timegm(item.published_parsed) < lastCheck:
          items.remove(item)

    self.lastCheck = rfc822.formatdate()
    num_notifications = len(items)

    if num_notifications > MAX_NOTIFICATIONS:
      if self.debug:
        print "%s new entries\n" %(num_notifications)
      self.NotifyNum(num_notifications)
    elif num_notifications < 0:
      if self.debug:
        print "no new entries\n"
    else:
      for item in items:
        path = os.path.join(os.path.expanduser('~'), '.mumbles', 'plugins', 'icons', 'github-%s' % item['author'])
        if not os.path.exists(path):
          html = urllib.urlopen('http://github.com/%s' % item['author']).read()
          soup = BeautifulSoup(html)
          img = soup.findAll('div', {'class':'identity'})[0].find('img')['src']
          img = img.replace("?s=50&", "?s=30&");
          urllib.urlretrieve(img, path)
        self.Notify(item['link'], item['author'], item['title'])
        time.sleep(6)
    gobject.timeout_add(self.interval,self._check)

if __name__ == '__main__':
  DBusGMainLoop(set_as_default=True)
  try:
    try:
      opts, args = getopt.getopt(
        sys.argv[1:], "hp", ["help"])
    except getopt.GetoptError:
      raise Usage()

    for o, a in opts:
      if o in ("-h", "--help"):
        raise Usage('help')
      else:
        raise Usage()
  except Usage, err:
    print >> sys.stderr, err.msg
    sys.exit(2)

  t = GithubCheck()
  try:
    loop = gobject.MainLoop()
    loop.run()
  except KeyboardInterrupt:
    print "githubcheck shut down..."
  except Exception, ex:
    print "Exception in githubcheck: %s" %(ex)
だらだらとしたコードですが、大体分かってもらえるかと思います。pitを使っているので初回起動のみユーザとトークンをエディタで入力する必要があります。トークンはGitHubのダッシュボードにあるRSSアイコンのリンク先URLに含まれています。

実行してしばらくすると以下の様な画面が表示されます。
mumbles-github-growler
これで快適になりました。
github上で全てのソースを公開しています。
mattn's mumbles-github-growler at master - GitHub

github growler using mumbles plugin and checker script.

http://github.com/mattn/mumbles-github-growler/tree/master
よろしければどうぞ。

2009/06/12

はてな
追記
パッチがいるのを忘れていました。
一番下にあります。

バッチファイル。実行すると直接バイナリ配置するので気をつけて。バッチの先頭にあるパスを環境に合わせ修正して下さい。
pythonのソースはオフィシャルから「Python-2.6.2.tar.bz2」を、pdcursesもオフィシャルから「pdcurs34.zip」をダウンロードし"C:¥TEMP¥"に展開しておきます。
@echo off
setlocal

set PYTHON26=c:\Python26

set PYSOURCE=c:\temp\Python-2.6.2
set PDSOURCE=c:\temp\pdcurses

if "%1" == "msvc" goto msvc
if "%1" == "gcc" goto gcc
goto usage

:msvc
set FLAGS=/nologo /LD -DHAVE_CURSES_RESIZE_TERM -DNCURSES_MOUSE_VERSION=2
echo building _curses.pyd
cl %FLAGS% -I%PYTHON26%\include -I%PDSOURCE% %PYSOURCE%\Modules\_cursesmodule.c /link %PYTHON26%\libs\python26.lib %PDSOURCE%\win32\pdcurses.lib %PDSOURCE%\win32\panel.lib /out:%PYTHON26%\lib\_curses.pyd
echo building _curses_panel.pyd
cl %FLAGS% -I%PYTHON26%\include -I%PDSOURCE% %PYSOURCE%\Modules\_curses_panel.c /link %PYTHON26%\libs\python26.lib %PDSOURCE%\win32\pdcurses.lib %PDSOURCE%\win32\panel.lib /out:%PYTHON26%\lib\curses\_curses_panel.pyd
goto end

:gcc
set FLAGS=-DHAVE_CURSES_RESIZE_TERM -DNCURSES_MOUSE_VERSION=2 -Wl,--enable-auto-import -Wl,--export-all -s -shared
echo building _curses.pyd
gcc %FLAGS% -I%PYTHON26%\include -I%PDSOURCE% %PYSOURCE%\Modules\_cursesmodule.c %PYTHON26%\libs\libpython26.a %PDSOURCE%\win32\pdcurses.a %PDSOURCE%\win32\panel.a -o %PYTHON26%\lib\curses\_curses.pyd
echo building _curses_panel.pyd
gcc %FLAGS% -I%PYTHON26%\include -I%PDSOURCE% %PYSOURCE%\Modules\_curses_panel.c %PYTHON26%\libs\libpython26.a %PDSOURCE%\win32\pdcurses.a %PDSOURCE%\win32\panel.a -o %PYTHON26%\lib\curses\_curses_panel.pyd
goto end

:usage
echo pycurses_build.bat [msvc or gcc]

:end
endlocal
以下ソースにあてるパッチです。Windows上のsetuptermは必ずエラーで返すコードになっているので呼ばないようにしています。
--- Modules/_cursesmodule.c.orig    2009-06-13 01:30:02.000000000 +0900
+++ Modules/_cursesmodule.c 2009-06-13 01:30:23.000000000 +0900
@@ -2036,6 +2036,7 @@
        }
    }
 
+#ifndef _WIN32
    if (setupterm(termstr,fd,&err) == ERR) {
        char* s = "setupterm: unknown error";
        
@@ -2048,6 +2049,7 @@
        PyErr_SetString(PyCursesError,s);
        return NULL;
    }
+#endif
 
    initialised_setupterm = TRUE;
 

2009/04/16

はてな
はてなブックマークをdeliciousに同期する場合、どうやってますか?
  • 同時ポストツール?
  • Plagger?
  • まさか、手作業?
同時ポストツールの場合、確かにその場で同時にポスト出来て便利ですね。でも携帯ではてなブックマークから登録した場合、同期されませんよね。 Plaggerだと、cronで動き続けるPCが要りますよね。家に24時間稼動可能なPC無いよ!なんて人いるかもしれません。
手作業?問題外!

先日、Google App Engineにcronが導入されました。
Google App Engine Blog: Seriously this time, the new language on App Engine: Java™

Cron support: schedule tasks like report generation or DB clean-up at an interval of your choosing.

http://googleappengine.blogspot.com/2009/04/seriously-this-time-new-language-on-app.html
つまり...
  1. cronでイベント発生
  2. はてなブックマークからRSS取得
  3. 内部のデータベースから既にポスト済みでないか確認
  4. deliciousにポストする
  5. モテモテ
って事で作ってみました。
mattn's hatenabookmark-meets-delicious at master - GitHub

post hatenabookmark to delicious periodically using google app engine

http://github.com/mattn/hatenabookmark-meets-delicious/tree/master
Google App Engineのアカウントを作成後アプリケーションを作成します。
app.yamlの
application: your_app_name
version: 1
runtime: python
api_version: 1

handlers:
  - url: /tasks/sbm-sync
    script: sbm-sync.py

  - url: /
    script: sbm-sync.py

  - url: /favicon.ico
    static_files: static/images/favicon.ico
    upload: static/images/favicon.ico
    mime_type: image/x-icon

  - url: /static
    static_dir: static
your_app_nameの部分を作成したアプリケーションIDに書き換えて下さい。次にmy-config.yaml.exampleをmy-config.yamlにコピーし
hatena_user : your_hatena_user
delicious_user : your_delicious_user
delicious_pass : your_delicious_password
timezone: JST
timeoffset: 9
あなたの、はてなブックマークIDとdeliciousユーザID/パスワードに書き換えて下さい。
あとはサーバにアップロードすれば10分毎に、はてなブックマークのRSSを取得してdeliciousに同期されます。
gae-sbm-sync

よろしければ使ってみて下さい。ソースはgithubにあるのでpatch welcomeです。