09

Python で Aipo にログインして掲示板に何かしらポストする

概要: requests 便利

というわけでとあるグループウェアの掲示板に定期的にポストするというのを自動化したかったので、やり方を調べてみると、requests というパッケージを使うと良いらしい。

http://docs.python-requests.org/en/latest/

今回のグループウェアは Aipo という製品のオープンソース版です。 http://www.aipo.com/

このグループウェアはApache Turbine というフレームワークをベースとしたポータルフレームワークである Jetspeed をベースに日本語化したプロダクトをベースにしたプロダクトで、オープンソースで提供されている。 由来が古いのでいろいろと古い部分もあり、いわゆるRESTful URLのような設計でも無いように見える。なので継続するリクエストパラメータやセッション情報の取り回しなどが重要。自前でHTTP リクエストを飛ばしたりして作るとその辺面倒だったりしますね。しかし今回requests のおかげで大分楽に実装できた感じ。

今回勘違いしてテストサーバにAipo Version 4をインストールしてしまって、それを対象にスクリプトを書いてしまったのだけれども、まあ最新のバージョンでも多分大筋変わらないんじゃないでしょうかね(という希望的観測)。

掲示板投稿するには

Aipoで普通に掲示板に投稿しようと思ったら以下のような手順を踏むことになる。

  1. Aipoにログイン
  2. 掲示板のフォーム(Ajax)を開く
  3. 内容を記入し、投稿

前提としてログインしたアカウントが掲示板に投稿する権限を持っている必要があるけど、それ以外にこの流れでプログラムが気にする必要があるのは以下。

  • ログインし、ログイン状態を保持しなければ投稿できない
  • CSRF対策等でフォームにワンタイムトークンが発行されている可能性

ログイン状態の保持というのは要するにセッション情報という奴だけど、これについてはCookieの取り回しができれば(だいたいの場合は)良いはず。 ワンタイムトークンは一度フォームにアクセスしてみないと分からないので、上記の手順を忠実に守った上で、取得したフォームの情報からトークンを抽出して記事投稿時に一緒に投げるようにしないといけない。

requests ってみる

さて、いきなりセッションのハンドリング。

    s = requests.Session()

これで s に HTTP GETやHTTP POSTのためのメソッドがついてくる。 つまりあとは手順に従って s.get() とか s.post() とか呼べば良いだけ。分かりやすい!

ログインするために以下のようなコードを書いた。

    # Login to Aipo
    login_data = {'action': 'ALJLoginUser',
                  'username': AIPO_USER,
                  'password': AIPO_PASSWD}
    s.post('%s://%s/aipo/portal' % (AIPO_SCHEMA, AIPO_HOST), login_data)

パラメータ名などはそのまんまですね。actionが何なのかよくわからないけど、Jetspeed か Turbine のお作法で操作をこんな形で指定するのだった気がする。 とりあえずこれでログインはOK。次はトークンを取得するために掲示板の投稿フォームをリクエストする。

    # Open posting form
    form_data = {
        'template': 'MsgboardTopicFormScreen',
        'entityid': 'new'
    }
    js_peid = 'P-14398eea627-1000b'
    form_response = \
        s.post('%s://%s/aipo/portal/media-type/html/user/pass/page/default.psml/js_peid/%s?'
               % (AIPO_SCHEMA, AIPO_HOST, js_peid),
               form_data)

Chromeでネットワークのキャプチャを見ながらパラメータを調べたんだけど、templatenew というパラメータで開くフォームを決定しているっぽい。 js_peid は jetspeed由来のパラメータかな。何度かログイン・ログアウトしてみても特にセッションによって変化する値のようでも無かったので、とりあえず固定値で持つ事に。若干ググってみたところ、おそらく、ポートレット固有のIDだと思われるのだけど、確定情報には至らず。

さて、上記で掲示板の投稿フォームが取得できるので、そのレスポンスからトークンらしき物を探してみたところ、secid なるパラメータを見つけた。

正規表現を使ってこれを取り出す。

    # Extract secid(security id?) from response text
    secid = re.search('name="secid" value="([^"]+)"', form_response.text).group(1)

どうせならタグからマッチさせた方が良い。もっと言うならDOMに変換すべきか・・・まあ、今回は置いておきましょう。 あとはこれを使って投稿するだけ。

    # Post main subject
    entry_data = {
        '_name': 'msgboardForm',
        'secid': secid,
        'is_new_category': 'false',
        'mode': 'insert',
        'category_name': '',
        'topic_name': 'こんにちわこんにちわ',
        'folderName': '',
        'eventSubmit_doMsgboard_insert': '追加する',
        'category_id': '2',
        'note': """
        てすててす
        """ ,
        'template': 'MsgboardTopicFormJSONScreen'}
    s.post('http://%s/aipo/portal/media-type/html/user/pass/page/default.psml/js_peid/%s' % (AIPO_SCHEMA, AIPO_HOST, js_peid),
           entry_data)

この辺のパラメータも全部Chromeのネットワークのキャプチャから引っ張ってきただけなんだけど。secidを設定し、topic_name と note に投稿したい内容を設定して s.post() すれば今回の目的は達成と。

まとめ:requests 便利

HTTPのキャプチャは最近ではブラウザ付属のデバッガなどで簡単に取れるようになってきているので、今回使ったrequestsのようなライブラリがあれば手で操作しているような操作を簡単にシミュレートできますね。こんなに簡単ならHTTPベースのソフトウェアテストなんかをそのまま書いてもそんなに面倒じゃないかも。