5
5

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 3 years have passed since last update.

Firebase で作るフレンドシステムの一例

Posted at

先週リリースしたNotespodというメモアプリでフレンドシステムを実装しました(後付けで)

備忘録もかねて記事にしておこうかと思った次第です。アプリはFlutterですが、この実装自体はFlutterに依存するものではないと思います。
あくまで一例ですが、参考になれば幸いです(もっといいやり方あったら教えてください)

フレンドシステムとは

基本的にゲームに多いんですが、ソシャゲとそれ以外では仕組みが異なる場合が多いです。自分のアプリで実装したのはフレンドコードを登録するタイプのものです。プラットフォームの機能でよくあるやつです。コード以外にもIDとかタグとか言い方は複数ありますが、意味はどれも同じようなものです。

  1. フレンドになりたいユーザーのフレンドコードを入力
  2. 該当するコードのユーザーにフレンド申請が送られる
  3. 申請を受け取った相手はフレンドになることもできるし拒否することもできる
  4. フレンドになったら一緒にゲームを楽しむことができる

自分のアプリの場合はゲームではなくメモを交換できるようになるわけですが、今回はこの1~4までを簡単にまとめてみます。


Firestore の階層構成

users - {user} - sentRequests - {sentRequest}
               - gotRequests  - {gotRequest}
               - friends      - {friend}

sentRequest は、送信したフレンド申請です。
gotRequest は、受け取ったフレンド申請です。
friend は、フレンドです。

UserA がUserB にフレンド申請を送った場合、UserAのsentRequestsとUserBのgotRequestsにそれぞれドキュメントが書き込まれます。

特にgotRequestsコレクションをsnapshotで常に更新を取得する形にすると、申請が来たタイミングで何らかのアクションが起こせていい感じです。

もちろん、UserA がUserB のコレクションに直接書き込むことはなく、処理はすべてCloud Functionsで行われます。

セキュリティルールですが、今回のようにFunctionsで処理する場合、readだけ残してwriteは消してしまっていいと思います。

rules_version = '2';
service cloud.firestore {
  match /databases/{database}/documents {
    match /users/{userId} {
        match /sentRequests/{sentRequestId} {
               allow read : if request.auth.uid == userId;
            }
        match /gotRequests/{gotRequestId} {
               allow read : if request.auth.uid == userId;
            }  
        match /friends/{friendId} {
               allow read : if request.auth.uid == userId;
            } 
    } 
  }
}

1. フレンドコードを入力して送信

自分のアプリではユーザー検索のような機能はないので、フレンドコードはSNS等で教えてもらう想定です。それを入力してもらいFunctionsに送信します

send_request.dart
final functions = FirebaseFunctions.instanceFor(region: 'asia-northeast1');
final callable = functions.httpsCallable('requestFriend');
final result = await callable({'friendCode': 'hogehoge'});
  if (result.data == 'success'){
      print('申請完了');
    }
  if (result.data == 'none') {
      print('ユーザーが見つかりませんでした');
    }

2. 相手にフレンド申請を送る

Functions の処理は以下の感じです。アプリではここに「すでに申請中」とか「ブロック中」とかの処理もあるんですが、省略してます。

import admin = require("firebase-admin");

const db = admin.firestore();

exports.requestFriend = functions
  .region("asia-northeast1")
  .https.onCall(async (data, context) => {
    const friendCode: string = data.friendCode;
    const sentRequestUserId = context.auth!.uid;
    const gotRequestUserDocs = await db.collection('users')
      .where("friendCode", "==", friendCode)
      .get();
    let result=''
    if (gotRequestUserDocs.empty) {
      result = "none";
    } else {
      const gotRequestUser = gotRequestUserDocs.docs[0]; // UserB
      await gotRequestUser.ref
      .collection('gotRequests')
      .doc(sentRequestUserId) // 送り側のIdにすると扱いやすい
      .set({id:sentRequestUserId}) // ほか必要なフィールドをセット

      await db.collection('users').doc(sentRequestUserId) // UserA
      .collection('sentRequests')
      .doc(gotRequestUser.id) // 受け手側のIdにすると扱いやすい
      .set({id:gotRequestUser.id}) // ほか必要なフィールドをセット

      result = 'success';
    }
  return result;
})

3. 受け取った申請を処理

フレンド申請を受け取ったUserB は、申請を受けるか拒否するか選択できます。拒否する場合は、基本的にはUserAのsentRequestとUserBのgotRequest をそれぞれ削除するだけでいいかと思います。実際にはここで「この相手をブロックする」といった選択肢も必要になりますが、その場合はgotRequestを削除せずにisBlockedフィールドをtrueにした状態で残しておくと扱いやすく感じました(申請時にgotRequestsコレクションにすでに同じIDのドキュメントがあると、「すでに申請中」か「ブロック中」のいずれかになる)

decline_request.dart
final functions = FirebaseFunctions.instanceFor(region: 'asia-northeast1');
final callable = functions.httpsCallable('declineFriendRequest');
final result = await callable({
    'sentRequestUserId': 'hugahuga',
    'blockRequest': blockRequest // bool
  });
  if (result.data == 'decline'){
      print('拒否しました');
    }
  if (result.data == 'block') {
      print('ブロックしました');
    }
import admin = require("firebase-admin");

const db = admin.firestore();

exports.declineFriendRequest = functions
  .region("asia-northeast1")
  .https.onCall(async (data, context) => {
    const gotRequestUserId = context.auth!.uid;
    const sentRequestUserId = data.sentRequestUserId;
    const blockRequest: boolean = data.blockRequest;
    let result =''
    await db.collection('users')
      .doc(sentRequestUserId) // UserA
      .collection('sentRequests')
      .doc(gotRequestUserId)
      .delete();
    if(blockRequest){
    // ブロックする場合
     await db.collection('users')
      .doc(gotRequestUserId) // UserB
      .collection('gotRequests')
      .doc(sentRequestUserId)
      .update({isBlocked: true})
     result = 'block'
    } else{
    // 拒否する場合
     await db.collection('users')
      .doc(gotRequestUserId) // UserB
      .collection('gotRequests')
      .doc(sentRequestUserId)
      .delete()
     result = 'decline'
    }
 return result
})

フレンドになる場合は、それぞれのfriendsコレクションにお互いのドキュメントをセットすることになります。不要になるのでUserAのsentRequestとUserBのgotRequest はそれぞれ削除されます。上の処理と同じなのでコードは省略します

import admin = require("firebase-admin");

const db = admin.firestore();

exports.registrateFriendRequest = functions
  .region("asia-northeast1")
  .https.onCall(async (data, context) => {
    const gotRequestUserId = context.auth!.uid;
    const sentRequestUserId = data.sentRequestUserId;
    await db.collection('users')
      .doc(sentRequestUserId) // UserA
      .collection('friends')
      .doc(gotRequestUserId)
      .set({id:gotRequestUserId}) // UserB
    await db.collection('users')
      .doc(gotRequestUserId) // UserB
      .collection('friends')
      .doc(sentRequestUserId)
      .set({id:sentRequestUserId}) // UserA
})

4. フレンドにメモを送る

複数のフレンドに送るコードです。Functionsにはメモではなくメモのパスを送ってますが、こっちのほうが処理が調整しやすく感じました。実際のアプリ内ではこれに加えて添付データの画像やら動画やらに関連したCloud storageの処理と通知周りのCloud Messagingの処理が入ってるんですが長いので省略

import admin = require("firebase-admin");

const db = admin.firestore();

exports.sendNoteToFriends = functions
  .region("asia-northeast1")
  .https.onCall(async (data, context) => {
    const senderUserId = context.auth!.uid;
    const notePath:string = data.notePath;
    const friends = await db.collection('users')
        .doc(senderUserId)
        .collection('friends')
        .get()
    const note = await db.doc(notePath).get();
    const noteData = note.data() as NOTE
    const promises = friends.docs.map(async (friend)=>{
      await db.collection('users')
        .doc(friend.id)
        .collection('notes')
        .doc(noteData.id)
        .set({...noteData})
     })
    await Promise.allSettled(promises);

})

もちろん、データは上階層に置いて、フレンドにはパスだけ送るのもアリかと思います。


終わりに

完全に余談なんですが、アプリ内での日本語の表現は少し気を付けました。例えば、**「フレンドを削除する」ってちょっと酷くないかと思ったので、「フレンド登録を解除する」にしてみたり、たまに見かける「承諾」「許可」も何か違う気がして「登録」**にしてみたりとかです。

記事内容はツッコミ歓迎です:rolling_eyes:

5
5
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
5
5

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?