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,41 @@
package handlers
import (
"messenger/internal/api/responses"
"messenger/internal/service"
"net/http"
//"github.com/go-chi/chi/v5"
)
type AdminHandler struct {
adminService *service.AdminService
}
func NewAdminHandler(adminService *service.AdminService) *AdminHandler {
return &AdminHandler{
adminService: adminService,
}
}
func (h *AdminHandler) DeleteUser(w http.ResponseWriter, r *http.Request) {
// userID, err := strconv.ParseInt(chi.URLParam(r, "id"), 10, 64)
// if err != nil {
// responses.BadRequest(w, "invalid user id")
// return
// }
// TODO: Реализовать удаление пользователя
responses.Success(w, http.StatusOK, map[string]string{"message": "user deleted"})
}
func (h *AdminHandler) DeleteMessage(w http.ResponseWriter, r *http.Request) {
// messageID, err := strconv.ParseInt(chi.URLParam(r, "id"), 10, 64)
// if err != nil {
// responses.BadRequest(w, "invalid message id")
// return
// }
// TODO: Реализовать удаление сообщения
responses.Success(w, http.StatusOK, map[string]string{"message": "message deleted"})
}

View File

@@ -0,0 +1,80 @@
package handlers
import (
"encoding/json"
"messenger/internal/api/middleware"
"messenger/internal/api/responses"
"messenger/internal/service"
"net/http"
)
type AuthHandler struct {
authService *service.AuthService
}
func NewAuthHandler(authService *service.AuthService) *AuthHandler {
return &AuthHandler{authService: authService}
}
type RegisterRequest struct {
Login string `json:"login"`
Password string `json:"password"`
}
type LoginRequest struct {
Login string `json:"login"`
Password string `json:"password"`
}
type AuthResponse struct {
Token string `json:"token"`
User interface{} `json:"user"`
}
func (h *AuthHandler) Register(w http.ResponseWriter, r *http.Request) {
var req RegisterRequest
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
responses.BadRequest(w, "invalid request body")
return
}
user, token, err := h.authService.Register(r.Context(), req.Login, req.Password)
if err != nil {
responses.BadRequest(w, err.Error())
return
}
responses.Success(w, http.StatusCreated, AuthResponse{
Token: token,
User: user.ToSafe(),
})
}
func (h *AuthHandler) Login(w http.ResponseWriter, r *http.Request) {
var req LoginRequest
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
responses.BadRequest(w, "invalid request body")
return
}
user, token, err := h.authService.Login(r.Context(), req.Login, req.Password)
if err != nil {
responses.Unauthorized(w, err.Error())
return
}
responses.Success(w, http.StatusOK, AuthResponse{
Token: token,
User: user.ToSafe(),
})
}
func (h *AuthHandler) GetMe(w http.ResponseWriter, r *http.Request) {
user := middleware.GetUserFromContext(r.Context())
if user == nil {
responses.Unauthorized(w, "user not found")
return
}
responses.Success(w, http.StatusOK, user.ToSafe())
}

View File

@@ -0,0 +1,228 @@
package handlers
import (
"encoding/json"
"messenger/internal/api/middleware"
"messenger/internal/api/responses"
"messenger/internal/models"
"messenger/internal/service"
"net/http"
"strconv"
"github.com/go-chi/chi/v5"
)
type ChatHandler struct {
chatService *service.ChatService
userService *service.UserService
}
func NewChatHandler(chatService *service.ChatService, userService *service.UserService) *ChatHandler {
return &ChatHandler{
chatService: chatService,
userService: userService,
}
}
func (h *ChatHandler) CreatePrivateChat(w http.ResponseWriter, r *http.Request) {
user := middleware.GetUserFromContext(r.Context())
if user == nil {
responses.Unauthorized(w, "user not found")
return
}
var req models.CreatePrivateChatRequest
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
responses.BadRequest(w, "invalid request body")
return
}
if req.TargetLogin == "" {
responses.BadRequest(w, "target_login is required")
return
}
// Находим целевого пользователя по логину
targetUser, err := h.userService.GetUserByLogin(r.Context(), req.TargetLogin)
if err != nil {
responses.NotFound(w, "target user not found")
return
}
if targetUser == nil {
responses.NotFound(w, "target user not found")
return
}
// Создаем приватный чат
chat, err := h.chatService.CreatePrivateChat(r.Context(), user.ID, targetUser.ID)
if err != nil {
responses.InternalServerError(w, err.Error())
return
}
responses.Success(w, http.StatusCreated, chat)
}
func (h *ChatHandler) CreateGroupChat(w http.ResponseWriter, r *http.Request) {
user := middleware.GetUserFromContext(r.Context())
if user == nil {
responses.Unauthorized(w, "user not found")
return
}
var req models.CreateGroupChatRequest
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
responses.BadRequest(w, "invalid request body")
return
}
chat, err := h.chatService.CreateGroupChat(r.Context(), user.ID, req.Title, req.MemberLogins)
if err != nil {
responses.BadRequest(w, err.Error())
return
}
responses.Success(w, http.StatusCreated, chat)
}
func (h *ChatHandler) GetMyChats(w http.ResponseWriter, r *http.Request) {
user := middleware.GetUserFromContext(r.Context())
if user == nil {
responses.Unauthorized(w, "user not found")
return
}
chats, err := h.chatService.GetUserChatsWithDetails(r.Context(), user.ID)
if err != nil {
responses.InternalServerError(w, err.Error())
return
}
responses.Success(w, http.StatusOK, chats)
}
func (h *ChatHandler) GetChatByID(w http.ResponseWriter, r *http.Request) {
user := middleware.GetUserFromContext(r.Context())
if user == nil {
responses.Unauthorized(w, "user not found")
return
}
chatID, err := strconv.ParseInt(chi.URLParam(r, "id"), 10, 64)
if err != nil {
responses.BadRequest(w, "invalid chat id")
return
}
chat, err := h.chatService.GetChatByID(r.Context(), chatID, user.ID)
if err != nil {
responses.NotFound(w, err.Error())
return
}
responses.Success(w, http.StatusOK, chat)
}
func (h *ChatHandler) GetChatMembers(w http.ResponseWriter, r *http.Request) {
user := middleware.GetUserFromContext(r.Context())
if user == nil {
responses.Unauthorized(w, "user not found")
return
}
chatID, err := strconv.ParseInt(chi.URLParam(r, "id"), 10, 64)
if err != nil {
responses.BadRequest(w, "invalid chat id")
return
}
members, err := h.chatService.GetChatMembers(r.Context(), chatID, user.ID)
if err != nil {
responses.Forbidden(w, err.Error())
return
}
responses.Success(w, http.StatusOK, members)
}
func (h *ChatHandler) AddMembers(w http.ResponseWriter, r *http.Request) {
user := middleware.GetUserFromContext(r.Context())
if user == nil {
responses.Unauthorized(w, "user not found")
return
}
chatID, err := strconv.ParseInt(chi.URLParam(r, "id"), 10, 64)
if err != nil {
responses.BadRequest(w, "invalid chat id")
return
}
var req models.AddMembersRequest
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
responses.BadRequest(w, "invalid request body")
return
}
if err := h.chatService.AddMembers(r.Context(), chatID, user.ID, req.UserLogins); err != nil {
responses.BadRequest(w, err.Error())
return
}
responses.Success(w, http.StatusOK, map[string]string{"message": "members added"})
}
func (h *ChatHandler) RemoveMember(w http.ResponseWriter, r *http.Request) {
user := middleware.GetUserFromContext(r.Context())
if user == nil {
responses.Unauthorized(w, "user not found")
return
}
chatID, err := strconv.ParseInt(chi.URLParam(r, "id"), 10, 64)
if err != nil {
responses.BadRequest(w, "invalid chat id")
return
}
memberID, err := strconv.ParseInt(chi.URLParam(r, "user_id"), 10, 64)
if err != nil {
responses.BadRequest(w, "invalid member id")
return
}
if err := h.chatService.RemoveMember(r.Context(), chatID, user.ID, memberID); err != nil {
responses.BadRequest(w, err.Error())
return
}
responses.Success(w, http.StatusOK, map[string]string{"message": "member removed"})
}
func (h *ChatHandler) UpdateChatTitle(w http.ResponseWriter, r *http.Request) {
user := middleware.GetUserFromContext(r.Context())
if user == nil {
responses.Unauthorized(w, "user not found")
return
}
chatID, err := strconv.ParseInt(chi.URLParam(r, "id"), 10, 64)
if err != nil {
responses.BadRequest(w, "invalid chat id")
return
}
var req models.UpdateChatTitleRequest
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
responses.BadRequest(w, "invalid request body")
return
}
if err := h.chatService.UpdateChatTitle(r.Context(), chatID, user.ID, req.Title); err != nil {
responses.BadRequest(w, err.Error())
return
}
responses.Success(w, http.StatusOK, map[string]string{"message": "chat title updated"})
}

View File

@@ -0,0 +1,82 @@
package handlers
import (
"messenger/internal/api/middleware"
"messenger/internal/api/responses"
"messenger/internal/service"
"net/http"
"strconv"
"time"
"github.com/go-chi/chi/v5"
)
type FileHandler struct {
fileService *service.FileService
}
func NewFileHandler(fileService *service.FileService) *FileHandler {
return &FileHandler{
fileService: fileService,
}
}
func (h *FileHandler) UploadFile(w http.ResponseWriter, r *http.Request) {
user := middleware.GetUserFromContext(r.Context())
if user == nil {
responses.Unauthorized(w, "user not found")
return
}
chatID, err := strconv.ParseInt(chi.URLParam(r, "id"), 10, 64)
if err != nil {
responses.BadRequest(w, "invalid chat id")
return
}
// Parse multipart form (20 MB max)
if err := r.ParseMultipartForm(20 << 20); err != nil {
responses.BadRequest(w, "failed to parse form")
return
}
file, header, err := r.FormFile("file")
if err != nil {
responses.BadRequest(w, "file is required")
return
}
defer file.Close()
attachment, err := h.fileService.UploadFile(r.Context(), chatID, user.ID, header)
if err != nil {
responses.BadRequest(w, err.Error())
return
}
responses.Success(w, http.StatusOK, attachment)
}
func (h *FileHandler) DownloadFile(w http.ResponseWriter, r *http.Request) {
user := middleware.GetUserFromContext(r.Context())
if user == nil {
responses.Unauthorized(w, "user not found")
return
}
attachmentID, err := strconv.ParseInt(chi.URLParam(r, "id"), 10, 64)
if err != nil {
responses.BadRequest(w, "invalid attachment id")
return
}
fileName, file, mimeType, err := h.fileService.DownloadFile(r.Context(), attachmentID, user.ID)
if err != nil {
responses.NotFound(w, err.Error())
return
}
defer file.Close()
w.Header().Set("Content-Type", mimeType)
w.Header().Set("Content-Disposition", "attachment; filename="+strconv.Quote(fileName))
http.ServeContent(w, r, fileName, time.Now(), file)
}

View File

@@ -0,0 +1,83 @@
package handlers
import (
//"encoding/json"
"messenger/internal/api/middleware"
"messenger/internal/api/responses"
//"messenger/internal/models"
"messenger/internal/service"
"net/http"
"strconv"
"time"
"github.com/go-chi/chi/v5"
)
type MessageHandler struct {
messageService *service.MessageService
chatService *service.ChatService
}
func NewMessageHandler(messageService *service.MessageService, chatService *service.ChatService) *MessageHandler {
return &MessageHandler{
messageService: messageService,
chatService: chatService,
}
}
func (h *MessageHandler) GetMessages(w http.ResponseWriter, r *http.Request) {
user := middleware.GetUserFromContext(r.Context())
if user == nil {
responses.Unauthorized(w, "user not found")
return
}
chatID, err := strconv.ParseInt(chi.URLParam(r, "id"), 10, 64)
if err != nil {
responses.BadRequest(w, "invalid chat id")
return
}
limit := 50
if l := r.URL.Query().Get("limit"); l != "" {
if parsed, err := strconv.Atoi(l); err == nil && parsed > 0 && parsed <= 100 {
limit = parsed
}
}
var before time.Time
if b := r.URL.Query().Get("before"); b != "" {
if parsed, err := time.Parse(time.RFC3339, b); err == nil {
before = parsed
}
}
messages, err := h.messageService.GetChatHistory(r.Context(), chatID, user.ID, limit, before)
if err != nil {
responses.InternalServerError(w, err.Error())
return
}
responses.Success(w, http.StatusOK, messages)
}
func (h *MessageHandler) MarkAsRead(w http.ResponseWriter, r *http.Request) {
user := middleware.GetUserFromContext(r.Context())
if user == nil {
responses.Unauthorized(w, "user not found")
return
}
messageID, err := strconv.ParseInt(chi.URLParam(r, "id"), 10, 64)
if err != nil {
responses.BadRequest(w, "invalid message id")
return
}
if err := h.messageService.MarkMessageAsRead(r.Context(), messageID, user.ID); err != nil {
responses.InternalServerError(w, err.Error())
return
}
responses.Success(w, http.StatusOK, map[string]string{"message": "marked as read"})
}

View File

@@ -0,0 +1,103 @@
package handlers
import (
"context"
"encoding/json"
"messenger/internal/api/middleware"
"messenger/internal/api/responses"
"messenger/internal/models"
"messenger/internal/service"
"net/http"
"strconv"
)
type UserHandler struct {
userService *service.UserService
}
func NewUserHandler(userService *service.UserService) *UserHandler {
return &UserHandler{userService: userService}
}
type UpdateProfileRequest struct {
DisplayName *string `json:"display_name,omitempty"`
Bio *string `json:"bio,omitempty"`
}
func (h *UserHandler) GetProfile(w http.ResponseWriter, r *http.Request) {
user := middleware.GetUserFromContext(r.Context())
if user == nil {
responses.Unauthorized(w, "user not found")
return
}
profile, err := h.userService.GetProfile(r.Context(), user.ID)
if err != nil {
responses.NotFound(w, err.Error())
return
}
responses.Success(w, http.StatusOK, profile)
}
func (h *UserHandler) UpdateProfile(w http.ResponseWriter, r *http.Request) {
user := middleware.GetUserFromContext(r.Context())
if user == nil {
responses.Unauthorized(w, "user not found")
return
}
var req UpdateProfileRequest
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
responses.BadRequest(w, "invalid request body")
return
}
if err := h.userService.UpdateProfile(r.Context(), user.ID, req.DisplayName, req.Bio); err != nil {
responses.BadRequest(w, err.Error())
return
}
responses.Success(w, http.StatusOK, map[string]string{"message": "profile updated"})
}
func (h *UserHandler) SearchUsers(w http.ResponseWriter, r *http.Request) {
query := r.URL.Query().Get("q")
if query == "" {
responses.BadRequest(w, "search query required")
return
}
limit := 20
if l := r.URL.Query().Get("limit"); l != "" {
if parsed, err := strconv.Atoi(l); err == nil && parsed > 0 && parsed <= 100 {
limit = parsed
}
}
users, err := h.userService.SearchUsers(r.Context(), query, limit)
if err != nil {
responses.InternalServerError(w, err.Error())
return
}
responses.Success(w, http.StatusOK, users)
}
func (h *UserHandler) GetUserByID(w http.ResponseWriter, r *http.Request) {
// Реализация с chi.URLParam будет в main.go
// Пока заглушка
responses.Success(w, http.StatusOK, nil)
}
// GetUserByLoginFromContext - метод для получения пользователя по логину (используется в других хендлерах)
func (h *UserHandler) GetUserByLogin(ctx context.Context, login string) (*models.SafeUser, error) {
user, err := h.userService.GetUserByLogin(ctx, login)
if err != nil {
return nil, err
}
if user == nil {
return nil, nil
}
return user.ToSafe(), nil
}

View File

@@ -0,0 +1,58 @@
package handlers
import (
"net/http"
gorillaWS "github.com/gorilla/websocket"
"messenger/internal/pkg/logger"
"messenger/internal/service"
ws "messenger/internal/websocket"
)
var upgrader = gorillaWS.Upgrader{
CheckOrigin: func(r *http.Request) bool {
return true
},
}
type WebSocketHandler struct {
hub *ws.Hub
authService *service.AuthService
}
func NewWebSocketHandler(hub *ws.Hub, authService *service.AuthService) *WebSocketHandler {
return &WebSocketHandler{
hub: hub,
authService: authService,
}
}
func (h *WebSocketHandler) HandleWebSocket(w http.ResponseWriter, r *http.Request) {
token := r.URL.Query().Get("token")
if token == "" {
http.Error(w, "missing token", http.StatusUnauthorized)
return
}
user, err := h.authService.ValidateToken(token)
if err != nil {
logger.Error("WebSocket auth failed", "error", err)
http.Error(w, "invalid token", http.StatusUnauthorized)
return
}
logger.Info("WebSocket connecting", "user_id", user.ID, "login", user.Login)
conn, err := upgrader.Upgrade(w, r, nil)
if err != nil {
logger.Error("WebSocket upgrade failed", "error", err)
return
}
client := ws.NewClient(h.hub, conn, user)
h.hub.GetRegisterChan() <- client
go client.WritePump()
client.ReadPump()
}