Initial commit: Эфир мессенджер

This commit is contained in:
2026-04-06 14:57:36 +03:00
commit ff93679b6d
50 changed files with 5642 additions and 0 deletions

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