Compare commits
40 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| ebbc4310a6 | |||
| d434c71821 | |||
|
9ca285bb86
|
|||
|
485860ef52
|
|||
|
fde19a2378
|
|||
|
04cfcc9ddc
|
|||
| 128aaf4c06 | |||
|
9df5e4a8dd
|
|||
|
46ebb5574e
|
|||
|
2ea3808f12
|
|||
|
d3771a874a
|
|||
|
cb86bafa7c
|
|||
|
b38ddd2fae
|
|||
|
c8d0502616
|
|||
|
069b1fd711
|
|||
|
21a764f298
|
|||
|
c341c881e3
|
|||
|
a10a18d0a9
|
|||
|
322951d28b
|
|||
|
30dee1ec2c
|
|||
|
fee1b968c5
|
|||
|
e9c999c7b5
|
|||
|
af1c35b618
|
|||
|
5a9bc0ea6c
|
|||
|
69344551b9
|
|||
|
1ab40fdc78
|
|||
|
c7c64cd8f7
|
|||
|
31802ac9fc
|
|||
|
79016134b6
|
|||
|
010494ed0e
|
|||
|
03695e773d
|
|||
|
33f55524a8
|
|||
|
b7c0f8b917
|
|||
|
3396778882
|
|||
|
f1743f4ed8
|
|||
| 64e8bfcbee | |||
| a7524c2766 | |||
| fabd0a6759 | |||
| b0fbb6f934 | |||
|
517a56ef13
|
@@ -61,4 +61,9 @@ steps:
|
|||||||
when:
|
when:
|
||||||
branch:
|
branch:
|
||||||
- master
|
- master
|
||||||
- staging
|
- staging
|
||||||
|
---
|
||||||
|
kind: signature
|
||||||
|
hmac: 7655eb6dead73d2ad977685120cee8562931036bb5d7fa59d30d5917840c4a22
|
||||||
|
|
||||||
|
...
|
||||||
|
|||||||
1
Makefile
1
Makefile
@@ -1,6 +1,5 @@
|
|||||||
clean:
|
clean:
|
||||||
rm -rf build
|
rm -rf build
|
||||||
rm -rf web/dist
|
|
||||||
|
|
||||||
test:
|
test:
|
||||||
go fmt ./...
|
go fmt ./...
|
||||||
|
|||||||
58
docs/docs.go
58
docs/docs.go
@@ -301,7 +301,8 @@ const docTemplate = `{
|
|||||||
{
|
{
|
||||||
"type": "array",
|
"type": "array",
|
||||||
"items": {
|
"items": {
|
||||||
"type": "integer"
|
"type": "integer",
|
||||||
|
"format": "int64"
|
||||||
},
|
},
|
||||||
"collectionFormat": "csv",
|
"collectionFormat": "csv",
|
||||||
"description": "Comma-separated array of time IDs (25 Limit)",
|
"description": "Comma-separated array of time IDs (25 Limit)",
|
||||||
@@ -453,6 +454,52 @@ const docTemplate = `{
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"/time/{id}/bot": {
|
||||||
|
"get": {
|
||||||
|
"security": [
|
||||||
|
{
|
||||||
|
"ApiKeyAuth": []
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"description": "Get a HTTP 302 Redirect to the download url for the bot replay of a time by its ID if it exists",
|
||||||
|
"tags": [
|
||||||
|
"times"
|
||||||
|
],
|
||||||
|
"summary": "Get redirect to bot download url by time ID",
|
||||||
|
"parameters": [
|
||||||
|
{
|
||||||
|
"type": "integer",
|
||||||
|
"description": "Time ID",
|
||||||
|
"name": "id",
|
||||||
|
"in": "path",
|
||||||
|
"required": true
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"responses": {
|
||||||
|
"302": {
|
||||||
|
"description": "Found",
|
||||||
|
"headers": {
|
||||||
|
"Location": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "Redirect URL"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"404": {
|
||||||
|
"description": "Time does not have a Bot",
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/definitions/Error"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"default": {
|
||||||
|
"description": "General error response",
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/definitions/Error"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"/user": {
|
"/user": {
|
||||||
"get": {
|
"get": {
|
||||||
"security": [
|
"security": [
|
||||||
@@ -631,12 +678,6 @@ const docTemplate = `{
|
|||||||
"Map": {
|
"Map": {
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"properties": {
|
"properties": {
|
||||||
"creator": {
|
|
||||||
"type": "string"
|
|
||||||
},
|
|
||||||
"date": {
|
|
||||||
"type": "string"
|
|
||||||
},
|
|
||||||
"display_name": {
|
"display_name": {
|
||||||
"type": "string"
|
"type": "string"
|
||||||
},
|
},
|
||||||
@@ -645,6 +686,9 @@ const docTemplate = `{
|
|||||||
},
|
},
|
||||||
"id": {
|
"id": {
|
||||||
"type": "integer"
|
"type": "integer"
|
||||||
|
},
|
||||||
|
"thumbnail": {
|
||||||
|
"type": "integer"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -294,7 +294,8 @@
|
|||||||
{
|
{
|
||||||
"type": "array",
|
"type": "array",
|
||||||
"items": {
|
"items": {
|
||||||
"type": "integer"
|
"type": "integer",
|
||||||
|
"format": "int64"
|
||||||
},
|
},
|
||||||
"collectionFormat": "csv",
|
"collectionFormat": "csv",
|
||||||
"description": "Comma-separated array of time IDs (25 Limit)",
|
"description": "Comma-separated array of time IDs (25 Limit)",
|
||||||
@@ -446,6 +447,52 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"/time/{id}/bot": {
|
||||||
|
"get": {
|
||||||
|
"security": [
|
||||||
|
{
|
||||||
|
"ApiKeyAuth": []
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"description": "Get a HTTP 302 Redirect to the download url for the bot replay of a time by its ID if it exists",
|
||||||
|
"tags": [
|
||||||
|
"times"
|
||||||
|
],
|
||||||
|
"summary": "Get redirect to bot download url by time ID",
|
||||||
|
"parameters": [
|
||||||
|
{
|
||||||
|
"type": "integer",
|
||||||
|
"description": "Time ID",
|
||||||
|
"name": "id",
|
||||||
|
"in": "path",
|
||||||
|
"required": true
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"responses": {
|
||||||
|
"302": {
|
||||||
|
"description": "Found",
|
||||||
|
"headers": {
|
||||||
|
"Location": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "Redirect URL"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"404": {
|
||||||
|
"description": "Time does not have a Bot",
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/definitions/Error"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"default": {
|
||||||
|
"description": "General error response",
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/definitions/Error"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"/user": {
|
"/user": {
|
||||||
"get": {
|
"get": {
|
||||||
"security": [
|
"security": [
|
||||||
@@ -624,12 +671,6 @@
|
|||||||
"Map": {
|
"Map": {
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"properties": {
|
"properties": {
|
||||||
"creator": {
|
|
||||||
"type": "string"
|
|
||||||
},
|
|
||||||
"date": {
|
|
||||||
"type": "string"
|
|
||||||
},
|
|
||||||
"display_name": {
|
"display_name": {
|
||||||
"type": "string"
|
"type": "string"
|
||||||
},
|
},
|
||||||
@@ -638,6 +679,9 @@
|
|||||||
},
|
},
|
||||||
"id": {
|
"id": {
|
||||||
"type": "integer"
|
"type": "integer"
|
||||||
|
},
|
||||||
|
"thumbnail": {
|
||||||
|
"type": "integer"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -7,16 +7,14 @@ definitions:
|
|||||||
type: object
|
type: object
|
||||||
Map:
|
Map:
|
||||||
properties:
|
properties:
|
||||||
creator:
|
|
||||||
type: string
|
|
||||||
date:
|
|
||||||
type: string
|
|
||||||
display_name:
|
display_name:
|
||||||
type: string
|
type: string
|
||||||
game_id:
|
game_id:
|
||||||
type: integer
|
type: integer
|
||||||
id:
|
id:
|
||||||
type: integer
|
type: integer
|
||||||
|
thumbnail:
|
||||||
|
type: integer
|
||||||
type: object
|
type: object
|
||||||
PagedResponse-Map:
|
PagedResponse-Map:
|
||||||
properties:
|
properties:
|
||||||
@@ -408,6 +406,36 @@ paths:
|
|||||||
summary: Get time by ID
|
summary: Get time by ID
|
||||||
tags:
|
tags:
|
||||||
- times
|
- times
|
||||||
|
/time/{id}/bot:
|
||||||
|
get:
|
||||||
|
description: Get a HTTP 302 Redirect to the download url for the bot replay
|
||||||
|
of a time by its ID if it exists
|
||||||
|
parameters:
|
||||||
|
- description: Time ID
|
||||||
|
in: path
|
||||||
|
name: id
|
||||||
|
required: true
|
||||||
|
type: integer
|
||||||
|
responses:
|
||||||
|
"302":
|
||||||
|
description: Found
|
||||||
|
headers:
|
||||||
|
Location:
|
||||||
|
description: Redirect URL
|
||||||
|
type: string
|
||||||
|
"404":
|
||||||
|
description: Time does not have a Bot
|
||||||
|
schema:
|
||||||
|
$ref: '#/definitions/Error'
|
||||||
|
default:
|
||||||
|
description: General error response
|
||||||
|
schema:
|
||||||
|
$ref: '#/definitions/Error'
|
||||||
|
security:
|
||||||
|
- ApiKeyAuth: []
|
||||||
|
summary: Get redirect to bot download url by time ID
|
||||||
|
tags:
|
||||||
|
- times
|
||||||
/time/placement:
|
/time/placement:
|
||||||
get:
|
get:
|
||||||
description: |-
|
description: |-
|
||||||
@@ -418,6 +446,7 @@ paths:
|
|||||||
description: Comma-separated array of time IDs (25 Limit)
|
description: Comma-separated array of time IDs (25 Limit)
|
||||||
in: query
|
in: query
|
||||||
items:
|
items:
|
||||||
|
format: int64
|
||||||
type: integer
|
type: integer
|
||||||
name: ids
|
name: ids
|
||||||
required: true
|
required: true
|
||||||
|
|||||||
2
go.mod
2
go.mod
@@ -4,7 +4,7 @@ go 1.24.0
|
|||||||
|
|
||||||
require (
|
require (
|
||||||
git.itzana.me/StrafesNET/dev-service v0.0.0-20250628022558-4cf59e46f9f1
|
git.itzana.me/StrafesNET/dev-service v0.0.0-20250628022558-4cf59e46f9f1
|
||||||
git.itzana.me/strafesnet/go-grpc v0.0.0-20250724030029-845bea991815
|
git.itzana.me/strafesnet/go-grpc v0.0.0-20250807005013-301d35b914ef
|
||||||
github.com/gin-gonic/gin v1.10.1
|
github.com/gin-gonic/gin v1.10.1
|
||||||
github.com/sirupsen/logrus v1.9.3
|
github.com/sirupsen/logrus v1.9.3
|
||||||
github.com/swaggo/files v1.0.1
|
github.com/swaggo/files v1.0.1
|
||||||
|
|||||||
4
go.sum
4
go.sum
@@ -2,8 +2,8 @@ cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMT
|
|||||||
cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
|
cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
|
||||||
git.itzana.me/StrafesNET/dev-service v0.0.0-20250628022558-4cf59e46f9f1 h1:Ndpa4d93gCQrW5pm64u9IRaDJTfu7fKfLqm8ctzML1o=
|
git.itzana.me/StrafesNET/dev-service v0.0.0-20250628022558-4cf59e46f9f1 h1:Ndpa4d93gCQrW5pm64u9IRaDJTfu7fKfLqm8ctzML1o=
|
||||||
git.itzana.me/StrafesNET/dev-service v0.0.0-20250628022558-4cf59e46f9f1/go.mod h1:KJal0K++M6HEzSry6JJ2iDPZtOQn5zSstNlDbU3X4Jg=
|
git.itzana.me/StrafesNET/dev-service v0.0.0-20250628022558-4cf59e46f9f1/go.mod h1:KJal0K++M6HEzSry6JJ2iDPZtOQn5zSstNlDbU3X4Jg=
|
||||||
git.itzana.me/strafesnet/go-grpc v0.0.0-20250724030029-845bea991815 h1:hkuOnehphRXUq/2z2UYgoqTq5MJj1GsWfshyc7bXda8=
|
git.itzana.me/strafesnet/go-grpc v0.0.0-20250807005013-301d35b914ef h1:SJi4V4+xzScFnbMRN1gkZxcqR1xKfiT7CaXanLltEzw=
|
||||||
git.itzana.me/strafesnet/go-grpc v0.0.0-20250724030029-845bea991815/go.mod h1:X7XTRUScRkBWq8q8bplbeso105RPDlnY7J6Wy1IwBMs=
|
git.itzana.me/strafesnet/go-grpc v0.0.0-20250807005013-301d35b914ef/go.mod h1:X7XTRUScRkBWq8q8bplbeso105RPDlnY7J6Wy1IwBMs=
|
||||||
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
|
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
|
||||||
github.com/KyleBanks/depth v1.2.1 h1:5h8fQADFrWtarTdtDudMmGsC7GPbOAu6RVB3ffsVFHc=
|
github.com/KyleBanks/depth v1.2.1 h1:5h8fQADFrWtarTdtDudMmGsC7GPbOAu6RVB3ffsVFHc=
|
||||||
github.com/KyleBanks/depth v1.2.1/go.mod h1:jzSb9d0L43HxTQfT+oSA1EEp2q+ne2uh6XgeJcm8brE=
|
github.com/KyleBanks/depth v1.2.1/go.mod h1:jzSb9d0L43HxTQfT+oSA1EEp2q+ne2uh6XgeJcm8brE=
|
||||||
|
|||||||
@@ -2,15 +2,17 @@ package dto
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"git.itzana.me/strafesnet/go-grpc/maps"
|
"git.itzana.me/strafesnet/go-grpc/maps"
|
||||||
"time"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
|
type MapFilter struct {
|
||||||
|
GameID *int32 `json:"game_id" form:"game_id"`
|
||||||
|
} // @name MapFilter
|
||||||
|
|
||||||
type Map struct {
|
type Map struct {
|
||||||
ID int64 `json:"id"`
|
ID int64 `json:"id"`
|
||||||
DisplayName string `json:"display_name"`
|
DisplayName string `json:"display_name"`
|
||||||
Creator string `json:"creator"`
|
|
||||||
GameID int32 `json:"game_id"`
|
GameID int32 `json:"game_id"`
|
||||||
Date time.Time `json:"date"`
|
Thumbnail uint64 `json:"thumbnail"`
|
||||||
} // @name Map
|
} // @name Map
|
||||||
|
|
||||||
// FromGRPC converts a maps.MapResponse protobuf message to a Map domain object
|
// FromGRPC converts a maps.MapResponse protobuf message to a Map domain object
|
||||||
@@ -21,9 +23,8 @@ func (m *Map) FromGRPC(resp *maps.MapResponse) *Map {
|
|||||||
|
|
||||||
m.ID = resp.ID
|
m.ID = resp.ID
|
||||||
m.DisplayName = resp.DisplayName
|
m.DisplayName = resp.DisplayName
|
||||||
m.Creator = resp.Creator
|
|
||||||
m.Date = time.Unix(resp.Date, 0)
|
|
||||||
m.GameID = resp.GameID
|
m.GameID = resp.GameID
|
||||||
|
m.Thumbnail = resp.Thumbnail
|
||||||
|
|
||||||
return m
|
return m
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,40 +0,0 @@
|
|||||||
package dto
|
|
||||||
|
|
||||||
import (
|
|
||||||
"git.itzana.me/strafesnet/go-grpc/maps_extended"
|
|
||||||
"time"
|
|
||||||
)
|
|
||||||
|
|
||||||
type MapExtendedFilter struct {
|
|
||||||
GameID *uint32 `json:"game_id" form:"game_id"`
|
|
||||||
} // @name MapFilter
|
|
||||||
|
|
||||||
type MapExtended struct {
|
|
||||||
ID int64 `json:"id"`
|
|
||||||
DisplayName string `json:"display_name"`
|
|
||||||
Creator string `json:"creator"`
|
|
||||||
GameID uint32 `json:"game_id"`
|
|
||||||
Date time.Time `json:"date"`
|
|
||||||
CreatedAt time.Time `json:created_at`
|
|
||||||
UpdatedAt time.Time `json:updated_at`
|
|
||||||
Submitter uint64 `json:submitter`
|
|
||||||
Thumbnail uint64 `json:thumbnail`
|
|
||||||
AssetVersion uint64 `json:asset_version`
|
|
||||||
LoadCount uint32 `json:load_count`
|
|
||||||
Modes uint32 `json:modes`
|
|
||||||
} // @name Map
|
|
||||||
|
|
||||||
// FromGRPC converts a maps.MapResponse protobuf message to a Map domain object
|
|
||||||
func (m *MapExtended) FromGRPC(resp *maps_extended.MapResponse) *MapExtended {
|
|
||||||
if resp == nil {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
m.ID = resp.ID
|
|
||||||
m.DisplayName = resp.DisplayName
|
|
||||||
m.Creator = resp.Creator
|
|
||||||
m.Date = time.Unix(resp.Date, 0)
|
|
||||||
m.GameID = resp.GameID
|
|
||||||
|
|
||||||
return m
|
|
||||||
}
|
|
||||||
@@ -1,9 +1,10 @@
|
|||||||
package dto
|
package dto
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"git.itzana.me/strafesnet/go-grpc/times"
|
|
||||||
"strconv"
|
"strconv"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"git.itzana.me/strafesnet/go-grpc/times"
|
||||||
)
|
)
|
||||||
|
|
||||||
type TimePlacement struct {
|
type TimePlacement struct {
|
||||||
|
|||||||
@@ -2,9 +2,10 @@ package handlers
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"strconv"
|
||||||
|
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
"google.golang.org/grpc"
|
"google.golang.org/grpc"
|
||||||
"strconv"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
@@ -14,7 +15,7 @@ const (
|
|||||||
// Handler is a base handler that provides common functionality for all HTTP handlers.
|
// Handler is a base handler that provides common functionality for all HTTP handlers.
|
||||||
type Handler struct {
|
type Handler struct {
|
||||||
dataClient *grpc.ClientConn
|
dataClient *grpc.ClientConn
|
||||||
mapsClient *grpc.ClientConn
|
storageUrl string
|
||||||
}
|
}
|
||||||
|
|
||||||
// HandlerOption defines a functional option for configuring a Handler
|
// HandlerOption defines a functional option for configuring a Handler
|
||||||
@@ -27,10 +28,10 @@ func WithDataClient(dataClient *grpc.ClientConn) HandlerOption {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// WithMapsClient sets the maps client for the Handler
|
// WithStorageUrl sets the storage url
|
||||||
func WithMapsClient(mapsClient *grpc.ClientConn) HandlerOption {
|
func WithStorageUrl(storageUrl string) HandlerOption {
|
||||||
return func(h *Handler) {
|
return func(cfg *Handler) {
|
||||||
h.mapsClient = mapsClient
|
cfg.storageUrl = storageUrl
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
package handlers
|
package handlers
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"git.itzana.me/strafesnet/go-grpc/maps_extended"
|
"git.itzana.me/strafesnet/go-grpc/maps"
|
||||||
"git.itzana.me/strafesnet/public-api/pkg/api/dto"
|
"git.itzana.me/strafesnet/public-api/pkg/api/dto"
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
log "github.com/sirupsen/logrus"
|
log "github.com/sirupsen/logrus"
|
||||||
@@ -49,7 +49,7 @@ func (h *MapHandler) Get(ctx *gin.Context) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Call the gRPC service
|
// Call the gRPC service
|
||||||
mapData, err := maps_extended.NewMapsServiceClient(h.mapsClient).Get(ctx, &maps_extended.MapId{
|
mapData, err := maps.NewMapsServiceClient(h.dataClient).Get(ctx, &maps.IdMessage{
|
||||||
ID: mapID,
|
ID: mapID,
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -72,11 +72,11 @@ func (h *MapHandler) Get(ctx *gin.Context) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Convert gRPC MapResponse object to dto.Map object
|
// Convert gRPC MapResponse object to dto.Map object
|
||||||
var mapDto dto.MapExtended
|
var mapDto dto.Map
|
||||||
result := mapDto.FromGRPC(mapData)
|
result := mapDto.FromGRPC(mapData)
|
||||||
|
|
||||||
// Return the map data
|
// Return the map data
|
||||||
ctx.JSON(http.StatusOK, dto.Response[dto.MapExtended]{
|
ctx.JSON(http.StatusOK, dto.Response[dto.Map]{
|
||||||
Data: *result,
|
Data: *result,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@@ -107,7 +107,7 @@ func (h *MapHandler) List(ctx *gin.Context) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Get list filter
|
// Get list filter
|
||||||
var filter dto.MapExtendedFilter
|
var filter dto.MapFilter
|
||||||
if err := ctx.ShouldBindQuery(&filter); err != nil {
|
if err := ctx.ShouldBindQuery(&filter); err != nil {
|
||||||
ctx.JSON(http.StatusBadRequest, dto.Error{
|
ctx.JSON(http.StatusBadRequest, dto.Error{
|
||||||
Error: err.Error(),
|
Error: err.Error(),
|
||||||
@@ -116,13 +116,13 @@ func (h *MapHandler) List(ctx *gin.Context) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Call the gRPC service
|
// Call the gRPC service
|
||||||
mapList, err := maps_extended.NewMapsServiceClient(h.mapsClient).List(ctx, &maps_extended.ListRequest{
|
mapList, err := maps.NewMapsServiceClient(h.dataClient).List(ctx, &maps.ListRequest{
|
||||||
Filter: &maps_extended.MapFilter{
|
Filter: &maps.MapFilter{
|
||||||
GameID: filter.GameID,
|
GameID: filter.GameID,
|
||||||
},
|
},
|
||||||
Page: &maps_extended.Pagination{
|
Page: &maps.Pagination{
|
||||||
Size: uint32(query.PageSize),
|
Size: int32(query.PageSize),
|
||||||
Number: uint32(query.PageNumber),
|
Number: int32(query.PageNumber),
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -136,14 +136,14 @@ func (h *MapHandler) List(ctx *gin.Context) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Convert gRPC MapResponse objects to dto.Map objects
|
// Convert gRPC MapResponse objects to dto.Map objects
|
||||||
dtoMaps := make([]dto.MapExtended, len(mapList.Maps))
|
dtoMaps := make([]dto.Map, len(mapList.Maps))
|
||||||
for i, m := range mapList.Maps {
|
for i, m := range mapList.Maps {
|
||||||
var mapDto dto.MapExtended
|
var mapDto dto.Map
|
||||||
dtoMaps[i] = *mapDto.FromGRPC(m)
|
dtoMaps[i] = *mapDto.FromGRPC(m)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Return the paged response
|
// Return the paged response
|
||||||
ctx.JSON(http.StatusOK, dto.PagedResponse[dto.MapExtended]{
|
ctx.JSON(http.StatusOK, dto.PagedResponse[dto.Map]{
|
||||||
Data: dtoMaps,
|
Data: dtoMaps,
|
||||||
Pagination: dto.Pagination{
|
Pagination: dto.Pagination{
|
||||||
Page: query.PageNumber,
|
Page: query.PageNumber,
|
||||||
|
|||||||
@@ -1,17 +1,22 @@
|
|||||||
package handlers
|
package handlers
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"math"
|
||||||
|
"net/http"
|
||||||
|
"net/url"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"git.itzana.me/strafesnet/go-grpc/bots"
|
||||||
"git.itzana.me/strafesnet/go-grpc/times"
|
"git.itzana.me/strafesnet/go-grpc/times"
|
||||||
"git.itzana.me/strafesnet/public-api/pkg/api/dto"
|
"git.itzana.me/strafesnet/public-api/pkg/api/dto"
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
log "github.com/sirupsen/logrus"
|
log "github.com/sirupsen/logrus"
|
||||||
"google.golang.org/grpc/codes"
|
"google.golang.org/grpc/codes"
|
||||||
"google.golang.org/grpc/status"
|
"google.golang.org/grpc/status"
|
||||||
"math"
|
|
||||||
"net/http"
|
|
||||||
"strconv"
|
|
||||||
"strings"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
// TimesHandler handles HTTP requests related to times.
|
// TimesHandler handles HTTP requests related to times.
|
||||||
@@ -317,3 +322,167 @@ func (h *TimesHandler) GetPlacements(ctx *gin.Context) {
|
|||||||
Data: ranks,
|
Data: ranks,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// @Summary Get redirect to bot download url by time ID
|
||||||
|
// @Description Get a HTTP 302 Redirect to the download url for the bot replay of a time by its ID if it exists
|
||||||
|
// @Tags times
|
||||||
|
// @Security ApiKeyAuth
|
||||||
|
// @Param id path int true "Time ID"
|
||||||
|
// @Success 302
|
||||||
|
// @Header 302 {string} Location "Redirect URL"
|
||||||
|
// @Failure 404 {object} dto.Error "Time does not have a Bot"
|
||||||
|
// @Failure default {object} dto.Error "General error response"
|
||||||
|
// @Router /time/{id}/bot [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, ×.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.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, err := url.JoinPath(h.storageUrl, botData.FileID)
|
||||||
|
if err != nil {
|
||||||
|
statusCode := http.StatusInternalServerError
|
||||||
|
errorMessage := "Error joining Url"
|
||||||
|
|
||||||
|
ctx.JSON(statusCode, dto.Error{
|
||||||
|
Error: errorMessage,
|
||||||
|
})
|
||||||
|
log.WithError(err).Error(
|
||||||
|
"Error joining Url",
|
||||||
|
)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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 := (&http.Client{
|
||||||
|
Timeout: 10 * time.Second,
|
||||||
|
}).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.Error("Unexpected status")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
type storageResp struct {
|
||||||
|
ID string `json:"ID"`
|
||||||
|
Created int64 `json:"Created"`
|
||||||
|
Url string `json:"Url"`
|
||||||
|
}
|
||||||
|
// Decode the JSON body into the storageResp struct.
|
||||||
|
var info storageResp
|
||||||
|
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 download url
|
||||||
|
ctx.Redirect(http.StatusFound, info.Url)
|
||||||
|
}
|
||||||
|
|||||||
@@ -4,6 +4,9 @@ import (
|
|||||||
"context"
|
"context"
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"net/http"
|
||||||
|
"time"
|
||||||
|
|
||||||
"git.itzana.me/StrafesNET/dev-service/pkg/api/middleware"
|
"git.itzana.me/StrafesNET/dev-service/pkg/api/middleware"
|
||||||
"git.itzana.me/strafesnet/public-api/docs"
|
"git.itzana.me/strafesnet/public-api/docs"
|
||||||
"git.itzana.me/strafesnet/public-api/pkg/api/handlers"
|
"git.itzana.me/strafesnet/public-api/pkg/api/handlers"
|
||||||
@@ -13,8 +16,6 @@ import (
|
|||||||
ginSwagger "github.com/swaggo/gin-swagger"
|
ginSwagger "github.com/swaggo/gin-swagger"
|
||||||
"github.com/urfave/cli/v2"
|
"github.com/urfave/cli/v2"
|
||||||
"google.golang.org/grpc"
|
"google.golang.org/grpc"
|
||||||
"net/http"
|
|
||||||
"time"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
// Option defines a function that configures a Router
|
// Option defines a function that configures a Router
|
||||||
@@ -25,7 +26,8 @@ type RouterConfig struct {
|
|||||||
port int
|
port int
|
||||||
devClient *grpc.ClientConn
|
devClient *grpc.ClientConn
|
||||||
dataClient *grpc.ClientConn
|
dataClient *grpc.ClientConn
|
||||||
mapsClient *grpc.ClientConn
|
httpClient *http.Client
|
||||||
|
storageUrl string
|
||||||
context *cli.Context
|
context *cli.Context
|
||||||
shutdownTimeout time.Duration
|
shutdownTimeout time.Duration
|
||||||
}
|
}
|
||||||
@@ -58,10 +60,10 @@ func WithDataClient(conn *grpc.ClientConn) Option {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// WithMapsClient sets the maps gRPC client
|
// WithStorageUrl sets the storage url
|
||||||
func WithMapsClient(conn *grpc.ClientConn) Option {
|
func WithStorageUrl(storageUrl string) Option {
|
||||||
return func(cfg *RouterConfig) {
|
return func(cfg *RouterConfig) {
|
||||||
cfg.mapsClient = conn
|
cfg.storageUrl = storageUrl
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -80,11 +82,13 @@ func setupRoutes(cfg *RouterConfig) (*gin.Engine, error) {
|
|||||||
|
|
||||||
handlerOptions := []handlers.HandlerOption{
|
handlerOptions := []handlers.HandlerOption{
|
||||||
handlers.WithDataClient(cfg.dataClient),
|
handlers.WithDataClient(cfg.dataClient),
|
||||||
handlers.WithMapsClient(cfg.mapsClient),
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Times handler
|
// Times handler
|
||||||
timesHandler, err := handlers.NewTimesHandler(handlerOptions...)
|
timesHandler, err := handlers.NewTimesHandler(
|
||||||
|
handlers.WithDataClient(cfg.dataClient),
|
||||||
|
handlers.WithStorageUrl(cfg.storageUrl),
|
||||||
|
)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
@@ -108,29 +112,36 @@ func setupRoutes(cfg *RouterConfig) (*gin.Engine, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
docs.SwaggerInfo.BasePath = "/api/v1"
|
docs.SwaggerInfo.BasePath = "/api/v1"
|
||||||
v1 := r.Group("/api/v1")
|
dataGroup := r.Group("/api/v1")
|
||||||
{
|
{
|
||||||
// Auth middleware
|
// Auth middleware
|
||||||
v1.Use(middleware.ValidateRequest("Data", "Read", cfg.devClient))
|
dataGroup.Use(middleware.ValidateRequest("Data", "Read", cfg.devClient))
|
||||||
|
|
||||||
// Times
|
// Times
|
||||||
v1.GET("/time", timesHandler.List)
|
dataGroup.GET("/time", timesHandler.List)
|
||||||
v1.GET("/time/worldrecord", timesHandler.WrList)
|
dataGroup.GET("/time/worldrecord", timesHandler.WrList)
|
||||||
v1.GET("/time/placement", timesHandler.GetPlacements)
|
dataGroup.GET("/time/placement", timesHandler.GetPlacements)
|
||||||
v1.GET("/time/:id", timesHandler.Get)
|
dataGroup.GET("/time/:id", timesHandler.Get)
|
||||||
|
|
||||||
// Users
|
// Users
|
||||||
v1.GET("/user", usersHandler.List)
|
dataGroup.GET("/user", usersHandler.List)
|
||||||
v1.GET("/user/:id", usersHandler.Get)
|
dataGroup.GET("/user/:id", usersHandler.Get)
|
||||||
v1.GET("/user/:id/rank", usersHandler.GetRank)
|
dataGroup.GET("/user/:id/rank", usersHandler.GetRank)
|
||||||
|
|
||||||
// Maps
|
// Maps
|
||||||
v1.GET("/map", mapsHandler.List)
|
dataGroup.GET("/map", mapsHandler.List)
|
||||||
v1.GET("/map/:id", mapsHandler.Get)
|
dataGroup.GET("/map/:id", mapsHandler.Get)
|
||||||
|
|
||||||
// Rank
|
// Rank
|
||||||
v1.GET("/rank", rankHandler.List)
|
dataGroup.GET("/rank", rankHandler.List)
|
||||||
|
}
|
||||||
|
|
||||||
|
botsGroup := r.Group("/api/v1")
|
||||||
|
{
|
||||||
|
// Auth middleware
|
||||||
|
botsGroup.Use(middleware.ValidateRequest("Data", "Bots", cfg.devClient))
|
||||||
|
|
||||||
|
botsGroup.GET("/time/:id/bot", timesHandler.GetDownloadUrl)
|
||||||
}
|
}
|
||||||
r.GET("/docs/*any", ginSwagger.WrapHandler(swaggerfiles.Handler))
|
r.GET("/docs/*any", ginSwagger.WrapHandler(swaggerfiles.Handler))
|
||||||
r.GET("/", func(ctx *gin.Context) {
|
r.GET("/", func(ctx *gin.Context) {
|
||||||
|
|||||||
@@ -32,10 +32,10 @@ func NewApiCommand() *cli.Command {
|
|||||||
Value: "data-service:9000",
|
Value: "data-service:9000",
|
||||||
},
|
},
|
||||||
&cli.StringFlag{
|
&cli.StringFlag{
|
||||||
Name: "maps-rpc-host",
|
Name: "storage-host",
|
||||||
Usage: "Host of maps rpc",
|
Usage: "Host of storage",
|
||||||
EnvVars: []string{"MAPS_RPC_HOST"},
|
EnvVars: []string{"STORAGE_HOST"},
|
||||||
Value: "maptest-api:8081",
|
Value: "http://storage-service:9000/v1/file/",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
@@ -54,17 +54,14 @@ func runAPI(ctx *cli.Context) error {
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
// Maps service client
|
// Storage service http client
|
||||||
mapsConn, err := grpc.Dial(ctx.String("maps-rpc-host"), grpc.WithTransportCredentials(insecure.NewCredentials()))
|
storageUrl := ctx.String("storage-host")
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
return api.NewRouter(
|
return api.NewRouter(
|
||||||
api.WithContext(ctx),
|
api.WithContext(ctx),
|
||||||
api.WithPort(ctx.Int("port")),
|
api.WithPort(ctx.Int("port")),
|
||||||
api.WithDevClient(devConn),
|
api.WithDevClient(devConn),
|
||||||
api.WithDataClient(dataConn),
|
api.WithDataClient(dataConn),
|
||||||
api.WithMapsClient(mapsConn),
|
api.WithStorageUrl(storageUrl),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user