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)!
!g; for my $image (@{ $item->{images} }) { # xxx this should be $entry->enclosures $body .= qq(
); } $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) 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 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 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 =head1 AUTHOR Tatsuhiko Miyagawa =head1 SEE ALSO L, L =cut