35f5084005
git-subtree-dir: backend git-subtree-mainline:dc11190527git-subtree-split:acae976622
180 lines
5.4 KiB
Go
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
|
|
}
|