package raffle import ( "encoding/json" "fmt" "net/http" "strconv" "time" "github.com/gofrs/uuid" "github.com/gorilla/mux" "github.com/pkg/errors" "github.com/sirupsen/logrus" "github.com/Luzifer/twitch-bot/v3/plugins" ) var apiRoutes = []plugins.HTTPRouteRegistrationArgs{ { Description: "Lists all raffles known to the bot", HandlerFunc: handleWrap(func(w http.ResponseWriter, r *http.Request, _ map[string]uint64) (any, error) { ras, err := dbc.List() return ras, errors.Wrap(err, "fetching raffles from database") }, nil), Method: http.MethodGet, Module: actorName, Name: "List Raffles", Path: "/", RequiresWriteAuth: true, ResponseType: plugins.HTTPRouteResponseTypeJSON, }, { Description: "Creates a new raffle based on the data in the body", HandlerFunc: handleWrap(func(w http.ResponseWriter, r *http.Request, _ map[string]uint64) (any, error) { var ra raffle if err := json.NewDecoder(r.Body).Decode(&ra); err != nil { return nil, errors.Wrap(err, "parsing raffle from body") } return nil, errors.Wrap(dbc.Create(ra), "creating raffle") }, nil), Method: http.MethodPost, Module: actorName, Name: "Create Raffle", Path: "/", RequiresWriteAuth: true, ResponseType: plugins.HTTPRouteResponseTypeNo200, }, { Description: "Deletes raffle by given ID including all entries", HandlerFunc: handleWrap(func(w http.ResponseWriter, r *http.Request, ids map[string]uint64) (any, error) { return nil, errors.Wrap(dbc.Delete(ids["id"]), "fetching raffle from database") }, []string{"id"}), Method: http.MethodDelete, Module: actorName, Name: "Delete Raffle", Path: "/{id}", RequiresWriteAuth: true, ResponseType: plugins.HTTPRouteResponseTypeNo200, RouteParams: []plugins.HTTPRouteParamDocumentation{ { Description: "ID of the raffle to fetch", Name: "id", }, }, }, { Description: "Gets raffle by given ID including all entries", HandlerFunc: handleWrap(func(w http.ResponseWriter, r *http.Request, ids map[string]uint64) (any, error) { ra, err := dbc.Get(ids["id"]) return ra, errors.Wrap(err, "fetching raffle from database") }, []string{"id"}), Method: http.MethodGet, Module: actorName, Name: "Get Raffle", Path: "/{id}", RequiresWriteAuth: true, ResponseType: plugins.HTTPRouteResponseTypeJSON, RouteParams: []plugins.HTTPRouteParamDocumentation{ { Description: "ID of the raffle to fetch", Name: "id", }, }, }, { Description: "Updates the given raffle (needs to include the whole object, not just changed fields)", HandlerFunc: handleWrap(func(w http.ResponseWriter, r *http.Request, ids map[string]uint64) (any, error) { var ra raffle if err := json.NewDecoder(r.Body).Decode(&ra); err != nil { return nil, errors.Wrap(err, "parsing raffle from body") } if ra.ID != ids["id"] { return nil, errors.New("raffle ID does not match") } return nil, errors.Wrap(dbc.Update(ra), "updating raffle") }, []string{"id"}), Method: http.MethodPut, Module: actorName, Name: "Update Raffle", Path: "/{id}", RequiresWriteAuth: true, ResponseType: plugins.HTTPRouteResponseTypeNo200, RouteParams: []plugins.HTTPRouteParamDocumentation{ { Description: "ID of the raffle to update", Name: "id", }, }, }, { Description: "Duplicates the raffle given by its ID", HandlerFunc: handleWrap(func(w http.ResponseWriter, r *http.Request, ids map[string]uint64) (any, error) { return nil, errors.Wrap(dbc.Clone(ids["id"]), "cloning raffle") }, []string{"id"}), Method: http.MethodPut, Module: actorName, Name: "Clone Raffle", Path: "/{id}/clone", RequiresWriteAuth: true, ResponseType: plugins.HTTPRouteResponseTypeNo200, RouteParams: []plugins.HTTPRouteParamDocumentation{ { Description: "ID of the raffle to clone", Name: "id", }, }, }, { Description: "Closes the raffle given by its ID", HandlerFunc: handleWrap(func(w http.ResponseWriter, r *http.Request, ids map[string]uint64) (any, error) { return nil, errors.Wrap(dbc.Close(ids["id"]), "closing raffle") }, []string{"id"}), Method: http.MethodPut, Module: actorName, Name: "Close Raffle", Path: "/{id}/close", RequiresWriteAuth: true, ResponseType: plugins.HTTPRouteResponseTypeNo200, RouteParams: []plugins.HTTPRouteParamDocumentation{ { Description: "ID of the raffle to close", Name: "id", }, }, }, { Description: "Picks a winner for the given raffle (this does NOT close the raffle, use only on closed raffle!)", HandlerFunc: handleWrap(func(w http.ResponseWriter, r *http.Request, ids map[string]uint64) (any, error) { return nil, errors.Wrap(dbc.PickWinner(ids["id"]), "picking winner") }, []string{"id"}), Method: http.MethodPut, Module: actorName, Name: "Pick Raffle Winner", Path: "/{id}/pick", RequiresWriteAuth: true, ResponseType: plugins.HTTPRouteResponseTypeNo200, RouteParams: []plugins.HTTPRouteParamDocumentation{ { Description: "ID of the raffle to pick a winner for", Name: "id", }, }, }, { Description: "Re-opens a raffle for additional entries, only Status and CloseAt are modified", HandlerFunc: handleWrap(func(w http.ResponseWriter, r *http.Request, ids map[string]uint64) (any, error) { dur, err := strconv.ParseInt(r.URL.Query().Get("duration"), 10, 64) if err != nil { return nil, errors.Wrap(err, "parsing duration") } return nil, errors.Wrap(dbc.Reopen(ids["id"], time.Duration(dur)*time.Second), "reopening raffle") }, []string{"id"}), Method: http.MethodPut, Module: actorName, Name: "Reopen Raffle", Path: "/{id}/reopen", QueryParams: []plugins.HTTPRouteParamDocumentation{ { Description: "Number of seconds to leave the raffle open", Name: "duration", Required: true, Type: "integer", }, }, RequiresWriteAuth: true, ResponseType: plugins.HTTPRouteResponseTypeNo200, RouteParams: []plugins.HTTPRouteParamDocumentation{ { Description: "ID of the raffle to pick a winner for", Name: "id", }, }, }, { Description: "Starts a raffle making it available for entries", HandlerFunc: handleWrap(func(w http.ResponseWriter, r *http.Request, ids map[string]uint64) (any, error) { return nil, errors.Wrap(dbc.Start(ids["id"]), "starting raffle") }, []string{"id"}), Method: http.MethodPut, Module: actorName, Name: "Start Raffle", Path: "/{id}/start", RequiresWriteAuth: true, ResponseType: plugins.HTTPRouteResponseTypeNo200, RouteParams: []plugins.HTTPRouteParamDocumentation{ { Description: "ID of the raffle to start", Name: "id", }, }, }, { Description: "Dismisses a previously picked winner and picks a new one", HandlerFunc: handleWrap(func(w http.ResponseWriter, r *http.Request, ids map[string]uint64) (any, error) { return nil, errors.Wrap(dbc.RedrawWinner(ids["id"], ids["winner"]), "re-picking winner") }, []string{"id", "winner"}), Method: http.MethodPut, Module: actorName, Name: "Re-Pick Raffle Winner", Path: "/{id}/repick/{winner}", RequiresWriteAuth: true, ResponseType: plugins.HTTPRouteResponseTypeNo200, RouteParams: []plugins.HTTPRouteParamDocumentation{ { Description: "ID of the raffle to re-pick the winner for", Name: "id", }, { Description: "ID of the winner to replace", Name: "winner", }, }, }, } func handleWrap(f func(http.ResponseWriter, *http.Request, map[string]uint64) (any, error), parseIDs []string) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { var ( ids = map[string]uint64{} reqUUID = uuid.Must(uuid.NewV4()).String() logger = logrus.WithFields(logrus.Fields{ "path": r.URL.Path, "req": reqUUID, }) ) for _, k := range parseIDs { id, err := strconv.ParseUint(mux.Vars(r)[k], 10, 64) if err != nil { http.Error(w, fmt.Sprintf("invalid ID field %q", k), http.StatusBadRequest) return } ids[k] = id } resp, err := f(w, r, ids) if err != nil { logger.WithError(err).Error("handling request") http.Error(w, "something went wrong", http.StatusInternalServerError) return } if resp == nil { w.WriteHeader(http.StatusNoContent) return } w.Header().Set("Content-Type", "applicatioh/json") if err = json.NewEncoder(w).Encode(resp); err != nil { logger.WithError(err).Error("serializing response") http.Error(w, "something went wrong", http.StatusInternalServerError) return } } } func registerAPI(args plugins.RegistrationArguments) (err error) { for i, r := range apiRoutes { if err = args.RegisterAPIRoute(r); err != nil { return errors.Wrapf(err, "registering route %d", i) } } return nil }