メールで Twitter、リファクタリング

メールで Twitter

  1. Twitterへのメールからの投稿 - Rubyとか Illustratorとか SFとか折紙とか
  2. メールによる Twitterの参照 - Rubyとか Illustratorとか SFとか折紙とか

暫く使ってるんだけどちょっと不満点もでてきた。Twitterの不安定でリクエストが通らないことがあり、それを見越して何度もメール再送するのつらい。
ので、少し改造しようかと思うから、まずリファクタリング

もう少し仕様を考える

現状

  1. twitter-update@<サーバ>」宛メール、本文一行目を呟きとして投稿
  2. twitter-timeline@<サーバ>」宛メール、本文一行目指定の数のタイムラインをメール送信者に送り返す

http://apiwiki.twitter.com/Twitter-API-Documentation から抜書き、区切りとしてこれ位対応すればいいかな

  • Timeline Methods
    • statuses/friends_timeline
      • 普通に目にするタイムラインと同等っぽいもの
      • 公式RTの扱いについて home_timeline と混乱中
    • statuses/home_timeline
      • 普通に目にするタイムラインと同等のもの
      • 公式RTの扱いについて friends_timeline と混乱中
    • statuses/user_timeline
      • id や呼び名を指定してその人の呟きをみるの
    • statuses/mentions
      • 人々の自分宛て(@自分)呟きをみるの
  • Status Methods
    • statuses/update
      • 呟きの投稿

その辺を踏まえて思うこと

  • APIのメソッド名とコマンドメールのメールアドレスと Rubyスクリプトのメソッド名はなるたけ合わせたい
    • でもコマンドメールが余り長たらしいのも困る
  • コマンドのパラメータはメール本文の一行目辺りからとる
  • タイムラインを持ってくるときの数なんだけど
    • 上限200固定だけでいいかもれないぐらい
    • あとは最近追いつく 55 くらい
    • それと更新確認に 11 とか
      • 更新を確実に出来れば必要ない
    • 頁機能使って更に遡るとかどうしよう
      • そこまで見たいなら PC 使おうよ

リファクタリングの方針

上記まとめるとこんな感じになるかな

  • Postfixでコマンドメールを振り分け Rubyスクリプトに渡すという大枠はそのまま
    • Rubyスクリプトは sender と recipient を引数にメール内容を標準入力として起動される
  • コマンドメールのメールアドレスの書式
    • twitter-<APIのメソッド名>
      • APIのメソッド名「statuses/云々」の云々の所
      • friends_timeline は省略形として timeline もあり
    • 何だ現状のアドレスは儘だ

Rubyスクリプトについて

  • 今 case when で分岐してる所は何かクラス作ってそのインスタンスへの send <メソッド名> で
    • send に渡すメソッド名称、ユーザ入力(コマンドメールのメールアドレス)そのまま渡すのはセキュリティ的によろしくないので何か考える
  • クラス定義部分をくくりだす一方実行部分を if $0==__FILE__ でくくったり
  • 記録(ログ)ファイルのブロックが全体を覆っている辺りなんとかしたい
    • 上記のように定義部分をくくりだせば実行部分は小さくなるのでまあまあそのままでもいいという感じになるかな
      • Logger とかなんとか使う程でもないでしょう
  • Net::HTTP とか Net::SMTP とか使ってローレベルから動かして行く方針は特に変えない
  • タイムラインから取ってくる status の数の既定値は 200 にして置く
    • 実際にはコマンドメールでの数値入力は苦ではないのであんまり意味無いかも
  • 呼び出し方が限定的なので optparseとかオプション解析は特に行わない
    • 何にもないときとか helpメッセージ表示くらいはしようか

そして

そういうわけで Rubyスクリプトリファクタリング

クラス名は Twitter と Statuses かな、Twitter::Statuses にしようか。それを sender で new して(使用者チェック)、recipient で指示されたメソッドを呼ぶ、引数に標準入力を与えたり与えなかったり。

#!/usr/bin/ruby
# -*- coding: utf-8 -*-
require 'nkf'
require 'net/http'
require 'net/smtp'
Net::HTTP.version_1_2

class TwitterError < StandardError; end

class Twitter
  User = Struct.new :acount, :password
  Users= {
    '<投稿者メールアドレス>' => User.new('<ツイッターアカウント名>', '<そのパスワード>')
    }
  Users['<二番目のメールアドレス>'] = Users['<投稿者メールアドレス>']
  Address = ['twitter.com', 80]

  class Statuses
    Path = '/statuses/'
    Commands = Hash.new do |h, key|
      raise TwitterError,  "invalid command '#{key}' called"
    end # Commands = Hash.new do |h, key|
    [
      'friends_timeline',
      'timeline',
      'user_timeline',
      'mentions',
      'update',
    ].inject(Commands){|cmds, cmd| cmds[cmd] = cmd.to_sym; cmds}

    def initialize(sender, recipient)
      @sender, @recipient = sender, recipient
      @user = Users[@sender]
      raise TwitterError, "sender '#{sender}' is not trusted user" unless @user
    end # def initialize(sender, recipient)

    def exec(lines)
      send Commands[@recipient.split('@').first.split('-').last], body(lines)
    end # def exec(lines)

    def update(lines)
      doing = lines[0].chomp
      request = Net::HTTP::Post.new "#{Path}update.json"
      request.basic_auth @user.acount, @user.password
      response = nil 
      Net::HTTP.start(*Address) do |http|
        response = http.request request, "status=#{NKF.nkf('-Jw', doing)}"
      end # Net::HTTP.start('twitter.com',80) do |http|
      [doing, response.body].join "\n\n"
    end # def update(lines)

    def friends_timeline(lines)
      counting = lines[0].chomp
      count = /\A\d+/=~counting ? [counting.to_i, 200].min : 200
      request = Net::HTTP::Get.new "#{Path}friends_timeline.rss"
      request.basic_auth @user.acount, @user.password
      response = nil
      Net::HTTP.start(*Address) do |http|
        response = http.request request, "count=#{count}"
      end # Net::HTTP.start(*Address) do |http|
      result = response.body.split("\n").select do |line|
        (line.include?('<description>')..line.include?('</description>')) ? true : false
      end.map do |line|
        line.sub(/^\s*<description>/, '').sub(/<\/description>\s*$/, '')
      end.map{|line| line.gsub(/&#(\d+?);/){ [$1.to_i].pack('U') }}
      Net::SMTP.start('localhost',25) do |smtp|
        smtp.send_mail <<-EOM, @recipient, @sender
From: #{@recipient}
To: #{@sender}
Subject: #{@recipient.split('@').first}
Date: #{Time.now}
Message-Id: <#{Time.now.to_i}.#{@recipient}>

#{NKF.nkf('-Wj', result.join("\n"))}
        EOM
      end # Net::SMTP.start('localhost',25) do |smtp|
    end # def friends_timeline(lines)
    alias :timeline :friends_timeline

    def user_timeline (lines)
    end # def user_timeline (lines)
    def mentions(lines)
    end # def mentions(lines)

    private
    def body(lines)
      loop{ break lines if 1 == lines.shift.length}
    end # def body(lines)
  end # class Statuses
end # class Twitter
  
if $0 == __FILE__ then
  sender, recipient, = ARGV
  File.open('/home/<ユーザ名>/projects/'+Time.now.strftime('%Y%m%d%H%M%S'), 'w'){ |f| 
  f.puts sender, recipient, ''
  begin # rescue TwitterError => evar
    f.puts Twitter::Statuses.new(sender, recipient).exec($stdin.readlines)
  rescue TwitterError => evar then
    f.puts evar, '', $stdin.read
  end # rescue TwitterError => evar
  } # File.open('/home/<ユーザ名>/projects/'+Time.now.strftime('%Y%m%d%H%M%S'), 'w')
end # if $0 == __FILE__
ちょっと
    Commands = Hash.new do |h, key|
      raise TwitterError,  "invalid command '#{key}' called"
    end # Commands = Hash.new do |h, key|

受け入れるコマンドのリスト、ハッシュの既定値で見付らないエラーを挙げる

    [
      'friends_timeline',
      'timeline',
      'user_timeline',
      'mentions',
      'update',
    ].inject(Commands){|cmds, cmd| cmds[cmd] = cmd.to_sym; cmds}

コマンドのリストの登録、これはなんかちょっと変な気がする。ハッシュを使うのがそもそもおかしいのかな、どうしたら良いんだろう。

      @user = Users[@sender]
      raise TwitterError, "sender '#{sender}' is not trusted user" unless @user

Userが見付らなかった時エラーを挙げる。上記 Commandsとエラーの挙げ方が違うのは、書いた時期が違うからでしょう。合わせた方が良いでしょう。

あとは元のスクリプトをコピペした感じ。如何でしょう。