Fri, 28 Nov 2003

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"?>

<!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

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 ツールに依存しないスタンドアロン実装としてはしょうがないところもありますが。
Prev: Trackback Ping を受信する
Next: Automatic Trackback

About This Blog

Weblog まわりのテクノロジーをネタにして、プログラミングテクニックを Cookbook 形式で紹介しています。

Site Search


WWW This Site