IPv6 接続を確認する方法

LAN 側からインターネット側へ

「手元の端末からインターネットへ IPv6 で接続できているか」をチェックするサイトは、test-ipv6.comIPv6-test.com など、たくさんあります。こういうところをブラウザで閲覧すれば、サイト側で接続元の情報を表示するなどして教えてくれます。

ブラウザの拡張 IPvFoo を利用すると、任意のサイトに IPv4 か IPv6 のどちらで接続しているかを知ることができます。これを入れていろいろ見て回ったら、IPv6 に対応していないサイトは案外とまだ多いのだと思いました。

インターネット側から LAN 側へ

今どきは少ないのかもしれませんが、自宅サーバーを設置しようという場合など、逆向きの情報を知りたいときがあります。インターネット側からこのサーバーに IPv6 で到達できるか、を調べるにはどうすればいいでしょう。

IPv6 でインターネットに接続できている別の環境からアクセスできるか試せればいいのですが、にわかにはそれが用意できません。自分でスマートフォンを使って、Wi-Fi を切って電話回線経由でと思ったら、現時点ではほとんどの MVNO は IPv6 に未対応でした。

そこで、自動化されているサイトはないものかと探してみました。【以下、この記事の執筆時点 (2018年12月) の状況です。おそらく数年で大きく状況は変わると思います。】

一般に、ブラウザテストのサービス (たとえば browserling とか WebPagetest など[1]) を利用すれば、そのサイトに到達できてどのように閲覧されるかを知ることができます。しかし、ここで例に挙げたサービスでも「IPv6 で」かどうかはわかりません。現時点ではおそらく IPv4 経由と思われます。また、ポート番号が80か443以外でそれを指定しなければならない URL は受け付けなかったり、トップではないページを指定できなかったりします。

さんざん探し回った挙げ句に見つけたのが、まずは外部から IPv6 でポートスキャンをやってくれるところ。自宅サーバーのターミナルで w3m を動かして、この3番か4番でチェックします。これで少なくとも外から到達できているかどうかが確認できます。

もう一つが CA App Synthetic Monitor の Web サイト チェック。「IPv6 がサポートされるようになりました 」と謳っています。チェック対象サイトの IP アドレスが表示されるので、その形式で IPv4 か IPv6 のどちらで接続しているかわかります。スクリーンショットまではありませんが、「ダウンロードサイズ」からおそらくちゃんと到達できているだろうと推測できます。このサービスでは、ポート番号指定の URL やトップでないページでも大丈夫でした。

「サーバー」と言っても WWW だけではありません。しかし上の2つで IPv6 での接続が確認できたので、WWW 以外のサービスもおそらく無事に IPv6 で開放できていると考えることにしました。

  1. もうひとつの例、WAVE は単にスクリーンショットを取ってくれるのではなく、いろいろアドバイスしてくれるので何かと便利です。

Emacs 風キーバインド

先日、久しぶりに勉強会のようなところへ出かけてみました。そこで会の主要なテーマとは直接関係のない Mac のキーバインドの話を聞きました。私は Mac にはほとんど触れたことがないので知りませんでしたが、おやこれは「Emacs風キーバインド」ではないかと思いました。起源はもう忘れられているかもしれません。

さて、そこで Debian をデスクトップで使っているという自分の環境を思い返してみました。エディターは主に Emacs なので、いろいろなところを「Emacs風キーバインド」にしたいと思いながら、ウィンドウマネージャーを変更したりブラウザーがバージョンアップして拡張機能が効かなくなっていたり、と、いつの間にか不便な状態になっていて、まあそのうち、と思いながら過ごしていたのです。

これを機会に、ちょっと探してみました。ほとんど自分用メモですが、Mac から Linux に乗り換えたい人への情報になるかもしれません。

Xfce4

以前 Gnome を使っていた頃はどこかで設定していたのですが、数年前に Xfce4 に切り替えてから、どうすればいいのか調べてもおらず、そのままになっていました。

「設定」→「設定エディター」で、「チャネル」の一番下の xsettings を開き、Gtk のグループの中のプロパティ KeyThemeName の値に Emacs と入力します。

これで、Xfce4 上で開く多くのアプリケーションで、C-aC-eC-k などが有効になりました。ただし C-y で取り出せるバッファが C-k と連動していないところが Emacs や次の bash と違う挙動で、ときどき混乱してしまいます。

Bash

ターミナル内のシェル は bash です。これは元々「Emacs風キーバインド」のようで、入力行で C-aC-eC-k が使えて、C-k したものを C-y で貼り付けることができます。C-pC-n で履歴をたどれるなど、かなり多くのキーバインドが使えます

Firefox

以前は、拡張機能でそういうのがあったのですが、ブラウザー本体のバージョンアップで使えなくなっていました。

上述の Xfce4 の設定で、いくらかのキーが使えるようになりました。しかし、上下に動かしたいときについ C-pC-n と打って「印刷」「新しいウィンドウを開く」になってしまったり、ページ内検索のつもりでつい C-s と打って「ファイルに保存」になってしまうことがしょっちゅうなので、この辺りを補ってくれるものはないかなあと思っています。

GhostText

Firefox の入力欄 (textarea) で、好きな外部エディターを使えます。以前は別の拡張機能を使っていたのですが、やはりブラウザー本体のバージョンアップで使えなくなってしまい、また別の拡張機能も WordPress の新しい入力画面 Gutenberg との相性が悪そうなので、現在は GhostText を使っています[1]

Emacs 側でも準備が必要で、atomic-chrome パッケージをインストールして、設定しておきます。

  1. GhostText という拡張機能は Firefox にも Chrome にもあり、元々 Emacs 以外のエディターを使うものだったので、それらのどの組み合わせでも使えます。

HTML の <ruby> に思うこと

ニュースサイトから記事本文を抜き出して利用しようという作業をやってみて、<ruby> 要素がほかの要素に比べてかなり異質なものあることに気がつきました。

ニュースの本文は

...<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 要素を削除しておく必要があります。

実際に行った作業は「HTML からタグを除去して本文を取り出す」なのですが、単純に行って得られるものは上記のとおりで、それを「本文」とは呼べないと思うのです。

マークアップ言語

はじめに、

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

という“内容”そのものがあります。これに“意味”をつけるために、たとえば「気象庁」は固有名詞という意味をつけるためにこれを <span> で囲み、それからこの文ひとつでひとつの段落という意味をつけるためにこれを <p> で囲みます。これがマークアップであり、そのためのタグであるはずです。

次の段階で、これをどう表示するか、たとえば固有名詞は文字を赤色にし、段落はそのはじめに改行する、というものはさらに分離してスタイルシートで記述することになっています。

逆向きに考えてみます。ブラウザで表示されるもののソースから表現のスタイルを取り除き、“意味”づけのタグを取り除いたら、残るのは“内容”そのものでなければならないはずです。

ところが今回の例では上述のように、余計なものが残ったままになります。

ルビは“内容”か

私は「ふりがなは内容そのものではなく、それに付け加えているもの」と考えますので、「余計なもの」と書きました。たとえば、ここの言葉は重要と思って下線を付ける、固有名詞ははっきり分かるように文字の色を他と違える、というのと同じで、内容に対して何らか助けのために付け加えているものだと思います。ですから、そういったものを剥ぎ取ったときには残っていてはならないと考えます。

そう考えたときに、現在の仕様の <rt> はかなり異質です。下線や色付けがタグ内の属性 (attribute) とそれに紐付けられるスタイルで表現されるように、ふりがなもタグではなく属性で示されるのが適当だと思います。

仮に <span>ruby という属性が定義されているとして

...<p><span class="colorC" ruby="きしょうちょう">気象庁</span>は「...」と<span ruby="はな">話</span>しています。</p>...

と書くのがもっとも自然のように感じます。

既存の仕様である <a> を逆に考えてみましょう。<a> にとって属性 href は、それがなければほとんど意味がないくらい重要なものです。

もっと<a href="example.html>二酸化炭素</a>を出さないようにしなければなりません

だからと言ってもしそれがタグになっていて

もっと<a>二酸化炭素<href>example.html</href></a>を出さないようにしなければなりません

と書かれるものだとしたら、その違和感がわかろうというものです。

<rt> のあり方

前節に書いたように、ルビは属性で指定されるのが自然だと思います。ではどうすればいいのか。

<ruby>関連要素って駄目駄目」に、この問題の歴史 (なんと不幸な!) から対案まで解説されていました。この記事をぜひじっくり読んでいただきたい。

この記事の「脱法ルビ」が、当面の対応策です。仕様に反せず、仕様内でできる策です。現状ここまでできるのですから、ここに示されているスタイルをレンダリングエンジン側で装備し、属性名を data-ruby ではなく ruby とできるよう仕様のほうが改訂されたら、当分は現在の <ruby>, <rt> との共存期間があったとしても、もうほとんど解決ではないですか。

……と思ったら、このスタイル、すなわち display: ruby-text を実装しているのは Gecko (Firefox) だけで、Webkit (Safari) も Blink (Chrome) も未対応でした (2018年6月現在)。ひどい状況です。

ルビを実際に使う人やデザイナのほとんどは、ほんの少し首をかしげるくらいはあっても、仕様に従って使うだけで、そのしようがどうあるべきかまで問い返したりはしません。そもそも世界のうちでごく限られた言語だけでしか知られておらず、その中でも使う人はごく少数ですから、問題にする人も滅多にいなければ対応もぜんぜん進まないのでしょう。ほんとうに不幸です。

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

ひらがな数文字を打ち返すだけのタイピング練習は案の定すぐに飽きてしまったので、何か別のネタを考えなくてはならなくなりました。飽きないためには膨大かまたは頻繁に更新される元データがあればいい、青空文庫かな、でも小学生に向いているものがどれほどあるかしらん、頻繁に更新されるといえばニュース、でもこれまた小学生向きではなさそう……と思ったら実にぴったりのものがありました。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 にお任せなので、書くのは実質ほんのわずかです。文字どおり子供だましで、そう遠からず飽きてしまいそうですが。

ノートパソコンに Debian をインストール

スウちゃん (仮名、4年生) が「ノートパソコンがほしい」と言い出しました。学校でも PC に少し触ることがあるし、家でも親がスマホやタブレットでなく大きな画面でカタカタやっているので興味はあるのでしょう。ほう、では何をやりたいか書き出してごらん、と言うと、「字を打てるようになる」「ゲーム」「メール」「プリント」とのこと。「プリント」って何と聞いてみると、宿題などのために調べ物をしたらそれをプリンターに出したいということでした。

ラップトップ型パソコン

興味を持ったときがチャンスだと思い、早速探してみました。はじめ中古は考えていなかったのですが本人に「中古でもいい?」と聞いてみたらまったく気にしないとのことで、1年落ちで14インチ、メモリ8GB、SSDが240GBという一段上のスペックのものが安い価格で見つかり早速購入。いまどき OS なしのパソコンを探すのは難しいので Windows10 も込みです。状態説明で「タッチパッドにテカリあり」となっていましたが、仮に新品でも使いはじめたらすぐにこのくらいになってしまうだろうという程度で、それに普段使う角度から見るとまったく気になりません。ほかに特に問題はなく、今のところいい買い物だったと思っています。

さて Windows10 が入っていても、私自身が普段使いしておらず面倒を見きれないので、さっそくおさらばすることにします。購入時に聞いたのですが Windows のプロダクトキーはどこかに記載したものは一切ないとのことでしたので、一度 Windows10 で起動して確認し (その方法はネットで探しました)、控えておきます。これで、そのうちどうしても他所と同じ Windows じゃないとと言い出した時には戻せます。

Debian stretch のインストール

現時点での安定版 Debian stretch をインストールしていきます。

まず親機で netinst イメージ (300MB ほど) を USB メモリにコピーします。これをラップトップ機に挿して起動し、あとは画面の指示に従っていくだけです。途中、ハードウェアを解析し必要なものを要求してくるので、それ (firmware-iwlwifi の deb パッケージ) を親機で 別の USB メモリに入れて、ラップトップ機の別の USB の口から与えました。ネットがつながればあとはあっさりインストール完了です。

task-xfce-desktop を選択したので、LibleOffice はじめ一通りのアプリケーションが揃っています。日本語変換は fcitx と mozc (キー設定はMS-IME互換) の組み合わせ。デスクトップのパネルを下に配置することで Windows にかなり似せました。「学校でさわるのとちょっと違うよ」と言い、スウちゃんも今の時点では難なく受け容れています。

スウちゃんの希望 (「ゲーム」は昨年のクリスマスに Nintendo Switch をもらったこともあって却下) に沿うため、thunderbird をインストールしましたが、実際にはまだ使っていません。XMPP (チャット) クライアントの Gajim を入れてやり、同じ家の中から私が話しかけてやります。スピーディだし、絵文字もすぐ選べるから楽しいし、なんとか返答しようとしてキー入力もやります。動機づけには十分です。

印刷のためには、cups のパッケージを一通りと、うちにあるのは Brother のプリンターなので、メーカーのサイトから入手したドライバーをインストール。そうか、そのドライバーは i386 用なので、その前にマルチアーキテクチャ対応にする必要がありました。

久しぶりにクリーンインストールを行いましたが、それにしても Debian のインストールからデスクトップ環境構築までがこんなに楽になっているとは驚きました。

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

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>

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