#!/usr/local/bin/perl
#################################
# #
# mixi RSS + LocalCache mod #
# #
# Created by Kero #
# #
#################################
# このスクリプトのご使用は、ご自身の責任でお願いします。
# HTMLの構造が変わると途端に使えなくなると思いますので、直す気力のある人推奨
# (っていうか、突貫で書いてるのでソース汚すぎ……)
# Ver upしたのを作った人は、作者にソースとトラバください。
# http://mo.kerosoft.com/0120/
package main;
use strict;
# 使ってるライブラリ類は用意のこと
# yum install perl-Digest-SHA1とかで簡単に入るはずだから
# 多分レンタルサーバーでこれだけ準備するのは辛いかと
use LWP::UserAgent;
use HTTP::Request::Common;
use DateTime;
use Digest::SHA1 qw/sha1/;
use HTTP::Request;
use MIME::Base64 qw/encode_base64/;
use CGI;
use HTML::TagParser; # これだけはCPANからpmをポンと置けばよい。(./HTML/TagParser.pm)
use HTTP::Cookies;
use Data::Dumper;
use XML::TreePP;
use XML::Twig;
use Encode;
my $v = "2.7";
# 保存先の相対パス
my $folder = q|./mixi/|;
# 保存先のURL
my $baseurl = q|https://your_server_name/mixi/|;
# あなたのメールアドレス
my $email = q|your@mail.address|;
# あなたのログインパスワード
my $passwd = q|LOGIN_PASSWORD|;
# あなたのmixi ID
my $id = q|0000000|;
################################ 設定は、ここまでのはず
### 準備 ###
# 一般的には日記の中にはプライベートなことも含まれるため、BASIC認証をかけた上でhttpsを使うのが
# 好ましいでしょう。が、このcgiをhttpsから呼ぶと、中身に入ってる絵文字がhttpなため、
# 混合ゾーン警告が出ます。それを防ぐため、絵文字もローカルキャッシュにしておきます。
# このスクリプトの下にemojiというフォルダを作り、
# http://img.mixi.jp/img/emoji/[1-246].gifを展開してダウンロードしておきます。(ログイン不要)
# 落とすのが面倒な人は、以下のスクリプトで置換してるところをコメントアウト。(2カ所)
### アクセス方法 ###
# ブラウザからmixi.cgi?(diary|members|log)のようにアクセス
# SSH環境からはmixi.cgi (diary|members|log)のように打つ(テスト用)
# 出力するものは全てRSS(またはエラーメッセージ)です。
# ?diaryが呼ばれた場合のみ、日記全文と画像類を取得してローカルキャッシュします。
# 鯖のスペックや回線スピードにもよりますが、処理が終わるまでに10秒~20秒程度と
# 時間がかかるので放置のこと。
my %params = (
'diary' => "http://mixi.jp/atom/updates/r=1/member_id=$id/-/diary", # 日記
'members' => "http://mixi.jp/atom/friends/r=1/member_id=$id", # マイミク一覧
'log' => "http://mixi.jp/atom/tracks/r=2/member_id=$id", # あしあと
# APIは他にもあるので、必要に応じてどうぞ
);
my $cgi = new CGI;
my $param = $cgi->param('type') || shift;
if(!defined($params{$param})){
print qq|Status: 404 Not Found\n|;
print qq|Content-Type: text/html; charset=utf-8\n\n|;
print qq|要求が不正です。|;
exit;
}
my $nonce = sha1(sha1(time().{}.rand().$$));
my $now = DateTime->now->iso8601.'Z';
my $digest = encode_base64(sha1($nonce.$now.$passwd||''),'');
my $credentials = sprintf(qq(UsernameToken Username="%s", PasswordDigest="%s", Nonce="%s", Created="%s"), $email, $digest, encode_base64($nonce,''), $now);
my $req = HTTP::Request->new(GET => $params{$param});
$req->header( Accept => 'application/x.atom+xml, application/xml, text/xml, */*' );
$req->header( 'X-WSSE' => $credentials );
# LWP::Authen::Wsseを入れてればこれでいいらしい。
#my $ua = LWP::UserAgent->new;
#$ua->credentials('mixi:80', '', $email, $passwd);
#my $res = $ua->get("http://mixi.jp/atom/updates/r=1/member_id=${id}/-/diary");
#print q|Content-Type: |.$res->header('Content-Type')."\n\n";
# RSSをもらいにいく
my $res = LWP::UserAgent->new->request($req);
if($res->is_success){
print q|Content-Type: |.$res->header('Content-Type').qq|\n\n|;
if($param ne "diary"){
my $tpp = XML::TreePP->new;
$tpp->set(force_array => ['entry'], ignore_error => 1);
my $hash = $tpp->parse($res->content);
# 最終更新日時を、API取得日時ではなく、エントリの最新の更新日時とする
# (RSSリーダーでRSS取得の度に更新マークがでまくるのを防止)
$hash->{feed}->{updated} = $hash->{feed}->{entry}[0]->{updated};
my $twig = new XML::Twig;
$twig->set_indent(" "x4);
$twig->parse($tpp->write($hash));
$twig->set_pretty_print("indented");
print $twig->sprint;
exit;
}
# 以下は日記だけの処理
(my $content = $res->content) =~ s!.*?\((.+?)\)!by $1!ig;
# ログイン
my $ua = LWP::UserAgent->new;
my $cookie = HTTP::Cookies->new( ignore_discard => 1 );
$ua->cookie_jar($cookie);
$req = HTTP::Request->new('POST',q|https://mixi.jp/login.pl|);
$req->content_type('application/x-www-form-urlencoded');
$req->content(qq|next_url=%2fhome%2epl&email=$email&password=$passwd&sticky=on&x=0&y=0|);
$res = $ua->request($req);
if($res->is_success){
# 各ユーザーの日記をhtmlに落としていく
my @diary_urls = ($content =~ m{}ig);
# 保存先のURL
my @diary_db;
foreach(@diary_urls){
$_ =~ s/&/&/g;
$ua = LWP::UserAgent->new;
$ua->cookie_jar($cookie);
$req = HTTP::Request->new(GET => $_);
$req->header( 'Connection' => "Keep-Alive" );
$res = $ua->request($req);
if($res->is_success){
my $article = HTML::TagParser->new($res->content);
my ($diary_author, $diary_title) = $article->getElementsByTagName("title")->innerText() =~ m{\[mixi\] (.*?) \| (.*?)$}i;
# my $diary_body = $article->getElementById("diary_body")->innerText; とやりたいが、
# HTML::TagParserの制約上タグが落ちる。ここでは、どうしてもタグ付きのままとりたいので、致し方ない……
(my $content = $res->content) =~ s/\r|\n//g;
Encode::from_to($content, "euc-jp", "utf8");
my ($diary_body) = ($content =~ m!
]*?>(.+)
\n*<\!--/viewDiaryBox-->!i);
$diary_body =~ s!
!
!g;
# WIDE DASHと絵文字をローカルに変換
$diary_body =~ s/\xE3\x80\x9C/~/g;
$diary_body =~ s!http://img.mixi.jp/img/emoji/!../emoji/!g;
my $diary_date = ($article->getElementsByTagName("dd"))->innerText;
my($dy,$dm,$dd,$th,$tm) = $diary_date =~ m/(\d+)年(\d+)月(\d+)日(\d+)\:(\d+)/;
chomp(my $gmtdate = `date --date "$dy/$dm/${dd} $th:$tm:00 9 hours ago" "+%Y-%m-%dT%H:%M:%SZ"`);
my $diary_pics;
{
# 日記からリンクしているAlbum photoを保存
while($content =~ m!!g){ # 1記事にこのタグは複数ある可能性あり
my $addr = qq|http://mixi.jp/|.$1;
my($img_aid, $img_num, $img_oid) = ($addr =~ m/album_id=(\d+)&number=(\d+)&owner_id=(\d+)/);
if(! -e qq|$folder$img_oid-$img_aid-$img_num.jpg|){
my $imghtml = $ua->request(HTTP::Request->new(GET => $addr));
next if(! $imghtml->is_success);
# アルバムの時はimgタグが貼ってあるhtmlを吐かれるので、それを辿る
my ($imgaddr) = ($imghtml->content =~ m!request(HTTP::Request->new(GET => $imgaddr));
next if(! $imgres->is_success);
if($imgres->is_success){
open(IMG, "> $folder$img_oid-$img_aid-$img_num.jpg");
flock(IMG, 2);
binmode(IMG);
print IMG $imgres->content;
close(IMG);
}
}
# いま処理したタグをローカルアドレスに置換
$diary_body =~ s!!!; # 1件だけ処理
}
# 日記に貼り付けているDiary photoを保存
my ($diary_photo) = ($content =~ m!(.+?)
!); # 1記事にこのタグは1個だけ
while($diary_photo =~ m/'(show_diary_picture.pl\?[^']+)'/g){
my $addr = qq|http://mixi.jp/|.$1;
my($img_oid, $img_id, $img_num) = ($addr =~ m/owner_id=(\d+)&id=(\d+)&number=(\d+)/);
if(! -e qq|$folder$img_oid-$img_id-$img_num.jpg|){
my $imgres = $ua->request(HTTP::Request->new(GET => $addr));
next if(! $imgres->is_success);
# imgタグを見つけてダウンロード
my ($imgaddr) = ($imgres->content =~ m!request(HTTP::Request->new(GET => $imgaddr));
if($imgres->is_success){
open(IMG, "> $folder$img_oid-$img_id-$img_num.jpg");
flock(IMG, 2);
binmode(IMG);
print IMG $imgres->content;
close(IMG);
}
}
push(@{$diary_pics}, "$folder$img_oid-$img_id-$img_num.jpg");
}
}
my $comments; my $cnt = 0;
# コメント類を取得
my (@c_names, @c_dates, @c_texts);
eval{
while($content =~ m!(.+?)!g){
$cnt++;
my $txt = $1;
# WIDE DASHと絵文字をローカルに置換
$txt =~ s/\xE3\x80\x9C/~/g;
$txt =~ s!http://img.mixi.jp/img/emoji/!../emoji/!g;
push(@c_texts, $txt);
}
};
# ハッシュを作る
for(my $i=0; $i<=$#c_texts; $i++){
push(@{$comments}, { name => $c_names[$i], date => $c_dates[$i], text => $c_texts[$i] });
}
my ($did, $oid) = $_ =~ m{id=(\d+)&(?:amp;)?owner_id=(\d+)};
# 日記記事1つのハッシュ
my $record = {
orgurl => $_,
url => "./$oid-$did.html",
fullurl => "$baseurl$oid-$did.html",
author => $diary_author,
title => $diary_title,
body => $diary_body,
date => $diary_date,
updated => $gmtdate,
comments => $comments,
comments_cnt => $cnt,
pics => $diary_pics,
};
# ファイルに
open(LOG, "> $folder$oid-$did.html");
flock(LOG, 2);
print LOG &getHTML($record);
close(LOG);
# 外部blogの場合は追加しない(ローカルキャッシュ対象外、RSSは出力する)
push(@diary_db, $record) if($diary_title ne "");
}else{
warn "Diary: ".$res->status_line."\n";
}
}
&writeIndex(@diary_db);
# アドレス全部書き換え
$content =~ s{http://mixi.jp/home.pl}{$baseurl}i;
# 日付書き替え(mixi本家の隠しAPIの日付は、常に取得時間になり、RSSリーダーにnewつきまくってウザ!!)
my $tpp = XML::TreePP->new;
$tpp->set(force_array => ['entry'], ignore_error => 1);
my $hash = $tpp->parse($content);
foreach(@{$hash->{feed}->{entry}}){
foreach my $db (@diary_db){
if($_->{link}->{'-href'} eq $db->{orgurl}){
$_->{link}->{'-href'} = $db->{fullurl};
$_->{updated} = $db->{updated};
$_->{title} .= "(".$db->{comments_cnt}.")";
last;
}
}
# 外部blogの場合は、投稿時間不明なため日付セクションを消す
if($_->{link}->{'-href'} !~ m!^$baseurl!){
# ついでに外部blogへのジャンクションページも要らん
my ($url) = ($_->{link}->{'-href'} =~ m!url=(.+?)&owner_id=\d+!);
$url =~ tr/+/ /;
$url =~ s/%([0-9A-Fa-f][0-9A-Fa-f])/pack('H2', $1)/eg;
$_->{link}->{'-href'} = $url;
$_->{updated} = "";
}
}
$hash->{feed}->{updated} = $hash->{feed}->{entry}[0]->{updated};
my $twig = new XML::Twig;
$twig->set_indent(" "x4);
$twig->parse($tpp->write($hash));
$twig->set_pretty_print("indented");
print encode("utf8", $twig->sprint);
}else{
warn "Login: ".$res->status_line."\n";
}
}else{
warn "RSS: ".$res->status_line."\n";
}
# ここでローカルキャッシュの各記事のhtmlを作ります
sub getHTML{
my $g = $_[0];
my ($pics, $comments);
if(defined($g->{pics})){
foreach my $pic (@{$g->{pics}}){
my ($fn) = ($pic =~ m{/?([^/]+?)$}ig);
`wget -q -O $folder$fn $pic` if(! -e $folder.$fn); # 落とし損ねてたら、もっかい落とす…とかいう若干意味不明な保険
$pics .= qq|\n|;
}
$pics .= "
\n";
}
if(defined($g->{comments})){
foreach(@{$g->{comments}}){
$comments .= qq|$_->{date} by $_->{name}
\n$_->{text}
\n|;
}
}
return qq|
$g->{author}の日記 - $g->{title}
$g->{title}
$g->{date} by. $g->{author}
$pics
$g->{body}
$comments
|;
}
# 各記事へのリンクを作ります。
sub writeIndex{
my @list;
foreach(@_){
push(@list, qq|$_->{author} - $_->{title} ($_->{date})|) if($_->{updated} ne "");
}
open(LOG, "${folder}list.txt"); # xmlつくればソートとかできたんだろうけど、面倒なのでパス
my @log = ;
foreach my $a (@list){
chomp($a);
my $flag = 1;
foreach my $b (@log){
chomp($b);
$flag = 0 if($a eq $b);
}
unshift(@log, $a) if($flag);
}
close(LOG);
open(LOG, "> ${folder}list.txt");
flock(LOG, 2);
print LOG join("\n", @log);
close(LOG);
open(LOG, "> ${folder}index.html");
flock(LOG, 2);
print LOG qq|
mixi日記一覧
mixi日記一覧
|;
close(LOG);
}