Personiumでのイベント発火をクライアントでリアルタイムに受け取る


## Personiumってなに？

Personiumはいまホットな`PDS (Personal Data Store)`を実装に落とし込んだサーバソフトウェアです。
`PDS`は、個人情報について「どの情報をどの人に開示するか」を個々人がコントロールできる`自己情報コントロール権`を実現します。

Personium自体についてはTakkyさんのQiita連載がとても為になるので、そちらを参照してください。
[情報銀行のプラットフォームとして利用できるかもしれないPersonium（PDS） を検証してみる １ ＜はじめに＞](https://qiita.com/takky0330/items/5352a820e5e59747218c)

## 対象とする読者・書きたい内容

Personium上でアプリケーションを開発している方。今回の記事では以下のことをご紹介します。

- セルマネージャでのイベント参照権限の付与
- クライアントアプリ側でのWebSocketによるリアルタイムイベント購読
- オレオレイベントの発火（`event.post`関数）

この記事に乗っていることはたいてい[ドキュメント](https://personium.io/docs/ja/apiref/current/279_Event_Bus_Connect.html)を読めばわかる内容です。
しかし仕様は網羅的に書いてあるものの、結局「やりたいこと」を実現するための逆引きができない現状ですので、
こういった「やってみた」記事を備忘的に書きます。

~~OSSなんだからドキュメントにコントリビュートしろと言われればそれまでですが~~

オレオレイベントの発火についてはドキュメントに記載がないため、今後変更される可能性があります。

また、現状（1.7.12）のPersoniumではイベント購読を許可するとすべてのイベントが購読可能になりますので、
本記事での使い方は開発目的以外で使用すると問題が起こる可能性があります。

## 実装

今回はPersoniumのデモ環境を使用します。デモ環境のセルはPersoniumコミュニティでお願いすると割り当ててもらえます。

[personium.io](https://personium.io/ja/index.html)

### セルマネージャでのイベント参照権限の付与

まずは、セルで発生したイベントを参照できる「ロール」を作成し、そのロールを「アカウント」に割り当てます。

#### セルマネージャへのログイン

1. `https://<CELL_FQDN>/io_personium_demo_cell-manager/src/login.html`にアクセスし、ログインします。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/328319/f54e5eb4-395e-9b00-6839-275525b889d6.png)

#### ロールの作成

1. ロール作成ダイアログを開く
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/328319/a708f00c-ee85-e22f-9c53-0b73555a35c4.png)
2. ロール名を決める
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/328319/55c012b7-326c-439b-684b-cc41a792793e.png)
3. Createボタンを押し、ロールが追加されたことを確認する
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/328319/5c9c9e8a-7ac0-d927-f932-5ca0601c9781.png)

#### ロールへのイベント参照権限の付与

1. Privileges設定画面を開く
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/328319/4fe3af37-5b99-796d-1895-896da04e33d6.png)
2. EventListenerロールに`Event` `Event-Read`を付与する
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/328319/c6a1035a-172f-a0ed-022a-e13e1c7c25bd.png)

#### `EventListener`ロールを付与したアカウントの作成

1. アカウント作成画面を開く
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/328319/ca76f9a7-8db9-3380-e2be-5d40f457c9bf.png)
2. `EventListener`ロールを付与したアカウントを作成する
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/328319/3f5de65c-2449-e38d-fad4-cd228c01b41f.png)

### イベント購読用トークンの発行

イベント購読可能なアカウント「eventListener」の認証トークンを返すエンドポイントを作成します。

```javascript
// Login
function init(request) {
  try {
    // 認証に使用するメソッドをPOSTに限定する
    personium.validateRequestMethod(['POST'], request);
    // アクセス元のWebページURLを照合する
    personium.verifyOrigin(request);

    // セルで認証し情報を取得する
    const appCell = _p.as("serviceSubject").cell();
    // トークンを取得する
    const ret = appCell.getToken();
    // ステータスコード200で結果（トークン）を返す
    return personium.createResponse(200, ret);
  } catch (e) {
    return personium.createErrorResponse(e);
  }
}

var _ = require("underscore")._;
var personium = require('personium').personium;
```

内部のpersoniumオブジェクトについては、[template-app-cell](https://github.com/personium/template-app-cell) に
同梱されているファイルを借用しました。

ここでは`serviceSubject`という設定値を用いていますが、これを`eventListener`に設定することで、
エンドポイントが`EventListener`のRoleで処理を行うように設定できます。
→ `serviceSubject`については [こちら](https://personium.io/docs/ja/apiref/current/380_Configure_Service_Collection.html)
→ （`serviceSubject`登録方法はリクエストがあれば書きます。）

**上記コードは模擬的なコードであり、来た全POSTリクエストに対してトークンを返してしまいます。**
**アクセス制限など、セキュリティ的な考慮を別途行ってください**

### WebSocketを用いた購読

イベント購読用エンドポイントを作成できたら、そことの通信を行い、実際にイベントを購読できるクライアントを実装します。

#### PersoniumWebSocketAdapter 

まずはWebSocketを司る `PersoniumWebSocketAdapter` です。

```javascript

class PersoniumWebSocketAdapter {
  constructor(appCellUrl, tokens) {
    this.appCellUrl = appCellUrl;
    this.tokens = tokens;
    this.ws = null;
    this.messageListeners = [];
  }
  handleClose(ex) {
    console.log('onclose#webSocket', ex);
  }
  handleMessage(ex) {
    console.log('onmessage#webSocket', ex);
    const jsonDat = ex.data ? JSON.parse(ex.data) : null;
    this.messageListeners = this.messageListeners.filter(
      listener => !listener(ex, jsonDat)
    )
    console.log("messageListener", this.messageListeners.length);
  }
  addListener(listener) {
    this.messageListeners.push(listener);
  }
  open() {
    return new Promise((resolve, reject) => {
      const wssAppCellUrl = appCellUrl.replace('https://', 'wss://');
      try{
        this.ws = new WebSocket(`${wssAppCellUrl}__event`);
        this.ws.onopen = (ex) => {
          console.log('onopen#webSocket', ex);
          resolve(true);
        }
        this.ws.onclose = (ex) => this.handleClose(ex);
        this.ws.onmessage = (ex) => this.handleMessage(ex);
      } catch(ex) {
        reject(ex);
      }
    });
  }
  subscribe(strEventType, strEventObject) {
    return new Promise((resolve, reject) => {
      this.addListener((ex, jsonDat) => {
        if (jsonDat.Response === "Subscribe") {
          console.log('Subscribe is accepted');
          resolve(true);
          return true;
        }
      });
      const requestObj = {
        Subscribe: {
          Type: strEventType,
          Object: strEventObject,
        },
      };
      this.send(JSON.stringify(requestObj));
    });
  }
  auth(AccessToken) {
    return new Promise((resolve, reject) => {
      this.addListener((ex) => {
        const jsonDat = JSON.parse(ex.data);
        if (jsonDat.Response === "AccessToken") {
          console.log('AccessToken is accepted');
          resolve(true);
          return true;
        }
      });
      this.send(JSON.stringify({AccessToken}));
    });
  }
  send(str) {
    this.ws.send(str);
  }
  close() {
    if (this.ws) this.ws.close();
  }
}
```

クラスの形で実装しています。このクラスを使用することで、下記ドキュメント記載の必要なステップを実行できるようになっています。
⇒　[イベントバスへの Web Socket 接続](https://personium.io/docs/ja/apiref/current/279_Event_Bus_Connect.html)

イベントの購読には以下のステップに従います。

1. WebSocket接続を確立する　→　`PersoniumWebSocketAdapter.open` メソッド
2. アクセストークン送付によるセッション確立　→　`PersoniumWebSocketAdapter.auth` メソッド
3. イベント購読対象の指定　→　`PersoniumWebSocketAdapter.subscribe` メソッド

#### クラスを使用したクライアントの実装

上記クラスを使用したサンプルが以下の通りです。

```javascript
// 対象セル
const appCellUrl = 'https://<CELL_FQDN>/';
// eventListenerのトークン取得エンドポイント
const appAuthURL = `${appCellUrl}__/html/Engine/getEventListenerToken`;
const accessData = {
  tokens: null,
  wsAdapter: null,
};

(async () => {
  let result = await fetch(appAuthURL, {method: 'POST'});
  if (!result.ok) throw new Exception("Cannot fetch EventListener token");

  let tokens = await result.json();
  accessData.tokens = Object.assign({}, tokens);
  accessData.wsAdapter = new PersoniumWebSocketAdapter(appCellUrl, tokens);

  // 1. WebSocket接続を確立する
  if (!await accessData.wsAdapter.open()) {
    throw new Exception("Cannot open websocket connection");
  }

  // 2. アクセストークン送付によるセッション確立
  if (!await accessData.wsAdapter.auth(accessData.tokens.access_token)) {
    throw new Exception("Cannot auth with EventListener token");
  }

  // 3. イベント購読対象の指定
  // EventType: 'console.log', EventObject: '*(すべて)'
  if (!await accessData.wsAdapter.subscribe('console.log', '*')) {
    throw new Exception("Cannot subscrive events");
  }

  // イベント受信時の処理記述
  accessData.wsAdapter.addListener((ex, jsonDat) => {
    // p タグ部分に内容を追加していく
    const child = document.createElement('p');
    child.innerHTML = jsonDat.Object;
    document.getElementById('root').appendChild(child);
  });
})();
```

### イベントのトリガ

任意のエンジンスクリプトから`オレオレイベント`をトリガ（発火）できるようにしましょう。

```javascript
const console = console || {};
console.log = function(str) {
  // セルを取得
  const appCell = _p.as("serviceSubject").cell();
  // 取得したセルに対しイベントを発火
  appCell.event.post({
    Type: 'console.log',
    Object: str,
    Info: 'infoData',
  });
};
```

Personiumのエンジンスクリプト内では`console`系が使えないので、横着しています。
ここで実装した`console.log`を使用することでクライアント上でリアルタイムに出力を確認することができます。


## 結果

上記コードでこのような形でリアルタイムなやり取りを実現できます。

![out.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/328319/61c9424b-4111-7f95-e290-546ddecb9dd0.gif)

画面向かって左側が今回作成したクライアントで、右側が実際にイベント発火用エンドポイントを開いた状態です。

右側のエンドポイントにアクセスすると内部的に上記の`console.log`を実行する処理が走り、
Personiumのイベントハブを経由して左側のクライアントで取得できる、という構造になっています。

## 終わりに

自身が作成するアプリのエンドポイントのデバッグにあたり、リアルタイムなログが見られないのを不便に思い、実装しました。

初Qiitaなので勝手がわからず、色々とアラが目立つかも知れませんが、どなたかのお役に立てば幸いです。
編集リクエスト・サンプルコードへの指摘等も大歓迎です。

## 参考URL

- [イベントの概要](https://personium.io/docs/ja/apiref/current/277_Event_Summary.html)
- [イベントバスへの Web Socket 接続](https://personium.io/docs/ja/apiref/current/279_Event_Bus_Connect.html)