The Perl Toolchain Summit needs more sponsors. If your company depends on Perl, please support this very important event.
package Plagger::Plugin::CustomFeed::Mixi;
use strict;
use base qw( Plagger::Plugin );

use DateTime::Format::Strptime;
use Encode;
use WWW::Mixi;
use Time::HiRes;
use URI;

our $MAP = {
    FriendDiary => {
        start_url  => 'http://mixi.jp/new_friend_diary.pl',
        title      => 'マイミク最新日記',
        get_list   => 'parse_new_friend_diary',
        get_detail => 'get_view_diary',
        icon_re    => qr/owner_id=(\d+)/,
    },
    # can't get icon
    Message => {
        start_url  => 'http://mixi.jp/list_message.pl',
        title      => 'ミクシィメッセージ受信箱',
        get_list   => 'parse_list_message',
        get_detail => 'get_view_message',
    },
    # can't get icon & body
    RecentComment => {
        start_url  => 'http://mixi.jp/list_comment.pl',
        title      => 'ミクシィ最近のコメント一覧',
        get_list   => 'parse_list_comment',
    },
    Log => {
        start_url => 'http://mixi.jp/show_log.pl',
        title     => 'ミクシィ足跡',
        get_list => 'parse_show_log',
        icon_re => qr/[^_]id=(\d+)/,
    }, 
};

sub plugin_id {
    my $self = shift;
    $self->class_id . '-' . $self->conf->{email};
}

sub register {
    my($self, $context) = @_;
    $context->register_hook(
        $self,
        'subscription.load' => \&load,
    );
}

sub load {
    my($self, $context) = @_;

    my $cookie_jar = $self->cookie_jar;
    if (ref($cookie_jar) ne 'HTTP::Cookies') {
        # using foreign cookies = don't have to set email/password. Fake them
        $self->conf->{email}    ||= 'plagger@localhost';
        $self->conf->{password} ||= 'pl4gg5r';
    }

    $self->{mixi} = WWW::Mixi->new($self->conf->{email}, $self->conf->{password});
    $self->{mixi}->cookie_jar($cookie_jar);

    my $feed = Plagger::Feed->new;
       $feed->aggregator(sub { $self->aggregate(@_) });
    $context->subscription->add($feed);
}

sub aggregate {
    my($self, $context, $args) = @_;
    for my $type (@{$self->conf->{feed_type} || ['FriendDiary']}) {
        $context->error("$type not found") unless $MAP->{$type};
        $self->aggregate_feed($context, $type, $args);
    }
}
sub aggregate_feed {
    my($self, $context, $type, $args) = @_;

    my $start_url = $MAP->{$type}->{start_url};
    my $response  = $self->{mixi}->get($start_url);

    my $next_url = URI->new($start_url)->path;

    if ($response->content =~ /action=login\.pl/) {
        $context->log(debug => "Cookie not found. Logging in");

        if ($self->conf->{email} eq 'plagger@localhost') {
            $context->log(error => 'email/password should be set to login');
        }

        $response = $self->{mixi}->post("http://mixi.jp/login.pl", {
            next_url => $next_url,
            email    => $self->conf->{email},
            password => $self->conf->{password},
            sticky   => 'on',
        });
        if (!$response->is_success || $response->content =~ /action=login\.pl/) {
            $context->log(error => "Login failed.");
            return;
        }

        # meta refresh, ugh!
        if ($response->content =~ m!"0;url=(.*?)"!) {
            $response = $self->{mixi}->get($1);
        }
    }

    my $feed = Plagger::Feed->new;
    $feed->type('mixi');
    $feed->title($MAP->{$type}->{title});
    $feed->link($MAP->{$type}->{start_url});

    my $format = DateTime::Format::Strptime->new(pattern => '%Y/%m/%d %H:%M');

    my $meth = $MAP->{$type}->{get_list};
    my @msgs = $self->{mixi}->$meth($response);
    my $items = $self->conf->{fetch_items} || 20;
    $self->log(info => 'fetch ' . scalar(@msgs) . ' entries');

    my $i = 0;
    my $blocked = 0;
    for my $msg (@msgs) {
        next if $type eq 'FriendDiary' and not $msg->{image}; # external blog
        last if $i++ >= $items;

        my $entry = Plagger::Entry->new;
        $entry->title( decode('euc-jp', $msg->{subject}) );
        $entry->link($msg->{link});
        $entry->author( decode('euc-jp', $msg->{name}) );
        $entry->date( Plagger::Date->parse($format, $msg->{time}) );

        if ($self->conf->{show_icon} && !$blocked && defined $MAP->{$type}->{icon_re}) {
            my $owner_id = ($msg->{link} =~ $MAP->{$type}->{icon_re})[0];
            my $link = "http://mixi.jp/show_friend.pl?id=$owner_id";
            $context->log(info => "Fetch icon from $link");

            my $item = $self->cache->get_callback(
                "outline-$owner_id",
                sub {
                    Time::HiRes::sleep( $self->conf->{fetch_body_interval} || 1.5 );
                    my($item) = $self->{mixi}->get_show_friend_outline($link);
                    $item;
                },
                '12 hours',
            );
            if ($item && $item->{image} !~ /no_photo/) {
                # prefer smaller image
                my $image = $item->{image};
                   $image =~ s/\.jpg$/s.jpg/;
                $entry->icon({
                    title => decode('euc-jp', $item->{name}),
                    url   => $image,
                    link  => $link,
                });
            }
        }

        if ($self->conf->{fetch_body} && !$blocked && $msg->{link} =~ /view_/ && defined $MAP->{$type}->{get_detail}) {
            $context->log(info => "Fetch body from $msg->{link}");
            my $item = $self->cache->get_callback(
                "item-$msg->{link}",
                sub {
                    Time::HiRes::sleep( $self->conf->{fetch_body_interval} || 1.5 );
                    my $meth = $MAP->{$type}->{get_detail};
                    my($item) = $self->{mixi}->$meth($msg->{link});
                    $item;
                },
                '12 hours',
            );
            if ($item) {
                my $body = decode('euc-jp', $item->{description});
                   $body =~ s!(\r\n?|\n)!<br />!g;
                for my $image (@{ $item->{images} }) {
                    # xxx this should be $entry->enclosures
                    $body .= qq(<div><a href="$image->{link}"><img src="$image->{thumb_link}" style="border:0" /></a></div>);
                }
                $entry->body($body);

                $entry->date( Plagger::Date->parse($format, $item->{time}) );
            } else {
                $context->log(warn => "Fetch body failed. You might be blocked?");
                $blocked++;
            }
        }

        $feed->add_entry($entry);
    }

    $context->update->add($feed);
}

1;

__END__

=head1 NAME

Plagger::Plugin::CustomFeed::Mixi -  Custom feed for mixi.jp

=head1 SYNOPSIS

    - module: CustomFeed::Mixi
      config:
        email: email@example.com
        password: password
        fetch_body: 1
        show_icon: 1
        feed_type:
          - RecentComment
          - FriendDiary
          - Message

=head1 DESCRIPTION

This plugin fetches your friends diary updates from mixi
(L<http://mixi.jp/>) and creates a custom feed.

=head1 CONFIGURATION

=over 4

=item email, password

Credential you need to login to mixi.jp.

Note that you don't have to supply email and password if you set
global cookie_jar in your configuration file and the cookie_jar
contains a valid login session there, such as:

  global:
    user_agent:
      cookies: /path/to/cookies.txt

See L<Plagger::Cookies> for details.

=item fetch_body

With this option set, this plugin fetches entry body HTML, not just a
link to the entry. Defaults to 0.

=item fetch_body_interval

With C<fetch_body> option set, your Plagger script is recommended to
wait for a little, to avoid mixi.jp throttling. Defaults to 1.5.

=item show_icon: 1

With this option set, this plugin fetches users buddy icon from
mixi.jp site, which makes the output HTML very user-friendly.

=item feed_type

With this option set, you can set the feed types.

Now supports: RecentComment, FriendDiary, and Message.

Default: FriendDiary.

=back

=head1 SCREENSHOT

L<http://blog.bulknews.net/mt/archives/plagger-mixi-icon.gif>

=head1 AUTHOR

Tatsuhiko Miyagawa

=head1 SEE ALSO

L<Plagger>, L<WWW::Mixi>

=cut