1package Plagger::Plugin::CustomFeed::Mixi; 2use strict; 3use base qw( Plagger::Plugin ); 4 5use DateTime::Format::Strptime; 6use Encode; 7use WWW::Mixi; 8use Time::HiRes; 9use URI; 10 11our $MAP = { 12 FriendDiary => { 13 start_url => 'http://mixi.jp/new_friend_diary.pl', 14 title => 'マイミク最新日記', 15 get_list => 'parse_new_friend_diary', 16 get_detail => 'get_view_diary', 17 icon_re => qr/owner_id=(\d+)/, 18 }, 19 # can't get icon 20 Message => { 21 start_url => 'http://mixi.jp/list_message.pl', 22 title => 'ミクシィメッセージ受信箱', 23 get_list => 'parse_list_message', 24 get_detail => 'get_view_message', 25 }, 26 # can't get icon & body 27 RecentComment => { 28 start_url => 'http://mixi.jp/list_comment.pl', 29 title => 'ミクシィ最近のコメント一覧', 30 get_list => 'parse_list_comment', 31 }, 32 Log => { 33 start_url => 'http://mixi.jp/show_log.pl', 34 title => 'ミクシィ足跡', 35 get_list => 'parse_show_log', 36 icon_re => qr/[^_]id=(\d+)/, 37 }, 38 MyDiary => { 39 start_url => 'http://mixi.jp/list_diary.pl', 40 title => 'ミクシィ日記', 41 get_list => 'parse_list_diary', 42 get_detail => 'get_view_diary', 43 icon_re => qr/owner_id=(\d+)/, 44 }, 45 Calendar => { 46 start_url => 'http://mixi.jp/show_calendar.pl', 47 title => 'ミクシィカレンダー', 48 get_list => 'parse_show_calendar', 49 get_detail => 'get_view_event', 50 }, 51}; 52 53sub plugin_id { 54 my $self = shift; 55 $self->class_id . '-' . $self->conf->{email}; 56} 57 58sub register { 59 my($self, $context) = @_; 60 $context->register_hook( 61 $self, 62 'subscription.load' => \&load, 63 ); 64} 65 66sub load { 67 my($self, $context) = @_; 68 69 my $cookie_jar = $self->cookie_jar; 70 if (ref($cookie_jar) ne 'HTTP::Cookies') { 71 # using foreign cookies = don't have to set email/password. Fake them 72 $self->conf->{email} ||= 'plagger@localhost'; 73 $self->conf->{password} ||= 'pl4gg5r'; 74 } 75 76 $self->{mixi} = WWW::Mixi->new($self->conf->{email}, $self->conf->{password}); 77 $self->{mixi}->cookie_jar($cookie_jar); 78 79 my $feed = Plagger::Feed->new; 80 $feed->aggregator(sub { $self->aggregate(@_) }); 81 $context->subscription->add($feed); 82} 83 84sub aggregate { 85 my($self, $context, $args) = @_; 86 for my $type (@{$self->conf->{feed_type} || ['FriendDiary']}) { 87 $context->error("$type not found") unless $MAP->{$type}; 88 $self->aggregate_feed($context, $type, $args); 89 } 90} 91sub aggregate_feed { 92 my($self, $context, $type, $args) = @_; 93 94 my $start_url = $MAP->{$type}->{start_url}; 95 my $response = $self->{mixi}->get($start_url); 96 97 my $next_url = URI->new($start_url)->path; 98 99 if ($response->content =~ /action="login\.pl"/) { 100 $context->log(debug => "Cookie not found. Logging in"); 101 102 if ($self->conf->{email} eq 'plagger@localhost') { 103 $context->log(error => 'email/password should be set to login'); 104 } 105 106 $response = $self->{mixi}->post("http://mixi.jp/login.pl", { 107 next_url => $next_url, 108 email => $self->conf->{email}, 109 password => $self->conf->{password}, 110 sticky => 'on', 111 }); 112 if (!$response->is_success || $response->content =~ /action=login\.pl/) { 113 $context->log(error => "Login failed."); 114 return; 115 } 116 117 # meta refresh, ugh! 118 if ($response->content =~ m!"0;url=(.*?)"!) { 119 $response = $self->{mixi}->get($1); 120 } 121 } 122 123 my $feed = Plagger::Feed->new; 124 $feed->type('mixi'); 125 $feed->title($MAP->{$type}->{title}); 126 $feed->link($MAP->{$type}->{start_url}); 127 128 my $format = DateTime::Format::Strptime->new(pattern => '%Y/%m/%d %H:%M'); 129 130 my $meth = $MAP->{$type}->{get_list}; 131 my @msgs = $self->{mixi}->$meth($response); 132 my $items = $self->conf->{fetch_items} || 20; 133 $self->log(info => 'fetch ' . scalar(@msgs) . ' entries'); 134 135 my $i = 0; 136 my $blocked = 0; 137 for my $msg (@msgs) { 138 next if $type eq 'FriendDiary' and not $msg->{image}; # external blog 139 last if $i++ >= $items; 140 141 my $entry = Plagger::Entry->new; 142 $entry->title( decode('euc-jp', $msg->{subject}) ); 143 $entry->link($msg->{link}); 144 $entry->author( decode('euc-jp', $msg->{name}) ); 145 $entry->date( Plagger::Date->parse($format, $msg->{time}) ); 146 147 if ($self->conf->{show_icon} && !$blocked && defined $MAP->{$type}->{icon_re}) { 148 my $owner_id = ($msg->{link} =~ $MAP->{$type}->{icon_re})[0]; 149 my $link = "http://mixi.jp/show_friend.pl?id=$owner_id"; 150 $context->log(info => "Fetch icon from $link"); 151 152 my $item = $self->cache->get_callback( 153 "outline-$owner_id", 154 sub { 155 Time::HiRes::sleep( $self->conf->{fetch_body_interval} || 1.5 ); 156 my($item) = $self->{mixi}->get_show_friend_outline($link); 157 $item; 158 }, 159 '12 hours', 160 ); 161 if ($item && $item->{image} !~ /no_photo/) { 162 # prefer smaller image 163 my $image = $item->{image}; 164 $image =~ s/\.jpg$/s.jpg/; 165 $entry->icon({ 166 title => decode('euc-jp', $item->{name}), 167 url => $image, 168 link => $link, 169 }); 170 } 171 } 172 173 if ($self->conf->{fetch_body} && !$blocked && $msg->{link} =~ /view_/ && defined $MAP->{$type}->{get_detail}) { 174 $context->log(info => "Fetch body from $msg->{link}"); 175 my $item = $self->cache->get_callback( 176 "item-$msg->{link}", 177 sub { 178 Time::HiRes::sleep( $self->conf->{fetch_body_interval} || 1.5 ); 179 my $meth = $MAP->{$type}->{get_detail}; 180 my($item) = $self->{mixi}->$meth($msg->{link}); 181 182 if ($meth eq 'get_view_diary') { 183 $item->{images} = $self->get_images($self->{mixi}->response->content); 184 } 185 $item; 186 }, 187 '12 hours', 188 ); 189 if ($item) { 190 my $body = decode('euc-jp', $item->{description}); 191 $body =~ s!(\r\n?|\n)!<br />!g; 192 for my $image (@{ $item->{images} }) { 193 $body .= qq(<div><a href="$image->{link}"><img src="$image->{thumb_link}" style="border:0" /></a></div>); 194 my $enclosure = Plagger::Enclosure->new; 195 $enclosure->url( URI->new($image->{thumb_link}) ); 196 $enclosure->auto_set_type; 197 $enclosure->is_inline(1); 198 $entry->add_enclosure($enclosure); 199 } 200 $entry->body($body); 201 202 $entry->date( Plagger::Date->parse($format, $item->{time}) ); 203 } else { 204 $context->log(warn => "Fetch body failed. You might be blocked?"); 205 $blocked++; 206 } 207 } 208 209 $feed->add_entry($entry); 210 } 211 212 $context->update->add($feed); 213} 214 215sub get_images { 216 my($self, $content) = @_; 217 218 my @images; 219 while ($content =~ m!MM_openBrWindow\('(show_diary_picture\.pl\?.*?)',.*?><img src="(http://ic\d+\.mixi\.jp/p/.*?)"!g) { 220 push @images, { link => "http://mixi.jp/$1", thumb_link => $2 }; 221 } 222 223 return \@images; 224} 225 2261; 227 228__END__ 229 230=head1 NAME 231 232Plagger::Plugin::CustomFeed::Mixi - Custom feed for mixi.jp 233 234=head1 SYNOPSIS 235 236 - module: CustomFeed::Mixi 237 config: 238 email: email@example.com 239 password: password 240 fetch_body: 1 241 show_icon: 1 242 feed_type: 243 - RecentComment 244 - FriendDiary 245 - Message 246 247=head1 DESCRIPTION 248 249This plugin fetches your friends diary updates from mixi 250(L<http://mixi.jp/>) and creates a custom feed. 251 252=head1 CONFIGURATION 253 254=over 4 255 256=item email, password 257 258Credential you need to login to mixi.jp. 259 260Note that you don't have to supply email and password if you set 261global cookie_jar in your configuration file and the cookie_jar 262contains a valid login session there, such as: 263 264 global: 265 user_agent: 266 cookies: /path/to/cookies.txt 267 268See L<Plagger::Cookies> for details. 269 270=item fetch_body 271 272With this option set, this plugin fetches entry body HTML, not just a 273link to the entry. Defaults to 0. 274 275=item fetch_body_interval 276 277With C<fetch_body> option set, your Plagger script is recommended to 278wait for a little, to avoid mixi.jp throttling. Defaults to 1.5. 279 280=item show_icon: 1 281 282With this option set, this plugin fetches users buddy icon from 283mixi.jp site, which makes the output HTML very user-friendly. 284 285=item feed_type 286 287With this option set, you can set the feed types. 288 289Now supports: RecentComment, FriendDiary, Message, Log, MyDiary, and Calendar. 290 291Default: FriendDiary. 292 293=back 294 295=head1 SCREENSHOT 296 297L<http://blog.bulknews.net/mt/archives/plagger-mixi-icon.gif> 298 299=head1 AUTHOR 300 301Tatsuhiko Miyagawa 302 303=head1 SEE ALSO 304 305L<Plagger>, L<WWW::Mixi> 306 307=cut 308