ドシロウトがHTMLファイルひとつで複数のマストドンからストリーミングしてみた

## ０．初めに

私はエンジニアではないただのドシロウトです。

自分が登録していないマストドンのインスタンスの直近のローカルタイムライン(LTL)を眺めようとして以下の記事を見つけました。

> タイムラインを“のぞき見”　アカウント作成不要のマストドン用Webサービス登場　「Mastodon Timeline Peeping Tool」
> http://www.itmedia.co.jp/news/articles/1704/20/news037.html

Mastodon Timeline Peeping Toolを利用すれば、確かにアカウントなしで他のインスタンスのローカル、連合タイムライン（FTL）をのぞき見できます。

私は複数インスタンスの直近の情報が見たかったのでこのツールの公開されているソースを調べました。

> yukimochi/Mastodon-Timeline-Peeping-Tool
> https://github.com/yukimochi/Mastodon-Timeline-Peeping-Tool

多分マストドン基礎知識なんでしょうがストリーミングAPIを利用して直近の情報を得ているとはじめて知りました。

> tootsuite/documentation
> https://github.com/tootsuite/documentation/blob/master/Using-the-API/Streaming-API.md

そこでMastodon Timeline Peeping Toolを参考にしてHTMLファイル一つで複数インスタンスのLTL、FTLをまとめて取得、簡易表示するツールを作ってみました。

## １．作ったもの

以下の画像の通りです。

![mastqiita.jpg](https://i.imgur.com/C5YrLwK.jpg)

ボタンを押すと事前に指定済みの複数インスタンスのLTLまたはFTLに新着投稿があるとリアルタイムに表示します。

HTML表示は可能な限り簡素化しました。

## ２．コード

短いですが以下です。HTMLファイル一つでストリーミングできてます。

```html:test.html
<!DOCTYPE html>
<html>
<head>
  <meta http-equiv="content-type" content="text/html; charset=UTF-8">
</head>
<body>
  <h1>マストドンストリーミングAPI</h1>
  <button class="start-ws-ltl">ローカルタイムライン</button>
  <button class="start-ws-ftl">連合タイムライン</button>
  <br><br>
  <div class="activity-stream"></div>
<script>
var ws_connection = null; /* webソケット接続用 */
// インスタンスをカンマ区切りで指定
var instance = '（インスタンスを指定）';
var instance2 = instance.split(',');
var federate = null; /* ローカル、連合タイムライン判定用 */

window.onload = function () {
  event.preventDefault();
  var ws_ltl = document.getElementsByClassName("start-ws-ltl")[0];
  var ws_ftl = document.getElementsByClassName("start-ws-ftl")[0];
  // ローカルタイムラインボタン押下時
  ws_ltl.addEventListener('click', function (event) {
    event.preventDefault();
    federate = false;
    // インスタンス分だけWebSocket接続を開く
    for (var i=0;i < instance2.length;i++) {
      open_ws(instance2[i], federate);
      // メッセージが受信時のddEventListener
      ws_connection.addEventListener('message', function (event) {
        insert_toot(JSON.parse(event.data).payload);
      });
    };
  });

  // 連合タイムラインボタン押下時
  ws_ftl.addEventListener('click', function (event) {
    event.preventDefault();
    federate = true;

    for (var i=0;i < instance2.length;i++) {
      open_ws(instance2[i], federate);
      ws_connection.addEventListener('message', function (event) {
        insert_toot(JSON.parse(event.data).payload);
      });
    };
  });
}

// WebSocket接続関数
function open_ws(instance2, federate) {
  url = 'wss://' + instance2 + '/api/v1/streaming';
  if (federate) {
      url += '?stream=public';
  } else {
      url += '?stream=public:local';
  }
  ws_connection = new WebSocket(url);
}

// トゥート→HTML書出し関数
function insert_toot(text) {
  entrys = processing_entrys(text); /* トゥート取得 */
  if (entrys !== null) {
    entrys.forEach(entry => {
      parent = document.getElementsByClassName("activity-stream")[0];
      parent.innerHTML = entry+'<hr>'+ parent.innerHTML;
    });
  }
}

// トゥート編集関数
function processing_entrys(data) {
  statuses = [JSON.parse(data)];
  if (statuses.length > 0) {
    entrys = [];
    for (let idx = 0; idx < statuses.length; idx++) {
      const status = statuses[idx];
      try {
        account = status['account'];
        media_attachments = status['media_attachments'];
        status__header = html_status__header(status['url'], status['created_at'], account['url'], account['avatar'], account['display_name'], account['acct']);
        status__content = html_status__content(status['content']);

        md_att = [];
        for (let idx_md_att = 0; idx_md_att < media_attachments.length; idx_md_att++) {
            const media_attachment = media_attachments[idx_md_att];
            md_att.push(media_attachment['url']);
        }
        MediaGallery = null;
        if (media_attachments.length > 0) {
            MediaGallery = html_MediaGallery(md_att);
        }
        entrys.unshift(html_entry(status__header, status__content, MediaGallery));
      }
      catch (e) {

      }
    };
    return entrys;
  } else {
    return null;
  }
}

// トゥート結合関数
function html_entry(status__header, status__content, MediaGallery) {
  var entry;
  entry = status__header + '<BR>';
  entry += status__content + '<BR>';
  if (MediaGallery !== null) {
    entry += MediaGallery + '<BR>';
  }
  return entry;
}

// トゥートヘッダー編集関数
function html_status__header(status_url, status_time, author_url, author_avatar_url, author_name, author_id) {
  var status__header;
  status__header = author_name + ' ' + new Date(status_time).toLocaleString();

  var wkaa = author_url.replace('https://','');
  wkaa = wkaa.replace(('/@'+author_id),'');
  status__header += ' @' + author_id +'(' + wkaa +')';

  return status__header;
}

// トゥートテキスト部編集関数…特に何もしていない
function html_status__content(text) {
  var status__content;
  status__content = text;
  return status__content;
}

// トゥート画像部編集関数
function html_MediaGallery(img_urls) {
  var media_gallery;
  img_urls.forEach(img_url => {
    media_gallery = '<img width=\"100\" src=\"';
    media_gallery += img_url;
    media_gallery += '\"><BR>';
  });
  return media_gallery;
}

</script>
</body>
</html>
```

## ３．気づいた事

試していて気づいた事は以下です。

* 流量の少ないインスタンスをまとめて眺めるのに便利
* 多分、ずっと動かすと重くなる（HTMLに書き出しつづけるので）
* インスタンスによってはストリーミングに対応していない(Qiita丼など)
* 複数タブでこのHTMLを実行するとエラーになる
* ローカルでも動く

## ４．まとめ

元のツールが素晴らしいのでたったこれだけでストリーミングが取得できました。

なかなか便利かなと思います。

以　上