From 8ad61e43a294e2f07e40d9319c1ff669cc4a83f0 Mon Sep 17 00:00:00 2001 From: Rhys Lloyd Date: Thu, 7 Aug 2025 16:14:34 -0700 Subject: [PATCH 1/4] openapi: add thumbnail endpoint --- openapi.yaml | 30 ++++++++++++++++++++++++++++++ 1 file changed, 30 insertions(+) diff --git a/openapi.yaml b/openapi.yaml index d233778..4a53fbd 100644 --- a/openapi.yaml +++ b/openapi.yaml @@ -186,6 +186,36 @@ paths: application/json: schema: $ref: "#/components/schemas/Error" + /maps/{MapID}/thumbnail: + patch: + summary: Set the map thumbnail + operationId: setMapThumbnail + tags: + - Maps + parameters: + - name: MapID + in: path + required: true + schema: + type: integer + format: int64 + minimum: 0 + - name: AssetID + in: query + required: true + schema: + type: integer + format: int64 + minimum: 0 + responses: + "200": + description: Successful response + default: + description: General Error + content: + application/json: + schema: + $ref: "#/components/schemas/Error" /mapfixes: get: summary: Get list of mapfixes -- 2.49.1 From ad99ac719f89aabdb12b896f9de4c5ec102c359f Mon Sep 17 00:00:00 2001 From: Rhys Lloyd Date: Thu, 7 Aug 2025 16:14:47 -0700 Subject: [PATCH 2/4] openapi: generate --- pkg/api/oas_client_gen.go | 148 ++++++++++++++++++++ pkg/api/oas_handlers_gen.go | 199 +++++++++++++++++++++++++++ pkg/api/oas_operations_gen.go | 1 + pkg/api/oas_parameters_gen.go | 144 +++++++++++++++++++ pkg/api/oas_response_decoders_gen.go | 60 ++++++++ pkg/api/oas_response_encoders_gen.go | 7 + pkg/api/oas_router_gen.go | 124 +++++++++++++---- pkg/api/oas_schemas_gen.go | 3 + pkg/api/oas_security_gen.go | 1 + pkg/api/oas_server_gen.go | 6 + pkg/api/oas_unimplemented_gen.go | 9 ++ 11 files changed, 677 insertions(+), 25 deletions(-) diff --git a/pkg/api/oas_client_gen.go b/pkg/api/oas_client_gen.go index bc9a36b..cec3e29 100644 --- a/pkg/api/oas_client_gen.go +++ b/pkg/api/oas_client_gen.go @@ -325,6 +325,12 @@ type Invoker interface { // // GET /session/validate SessionValidate(ctx context.Context) (bool, error) + // SetMapThumbnail invokes setMapThumbnail operation. + // + // Set the map thumbnail. + // + // PATCH /maps/{MapID}/thumbnail + SetMapThumbnail(ctx context.Context, params SetMapThumbnailParams) error // SetMapfixCompleted invokes setMapfixCompleted operation. // // Called by maptest when a player completes the map. @@ -6544,6 +6550,148 @@ func (c *Client) sendSessionValidate(ctx context.Context) (res bool, err error) return result, nil } +// SetMapThumbnail invokes setMapThumbnail operation. +// +// Set the map thumbnail. +// +// PATCH /maps/{MapID}/thumbnail +func (c *Client) SetMapThumbnail(ctx context.Context, params SetMapThumbnailParams) error { + _, err := c.sendSetMapThumbnail(ctx, params) + return err +} + +func (c *Client) sendSetMapThumbnail(ctx context.Context, params SetMapThumbnailParams) (res *SetMapThumbnailOK, err error) { + otelAttrs := []attribute.KeyValue{ + otelogen.OperationID("setMapThumbnail"), + semconv.HTTPRequestMethodKey.String("PATCH"), + semconv.HTTPRouteKey.String("/maps/{MapID}/thumbnail"), + } + + // Run stopwatch. + startTime := time.Now() + defer func() { + // Use floating point division here for higher precision (instead of Millisecond method). + elapsedDuration := time.Since(startTime) + c.duration.Record(ctx, float64(elapsedDuration)/float64(time.Millisecond), metric.WithAttributes(otelAttrs...)) + }() + + // Increment request counter. + c.requests.Add(ctx, 1, metric.WithAttributes(otelAttrs...)) + + // Start a span for this request. + ctx, span := c.cfg.Tracer.Start(ctx, SetMapThumbnailOperation, + trace.WithAttributes(otelAttrs...), + clientSpanKind, + ) + // Track stage for error reporting. + var stage string + defer func() { + if err != nil { + span.RecordError(err) + span.SetStatus(codes.Error, stage) + c.errors.Add(ctx, 1, metric.WithAttributes(otelAttrs...)) + } + span.End() + }() + + stage = "BuildURL" + u := uri.Clone(c.requestURL(ctx)) + var pathParts [3]string + pathParts[0] = "/maps/" + { + // Encode "MapID" parameter. + e := uri.NewPathEncoder(uri.PathEncoderConfig{ + Param: "MapID", + Style: uri.PathStyleSimple, + Explode: false, + }) + if err := func() error { + return e.EncodeValue(conv.Int64ToString(params.MapID)) + }(); err != nil { + return res, errors.Wrap(err, "encode path") + } + encoded, err := e.Result() + if err != nil { + return res, errors.Wrap(err, "encode path") + } + pathParts[1] = encoded + } + pathParts[2] = "/thumbnail" + uri.AddPathParts(u, pathParts[:]...) + + stage = "EncodeQueryParams" + q := uri.NewQueryEncoder() + { + // Encode "AssetID" parameter. + cfg := uri.QueryParameterEncodingConfig{ + Name: "AssetID", + Style: uri.QueryStyleForm, + Explode: true, + } + + if err := q.EncodeParam(cfg, func(e uri.Encoder) error { + return e.EncodeValue(conv.Int64ToString(params.AssetID)) + }); err != nil { + return res, errors.Wrap(err, "encode query") + } + } + u.RawQuery = q.Values().Encode() + + stage = "EncodeRequest" + r, err := ht.NewRequest(ctx, "PATCH", u) + if err != nil { + return res, errors.Wrap(err, "create request") + } + + { + type bitset = [1]uint8 + var satisfied bitset + { + stage = "Security:CookieAuth" + switch err := c.securityCookieAuth(ctx, SetMapThumbnailOperation, r); { + case err == nil: // if NO error + satisfied[0] |= 1 << 0 + case errors.Is(err, ogenerrors.ErrSkipClientSecurity): + // Skip this security. + default: + return res, errors.Wrap(err, "security \"CookieAuth\"") + } + } + + if ok := func() bool { + nextRequirement: + for _, requirement := range []bitset{ + {0b00000001}, + } { + for i, mask := range requirement { + if satisfied[i]&mask != mask { + continue nextRequirement + } + } + return true + } + return false + }(); !ok { + return res, ogenerrors.ErrSecurityRequirementIsNotSatisfied + } + } + + stage = "SendRequest" + resp, err := c.cfg.Client.Do(r) + if err != nil { + return res, errors.Wrap(err, "do request") + } + defer resp.Body.Close() + + stage = "DecodeResponse" + result, err := decodeSetMapThumbnailResponse(resp) + if err != nil { + return res, errors.Wrap(err, "decode response") + } + + return result, nil +} + // SetMapfixCompleted invokes setMapfixCompleted operation. // // Called by maptest when a player completes the map. diff --git a/pkg/api/oas_handlers_gen.go b/pkg/api/oas_handlers_gen.go index 197e414..d084066 100644 --- a/pkg/api/oas_handlers_gen.go +++ b/pkg/api/oas_handlers_gen.go @@ -9168,6 +9168,205 @@ func (s *Server) handleSessionValidateRequest(args [0]string, argsEscaped bool, } } +// handleSetMapThumbnailRequest handles setMapThumbnail operation. +// +// Set the map thumbnail. +// +// PATCH /maps/{MapID}/thumbnail +func (s *Server) handleSetMapThumbnailRequest(args [1]string, argsEscaped bool, w http.ResponseWriter, r *http.Request) { + statusWriter := &codeRecorder{ResponseWriter: w} + w = statusWriter + otelAttrs := []attribute.KeyValue{ + otelogen.OperationID("setMapThumbnail"), + semconv.HTTPRequestMethodKey.String("PATCH"), + semconv.HTTPRouteKey.String("/maps/{MapID}/thumbnail"), + } + + // Start a span for this request. + ctx, span := s.cfg.Tracer.Start(r.Context(), SetMapThumbnailOperation, + trace.WithAttributes(otelAttrs...), + serverSpanKind, + ) + defer span.End() + + // Add Labeler to context. + labeler := &Labeler{attrs: otelAttrs} + ctx = contextWithLabeler(ctx, labeler) + + // Run stopwatch. + startTime := time.Now() + defer func() { + elapsedDuration := time.Since(startTime) + + attrSet := labeler.AttributeSet() + attrs := attrSet.ToSlice() + code := statusWriter.status + if code != 0 { + codeAttr := semconv.HTTPResponseStatusCode(code) + attrs = append(attrs, codeAttr) + span.SetAttributes(codeAttr) + } + attrOpt := metric.WithAttributes(attrs...) + + // Increment request counter. + s.requests.Add(ctx, 1, attrOpt) + + // Use floating point division here for higher precision (instead of Millisecond method). + s.duration.Record(ctx, float64(elapsedDuration)/float64(time.Millisecond), attrOpt) + }() + + var ( + recordError = func(stage string, err error) { + span.RecordError(err) + + // https://opentelemetry.io/docs/specs/semconv/http/http-spans/#status + // Span Status MUST be left unset if HTTP status code was in the 1xx, 2xx or 3xx ranges, + // unless there was another error (e.g., network error receiving the response body; or 3xx codes with + // max redirects exceeded), in which case status MUST be set to Error. + code := statusWriter.status + if code >= 100 && code < 500 { + span.SetStatus(codes.Error, stage) + } + + attrSet := labeler.AttributeSet() + attrs := attrSet.ToSlice() + if code != 0 { + attrs = append(attrs, semconv.HTTPResponseStatusCode(code)) + } + + s.errors.Add(ctx, 1, metric.WithAttributes(attrs...)) + } + err error + opErrContext = ogenerrors.OperationContext{ + Name: SetMapThumbnailOperation, + ID: "setMapThumbnail", + } + ) + { + type bitset = [1]uint8 + var satisfied bitset + { + sctx, ok, err := s.securityCookieAuth(ctx, SetMapThumbnailOperation, r) + if err != nil { + err = &ogenerrors.SecurityError{ + OperationContext: opErrContext, + Security: "CookieAuth", + Err: err, + } + if encodeErr := encodeErrorResponse(s.h.NewError(ctx, err), w, span); encodeErr != nil { + defer recordError("Security:CookieAuth", err) + } + return + } + if ok { + satisfied[0] |= 1 << 0 + ctx = sctx + } + } + + if ok := func() bool { + nextRequirement: + for _, requirement := range []bitset{ + {0b00000001}, + } { + for i, mask := range requirement { + if satisfied[i]&mask != mask { + continue nextRequirement + } + } + return true + } + return false + }(); !ok { + err = &ogenerrors.SecurityError{ + OperationContext: opErrContext, + Err: ogenerrors.ErrSecurityRequirementIsNotSatisfied, + } + if encodeErr := encodeErrorResponse(s.h.NewError(ctx, err), w, span); encodeErr != nil { + defer recordError("Security", err) + } + return + } + } + params, err := decodeSetMapThumbnailParams(args, argsEscaped, r) + if err != nil { + err = &ogenerrors.DecodeParamsError{ + OperationContext: opErrContext, + Err: err, + } + defer recordError("DecodeParams", err) + s.cfg.ErrorHandler(ctx, w, r, err) + return + } + + var response *SetMapThumbnailOK + if m := s.cfg.Middleware; m != nil { + mreq := middleware.Request{ + Context: ctx, + OperationName: SetMapThumbnailOperation, + OperationSummary: "Set the map thumbnail", + OperationID: "setMapThumbnail", + Body: nil, + Params: middleware.Parameters{ + { + Name: "MapID", + In: "path", + }: params.MapID, + { + Name: "AssetID", + In: "query", + }: params.AssetID, + }, + Raw: r, + } + + type ( + Request = struct{} + Params = SetMapThumbnailParams + Response = *SetMapThumbnailOK + ) + response, err = middleware.HookMiddleware[ + Request, + Params, + Response, + ]( + m, + mreq, + unpackSetMapThumbnailParams, + func(ctx context.Context, request Request, params Params) (response Response, err error) { + err = s.h.SetMapThumbnail(ctx, params) + return response, err + }, + ) + } else { + err = s.h.SetMapThumbnail(ctx, params) + } + if err != nil { + if errRes, ok := errors.Into[*ErrorStatusCode](err); ok { + if err := encodeErrorResponse(errRes, w, span); err != nil { + defer recordError("Internal", err) + } + return + } + if errors.Is(err, ht.ErrNotImplemented) { + s.cfg.ErrorHandler(ctx, w, r, err) + return + } + if err := encodeErrorResponse(s.h.NewError(ctx, err), w, span); err != nil { + defer recordError("Internal", err) + } + return + } + + if err := encodeSetMapThumbnailResponse(response, w, span); err != nil { + defer recordError("EncodeResponse", err) + if !errors.Is(err, ht.ErrInternalServerErrorResponse) { + s.cfg.ErrorHandler(ctx, w, r, err) + } + return + } +} + // handleSetMapfixCompletedRequest handles setMapfixCompleted operation. // // Called by maptest when a player completes the map. diff --git a/pkg/api/oas_operations_gen.go b/pkg/api/oas_operations_gen.go index b5a33a8..fc6a4cb 100644 --- a/pkg/api/oas_operations_gen.go +++ b/pkg/api/oas_operations_gen.go @@ -55,6 +55,7 @@ const ( SessionRolesOperation OperationName = "SessionRoles" SessionUserOperation OperationName = "SessionUser" SessionValidateOperation OperationName = "SessionValidate" + SetMapThumbnailOperation OperationName = "SetMapThumbnail" SetMapfixCompletedOperation OperationName = "SetMapfixCompleted" SetSubmissionCompletedOperation OperationName = "SetSubmissionCompleted" UpdateMapfixModelOperation OperationName = "UpdateMapfixModel" diff --git a/pkg/api/oas_parameters_gen.go b/pkg/api/oas_parameters_gen.go index ac8c0f6..f1716e7 100644 --- a/pkg/api/oas_parameters_gen.go +++ b/pkg/api/oas_parameters_gen.go @@ -5950,6 +5950,150 @@ func decodeListSubmissionsParams(args [0]string, argsEscaped bool, r *http.Reque return params, nil } +// SetMapThumbnailParams is parameters of setMapThumbnail operation. +type SetMapThumbnailParams struct { + MapID int64 + AssetID int64 +} + +func unpackSetMapThumbnailParams(packed middleware.Parameters) (params SetMapThumbnailParams) { + { + key := middleware.ParameterKey{ + Name: "MapID", + In: "path", + } + params.MapID = packed[key].(int64) + } + { + key := middleware.ParameterKey{ + Name: "AssetID", + In: "query", + } + params.AssetID = packed[key].(int64) + } + return params +} + +func decodeSetMapThumbnailParams(args [1]string, argsEscaped bool, r *http.Request) (params SetMapThumbnailParams, _ error) { + q := uri.NewQueryDecoder(r.URL.Query()) + // Decode path: MapID. + if err := func() error { + param := args[0] + if argsEscaped { + unescaped, err := url.PathUnescape(args[0]) + if err != nil { + return errors.Wrap(err, "unescape path") + } + param = unescaped + } + if len(param) > 0 { + d := uri.NewPathDecoder(uri.PathDecoderConfig{ + Param: "MapID", + Value: param, + Style: uri.PathStyleSimple, + Explode: false, + }) + + if err := func() error { + val, err := d.DecodeValue() + if err != nil { + return err + } + + c, err := conv.ToInt64(val) + if err != nil { + return err + } + + params.MapID = c + return nil + }(); err != nil { + return err + } + if err := func() error { + if err := (validate.Int{ + MinSet: true, + Min: 0, + MaxSet: false, + Max: 0, + MinExclusive: false, + MaxExclusive: false, + MultipleOfSet: false, + MultipleOf: 0, + }).Validate(int64(params.MapID)); err != nil { + return errors.Wrap(err, "int") + } + return nil + }(); err != nil { + return err + } + } else { + return validate.ErrFieldRequired + } + return nil + }(); err != nil { + return params, &ogenerrors.DecodeParamError{ + Name: "MapID", + In: "path", + Err: err, + } + } + // Decode query: AssetID. + if err := func() error { + cfg := uri.QueryParameterDecodingConfig{ + Name: "AssetID", + Style: uri.QueryStyleForm, + Explode: true, + } + + if err := q.HasParam(cfg); err == nil { + if err := q.DecodeParam(cfg, func(d uri.Decoder) error { + val, err := d.DecodeValue() + if err != nil { + return err + } + + c, err := conv.ToInt64(val) + if err != nil { + return err + } + + params.AssetID = c + return nil + }); err != nil { + return err + } + if err := func() error { + if err := (validate.Int{ + MinSet: true, + Min: 0, + MaxSet: false, + Max: 0, + MinExclusive: false, + MaxExclusive: false, + MultipleOfSet: false, + MultipleOf: 0, + }).Validate(int64(params.AssetID)); err != nil { + return errors.Wrap(err, "int") + } + return nil + }(); err != nil { + return err + } + } else { + return err + } + return nil + }(); err != nil { + return params, &ogenerrors.DecodeParamError{ + Name: "AssetID", + In: "query", + Err: err, + } + } + return params, nil +} + // SetMapfixCompletedParams is parameters of setMapfixCompleted operation. type SetMapfixCompletedParams struct { // The unique identifier for a mapfix. diff --git a/pkg/api/oas_response_decoders_gen.go b/pkg/api/oas_response_decoders_gen.go index 958d0d9..08c6e78 100644 --- a/pkg/api/oas_response_decoders_gen.go +++ b/pkg/api/oas_response_decoders_gen.go @@ -3951,6 +3951,66 @@ func decodeSessionValidateResponse(resp *http.Response) (res bool, _ error) { return res, errors.Wrap(defRes, "error") } +func decodeSetMapThumbnailResponse(resp *http.Response) (res *SetMapThumbnailOK, _ error) { + switch resp.StatusCode { + case 200: + // Code 200. + return &SetMapThumbnailOK{}, nil + } + // Convenient error response. + defRes, err := func() (res *ErrorStatusCode, err error) { + ct, _, err := mime.ParseMediaType(resp.Header.Get("Content-Type")) + if err != nil { + return res, errors.Wrap(err, "parse media type") + } + switch { + case ct == "application/json": + buf, err := io.ReadAll(resp.Body) + if err != nil { + return res, err + } + d := jx.DecodeBytes(buf) + + var response Error + if err := func() error { + if err := response.Decode(d); err != nil { + return err + } + if err := d.Skip(); err != io.EOF { + return errors.New("unexpected trailing data") + } + return nil + }(); err != nil { + err = &ogenerrors.DecodeBodyError{ + ContentType: ct, + Body: buf, + Err: err, + } + return res, err + } + // Validate response. + if err := func() error { + if err := response.Validate(); err != nil { + return err + } + return nil + }(); err != nil { + return res, errors.Wrap(err, "validate") + } + return &ErrorStatusCode{ + StatusCode: resp.StatusCode, + Response: response, + }, nil + default: + return res, validate.InvalidContentType(ct) + } + }() + if err != nil { + return res, errors.Wrapf(err, "default (code %d)", resp.StatusCode) + } + return res, errors.Wrap(defRes, "error") +} + func decodeSetMapfixCompletedResponse(resp *http.Response) (res *SetMapfixCompletedNoContent, _ error) { switch resp.StatusCode { case 204: diff --git a/pkg/api/oas_response_encoders_gen.go b/pkg/api/oas_response_encoders_gen.go index fb923c6..5df4e3e 100644 --- a/pkg/api/oas_response_encoders_gen.go +++ b/pkg/api/oas_response_encoders_gen.go @@ -533,6 +533,13 @@ func encodeSessionValidateResponse(response bool, w http.ResponseWriter, span tr return nil } +func encodeSetMapThumbnailResponse(response *SetMapThumbnailOK, w http.ResponseWriter, span trace.Span) error { + w.WriteHeader(200) + span.SetStatus(codes.Ok, http.StatusText(200)) + + return nil +} + func encodeSetMapfixCompletedResponse(response *SetMapfixCompletedNoContent, w http.ResponseWriter, span trace.Span) error { w.WriteHeader(204) span.SetStatus(codes.Ok, http.StatusText(204)) diff --git a/pkg/api/oas_router_gen.go b/pkg/api/oas_router_gen.go index 1dc226d..8ef9e5e 100644 --- a/pkg/api/oas_router_gen.go +++ b/pkg/api/oas_router_gen.go @@ -592,26 +592,62 @@ func (s *Server) ServeHTTP(w http.ResponseWriter, r *http.Request) { return } switch elem[0] { - case '/': // Prefix: "/download" + case '/': // Prefix: "/" - if l := len("/download"); len(elem) >= l && elem[0:l] == "/download" { + if l := len("/"); len(elem) >= l && elem[0:l] == "/" { elem = elem[l:] } else { break } if len(elem) == 0 { - // Leaf node. - switch r.Method { - case "GET": - s.handleDownloadMapAssetRequest([1]string{ - args[0], - }, elemIsEscaped, w, r) - default: - s.notAllowed(w, r, "GET") + break + } + switch elem[0] { + case 'd': // Prefix: "download" + + if l := len("download"); len(elem) >= l && elem[0:l] == "download" { + elem = elem[l:] + } else { + break + } + + if len(elem) == 0 { + // Leaf node. + switch r.Method { + case "GET": + s.handleDownloadMapAssetRequest([1]string{ + args[0], + }, elemIsEscaped, w, r) + default: + s.notAllowed(w, r, "GET") + } + + return + } + + case 't': // Prefix: "thumbnail" + + if l := len("thumbnail"); len(elem) >= l && elem[0:l] == "thumbnail" { + elem = elem[l:] + } else { + break + } + + if len(elem) == 0 { + // Leaf node. + switch r.Method { + case "PATCH": + s.handleSetMapThumbnailRequest([1]string{ + args[0], + }, elemIsEscaped, w, r) + default: + s.notAllowed(w, r, "PATCH") + } + + return } - return } } @@ -2060,28 +2096,66 @@ func (s *Server) FindPath(method string, u *url.URL) (r Route, _ bool) { } } switch elem[0] { - case '/': // Prefix: "/download" + case '/': // Prefix: "/" - if l := len("/download"); len(elem) >= l && elem[0:l] == "/download" { + if l := len("/"); len(elem) >= l && elem[0:l] == "/" { elem = elem[l:] } else { break } if len(elem) == 0 { - // Leaf node. - switch method { - case "GET": - r.name = DownloadMapAssetOperation - r.summary = "Download the map asset" - r.operationID = "downloadMapAsset" - r.pathPattern = "/maps/{MapID}/download" - r.args = args - r.count = 1 - return r, true - default: - return + break + } + switch elem[0] { + case 'd': // Prefix: "download" + + if l := len("download"); len(elem) >= l && elem[0:l] == "download" { + elem = elem[l:] + } else { + break } + + if len(elem) == 0 { + // Leaf node. + switch method { + case "GET": + r.name = DownloadMapAssetOperation + r.summary = "Download the map asset" + r.operationID = "downloadMapAsset" + r.pathPattern = "/maps/{MapID}/download" + r.args = args + r.count = 1 + return r, true + default: + return + } + } + + case 't': // Prefix: "thumbnail" + + if l := len("thumbnail"); len(elem) >= l && elem[0:l] == "thumbnail" { + elem = elem[l:] + } else { + break + } + + if len(elem) == 0 { + // Leaf node. + switch method { + case "PATCH": + r.name = SetMapThumbnailOperation + r.summary = "Set the map thumbnail" + r.operationID = "setMapThumbnail" + r.pathPattern = "/maps/{MapID}/thumbnail" + r.args = args + r.count = 1 + return r, true + default: + return + } + } + } } diff --git a/pkg/api/oas_schemas_gen.go b/pkg/api/oas_schemas_gen.go index b65b80d..d6994bd 100644 --- a/pkg/api/oas_schemas_gen.go +++ b/pkg/api/oas_schemas_gen.go @@ -1271,6 +1271,9 @@ func (s *ScriptUpdate) SetResourceID(val OptInt64) { s.ResourceID = val } +// SetMapThumbnailOK is response for SetMapThumbnail operation. +type SetMapThumbnailOK struct{} + // SetMapfixCompletedNoContent is response for SetMapfixCompleted operation. type SetMapfixCompletedNoContent struct{} diff --git a/pkg/api/oas_security_gen.go b/pkg/api/oas_security_gen.go index 100c173..22bf7ce 100644 --- a/pkg/api/oas_security_gen.go +++ b/pkg/api/oas_security_gen.go @@ -71,6 +71,7 @@ var operationRolesCookieAuth = map[string][]string{ SessionRolesOperation: []string{}, SessionUserOperation: []string{}, SessionValidateOperation: []string{}, + SetMapThumbnailOperation: []string{}, SetMapfixCompletedOperation: []string{}, SetSubmissionCompletedOperation: []string{}, UpdateMapfixModelOperation: []string{}, diff --git a/pkg/api/oas_server_gen.go b/pkg/api/oas_server_gen.go index bc8d72e..c04217e 100644 --- a/pkg/api/oas_server_gen.go +++ b/pkg/api/oas_server_gen.go @@ -304,6 +304,12 @@ type Handler interface { // // GET /session/validate SessionValidate(ctx context.Context) (bool, error) + // SetMapThumbnail implements setMapThumbnail operation. + // + // Set the map thumbnail. + // + // PATCH /maps/{MapID}/thumbnail + SetMapThumbnail(ctx context.Context, params SetMapThumbnailParams) error // SetMapfixCompleted implements setMapfixCompleted operation. // // Called by maptest when a player completes the map. diff --git a/pkg/api/oas_unimplemented_gen.go b/pkg/api/oas_unimplemented_gen.go index 2c74330..3b04029 100644 --- a/pkg/api/oas_unimplemented_gen.go +++ b/pkg/api/oas_unimplemented_gen.go @@ -456,6 +456,15 @@ func (UnimplementedHandler) SessionValidate(ctx context.Context) (r bool, _ erro return r, ht.ErrNotImplemented } +// SetMapThumbnail implements setMapThumbnail operation. +// +// Set the map thumbnail. +// +// PATCH /maps/{MapID}/thumbnail +func (UnimplementedHandler) SetMapThumbnail(ctx context.Context, params SetMapThumbnailParams) error { + return ht.ErrNotImplemented +} + // SetMapfixCompleted implements setMapfixCompleted operation. // // Called by maptest when a player completes the map. -- 2.49.1 From 5c5539ddc4ee4dfd520192edd4e2990ff8d242ad Mon Sep 17 00:00:00 2001 From: Rhys Lloyd Date: Thu, 7 Aug 2025 16:20:18 -0700 Subject: [PATCH 3/4] submissions: map thumbnail role --- pkg/model/roles.go | 1 + 1 file changed, 1 insertion(+) diff --git a/pkg/model/roles.go b/pkg/model/roles.go index f22bb52..5e9de51 100644 --- a/pkg/model/roles.go +++ b/pkg/model/roles.go @@ -3,6 +3,7 @@ package model // Submissions roles bitflag type Roles int32 var ( + RolesMapThumbnail Roles = 1<<7 RolesSubmissionUpload Roles = 1<<6 RolesSubmissionReview Roles = 1<<5 RolesSubmissionRelease Roles = 1<<4 -- 2.49.1 From c7b83071b0cdeb539843051239aa4fe81a191b2c Mon Sep 17 00:00:00 2001 From: Rhys Lloyd Date: Thu, 7 Aug 2025 16:20:26 -0700 Subject: [PATCH 4/4] submissions: SetMapThumbnail endpoint --- pkg/web_api/maps.go | 31 +++++++++++++++++++++++++++++++ 1 file changed, 31 insertions(+) diff --git a/pkg/web_api/maps.go b/pkg/web_api/maps.go index 97574b1..7d4f8ff 100644 --- a/pkg/web_api/maps.go +++ b/pkg/web_api/maps.go @@ -129,3 +129,34 @@ func (svc *Service) DownloadMapAsset(ctx context.Context, params api.DownloadMap ok.Data = asset return ok, nil } + +// DownloadMapAsset invokes downloadMapAsset operation. +// +// Set the map thumbnail +// +// POST /maps/{MapID}/thumbnail +func (svc *Service) SetMapThumbnail(ctx context.Context, params api.SetMapThumbnailParams) error { + userInfo, success := ctx.Value("UserInfo").(UserInfoHandle) + if !success { + return ErrUserInfo + } + + has_role, err := userInfo.HasRoles(model.RolesMapThumbnail) + if err != nil { + return err + } + + if !has_role { + return ErrPermissionDenied + } + + map_update := service.NewMapUpdate() + map_update.SetThumbnail(uint64(params.AssetID)) + + err = svc.inner.UpdateMap(ctx, params.MapID, map_update) + if err != nil { + return err + } + + return nil +} -- 2.49.1