ざっくりいうと
HipChat 独自仕様の <query>
付き <iq>
を投げる必要がある。
応答の一部に joined room list が含まれる。
もうちょっと具体的にいうと
以下のような <iq>
を投げると,HipChat の個人設定等が返ってくる。
<iq>
の to
のところには MUC Domain (multi-user-chat domain) を指定する。一般の HipChat では conf.hipchat.com であるが,HipChat Server (オンプレ版) では適切に変更する必要がある (おそらくデフォルトで conf.btf.hipchat.com)。(下記では省略しているが,もちろん id をつけておいたほうがレスポンスをハンドリングしやすいのはいうまでもない。)
<iq to='conf.hipchat.com' type='get'>
<query xmlns='http://hipchat.com/protocol/startup'/>
</iq>
戻り値はこんな感じ。
/iq/query/preferences/autoJoin
以下の <item>
群が join している room である。
<iq from='conf.hipchat.com' type='result' id='ID_OF_IQ' to='USER_JID'>
<query xmlns='http://hipchat.com/protocol/startup'>
<user_id>USER_ID_IN_HIPCHAT</user_id>
<group_id>GROUP_ID_IN_HIPCHAT</group_id>
<!-- OTHER SETTINGS -->
<preferences>
<autoJoin>
<item jid='ROOM_JID' name='ROOM_NAME'>
<x xmlns='http://hipchat.com/protocol/muc#room'>
<!-- INFORMATION OF THE ROOM -->
</x>
</item>
<!-- OTHER ROOMS -->
</autoJoin>
<!-- OTHER PREFERENCES -->
</preferences>
</query>
</iq>
さらに詳しく
- XMPP は,各接続クライアントが明示的に room の join (
<presence>
を送る) しないと room の messages をとれない。 - ユーザーに紐付いた joined room list をとれる XMPP 標準仕様はないっぽい。
- 一応,ユーザー (Full JID; resource まで指定された JID) の所属している room を取得する方法はあるのだが (
to
に full user jid を指定して,<query>
のnode
attribute にhttp://jabber.org/protocol/muc#rooms
を指定する; XEP-0045 Example 16 参照),これはユーザーの各ログイン状態に対応するものであるし,そもそも HipChat に投げると 501 feature-not-implemented error が返ってくる。 - そもそも XMPP 自体にユーザーが join している room を keep するという概念がないのかもしれない。
- 一応,ユーザー (Full JID; resource まで指定された JID) の所属している room を取得する方法はあるのだが (
- HipChat でも MUC Domain に対する Service Discovery を普通に発行できて,これを利用するとアクセス可能な全 room list を取得できる (が,これは求めているものではない)
- アクセス可能 = 全 public room + アクセス権限をもっている全 private room
- 「今現在」 join している room ではない
- HipChat はユーザアカウントの設定情報 (サーバサイドで記憶) として joined room list を保持している。
- たとえば HipChat Web Client は XMPP と別口で設定情報を取得し, joined rooms に再 join している。
- XMPP の場合
http://hipchat.com/protocol/startup
XML namespace の<query>
をもった iq を発行すると設定情報をとれる。- つまり HipChat 独自仕様に依存している。
- HipChat ユーザ設定情報から joined room list をとりだせる。
おまけ
- HipChat Web Client (以下 Web Client) は XMPP over HTTP (XMPP over BOSH) を使っているので, Chrome Developer Tools などで通信をみることができる。
- Web Client で使われている JavaScript は minify されているものの難読化はされていないため,探したいキーワードさえはっきりしていればそれほど読みにくくはない。また jQuery が利用されている。
- Web Client では『HipChat の個人設定等』は非同期通信によって取得しているのではなく,アプリの HTML の JavaScript の中にうめこまれている。
- グローバル変数
config
に設定されている。このため, Developer Tools の Console でconfig
と入力するとみることができる。
- グローバル変数
- HipChat 特有の
<query>
については https://bitbucket.org/hipchat/hipchat-js-client が参考になる。- hipchat アカウント下の bitbucket プロジェクトではあるが個人 (社員) 発祥のプロジェクトっぽくて位置づけが謎。
- 「これ使うと独自のクライアントが作れるぜ」と書いてあるものの,設定の取得に XHR で API たたいたりしているので node.js とかでは動かなさそうだし, HipChat Server のように手元にいじれるサーバ環境がないと難しそう。あるいは言及されているけど, Chrome の Extension などにするとか。
- ソースをみると
http://hipchat.com/protocol/startup
だけではなくhttp://hipchat.com/protocol/emoticons
など他にもいくつかの独自仕様 query がある (とはいえ,いくつかの内容は startup の応答に含まれているが)。
- hipchat アカウント下の bitbucket プロジェクトではあるが個人 (社員) 発祥のプロジェクトっぽくて位置づけが謎。
- 上記レポジトリによれば
<query xmlns='http://hipchat.com/protocol/startup' send_auto_join_user_presences="true"/>
のように発行すると自動で presence を発行してくれそう (自動 join) な雰囲気を感じるが,うまく動かない。なにか間違えているのだろうか。
実証コード
require 'xrc'
require 'stringio'
client = Xrc::Client.new(
jid: "USER_JID",
nickname: "NICKNAME",
password: "PASSWORD",
)
client.on_connection_established do
# <iq type="get" to="MUC_DOMAIN"><query xmlns="http://hipchat.com/protocol/startup"/></iq>
iq = REXML::Element.new('iq')
iq.attributes['type'] = 'get'
iq.attributes['to'] = 'conf.hipchat.com' # muc domain を指定する
query = REXML::Element.new('query')
query.add_namespace('http://hipchat.com/protocol/startup')
query.attributes['send_auto_join_user_presences'] = true # doesn't work
iq.add(query)
client.instance_eval do # post() が private method なので
post(iq) do |response|
formatter = REXML::Formatters::Pretty.new
output = StringIO.new
formatter.write(response, output)
puts output.string # 応答全体を出力
# 各 room の情報を出力
response.get_elements('./query/preferences/autoJoin/item').each do |item|
jid = item.attributes['jid']
puts "=== #{jid} ==="
next if jid !~ /@conf\./
puts "name = #{item.attributes['name']}"
puts "privacy = #{item.elements['.//privacy'].text}"
puts "owner = #{item.elements['.//owner'].text}"
puts "topic = #{item.elements['.//topic'].text}"
end
end
end
end
client.connect