Open2
ChatApp設計
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,
),
),
),
],
),
);
}
}
このコードについて説明します:
- データモデル設計:
-
UserModel
: ユーザー情報を管理するモデル -
ChatMessage
: チャットメッセージを管理するモデル - DocumentReferenceを使って
user_ref
フィールドでユーザーを参照
- 主な機能:
- ユーザー情報の取得と保存
- リアルタイムなメッセージの送受信
- メッセージとユーザーの関連付け
- Firebaseのコレクション構造:
- users/
- userId/
- email
- display_name
- photo_url
- phone_number
- created_time
- messages/
- messageId/
- sender_id
- text
- timestamp
- user_ref (DocumentReference to users collection)
- 実装のポイント:
-
StreamBuilder
を使用してリアルタイムなメッセージ更新を実現 -
FutureBuilder
でユーザー情報を非同期に取得 - DocumentReferenceで効率的なユーザー参照を実現
使用方法や追加機能について質問があればお答えします。例えば:
- グループチャットの実装
- メッセージの既読機能
- 画像送信機能
などを追加することもできます。
はい、セキュリティルールは非常に重要です。認証済みユーザーのみがデータにアクセスできるように設定しましょう。
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();
}
}
}
このセキュリティルールについて説明します:
- 基本的なセキュリティ設定:
- すべてのアクセスに認証を要求
- ユーザーは自分のデータのみ編集可能
- メッセージの読み取りは認証済みユーザーのみ可能
- データ検証:
- メッセージの長さ制限(1000文字以下)
- 必須フィールドの存在確認
- データ型の検証
- 具体的な制限:
-
ユーザードキュメント:
- 読み取り:認証済みユーザーなら誰でも可能
- 作成/更新/削除:本人のみ可能
-
メッセージドキュメント:
- 読み取り:認証済みユーザーのみ
- 作成:認証済みで、送信者IDが一致する場合のみ
- 更新/削除:メッセージの作成者のみ可能
- 追加で考慮すべきセキュリティ対策:
// レート制限の例(一定時間内のメッセージ数を制限)
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))
);
}
}
- セキュリティ強化のためのベストプラクティス:
- 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;
}
}
}
これらのセキュリティ設定を実装することで、以下が保証されます:
- データの保護
- 不正アクセスの防止
- データの整合性の維持
- ユーザープライバシーの保護
追加で必要なセキュリティ設定や、特定のユースケースに応じたカスタマイズについても対応できますので、お気軽にご質問ください。