Initial commit: Эфир мессенджер
This commit is contained in:
278
internal/service/message_service.go
Normal file
278
internal/service/message_service.go
Normal file
@@ -0,0 +1,278 @@
|
||||
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)
|
||||
}
|
||||
Reference in New Issue
Block a user