Hatena::ブログ(Diary)

Rubyとか Illustratorとか SFとか折紙とか このページをアンテナに追加 RSSフィード

2009-07-16

メールによる Twitterの参照

| 21:42 | メールによる Twitterの参照 - Rubyとか Illustratorとか SFとか折紙とか を含むブックマーク メールによる Twitterの参照 - Rubyとか Illustratorとか SFとか折紙とか のブックマークコメント

Twitterへのメールからの投稿 - Rubyとか Illustratorとか SFとか折紙とか続き

それでメールによる Twitterの参照を受け付けるようにしてみた、参照結果をメールで送り返す。

#!/usr/bin/ruby
# -*- coding: utf-8 -*-
require 'nkf'
require 'net/http'
require 'net/smtp'
Net::HTTP.version_1_2
Trusted = [
  '<投稿者メールアドレス>',
  ]
Twitt = Struct.new :acount, :password
Twitts = {
  '<メールアドレス@の前>' => Twitt.new('<Twitterアカウント>', '<Twitterパスワード>')
  }

sender, recipient, = ARGV
File.open('/home/<ユーザ名>/projects/'+Time.now.strftime('%Y%m%d%H%M%S'), 'w'){ |f|
f.puts sender, recipient, ''
if Trusted.include? sender then
  #File.open('/home/<ユーザ名>/projects/'+Time.now.strftime('%Y%m%d%H%M%S'), 'w'){ |f| f.puts ARGV, $stdin.read }
  twitt = Twitts[sender.split('@')[0]]
  case recipient.split('@')[0]
    when 'twitter-update' then
      null_line = false
      doing = ''
      $stdin.readlines.each do |line|
         (doing = line.chomp; break) if null_line
         null_line = true if 1 == line.length
      end # $stdin.readlines.each do |line|
      f.puts doing
      request = Net::HTTP::Post.new('/statuses/update.json')
      request.basic_auth twitt.acount, twitt.password
      response = nil
      Net::HTTP.start('twitter.com',80) do |http|
        response = http.request(request, "status=#{NKF.nkf('-Jw', doing)}")
      end # Net::HTTP.start('twitter.com',80) do |http|
      f.puts response.body
    when 'twitter-timeline' then
      null_line = false
      doing = ''
      $stdin.readlines.each do |line|
         (doing = line.chomp; break) if null_line
         null_line = true if 1 == line.length
      end # $stdin.readlines.each do |line|
      count = /\A\d+/=~doing ? doing.to_i : 20
      f.puts doing, count, ''
      request = Net::HTTP::Get.new('/statuses/friends_timeline.rss')
      request.basic_auth twitt.acount, twitt.password
      response = nil
      Net::HTTP.start('twitter.com',80) do |http|
        response = http.request(request, "count=#{count}")
      end # Net::HTTP.start('twitter.com',80) 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') } }
      f.puts result
      Net::SMTP.start('localhost',25) do |smtp|
        smtp.send_mail <<-EOM, recipient, sender
From: #{recipient}
To: #{sender}
Subject: #{recipient.split('@')[0]}
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|
    else
  end # case recipient.split('@')[0]
else# if Trusted.include? sender
  f.puts $stdin.read
end # if Trusted.include? sender
}

大きく付け加わってるのはこの辺、when 節ひとつ。

    when 'twitter-timeline' then
      null_line = false
      doing = ''
      $stdin.readlines.each do |line|
         (doing = line.chomp; break) if null_line
         null_line = true if 1 == line.length
      end # $stdin.readlines.each do |line|
      count = /\A\d+/=~doing ? doing.to_i : 20
      f.puts doing, count, ''
      request = Net::HTTP::Get.new('/statuses/friends_timeline.rss')
      request.basic_auth twitt.acount, twitt.password
      response = nil
      Net::HTTP.start('twitter.com',80) do |http|
        response = http.request(request, "count=#{count}")
      end # Net::HTTP.start('twitter.com',80) 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') } }
      f.puts result
      Net::SMTP.start('localhost',25) do |smtp|
        smtp.send_mail <<-EOM, recipient, sender
From: #{recipient}
To: #{sender}
Subject: #{recipient.split('@')[0]}
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|

メールを送るのどうしようかと思った、ActionMailer とは言わないが TMail とか使おうかとも思ったが、Net::SMTP で手書きする。その為の require を冒頭に。

require 'net/smtp'

また、case 分岐処理のところ、コマンドメールアドレス前半だけ見れば良いよね、一々SMTPサーバのアドレスまで書かない。

  case recipient.split('@')[0]
    when 'twitter-update' then

それはそれとして

本文一行目
      count = /\A\d+/=~doing ? doing.to_i : 20

本文一行目が数字っぽかったら TimeLine の参照件数と見做すことにしよう。にしてもここの一行目文字列の変数名に doing は無いよね、update方面のコピペがたたる。

フリップフロップ
      result = response.body.split("\n").select do |line|
        (line.include?('<description>')..line.include?('</description>')) ? true : false
      end.map do |line|

「<description>」タグの中だけみる、Range のフリップフロップ動作って始めてそれと意識して使った気がする。本当に初めてって事も無いだろうけど。”if /^begin/ .. /^end/ など 条件式 式 .. 式”

「end.map do 」はどうですか、僕は結構好き。

ユニコード数値文字参照
      end.map{ |line| line.gsub(/&#(\d+?);/){ [$1.to_i].pack('U') } }

ユニコードの数値文字参照の解決 - Rubyとか Illustratorとか SFとか折紙とか

メールの返信
      Net::SMTP.start('localhost',25) do |smtp|
        smtp.send_mail <<-EOM, recipient, sender
From: #{recipient}
To: #{sender}
Subject: #{recipient.split('@')[0]}
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|

メールを送る所は良いよね。Net::SMTP とはちょっとローレベルに戻り過ぎたか、まあ凝ったもんじゃなし勘弁して、大体見に行く方も Net::HTTP なんだし。

404 Not Found

課題

ちょっとコピーペースト、だからコードの繰り返し。今後コマンドメール対応増やすならなんとかしないと。

「case」分岐もどうかな、「send <メソッド名シンボル>」にしましょう。と言うことは何かクラスかモジュールを作った方が良いかな、それは何のどんなクラス(モジュール)なのだろう。

そう思うと、コマンドメール「twitter-timeline@」はどうか、Twitter APIに拠れば「friends_timeline」「user_timeline 」「mentions」くらいか、或は「statuses/friends_timeline」。どうかな。

2009-07-14

Twitterへのメールからの投稿

| 10:27 | Twitterへのメールからの投稿 - Rubyとか Illustratorとか SFとか折紙とか を含むブックマーク Twitterへのメールからの投稿 - Rubyとか Illustratorとか SFとか折紙とか のブックマークコメント

Twitter 始めました、(hs9587) on Twitter

で、だ。メールを送ったら Twitter投稿してくれるサービス無いかなと。いかにもありそうだけど探すの面倒だし、自分用に作った。これで出先からの投稿が出来るようになった。

環境というか方針としては、CentOSPostfixSMTPサービスが動いてる所にメールを送り、それを Rubyスクリプトに渡してTwitter / ?をどうこうする。

  1. Postfix が特定のアドレスへのメールを捕まえる
  2. それを Postfix から外部コマンドへ渡す
  3. 渡された RubyスクリプトTwitter API で投稿する

Postfix 辞典 (DESKTOP REFERENCE)

Postfix 辞典 (DESKTOP REFERENCE)

とか参考に

Postfix が特定のアドレスへのメールを捕まえる

Postfix 辞典」逆引きリファレンス27「外部コマンドを利用したい」を参考に「transport_maps = hash:/etc/postfix/transport」の設定をする。

/etc/postfix/transport

twitter-update@<サーバアドレス> twitter_api:
twitter-timeline@<サーバアドレス> twitter_api:
twitter-mentions@<サーバアドレス> twitter_api:
twitter-user@<サーバアドレス> twitter_api:

取り敢えず twitter-update しか実装してないけどタイムラインぐらい将来取りたいのでキーワード書いとく。

あと、これらのアドレスが有効になるのは、「transport_maps = <云々>」の設定と同様に /etc/postfix/main.cf に「local_recipient_maps =」として(= の右辺は無し)任意の @左辺アドレスを有効にしているから。そもそもなぜそうしてるかというと、それはまあいろいろある。そちらのメールアドレス名前空間からはこの4つの名前を奪った事になる。

書き換え後は「sudo /usr/sbin/postmap /etc/postfix/transport」を忘れずに。

そして「twitter_api:」というのは次

それを Postfix から外部コマンドへ渡す

上記キーワード「twitter_api:」は転送エージェントの指定と言う事になっている、それは下記に定義したの。

/etc/postfix/master.cf

twitter_api unix -      n       n       -       -       pipe                    
  user=<ユーザ名> argv=/home/<ユーザ名>/projects/twitter_api.rb ${sender} ${recipient}

最初 master.cf での定義と思わず main.cf に書いててはまった。いろいろエラーになったり或は何も起こらなかったり、ログを見れば上記 transport でメール捕まえてはいるのだが twitter_api 見付らないとエラーになってたり。

設定は /etc/postfix/master.cf です、「Postfix 辞典」逆引きリファレンス27「外部コマンドを利用したい」の記述はちょっとわかり難かった。

改行の作法とか、「unix - n n - - pipe」の項目については適宜 Postfix の資料を見よ。

Postfix詳解―MTAの理解とメールサーバの構築・運用

Postfix詳解―MTAの理解とメールサーバの構築・運用

第13章とか。

${sender} ${recipient} 以外に使用できる変数は「Postfix 辞典」機能リファレンス「pipe」p.111 とか。subject はないのね、したらまあこの二つくらいか。

渡された RubyスクリプトTwitter API で投稿する

Rubyスクリプト側での処理の分岐は第二引数 ${recipient} を見ることにしよう。登録されてる ${sender} に応じた TwitterアカウントTwitter REST API に出掛ける。取り敢えず認証は甘い、まあ、自分専用だし。変なのが来る様ならブロックしましょう(それで間に合うのか、間に合うといいな)

メール内容は標準入力でやってくる。はじめ subject を Twitter投稿内容にすることも考えたが、subject なので Base64エンコードされてる。なので本文一行目(JISコードというか、ISO-2022-JP)を投稿する事にしよう。

#!/usr/bin/ruby
# -*- coding: utf-8 -*-
require 'nkf'
require 'net/http'
Net::HTTP.version_1_2
Trusted = [
  '<投稿者メールアドレス>',
  ]
Twitt = Struct.new :acount, :password
Twitts = {
  '<メールアドレス@の前>' => Twitt.new('<Twitterアカウント>', '<Twitterパスワード>')
  }

sender, recipient, = ARGV
File.open('/home/<ユーザ名>/projects/'+Time.now.strftime('%Y%m%d%H%M%S'), 'w'){ |f|
f.puts sender, recipient, ''
if Trusted.include? sender then
  #File.open('/home/<ユーザ名>/projects/'+Time.now.strftime('%Y%m%d%H%M%S'), 'w'){ |f| f.puts ARGV, $stdin.read }
  twitt = Twitts[sender.split('@')[0]]
  case recipient
    when 'twitter-update@<サーバアドレス>' then
      null_line = false
      doing = ''
      $stdin.readlines.each do |line|
         (doing = line.chomp; break) if null_line
         null_line = true if 1 == line.length
      end # $stdin.readlines.each do |line|
      f.puts doing
      request = Net::HTTP::Post.new('/statuses/update.json')
      request.basic_auth twitt.acount, twitt.password
      response = nil
      Net::HTTP.start('twitter.com',80) do |http|
        response = http.request(request, "status=#{NKF.nkf('-Jw', doing)}")
      end # Net::HTTP.start('twitter.com',80) do |http|
      f.puts response.body
    else
  end # case recipient
else# if Trusted.include? sender
  f.puts $stdin.read
end # if Trusted.include? sender
}

初め openuri でどうかなと思ったけど、Post だし基本認証だし Net::HTTP を直接使う。あと、gem にも Twitterクライアントっぽいもの幾つかあるっぽいのだけど、よく分からないので全部手で書いた。

メール本文の一行目、空行の次の行を見分ける所どうでしょう。メール全体をパースしたりしないでいいよね、でもちょっと手抜き過ぎか。

日本語は最初化けっぽかった、NKFユニコードにしたらなんとか。

Trusted配列と、Twittsハッシュを二つ持ってる必要は無いよね、ハッシュに(キーが)あるなら OK とかそういうロジックで良い筈。

課題とか

そもそも自前で自由に出来る SMTPサービス(Postfixサーバ)があるという前提だし、大仕掛けに過ぎるような気もする。foward処理を頑張ればここまで仕掛け大きくしなくても良い様な気もする。いかがでしょうか

transport設定いじるという大仕掛けの割りに Rubyスクリプトをユーザホームディレクトリに置いてるのも不釣合いかな。まあ、そんな特権的なユーザなんだという風に解釈しましょう。

あとは Rubyスクリプトで、${recipient} による処理の分岐やタイムラインの取得、と、それを ${sender} に送るとか。かな。

上記スクリプト自体もリファクタリングの余地あるよね、ちょっと、いろいろ、大分。

それはそれとして

そして作ったあたりで「モバツイッターとかではダメなんですか?」とツイッター返されました。

どうなんでしょう、モバツイッターの説明の「携帯電話iPhoneWindows Mobileで快適にご利用いただけます。」を見て、自分のかなり古いPHSが対象外と思い込んでしまってちゃんと検討してない。

「モバツイッターとは?」の説明からはそれくらいしか読み取れなくてよく分からない。

どうなんでしょう?