Files
efir-api-server/internal/service/message_service.go

278 lines
8.1 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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)
}