Open2

ChatApp設計

JboyHashimotoJboyHashimoto

CloudFirestore ChatApp

Firebaseを使ったチャットアプリのユーザー管理とデータ設計を実装しましょう。

// users.dart
class UserModel {
  final String uid;
  final String email;
  final String displayName;
  final String? photoUrl;
  final String? phoneNumber;
  final DateTime createdTime;

  UserModel({
    required this.uid,
    required this.email,
    required this.displayName,
    this.photoUrl,
    this.phoneNumber,
    required this.createdTime,
  });

  factory UserModel.fromFirestore(DocumentSnapshot doc) {
    Map<String, dynamic> data = doc.data() as Map<String, dynamic>;
    return UserModel(
      uid: doc.id,
      email: data['email'] ?? '',
      displayName: data['display_name'] ?? '',
      photoUrl: data['photo_url'],
      phoneNumber: data['phone_number'],
      createdTime: (data['created_time'] as Timestamp).toDate(),
    );
  }

  Map<String, dynamic> toFirestore() {
    return {
      'email': email,
      'display_name': displayName,
      'photo_url': photoUrl,
      'phone_number': phoneNumber,
      'created_time': Timestamp.fromDate(createdTime),
    };
  }
}

// chat_message.dart
class ChatMessage {
  final String messageId;
  final String senderId;
  final String text;
  final DateTime timestamp;
  final DocumentReference userRef;

  ChatMessage({
    required this.messageId,
    required this.senderId,
    required this.text,
    required this.timestamp,
    required this.userRef,
  });

  factory ChatMessage.fromFirestore(DocumentSnapshot doc) {
    Map<String, dynamic> data = doc.data() as Map<String, dynamic>;
    return ChatMessage(
      messageId: doc.id,
      senderId: data['sender_id'] ?? '',
      text: data['text'] ?? '',
      timestamp: (data['timestamp'] as Timestamp).toDate(),
      userRef: data['user_ref'],
    );
  }

  Map<String, dynamic> toFirestore() {
    return {
      'sender_id': senderId,
      'text': text,
      'timestamp': Timestamp.fromDate(timestamp),
      'user_ref': userRef,
    };
  }
}

// chat_service.dart
class ChatService {
  final FirebaseFirestore _firestore = FirebaseFirestore.instance;
  
  // メッセージを送信
  Future<void> sendMessage(String text, String currentUserId) async {
    final userRef = _firestore.collection('users').doc(currentUserId);
    
    await _firestore.collection('messages').add({
      'sender_id': currentUserId,
      'text': text,
      'timestamp': FieldValue.serverTimestamp(),
      'user_ref': userRef,
    });
  }

  // チャットメッセージのストリームを取得
  Stream<List<ChatMessage>> getMessages() {
    return _firestore
        .collection('messages')
        .orderBy('timestamp', descending: true)
        .snapshots()
        .map((snapshot) {
      return snapshot.docs
          .map((doc) => ChatMessage.fromFirestore(doc))
          .toList();
    });
  }

  // ユーザー情報を取得
  Future<UserModel?> getUserData(String userId) async {
    final doc = await _firestore.collection('users').doc(userId).get();
    if (doc.exists) {
      return UserModel.fromFirestore(doc);
    }
    return null;
  }
}

class ChatScreen extends StatelessWidget {
  final ChatService _chatService = ChatService();
  final String currentUserId;

  ChatScreen({required this.currentUserId});

  
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: Text('Chat')),
      body: Column(
        children: [
          Expanded(
            child: StreamBuilder<List<ChatMessage>>(
              stream: _chatService.getMessages(),
              builder: (context, snapshot) {
                if (!snapshot.hasData) {
                  return Center(child: CircularProgressIndicator());
                }
                
                final messages = snapshot.data!;
                return ListView.builder(
                  reverse: true,
                  itemCount: messages.length,
                  itemBuilder: (context, index) {
                    final message = messages[index];
                    return FutureBuilder<UserModel?>(
                      future: _chatService.getUserData(message.senderId),
                      builder: (context, userSnapshot) {
                        if (!userSnapshot.hasData) {
                          return SizedBox.shrink();
                        }
                        
                        final user = userSnapshot.data!;
                        return MessageBubble(
                          message: message,
                          user: user,
                          isMe: currentUserId == message.senderId,
                        );
                      },
                    );
                  },
                );
              },
            ),
          ),
          MessageInput(
            onSend: (text) {
              _chatService.sendMessage(text, currentUserId);
            },
          ),
        ],
      ),
    );
  }
}

class MessageBubble extends StatelessWidget {
  final ChatMessage message;
  final UserModel user;
  final bool isMe;

  const MessageBubble({
    required this.message,
    required this.user,
    required this.isMe,
  });

  
  Widget build(BuildContext context) {
    return Padding(
      padding: EdgeInsets.symmetric(horizontal: 8, vertical: 4),
      child: Column(
        crossAxisAlignment:
            isMe ? CrossAxisAlignment.end : CrossAxisAlignment.start,
        children: [
          Text(user.displayName, style: TextStyle(fontSize: 12)),
          Container(
            margin: EdgeInsets.only(top: 4),
            padding: EdgeInsets.symmetric(horizontal: 12, vertical: 8),
            decoration: BoxDecoration(
              color: isMe ? Colors.blue : Colors.grey[300],
              borderRadius: BorderRadius.circular(16),
            ),
            child: Text(
              message.text,
              style: TextStyle(
                color: isMe ? Colors.white : Colors.black,
              ),
            ),
          ),
        ],
      ),
    );
  }
}

このコードについて説明します:

  1. データモデル設計:
  • UserModel: ユーザー情報を管理するモデル
  • ChatMessage: チャットメッセージを管理するモデル
  • DocumentReferenceを使ってuser_refフィールドでユーザーを参照
  1. 主な機能:
  • ユーザー情報の取得と保存
  • リアルタイムなメッセージの送受信
  • メッセージとユーザーの関連付け
  1. Firebaseのコレクション構造:
- users/
  - userId/
    - email
    - display_name
    - photo_url
    - phone_number
    - created_time

- messages/
  - messageId/
    - sender_id
    - text
    - timestamp
    - user_ref (DocumentReference to users collection)
  1. 実装のポイント:
  • StreamBuilderを使用してリアルタイムなメッセージ更新を実現
  • FutureBuilderでユーザー情報を非同期に取得
  • DocumentReferenceで効率的なユーザー参照を実現

使用方法や追加機能について質問があればお答えします。例えば:

  • グループチャットの実装
  • メッセージの既読機能
  • 画像送信機能
    などを追加することもできます。
JboyHashimotoJboyHashimoto

はい、セキュリティルールは非常に重要です。認証済みユーザーのみがデータにアクセスできるように設定しましょう。

rules_version = '2';
service cloud.firestore {
  match /databases/{database}/documents {
    // ユーザーが認証済みかチェックする関数
    function isAuthenticated() {
      return request.auth != null;
    }
    
    // リクエストユーザーが対象ドキュメントの所有者かチェックする関数
    function isOwner(userId) {
      return request.auth.uid == userId;
    }

    // usersコレクションのルール
    match /users/{userId} {
      // 読み取り: 認証済みユーザーなら誰でも可能
      // 書き込み: 自分のドキュメントのみ可能
      allow read: if isAuthenticated();
      allow create: if isAuthenticated() && isOwner(userId);
      allow update: if isAuthenticated() && isOwner(userId);
      allow delete: if isAuthenticated() && isOwner(userId);
    }

    // messagesコレクションのルール
    match /messages/{messageId} {
      // 読み取り: 認証済みユーザーのみ
      // 作成: 認証済みユーザーで、送信者IDが自分のIDと一致
      // 更新・削除: 送信者のみ可能
      allow read: if isAuthenticated();
      allow create: if isAuthenticated() && 
                   request.resource.data.sender_id == request.auth.uid;
      allow update, delete: if isAuthenticated() && 
                          get(/databases/$(database)/documents/messages/$(messageId)).data.sender_id == request.auth.uid;
      
      // メッセージの内容を検証
      function isValidMessage() {
        let incoming = request.resource.data;
        return incoming.text is string && 
               incoming.text.size() > 0 && 
               incoming.text.size() <= 1000 &&
               incoming.timestamp is timestamp &&
               incoming.user_ref is path;
      }
      
      // メッセージ作成時のバリデーション
      allow create: if isAuthenticated() && 
                   request.resource.data.sender_id == request.auth.uid &&
                   isValidMessage();
    }
  }
}

このセキュリティルールについて説明します:

  1. 基本的なセキュリティ設定:
  • すべてのアクセスに認証を要求
  • ユーザーは自分のデータのみ編集可能
  • メッセージの読み取りは認証済みユーザーのみ可能
  1. データ検証:
  • メッセージの長さ制限(1000文字以下)
  • 必須フィールドの存在確認
  • データ型の検証
  1. 具体的な制限:
  • ユーザードキュメント:

    • 読み取り:認証済みユーザーなら誰でも可能
    • 作成/更新/削除:本人のみ可能
  • メッセージドキュメント:

    • 読み取り:認証済みユーザーのみ
    • 作成:認証済みで、送信者IDが一致する場合のみ
    • 更新/削除:メッセージの作成者のみ可能
  1. 追加で考慮すべきセキュリティ対策:
// レート制限の例(一定時間内のメッセージ数を制限)
match /messages/{messageId} {
  allow create: if isAuthenticated() &&
                request.resource.data.sender_id == request.auth.uid &&
                isValidMessage() &&
                getRecentMessages() < 50;  // 例:直近のメッセージ数を制限

  function getRecentMessages() {
    return getCount(
      /databases/$(database)/documents/messages
      .where('sender_id', '==', request.auth.uid)
      .where('timestamp', '>', timestamp.date(2024, request.time.toDate().month, request.time.toDate().day))
    );
  }
}
  1. セキュリティ強化のためのベストプラクティス:
  • APIキーの保護
// main.dart
void main() async {
  WidgetsFlutterBinding.ensureInitialized();
  await Firebase.initializeApp(
    options: DefaultFirebaseOptions.currentPlatform,
  );
  
  // デバッグモードでない場合のみFirebase Analyticsを有効化
  if (kReleaseMode) {
    await FirebaseAnalytics.instance.setAnalyticsCollectionEnabled(true);
  }
  
  runApp(MyApp());
}
  • ユーザー認証の確認
// auth_service.dart
class AuthService {
  final FirebaseAuth _auth = FirebaseAuth.instance;

  // 現在のユーザーの認証状態を確認
  Stream<User?> get authStateChanges => _auth.authStateChanges();

  // ログイン状態の確認
  bool isUserLoggedIn() {
    return _auth.currentUser != null;
  }

  // セッションの検証
  Future<bool> validateSession() async {
    try {
      await _auth.currentUser?.getIdToken(true);
      return true;
    } catch (e) {
      return false;
    }
  }
}

これらのセキュリティ設定を実装することで、以下が保証されます:

  1. データの保護
  2. 不正アクセスの防止
  3. データの整合性の維持
  4. ユーザープライバシーの保護

追加で必要なセキュリティ設定や、特定のユースケースに応じたカスタマイズについても対応できますので、お気軽にご質問ください。