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)
|
|||
|
|
}
|