package api import ( "context" "encoding/json" "log/slog" "net/http" "github.com/go-chi/chi/v5" "github.com/go-chi/render" "github.com/google/uuid" "time" ) func MessageCtx(next http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { slog.Debug("message: entering MessageCtx middleware") var message *Message var err error if messageID := chi.URLParam(r, "messageID"); messageID != "" { slog.Debug("message: fetching message", "messageID", messageID) message, err = dbGetMessage(messageID) } else { slog.Error("message: messageID not found in URL parameters") render.Render(w, r, ErrNotFound) return } if err != nil { slog.Error("message: failed to fetch message", "messageID", chi.URLParam(r, "messageID"), "error", err) render.Render(w, r, ErrNotFound) return } slog.Debug("message: successfully fetched message", "messageID", message.ID) ctx := context.WithValue(r.Context(), messageKey{}, message) next.ServeHTTP(w, r.WithContext(ctx)) }) } func GetMessage(w http.ResponseWriter, r *http.Request) { slog.Debug("message: entering GetMessage handler") message, ok := r.Context().Value(messageKey{}).(*Message) if !ok || message == nil { slog.Error("message: message not found in context") render.Render(w, r, ErrNotFound) return } slog.Debug("message: rendering message", "messageID", message.ID) if err := render.Render(w, r, NewMessageResponse(message)); err != nil { slog.Error("message: failed to render message response", "messageID", message.ID, "error", err) render.Render(w, r, ErrRender(err)) return } } func EditMessage(w http.ResponseWriter, r *http.Request) { slog.Debug("message: entering EditMessage handler") message, ok := r.Context().Value(messageKey{}).(*Message) if !ok || message == nil { slog.Error("message: message not found in context") render.Render(w, r, ErrNotFound) return } err := r.ParseMultipartForm(64 << 10) if err != nil { slog.Error("message: failed to parse multipart form", "error", err) http.Error(w, "Unable to parse form", http.StatusBadRequest) return } body := r.FormValue("body") if body == "" { slog.Error("message: message body is empty") http.Error(w, "Message body cannot be empty", http.StatusBadRequest) return } slog.Debug("message: updating message", "messageID", message.ID) message.Body = body editedTime := time.Now() message.Edited = &editedTime err = dbUpdateMessage(message) if err != nil { slog.Error("message: failed to update message", "messageID", message.ID, "error", err) render.Render(w, r, ErrRender(err)) return } slog.Debug("message: successfully updated message", "messageID", message.ID) if err := render.Render(w, r, NewMessageResponse(message)); err != nil { slog.Error("message: failed to render updated message response", "messageID", message.ID, "error", err) render.Render(w, r, ErrRender(err)) return } } func DeleteMessage(w http.ResponseWriter, r *http.Request) { slog.Debug("message: entering DeleteMessage handler") message, ok := r.Context().Value(messageKey{}).(*Message) if !ok || message == nil { slog.Error("message: message not found in context") render.Render(w, r, ErrNotFound) return } slog.Debug("message: deleting message", "messageID", message.ID) err := dbDeleteMessage(message.ID.String()) if err != nil { slog.Error("message: failed to delete message", "messageID", message.ID, "error", err) render.Render(w, r, ErrRender(err)) return } slog.Debug("message: successfully deleted message", "messageID", message.ID) if err := render.Render(w, r, NewMessageResponse(message)); err != nil { slog.Error("message: failed to render deleted message response", "messageID", message.ID, "error", err) render.Render(w, r, ErrRender(err)) return } } func ListMessages(w http.ResponseWriter, r *http.Request) { slog.Debug("message: entering ListMessages handler") dbMessages, err := dbGetAllMessages() if err != nil { slog.Error("message: failed to fetch messages", "error", err) render.Render(w, r, ErrRender(err)) return } slog.Debug("message: successfully fetched messages", "count", len(dbMessages)) if err := render.RenderList(w, r, NewMessageListResponse(dbMessages)); err != nil { slog.Error("message: failed to render message list response", "error", err) render.Render(w, r, ErrRender(err)) return } } func newMessageID() uuid.UUID { return uuid.New() } func NewMessage(w http.ResponseWriter, r *http.Request) { slog.Debug("message: entering NewMessage handler") err := r.ParseMultipartForm(64 << 10) if err != nil { slog.Error("message: failed to parse multipart form", "error", err) http.Error(w, "Unable to parse form", http.StatusBadRequest) return } var user = r.Context().Value(userKey{}).(*User) body := r.FormValue("body") if body == "" { slog.Error("message: message body is empty") http.Error(w, "Invalid body", http.StatusBadRequest) return } msg := Message{ ID: newMessageID(), UserID: user.ID, Body: body, Timestamp: time.Now(), } slog.Debug("message: creating new message", "messageID", msg.ID) err = dbAddMessage(&msg) if err != nil { slog.Error("message: failed to add new message", "messageID", msg.ID, "error", err) render.Render(w, r, ErrRender(err)) return } slog.Debug("message: successfully created new message", "messageID", msg.ID) render.Render(w, r, NewMessageResponse(&msg)) } type messageKey struct{} type Message struct { ID uuid.UUID `json:"id"` UserID uuid.UUID `json:"user_id"` Body string `json:"body"` Timestamp time.Time `json:"timestamp"` Edited *time.Time `json:"edited"` } type MessageRequest struct { *Message User *UserPayload `json:"user"` ProtectedID string `json:"id"` } type MessageResponse struct { *Message User *UserPayload `json:"user,omitempty"` Elapsed int64 `json:"elapsed"` } func (m MessageResponse) MarshalJSON() ([]byte, error) { type OrderedMessageResponse struct { ID uuid.UUID `json:"id"` UserID uuid.UUID `json:"user_id"` Body string `json:"body"` Timestamp string `json:"timestamp"` Edited *string `json:"edited,omitempty"` // Use a pointer to allow null values User *UserPayload `json:"user,omitempty"` Elapsed int64 `json:"elapsed"` } var edited *string if m.Message.Edited != nil { // Check if Edited is not the zero value editedStr := m.Message.Edited.Format(time.RFC3339) edited = &editedStr } ordered := OrderedMessageResponse{ ID: m.Message.ID, UserID: m.Message.UserID, Body: m.Message.Body, Timestamp: m.Message.Timestamp.Format(time.RFC3339), Edited: edited, // Null if Edited is zero User: m.User, Elapsed: m.Elapsed, } return json.Marshal(ordered) }