[Firebase] Firestoreでログイン中のユーザーを取得する

SNSやSlackなどのアプリではログイン中のユーザーがマーク等でわかるようになっています。
（オンラインでアクティブになっているユーザーやデバイスを検出する事を**プレゼンス**の検出とも呼びます）
こうした機能をFirestoreで構築できないかと調べたものを備忘録としてまとめます。

結論から言って、Firestore単品ではプレゼンスを構築することは現状できないようです。（必要なAPIがないため）
Firestoreを使ってプレゼンスを構築するためには、
**Firestore**と**RealtimeDatabase**、**FirebaseCloudFunctions**を組み合わせる必要があります。

# 手順1 RealtimeDatabaseのプレゼンスを準備

Firestoreではオンラインになったことは検知できてもアプリを切断したりオフラインになった場合の検知はできないため、RealtimeDatabaseのAPIを使用します。

```js:JS
const uid = firebase.auth().currentUser.uid;

// プレゼンス情報の格納先
const userStatusDatabaseRef = firebase.database().ref('/status/' + uid);

// オフライン時の設定値
const isOfflineForDatabase = {
    state: 'offline',
    last_changed: firebase.database.ServerValue.TIMESTAMP,
};

// オンライン時の設定値
const isOnlineForDatabase = {
    state: 'online',
    last_changed: firebase.database.ServerValue.TIMESTAMP,
};

firebase.database().ref('.info/connected').on('value', function(snapshot) {
    // オフライン時は何もしない
    if (snapshot.val() == false) {
        return;
    };

    // onDisconnect()を使って、オフライン時の処理を予約しておく
    userStatusDatabaseRef.onDisconnect().set(isOfflineForDatabase).then(function() {
        // オンラインにする
        userStatusDatabaseRef.set(isOnlineForDatabase);
    });
});

```

# 手順2 firestore側の準備
手順1でRealtimeDatabaseにプレゼンス情報は格納されるので、その情報をFirestore側にコピーします。

```js:JS

// プレゼンス情報の格納先
const userStatusFirestoreRef = firebase.firestore().doc('/status/' + uid);

const isOfflineForFirestore = {
    state: 'offline',
    last_changed: firebase.firestore.FieldValue.serverTimestamp(),
};

const isOnlineForFirestore = {
    state: 'online',
    last_changed: firebase.firestore.FieldValue.serverTimestamp(),
};

firebase.database().ref('.info/connected').on('value', function(snapshot) {
    if (snapshot.val() == false) {
        // RealtimeDatabase側がオフラインなら、firestore側もオフラインにする
        userStatusFirestoreRef.set(isOfflineForFirestore);
        return;
    };
    // onDisconnect()を使って、オフライン時の処理を予約しておく
    userStatusDatabaseRef.onDisconnect().set(isOfflineForDatabase).then(function() {
        // オンラインにする
        userStatusDatabaseRef.set(isOnlineForDatabase);
        userStatusFirestoreRef.set(isOnlineForFirestore);
    });
});

```

# 手順3 CloudFunctionsでグローバルにプレゼンスを更新する
オフラインステータスでの書き込みはローカルのみで行われ、接続が復元したときに同期がされません。
つまり、自身のアプリ側ではオンラインを検知できますが、他のFirestoreのステータスは最新ではありません。
そのため、RealtimeDatabseの`/status/{uid}`を監視し、値の変更をトリガーにFirestoreに同期することで、すべてのユーザーに正しく表示されます。

```js:Node.js

const functions = require('firebase-functions');
const admin = require('firebase-admin');
admin.initializeApp();

const firestore = admin.firestore();

exports.onUserStatusChanged = functions.region('asia-northeast1').database.ref('/status/{uid}').onUpdate(
    async (change, context) => {
      const eventStatus = change.after.val();

      const userStatusFirestoreRef = firestore.doc(`status/${context.params.uid}`);

      const statusSnapshot = await change.after.ref.once('value');
      const status = statusSnapshot.val();
 
      if (status.last_changed > eventStatus.last_changed) {
        return null;
      }

      eventStatus.last_changed = new Date(eventStatus.last_changed);

      return userStatusFirestoreRef.set(eventStatus);
    });

```

この関数をデプロイすればプレゼンスの構築は完了です。

# さいごに

firestore側にプレゼンス情報が格納されたら、あとはクエリでオンラインのユーザーを取得し
いろいろできると思います。

今回は一度RealtimeDatabaseでプレゼンスを取得しそれをFirestoreにコピーしてますが、
そもそもFirestoreにコピーする必要があるのかという疑問もあるかと思います。
メリットとしてはFirestoreの格納先を工夫すれば1つのクエリでユーザ情報（プレゼンス含む）を取得できます。
しかし、RealtimeDatabaseとFirestoreの二重管理である点も否めないので、ベストプラクティスとは言い難い気もします。。
ここら辺の機能でFirestoreがアップデートがされた時は、また記事を書ければと思います。

最後までお読みいただきありがとうございました。