あらためてチャットサポートを構築する

Converse.js という XMPP の Web クライアントを見つけて「チャットサポートを構築する」の記事を書いたのは2014年初頭でした。Converse.js が公開されて1年めほどでバージョンは 0.7.2 でした。

2018年4月現在の Converse.js は 3.3.4 で、かなり進歩しています。情報もあちこちに出てきて、以前は苦労したことも現在では簡単にできるようになっていました。本家ページ以外に情報が散らばっていましたのでここにまとめてながら、あらためてチャットサポートを設置してみます。

PreBind

その前に、今回の構築には用いませんが、以前は私がよく理解できておらず適当に誤魔化していた PreBind について、簡潔に解説したページ PreBind: connettersi senza password がありましたので、紹介しておきます。

PHP のクラス XMPP Prebind for PHP をダウンロードしておき、次のようにインスタンス化します。

<?php 
include 'xmpp-prebind-php/lib/XmppPrebind.php';

$boshUrl = 'https://example.com:5280/bosh/';  // 環境に合わせて変更
$server = 'example.com';           // 環境に合わせて変更
$username = 'your_username';   // 環境に合わせて変更
$password = 'your_password';   // 環境に合わせて変更
$resource = rand(10000,99999); // ここでは5桁の数字。環境に合わせて変更

$xmppPrebind = new XmppPrebind($server, $boshUrl, $resource, false, false);
$xmppPrebind->connect($username, $password);
$xmppPrebind->auth();
$sessionInfo = $xmppPrebind->getSessionInfo(); // array containing sid, rid and jid

header('Content-Type: application/json');
echo json_encode($sessionInfo);

?>

これだけで動かしてみると分かりますが、sid, rid, jid の3つを JSON 形式で返すものです。

次に、Converse.js の初期化の際に、上のスクリプト名 (prebind.php だとします) を prebind_url で指定します。

<?php
  $boshUrl = 'https://example.com:5280/bosh/';  // 環境に合わせて変更
  $server = 'example.com';      // 環境に合わせて変更
  $username = 'your_username';  // 環境に合わせて変更
  $prebindUrl = 'prebind.php';  // 上のスクリプトの URL
?>
<html>
  <head>
    ...
    <link type="text/css" rel="stylesheet" media="screen" href="https://cdn.conversejs.org/css/inverse.min.css" />
    <script src="https://cdn.conversejs.org/dist/converse.min.js"></script>
    ...
  </head>
  <body>
     ...
    <script>
      converse.initialize({
        i18n: 'ja',
        locales_url: 'https://cdn.conversejs.org/locale/{{{locale}}}/LC_MESSAGES/converse.json',
        authentication: 'prebind',
        bosh_service_url: '<?php echo $boshUrl; ?>', 
        keepalive: true,
        jid: '<?php echo $username."@".$server; ?>',
        prebind_url: '<?php echo $prebindUrl; ?>',
        prebind: true,
        ...
      });
    </script>
  </body>
</html>

この HTML は誰にも見られることになりますが、パスワードは事前接続にのみ用いられているので、ここに書かずに済むというわけです。

チャットサポートの構築

XMPP サーバーで匿名接続を利用できる場合、Converse.js の初期化時に authentication: 'anonymous' とすることで、実は上述の PreBind はまったく使わずに済みます。

以下、この匿名接続を使ってチャットサポートを組み立てます。

  • 客側は Converse.js で匿名 (一時的な JID を使う) とする。
  • 窓口側の JID (たとえば support@example.com) は共有名簿として匿名接続時に与えられる。
  • 客側の Converse.js は自動的に窓口側 JID を呼び出しチャット窓を開く。

という流れです。

サーバー側設定

BOSH または Websocket

前述の PreBind でもそうでしたが、BOSH (または Websocket) を利用しますので、XMPP サーバーに付属のそれらの機能を有効にしておきます。Ejabberd の場合、ejabberd.yml は次のようになります。

listen:
  ...
  -
    port: 5280
    ip: "::"
    module: ejabberd_http
    request_handlers:
      "/ws": ejabberd_http_ws
      "/bosh": mod_bosh
    http_bind: true
    register: true
    tls: true
  ...
modules:
  ...
  mod_bosh: {}
  ...

匿名サーバー

通常クライアントがサーバーに接続する場合は、事前に作成しておいた user@example.com という形をした JID とパスワードが必要になります。匿名サーバーは、そのサーバー名だけを指定して接続を試み、@ より前のユーザー名を乱数のようにそのつど生成して接続します。

サーバーアプリケーション Ejabberd の場合、ejabberd.yml に次のように設定して SASL 匿名サーバー (anonymous.example.com という名前だとします) を設定します。

hosts:
  - "example.com"
  - "anonymous.example.com"
  ...
host_config:
  "anonymous.example.com":
    auth_method: [anonymous]
    anonymous_protocol: sasl_anon

なお今回は特に関係ありませんが、匿名サーバーで接続するユーザーについては、悪用を防ぐため権限を絞っておくのが望ましいでしょう。たとえば、別のサーバーとの通信の禁止、MUC 談話室や PubSub ノードの作成の禁止、などです。

窓口側のJID

窓口側のJID (例: support@example.com) の作成は、通常どおり行います。

共有名簿 (shared roster)

匿名サーバーの場合、接続のたびにアカウントが新たに一時的に生成されるため、その JID には相手先名簿がありません。そこで、共有名簿 (shared roster) という機能を使うことにします。

まず Ejabberd の設定ファイル ejabberd.yml でこの機能を有効にしておきます。

modules:
  ...
  mod_shared_roster: {}
  ...

Ejabberd のウェブ管理画面の「バーチャルホスト -> anonymous.example.com -> 共有名簿グループ」の画面で、まずひとつのグループを作成します。たとえば

  • 名前: support
  • 説明: サポート窓口
  • メンバー: support@example.com
  • 表示グループ: (空欄)

のようにします。ここで support@example.com は、窓口側 (問い合わせを受ける側) の JID です。

さらにもうひとつグループを作成します。

  • 名前: all
  • 説明: 全ユーザー
  • メンバー: @all@
  • 表示グループ: support

ここで @all@ は、「全ユーザー」を意味します。

これで、「anonymous.example.com の全ユーザーの名簿に support グループのメンバー (ここでは support@example.com のみ) を自動的にセットする」ことができました。

Converse.js の設定

チャットウィンドウを設置する Web ページで、Converse.js を読み込むようにいくつかの行を書き加えます。Converse.js そのものを同じサイトに置いてもいいですし、CDN から読み込むようにもできます。

<head>
    ...
    <link type="text/css" rel="stylesheet" media="screen" href="https://cdn.conversejs.org/css/converse.min.css" />
    <script src="https://cdn.conversejs.org/dist/converse.min.js"></script>
    ...
</head>

それから、パラメータとともに Converse.js を初期化することで、そのページにチャットウィンドウを設置します。基本は

converse.initialize({
        websocket_url: 'wss://example.com:5280/ws/',
        authentication: 'anonymous',
        jid: 'anonymous.example.com',
        auto_login: true,
})

です。ここでは BOSH ではなく WebSocket を使う場合です。匿名接続の場合 jid はサーバー名のみにします。自動ログインでサーバーに接続するのですが、このままでは相手先とのチャットを開くところはできません。

自動で相手先とのチャットを開く

この情報は本家ではないところにありました。Converse opening a chat from API の回答で Converse.js の作者がプラグインを紹介しています。

converse.plugins.add('chatsopen', {
      initialize: function() {
        var _converse = this._converse;
        Promise.all([
            _converse.api.waitUntil('rosterContactsFetched'),
            _converse.api.waitUntil('chatBoxesFetched')
        ]).then(function() {
          _converse.api.chats.open('support@example.com');
        });
      }
    });

初期化時にこれを利用するようにします。

whitelisted_plugins: ['chatsopen'],

コントロールボックスは全部隠してしまうこともできます。

blacklisted_plugins: ['converse-controlbox'],

まとめ

以上をまとめて (ついでに他のパラメータも追加して)、チャットウィンドウを設置する Web ページまるごとの例です。

<html>
<head>
    <meta charset="utf-8"/>
    <title>チャットサポート</title>
    <link type="text/css" rel="stylesheet" media="screen" href="https://cdn.conversejs.org/css/converse.min.css" />
    <script src="https://cdn.conversejs.org/dist/converse.min.js"></script>
</head>
<body>
<h1>チャットサポートのページ</h1>
<p>右下の窓で話しかけてください。</p>
</body>
<script>
    converse.plugins.add('chatsopen', {
      initialize: function() {
        var _converse = this._converse;
        Promise.all([
            _converse.api.waitUntil('rosterContactsFetched'),
            _converse.api.waitUntil('chatBoxesFetched')
        ]).then(function() {
          _converse.api.chats.open('support@example.com');
        });
      }
    });

    converse.initialize({
        i18n: 'ja',
        locales_url: 'https://cdn.conversejs.org/locale/{{{locale}}}/LC_MESSAGES/converse.json',
        authentication: 'anonymous',
        websocket_url: 'wss://example.com:5280/ws/',
        auto_away: 300,
        auto_reconnect: true,
        keep_alive: true,
        jid: 'anonymous.example.com',
        auto_login: true,
        allow_logout: false,
        allow_contact_removal: false,
        allow_contact_requests: false,
        allow_muc: false,
        allow_otr: false,
        allow_registration: false,
        blacklisted_plugins: [
            'converse-bookmarks',
            'converse-controlbox',
            'converse-mam',
            'converse-muc',
            'converse-notification',
            'converse-otr',
            'converse-register'
        ],
        whitelisted_plugins: ['chatsopen'],
        forward_messages: false,
        message_carbons: false,
        message_archiving: 'never',
        use_vcards: true,
        view_mode: 'overlayed',
        visible_toolbar_buttons: {
            call: false,
            clear: false,
            emoji: true
        }
    });
</script>
</html>

実際に設置してみたのが次です (サーバー名など変更)。