bot download endpoint

This commit is contained in:
2026-02-23 09:09:32 -08:00
parent 3396778882
commit b7c0f8b917
3 changed files with 185 additions and 5 deletions

View File

@@ -1,9 +1,10 @@
package dto
import (
"git.itzana.me/strafesnet/go-grpc/times"
"strconv"
"time"
"git.itzana.me/strafesnet/go-grpc/times"
)
type TimePlacement struct {
@@ -41,6 +42,16 @@ type TimeData struct {
GameID int32 `json:"game_id"`
} // @name Time
type BotDownloadUrl struct {
Url string `json:"url"`
} // @name BotDownloadUrl
type FileInfo struct {
ID string `json:"ID"`
Created int64 `json:"Created"`
Url string `json:"Url"`
} // @name FileInfo
// FromGRPC converts a TimeResponse protobuf message to a TimeData domain object
func (t *TimeData) FromGRPC(resp *times.TimeResponse) *TimeData {
if resp == nil {

View File

@@ -1,12 +1,14 @@
package handlers
import (
"encoding/json"
"fmt"
"math"
"net/http"
"strconv"
"strings"
"git.itzana.me/strafesnet/go-grpc/bots"
"git.itzana.me/strafesnet/go-grpc/times"
"git.itzana.me/strafesnet/public-api/pkg/api/dto"
"github.com/gin-gonic/gin"
@@ -18,16 +20,20 @@ import (
// TimesHandler handles HTTP requests related to times.
type TimesHandler struct {
*Handler
client *http.Client
url string
}
// NewTimesHandler creates a new TimesHandler with the provided options.
func NewTimesHandler(options ...HandlerOption) (*TimesHandler, error) {
func NewTimesHandler(http_client *http.Client, storage_url string, options ...HandlerOption) (*TimesHandler, error) {
baseHandler, err := NewHandler(options...)
if err != nil {
return nil, err
}
return &TimesHandler{
Handler: baseHandler,
client: http_client,
url: storage_url,
}, nil
}
@@ -318,3 +324,158 @@ func (h *TimesHandler) GetPlacements(ctx *gin.Context) {
Data: ranks,
})
}
// @Summary Get bot download url by time ID
// @Description Get a download url for the bot replay of a time by its ID if it exists
// @Tags times
// @Produce json
// @Security ApiKeyAuth
// @Param id path int true "Time ID"
// @Success 200 {object} dto.Response[dto.BotDownloadUrl]
// @Failure 404 {object} dto.Error "Time not found"
// @Failure 404 {object} dto.Error "Time does not have a Bot"
// @Failure 404 {object} dto.Error "Bot not found"
// @Failure default {object} dto.Error "General error response"
// @Router /time/{id} [get]
func (h *TimesHandler) GetDownloadUrl(ctx *gin.Context) {
// Extract time ID from path parameter
id := ctx.Param("id")
timeID, err := strconv.ParseInt(id, 10, 64)
if err != nil {
ctx.JSON(http.StatusBadRequest, dto.Error{
Error: "Invalid time ID format",
})
return
}
// Call the gRPC service
timeData, err := times.NewTimesServiceClient(h.dataClient).Get(ctx, &times.IdMessage{
ID: timeID,
})
if err != nil {
statusCode := http.StatusInternalServerError
errorMessage := "Failed to get time"
// Check if it's a "not found" error
if status.Code(err) == codes.NotFound {
statusCode = http.StatusNotFound
errorMessage = "Time not found"
}
ctx.JSON(statusCode, dto.Error{
Error: errorMessage,
})
log.WithError(err).Error(
"Failed to get time",
)
return
}
// check if bot exists
if timeData.Bot == nil {
statusCode := http.StatusNotFound
errorMessage := "Time does not have a Bot"
ctx.JSON(statusCode, dto.Error{
Error: errorMessage,
})
log.WithError(err).Error(
"Time does not have a Bot",
)
return
}
// Call the gRPC service
botData, err := bots.NewBotsServiceClient(h.dataClient).Get(ctx, &bots.IdMessage{
ID: timeData.Bot.ID,
})
if err != nil {
statusCode := http.StatusInternalServerError
errorMessage := "Failed to get bot"
// Check if it's a "not found" error
if status.Code(err) == codes.NotFound {
statusCode = http.StatusNotFound
errorMessage = "Bot not found"
}
ctx.JSON(statusCode, dto.Error{
Error: errorMessage,
})
log.WithError(err).Error(
"Failed to get bot",
)
return
}
// fetch download url from storage service
// Build the full URL.
fullURL := h.url + botData.FileID
// Create the request with the supplied context so callers can cancel it.
req, err := http.NewRequestWithContext(ctx, http.MethodGet, fullURL, nil)
if err != nil {
statusCode := http.StatusInternalServerError
errorMessage := "Error creating http request to storage"
ctx.JSON(statusCode, dto.Error{
Error: errorMessage,
})
log.WithError(err).Error(
"Error creating http request to storage",
)
return
}
// Send the request.
resp, err := h.client.Do(req)
if err != nil {
statusCode := http.StatusInternalServerError
errorMessage := "Storage http request failed"
ctx.JSON(statusCode, dto.Error{
Error: errorMessage,
})
log.WithError(err).Error(
"Storage http request failed",
)
return
}
defer resp.Body.Close()
// check status
if resp.StatusCode != 200 {
statusCode := http.StatusInternalServerError
errorMessage := "Unexpected status"
ctx.JSON(statusCode, dto.Error{
Error: errorMessage,
})
log.WithError(err).Error(
"Unexpected status",
)
return
}
// Decode the JSON body into the FileInfo struct.
var info dto.FileInfo
if err := json.NewDecoder(resp.Body).Decode(&info); err != nil {
statusCode := http.StatusInternalServerError
errorMessage := "Error decoding json"
ctx.JSON(statusCode, dto.Error{
Error: errorMessage,
})
log.WithError(err).Error(
"Error decoding json",
)
return
}
// Return the time data
ctx.JSON(http.StatusOK, dto.Response[dto.BotDownloadUrl]{
Data: dto.BotDownloadUrl{
Url: info.Url,
},
})
}

View File

@@ -4,6 +4,9 @@ import (
"context"
"errors"
"fmt"
"net/http"
"time"
"git.itzana.me/StrafesNET/dev-service/pkg/api/middleware"
"git.itzana.me/strafesnet/public-api/docs"
"git.itzana.me/strafesnet/public-api/pkg/api/handlers"
@@ -13,8 +16,6 @@ import (
ginSwagger "github.com/swaggo/gin-swagger"
"github.com/urfave/cli/v2"
"google.golang.org/grpc"
"net/http"
"time"
)
// Option defines a function that configures a Router
@@ -75,7 +76,7 @@ func setupRoutes(cfg *RouterConfig) (*gin.Engine, error) {
}
// Times handler
timesHandler, err := handlers.NewTimesHandler(handlerOptions...)
timesHandler, err := handlers.NewTimesHandler(HTTP_CLIENT, STORAGE_URL, handlerOptions...)
if err != nil {
return nil, err
}
@@ -121,7 +122,14 @@ func setupRoutes(cfg *RouterConfig) (*gin.Engine, error) {
// Rank
v1.GET("/rank", rankHandler.List)
}
v1_bots := r.Group("/api/v1")
{
// Auth middleware
v1_bots.Use(middleware.ValidateRequest("Storage", "Read", cfg.devClient))
v1_bots.GET("/time/:id/download-url", timesHandler.GetDownloadUrl)
}
r.GET("/docs/*any", ginSwagger.WrapHandler(swaggerfiles.Handler))
r.GET("/", func(ctx *gin.Context) {