Published on Blog Developer's Cookbook.
Printer friendly version of http://blog.bulknews.net/cookbook/blosxom/trackback/tb_rss.html.
Trackback Ping 一覧を RSS 出力する
by miyagawa at Fri, 28 Nov 2003 01:36
受信した Trackback Ping の一覧を取得するために、RSS で Trackback 一覧を出力するという仕様があります。今回は、Trackback Ping Server で作成したスクリプトに、RSS 出力機能を実装してみます。
Trackback 規格仕様書 によると、ある Trackback Ping URL に対して送信された Ping のリストは、Ping URL にクエリパラメータ __mode=rss
を付加することによって、RSS データをレスポンスとして取得することができます。
例えば、Trackback Ping URL が
http://example.com/tb/1234
であれば、該当する RSS 取得 URL は
http://example.com/tb/1234?__mode=rss
となり、この URL に HTTP GET でリクエストを送信すると、
<?xml version="1.0" encoding="utf-8"?>
<response>
<error>0</error>
<rss version="0.91"><channel>
<title>TrackBack Test</title>
<link>http://example.com/tb/1234</link>
<description>Description of the TrackBack item</description>
<item>
<title>TrackBack Demo</title>
<link>http://blog.example.com/permalink/</link>
<description>Excerpt</description>
</item>
</channel>
</rss></response>
のような XML が取得できます。よく見ると気付くと思いますが、この XML は純粋な RSS ではなく、Trackback レスポンスの中に RSS ノードを埋めこむ形になっています(*1)。
サンプルコード
Trackback Ping Server に __mode=rss を実装したスクリプトは List 1 のようになります。前回との差分についてのみ解説します。
our $tb_prefix = "Blog Developer's Trackback";
our $tb_link = "http://example.com/tb";
RSS の channel 要素の title, link に使用する文字列や URL を指定しています。$tb_link
の方は、このスクリプト 自体の URL にしておけばよいでしょう。
my $mode = $query->param('__mode');
if ($mode && $mode eq 'rss') {
show_rss($query, $tb_id);
} else {
receive_ping($query, $tb_id);
}
CGI パラメータの __mode
を取得し、 rss であれば RSS 出力、そうでなければ受信モードに移行します。
sub connect_db {
my $tb_id = shift;
my $is_existent = -e "$dbdir/$tb_id.db";
my @dsn = ("dbi:SQLite:dbname=$dbdir/$tb_id.db", "", "");
my $dbh = DBI->connect(@dsn, { RaiseError => 1,
AutoCommit => 1, });
# Create table "tb" if it's first time
init_table($dbh) unless $is_existent;
return $dbh;
}
データベース接続部分を再利用できるよう、connect_db
としてまとめてあります。
sub show_rss {
my($query, $tb_id) = @_;
my $dbh = connect_db($tb_id);
my $sql = sprintf "SELECT %s FROM tb ORDER BY timestamp DESC",
join(",", @cols);
my $sth = $dbh->prepare($sql);
$sth->execute();
まずはデータベースに接続し、SELECT 文を発行します。ここでもカラムの指定はパッケージ変数 @cols
から生成しています。Trackback Ping は timestamp の新しい順にソートして取得します。
my $rss = XML::RSS->new(version => 0.91);
$rss->channel(
title => "$tb_prefix: $tb_id",
link => "$tb_link/$tb_id",
description => "Trackback Discussion on $tb_id",
);
while (my $data = $sth->fetchrow_hashref) {
$rss->add_item(
title => $data->{title},
link => $data->{url},
);
}
$sth->finish();
XML::RSS オブジェクトを version 0.91 (*2) で new し、channel や item をセットします。出来上がった XML::RSS オブジェクトを send_rss
に渡し、レスポンスを出力します。
sub send_rss {
my($query, $rss) = @_;
my $rss_node = hack_rss_node($rss->as_string);
print $query->header('text/xml'), <<XML;
<?xml encoding="utf-8"?>
<response>
<error>0</error>
$rss_node
</response>
XML
;
}
RSS オブジェクトを as_string
で文字列化して Trackback の response ノード内に埋め込みます。ここで XML::RSS を 0.91 で実行すると、
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE rss PUBLIC "-//Netscape Communications//DTD RSS 0.91//EN"
"http://my.netscape.com/publish/formats/rss-0.91.dtd">
<rss version="0.91">
...
のような XML 宣言や DTD 宣言が入ってしまい、邪魔になるため、hack_rss_node
というサブルーチンでその部分を除去します(*3)。
sub hack_rss_node {
my $rss = shift;
$rss =~ s@<\?xml .*?>\n*@@s;
$rss =~ s@<!DOCTYPE rss .*?>\n*@@s;
return $rss;
}
XML や DTD 宣言を strip し、余計な空行もとります。
実行例
いくつか Ping を送信した URL に対し、__mode=rss を付加してアクセスすると、
<?xml encoding="utf-8"?>
<response>
<error>0</error>
<rss version="0.91">
<channel>
<title>Blog Hacks Trackback: 1234</title>
<link>http://example.com/tb/1234</link>
<description>Trackback Discussion on 1234</description>
<item>
<title>TEST</title>
<link>http://blog.example.com/</link>
</item>
</channel>
</rss>
</response>
といった XML レスポンスが取得できます。
Hack the Hacks
このサンプルでは、存在しない Trackback ID をいれた場合でも、とりあえずつなぎにいって init_table
でテーブルを生成してしまいます(*4)。ファイルが存在するかどうか最初にチェックして、なければつくらないようにした方がよりよいでしょう。
Trackback 仕様書には、
In future revisions of the specification--once the grace period for switching to GET from POST has passed--this may be simplified such that sending a GET request to the TrackBack Ping URL will return the list of pings.
と記載されており、将来的には Ping の送信は POST, RSS の受信は GET という風にしようと考えているようです。ただ実際には、執筆時点ではこうした Trackback 自体の拡張より、Atom API への統合の方が現実的なのかもしれません。
Listings
#!/usr/local/bin/perl -w
# tb_rss - implements __mode=rss
use strict;
use CGI;
use DBI;
use HTML::Entities;
use Time::Piece;
use XML::RSS;
our $dbdir = "/tmp";
our @cols = qw(url title blog_name excerpt timestamp);
our $tb_prefix = "Blog Developer's Trackback";
our $tb_link = "http://example.com/tb";
my $query = CGI->new();
$query->charset('utf-8');
my $tb_id = munge_tb_id($query) or do {
send_response(1 => "Invalid Trackback ID");
exit;
};
my $mode = $query->param('__mode');
if ($mode && $mode eq 'rss') {
show_rss($query, $tb_id);
} else {
receive_ping($query, $tb_id);
}
sub receive_ping {
my($query, $tb_id) = @_;
my $vars = $query->Vars();
if (!$vars->{url}) {
send_response(1 => "No url parameter (required)");
return;
}
fix_encoding($vars) if $vars->{charset};
# Default values
$vars->{title} ||= $vars->{url};
$vars->{blog_name} ||= do {
require URI;
URI->new($vars->{url})->host;
};
$vars->{excerpt} ||= "No excerpt.";
$vars->{timestamp} = Time::Piece->new->datetime;
eval {
store_ping($tb_id, $vars);
send_response(0 => "Thanks for your Ping to $tb_id");
};
if ($@) {
send_response(1 => "Error while storing ping: $@");
}
}
sub fix_encoding {
my $vars = shift;
require Encode;
Encode::from_to($vars->{$_},
$vars->{charset} => "utf-8") for @cols;
}
sub store_ping {
my($tb_id, $vars) = @_;
my $dbh = connect_db($tb_id);
# Generate SQL statements
my $sql = sprintf "INSERT INTO tb (%s) VALUES (%s)",
join(",", @cols), join(",", ('?') x @cols);
my $sth = $dbh->prepare($sql);
$sth->execute(@{$vars}{@cols});
$sth->finish();
$dbh->disconnect();
}
sub connect_db {
my $tb_id = shift;
my $is_existent = -e "$dbdir/$tb_id.db";
my @dsn = ("dbi:SQLite:dbname=$dbdir/$tb_id.db", "", "");
my $dbh = DBI->connect(@dsn, { RaiseError => 1,
AutoCommit => 1, });
# Create table "tb" if it's first time
init_table($dbh) unless $is_existent;
return $dbh;
}
sub init_table {
my $dbh = shift;
$dbh->do(<<SQL);
CREATE TABLE tb (
url VARCHAR(255) NOT NULL,
title VARCHAR(255) NOT NULL,
blog_name VARCHAR(255) NOT NULL,
excerpt TEXT,
timestamp DATETIME
)
SQL
;
}
sub show_rss {
my($query, $tb_id) = @_;
my $dbh = connect_db($tb_id);
my $sql = sprintf "SELECT %s FROM tb ORDER BY timestamp DESC",
join(",", @cols);
my $sth = $dbh->prepare($sql);
$sth->execute();
my $rss = XML::RSS->new(version => 0.91);
$rss->channel(
title => "$tb_prefix: $tb_id",
link => "$tb_link/$tb_id",
description => "Trackback Discussion on $tb_id",
);
while (my $data = $sth->fetchrow_hashref) {
$rss->add_item(
title => $data->{title},
link => $data->{url},
);
}
$sth->finish();
send_rss($query, $rss);
}
sub munge_tb_id {
my $query = shift;
my $path_info = $query->path_info();
$path_info && $path_info =~ m!(\w+)! && return $1;
return;
}
sub send_response {
my($error, $message, $no_escape) = @_;
my $msg = $no_escape ? $message : encode_entities($message);
print $query->header('text/xml'), <<XML;
<?xml encoding="utf-8"?>
<response>
<error>$error</error>
<message>$msg</message>
</response>
XML
;
}
sub send_rss {
my($query, $rss) = @_;
my $rss_node = hack_rss_node($rss->as_string);
print $query->header('text/xml'), <<XML;
<?xml encoding="utf-8"?>
<response>
<error>0</error>
$rss_node
</response>
XML
;
}
sub hack_rss_node {
my $rss = shift;
$rss =~ s@<\?xml .*?>\n*@@s;
$rss =~ s@<!DOCTYPE rss .*?>\n*@@s;
return $rss;
}
Copyright©2002-2003 Tatsuhiko Miyagawa