Notify::MSNMessenger

[追記 2006/06/01 22:17:43]
http://bonnu.heteml.jp/Plugin-Notify-MSNMessenger-0.03.tar.gz
メンバの不特定多数にスパムメッセージを送っていることが判明、
修正・・・orz


[追記 2006/06/01 21:04:20]
http://bonnu.heteml.jp/Plugin-Notify-MSNMessenger-0.02.tar.gz
templatize を エントリー単位で実行するよう修正しました。
概要を含めるとして、文字数制限を解決する上ではこれが一番楽。
Feed の差分のみ send message するようにすれば、案外丁度よい push かも・・・?


[追記]
すいません。色んな Feed で試してみると、正常に動かないものが多々あることに気付きました。
もうちょっと修正してまたUpします。


5月はコードを書けなかったので6月はコードを書く!
ということで Notify::MSNMessenger を暫定的にアップしてみる。


plugin/MSNMessenger.pm ※注意!以下のソースは古いです。

package Plagger::Plugin::Notify::MSNMessenger;

use strict;
use base qw(Plagger::Plugin);

use Digest::MD5 qw(md5_hex);
use Encode;
use Net::MSN;

our $VERSION = '0.01';

sub msn_client  {}
sub member_list {}

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

sub init {
    my $self = shift;
    $self->SUPER::init(@_);
    my $conf = $self->conf;
    $conf->{account}  or Plagger->context->error('account is required');
    $conf->{password} or Plagger->context->error('password is required');
    my $timeout = $conf->{action_timeout} || 10;
    $conf->{on_initial_timeout} ||= $timeout;
    $conf->{on_join_timeout}    ||= $timeout;
    if (my $member = $conf->{member}) {
        $conf->{member} = [ $member ] unless (ref $member);
    } else {
        Plagger->context->error('member(s) is required');
    }
}

sub initialize {
    my($self, $context) = @_;
    my $conf = $self->conf;
    unless (msn_client) {
        my $client   = Net::MSN->new;
        my %member   = map { $_ => 0 } @{ $conf->{member} };
        *msn_client  = sub { $client };
        *member_list = sub { \%member };
        msn_client->set_event(
            on_initial => sub {
                my ($msn, $chandle, $fname, $status) = @_;
                member_list->{$chandle} = 1;
            },
            on_join => sub {
                my ($msn, $chandle, $friendly) = @_;
                member_list->{$chandle} = 2;
            },
        );
        msn_client->connect($conf->{account}, $self->conf->{password});
    } else {
        map {
            member_list->{$_} == 2 && (member_list->{$_} = 1)
        } keys %{ &member_list };
    }

}

sub notify {
    my($self, $context, $args) = @_;
    my $conf = $self->conf;
    my $feed = $args->{feed};
    my $body = encode('utf-8', $self->templatize($context, $feed));
    eval {
        local $SIG{ALRM} = sub { die 'on_initial timeout' };
        alarm $conf->{on_initial_timeout};
        while (1) {
            msn_client->check_event;
            last unless (grep { $_ == 0 } values %{ &member_list });
        }
    };
    for my $member (keys %{ &member_list }) {
        msn_client->sendmsg($member, $body);
    }
    eval {
        local $SIG{ALRM} = sub { die 'on_join timeout' };
        alarm $conf->{on_join_timeout};
        while (1) {
            msn_client->check_event;
            last unless (grep { $_ <= 1 } values %{ &member_list });
        }
    };
}

sub templatize {
    my($self, $context, $feed) = @_;
    my $tt = $context->template();
    $tt->process('msn_notify.tt', {
        feed => $feed,
    }, \my $out) or $context->error($tt->error);
    $out;
}

sub finalize {
    my $self = shift;
    msn_client->disconnect();
}

 *Net::MSN::process_event = sub {
    my ($self, $this_self, $line, $fh) = @_; 
    my ($cmd, @data) = split(/ /, $line);
    return unless (defined $cmd && $cmd);
    if ($cmd eq 'VER') {
        $this_self->send(
            'CVR',
            '0x0409 ' . Net::MSN::OPERATING_SYSTEM .
            ' MSNMSGR ' . Net::MSN::MSN_VERSION .
            ' MSMSGS '.  $self->{'Handle'}
        );
    } elsif ($cmd eq 'CVR') {
        $this_self->send('USR', 'TWN I '. $self->{'Handle'});
    } elsif ($cmd eq 'USR') {
        if ($data[1] eq 'TWN' && $data[2] eq 'S') {
            my $key = $self->{_PassPort}->login(
                $self->{'Handle'}, $self->{'Password'}, $data[3]
            );
            die "Couldnt retrieve session key!" unless (defined $key);
            $this_self->send('USR', 'TWN S '. $key);      
        } elsif ($data[1] eq 'OK') {
            if ($this_self->{_Type} eq 'SB') {
                $this_self->{Connected} = 1;
                if (defined $this_self->{PendingCall} && $this_self->{PendingCall} == 1) {
                    $this_self->send('CAL', $this_self->{Handle});
                } 
            } else {
                $self->{'Handle'} = $data[2];
                $self->{'ScreenName'} = $self->normalize($data[3]);
                $this_self->send('SYN', '0');
                $this_self->send('CHG', 'NLN');
                ($self->if_callback_exists('on_connect')) and &{$self->{Callback}->{on_connect}}
            } 
        } else {
            die 'Unsupported authentication method: "', join(" ", @data), "\"\n";
        }
    } elsif ($cmd eq 'XFR') {
        if ($data[1] eq 'NS') {
            $self->cycle_socket(split(/:/, $data[2]));
            $self->send('VER', Net::MSN::MSN_PROTOCOL);
        } elsif ($data[1] eq 'SB') {
            if ($self->if_request_type_exists($data[0], 'XFR') &&
                exists $self->{Requests}->{$data[0]}->{Call} &&
                $self->{Requests}->{$data[0]}->{Call} == 1 &&
                exists $self->{Requests}->{$data[0]}->{Handle}
            ) {
                my ($h, undef) = split(/:/, $data[2]);
                $self->_connect_SB(
                    $self->{Requests}->{$data[0]}->{Handle}, $h, undef, $data[4], 'USR', undef, 1
                );
                $self->remove_request($data[0]);
            } else {
                $self->{_Log}( "Huh? Recieved XFR SB request, but there are no pending calls!", 1)
            }
        }
    } elsif ($cmd eq 'CHL') {
        my ($TrID, $key) = @data;
        my $md5 = md5_hex($key, 'Q1P7W2E4J9R8U3S5');
        $this_self->sendraw(
            'QRY',
            'msmsgs@msnmsgr.com ' . length($md5) . "\r\n". $md5
        );
    } elsif ($cmd eq 'QRY') {
        $this_self->sendnotrid('PNG');
    } elsif ($cmd eq 'PNG') {
    } elsif ($cmd eq 'CHG') {
        return;
    } elsif ($cmd eq 'SYN') {
        return;
    } elsif ($cmd eq 'JOI') {
        my ($chandle, $friendly) = @data;
        if ($self->if_callback_exists('on_join')) {
            if ($self->if_session_exists($chandle)) {
                &{$self->{Callback}->{on_join}}($this_self, $chandle, $friendly);
            } else {
                $self->{_Log}('#### WHY AM I HERE?! JOI W/OUT session ####', 1);
            }
        }
        if (defined $this_self->{PendingMsgs} && 
                $this_self->{PendingMsgs} == 1 && $self->have_pending_msgs($chandle)) {
            while (my $message = shift @{$Net::MSN::PendingMsgs{$chandle}}) {
                $this_self->sendmsg($message);
            }
            $this_self->{PendingMsgs} = 0;
        }
    } elsif ($cmd eq 'BYE') {
        my ($chandle) = @data;
        $self->_disconnect_SB($this_self);
        if ($self->if_callback_exists('on_bye_idle') && defined $data[1]) {
            &{$self->{Callback}->{on_bye_idle}}($chandle)
        }
        if ($self->if_callback_exists('on_bye_win') && ! defined $data[1]) {
            &{$self->{Callback}->{on_bye_win}}($chandle)
        }
        if ($self->if_callback_exists('on_bye')) {
            &{$self->{Callback}->{on_bye}}($chandle)
        }
    } elsif ($cmd eq 'CAL') {
        if (defined $this_self->{PendingCall} && $this_self->{PendingCall} == 1) {
            $this_self->{PendingCall} = 0;
        }
    } elsif ($cmd eq 'RNG') {
        my ($sid, $addr, undef, $key, $chandle, $cname) = @data;
        my ($h, undef) = split(/:/, $addr);
        $self->_connect_SB($chandle, $h, '', $key, 'ANS', $sid);
    } elsif ($cmd eq 'ANS') {
        my ($response) = @data;
        $this_self->{Connected} = 1;
        $self->if_callback_exists('on_answer') and
            &{$self->{Callback}->{on_answer}}($this_self, @data)
    } elsif ($cmd eq 'IRO') {
        if ($self->if_callback_exists('on_iro')) {
            &{$self->{Callback}->{on_iro}}($this_self, $data[3], $data[4]);
        }
    } elsif ($cmd eq 'MSG') {
        my ($chandle, $friendly, $length) = @data;
        my ($msg, $response) = ();
        $fh->read($msg, $length);
        unless ($msg =~ m{Content-Type: text/x-msmsgscontrol}s) {
            $msg = $self->normalize($self->stripheader($msg));
            $friendly = $self->normalize($friendly);
            if ($this_self->{_Type} eq 'SB') {
                if ($self->if_session_exists($chandle)) {
                    if ($self->if_callback_exists('on_message')) {
                        &{$self->{Callback}->{on_message}}(
                            $this_self, $chandle, $friendly, $msg
                        )
                    }
                } else {
                    $self->{_Log}('#### WHY AM I HERE?! MSG W/out session ####', 1);
                }
            }
        }
    } elsif ($cmd eq 'LST') {
        return unless ($data[1] eq 'FL');
        $self->buddyadd($data[5], $data[6]);
    } elsif ($cmd eq 'ILN') {
        my (undef, $status, $username, $fname) = @data;
        $self->buddyupdate($username, $fname, $status);
        if ($self->if_callback_exists('on_initial')) {
            &{$self->{Callback}->{on_initial}}($self, $username, $fname, $status)
        }
    } elsif ($cmd eq 'NLN') {
        my ($status, $username, $fname) = @data;
        $self->buddyupdate($username, $fname, $status);
        if ($self->if_callback_exists('on_standby')) {
            &{$self->{Callback}->{on_standby}}($self, $username, $fname, $status)
        }
    } elsif ($cmd eq 'FLN') {
        my ($username) = @data;
        $self->buddyupdate($username, undef, $cmd);
    } elsif ($cmd =~ /^[0-9]+$/) {
        if (defined $this_self->{PendingCall} && $this_self->{PendingCall} == 1) {
            $self->_disconnect_SB($this_self);
        }
        $self->{_Log}('ERROR: '. $self->converterror($cmd), 1);
    } elsif ($cmd eq 'ADD') {
        my (undef, $type, undef, $chandle, $friendly) = @data;
        if (defined $type && $type eq 'RL' && !$self->if_buddy_exists($chandle)) {
            if ($self->if_callback_exists('auth_add')) {
                if (&{$self->{Callback}->{auth_add}}($chandle, $friendly)) {
                    $self->buddyaddfl($chandle, $chandle);
                    $self->buddyaddal($chandle, $chandle);
                }
            } else {
                $self->buddyaddfl($chandle, $chandle);
                $self->buddyaddal($chandle, $chandle);
            }
        } 
    } elsif ($cmd eq 'REM') {
        my ($type, $chandle, $friendly) = @data[1, 3 .. 4];
        if (defined $type && $type eq 'RL') {
            $self->{_Log}($chandle. ' has removed us from their contact list', 3);
        } elsif (defined $type && $type eq 'FL') {
            $self->{_Log}('removing '. $chandle. ' from our contact list', 3);
            $self->remove_buddy($chandle);
        } elsif (defined $type && $type eq 'AL') { }
    } else {
        $self->{_Log}('RECIEVED UNKNOWN: '. $cmd. ' '. @data, 2);
    }
    return 1;
};

1;


assets/plugins/Notify-MSNMessenger/msn_notify.tt

[% FOREACH entry = feed.entries -%]
[% entry.title %]
[% SET link = entry.link || entry.id -%]
[% link %]
[% IF entry.author %]by [% entry.author %][% END %][% IF entry.tags.size %] on [% entry.tags.join(',') %][% END %]
[% UNLESS loop.last %]----[% END %]
[% END %]

ほとんど Publish::Gmail ...
Yappoさんのパッチを当てるために Net::MSN の process_event を上書きしてます。
あとイベントも一個二個*1増やした。。。本当はもっとうまいやり方があると思う。
ついでに言うと今のまんまだと全然バギーで、メッセンジャーの文字数制限を解決してませんけどたぶん今日中には解決するかと。


http://bonnu.heteml.jp/Plugin-Notify-MSNMessenger-0.01.tar.gz

*1:on_initial と on_standby. on_initial しか使ってない