278 lines
8.1 KiB
Go
278 lines
8.1 KiB
Go
package service
|
||
|
||
import (
|
||
"context"
|
||
"errors"
|
||
"fmt"
|
||
"messenger/internal/crypto"
|
||
"messenger/internal/models"
|
||
"messenger/internal/pkg/logger"
|
||
"messenger/internal/repository"
|
||
"time"
|
||
)
|
||
|
||
type MessageService struct {
|
||
messageRepo repository.MessageRepository
|
||
chatRepo repository.ChatRepository
|
||
userRepo repository.UserRepository
|
||
attachmentRepo repository.AttachmentRepository
|
||
encryptor *crypto.Encryptor
|
||
}
|
||
|
||
func NewMessageService(
|
||
messageRepo repository.MessageRepository,
|
||
chatRepo repository.ChatRepository,
|
||
userRepo repository.UserRepository,
|
||
attachmentRepo repository.AttachmentRepository,
|
||
encryptor *crypto.Encryptor,
|
||
) *MessageService {
|
||
return &MessageService{
|
||
messageRepo: messageRepo,
|
||
chatRepo: chatRepo,
|
||
userRepo: userRepo,
|
||
attachmentRepo: attachmentRepo,
|
||
encryptor: encryptor,
|
||
}
|
||
}
|
||
|
||
// SendMessage отправляет новое сообщение в чат
|
||
func (s *MessageService) SendMessage(ctx context.Context, senderID, chatID int64, plaintext string, attachmentID *int64) (*models.MessageResponse, error) {
|
||
// Проверяем, что пользователь участник чата
|
||
isMember, err := s.chatRepo.IsMember(ctx, chatID, senderID)
|
||
if err != nil {
|
||
return nil, fmt.Errorf("failed to check membership: %w", err)
|
||
}
|
||
|
||
if !isMember {
|
||
return nil, errors.New("you are not a member of this chat")
|
||
}
|
||
|
||
// Шифруем сообщение
|
||
encryptedBody, err := s.encryptor.EncryptString(plaintext)
|
||
if err != nil {
|
||
logger.Error("Failed to encrypt message", "error", err)
|
||
return nil, errors.New("failed to encrypt message")
|
||
}
|
||
|
||
// Создаем сообщение
|
||
message := &models.Message{
|
||
ChatID: chatID,
|
||
SenderID: senderID,
|
||
EncryptedBody: []byte(encryptedBody),
|
||
AttachmentID: attachmentID,
|
||
IsRead: false,
|
||
CreatedAt: time.Now(),
|
||
}
|
||
|
||
if err := s.messageRepo.Create(ctx, message); err != nil {
|
||
logger.Error("Failed to create message", "error", err)
|
||
return nil, errors.New("failed to send message")
|
||
}
|
||
|
||
// Обновляем last_seen пользователя
|
||
go func() {
|
||
_ = s.userRepo.UpdateLastSeen(context.Background(), senderID)
|
||
}()
|
||
|
||
// Возвращаем расшифрованное сообщение
|
||
return &models.MessageResponse{
|
||
ID: message.ID,
|
||
ChatID: message.ChatID,
|
||
SenderID: message.SenderID,
|
||
Plaintext: plaintext,
|
||
IsRead: message.IsRead,
|
||
CreatedAt: message.CreatedAt,
|
||
}, nil
|
||
}
|
||
|
||
// GetMessageByID возвращает сообщение по ID с расшифровкой
|
||
func (s *MessageService) GetMessageByID(ctx context.Context, messageID int64) (*models.MessageResponse, error) {
|
||
message, err := s.messageRepo.FindByID(ctx, messageID)
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
|
||
if message == nil {
|
||
return nil, errors.New("message not found")
|
||
}
|
||
|
||
// Расшифровываем
|
||
plaintext, err := s.encryptor.DecryptString(string(message.EncryptedBody))
|
||
if err != nil {
|
||
logger.Error("Failed to decrypt message", "error", err)
|
||
return nil, errors.New("failed to decrypt message")
|
||
}
|
||
|
||
response := &models.MessageResponse{
|
||
ID: message.ID,
|
||
ChatID: message.ChatID,
|
||
SenderID: message.SenderID,
|
||
Plaintext: plaintext,
|
||
IsRead: message.IsRead,
|
||
CreatedAt: message.CreatedAt,
|
||
}
|
||
|
||
// Добавляем информацию о вложении, если есть
|
||
if message.AttachmentID != nil {
|
||
attachment, err := s.attachmentRepo.FindByID(ctx, *message.AttachmentID)
|
||
if err == nil && attachment != nil {
|
||
response.Attachment = &models.Attachment{
|
||
ID: attachment.ID,
|
||
FileName: attachment.FileName,
|
||
FileSize: attachment.FileSize,
|
||
MimeType: attachment.MimeType,
|
||
UploadedAt: attachment.UploadedAt,
|
||
}
|
||
}
|
||
}
|
||
|
||
return response, nil
|
||
}
|
||
|
||
// GetChatHistory возвращает историю сообщений чата
|
||
func (s *MessageService) GetChatHistory(ctx context.Context, chatID, userID int64, limit int, before time.Time) ([]*models.MessageResponse, error) {
|
||
// Проверяем доступ к чату
|
||
isMember, err := s.chatRepo.IsMember(ctx, chatID, userID)
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
|
||
if !isMember {
|
||
return nil, errors.New("access denied")
|
||
}
|
||
|
||
if limit <= 0 || limit > 100 {
|
||
limit = 50
|
||
}
|
||
|
||
messages, err := s.messageRepo.GetChatHistory(ctx, chatID, limit, before)
|
||
if err != nil {
|
||
logger.Error("Failed to get chat history", "error", err)
|
||
return nil, errors.New("failed to get messages")
|
||
}
|
||
|
||
// Расшифровываем сообщения
|
||
responses := make([]*models.MessageResponse, 0, len(messages))
|
||
for _, msg := range messages {
|
||
plaintext, err := s.encryptor.DecryptString(string(msg.EncryptedBody))
|
||
if err != nil {
|
||
logger.Error("Failed to decrypt message", "message_id", msg.ID, "error", err)
|
||
continue
|
||
}
|
||
|
||
response := &models.MessageResponse{
|
||
ID: msg.ID,
|
||
ChatID: msg.ChatID,
|
||
SenderID: msg.SenderID,
|
||
Plaintext: plaintext,
|
||
IsRead: msg.IsRead,
|
||
CreatedAt: msg.CreatedAt,
|
||
}
|
||
|
||
responses = append(responses, response)
|
||
}
|
||
|
||
return responses, nil
|
||
}
|
||
|
||
// MarkMessageAsRead отмечает сообщение как прочитанное
|
||
func (s *MessageService) MarkMessageAsRead(ctx context.Context, messageID, userID int64) error {
|
||
message, err := s.messageRepo.FindByID(ctx, messageID)
|
||
if err != nil {
|
||
return err
|
||
}
|
||
|
||
if message == nil {
|
||
return errors.New("message not found")
|
||
}
|
||
|
||
// Нельзя отметить своё сообщение как прочитанное
|
||
if message.SenderID == userID {
|
||
return nil
|
||
}
|
||
|
||
// Проверяем, что пользователь участник чата
|
||
isMember, err := s.chatRepo.IsMember(ctx, message.ChatID, userID)
|
||
if err != nil {
|
||
return err
|
||
}
|
||
|
||
if !isMember {
|
||
return errors.New("access denied")
|
||
}
|
||
|
||
return s.messageRepo.MarkAsRead(ctx, messageID)
|
||
}
|
||
|
||
// EditMessage редактирует существующее сообщение
|
||
func (s *MessageService) EditMessage(ctx context.Context, userID, messageID int64, newPlaintext string) error {
|
||
message, err := s.messageRepo.FindByID(ctx, messageID)
|
||
if err != nil {
|
||
return err
|
||
}
|
||
|
||
if message == nil {
|
||
return errors.New("message not found")
|
||
}
|
||
|
||
// Только автор может редактировать
|
||
if message.SenderID != userID {
|
||
return errors.New("only the author can edit the message")
|
||
}
|
||
|
||
// Шифруем новое содержимое
|
||
encryptedBody, err := s.encryptor.EncryptString(newPlaintext)
|
||
if err != nil {
|
||
logger.Error("Failed to encrypt edited message", "error", err)
|
||
return errors.New("failed to encrypt message")
|
||
}
|
||
|
||
message.EncryptedBody = []byte(encryptedBody)
|
||
|
||
return s.messageRepo.Update(ctx, message)
|
||
}
|
||
|
||
// DeleteMessage удаляет сообщение
|
||
func (s *MessageService) DeleteMessage(ctx context.Context, messageID int64) error {
|
||
return s.messageRepo.Delete(ctx, messageID)
|
||
}
|
||
|
||
// CanDeleteMessage проверяет, может ли пользователь удалить сообщение
|
||
func (s *MessageService) CanDeleteMessage(ctx context.Context, userID, messageID int64) (bool, error) {
|
||
message, err := s.messageRepo.FindByID(ctx, messageID)
|
||
if err != nil {
|
||
return false, err
|
||
}
|
||
|
||
if message == nil {
|
||
return false, errors.New("message not found")
|
||
}
|
||
|
||
// Автор может удалить своё сообщение
|
||
if message.SenderID == userID {
|
||
return true, nil
|
||
}
|
||
|
||
// Проверяем, является ли пользователь глобальным админом
|
||
user, err := s.userRepo.FindByID(ctx, userID)
|
||
if err != nil {
|
||
return false, err
|
||
}
|
||
|
||
if user != nil && user.IsGlobalAdmin() {
|
||
return true, nil
|
||
}
|
||
|
||
// Проверяем, является ли пользователь админом чата
|
||
role, err := s.chatRepo.GetMemberRole(ctx, message.ChatID, userID)
|
||
if err != nil {
|
||
return false, err
|
||
}
|
||
|
||
return role != nil && *role == models.MemberRoleAdmin, nil
|
||
}
|
||
|
||
// GetUnreadCount возвращает количество непрочитанных сообщений в чате
|
||
func (s *MessageService) GetUnreadCount(ctx context.Context, chatID, userID int64) (int, error) {
|
||
return s.messageRepo.GetUnreadCount(ctx, chatID, userID)
|
||
} |