先日から使い始めた自分専用のTwitter”投稿のみ”クライアントをさらに改良してみました。そもそもどういう経緯でこれを作ろうと思ったのかというと、、、
- Twitterジャンキーな自分を改善したかった(笑)
- でもいきなり使わないとか、アカウント消すとか過激なことはしたくない
- そもそもTwitter自体は好き。
- 元凶はTLを眺めてると常に面白い。ということ
- なのでTLを見ずに呟くだけにしたい
というものでした。始めはOSX用投稿に特化した(閲覧はGrowl経由)クライアントがあったのでそれを使うつもりでしたが、どうやらちゃんと動いてくれない。なので、折角なので、と自作してみることにしたのですが、いきなりOSXのクライアントを作るというよりも、お手軽にTerminalで作れないかと始めたわけです。
ここまで機能としては、、、
- Twitterに投稿できる
- bit.lyに対応してる
という機能を実装していますが、これだと結局「@」や「DM」を見逃がしたり、確認するのにechofonを起動したり(普段はechofonをつかっていました)と、terminalの外でTwitter関連のことをやらないとならなくなるので今回は次の機能を実装することに。
- 「@」が来てるか $ tw @ で確認できるようにする
- 「DM」が来てるか $tw d で確認できるようにする
- 「@」と「DM」を両方確認するには$ tw のみ
- ついでにTerminalからechofonを起動できるようにする(@が来てるときやDM自体はechofonで対応することにします)。
では、いきなりスクリプトです。
#!/usr/bin/perl
use strict;
use warnings;
use Encode;
use utf8;
use Net::Twitter::Lite;
use LWP::Simple;
use Data::Dumper;
my $script_dir = "このスクリプトの置き先を絶対パスで";
my %last_id;
my $tw = Net::Twitter::Lite->new(
consumer_key => '自分のに置き換えてね。',
consumer_secret => '自分のに置き換えてね。',
ssl => 1,
);
$tw->access_token('自分のに置き換えてね。);
$tw->access_token_secret('自分のに置き換えてね。');
sub convert_bitly{
my ($string) = @_;
#encode_utf8($string);
my $login = '自分のに置き換えてね。';
my $apikey = '自分のに置き換えてね。';
my $apiurl = 'http://api.bit.ly/v3/shorten?login=%s&apiKey=%s&longUrl=%s&format=txt';
my $http_url_regex =
q{\b(?:https?|shttp)://(?:(?:[-_.!~*'()a-zA-Z0-9;:&=+$,]|%[0-9A-Fa-f} .
q{][0-9A-Fa-f])*@)?(?:(?:[a-zA-Z0-9](?:[-a-zA-Z0-9]*[a-zA-Z0-9])?\.)} .
q{*[a-zA-Z](?:[-a-zA-Z0-9]*[a-zA-Z0-9])?\.?|[0-9]+\.[0-9]+\.[0-9]+\.} .
q{[0-9]+)(?::[0-9]*)?(?:/(?:[-_.!~*'()a-zA-Z0-9:@&=+$,]|%[0-9A-Fa-f]} .
q{[0-9A-Fa-f])*(?:;(?:[-_.!~*'()a-zA-Z0-9:@&=+$,]|%[0-9A-Fa-f][0-9A-} .
q{Fa-f])*)*(?:/(?:[-_.!~*'()a-zA-Z0-9:@&=+$,]|%[0-9A-Fa-f][0-9A-Fa-f} .
q{])*(?:;(?:[-_.!~*'()a-zA-Z0-9:@&=+$,]|%[0-9A-Fa-f][0-9A-Fa-f])*)*)} .
q{*)?(?:\?(?:[-_.!~*'()a-zA-Z0-9;/?:@&=+$,]|%[0-9A-Fa-f][0-9A-Fa-f])} .
q{*)?(?:#(?:[-_.!~*'()a-zA-Z0-9;/?:@&=+$,]|%[0-9A-Fa-f][0-9A-Fa-f])*} .
q{)?};
my @http_url = $string =~ /($http_url_regex)/g;
foreach my $longurl (@http_url){
my $bitly = sprintf($apiurl, $login, $apikey, $longurl);
my $bitlytxt = get($bitly);
chomp($bitlytxt);
$string =~ s/$longurl/$bitlytxt/g;
}
#return decode_utf8($string);
return $string;
}
sub check_mention {
my $number;
dbmopen(%last_id, $script_dir.'twcheck_last_id', 0666) or die $!;
if($last_id{'mention'}){
my $statuses = $tw->mentions({since_id => $last_id{'mention'}});
$number = @$statuses;
}else{
$last_id{'mention'} = 1;
my $statuses = $tw->mentions({since_id => $last_id{'mention'}});
$number = @$statuses;
}
my $last_status = $tw->mentions({since_id => $last_id{'mention'}, count => 1});
for my $status (@$last_status){
$last_id{'mention'} = $status->{id};
}
dbmclose(%last_id);
return $number;
}
sub check_dm {
my $number;
dbmopen(%last_id, $script_dir.'twcheck_last_id', 0666) or die $!;
if($last_id{'dm'}){
my $statuses = $tw->direct_messages({since_id => $last_id{'dm'}});
$number = @$statuses;
}else{
$last_id{'dm'} = 1;
my $statuses = $tw->direct_messages({since_id => $last_id{'dm'}});
$number = @$statuses;
}
my $last_status = $tw->direct_messages({since_id => $last_id{'dm'}, count => 1});
for my $status (@$last_status){
$last_id{'dm'} = $status->{id};
}
dbmclose(%last_id);
return $number;
}
if(@ARGV){
for(@ARGV){
my $string = decode_utf8($_);
if($string eq "@"){
my $mention = check_mention();
print '@:'.$mention."\n";
exit;
}elsif($string eq "d"){
my $dm = check_dm();
print 'DM:'.$dm."\n";
exit;
}elsif($string eq "e"){
my $cmd = `open /Applications/Echofon.app`;
print $cmd;
exit;
}
$string = convert_bitly($string);
if( length($string) > 140 ){
print "over 140";
}else{
$tw->update($string);
}
}
}else{
my $mention = check_mention();
my $dm = check_dm();
print '@:'.$mention."\n";
print 'DM:'.$dm."\n";
exit;
}
exit
__END__
あまり特に詳しい解説をしてこなったのですが、ちょっとソースの量が増え始めたので簡単になにをやってるか? を解説していきたいとおもいます。基本的に、初心者向きにだが内容は前回の違いと自分的に気になったところだけ。ですので、興味のある人は全体のスクリプトとパズルを解くような気分で見比べながら考えてもらえたら良いかなとおもいます。
my $script_dir = "このスクリプトの置き先を絶対パスで";
my %last_id;
my $tw = Net::Twitter::Lite->new(
consumer_key => '自分のに置き換えてね。',
consumer_secret => '自分のに置き換えてね。',
ssl => 1,
);
上から。まず今回は@やDMの数を取得するのですが、そのときにその時点で取得した最新のTweetのIDを記憶させておかないと、次回に数を取得したときに差分がわかりません。で、その為にdbmを使うことにしたのですが、そのdbmのファイルを何処に生成するか? で、dbmopenにファイルの絶対パスなりを書いておいてもいいのですが、そういうのは使いまわすだろう。他の人がつかったときには変更するんだしね、、というわけで、$script_dir変数に入れてしまっています。次に、そのdbmopenの為のハッシュを用意します。最後は、Net::Twitter::Liteのインスタンスを生成しています。
$tw->access_token('自分のに置き換えてね。);
$tw->access_token_secret('自分のに置き換えてね。');
Twitterのサイトで取得したアクセストークンをインスタンスにあたえて、Twitterにアクセスできるようにします。
sub convert_bitly{
my ($string) = @_;
#encode_utf8($string);
my $login = '自分のに置き換えてね。';
my $apikey = '自分のに置き換えてね。';
my $apiurl = 'http://api.bit.ly/v3/shorten?login=%s&apiKey=%s&longUrl=%s&format=txt';
my $http_url_regex =
q{\b(?:https?|shttp)://(?:(?:[-_.!~*'()a-zA-Z0-9;:&=+$,]|%[0-9A-Fa-f} .
q{][0-9A-Fa-f])*@)?(?:(?:[a-zA-Z0-9](?:[-a-zA-Z0-9]*[a-zA-Z0-9])?\.)} .
q{*[a-zA-Z](?:[-a-zA-Z0-9]*[a-zA-Z0-9])?\.?|[0-9]+\.[0-9]+\.[0-9]+\.} .
q{[0-9]+)(?::[0-9]*)?(?:/(?:[-_.!~*'()a-zA-Z0-9:@&=+$,]|%[0-9A-Fa-f]} .
q{[0-9A-Fa-f])*(?:;(?:[-_.!~*'()a-zA-Z0-9:@&=+$,]|%[0-9A-Fa-f][0-9A-} .
q{Fa-f])*)*(?:/(?:[-_.!~*'()a-zA-Z0-9:@&=+$,]|%[0-9A-Fa-f][0-9A-Fa-f} .
q{])*(?:;(?:[-_.!~*'()a-zA-Z0-9:@&=+$,]|%[0-9A-Fa-f][0-9A-Fa-f])*)*)} .
q{*)?(?:\?(?:[-_.!~*'()a-zA-Z0-9;/?:@&=+$,]|%[0-9A-Fa-f][0-9A-Fa-f])} .
q{*)?(?:#(?:[-_.!~*'()a-zA-Z0-9;/?:@&=+$,]|%[0-9A-Fa-f][0-9A-Fa-f])*} .
q{)?};
my @http_url = $string =~ /($http_url_regex)/g;
foreach my $longurl (@http_url){
my $bitly = sprintf($apiurl, $login, $apikey, $longurl);
my $bitlytxt = get($bitly);
chomp($bitlytxt);
$string =~ s/$longurl/$bitlytxt/g;
}
#return decode_utf8($string);
return $string;
}
ここではbit.lyをつかってTweetの中のURLを短縮URLにしています。$login ではbit.lyのログイン名、$apikeyではbit.lyのAPIキーを、$apiurlにはRESTで呼ぶURLを用意しています。つぎに、$http_url_regexにはTweetの中のURLを取りだす為の正規表現を格納します。その後 $string =~ /($http_url_regex)/g; 正規表現で取り出したURLを配列@http_urlに格納します。
格納した配列からURLをforeachで取りだして、foreach内の$bitlyにsprintf()でREST URLを作成し、get(bitly)でシンプルにbit.lyからの戻り値をテキストで取得します。このときにテキストで取得でいるのは、$apiurlで用意したREST URLの雛形の中でformat=txtとしているからです。その戻り値から改行コードをchomp()で取って、最後に先程取り出しているURLと $string =~ s/$longurl/$bitlytxt/g; と置換え行っています。
sub check_mention {
my $number;
dbmopen(%last_id, $script_dir.'twcheck_last_id', 0666) or die $!;
if($last_id{'mention'}){
my $statuses = $tw->mentions({since_id => $last_id{'mention'}});
$number = @$statuses;
}else{
$last_id{'mention'} = 1;
my $statuses = $tw->mentions({since_id => $last_id{'mention'}});
$number = @$statuses;
}
my $last_status = $tw->mentions({since_id => $last_id{'mention'}, count => 1});
for my $status (@$last_status){
$last_id{'mention'} = $status->{id};
}
dbmclose(%last_id);
return $number;
}
ここではTwitterのmentionsの数(「@」の数)をチェックするサブルーチンを用意しています。dbmopenの中では先に設定してる$script_dirを利用しています。dbmopen 〜 dbmcloseの中で、mentionの取得と最新のmentionのIDの取得をおこなっています。
ifの中では、dbmの中にハッシュの形で格納されているkey=mentionと対応したlDがある場合とない場合で切り分けています。ない場合は $last_id{‘mention’} = 1; で初期値を設定しておきます。mentionの取得の数自体はdefaultの20ですが、 count=>100 などとすると、数が増やせます。それを利用して、ifの外では、count=>1 とすることで最新の一つを取得し、ここから最後にチェックしたときの最新のIDをdbmに格納しています。
また、$tw->mentions で取得したTwitterからの戻り値は配列のリファレンストとして戻ってくるので、それをいったん $変数名 にそれぞれ格納して @$変数名 でデリファレンスしてから、配列の数をカウントしたり、中からIDを取り出したりしています。デリファレンスしないと、配列の数は1になってしまい、IDの取り出しをすることもできません。
sub check_dm {
my $number;
dbmopen(%last_id, $script_dir.'twcheck_last_id', 0666) or die $!;
if($last_id{'dm'}){
my $statuses = $tw->direct_messages({since_id => $last_id{'dm'}});
$number = @$statuses;
}else{
$last_id{'dm'} = 1;
my $statuses = $tw->direct_messages({since_id => $last_id{'dm'}});
$number = @$statuses;
}
my $last_status = $tw->direct_messages({since_id => $last_id{'dm'}, count => 1});
for my $status (@$last_status){
$last_id{'dm'} = $status->{id};
}
dbmclose(%last_id);
return $number;
}
ここはDMの数を取得しています。内容の違いは上と見比べてみましょう。
また、Net::Twitter::Liteの使いかたを調べてみると他にもいろいろな機能があり同じような形で利用できるので、夢が広がるかもしれませんね(笑)。
if(@ARGV){
for(@ARGV){
my $string = decode_utf8($_);
if($string eq "@"){
my $mention = check_mention();
print '@:'.$mention."\n";
exit;
}elsif($string eq "d"){
my $dm = check_dm();
print 'DM:'.$dm."\n";
exit;
}elsif($string eq "e"){
my $cmd = `open /Applications/Echofon.app`;
print $cmd;
exit;
}
$string = convert_bitly($string);
if( length($string) > 140 ){
print "over 140";
}else{
$tw->update($string);
}
}
}else{
my $mention = check_mention();
my $dm = check_dm();
print '@:'.$mention."\n";
print 'DM:'.$dm."\n";
exit;
}
最後はこのプログラムの実行部分です。上記に設定したサブルーチンを条件分岐でそれぞれ呼びだしています。elsif($string eq “e”) { } のところでは、terminalで $ tw e の形でechofonを呼びだすところを実装しています。openコマンドをprintで実行してるだけです。簡単ですね。ただ、コマンドはシングルクォートではなく、バッククォートで囲まないと実行されないのでお待ちがえなく。
如何でしたか? 自分の理解の範囲でがんばって説明してみました(笑)。
こういう風に一旦動かしたものを解説していく、っていう作業自体もプログラムを学ぶのには非常に役にたちます(特に記憶の定着に・笑)。もしお役に立てたら幸いですが、理解が間違っているところがあれば是非ご指摘などしていただければ幸いです。
というわけで、一旦このスクリプトは落ち着いた、、、と思うんですが、他に実装していみたい機能としては、
- @でtweetするときにインクリメンタルに自分のフォローしてる人のユーザー名の候補を出したい
- @やDMが見付かったときに、$ tw show @などとして、読めるようにしたい。
- コマンドが増えたので、Tweet自体はかならずシングルクォートで囲んでいることを条件にしたい。
- 拡張しすぎたので、そろそろOSXなりのクライアントとして実装したい(笑・本末が転倒してる!!!)
というあたりを考えています。インクリメンタルに候補をだすっていうのはTerminarl上でどうやって表示したら良いのか? まったくアイデアがないのでヒントがあれば是非コメントなり@なりでお知らせください。僕のアカウントは @yamato です。

2010年 08月 19日 → 11:39 am @ 大和 比呂志
0