Files
mobiniqow 35f5084005 chore: merge backend repo history into monorepo at backend/
git-subtree-dir: backend
git-subtree-mainline: dc11190527
git-subtree-split: acae976622
2026-09-27 10:30:16 +03:30

180 lines
5.4 KiB
Go

package service
import (
"encoding/base64"
"errors"
"time"
"dating-app/internal/dto"
"dating-app/internal/models"
"dating-app/internal/repository"
)
var (
ErrNotMatchMember = errors.New("این گفتگو مال تو نیست")
ErrInvalidMessageEncoding = errors.New("پیام رمزنگاری‌شده نامعتبر است (کدگذاری اشتباه)")
)
type ChatService struct {
matchRepo *repository.MatchRepository
messageRepo *repository.MessageRepository
userRepo *repository.UserRepository
photoRepo *repository.PhotoRepository
}
func NewChatService(
matchRepo *repository.MatchRepository,
messageRepo *repository.MessageRepository,
userRepo *repository.UserRepository,
photoRepo *repository.PhotoRepository,
) *ChatService {
return &ChatService{
matchRepo: matchRepo,
messageRepo: messageRepo,
userRepo: userRepo,
photoRepo: photoRepo,
}
}
// ListMatches همه‌ی مچ‌های کاربر رو همراه با اطلاعات طرف مقابل و آخرین پیام
// برمی‌گردونه. قبلاً به‌ازای هر مچ ۳ کوئری جدا (کاربر، عکس‌ها، آخرین پیام)
// می‌زد؛ الان هر سه رو batch می‌کنه تا صرف‌نظر از تعداد مچ‌ها، تعداد کوئری‌ها
// ثابت (۴ تا) بمونه.
func (s *ChatService) ListMatches(userID uint) ([]dto.MatchView, error) {
matches, err := s.matchRepo.ListByUser(userID)
if err != nil {
return nil, err
}
if len(matches) == 0 {
return []dto.MatchView{}, nil
}
otherIDs := make([]uint, len(matches))
matchIDs := make([]uint, len(matches))
for i, m := range matches {
otherIDs[i] = repository.OtherUserID(&m, userID)
matchIDs[i] = m.ID
}
otherUsers, err := s.userRepo.FindByIDs(otherIDs)
if err != nil {
return nil, err
}
userByID := make(map[uint]models.User, len(otherUsers))
for _, u := range otherUsers {
userByID[u.ID] = u
}
photos, err := s.photoRepo.ListByUserIDs(otherIDs)
if err != nil {
return nil, err
}
photosByUser := make(map[uint][]string, len(otherIDs))
for _, p := range photos {
photosByUser[p.UserID] = append(photosByUser[p.UserID], p.URL)
}
lastMessages, err := s.messageRepo.LastMessagesByMatchIDs(matchIDs)
if err != nil {
return nil, err
}
views := make([]dto.MatchView, 0, len(matches))
for _, m := range matches {
otherID := repository.OtherUserID(&m, userID)
other, ok := userByID[otherID]
if !ok {
continue
}
view := dto.MatchView{
ID: m.ID,
UserID: other.ID,
FirstName: other.FirstName,
Photos: orEmptyStrings(photosByUser[otherID]),
CreatedAt: m.CreatedAt.Format(time.RFC3339),
PartnerPublicKey: other.PublicKey,
}
if last, ok := lastMessages[m.ID]; ok {
at := last.CreatedAt.Format(time.RFC3339)
view.LastAt = &at
if last.CipherText != nil {
view.LastMessageCipherText = last.CipherText
view.LastMessageNonce = last.Nonce
} else {
text := last.Text
view.LastMessage = &text
}
}
views = append(views, view)
}
return views, nil
}
func (s *ChatService) getAuthorizedMatch(userID, matchID uint) (uint, error) {
match, err := s.matchRepo.FindByID(matchID)
if err != nil {
return 0, err
}
if !repository.IsMember(match, userID) {
return 0, ErrNotMatchMember
}
return match.ID, nil
}
// ListMessages پیام‌های یک مچ رو برمی‌گردونه؛ فقط اگه کاربر عضو اون مچ باشه.
func (s *ChatService) ListMessages(userID, matchID uint) ([]dto.MessageView, error) {
if _, err := s.getAuthorizedMatch(userID, matchID); err != nil {
return nil, err
}
messages, err := s.messageRepo.ListByMatch(matchID)
if err != nil {
return nil, err
}
views := make([]dto.MessageView, 0, len(messages))
for _, m := range messages {
views = append(views, dto.MessageView{
ID: m.ID,
MatchID: m.MatchID,
SenderID: m.SenderID,
Text: m.Text,
CipherText: m.CipherText,
Nonce: m.Nonce,
CreatedAt: m.CreatedAt.Format(time.RFC3339),
IsMine: m.SenderID == userID,
})
}
return views, nil
}
// SendMessage یک پیام رمزنگاری‌شده توی یک مچ ثبت می‌کنه؛ فقط اگه کاربر عضو
// اون مچ باشه. سرور محتوای پیام رو نمی‌بینه و نمی‌تونه — CipherText/Nonce
// قبلاً روی دستگاه فرستنده با کلید عمومی طرف مقابل رمزنگاری شدن؛ اینجا فقط
// اندازه‌ی base64 اونا (از طریق binding توی SendMessageRequest) چک می‌شه.
func (s *ChatService) SendMessage(userID, matchID uint, cipherText, nonce string) (*dto.MessageView, error) {
if _, err := s.getAuthorizedMatch(userID, matchID); err != nil {
return nil, err
}
if _, err := base64.StdEncoding.DecodeString(cipherText); err != nil {
return nil, ErrInvalidMessageEncoding
}
if _, err := base64.StdEncoding.DecodeString(nonce); err != nil {
return nil, ErrInvalidMessageEncoding
}
msg := &models.Message{MatchID: matchID, SenderID: userID, CipherText: &cipherText, Nonce: &nonce}
if err := s.messageRepo.Create(msg); err != nil {
return nil, err
}
return &dto.MessageView{
ID: msg.ID,
MatchID: msg.MatchID,
SenderID: msg.SenderID,
CipherText: msg.CipherText,
Nonce: msg.Nonce,
CreatedAt: msg.CreatedAt.Format(time.RFC3339),
IsMine: true,
}, nil
}