Ruby から multipart/form-data を OAuth で POST する

Twitter の背景画像を日替わりで変えたくてAPIを叩こうと思い、ググったところ失敗報告しか見つからなかったけど、適当にコピペしてたら出来た。

require 'open-uri'
require 'rubygems'
require 'oauth'

$CONSUMER_KEY = 'xxxxxxxx'
$CONSUMER_SECRET = 'xxxxxxxxxxxxxxxx'
$consumer = OAuth::Consumer.new(
  $CONSUMER_KEY,
  $CONSUMER_SECRET,
  :site => 'http://api.twitter.com'
)

$ACCESS_TOKEN = 'xxxx-xxxxxxxx'
$ACCESS_TOKEN_SECRET = 'xxxxxxxxxxxxxxxx'

$access_token = OAuth::AccessToken.new(
  $consumer,
  $ACCESS_TOKEN,
  $ACCESS_TOKEN_SECRET
)

url = URI.parse("http://api.twitter.com/1/account/update_profile_background_image.json")
Net::HTTP.new(url.host, url.port).start do |http|
  req = Net::HTTP::Post.new(url.request_uri)

  req["content-type"] = "multipart/form-data; boundary=myboundary"

  body = ""

  body.concat("--myboundary\r\n")
  body.concat("content-disposition: form-data; name=\"image\"; filename=\"bgimage.jpg\"\r\n")
  body.concat("\r\n")
  File::open("bgimage.jpg"){|f| body.concat(f.read + "\r\n") }

  body.concat("--myboundary\r\n")
  body.concat("content-disposition: form-data; name=\"tile\";\r\n")
  body.concat("\r\n")
  body.concat("true\r\n")

  body.concat("--myboundary--\r\n")
  req.body = body
  $consumer.sign!(req,$access_token)
  res = http.request(req)
  #puts res.body
end

meganeou氏、ksorano氏、egachan氏が関係するあらゆるメディアをボイコットする方法

cf. meganeou氏、ksorano氏、egachan氏が関係するあらゆるメディアをボイコットしよう! - 過ぎ去ろうとしない過去

PVを与えないためには /etc/hosts でブロックするのが確実ですね。

0.0.0.0	himote.in himo2.jp hotcocoa.biz anond.jp pocha3.com
0.0.0.0	cloudnote.jp 2ch.cloudnote.jp
0.0.0.0	ptflive.jp xn--3ckwar1i4d.jp

xn--3ckwar1i4d.jp は「ソラノート.jp」です。

各OSでの設定方法はこの辺りを参照。
http://seo-engineer.info/archives/hosts.html

127.0.0.1 → 0.0.0.0 に変えました。

Twitterのフォロー関係を利用して本をレコメンド

Livlisの肝は、Twitterでフォロー関係にある≒趣味趣向が似ていたり活動地域が近かったりする人達の間で、物品のやり取りをする点にあると捉えている。同じ仕組みで、本のレコメンドシステムが出来るのではないか、と考えてちょこちょこコードを書いていた。

読書メーターから投稿している人を見かけるし、読んだ4!というのもあった。自分のタイムラインに何度も現れる本というのは、おすすめされている、読むべき本ではないかという仮説だ。

とはいえタイムラインから拾おうとすると書式の違いや短縮URLの処理で膨大な作業になると考え、本棚サービスを利用することに。しかしリソースをTwitterと本棚サービスに依存すると、やはり限界があった。

本棚サービスからRSSフィードで取得すると最近の何件かに限られるのは、最近注目されている本に限定されるので悪いことではない。だが、既に読んだ本を重複してレコメンドしないためには過去の全ての読書履歴を使う必要がある。
また、毎回RSSフィードを取りに行くので時間がかかり、少しアクセスが集中したら処理できなくなると思われる。
しかしユーザーの登録した本をクロールして自前のDBに持つのは、プログラミングもメンテナンスも、重い。

あと勿論Livlisと同様に自分のTLにいる人達がそのサービスを利用していないと標本が少なくて寂しいことになる。

というわけで、読書メーターブクログがこの機能を備えてくれればいいのですが、とりあえず実装したのがこれです。

TwRecoBks 〜 Twitter上のお友達が読んでいる本をお薦め

レコメンドエンジンには id:masui さんの本棚演算を拝借。書籍情報も持たないでAmazonに依存。協調フィルタリングというと難しいアルゴリズムかと思っていたらそうでもない。やはり標本数が重要。

これをつぶやいて一日経って、自分の「フォローしている」は5人増えた。あんまりアクセス集中しないで登録が増えていったらいいなぁ。

Twitterマネタイズ作戦その2

その1はこちら
@train_kanto@train_kansai というbot3年前から運用している。
このほどTwitterBasic認証が停止するのに伴い、スクレイピングしているだけで面白味もなく精度も高くないbotなので廃止しようと思って告知したところ、幾人かから惜しむ声が寄せられた。
で、迷った末にこういう仕掛けを施して続行してみることにした。

Twitter OAuthのクライアント名とリンク先を広告にした。
2つのbotを合わせて20,000フォロワーほどあり、カウントしてみると一日に約50クリックされている。


今は広告といってもただのリンクである。Amazonを貼るのはOKかもしれないがバリューコマースはNGっぽい。そもそもTwitter的にはOKなのだろうか。使えるアフィリエイトサービスを探すのも面倒なので当面このままの運用にしておく。もし広告を出したい人がいたら連絡ください。

Twitter→はてブ連携について

はてなブックマークのTwitter連携機能について。
人力検索にこんな質問があった。
はてなブックマークをご利用の方にTwitterについてお聞きします… - 人力検索はてな
半分以上が知らなくて、使っているのは6%ちょいか。
でも他にもはてブTwitterを連携させる方法はあるんだよね、と、こんな質問をしてみた。
はてなブックマークとTwitterを連携させてご利用の方にお聞きし… - 人力検索はてな
さすがに公式機能だけあって30%近くが採用していた。


まぁ、そもそもはてブTwitterにポストすることを嫌う人もいるけどね。


しかし、この公式機能が良くないと思う。
こんなエントリーページが出来るから。

はてなブックマーク - kari PTFlive on USTREAM: . All
他にも、ページタイトルがそのままコメントになっていたり、RTがそのまま入ったブクマも散見される。

はてなブックマーク - Twitter、スパムを半年で約10分の1に減少させることに成功 -INTERNET Watch Watch
botぽいのは非表示にしてるんだけどねー。
これらのコメントも何らかの価値を持っていると、はてなは考えているのだろうか。
あるいはただエントリーページが増えて広告収入が得られればオールオッケーなのだろうか。

Twitter からのリンクをマネタイズする試み

はてブツイート : はてなブックマークをTwitterに投稿 というサービスを作った。はてなブックマークweb hook を受けて、Twitter の OAuth で投稿するものだ。OAuth 画面によると約500ユーザーに使ってもらっている。
そうなるとこれを何とか収益化できないかと思うのが人情だ。簡単に思い付くのは、短縮URLを利用して広告を入れることだが、permalink を阻害したり、いろいろ邪悪だ。あとそもそも日本語広告と日本への送金に対応したサービスが無い。
というわけで奇抜なアイディアも湧いて来ないので、素直に広告を貼っておく。
ところが Google Adsense で表示される広告はこんなので、クリックを誘うとは思えない。


で、ちょっと細工をしてみた。
普通に http://worris3.sakura.ne.jp/HatenaBookmarkMultiPost/ を開くと、ただのカテゴリー広告が表示される。

一方、Twittertweet (例: http://twitter.com/worris/status/6332216695)に表示される source クライアント名

をクリックして開くと、元投稿にマッチした広告が表示される。


まぁ、いちいち個別 tweetpermalink からクライアント名をクリックして飛んでくる人はあまりいないと思うので効果は期待していない。
Twitter のユーザーページからのリンクにも対応すれば多少向上するかもしれないが処理が面倒なので今のところパス。
とりあえず実験ということで。


大体こんなコード。JavaScriptリファラを取得して本文を読み、Yahoo!キーフレーズ抽出API にかける。JavaScript だけでも書けるが楽な方へ。
JavaScript オンリーにあっさり書き直し。ユーザーページにも対応。
精度を見たい人はこちらからどうぞ。

<script src="referer2ad.js" type="text/javascript"></script>
<script type="text/javascript">
referer2ad();
</script>
function referer2ad(){
  document.write('<div id="referer2ad"></div>');
  if (typeof(document.referrer)!='undefined') {
    var referer=document.referrer.split('/');
    if (referer[2]=='twitter.com') {
      if (referer[4]=='status' || referer[4]=='statuses') {
        var s=document.createElement('script');
        s.src='http://twitter.com/statuses/show/'+referer[5]+'.json?callback=r2aGetTweet';
        document.body.appendChild(s);
      }else if (referer.length==4 && referer[3]!='home') {
        var s=document.createElement('script');
        s.src='http://twitter.com/users/show/'+referer[3]+'.json?callback=r2aGetTweet';
        document.body.appendChild(s);
      }else{
        document.getElementById('referer2ad').innerHTML='<iframe src="http://rcm-jp.amazon.co.jp/e/cm?t=worris-22&o=9&p=12&l=bn1&mode=books-jp&browse=466298&fc1=000000&lt1=_blank&lc1=0000FF&bg1=E0FF92&f=ifr" marginwidth="0" marginheight="0" width="300" height="250" border="0" frameborder="0" style="border:none;" scrolling="no"></iframe>';
      }
    }else{
      document.getElementById('referer2ad').innerHTML='<iframe src="http://rcm-jp.amazon.co.jp/e/cm?t=worris-22&o=9&p=12&l=bn1&mode=books-jp&browse=466298&fc1=000000&lt1=_blank&lc1=0000FF&bg1=E0FF92&f=ifr" marginwidth="0" marginheight="0" width="300" height="250" border="0" frameborder="0" style="border:none;" scrolling="no"></iframe>';
    }
  }else{
  document.getElementById('referer2ad').innerHTML='<iframe src="http://rcm-jp.amazon.co.jp/e/cm?t=worris-22&o=9&p=12&l=bn1&mode=books-jp&browse=466298&fc1=000000&lt1=_blank&lc1=0000FF&bg1=E0FF92&f=ifr" marginwidth="0" marginheight="0" width="300" height="250" border="0" frameborder="0" style="border:none;" scrolling="no"></iframe>';
  }
}

function r2aGetTweet(data) {
  var s=document.createElement('script');
  s.src='http://jlp.yahooapis.jp/KeyphraseService/V1/extract?appid=apnJtpOxg65jfO8EeqccQdCbjtWKGjtyvMx_77sLl_4iebjXVpmSZEblhkEE&sentence='+encodeURIComponent((typeof(data['text'])=='undefined' ? data['status']['text'] : data['text']).replace('[B!]','').replace(/http:\/\/\S+/,''))+'&output=json&callback=r2aShowAd';
  document.body.appendChild(s);
}

function r2aShowAd(data) {
  var keyword='Twitter';
  for (var i in data) {
    if (data[i]==100) {keyword=i; break;}
  }
  document.getElementById("referer2ad").innerHTML='<iframe src="http://rcm-jp.amazon.co.jp/e/cm?t=worris-22&o=9&p=12&l=st1&mode=books-jp&search='+encodeURIComponent(keyword)+'&fc1=000000&lt1=_blank&lc1=0000FF&bg1=E0FF92&f=ifr" marginwidth="0" marginheight="0" width="300" height="250" border="0" frameborder="0" style="border:none;" scrolling="no"></iframe><br /><span style="font-size:10pt;">Keyword: '+keyword+'</span>';
}

Twitter で引用返信する Greasemonkey

上から目線のコメント付き非公式 RT がうざいので、ちゃんと reply させるために書こうとしたが上手く書けくて飽きた。

// ==UserScript==
// @name          Twitter QuoteReply
// @namespace     http://d.hatena.ne.jp/worris2/
// @description   Make replies with quoting in Twitter
// @include       http://twitter.com/*
// ==/UserScript==

(function() {

var replyButton = document.evaluate("//span[@class='reply']/a", document, null, XPathResult.ORDERED_NODE_SNAPSHOT_TYPE, null);
var replyText = document.evaluate("//span[@class='entry-content']", document, null, XPathResult.ORDERED_NODE_SNAPSHOT_TYPE, null);
for (i=0;i<replyButton.snapshotLength;i++){
  var newButton=document.createElement('a');
  newButton.setAttribute('href',replyButton.snapshotItem(i).getAttribute('href').replace('%20',':%20'+replyText.snapshotItem(i).innerHTML+'%20').replace('=','=%20QT%20'));
  newButton.innerHTML='(QT)';
  replyButton.snapshotItem(i).parentNode.parentNode.appendChild(newButton);
}
})();