こんにちは。
キャスレーコンサルティング システムインテグレーション部の田中(雅)です。

今まさに話題となっているMastodonと、まだまだ現役で動いているCGIを組み合わせてみました!

CGIでMastodon APIを実行、
そしてMastodonへトゥートし、タイムラインを見るWebアプリを実際に作っていきます。

Mastodon APIから情報を取得する部分は非常に簡単にできるので、
こちらを参考にどんどんMastodonを使っていただき、
コミュニケーションを活性化していただければ幸いです。

Mastodonとは

twitterによく似た短文投稿型のSNSです。

twitterと異なる部分の特徴として、
1つのインスタンスに趣味・嗜好が同じ人たちがあつまり、
その中でトゥート(twitterでいうツイート)を行うことで
検索せずともインスタンス内のすべてのトゥートを見ることができます。

また、サーバーを用意すればそれぞれでインスタンスを作ることも可能なため、
プライベートなコミュニケーションツールにもなります。

開発環境

CentOS7
Apache 2.4.6
Perl 5.16
主なPerl モジュールは
Template Toolkit
WWW::Curl::Easy
になります。
※今回はVirtualBoxを使用し開発を行いました。

事前準備

Mastodonのインスタンスにてアカウントを作成してください。

作成したプログラムファイル

cgi-bin
  └mastodon
    └conf
     └mastodon.ini–アカウント情報などの設定ファイル
    └tt
     └home.html–テンプレートファイル
    └toot.cgi–トゥート用CGI
    └home.cgi–タイムライン表示用CGI
    └Mastodon.pm–package

アカウント情報を取得

Mastodon API を実行するためAPI実行用のアカウントを取得する必要がありますので
APIを使用し、アカウント情報取得処理を実装しましょう。

home.cgi内アクセストークン取得処理

#!/usr/bin/perl -w

use strict;
use JSON;
use utf8;
use Data::Dumper;
use Template;
use Encode;
use warnings;

use Mastodon;

# アカウント情報取得 初期時のみ
my $account_info = Mastodon::get_account();
print "Content-type: text/html\n\n";
print $account_info;
exit;

conf/mastodon.ini内

[account]
host = [アカウントを取得しインスタンスのホスト]
client_name = [トゥートの横の@部分]
username = [ログイン時のメールアドレス]
password = [ログイン時のパスワード]
scope = read%20write%20follow</pre>

Mastodon.pm内

package Mastodon;

use strict;
use JSON;
use utf8;
use Data::Dumper;
use Encode;
use warnings;
use URI;

use Config::Tiny;
use WWW::Curl::Easy;

my $conf_file = 'conf/mastodon.ini';

my $conf = Config::Tiny->new->read($conf_file);
our $mastodon_host             = $conf->{account}->{host};

# 設定ファイルからアカウント情報取得
our $client_name   = $conf->{account}->{client_name};
our $client_id     = $conf->{account}->{client_id};
our $client_secret = $conf->{account}->{client_secret};
our $username      = $conf->{account}->{username};
our $password      = $conf->{account}->{password};

=comment
POST リクエスト用関数
@param String $api_url
@param Hash %request_param
=cut
sub post_request {
    my $api_url         = shift;
    my %request_param   = @_;
    my %response;

    my $uri = URI->new($api_url);
    $uri->query_form(\%request_param);

    my $curl = WWW::Curl::Easy->new();

    $curl->setopt(CURLOPT_HEADER,1);
    $curl->setopt(CURLOPT_URL, $api_url);
    $curl->setopt(CURLOPT_POST, 1);
    $curl->setopt(CURLOPT_POSTFIELDS, $uri->query);

    my $response_body;
    $curl->setopt(CURLOPT_WRITEDATA,\$response_body);

    my $retcode = $curl->perform;
    if ($retcode == 0) {
        $response{'body'}        = $response_body;
        $response{'header_size'} = $curl->getinfo(CURLINFO_HEADER_SIZE);
        return %response;
    } else {
        print "Error " . $curl->strerror($retcode) . "\n";
        exit;
    }
}


省略

=comment
アカウント情報取得用関数
@param String $client_name
=cut
sub get_account {

    my $api_url = "https://".$mastodon_host."/api/v1/apps";
    my %request_param = (
        'client_name'   => $client_name,
        'redirect_uris' => 'urn:ietf:wg:oauth:2.0:oob',
        'scope'         => 'read write follow',
    );

    my %api_response = post_request($api_url, %request_param);

    # レスポンスからヘッダを削除
    my $response_body = substr($api_response{'body'}, $api_response{'header_size'});
    return $response_body;
}

こちらで
http://localhost/cgi-bin/mastodon/home.cgi
をブラウザで見ると
{
“id”:******,
“redirect_uri”:”urn:ietf:wg:oauth:2.0:oob”,
“client_id”:”******”,
“client_secret”:”******”
}
といったJSONが表示されます。

ここで取得できた
client_idとclient_secretの値を
conf/mastodon.ini内に
client_id = [client_idの値]
client_secret = [client_secretの値]
として設定を追記ください。

では、本格的に実装を進めて参ります。

タイムラインをCGIで見よう

ローカルのタイムラインを見られるまでの機能を実装します。

Mastodon で用意されているページ
タイムライン初期

同様にホームのタイムラインが見られるのが、今回作成したページになります。
ローカルタイムライン画面

タイムライン表示処理

home.cgi
アカウント情報取得に使った処理はコメントアウトし、
以下のような形にします。


省略

=account get
# アカウント情報取得 初期時のみ
my $account_info = Mastodon::get_account();
print "Content-type: text/html\n\n";
print $account_info;
exit;
=cut

# access_token 取得
my $access_token = Mastodon::get_access_token();

# timeline取得
my $timeline_all = Mastodon::get_timeline_home($access_token);

# 必要な情報のみをハッシュに格納
my %timeline;
=comment
# public用
foreach my $timeline_hash ( @$timeline_all ) {
    my $id = $timeline_hash->{"id"};
    $timeline{$id}{"toot"} = $timeline_hash->{"account"}->{"note"};
    $timeline{$id}{"tootter"} = $timeline_hash->{"account"}->{"display_name"};
}
=cut
# home用
foreach my $timeline_hash ( @$timeline_all ) {
    my $id = $timeline_hash->{"id"};
    $timeline{$id}{"id"}      = $timeline_hash->{"id"};
    $timeline{$id}{"toot"}    = $timeline_hash->{"content"};
    $timeline{$id}{"tootter"} = $timeline_hash->{"account"}->{"username"};
}

# ID降順でソート
my %timeline_sort;
my $i;
foreach my $id (sort {$b <=> $a} keys %timeline) {
    $i++;
    $timeline_sort{$i} = $timeline{$id};
}

my $template = Template->new(
    UNICODE  => 1,
    ENCODING => 'utf-8',
);

my $output;
$template->process(
      'tt/home.html',
      { timeline => \%timeline_sort },
      \$output,
      'binmode' => ':utf8'
);

print "Content-type: text/html\n\n";
print encode('utf8', $output);

Mastodon.pm

=comment
トークン認証付きGET リクエスト用関数
@param String $api_url
@param String $access_token
@param hash $request_param
=cut
sub get_request_auth {
    my $api_url         = shift;
    my $access_token    = shift;
    my %request_param   = @_;
    my %response;

    my $curl = WWW::Curl::Easy->new();
    $curl->setopt(CURLOPT_URL, $api_url);
    $curl->setopt(CURLOPT_HTTPHEADER, ['Authorization: Bearer '. $access_token]);
    my $response_body;
    $curl->setopt(CURLOPT_WRITEDATA,\$response_body);

    my $retcode = $curl->perform;
    if ($retcode == 0) {
        $response{'body'}        = $response_body;
        $response{'header_size'} = $curl->getinfo(CURLINFO_HEADER_SIZE);
        return %response;
    } else {
        print "Error " . $curl->strerror($retcode) . "\n";
        exit;
    }
}


省略

=comment
アクセストークン取得用関数
@param String $client_id
@param String $client_secret
@param String $username
@param String $password
=cut
sub get_access_token {
    # API パラメータ設定
    my $api_url = "https://".$mastodon_host."/oauth/token";
    my %request_param = (
        'client_id'     => $client_id,
        'client_secret' => $client_secret,
        'username'      => $username,
        'password'      => $password,
        'grant_type'    => 'password',#パスワード認証はサービス向きでないので非推奨
        'scope'         => 'read write follow',
    );
    my %api_response = post_request($api_url, %request_param);

    # APIのレスポンスからアクセストークン取得
    my $response_body = substr($api_response{'body'}, $api_response{'header_size'});
    my $api_result = JSON->new->utf8->decode($response_body);
    return $api_result->{access_token};
}

=comment
タイムライン取得用関数(home)
@param String $access_token
=cut
sub get_timeline_home {
    my $access_token = shift;

    # タイムライン API URL
    my $api_url = "https://".$mastodon_host."/api/v1/timelines/home";

    my %response = get_request_auth($api_url, $access_token);

    my $response_json = JSON->new->utf8->decode($response{'body'});
    return $response_json;
}

といった形で
まず、アクセストークンの取得を行い、それをヘッダに入れ、
タイムライン取得用(ホーム用)のAPIをCurlにて実行した後、
APIからのレスポンスを取得し、表示させています。

トゥートしてみよう!

トゥート前
といった形で
テキストエリアに「created shared value」と入力し、
「トゥート!!」ボタンを押すと
トゥートしてくれるという機能の実装になります。

「トゥート!!」ボタン押下後の遷移先は
toot.cgi にしており、toot.cgiに遷移するとトゥート用のAPIを実行し、
home.cgi に帰ってくる形の実装になります。

投稿後このような形で「created shared value」がトゥートされます。
公式トゥート後

では、トゥートについての実装部分になります。

toot.cgi

#!/usr/bin/perl -w

use strict;
use JSON;
use utf8;
use Data::Dumper;
use Encode;
use warnings;
use CGI;

use Mastodon;

# access_token 取得
my $access_token = Mastodon::get_access_token();

# パラメータ名を指定し取得
my $input_param = new CGI;
my $toot = $input_param->param('toot');

# トゥート投稿用 API実行
my $api_response = Mastodon::post_statuses($access_token, $toot);

# home.cgiへリダイレクト
print "Location: http://localhost/cgi-bin/mastodon/home.cgi\n\n";

Mastodon.pm

=comment
トークン認証付きPOST リクエスト用関数
@param String $api_url
@param String $access_token
@param hash $request_param
=cut
sub post_request_auth {
    my $api_url         = shift;
    my $access_token    = shift;
    my %request_param   = @_;
    my %response;

    my $uri = URI->new($api_url);
    $uri->query_form(\%request_param);

    my $curl = WWW::Curl::Easy->new();
    $curl->setopt(CURLOPT_URL, $api_url);
    $curl->setopt(CURLOPT_HTTPHEADER, ['Authorization: Bearer '. $access_token]);
    $curl->setopt(CURLOPT_POST, 1);
    $curl->setopt(CURLOPT_POSTFIELDS, $uri->query);
    my $response_body;
    $curl->setopt(CURLOPT_WRITEDATA,\$response_body);

    my $retcode = $curl->perform;
    if ($retcode == 0) {
        $response{'body'}        = $response_body;
        $response{'header_size'} = $curl->getinfo(CURLINFO_HEADER_SIZE);
        return %response;
    } else {
        print "Error " . $curl->strerror($retcode) . "\n";
        exit;
    }
}


省略

=comment
トゥート投稿用関数
@param String $access_token
@param String $status
=cut
sub post_statuses {
    my $access_token = shift;
    my $status = shift;

    # タイムライン API URL
    my $api_url = "https://".$mastodon_host."/api/v1/statuses";

    my %request_param = (
        'status'     => $status,
    );

    my %response = post_request_auth($api_url, $access_token, %request_param);

    my $response_json = JSON->new->utf8->decode($response{'body'});
    return $response_json;
}

終わりに

今回使用したAPI以外にも
・パブリックなタイムラインを見るAPI
・フォロワー/フォローしている人を取得するAPI
・アカウントを検索するAPI
等もありますし、
websocket にてリアルタイムでタイムラインを見られるようにするのも、面白いかなと思っております。

また、この数日間でも様々な言語によって、Mastodonのライブラリが増えてきています。
ぜひ試してみてください。

最後までお読みいただきありがとうございました。