Initial commit: Эфир мессенджер
This commit is contained in:
180
internal/service/admin_service.go
Normal file
180
internal/service/admin_service.go
Normal file
@@ -0,0 +1,180 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"messenger/internal/models"
|
||||
"messenger/internal/pkg/logger"
|
||||
"messenger/internal/repository"
|
||||
)
|
||||
|
||||
type AdminService struct {
|
||||
userRepo repository.UserRepository
|
||||
chatRepo repository.ChatRepository
|
||||
messageRepo repository.MessageRepository
|
||||
fileService *FileService
|
||||
}
|
||||
|
||||
func NewAdminService(
|
||||
userRepo repository.UserRepository,
|
||||
chatRepo repository.ChatRepository,
|
||||
messageRepo repository.MessageRepository,
|
||||
fileService *FileService,
|
||||
) *AdminService {
|
||||
return &AdminService{
|
||||
userRepo: userRepo,
|
||||
chatRepo: chatRepo,
|
||||
messageRepo: messageRepo,
|
||||
fileService: fileService,
|
||||
}
|
||||
}
|
||||
|
||||
// DeleteUser удаляет пользователя (каскадно)
|
||||
func (s *AdminService) DeleteUser(ctx context.Context, adminUserID, targetUserID int64) error {
|
||||
// Проверяем, что администратор имеет права
|
||||
admin, err := s.userRepo.FindByID(ctx, adminUserID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if admin == nil || !admin.IsGlobalAdmin() {
|
||||
return errors.New("only global admin can delete users")
|
||||
}
|
||||
|
||||
// Нельзя удалить самого себя через эту функцию
|
||||
if adminUserID == targetUserID {
|
||||
return errors.New("cannot delete yourself")
|
||||
}
|
||||
|
||||
// Проверяем существование пользователя
|
||||
target, err := s.userRepo.FindByID(ctx, targetUserID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if target == nil {
|
||||
return errors.New("user not found")
|
||||
}
|
||||
|
||||
// Удаляем пользователя (каскадное удаление сработает через foreign keys)
|
||||
if err := s.userRepo.Delete(ctx, targetUserID); err != nil {
|
||||
logger.Error("Failed to delete user", "error", err)
|
||||
return errors.New("failed to delete user")
|
||||
}
|
||||
|
||||
logger.Info("User deleted by admin", "admin_id", adminUserID, "target_user_id", targetUserID)
|
||||
return nil
|
||||
}
|
||||
|
||||
// DeleteMessage удаляет любое сообщение (админское право)
|
||||
func (s *AdminService) DeleteMessage(ctx context.Context, adminUserID, messageID int64) error {
|
||||
// Проверяем права администратора
|
||||
admin, err := s.userRepo.FindByID(ctx, adminUserID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if admin == nil || !admin.IsGlobalAdmin() {
|
||||
return errors.New("only global admin can delete any message")
|
||||
}
|
||||
|
||||
if err := s.messageRepo.Delete(ctx, messageID); err != nil {
|
||||
logger.Error("Failed to delete message by admin", "error", err)
|
||||
return errors.New("failed to delete message")
|
||||
}
|
||||
|
||||
logger.Info("Message deleted by admin", "admin_id", adminUserID, "message_id", messageID)
|
||||
return nil
|
||||
}
|
||||
|
||||
// DeleteChat удаляет любой чат (админское право)
|
||||
func (s *AdminService) DeleteChat(ctx context.Context, adminUserID, chatID int64) error {
|
||||
// Проверяем права администратора
|
||||
admin, err := s.userRepo.FindByID(ctx, adminUserID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if admin == nil || !admin.IsGlobalAdmin() {
|
||||
return errors.New("only global admin can delete any chat")
|
||||
}
|
||||
|
||||
if err := s.chatRepo.Delete(ctx, chatID); err != nil {
|
||||
logger.Error("Failed to delete chat by admin", "error", err)
|
||||
return errors.New("failed to delete chat")
|
||||
}
|
||||
|
||||
logger.Info("Chat deleted by admin", "admin_id", adminUserID, "chat_id", chatID)
|
||||
return nil
|
||||
}
|
||||
|
||||
// PromoteToAdmin повышает пользователя до глобального администратора
|
||||
func (s *AdminService) PromoteToAdmin(ctx context.Context, adminUserID, targetUserID int64) error {
|
||||
// Проверяем права администратора
|
||||
admin, err := s.userRepo.FindByID(ctx, adminUserID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if admin == nil || !admin.IsGlobalAdmin() {
|
||||
return errors.New("only global admin can promote users")
|
||||
}
|
||||
|
||||
if err := s.userRepo.UpdateRole(ctx, targetUserID, models.RoleGlobalAdmin); err != nil {
|
||||
logger.Error("Failed to promote user to admin", "error", err)
|
||||
return errors.New("failed to promote user")
|
||||
}
|
||||
|
||||
logger.Info("User promoted to global admin", "admin_id", adminUserID, "target_user_id", targetUserID)
|
||||
return nil
|
||||
}
|
||||
|
||||
// DemoteFromAdmin понижает глобального администратора до обычного пользователя
|
||||
func (s *AdminService) DemoteFromAdmin(ctx context.Context, adminUserID, targetUserID int64) error {
|
||||
// Проверяем права администратора
|
||||
admin, err := s.userRepo.FindByID(ctx, adminUserID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if admin == nil || !admin.IsGlobalAdmin() {
|
||||
return errors.New("only global admin can demote users")
|
||||
}
|
||||
|
||||
// Нельзя понизить самого себя
|
||||
if adminUserID == targetUserID {
|
||||
return errors.New("cannot demote yourself")
|
||||
}
|
||||
|
||||
if err := s.userRepo.UpdateRole(ctx, targetUserID, models.RoleUser); err != nil {
|
||||
logger.Error("Failed to demote user", "error", err)
|
||||
return errors.New("failed to demote user")
|
||||
}
|
||||
|
||||
logger.Info("User demoted from global admin", "admin_id", adminUserID, "target_user_id", targetUserID)
|
||||
return nil
|
||||
}
|
||||
|
||||
// GetSystemStats возвращает системную статистику
|
||||
func (s *AdminService) GetSystemStats(ctx context.Context, adminUserID int64) (map[string]interface{}, error) {
|
||||
// Проверяем права администратора
|
||||
admin, err := s.userRepo.FindByID(ctx, adminUserID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if admin == nil || !admin.IsGlobalAdmin() {
|
||||
return nil, errors.New("only global admin can view system stats")
|
||||
}
|
||||
|
||||
// Здесь можно добавить реальную статистику из БД
|
||||
// Например, количество пользователей, сообщений, чатов и т.д.
|
||||
stats := make(map[string]interface{})
|
||||
|
||||
// Это заглушка - в реальном коде нужно добавить методы в репозитории
|
||||
stats["version"] = "1.0.0"
|
||||
stats["status"] = "healthy"
|
||||
|
||||
logger.Info("System stats viewed by admin", "admin_id", adminUserID)
|
||||
return stats, nil
|
||||
}
|
||||
199
internal/service/auth_service.go
Normal file
199
internal/service/auth_service.go
Normal file
@@ -0,0 +1,199 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"messenger/internal/models"
|
||||
"messenger/internal/pkg/logger"
|
||||
"messenger/internal/pkg/validator"
|
||||
"messenger/internal/repository"
|
||||
"time"
|
||||
|
||||
"github.com/golang-jwt/jwt/v5"
|
||||
"golang.org/x/crypto/bcrypt"
|
||||
)
|
||||
|
||||
type AuthService struct {
|
||||
userRepo repository.UserRepository
|
||||
profileRepo repository.ProfileRepository
|
||||
jwtSecret []byte
|
||||
jwtExpiry int64
|
||||
}
|
||||
|
||||
type Claims struct {
|
||||
UserID int64 `json:"user_id"`
|
||||
Login string `json:"login"`
|
||||
Role string `json:"role"`
|
||||
jwt.RegisteredClaims
|
||||
}
|
||||
|
||||
func NewAuthService(userRepo repository.UserRepository, profileRepo repository.ProfileRepository, jwtSecret []byte, jwtExpiry int64) *AuthService {
|
||||
return &AuthService{
|
||||
userRepo: userRepo,
|
||||
profileRepo: profileRepo,
|
||||
jwtSecret: jwtSecret,
|
||||
jwtExpiry: jwtExpiry,
|
||||
}
|
||||
}
|
||||
|
||||
// Register регистрирует нового пользователя
|
||||
func (s *AuthService) Register(ctx context.Context, login, password string) (*models.User, string, error) {
|
||||
// Валидация входных данных
|
||||
if !validator.ValidateLogin(login) {
|
||||
return nil, "", errors.New("invalid login: must be 3-32 characters, only letters, numbers and underscore")
|
||||
}
|
||||
|
||||
if !validator.ValidatePassword(password) {
|
||||
return nil, "", errors.New("invalid password: must be at least 6 characters")
|
||||
}
|
||||
|
||||
// Проверка существования пользователя
|
||||
exists, err := s.userRepo.Exists(ctx, login)
|
||||
if err != nil {
|
||||
logger.Error("Failed to check user existence", "error", err)
|
||||
return nil, "", errors.New("internal server error")
|
||||
}
|
||||
|
||||
if exists {
|
||||
return nil, "", errors.New("user already exists")
|
||||
}
|
||||
|
||||
// Хэширование пароля
|
||||
passwordHash, err := bcrypt.GenerateFromPassword([]byte(password), bcrypt.DefaultCost)
|
||||
if err != nil {
|
||||
logger.Error("Failed to hash password", "error", err)
|
||||
return nil, "", errors.New("internal server error")
|
||||
}
|
||||
|
||||
// Создание пользователя
|
||||
user := &models.User{
|
||||
Login: login,
|
||||
PasswordHash: string(passwordHash),
|
||||
Role: models.RoleUser,
|
||||
CreatedAt: time.Now(),
|
||||
}
|
||||
|
||||
if err := s.userRepo.Create(ctx, user); err != nil {
|
||||
logger.Error("Failed to create user", "error", err)
|
||||
return nil, "", errors.New("internal server error")
|
||||
}
|
||||
|
||||
// Создание профиля
|
||||
profile := &models.Profile{
|
||||
UserID: user.ID,
|
||||
}
|
||||
|
||||
if err := s.profileRepo.Create(ctx, profile); err != nil {
|
||||
logger.Error("Failed to create profile", "error", err)
|
||||
// Не фатально, профиль можно создать позже
|
||||
}
|
||||
|
||||
// Генерация JWT токена
|
||||
token, err := s.generateToken(user)
|
||||
if err != nil {
|
||||
logger.Error("Failed to generate token", "error", err)
|
||||
return nil, "", errors.New("internal server error")
|
||||
}
|
||||
|
||||
logger.Info("User registered successfully", "user_id", user.ID, "login", user.Login)
|
||||
return user, token, nil
|
||||
}
|
||||
|
||||
// Login аутентифицирует пользователя
|
||||
func (s *AuthService) Login(ctx context.Context, login, password string) (*models.User, string, error) {
|
||||
// Поиск пользователя
|
||||
user, err := s.userRepo.FindByLogin(ctx, login)
|
||||
if err != nil {
|
||||
logger.Error("Failed to find user", "error", err)
|
||||
return nil, "", errors.New("internal server error")
|
||||
}
|
||||
|
||||
if user == nil {
|
||||
return nil, "", errors.New("invalid credentials")
|
||||
}
|
||||
|
||||
// Проверка пароля
|
||||
if err := bcrypt.CompareHashAndPassword([]byte(user.PasswordHash), []byte(password)); err != nil {
|
||||
return nil, "", errors.New("invalid credentials")
|
||||
}
|
||||
|
||||
// Обновление last_seen
|
||||
go func() {
|
||||
if err := s.userRepo.UpdateLastSeen(context.Background(), user.ID); err != nil {
|
||||
logger.Error("Failed to update last_seen", "error", err)
|
||||
}
|
||||
}()
|
||||
|
||||
// Генерация JWT токена
|
||||
token, err := s.generateToken(user)
|
||||
if err != nil {
|
||||
logger.Error("Failed to generate token", "error", err)
|
||||
return nil, "", errors.New("internal server error")
|
||||
}
|
||||
|
||||
logger.Info("User logged in", "user_id", user.ID, "login", user.Login)
|
||||
return user, token, nil
|
||||
}
|
||||
|
||||
// ValidateToken проверяет валидность JWT токена и возвращает пользователя
|
||||
func (s *AuthService) ValidateToken(tokenString string) (*models.User, error) {
|
||||
claims := &Claims{}
|
||||
|
||||
token, err := jwt.ParseWithClaims(tokenString, claims, func(token *jwt.Token) (interface{}, error) {
|
||||
if _, ok := token.Method.(*jwt.SigningMethodHMAC); !ok {
|
||||
return nil, fmt.Errorf("unexpected signing method: %v", token.Header["alg"])
|
||||
}
|
||||
return s.jwtSecret, nil
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
return nil, errors.New("invalid token")
|
||||
}
|
||||
|
||||
if !token.Valid {
|
||||
return nil, errors.New("invalid token")
|
||||
}
|
||||
|
||||
// Получаем пользователя из БД (на случай, если роль изменилась)
|
||||
ctx := context.Background()
|
||||
user, err := s.userRepo.FindByID(ctx, claims.UserID)
|
||||
if err != nil {
|
||||
return nil, errors.New("user not found")
|
||||
}
|
||||
|
||||
if user == nil {
|
||||
return nil, errors.New("user not found")
|
||||
}
|
||||
|
||||
return user, nil
|
||||
}
|
||||
|
||||
// generateToken генерирует JWT токен для пользователя
|
||||
func (s *AuthService) generateToken(user *models.User) (string, error) {
|
||||
expirationTime := time.Now().Add(time.Duration(s.jwtExpiry) * time.Hour)
|
||||
|
||||
claims := &Claims{
|
||||
UserID: user.ID,
|
||||
Login: user.Login,
|
||||
Role: string(user.Role),
|
||||
RegisteredClaims: jwt.RegisteredClaims{
|
||||
ExpiresAt: jwt.NewNumericDate(expirationTime),
|
||||
IssuedAt: jwt.NewNumericDate(time.Now()),
|
||||
NotBefore: jwt.NewNumericDate(time.Now()),
|
||||
},
|
||||
}
|
||||
|
||||
token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
|
||||
return token.SignedString(s.jwtSecret)
|
||||
}
|
||||
|
||||
// GetUserByID возвращает пользователя по ID
|
||||
func (s *AuthService) GetUserByID(ctx context.Context, userID int64) (*models.User, error) {
|
||||
return s.userRepo.FindByID(ctx, userID)
|
||||
}
|
||||
|
||||
// GetUserByLogin возвращает пользователя по логину
|
||||
func (s *AuthService) GetUserByLogin(ctx context.Context, login string) (*models.User, error) {
|
||||
return s.userRepo.FindByLogin(ctx, login)
|
||||
}
|
||||
383
internal/service/chat_service.go
Normal file
383
internal/service/chat_service.go
Normal file
@@ -0,0 +1,383 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
//"fmt"
|
||||
"messenger/internal/models"
|
||||
"messenger/internal/pkg/logger"
|
||||
"messenger/internal/repository"
|
||||
"time"
|
||||
)
|
||||
|
||||
type ChatService struct {
|
||||
chatRepo repository.ChatRepository
|
||||
userRepo repository.UserRepository
|
||||
messageRepo repository.MessageRepository
|
||||
}
|
||||
|
||||
func NewChatService(chatRepo repository.ChatRepository, userRepo repository.UserRepository, messageRepo repository.MessageRepository) *ChatService {
|
||||
return &ChatService{
|
||||
chatRepo: chatRepo,
|
||||
userRepo: userRepo,
|
||||
messageRepo: messageRepo,
|
||||
}
|
||||
}
|
||||
|
||||
// CreatePrivateChat создает личный чат между двумя пользователями
|
||||
func (s *ChatService) CreatePrivateChat(ctx context.Context, userID, targetUserID int64) (*models.Chat, error) {
|
||||
// Проверка, что чат не существует
|
||||
existingChat, err := s.chatRepo.FindPrivateChat(ctx, userID, targetUserID)
|
||||
if err != nil {
|
||||
logger.Error("Failed to find private chat", "error", err)
|
||||
return nil, errors.New("internal server error")
|
||||
}
|
||||
|
||||
if existingChat != nil {
|
||||
return existingChat, nil
|
||||
}
|
||||
|
||||
// Создаем чат
|
||||
chat := &models.Chat{
|
||||
Type: models.ChatTypePrivate,
|
||||
CreatedAt: time.Now(),
|
||||
}
|
||||
|
||||
if err := s.chatRepo.Create(ctx, chat); err != nil {
|
||||
logger.Error("Failed to create chat", "error", err)
|
||||
return nil, errors.New("failed to create chat")
|
||||
}
|
||||
|
||||
// Добавляем участников
|
||||
if err := s.chatRepo.AddMember(ctx, chat.ID, userID, models.MemberRoleMember); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if err := s.chatRepo.AddMember(ctx, chat.ID, targetUserID, models.MemberRoleMember); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
logger.Info("Private chat created", "chat_id", chat.ID, "user1", userID, "user2", targetUserID)
|
||||
return chat, nil
|
||||
}
|
||||
|
||||
// CreateGroupChat создает групповой чат
|
||||
func (s *ChatService) CreateGroupChat(ctx context.Context, creatorID int64, title string, memberLogins []string) (*models.Chat, error) {
|
||||
if title == "" {
|
||||
return nil, errors.New("group title is required")
|
||||
}
|
||||
|
||||
// Создаем чат
|
||||
chat := &models.Chat{
|
||||
Type: models.ChatTypeGroup,
|
||||
Title: &title,
|
||||
CreatedAt: time.Now(),
|
||||
}
|
||||
|
||||
if err := s.chatRepo.Create(ctx, chat); err != nil {
|
||||
logger.Error("Failed to create group chat", "error", err)
|
||||
return nil, errors.New("failed to create chat")
|
||||
}
|
||||
|
||||
// Добавляем создателя как админа
|
||||
if err := s.chatRepo.AddMember(ctx, chat.ID, creatorID, models.MemberRoleAdmin); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Добавляем остальных участников
|
||||
for _, login := range memberLogins {
|
||||
user, err := s.userRepo.FindByLogin(ctx, login)
|
||||
if err != nil {
|
||||
logger.Error("Failed to find user", "login", login, "error", err)
|
||||
continue
|
||||
}
|
||||
|
||||
if user != nil && user.ID != creatorID {
|
||||
if err := s.chatRepo.AddMember(ctx, chat.ID, user.ID, models.MemberRoleMember); err != nil {
|
||||
logger.Error("Failed to add member", "user_id", user.ID, "error", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
logger.Info("Group chat created", "chat_id", chat.ID, "creator", creatorID, "title", title)
|
||||
return chat, nil
|
||||
}
|
||||
|
||||
// GetUserChats возвращает все чаты пользователя
|
||||
func (s *ChatService) GetUserChats(ctx context.Context, userID int64) ([]*models.ChatWithDetails, error) {
|
||||
chats, err := s.chatRepo.GetUserChats(ctx, userID)
|
||||
if err != nil {
|
||||
logger.Error("Failed to get user chats", "error", err)
|
||||
return nil, errors.New("internal server error")
|
||||
}
|
||||
|
||||
var chatsWithDetails []*models.ChatWithDetails
|
||||
for _, chat := range chats {
|
||||
lastMessage, _ := s.messageRepo.GetLastMessage(ctx, chat.ID)
|
||||
unreadCount, _ := s.messageRepo.GetUnreadCount(ctx, chat.ID, userID)
|
||||
|
||||
chatDetail := &models.ChatWithDetails{
|
||||
ID: chat.ID,
|
||||
Type: chat.Type,
|
||||
Title: chat.Title,
|
||||
CreatedAt: chat.CreatedAt,
|
||||
UnreadCount: unreadCount,
|
||||
}
|
||||
|
||||
if lastMessage != nil {
|
||||
chatDetail.LastMessage = lastMessage
|
||||
}
|
||||
|
||||
chatsWithDetails = append(chatsWithDetails, chatDetail)
|
||||
}
|
||||
|
||||
return chatsWithDetails, nil
|
||||
}
|
||||
|
||||
// GetChatByID возвращает чат по ID с проверкой доступа
|
||||
func (s *ChatService) GetChatByID(ctx context.Context, chatID, userID int64) (*models.Chat, error) {
|
||||
// Проверяем, что пользователь участник чата
|
||||
isMember, err := s.chatRepo.IsMember(ctx, chatID, userID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if !isMember {
|
||||
return nil, errors.New("access denied")
|
||||
}
|
||||
|
||||
return s.chatRepo.FindByID(ctx, chatID)
|
||||
}
|
||||
|
||||
// GetChatMembers возвращает участников чата
|
||||
func (s *ChatService) GetChatMembers(ctx context.Context, chatID, userID int64) ([]*models.ChatMember, error) {
|
||||
// Проверяем доступ
|
||||
isMember, err := s.chatRepo.IsMember(ctx, chatID, userID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if !isMember {
|
||||
return nil, errors.New("access denied")
|
||||
}
|
||||
|
||||
return s.chatRepo.GetMembers(ctx, chatID)
|
||||
}
|
||||
|
||||
// AddMembers добавляет участников в групповой чат
|
||||
func (s *ChatService) AddMembers(ctx context.Context, chatID, adminID int64, userLogins []string) error {
|
||||
// Проверяем, что администратор имеет права
|
||||
role, err := s.chatRepo.GetMemberRole(ctx, chatID, adminID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if role == nil || (*role != models.MemberRoleAdmin) {
|
||||
return errors.New("only admins can add members")
|
||||
}
|
||||
|
||||
// Проверяем, что чат групповой
|
||||
chat, err := s.chatRepo.FindByID(ctx, chatID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if chat == nil || chat.Type != models.ChatTypeGroup {
|
||||
return errors.New("only group chats can have members added")
|
||||
}
|
||||
|
||||
// Добавляем участников
|
||||
for _, login := range userLogins {
|
||||
user, err := s.userRepo.FindByLogin(ctx, login)
|
||||
if err != nil || user == nil {
|
||||
logger.Error("User not found", "login", login)
|
||||
continue
|
||||
}
|
||||
|
||||
isMember, _ := s.chatRepo.IsMember(ctx, chatID, user.ID)
|
||||
if !isMember {
|
||||
if err := s.chatRepo.AddMember(ctx, chatID, user.ID, models.MemberRoleMember); err != nil {
|
||||
logger.Error("Failed to add member", "user_id", user.ID, "error", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
logger.Info("Members added to chat", "chat_id", chatID, "admin_id", adminID)
|
||||
return nil
|
||||
}
|
||||
|
||||
// RemoveMember удаляет участника из группового чата
|
||||
func (s *ChatService) RemoveMember(ctx context.Context, chatID, adminID, targetID int64) error {
|
||||
// Проверяем права администратора
|
||||
role, err := s.chatRepo.GetMemberRole(ctx, chatID, adminID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if role == nil || (*role != models.MemberRoleAdmin) {
|
||||
return errors.New("only admins can remove members")
|
||||
}
|
||||
|
||||
// Нельзя удалить самого себя через эту функцию (есть LeaveChat)
|
||||
if adminID == targetID {
|
||||
return errors.New("use leave chat instead")
|
||||
}
|
||||
|
||||
// Проверяем, что чат групповой
|
||||
chat, err := s.chatRepo.FindByID(ctx, chatID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if chat == nil || chat.Type != models.ChatTypeGroup {
|
||||
return errors.New("only group chats can have members removed")
|
||||
}
|
||||
|
||||
return s.chatRepo.RemoveMember(ctx, chatID, targetID)
|
||||
}
|
||||
|
||||
// LeaveChat выход из чата
|
||||
func (s *ChatService) LeaveChat(ctx context.Context, chatID, userID int64) error {
|
||||
chat, err := s.chatRepo.FindByID(ctx, chatID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if chat == nil {
|
||||
return errors.New("chat not found")
|
||||
}
|
||||
|
||||
// Для приватных чатов выход означает удаление чата?
|
||||
if chat.Type == models.ChatTypePrivate {
|
||||
// В приватном чате выход не допускается, только удаление
|
||||
return errors.New("cannot leave private chat")
|
||||
}
|
||||
|
||||
// Проверяем, что пользователь участник
|
||||
isMember, err := s.chatRepo.IsMember(ctx, chatID, userID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if !isMember {
|
||||
return errors.New("not a member of this chat")
|
||||
}
|
||||
|
||||
return s.chatRepo.RemoveMember(ctx, chatID, userID)
|
||||
}
|
||||
|
||||
// UpdateMemberRole обновляет роль участника в группе
|
||||
func (s *ChatService) UpdateMemberRole(ctx context.Context, chatID, adminID, targetID int64, role models.MemberRole) error {
|
||||
// Проверяем права администратора
|
||||
adminRole, err := s.chatRepo.GetMemberRole(ctx, chatID, adminID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if adminRole == nil || (*adminRole != models.MemberRoleAdmin) {
|
||||
return errors.New("only admins can update member roles")
|
||||
}
|
||||
|
||||
// Нельзя изменить роль администратора, если он единственный
|
||||
if targetID == adminID && role != models.MemberRoleAdmin {
|
||||
members, err := s.chatRepo.GetMembers(ctx, chatID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
adminCount := 0
|
||||
for _, m := range members {
|
||||
if m.Role == models.MemberRoleAdmin {
|
||||
adminCount++
|
||||
}
|
||||
}
|
||||
|
||||
if adminCount <= 1 {
|
||||
return errors.New("cannot remove the only admin")
|
||||
}
|
||||
}
|
||||
|
||||
return s.chatRepo.UpdateMemberRole(ctx, chatID, targetID, role)
|
||||
}
|
||||
|
||||
// UpdateChatTitle обновляет название группового чата
|
||||
func (s *ChatService) UpdateChatTitle(ctx context.Context, chatID, adminID int64, title string) error {
|
||||
// Проверяем права администратора
|
||||
role, err := s.chatRepo.GetMemberRole(ctx, chatID, adminID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if role == nil || (*role != models.MemberRoleAdmin) {
|
||||
return errors.New("only admins can update chat title")
|
||||
}
|
||||
|
||||
if title == "" {
|
||||
return errors.New("title cannot be empty")
|
||||
}
|
||||
|
||||
return s.chatRepo.UpdateTitle(ctx, chatID, title)
|
||||
}
|
||||
|
||||
// DeleteChat удаляет чат (только для глобального админа)
|
||||
func (s *ChatService) DeleteChat(ctx context.Context, chatID int64, isGlobalAdmin bool) error {
|
||||
if !isGlobalAdmin {
|
||||
return errors.New("only global admin can delete chats")
|
||||
}
|
||||
|
||||
return s.chatRepo.Delete(ctx, chatID)
|
||||
}
|
||||
|
||||
// IsMember проверяет, является ли пользователь участником чата
|
||||
func (s *ChatService) IsMember(ctx context.Context, chatID, userID int64) (bool, error) {
|
||||
return s.chatRepo.IsMember(ctx, chatID, userID)
|
||||
}
|
||||
|
||||
// GetUserChatsWithDetails возвращает чаты пользователя с информацией о собеседнике
|
||||
func (s *ChatService) GetUserChatsWithDetails(ctx context.Context, userID int64) ([]*models.ChatWithDetails, error) {
|
||||
chats, err := s.chatRepo.GetUserChats(ctx, userID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var chatsWithDetails []*models.ChatWithDetails
|
||||
for _, chat := range chats {
|
||||
lastMessage, _ := s.messageRepo.GetLastMessage(ctx, chat.ID)
|
||||
unreadCount, _ := s.messageRepo.GetUnreadCount(ctx, chat.ID, userID)
|
||||
|
||||
chatDetail := &models.ChatWithDetails{
|
||||
ID: chat.ID,
|
||||
Type: chat.Type,
|
||||
CreatedAt: chat.CreatedAt,
|
||||
UnreadCount: unreadCount,
|
||||
}
|
||||
|
||||
if lastMessage != nil {
|
||||
chatDetail.LastMessage = lastMessage
|
||||
}
|
||||
|
||||
// Для приватных чатов - подставляем имя собеседника
|
||||
if chat.Type == models.ChatTypePrivate {
|
||||
members, err := s.chatRepo.GetMembers(ctx, chat.ID)
|
||||
if err == nil {
|
||||
for _, member := range members {
|
||||
if member.UserID != userID {
|
||||
otherUser, err := s.userRepo.FindByID(ctx, member.UserID)
|
||||
if err == nil && otherUser != nil {
|
||||
title := otherUser.Login
|
||||
chatDetail.Title = &title
|
||||
}
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// Для групповых чатов - используем заданное название
|
||||
chatDetail.Title = chat.Title
|
||||
}
|
||||
|
||||
chatsWithDetails = append(chatsWithDetails, chatDetail)
|
||||
}
|
||||
|
||||
return chatsWithDetails, nil
|
||||
}
|
||||
216
internal/service/file_service.go
Normal file
216
internal/service/file_service.go
Normal file
@@ -0,0 +1,216 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"messenger/internal/models"
|
||||
"messenger/internal/pkg/logger"
|
||||
"messenger/internal/repository"
|
||||
"mime/multipart"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/google/uuid"
|
||||
)
|
||||
|
||||
type FileService struct {
|
||||
attachmentRepo repository.AttachmentRepository
|
||||
chatRepo repository.ChatRepository
|
||||
storagePath string
|
||||
maxFileSize int64
|
||||
}
|
||||
|
||||
func NewFileService(
|
||||
attachmentRepo repository.AttachmentRepository,
|
||||
chatRepo repository.ChatRepository,
|
||||
storagePath string,
|
||||
maxFileSizeMB int64,
|
||||
) *FileService {
|
||||
return &FileService{
|
||||
attachmentRepo: attachmentRepo,
|
||||
chatRepo: chatRepo,
|
||||
storagePath: storagePath,
|
||||
maxFileSize: maxFileSizeMB * 1024 * 1024, // Convert to bytes
|
||||
}
|
||||
}
|
||||
|
||||
// UploadFile загружает файл и создает запись в БД
|
||||
func (s *FileService) UploadFile(ctx context.Context, chatID, userID int64, fileHeader *multipart.FileHeader) (*models.Attachment, error) {
|
||||
// Проверяем, что пользователь участник чата
|
||||
isMember, err := s.chatRepo.IsMember(ctx, chatID, userID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if !isMember {
|
||||
return nil, errors.New("you are not a member of this chat")
|
||||
}
|
||||
|
||||
// Проверяем размер файла
|
||||
if fileHeader.Size > s.maxFileSize {
|
||||
return nil, fmt.Errorf("file too large: max %d MB", s.maxFileSize/(1024*1024))
|
||||
}
|
||||
|
||||
// Открываем файл
|
||||
file, err := fileHeader.Open()
|
||||
if err != nil {
|
||||
return nil, errors.New("failed to open file")
|
||||
}
|
||||
defer file.Close()
|
||||
|
||||
// Генерируем уникальное имя файла
|
||||
ext := filepath.Ext(fileHeader.Filename)
|
||||
uniqueID := uuid.New().String()
|
||||
safeFileName := uniqueID + ext
|
||||
|
||||
// Создаем поддиректорию по дате
|
||||
now := time.Now()
|
||||
subDir := filepath.Join(now.Format("2006"), now.Format("01"))
|
||||
fullDir := filepath.Join(s.storagePath, subDir)
|
||||
|
||||
// Создаем директорию если не существует
|
||||
if err := os.MkdirAll(fullDir, 0755); err != nil {
|
||||
logger.Error("Failed to create storage directory", "error", err)
|
||||
return nil, errors.New("failed to save file")
|
||||
}
|
||||
|
||||
// Полный путь к файлу
|
||||
filePath := filepath.Join(fullDir, safeFileName)
|
||||
|
||||
// Создаем файл на диске
|
||||
dst, err := os.Create(filePath)
|
||||
if err != nil {
|
||||
logger.Error("Failed to create file", "error", err)
|
||||
return nil, errors.New("failed to save file")
|
||||
}
|
||||
defer dst.Close()
|
||||
|
||||
// Копируем содержимое
|
||||
if _, err := io.Copy(dst, file); err != nil {
|
||||
logger.Error("Failed to copy file", "error", err)
|
||||
return nil, errors.New("failed to save file")
|
||||
}
|
||||
|
||||
// Определяем MIME тип
|
||||
mimeType := fileHeader.Header.Get("Content-Type")
|
||||
if mimeType == "" {
|
||||
// Простая проверка по расширению
|
||||
switch strings.ToLower(ext) {
|
||||
case ".jpg", ".jpeg":
|
||||
mimeType = "image/jpeg"
|
||||
case ".png":
|
||||
mimeType = "image/png"
|
||||
case ".gif":
|
||||
mimeType = "image/gif"
|
||||
case ".pdf":
|
||||
mimeType = "application/pdf"
|
||||
case ".txt":
|
||||
mimeType = "text/plain"
|
||||
default:
|
||||
mimeType = "application/octet-stream"
|
||||
}
|
||||
}
|
||||
|
||||
// Создаем запись в БД
|
||||
attachment := &models.Attachment{
|
||||
FileName: fileHeader.Filename,
|
||||
FileSize: fileHeader.Size,
|
||||
StoragePath: filePath,
|
||||
MimeType: mimeType,
|
||||
UploadedAt: now,
|
||||
}
|
||||
|
||||
if err := s.attachmentRepo.Create(ctx, attachment); err != nil {
|
||||
// Если не удалось сохранить в БД, удаляем файл
|
||||
os.Remove(filePath)
|
||||
logger.Error("Failed to create attachment record", "error", err)
|
||||
return nil, errors.New("failed to save file info")
|
||||
}
|
||||
|
||||
logger.Info("File uploaded", "attachment_id", attachment.ID, "user_id", userID, "chat_id", chatID)
|
||||
return attachment, nil
|
||||
}
|
||||
|
||||
// GetAttachment возвращает информацию о вложении
|
||||
func (s *FileService) GetAttachment(ctx context.Context, attachmentID, userID int64) (*models.Attachment, error) {
|
||||
attachment, err := s.attachmentRepo.FindByID(ctx, attachmentID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if attachment == nil {
|
||||
return nil, errors.New("attachment not found")
|
||||
}
|
||||
|
||||
// Проверяем доступ пользователя к файлу
|
||||
// Находим сообщение, к которому прикреплен файл
|
||||
if attachment.MessageID != nil {
|
||||
// Здесь нужно проверить, что пользователь участник чата
|
||||
// Для этого нужен доступ к messageRepo, но пока пропустим
|
||||
// В реальном коде нужно добавить проверку
|
||||
}
|
||||
|
||||
return attachment, nil
|
||||
}
|
||||
|
||||
// DownloadFile возвращает файл для скачивания
|
||||
func (s *FileService) DownloadFile(ctx context.Context, attachmentID, userID int64) (string, *os.File, string, error) {
|
||||
attachment, err := s.GetAttachment(ctx, attachmentID, userID)
|
||||
if err != nil {
|
||||
return "", nil, "", err
|
||||
}
|
||||
|
||||
// Открываем файл
|
||||
file, err := os.Open(attachment.StoragePath)
|
||||
if err != nil {
|
||||
logger.Error("Failed to open file", "error", err)
|
||||
return "", nil, "", errors.New("file not found")
|
||||
}
|
||||
|
||||
return attachment.FileName, file, attachment.MimeType, nil
|
||||
}
|
||||
|
||||
// DeleteAttachment удаляет вложение (только если оно не привязано к сообщению)
|
||||
func (s *FileService) DeleteAttachment(ctx context.Context, attachmentID, userID int64, isGlobalAdmin bool) error {
|
||||
attachment, err := s.attachmentRepo.FindByID(ctx, attachmentID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if attachment == nil {
|
||||
return errors.New("attachment not found")
|
||||
}
|
||||
|
||||
// Нельзя удалить файл, который привязан к сообщению
|
||||
if attachment.MessageID != nil && !isGlobalAdmin {
|
||||
return errors.New("cannot delete attachment that is linked to a message")
|
||||
}
|
||||
|
||||
// Удаляем файл с диска
|
||||
if err := os.Remove(attachment.StoragePath); err != nil {
|
||||
logger.Error("Failed to delete file", "error", err)
|
||||
// Не возвращаем ошибку, продолжаем удаление из БД
|
||||
}
|
||||
|
||||
// Удаляем запись из БД
|
||||
if err := s.attachmentRepo.Delete(ctx, attachmentID); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
logger.Info("Attachment deleted", "attachment_id", attachmentID, "user_id", userID)
|
||||
return nil
|
||||
}
|
||||
|
||||
// GetMaxFileSize возвращает максимальный размер файла в байтах
|
||||
func (s *FileService) GetMaxFileSize() int64 {
|
||||
return s.maxFileSize
|
||||
}
|
||||
|
||||
// GetMaxFileSizeMB возвращает максимальный размер файла в мегабайтах
|
||||
func (s *FileService) GetMaxFileSizeMB() int64 {
|
||||
return s.maxFileSize / (1024 * 1024)
|
||||
}
|
||||
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)
|
||||
}
|
||||
125
internal/service/user_service.go
Normal file
125
internal/service/user_service.go
Normal file
@@ -0,0 +1,125 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"messenger/internal/models"
|
||||
"messenger/internal/pkg/logger"
|
||||
"messenger/internal/pkg/validator"
|
||||
"messenger/internal/repository"
|
||||
)
|
||||
|
||||
type UserService struct {
|
||||
userRepo repository.UserRepository
|
||||
profileRepo repository.ProfileRepository
|
||||
}
|
||||
|
||||
func NewUserService(userRepo repository.UserRepository, profileRepo repository.ProfileRepository) *UserService {
|
||||
return &UserService{
|
||||
userRepo: userRepo,
|
||||
profileRepo: profileRepo,
|
||||
}
|
||||
}
|
||||
|
||||
// GetProfile возвращает профиль пользователя
|
||||
func (s *UserService) GetProfile(ctx context.Context, userID int64) (*models.ProfileWithUser, error) {
|
||||
user, err := s.userRepo.FindByID(ctx, userID)
|
||||
if err != nil {
|
||||
logger.Error("Failed to get user", "error", err)
|
||||
return nil, errors.New("internal server error")
|
||||
}
|
||||
|
||||
if user == nil {
|
||||
return nil, errors.New("user not found")
|
||||
}
|
||||
|
||||
profile, err := s.profileRepo.FindByUserID(ctx, userID)
|
||||
if err != nil {
|
||||
logger.Error("Failed to get profile", "error", err)
|
||||
// Профиль может отсутствовать - не ошибка
|
||||
profile = &models.Profile{UserID: userID}
|
||||
}
|
||||
|
||||
return &models.ProfileWithUser{
|
||||
User: user.ToSafe(),
|
||||
DisplayName: profile.DisplayName,
|
||||
Bio: profile.Bio,
|
||||
AvatarURL: profile.AvatarURL,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// UpdateProfile обновляет профиль пользователя
|
||||
func (s *UserService) UpdateProfile(ctx context.Context, userID int64, displayName, bio *string) error {
|
||||
// Валидация
|
||||
if displayName != nil && !validator.ValidateDisplayName(*displayName) {
|
||||
return errors.New("display name too long (max 100 characters)")
|
||||
}
|
||||
|
||||
if bio != nil && !validator.ValidateBio(*bio) {
|
||||
return errors.New("bio too long (max 500 characters)")
|
||||
}
|
||||
|
||||
profile := &models.Profile{
|
||||
UserID: userID,
|
||||
DisplayName: displayName,
|
||||
Bio: bio,
|
||||
}
|
||||
|
||||
if err := s.profileRepo.Update(ctx, profile); err != nil {
|
||||
logger.Error("Failed to update profile", "error", err)
|
||||
return errors.New("failed to update profile")
|
||||
}
|
||||
|
||||
logger.Info("Profile updated", "user_id", userID)
|
||||
return nil
|
||||
}
|
||||
|
||||
// UpdateAvatar обновляет аватар пользователя
|
||||
func (s *UserService) UpdateAvatar(ctx context.Context, userID int64, avatarURL *string) error {
|
||||
if err := s.profileRepo.UpdateAvatar(ctx, userID, avatarURL); err != nil {
|
||||
logger.Error("Failed to update avatar", "error", err)
|
||||
return errors.New("failed to update avatar")
|
||||
}
|
||||
|
||||
logger.Info("Avatar updated", "user_id", userID)
|
||||
return nil
|
||||
}
|
||||
|
||||
// SearchUsers ищет пользователей по логину
|
||||
func (s *UserService) SearchUsers(ctx context.Context, query string, limit int) ([]*models.SafeUser, error) {
|
||||
if limit <= 0 || limit > 100 {
|
||||
limit = 20
|
||||
}
|
||||
|
||||
users, err := s.userRepo.SearchByLogin(ctx, query, limit)
|
||||
if err != nil {
|
||||
logger.Error("Failed to search users", "error", err)
|
||||
return nil, errors.New("internal server error")
|
||||
}
|
||||
|
||||
safeUsers := make([]*models.SafeUser, len(users))
|
||||
for i, user := range users {
|
||||
safeUsers[i] = user.ToSafe()
|
||||
}
|
||||
|
||||
return safeUsers, nil
|
||||
}
|
||||
|
||||
// GetUserByID возвращает безопасное представление пользователя
|
||||
func (s *UserService) GetUserByID(ctx context.Context, userID int64) (*models.SafeUser, error) {
|
||||
user, err := s.userRepo.FindByID(ctx, userID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if user == nil {
|
||||
return nil, errors.New("user not found")
|
||||
}
|
||||
|
||||
return user.ToSafe(), nil
|
||||
}
|
||||
|
||||
// GetUserByLogin возвращает пользователя по логину
|
||||
func (s *UserService) GetUserByLogin(ctx context.Context, login string) (*models.User, error) {
|
||||
return s.userRepo.FindByLogin(ctx, login)
|
||||
}
|
||||
Reference in New Issue
Block a user