ニュースのスクレイピングでタイピング練習

ひらがな数文字を打ち返すだけのタイピング練習は案の定すぐに飽きてしまったので、何か別のネタを考えなくてはならなくなりました。飽きないためには膨大かまたは頻繁に更新される元データがあればいい、青空文庫かな、でも小学生に向いているものがどれほどあるかしらん、頻繁に更新されるといえばニュース、でもこれまた小学生向きではなさそう……と思ったら実にぴったりのものがありました。NHK NENS WEB EASY です。ひとつの記事で50字ほどの文が10ほど。意味もわかりやすくて量もちょうどいい。かなり手間をかけて作られているようです。

さて、これをなんとか持ってきてタイピング練習の材料にしようと思ったのですが、何しろ本業でも何でもないので情報を集めるところからスタートでした。今回やっていることは実は「スクレイピング」という程のこともないのですが、そのとっかかりということで、せっかくなのでここに記録しておきます。

環境の準備

NHK NENS WEB EASY のページの肝心な部分は JavaScript で生成されているようで、Python で単純に requests.get(url) とやっても、ブラウザで見ている HTML ソースとは別のものしか得ることができません。そこでまず、ブラウザが実際に表示するページを取得できるようにします。

Debian パッケージ chromium-driver をインストールします。

sudo apt-get install chromium-driver

これを Python から使うためにライブラリ Selenium をインストールします。

pip install selenium

取得した HTML から必要な箇所を切り出すのには BeautifulSoup4 を使います[1]

pip install beautifulsoup4

でインストールします。

ニュースサイトの構造

ニュースサイトによくあることですが、各記事の URL は数字の羅列のような名前で、先頭ページではそれが日々更新されます。

ブラウザのデベロッパーツールで NHK NENS WEB EASY の先頭ページ https://www3.nhk.or.jp/news/easy/ の構造を見てみます。

<div class="top-news-list__pickup news-list-item" id="js-news-pickup">
  ...
  <h1 class="news-list-item__title is-pickup">
  <a href="./k10011463631000/k10011463631000.html"><em class="title"><ruby>日本<rt>にっぽん</rt></ruby>の<ruby>二酸化炭素<rt>にさんかたんそ</rt></ruby>の<ruby>濃度<rt>のうど</rt></ruby>が<ruby>今<rt>いま</rt></ruby>まででいちばん<ruby>高<rt>たか</rt></ruby>くなる</em><time class="time">6月5日 11時30分</time></a>
  </h1>
</div>

最初に大きく取り上げられている記事は <div id="js-news-pickup"> で、その中の <h1> の中の <a> から記事個別ページの URL が得られます。

その記事個別ページを同様にブラウザのデベロッパーツールで見てみると、記事本文は <div id="#js-article-body"> にあることがわかります。これを切り出してくればいい訳です。

スクレイピング

参考にした (というか、ほぼそのままコピーさせてもらった) コードは「Python Webスクレイピング テクニック集」の「JavaScriptによる描画に対応する」です。

ソース中のコメント「ブラウザを起動する」の箇所で、ブラウザのパスを指定する必要がありました。Debian のパッケージを使っている場合

driver = webdriver.Chrome(executable_path='/usr/bin/chromedriver', chrome_options=options)

です。

切り出し

1回めの

        # ブラウザでアクセスする
        siteurl = "https://www3.nhk.or.jp/news/easy/"
        driver.get(siteurl)

        ...

        # BeautifulSoupで扱えるようにパースします
        soup = BeautifulSoup(html, "html.parser")

        # id で特定の要素を切り出す
        href = soup.select_one("#js-news-pickup h1 a").get('href')

で記事個別ニュースの URL が得られるので、2回めは

        driver.get(newsurl)
        html = driver.page_source.encode('utf-8')
        soup = BeautifulSoup(html, "html.parser")
        # ルビを削除
        for s in soup(['rt']):
            s.decompose()

        ...

        # ニュースの本文
        text = soup.select_one("#js-article-body").text

で、記事本文を切り出します。

ルビを削除

ニュースの本文は

...<p><span class="colorC"><ruby>気象庁<rt>きしょうちょう</rt></ruby></span>は「もっと<a href="javascript:void(0)" class="dicWin" id="id-0000"><ruby><span class="under">二酸化炭素</span><rt>にさんかたんそ</rt></ruby></a>を<ruby>出<rt>だ</rt></ruby>さないようにしなければなりません」と<ruby>話<rt>はな</rt></ruby>しています。</p>...

のようになっています。BeautifulSoup の .text で単純にタグを削除するだけでは

気象庁きしょうちょうは「もっと二酸化炭素にさんかたんそを出ださないようにしなければなりません」と話はなしています。

と、ルビが本文に混じってしまい、まともな文になりません。事前に rt 要素を削除しておく必要があります。

この作業をやってみて、rt がほかとは異質なタグ(要素)であることを実感しました。これについてはまた別の記事に書こうと思います。

1文ずつに分解

記事を「。」で区切り、リストにします。「。」自身も含めたいので split が使えません。NHK のニュース記事で全体の最後に「。」がないことはまさかないだろうと仮定して、

        lines = re.findall(".*?。", text)

とします。あとは Errnot がこれを1文ずつ表示するようにするだけです。これでこの bot を相手に XMPP のチャットでオウム返しにタイピングの練習をすることができるようになりました。

それにしても、いろいろ寄せ集めるだけでこれだけできるのですから、便利な世の中になったものだとつくづく思いました。

  1. Python にはじめからある html.parser でもある程度できます。また Selenium にも同様の機能があるようです。

XMPP チャットと bot でタイピング練習

スウちゃん (仮名、4年生) がパソコンでやりたいことのひとつが「字を打てるようになる」こと。

学校でローマ字も習ったし、パソコンには以前からちょっとは触っているので、人差し指打法でたどたどしくは打てます。せっかくだから「正しい」タイピング技法 (タッチタイピング) を習得させてやりたいと思っています。父である私はそれをやらずに我流のまま来てしまいました。ある程度までは速く打てるものの指が定まっていないため間違いが多く、打ち直しのため結局は遅いことになってしまっています。きれいにすらすらと打てる人は本当に間違いも少ないので、スウちゃんには最初からその方向でいってほしいと思っているのです。

子どもでもできそうな練習アプリやサイトを見て回りました。Windows 向けはまず却下。ウェブ版のほとんどは Flash ですが、いまどきこれで大丈夫なのでしょうか。Debian の Firefox に Flash を入れて動かすのが面倒なので、これもできるだけ避けたいところです。

いずれにしても、練習の順番に疑問を感じます。たいていのものはまずホームポジション fj からです。しかしこの2字とも日本語での出現頻度はかなり小さいはずです。jjj fff など日本語でもないから、つまらないことこの上なしです。もっと「日本語のローマ字入力のためのタイピング練習」というのがないものでしょうか。

ちょっと検索してみて、こんな意見を見つけました。「これぞ最速!ブラインドタッチ(タッチタイピング)の効率的な練習法」です。

  1. 指を基本となるホームポジションに置く
  2. パソコンのキーボードを見ながらローマ字のAIUEOを入力する
  3. キーボードを見ずに、パソコン画面を見ながらAIUEOを何度も入力する
  4. 子音を入れつつ練習する
  5. ……

そうだよなあ。この順番のほうがうんと納得がいきます。練習ソフトでこの順番になっているものは意外と見つかりません。

というわけで、既存の練習ソフトに頼らないことにしました。

前回、チャット (XMPP) のクライアント使えるようにしたので、チャットの相手側に私がいて「あお」「いおえ」……などと打ってやり、それをオウム返しに打つことにしました。日本語にない単語になってしまいますが、この際しばらく我慢してもらうことにして。

ついでに、日本語でのキーの出現頻度を検索して見つけた「ローマ字頻度表」によると、母音、n の後は t, k, s, … らしいので、この順に1字づつ (ひらがなでいえば5音づつ) 増やしていくことにしましょう。

さて、ずっと付き合うのは私もたいへんなので、Errbot が相手をするようにプラグインを書いてみました。超いい加減ですがメインはこんな感じ。

    @re_botcmd(pattern=r"^(.*)$", prefixed=False, flags=re.IGNORECASE)
    def question(self, msg, match):
        """文字列を表示する。それを入力してみてください。"""
        if not self.active:
            return None

        s = [[] for i in range(self.levelmax+1)]
        s[0] = 'あいうえおん' * 15
        s[1] = s[0] + 'かきくけこたちつてと' * 10
        s[2] = s[1] + 'さしすせそやゆよ' * 10
        s[3] = s[2] + 'なにぬねのはひふへほ' * 8
        s[4] = s[3] + 'らりるれろ' * 8
        s[5] = s[4] + 'まみむめもわを' * 6
        s[6] = s[5] + '、。ー' * 5
        s[7] = s[6] + 'がぎぐげござじずぜぞ' * 5
        s[8] = s[7] + 'だでどばびぶべぼ'  * 3
        s[9] = s[8] + 'ぢづ'
        s[10]= s[9] + 'ぱぴぷぺぽ' * 2

        reward1 = ['😀','😃','😉','🙂','❤','💗','💞','🎀','👌','✌','👏']
        reward2 = [' OK! ',' いいね! ',' ばっちり! ',' うまい! ',' じょうず! ']

        if match.group(0) == self.get('word'):
            yield(random.choice(reward1) + random.choice(reward2) + random.choice(reward1))

        self['word'] = ''.join(random.choices(s[self.level], k=self.length))
        yield(self['word'])

UI は XMPP クライアントだし応答は Errbot にお任せなので、書くのは実質ほんのわずかです。文字どおり子供だましで、そう遠からず飽きてしまいそうですが。

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

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>

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

Gajim で IRC

PC の中を整理していたら、2012年のメモが出てきました。そのまま捨てていいものか判断に悩んだので、ここに転記してから捨てることにします。

Gajim という XMPP クライアントから IRC を使う、というものです。いまさらIRCもないのですが……。

中継先の設定

まず、中継先を設定します。

操作 → サービスを探索 → … で「アドレス」に、IRC中継を行ってくれている適当なXMPPサーバーを入力します。たとえば step.im とします。「IRCトランスポート」を選択し、一番下の「登録」をクリックします。これで、名簿の「中継先」の欄に、irc.step.im ができます。

チャンネルの登録

次に、その名簿の欄のサーバー名を右クリックして、「コマンドを実行…」とします。「Join channel」を選んで「進む」をクリックします。

IRCチャンネル(先頭には#は不要)に、たとえば「wordpress」と入力し、IRCサーバーに「irc.freenode.net」と入力し、「実行」をクリックします。

チャンネルに参加

操作 → グループチャットに参加 → … → 新しいグループチャットに参加 と進み、

ニックネーム (適当に)
談話室 wordpress%irc.freenode.net
サーバー irc.step.im
パスワード (適当に)

のように入力します。談話室名は、IRC のチャンネル名とサーバー名を「%」でつなげたものにします。ニックネームは適当ですが、NickServ に登録しているならそれを入力し、パスワードもそれに合わせます。

……と、5年ほど前のメモを見ながら試しにやってみてできはしましたが、何しろいまでは IRC をぜんぜん使っていないので、いったい何の役に立つのやら。

Google Talk は生きている、のか?

先日、ChatSecure という携帯端末向け XMPP クライアントを試してみたとき、Google Talk (Google トーク)をふつうの XMPP として使えることに気がつきました。

2015年の初めころ、Google Finally Kills Off GoogleTalk and XMPP (Jabber) Integration などの記事を見て、XMPP との互換性を捨てて「Google ハングアウト」に移行する形で Google Talk はなくなったものだとばかり思っていました。そう、No, it’s not the end of XMPP for Google Talk というタイトルの記事を読んでいても、です。

これらの記事を目にした前後からついその時に至るまで、自分がふだん使っているクライアント Gajim で Google Talk のアカウントにログインできなくなっていました。それで「ああ、終わったんだな」と思い込んでしまったのです。

ところが先日 ChatSecure を試した際、その ChatSecure では GMail アカウントを用い、まったく他所の XMPP サーバーのアカウントの間でチャットできたのです。

前の記事を今よく読み返してみると、

Note that you can still continue to use the Google Talk Service with a third party XMPP client and the Google Talk XMPP servers continue to federate with other domains.

とあります。

ではどうして、PCのクライアントからはログインできなかったのでしょう? いろいろ検索して調べてみてわかったことは、Google アカウントの「2段階認証」を有効にしていたためにクライアント Gajim からログインできなかったようです。自分もこの2段階認証に切り替えたのが、ちょうど前の記事が出てそれを読んだ頃だったのかもしれません。

アプリ パスワードでログイン」というヘルプがその解決法でした。そこにあるとおりにパスワードを生成し、それを Gajim で使うことで無事にログインできました。いったんログインできてしまえば、あとはまったくふつうの XMPP アカウントとして使うことができます。

時系列的にまとめると

  • Google は 2005年ころ Google Talk というサービスを始めた。それは XMPP の拡張で、サードパーティの XMPP クライアントでも利用でき、また他の XMPP サーバーとも相互に会話できた。
  • Google は 2013年に Google Hangout という、Google Talk 後継のサービスを始めた。この時点で「Google Talk というサービスは終了」と言われているが、Google Hangout の機能のうちの文字によるチャットは Google Talk 互換であり、つまり XMPP として利用できた。
  • 2015年2月、Google 自身による Google Talk アプリ(Windows版)は廃止された。(Android版や、FirefoxやChromeの拡張機能の版も存在していたと思うが、たぶん同じ頃に消えたのだろう)
  • この2015年2月に「Google Talk は死んだ」と言われた。たとえば You Have No Choice: Google To Shutdown GTalk Feb. 23, Hello Hangouts など。しかしその記事でもじっくり読んでみると、However, users who are unable to give up GTalk can use third-party Windows apps, such as the open-source Miranda IM, Jitsi, and Psi, to continue using GTalk. と最後のほうに書いてある。

ということのようです。ただし「安全な接続」については注意が必要です。それこそ前の記事に書いたような、OTR など終端間暗号化(E2EE)を用いるのがいいかもしれません。

ChatSecure — 携帯端末向け XMPP クライアント

ChatSecure という XMPP クライアントの存在を教えられて、手元の Android 端末にインストールしてみました。

インストールは簡単。初期設定はまずアカウントです。Android端末だとたいていは google アカウントが入っているはずで、ChatSecure にもそれを使うかという画面になります。他の XMPP サーバーにアカウントがあってそれを使いたい場合は横にスワイプしてそこで設定します。

ChatSecure の特徴は、このアプリの名前や開発元(The Guadian Project)からわかるように、何と言っても「暗号化」です。OTR (Off-the-Record) という規格にしたがって、終端間暗号化でチャットできます(もちろん相手先もこれに対応していること)。 OTR については、「OTRでオフレコチャット!」の記事などが詳しいです。

「大した内容のチャットじゃなし、暗号化なんて別にどうでもいい」と思うかもしれませんが、巷で流行している LINE のようなサービスと比較して書いてみます。

クライアント-サーバー間が暗号化されているか

これは TLS(SSL) でも実現できます。Wi-Fi 接続やら、インターネット接続プロバイダー、回線会社など途中経路での盗み見・改竄を防げます。

サーバー-サーバー間が暗号化されているか

独立していて他のサーバーとの相互乗り入れができない、ほとんどのメッセージングサービスではあまり関係がないかもしれません。XMPP はサーバー間の相互接続が当たり前ですが、それらのサーバーが適切に設定されていればサーバー-サーバー間も暗号化されています(ちょっと古い記事ですが、Support for STARTTLS and SASL in s2s Connections の図がちょうどいいイメージです)。

サーバー内でも暗号化されているか

TLS でクライアント-サーバー間が暗号化されていても、たいていの場合、サーバー内では復号されて相手先の読めるところに保存されます。サーバー内で何がなされているか、ユーザーは知るすべがありません。サーバーの運営者を信用できるかどうかにかかってきます。発信時に個々のメッセージを暗号化(つまり終端間暗号化 End-to-End Encryption (E2EE))すればここで盗み見・改竄されることを防げます。

端末内の余計な情報を収集していないか

ChatSecureこれは通信の暗号化とは関係ありません。たとえば LINE では、端末内のアドレス帳など余計な情報を収集したりすることがあります。LINE のアプリはソースが公開されていませんので、本当のところ何をしているかはわかりません。独立型チャットサービスでしかもサードパーティのクライアントが許されていないようなものは、運営者をもう単に信じるかどうかという問題です。XMPP や OTR といったオープンな規格、ejabberd のようなオープンソースのサーバー、ChatSecure のようなオープンソースのクライアントという組み合わせでは、そのような信頼できない行為を隠しようがありません。

ChatSecure は OTR を使わなくてもよくできた XMPP クライアントだと思います。見た目は直感的で、操作に戸惑うこともあまりなさそうです。日本語化もされていますし、さほど詳しくないような人にもお勧めできます。

チャットサポートを構築する (その4) 連絡先の設定とまとめ

【2018年4月20日追記】この記事は内容が古くなっている部分があります。「あらためてチャットサポートを構築する」もご覧ください。【追記ここまで】

共有名簿 (shared roster)

ふつうの XMPP では、相手先名簿 (roster) は JID に紐付けられ、サーバーに保持されています。ところが匿名サーバーの場合、接続要求があった際にそのアカウントが新たに一時的に生成されるため、その JID には相手先名簿がありません。そこで、XMPP の共有名簿(shared roster)という機能を使うことにします。

ejabberd の設定ファイル ejabberd.cfg では

{modules,
 [
  ...
  {mod_shared_roster,[]},
  ...
]}.

とモジュールを有効化しておきます。

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

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

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

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

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

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

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

チャットサポート構築のまとめ

ここまでで、どうにかチャットサポートを構築できました。まとめますと、「チャットサポート」に求められる要件、

  1. アプリケーションの事前インストールが不要
  2. アカウント(JID)の登録が不要
  3. 連絡先が登録済み

に対して、(1)は Converse.js を導入することで、(2)はクッキーを利用し匿名サーバーを設定することで、そして(3)は共有名簿 (shared roster) を使うことでクリアして、一応「チャットサポート」的なものができました。そしてこれらを WordPress のサイトに導入するために プラグイン化しました。試しにこのサイトに設置してみました。画面の右下に小さなタブが見えると思います。

とりあえずはできたものの、課題も残っています。

前にも書いたとおり、事前接続のところがよく理解できていないこともあって、匿名サーバーだと使えますが、特定の JID を事前設定しようとするとうまくいきません。また、ページ遷移に対応できていません。

匿名サーバーの場合、共有名簿を使ってみましたが、多重に読み込んでいるのか、同じ相手先が増殖することがあります。どういうことなのかよくわかりません。また、これは匿名サーバーの宿命なのですが、接続した時点で相手先からは認証されていないので(その時点で生成された ID なので当然)、その相手先の在席状況が不明で「オフライン」と表示されてしまいます。印象が悪いので、WordPress のプラグインではこれを隠すようにしてみました。

今回、恥を忍んで中途半端な状態でも公開してこのような文を書いたのは、誰か能力のある人がちゃんとした実装を教えてくれることを期待してのことです。XMPP ならまだしも、Strophe.js や Converse.js で検索しても、とにかく日本語での情報は非常に少なく、どこで聞いたらいいかもわからないほどです。

何かわかる方がありましたら、教えてください。または情報をぜひ公開してください。お願いします。