Twitterのアイコンに自動で文字入れたりするPlagger

Plagger+ImageMagick+WWW::Mechanizeといったモジュールの組みあわせで, 「○○なう」とつぶやいたら,アイコン画像に「○○」を書き込んで自動で Twitterに送信するというPlaggerモジュールを書きました.ただ,あんまりやりすぎると Twitterから規制が入る予感もあるので,あまり行儀のいいものではありません.

Plagger::Plugin::Publish::Icontter

使い方

riywo20090225061218 後述のYAMLファイルみたいに設定したPlaggerを定期的(僕は2分おき)に 回していること前提で,自分のTwitterで

  • 「自宅なう」

と発言すれば,アイコンの下に「自宅」と書き込んで自動でアップロードしてくれます. その他にもソースを見てもらえばいろいろあることがわかりますし,いくらでも 追加できます. riywo20090225051029 riywo20090225064822

  • おれ爆発しろ
    • 爆発します
  • おはよう
    • 起床します
  • 帰宅
    • 帰宅します
  • おやすみ
    • 睡眠します
  • ダメだ
    • ダメ人間です
  • アイコン戻す
    • 元に戻します

メモ書き

いろいろBKが入ってますw まず,文字サイズの決定のために半角文字数相当を 計算するところでは,UTF-8なので文字数とバイト数を数えて足して2で割ってます. こんなんでいいんだろうかw

で,文字サイズの計算はマジックナンバー入れてます.自分の使ってるアイコン サイズ(368px × 394px)でいい感じのくらいです.使うなら適当に調整してください. こういうの苦手なので><

フォントのパスの指定は自分の環境に合わせて好きにやってください.

それから,Plaggerのフックポイントが大事で,publish.feedにフックしています. こうすることで,自作のPublish::Twtter2HatenaDiaryみたいにエントリ毎に 処理をしてほしいプラグインと一つのYAMLで使うことができます.つまり,

  • Feedでまとめて1処理ならpublish.feed
  • Entryごとに処理するならpublish.entry

というフックをさせれば良いようです.他にもPlaggerのフックは調べた方が良さそうだ.

その他,無名関数やList::Utilなどこれまで使ってなかったことをやったので いろいろ勉強になりました.また,ImageMagickは使いにくいとの噂でしたが, まぁそれなりに歴史があるのでPerlと同じでWEBに知識がたくさん落ちてるのが いい感じでした.いちいちドキュメント読まなくてもなんとなくまとめページを いくつか見てれば使えました.

lib/Plagger/Plugin/Publish/Icontter.pm

package Plagger::Plugin::Publish::Icontter;
use strict;
use base qw( Plagger::Plugin );

use Encode;
use WWW::Mechanize;
use Image::Magick;
use utf8;
use List::Util qw/max/;
use bytes ();
use DateTime;

sub register {
    my($self, $context) = @_;
    $context->register_hook(
        $self,
        'plugin.init' => \&initialize,
        'publish.feed' => \&publish_feed,
    );
}

sub initialize {
    my($self, $context) = @_;
    $self->{mech} = WWW::Mechanize->new;
    $self->{mech}->agent_alias('Linux Mozilla');
    $self->{mech}->get('http://twitter.com');
    $self->{mech}->submit_form(
        form_number => 2,
        fields => {
            'session[username_or_email]' => $self->conf->{username},
            'session[password]' => $self->conf->{password}});
    $self->{base_file} = $self->conf->{basefile};
    my $dt = DateTime->now(time_zone => 'local');
    my $date = $dt->strftime('%Y%m%d%H%M%S');
    $self->{upload_file} = $self->conf->{basefile};
    $self->{upload_file} =~ s/^(.+?)(\..{3})$/$1${date}$2/;
}

sub publish_feed {
    my($self, $context, $args) = @_;
    return unless $self->{mech};

    my @case = (
        {
            reg => '^アイコン戻|^アイコンもど',
            prc => sub{$self->{upload_file} = $self->{base_file};}
        },
        {
            reg => '^帰宅',
            prc => sub{$self->annotate($self->{base_file}, 'pink', '帰宅', 'しました');},
        },
        {
            reg => '^俺爆発|^おれ爆発',
            prc => sub{$self->annotate($self->{base_file}, 'red', '爆発');},
        },
        {
            reg => '^おはよう',
            prc => sub{$self->annotate($self->{base_file}, 'pink', '起床');},
        },
        {
            reg => '^おやすみ',
            prc => sub{$self->annotate($self->{base_file}, 'blue', '睡眠');},
        },
        {
            reg => '(.+?)なう[\..。!]{0,1}$',
            prc => sub{$self->annotate($self->{base_file}, 'white', $1);},
        },
        {
            reg => '^ダメだ',
            prc => sub{$self->swirl($self->{base_file});
                       $self->annotate($self->{upload_file}, 'yellow', 'ダメ', '人間');},
        }
    );

    $context->log(debug => "Icontter Search...");
    foreach my $entry (reverse $args->{feed}->entries){
        my $post = $entry->title_text;
        $post =~ s/@.+? //g;

        foreach my $check (@case){
            my $reg = $check->{reg};
            if($post =~ /${reg}/){
                $check->{prc}->();
                $self->{mech}->get('http://twitter.com/account/picture');
                $self->{mech}->submit_form(
                    form_number => 1,
                    fields => {'profile_image[uploaded_data]' => $self->{upload_file}}
                );
                $context->log(debug => "Upload " . $self->{upload_file});
                return;
            }
        }
    }
    $context->log(debug => "No Update");
}

sub annotate {
    my($self, $src, $f_color, @line) = @_;
    my $image = Image::Magick->new;
    $image->Read($src);
    my $max_length = max (map {(length($_) + bytes::length($_))/2} @line)/2;
    $" = '\n';
    my $text = "@line";

    my $sazanami = '/usr/share/fonts/truetype/sazanami/sazanami-gothic.ttf';
    my $mona = '/usr/share/fonts/truetype/mona/mona.ttf';
    my $kochi = '/usr/share/fonts/truetype/kochi/kochi-gothic.ttf';
    my $font = $kochi;

    my ($width, $height) = $image->Get('width', 'height');
    my $pointsize = int(($width-30)/$max_length);
    my $y = $height-30-($pointsize*$#line);
    my $b_color = 'black';
    my $f_width = int((7/90)*$pointsize);
    my $b_width = int((14/90)*$pointsize);
    $image->Annotate(text => $text, stroke => $b_color, fill => $b_color,
                     font => $font, pointsize => $pointsize, strokewidth => $b_width,
                     x => 15, y => $y, encoding =>'UTF-8');
    $image->Annotate(text => $text, stroke => $f_color, fill => $f_color,
                     font => $font, pointsize => $pointsize, strokewidth => $f_width,
                     x => 15, y => $y, encoding =>'UTF-8');
    $image->Write($self->{upload_file});

    undef $image;
}

sub swirl {
    my($self, $src) = @_;
    my $image = Image::Magick->new;
    $image->Read($src);
    $image->Swirl(degrees => 400);
    $image->Write($self->{upload_file});
    undef $image;
}

sub copy {
    my($self, $src) = @_;
    my $image = Image::Magick->new;
    $image->Read($src);
    $image->Write($self->{upload_file});
    undef $image;
}

1;

icontter.yaml

レシピはこんな感じです.いつも通りのbase.yamlを読んだあと, TwitterのRSSを読んで,Dedupedで一度処理した奴を処理しないようにします. ちなみにここではDB_File_URLというのを使っています.

日付を消すというハックもあるようですが,後で日付使うので無しで, TwitterのWEBからスクレイプする場合と相互にDBを使えるようにするために 一応入れてますが,まだそんなことやってませんw

Filter::RemoveIDFromTwitterRSSというフィルタは,単にTwitterのRSSから 発言者IDを削除して,Post内容にする(+改行を削除)だけのFilterです. 一応ソースも下の方に載せておきます.これで,EntryのTitleとBodyに 発言内容が入った状態なので,IcontterやTwitter2HatenaDiaryに流せます (Twitter2HatenaDiaryからFilter分は削除してます).

アイコン変えたいだけなら,はてダ投稿部分はまるまる消せばOK. あとはcronで定期的に回してあげるだけ.わーい

ちなみに食べるフィードをTwitterSearchで検索した自分向けの 返信とかにすると,誰かに「爆発しろ」と言われたら爆発する,とかも 可能ですが,これと同時に走らせると多分アップロードの部分で なんかおかしなことが起こるっぽいので,僕はやってません.単独なら 大丈夫なはず.この辺,Ruleとか使えば一つのYAMLでできると 思ったのですが,Ruleの使い方がついに良くわからず断念しましたw

include:
  - /home/user/yaml/base.yaml

plugins:
  - module: Subscription::Config
    config:
      feed:
        - http://twitter.com/statuses/user_timeline/6078772.rss

  - module: Filter::Rule
    rule:
      module: Deduped
      engine: DB_File_URL
      path: /home/user/Dropbox/cache/plagger-mytwitter.db
  - module: Filter::Reverse
  - module: Filter::RemoveIDFromTwitterRSS

  - module: Publish::Icontter
    config:
      basefile: /home/user/riywo.png
      username: user
      password: ********

  - module: Publish::Twitter2HatenaDiary
    config:
      username: user
      password: ********

Plagger::Plugin::Filter::RemoveIDFromTwitterRSS

lib/Plagger/Plugin/Filter/RemoveIDFromTwitterRSS.pm

package Plagger::Plugin::Filter::RemoveIDFromTwitterRSS;
use strict;
use base qw( Plagger::Plugin );

use Encode;

use utf8;
use DateTime;
use DateTime::Format::Strptime;
use DateTime::Format::HTTP;

sub register {
    my ($self, $context) = @_;
    $context->register_hook(
        $self,
        'update.entry.fixup' => \&filter,
    );
}

sub filter {
    my ($self, $context, $args) = @_;
    my $entry_title = $args->{entry}->title_text;
    $entry_title =~ s/\r|\n//g;
    $entry_title =~ s/^.+?: //o;

    $args->{entry}->title($entry_title);
    $args->{entry}->body($entry_title);
}

1;

Twitterのアイコン画像について

Twitterのアイコンは現在Amazonのクラウドを使っています.ランダム?な 数字の入ったURLであり,またローカルのファイル名がそのまま使われます.

で,同じファイル名だとなんか割とおかしなことが発生しがちだったので アップロードするファイル名に日付を付加しています.

何がよく発生するかというと,オリジナルファイルはきちんとアップロード できているのに,サムネイルが変更されないことですね.サムネイルの URLが更新前の画像のままだったり,新しいのは作成されなかったり, なんか結構不安定でした.

まぁ頻繁に変更しなければ,それなりに期待通り動作してくれるようなので しばらく回しておきます.

ちなみに,モバツイなどのクライアントではアイコンを結構長くキャッシュしている ので変更が反映されませんが,そんなの僕にはどうにもできませんw

おわりに

募集するのは以下の2つ

  • おもしろコマンド
    • 文字だけじゃなくて,画像処理もできるのでなんかあれば.
      • とりあえず「クリスマスだ」と言ったらクリスマス仕様になる,くらいは書きます.
  • Perlの間違いなど
    • 突貫工事で書いてるのでひどいソースです.Perl界隈のエロい人,直してw

以上おわり.おもしろかった.

otsune 09-02-25 (水) 18:53

これと同時に走らせると多分アップロードの部分で なんかおかしなことが起こるっぽいので,僕はやってません.単独なら 大丈夫なはず.この辺,Ruleとか使えば一つのYAMLでできると 思ったのですが,Ruleの使い方がついに良くわからず断念しましたw

plaggerのプラグイン一つに何でもかんでも全機能を詰め込むのではなく、「Twitterアイコンを投稿するPublish::TwitterIcon」と「画像に文字を埋め込むFilter::Imageなんちゃら」と分けて書けば、あとはRuleなりyamlなりで対処できるのかなぁ。と思いました。

riywo 09-02-25 (水) 23:51

>otsuneさん

さすがotsuneさん,というか,なんか前も同じ様なことを
言われた気がしますw

確かに一つのプラグインに詰め込みすぎると後で苦労しますね.
参考にさせて頂きます,ありがとうございました.