package handlers import ( "context" "log/slog" "net/http" "strconv" "strings" "time" "github.com/labstack/echo/v4" "github.com/memohai/memoh/internal/accounts" "github.com/memohai/memoh/internal/bots" "github.com/memohai/memoh/internal/schedule" ) type ScheduleHandler struct { service *schedule.Service botService *bots.Service accountService *accounts.Service logger *slog.Logger } func NewScheduleHandler(log *slog.Logger, service *schedule.Service, botService *bots.Service, accountService *accounts.Service) *ScheduleHandler { return &ScheduleHandler{ service: service, botService: botService, accountService: accountService, logger: log.With(slog.String("handler", "schedule")), } } func (h *ScheduleHandler) Register(e *echo.Echo) { group := e.Group("/bots/:bot_id/schedule") group.POST("", h.Create) group.GET("", h.List) group.GET("/logs", h.ListLogs) group.DELETE("/logs", h.DeleteLogs) group.GET("/:id", h.Get) group.GET("/:id/logs", h.ListLogsBySchedule) group.PUT("/:id", h.Update) group.DELETE("/:id", h.Delete) } // Create godoc // @Summary Create schedule // @Description Create a schedule for current user // @Tags schedule // @Param payload body schedule.CreateRequest true "Schedule payload" // @Success 201 {object} schedule.Schedule // @Failure 400 {object} ErrorResponse // @Failure 500 {object} ErrorResponse // @Router /bots/{bot_id}/schedule [post]. func (h *ScheduleHandler) Create(c echo.Context) error { userID, err := h.requireUserID(c) if err != nil { return err } botID := strings.TrimSpace(c.Param("bot_id")) if botID == "" { return echo.NewHTTPError(http.StatusBadRequest, "bot id is required") } if _, err := h.authorizeBotAccess(c.Request().Context(), userID, botID); err != nil { return err } var req schedule.CreateRequest if err := c.Bind(&req); err != nil { return echo.NewHTTPError(http.StatusBadRequest, err.Error()) } resp, err := h.service.Create(c.Request().Context(), botID, req) if err != nil { return echo.NewHTTPError(http.StatusInternalServerError, err.Error()) } return c.JSON(http.StatusCreated, resp) } // List godoc // @Summary List schedules // @Description List schedules for current user // @Tags schedule // @Success 200 {object} schedule.ListResponse // @Failure 400 {object} ErrorResponse // @Failure 500 {object} ErrorResponse // @Router /bots/{bot_id}/schedule [get]. func (h *ScheduleHandler) List(c echo.Context) error { userID, err := h.requireUserID(c) if err != nil { return err } botID := strings.TrimSpace(c.Param("bot_id")) if botID == "" { return echo.NewHTTPError(http.StatusBadRequest, "bot id is required") } if _, err := h.authorizeBotAccess(c.Request().Context(), userID, botID); err != nil { return err } items, err := h.service.List(c.Request().Context(), botID) if err != nil { return echo.NewHTTPError(http.StatusInternalServerError, err.Error()) } return c.JSON(http.StatusOK, schedule.ListResponse{Items: items}) } // Get godoc // @Summary Get schedule // @Description Get a schedule by ID // @Tags schedule // @Param id path string true "Schedule ID" // @Success 200 {object} schedule.Schedule // @Failure 400 {object} ErrorResponse // @Failure 404 {object} ErrorResponse // @Failure 500 {object} ErrorResponse // @Router /bots/{bot_id}/schedule/{id} [get]. func (h *ScheduleHandler) Get(c echo.Context) error { userID, err := h.requireUserID(c) if err != nil { return err } botID := strings.TrimSpace(c.Param("bot_id")) if botID == "" { return echo.NewHTTPError(http.StatusBadRequest, "bot id is required") } id := c.Param("id") if id == "" { return echo.NewHTTPError(http.StatusBadRequest, "id is required") } item, err := h.service.Get(c.Request().Context(), id) if err != nil { return echo.NewHTTPError(http.StatusNotFound, err.Error()) } if item.BotID != botID { return echo.NewHTTPError(http.StatusForbidden, "bot mismatch") } if _, err := h.authorizeBotAccess(c.Request().Context(), userID, botID); err != nil { return err } return c.JSON(http.StatusOK, item) } // Update godoc // @Summary Update schedule // @Description Update a schedule by ID // @Tags schedule // @Param id path string true "Schedule ID" // @Param payload body schedule.UpdateRequest true "Schedule payload" // @Success 200 {object} schedule.Schedule // @Failure 400 {object} ErrorResponse // @Failure 500 {object} ErrorResponse // @Router /bots/{bot_id}/schedule/{id} [put]. func (h *ScheduleHandler) Update(c echo.Context) error { userID, err := h.requireUserID(c) if err != nil { return err } botID := strings.TrimSpace(c.Param("bot_id")) if botID == "" { return echo.NewHTTPError(http.StatusBadRequest, "bot id is required") } id := c.Param("id") if id == "" { return echo.NewHTTPError(http.StatusBadRequest, "id is required") } var req schedule.UpdateRequest if err := c.Bind(&req); err != nil { return echo.NewHTTPError(http.StatusBadRequest, err.Error()) } item, err := h.service.Get(c.Request().Context(), id) if err != nil { return echo.NewHTTPError(http.StatusNotFound, err.Error()) } if item.BotID != botID { return echo.NewHTTPError(http.StatusForbidden, "bot mismatch") } if _, err := h.authorizeBotAccess(c.Request().Context(), userID, botID); err != nil { return err } resp, err := h.service.Update(c.Request().Context(), id, req) if err != nil { return echo.NewHTTPError(http.StatusInternalServerError, err.Error()) } return c.JSON(http.StatusOK, resp) } // Delete godoc // @Summary Delete schedule // @Description Delete a schedule by ID // @Tags schedule // @Param id path string true "Schedule ID" // @Success 204 "No Content" // @Failure 400 {object} ErrorResponse // @Failure 500 {object} ErrorResponse // @Router /bots/{bot_id}/schedule/{id} [delete]. func (h *ScheduleHandler) Delete(c echo.Context) error { userID, err := h.requireUserID(c) if err != nil { return err } botID := strings.TrimSpace(c.Param("bot_id")) if botID == "" { return echo.NewHTTPError(http.StatusBadRequest, "bot id is required") } id := c.Param("id") if id == "" { return echo.NewHTTPError(http.StatusBadRequest, "id is required") } item, err := h.service.Get(c.Request().Context(), id) if err != nil { return echo.NewHTTPError(http.StatusNotFound, err.Error()) } if item.BotID != botID { return echo.NewHTTPError(http.StatusForbidden, "bot mismatch") } if _, err := h.authorizeBotAccess(c.Request().Context(), userID, botID); err != nil { return err } if err := h.service.Delete(c.Request().Context(), id); err != nil { return echo.NewHTTPError(http.StatusInternalServerError, err.Error()) } return c.NoContent(http.StatusNoContent) } // ListLogs godoc // @Summary List schedule logs // @Description List schedule execution logs for a bot // @Tags schedule // @Param bot_id path string true "Bot ID" // @Param before query string false "Before timestamp (RFC3339)" // @Param limit query int false "Limit" default(50) // @Success 200 {object} schedule.ListLogsResponse // @Failure 400 {object} ErrorResponse // @Failure 500 {object} ErrorResponse // @Router /bots/{bot_id}/schedule/logs [get]. func (h *ScheduleHandler) ListLogs(c echo.Context) error { userID, err := h.requireUserID(c) if err != nil { return err } botID := strings.TrimSpace(c.Param("bot_id")) if botID == "" { return echo.NewHTTPError(http.StatusBadRequest, "bot id is required") } if _, err := h.authorizeBotAccess(c.Request().Context(), userID, botID); err != nil { return err } before, limit := parseBeforeLimit(c) items, err := h.service.ListLogs(c.Request().Context(), botID, before, limit) if err != nil { return echo.NewHTTPError(http.StatusInternalServerError, err.Error()) } return c.JSON(http.StatusOK, schedule.ListLogsResponse{Items: items}) } // ListLogsBySchedule godoc // @Summary List schedule logs by schedule // @Description List execution logs for a specific schedule // @Tags schedule // @Param bot_id path string true "Bot ID" // @Param id path string true "Schedule ID" // @Param before query string false "Before timestamp (RFC3339)" // @Param limit query int false "Limit" default(50) // @Success 200 {object} schedule.ListLogsResponse // @Failure 400 {object} ErrorResponse // @Failure 500 {object} ErrorResponse // @Router /bots/{bot_id}/schedule/{id}/logs [get]. func (h *ScheduleHandler) ListLogsBySchedule(c echo.Context) error { userID, err := h.requireUserID(c) if err != nil { return err } botID := strings.TrimSpace(c.Param("bot_id")) if botID == "" { return echo.NewHTTPError(http.StatusBadRequest, "bot id is required") } if _, err := h.authorizeBotAccess(c.Request().Context(), userID, botID); err != nil { return err } scheduleID := strings.TrimSpace(c.Param("id")) if scheduleID == "" { return echo.NewHTTPError(http.StatusBadRequest, "schedule id is required") } before, limit := parseBeforeLimit(c) items, err := h.service.ListLogsBySchedule(c.Request().Context(), scheduleID, before, limit) if err != nil { return echo.NewHTTPError(http.StatusInternalServerError, err.Error()) } return c.JSON(http.StatusOK, schedule.ListLogsResponse{Items: items}) } // DeleteLogs godoc // @Summary Delete schedule logs // @Description Delete all schedule execution logs for a bot // @Tags schedule // @Param bot_id path string true "Bot ID" // @Success 204 "No Content" // @Failure 400 {object} ErrorResponse // @Failure 500 {object} ErrorResponse // @Router /bots/{bot_id}/schedule/logs [delete]. func (h *ScheduleHandler) DeleteLogs(c echo.Context) error { userID, err := h.requireUserID(c) if err != nil { return err } botID := strings.TrimSpace(c.Param("bot_id")) if botID == "" { return echo.NewHTTPError(http.StatusBadRequest, "bot id is required") } if _, err := h.authorizeBotAccess(c.Request().Context(), userID, botID); err != nil { return err } if err := h.service.DeleteLogs(c.Request().Context(), botID); err != nil { return echo.NewHTTPError(http.StatusInternalServerError, err.Error()) } return c.NoContent(http.StatusNoContent) } func parseBeforeLimit(c echo.Context) (*time.Time, int) { var before *time.Time if raw := strings.TrimSpace(c.QueryParam("before")); raw != "" { if t, err := time.Parse(time.RFC3339Nano, raw); err == nil { before = &t } } limit := 50 if raw := strings.TrimSpace(c.QueryParam("limit")); raw != "" { if v, err := strconv.Atoi(raw); err == nil && v > 0 { limit = v } } return before, limit } func (*ScheduleHandler) requireUserID(c echo.Context) (string, error) { return RequireChannelIdentityID(c) } func (h *ScheduleHandler) authorizeBotAccess(ctx context.Context, userID, botID string) (bots.Bot, error) { return AuthorizeBotAccess(ctx, h.botService, h.accountService, userID, botID) }