Trackback Ping 一覧を RSS 出力する
受信した 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"?>のような XML 宣言や DTD 宣言が入ってしまい、邪魔になるため、
<!DOCTYPE rss PUBLIC "-//Netscape Communications//DTD RSS 0.91//EN"
"http://my.netscape.com/publish/formats/rss-0.91.dtd">
<rss version="0.91">
...
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
List 1: tb_rss.pl
#!/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; }
*1) REST の対称性からこういう仕様にしたのだとは思いますが、はっきり言って不便です...
*2) Trackback 仕様書や MovableType での実装での RSS version が 0.91 となっているため、ここでも 0.91 としました。明確な規定はないようです。
*3) 名前からわかるように非常にハック的なので、あまり参考にしない方がよいでしょう ;-)
*4) どういったエントリが存在するかがわからないので、Blog ツールに依存しないスタンドアロン実装としてはしょうがないところもありますが。
*2) Trackback 仕様書や MovableType での実装での RSS version が 0.91 となっているため、ここでも 0.91 としました。明確な規定はないようです。
*3) 名前からわかるように非常にハック的なので、あまり参考にしない方がよいでしょう ;-)
*4) どういったエントリが存在するかがわからないので、Blog ツールに依存しないスタンドアロン実装としてはしょうがないところもありますが。
posted at: 01:36 by miyagawa | category: Trackback Pings | permalink
Trackback (2) | Printer Friendly | Email this blog