generated from VLADIMIR/template
up
This commit is contained in:
@@ -98,6 +98,16 @@ make test
|
||||
восстановление worker и команды проверки описаны в
|
||||
[`docs/GAME_PACKAGE.md`](docs/GAME_PACKAGE.md#экспорт-со-статусом-офлайн-режима-этап-5).
|
||||
|
||||
## Учёт устройств площадки
|
||||
|
||||
В `APP_MODE=venue` публичный `POST /api/devices/heartbeat` учитывает подключения
|
||||
браузеров в памяти; `GET /api/devices` доступен организатору, администратору и
|
||||
разработчику, с необязательным `gameID`. В prod heartbeat возвращает
|
||||
`enabled=false`, список недоступен. TTL связи — менее 60 секунд, удаление —
|
||||
через 15 минут, предел — 1024 подключения. Credentials не сохраняются.
|
||||
Подробности и ограничения описаны в
|
||||
[workspace README](../README.md#учёт-устройств-этап-12).
|
||||
|
||||
## Документация и юридические документы
|
||||
|
||||
- [`docs/TERMS.md`](docs/TERMS.md) — Пользовательское соглашение (оферта); раздаётся сервером по постоянному URL `/api/terms`;
|
||||
|
||||
@@ -41,6 +41,23 @@ option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_swagger) = {
|
||||
};
|
||||
|
||||
service EveningDetectiveServer {
|
||||
rpc DeviceHeartbeat(DeviceHeartbeatReq) returns (DeviceHeartbeatRsp) {
|
||||
option (google.api.http) = { post: "/api/devices/heartbeat" body: "*" };
|
||||
option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = {
|
||||
tags : "Устройства";
|
||||
summary : "Обновить присутствие подключения браузера";
|
||||
description: "Публичный endpoint. Контексты взаимоисключающие и проверяются сервером. В prod enabled=false без регистрации. UUID обозначает браузерный профиль, не физический телефон.";
|
||||
security : {};
|
||||
};
|
||||
}
|
||||
rpc GetDevices(GetDevicesReq) returns (GetDevicesRsp) {
|
||||
option (google.api.http) = { get: "/api/devices" };
|
||||
option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = {
|
||||
tags : "Устройства";
|
||||
summary : "Получить устройства площадки";
|
||||
description: "Только venue; организатор, администратор или разработчик. Связь актуальна менее 60 секунд; подключения удаляются через 15 минут. Фильтр gameID оставляет только подключения указанной игры.";
|
||||
};
|
||||
}
|
||||
rpc GetTasks(GetTasksReq) returns (GetTasksRsp) {
|
||||
option (google.api.http) = { get: "/api/tasks" };
|
||||
option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = {
|
||||
@@ -2382,3 +2399,46 @@ message GameResultsPackageRsp {
|
||||
string packageID = 2;
|
||||
string appliedAt = 3;
|
||||
}
|
||||
|
||||
message DeviceHeartbeatReq {
|
||||
string deviceID = 1;
|
||||
string connectionID = 2;
|
||||
optional int32 organizerGameID = 3;
|
||||
optional int32 teamID = 4;
|
||||
string password = 5;
|
||||
string screenID = 6;
|
||||
string accessToken = 7;
|
||||
}
|
||||
|
||||
message DeviceHeartbeatRsp {
|
||||
optional bool enabled = 1;
|
||||
google.protobuf.Timestamp serverTime = 2;
|
||||
}
|
||||
|
||||
message GetDevicesReq { optional int32 gameID = 1; }
|
||||
|
||||
message GetDevicesRsp {
|
||||
repeated VenueDevice devices = 1;
|
||||
google.protobuf.Timestamp serverTime = 2;
|
||||
}
|
||||
|
||||
message VenueDevice {
|
||||
string ID = 1;
|
||||
google.protobuf.Timestamp firstSeenAt = 2;
|
||||
google.protobuf.Timestamp lastSeenAt = 3;
|
||||
bool connected = 4;
|
||||
repeated DeviceConnection connections = 5;
|
||||
}
|
||||
|
||||
message DeviceConnection {
|
||||
string ID = 1;
|
||||
google.protobuf.Timestamp firstSeenAt = 2;
|
||||
google.protobuf.Timestamp lastSeenAt = 3;
|
||||
string IP = 4;
|
||||
string userAgent = 5;
|
||||
bool connected = 6;
|
||||
optional int32 gameID = 7;
|
||||
optional int32 teamID = 8;
|
||||
optional int32 screenID = 9;
|
||||
string kind = 10; // browser, organizer, team, screen
|
||||
}
|
||||
|
||||
Binary file not shown.
@@ -35,6 +35,7 @@ import (
|
||||
"evening_detective_server/internal/repos/tickets_repo"
|
||||
"evening_detective_server/internal/repos/users_repo"
|
||||
"evening_detective_server/internal/services/answer_service"
|
||||
"evening_detective_server/internal/services/device_presence_service"
|
||||
"evening_detective_server/internal/services/file_service"
|
||||
"evening_detective_server/internal/services/game_export_service"
|
||||
"evening_detective_server/internal/services/game_import_service"
|
||||
@@ -228,6 +229,8 @@ func main() {
|
||||
log.Fatalln("Failed to listen:", err)
|
||||
}
|
||||
|
||||
devicePresenceService := device_presence_service.NewDevicePresenceService(venueMode, gameService, teamService, screenService)
|
||||
|
||||
s := grpc.NewServer(
|
||||
grpc.MaxRecvMsgSize(grpcMsgLimit),
|
||||
grpc.UnaryInterceptor(
|
||||
@@ -237,6 +240,7 @@ func main() {
|
||||
"/crabs.evening_detective_server.EveningDetectiveServer/GetParticipation": true,
|
||||
"/crabs.evening_detective_server.EveningDetectiveServer/GetTicket": true,
|
||||
"/crabs.evening_detective_server.EveningDetectiveServer/AnswerParticipation": true,
|
||||
"/crabs.evening_detective_server.EveningDetectiveServer/DeviceHeartbeat": true,
|
||||
"/crabs.evening_detective_server.EveningDetectiveServer/Ping": true,
|
||||
"/crabs.evening_detective_server.EveningDetectiveServer/Echo": true,
|
||||
"/crabs.evening_detective_server.EveningDetectiveServer/Signup": true,
|
||||
@@ -287,6 +291,7 @@ func main() {
|
||||
gameImportService,
|
||||
gameResultsService,
|
||||
gameResultsApplyService,
|
||||
devicePresenceService,
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
@@ -333,6 +333,74 @@
|
||||
]
|
||||
}
|
||||
},
|
||||
"/api/devices": {
|
||||
"get": {
|
||||
"summary": "Получить устройства площадки",
|
||||
"description": "Только venue; организатор, администратор или разработчик. Связь актуальна менее 60 секунд; подключения удаляются через 15 минут. Фильтр gameID оставляет только подключения указанной игры.",
|
||||
"operationId": "EveningDetectiveServer_GetDevices",
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "A successful response.",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/evening_detective_serverGetDevicesRsp"
|
||||
}
|
||||
},
|
||||
"default": {
|
||||
"description": "An unexpected error response.",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/rpcStatus"
|
||||
}
|
||||
}
|
||||
},
|
||||
"parameters": [
|
||||
{
|
||||
"name": "gameID",
|
||||
"in": "query",
|
||||
"required": false,
|
||||
"type": "integer",
|
||||
"format": "int32"
|
||||
}
|
||||
],
|
||||
"tags": [
|
||||
"Устройства"
|
||||
]
|
||||
}
|
||||
},
|
||||
"/api/devices/heartbeat": {
|
||||
"post": {
|
||||
"summary": "Обновить присутствие подключения браузера",
|
||||
"description": "Публичный endpoint. Контексты взаимоисключающие и проверяются сервером. В prod enabled=false без регистрации. UUID обозначает браузерный профиль, не физический телефон.",
|
||||
"operationId": "EveningDetectiveServer_DeviceHeartbeat",
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "A successful response.",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/evening_detective_serverDeviceHeartbeatRsp"
|
||||
}
|
||||
},
|
||||
"default": {
|
||||
"description": "An unexpected error response.",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/rpcStatus"
|
||||
}
|
||||
}
|
||||
},
|
||||
"parameters": [
|
||||
{
|
||||
"name": "body",
|
||||
"in": "body",
|
||||
"required": true,
|
||||
"schema": {
|
||||
"$ref": "#/definitions/evening_detective_serverDeviceHeartbeatReq"
|
||||
}
|
||||
}
|
||||
],
|
||||
"tags": [
|
||||
"Устройства"
|
||||
],
|
||||
"security": []
|
||||
}
|
||||
},
|
||||
"/api/files/upload": {
|
||||
"post": {
|
||||
"summary": "Загрузить файл",
|
||||
@@ -4700,6 +4768,87 @@
|
||||
},
|
||||
"description": "Developer tasks use new, in_progress, review and done statuses."
|
||||
},
|
||||
"evening_detective_serverDeviceConnection": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"ID": {
|
||||
"type": "string"
|
||||
},
|
||||
"firstSeenAt": {
|
||||
"type": "string",
|
||||
"format": "date-time"
|
||||
},
|
||||
"lastSeenAt": {
|
||||
"type": "string",
|
||||
"format": "date-time"
|
||||
},
|
||||
"IP": {
|
||||
"type": "string"
|
||||
},
|
||||
"userAgent": {
|
||||
"type": "string"
|
||||
},
|
||||
"connected": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"gameID": {
|
||||
"type": "integer",
|
||||
"format": "int32"
|
||||
},
|
||||
"teamID": {
|
||||
"type": "integer",
|
||||
"format": "int32"
|
||||
},
|
||||
"screenID": {
|
||||
"type": "integer",
|
||||
"format": "int32"
|
||||
},
|
||||
"kind": {
|
||||
"type": "string",
|
||||
"title": "browser, organizer, team, screen"
|
||||
}
|
||||
}
|
||||
},
|
||||
"evening_detective_serverDeviceHeartbeatReq": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"deviceID": {
|
||||
"type": "string"
|
||||
},
|
||||
"connectionID": {
|
||||
"type": "string"
|
||||
},
|
||||
"organizerGameID": {
|
||||
"type": "integer",
|
||||
"format": "int32"
|
||||
},
|
||||
"teamID": {
|
||||
"type": "integer",
|
||||
"format": "int32"
|
||||
},
|
||||
"password": {
|
||||
"type": "string"
|
||||
},
|
||||
"screenID": {
|
||||
"type": "string"
|
||||
},
|
||||
"accessToken": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
"evening_detective_serverDeviceHeartbeatRsp": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"enabled": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"serverTime": {
|
||||
"type": "string",
|
||||
"format": "date-time"
|
||||
}
|
||||
}
|
||||
},
|
||||
"evening_detective_serverDoor": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
@@ -4894,6 +5043,22 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"evening_detective_serverGetDevicesRsp": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"devices": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "object",
|
||||
"$ref": "#/definitions/evening_detective_serverVenueDevice"
|
||||
}
|
||||
},
|
||||
"serverTime": {
|
||||
"type": "string",
|
||||
"format": "date-time"
|
||||
}
|
||||
}
|
||||
},
|
||||
"evening_detective_serverGetGameAnswersRsp": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
@@ -6155,6 +6320,32 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"evening_detective_serverVenueDevice": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"ID": {
|
||||
"type": "string"
|
||||
},
|
||||
"firstSeenAt": {
|
||||
"type": "string",
|
||||
"format": "date-time"
|
||||
},
|
||||
"lastSeenAt": {
|
||||
"type": "string",
|
||||
"format": "date-time"
|
||||
},
|
||||
"connected": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"connections": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "object",
|
||||
"$ref": "#/definitions/evening_detective_serverDeviceConnection"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"protobufAny": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
|
||||
@@ -0,0 +1,59 @@
|
||||
package app
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"evening_detective_server/internal/modules/processor_jwt"
|
||||
"evening_detective_server/internal/modules/roles"
|
||||
"evening_detective_server/internal/services/device_presence_service"
|
||||
proto "evening_detective_server/proto"
|
||||
|
||||
"google.golang.org/protobuf/types/known/timestamppb"
|
||||
)
|
||||
|
||||
func (s *server) DeviceHeartbeat(ctx context.Context, req *proto.DeviceHeartbeatReq) (*proto.DeviceHeartbeatRsp, error) {
|
||||
if req == nil {
|
||||
req = &proto.DeviceHeartbeatReq{}
|
||||
}
|
||||
ip, userAgent := clientInfoFromContext(ctx)
|
||||
claims, authenticated := processor_jwt.ClaimsFromContext(ctx)
|
||||
input := device_presence_service.Heartbeat{
|
||||
DeviceID: req.DeviceID,
|
||||
ConnectionID: req.ConnectionID,
|
||||
OrganizerGameID: deviceOptionalID(req.OrganizerGameID),
|
||||
TeamID: deviceOptionalID(req.TeamID),
|
||||
Password: req.Password,
|
||||
ScreenID: req.ScreenID,
|
||||
IP: ip,
|
||||
UserAgent: userAgent,
|
||||
IsAuthenticated: authenticated,
|
||||
CanManage: authenticated && (roles.HasRole(claims, roles.Organizer) || roles.HasRole(claims, roles.Author)),
|
||||
}
|
||||
if req.AccessToken != "" {
|
||||
input.ScreenTokenHash = hashScreenToken(req.AccessToken)
|
||||
}
|
||||
enabled, now, err := s.devicePresenceService.Heartbeat(ctx, input)
|
||||
if err != nil {
|
||||
return nil, deviceErrorStatus(err)
|
||||
}
|
||||
return &proto.DeviceHeartbeatRsp{
|
||||
Enabled: &enabled,
|
||||
ServerTime: timestamppb.New(now),
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (s *server) GetDevices(ctx context.Context, req *proto.GetDevicesReq) (*proto.GetDevicesRsp, error) {
|
||||
if req == nil {
|
||||
req = &proto.GetDevicesReq{}
|
||||
}
|
||||
claims, authenticated := processor_jwt.ClaimsFromContext(ctx)
|
||||
canRead := authenticated && (roles.HasRole(claims, roles.Organizer) || roles.HasRole(claims, roles.Developer))
|
||||
devices, now, err := s.devicePresenceService.List(ctx, deviceOptionalID(req.GameID), canRead)
|
||||
if err != nil {
|
||||
return nil, deviceErrorStatus(err)
|
||||
}
|
||||
return &proto.GetDevicesRsp{
|
||||
Devices: mapVenueDevices(devices),
|
||||
ServerTime: timestamppb.New(now),
|
||||
}, nil
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
package app
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"log"
|
||||
|
||||
"evening_detective_server/internal/services/device_presence_service"
|
||||
"evening_detective_server/internal/services/game_service"
|
||||
"evening_detective_server/internal/services/screen_service"
|
||||
"evening_detective_server/internal/services/team_service"
|
||||
|
||||
"google.golang.org/grpc/codes"
|
||||
"google.golang.org/grpc/status"
|
||||
)
|
||||
|
||||
func deviceErrorStatus(err error) error {
|
||||
switch {
|
||||
case errors.Is(err, context.Canceled):
|
||||
return status.Error(codes.Canceled, "запрос отменён")
|
||||
case errors.Is(err, context.DeadlineExceeded):
|
||||
return status.Error(codes.DeadlineExceeded, "время запроса истекло")
|
||||
case errors.Is(err, device_presence_service.ErrInvalid):
|
||||
return status.Error(codes.InvalidArgument, device_presence_service.ErrInvalid.Error())
|
||||
case errors.Is(err, device_presence_service.ErrDisabled):
|
||||
return status.Error(codes.FailedPrecondition, device_presence_service.ErrDisabled.Error())
|
||||
case errors.Is(err, device_presence_service.ErrPermission):
|
||||
return status.Error(codes.PermissionDenied, device_presence_service.ErrPermission.Error())
|
||||
case errors.Is(err, device_presence_service.ErrUnauthorized):
|
||||
return status.Error(codes.Unauthenticated, device_presence_service.ErrUnauthorized.Error())
|
||||
case errors.Is(err, device_presence_service.ErrFull):
|
||||
return status.Error(codes.ResourceExhausted, device_presence_service.ErrFull.Error())
|
||||
case errors.Is(err, team_service.ErrTeamNotFound):
|
||||
return status.Error(codes.NotFound, team_service.ErrTeamNotFound.Error())
|
||||
case errors.Is(err, game_service.ErrGameNotFound):
|
||||
return status.Error(codes.NotFound, game_service.ErrGameNotFound.Error())
|
||||
case errors.Is(err, screen_service.ErrScreenNotFound):
|
||||
return status.Error(codes.Unauthenticated, "недействительная сессия экрана")
|
||||
default:
|
||||
log.Printf("device presence: %v", err)
|
||||
return status.Error(codes.Internal, "не удалось обновить учёт устройств")
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,53 @@
|
||||
package app
|
||||
|
||||
import (
|
||||
"evening_detective_server/internal/services/device_presence_service"
|
||||
proto "evening_detective_server/proto"
|
||||
|
||||
"google.golang.org/protobuf/types/known/timestamppb"
|
||||
)
|
||||
|
||||
func deviceOptionalID(id *int32) *int {
|
||||
if id == nil {
|
||||
return nil
|
||||
}
|
||||
value := int(*id)
|
||||
return &value
|
||||
}
|
||||
|
||||
func devicePublicID(id int) *int32 {
|
||||
if id == 0 {
|
||||
return nil
|
||||
}
|
||||
value := int32(id)
|
||||
return &value
|
||||
}
|
||||
|
||||
func mapVenueDevices(devices []device_presence_service.Device) []*proto.VenueDevice {
|
||||
result := make([]*proto.VenueDevice, 0, len(devices))
|
||||
for _, device := range devices {
|
||||
connections := make([]*proto.DeviceConnection, 0, len(device.Connections))
|
||||
for _, connection := range device.Connections {
|
||||
connections = append(connections, &proto.DeviceConnection{
|
||||
ID: connection.ID,
|
||||
FirstSeenAt: timestamppb.New(connection.FirstSeenAt),
|
||||
LastSeenAt: timestamppb.New(connection.LastSeenAt),
|
||||
IP: connection.IP,
|
||||
UserAgent: connection.UserAgent,
|
||||
Connected: connection.Connected,
|
||||
GameID: devicePublicID(connection.GameID),
|
||||
TeamID: devicePublicID(connection.TeamID),
|
||||
ScreenID: devicePublicID(connection.ScreenID),
|
||||
Kind: connection.Kind,
|
||||
})
|
||||
}
|
||||
result = append(result, &proto.VenueDevice{
|
||||
ID: device.ID,
|
||||
FirstSeenAt: timestamppb.New(device.FirstSeenAt),
|
||||
LastSeenAt: timestamppb.New(device.LastSeenAt),
|
||||
Connected: device.Connected,
|
||||
Connections: connections,
|
||||
})
|
||||
}
|
||||
return result
|
||||
}
|
||||
@@ -0,0 +1,99 @@
|
||||
package app
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"evening_detective_server/internal/modules/processor_jwt"
|
||||
"evening_detective_server/internal/services/device_presence_service"
|
||||
"evening_detective_server/internal/services/game_service"
|
||||
"evening_detective_server/internal/services/screen_service"
|
||||
"evening_detective_server/internal/services/team_service"
|
||||
proto "evening_detective_server/proto"
|
||||
|
||||
"github.com/google/uuid"
|
||||
"google.golang.org/grpc/codes"
|
||||
"google.golang.org/grpc/metadata"
|
||||
"google.golang.org/grpc/status"
|
||||
"google.golang.org/protobuf/encoding/protojson"
|
||||
)
|
||||
|
||||
func TestDeviceTransport(t *testing.T) {
|
||||
s := &server{devicePresenceService: device_presence_service.NewDevicePresenceService(true, nil, nil, nil)}
|
||||
ctx := metadata.NewIncomingContext(context.Background(), metadata.Pairs("x-real-ip", "192.0.2.3", "grpcgateway-user-agent", "test-browser"))
|
||||
req := &proto.DeviceHeartbeatReq{DeviceID: uuid.NewString(), ConnectionID: uuid.NewString()}
|
||||
rsp, err := s.DeviceHeartbeat(ctx, req)
|
||||
if err != nil || !rsp.GetEnabled() || rsp.ServerTime == nil {
|
||||
t.Fatal(rsp, err)
|
||||
}
|
||||
for _, role := range []string{"organizer", "admin", "developer", "author", "user", ""} {
|
||||
t.Run(role, func(t *testing.T) {
|
||||
roleCtx := processor_jwt.ContextWithClaims(ctx, &processor_jwt.JWTClaims{Roles: []string{role}})
|
||||
devices, err := s.GetDevices(roleCtx, &proto.GetDevicesReq{})
|
||||
if role == "author" || role == "user" || role == "" {
|
||||
if status.Code(err) != codes.PermissionDenied {
|
||||
t.Fatal(err)
|
||||
}
|
||||
return
|
||||
}
|
||||
if err != nil || len(devices.Devices) != 1 {
|
||||
t.Fatal(devices, err)
|
||||
}
|
||||
connection := devices.Devices[0].Connections[0]
|
||||
if connection.IP != "192.0.2.3" || connection.UserAgent != "test-browser" {
|
||||
t.Fatal(connection)
|
||||
}
|
||||
raw, err := protojson.Marshal(devices)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
for _, secret := range []string{"password", "accessToken", "ScreenTokenHash", "email", "URL"} {
|
||||
if strings.Contains(string(raw), secret) {
|
||||
t.Fatal("secret field in response")
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
req.DeviceID = "bad"
|
||||
if _, err = s.DeviceHeartbeat(ctx, req); status.Code(err) != codes.InvalidArgument {
|
||||
t.Fatal(err)
|
||||
}
|
||||
s.devicePresenceService = device_presence_service.NewDevicePresenceService(false, nil, nil, nil)
|
||||
rsp, err = s.DeviceHeartbeat(ctx, req)
|
||||
if err != nil || rsp.GetEnabled() {
|
||||
t.Fatal(rsp, err)
|
||||
}
|
||||
raw, err := protojson.Marshal(rsp)
|
||||
if err != nil || !strings.Contains(string(raw), `"enabled":false`) {
|
||||
t.Fatal("disabled flag must be explicit", string(raw), err)
|
||||
}
|
||||
if _, err = s.GetDevices(ctx, &proto.GetDevicesReq{}); status.Code(err) != codes.FailedPrecondition {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestDeviceErrors(t *testing.T) {
|
||||
for _, test := range []struct {
|
||||
err error
|
||||
code codes.Code
|
||||
}{
|
||||
{context.Canceled, codes.Canceled},
|
||||
{context.DeadlineExceeded, codes.DeadlineExceeded},
|
||||
{device_presence_service.ErrInvalid, codes.InvalidArgument},
|
||||
{device_presence_service.ErrDisabled, codes.FailedPrecondition},
|
||||
{device_presence_service.ErrPermission, codes.PermissionDenied},
|
||||
{device_presence_service.ErrUnauthorized, codes.Unauthenticated},
|
||||
{device_presence_service.ErrFull, codes.ResourceExhausted},
|
||||
{team_service.ErrTeamNotFound, codes.NotFound},
|
||||
{game_service.ErrGameNotFound, codes.NotFound},
|
||||
{screen_service.ErrScreenNotFound, codes.Unauthenticated},
|
||||
{errors.New("SQL password=secret /private/path"), codes.Internal},
|
||||
} {
|
||||
err := deviceErrorStatus(test.err)
|
||||
if status.Code(err) != test.code || strings.Contains(err.Error(), "secret") || strings.Contains(err.Error(), "/private") {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -2,6 +2,7 @@ package app
|
||||
|
||||
import (
|
||||
"evening_detective_server/internal/services/answer_service"
|
||||
"evening_detective_server/internal/services/device_presence_service"
|
||||
"evening_detective_server/internal/services/file_service"
|
||||
"evening_detective_server/internal/services/game_export_service"
|
||||
"evening_detective_server/internal/services/game_import_service"
|
||||
@@ -26,6 +27,7 @@ import (
|
||||
type server struct {
|
||||
proto.UnsafeEveningDetectiveServerServer
|
||||
|
||||
devicePresenceService *device_presence_service.DevicePresenceService
|
||||
usersService *users_service.UsersService
|
||||
uiService *ui_service.UIService
|
||||
taskService *task_service.TaskService
|
||||
@@ -70,8 +72,10 @@ func NewServer(
|
||||
gameImportService *game_import_service.GameImportService,
|
||||
gameResultsService *game_results_package_service.GameResultsPackageService,
|
||||
gameResultsApplyService *game_results_package_service.GameResultsApplyService,
|
||||
devicePresenceService *device_presence_service.DevicePresenceService,
|
||||
) proto.EveningDetectiveServerServer {
|
||||
return &server{
|
||||
devicePresenceService: devicePresenceService,
|
||||
gameImportService: gameImportService,
|
||||
gameResultsService: gameResultsService,
|
||||
gameResultsApplyService: gameResultsApplyService,
|
||||
|
||||
@@ -0,0 +1,269 @@
|
||||
// Package device_presence_service tracks browser connections in venue memory.
|
||||
package device_presence_service
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"sort"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"evening_detective_server/internal/services/game_model"
|
||||
"evening_detective_server/internal/services/screen_service"
|
||||
|
||||
"github.com/google/uuid"
|
||||
)
|
||||
|
||||
var (
|
||||
ErrInvalid = errors.New("некорректные данные устройства")
|
||||
ErrDisabled = errors.New("учёт устройств доступен только на площадке")
|
||||
ErrPermission = errors.New("нет доступа к устройствам")
|
||||
ErrUnauthorized = errors.New("требуется авторизация")
|
||||
ErrFull = errors.New("слишком много подключений")
|
||||
)
|
||||
|
||||
const (
|
||||
connectedFor = time.Minute
|
||||
retainedFor = 15 * time.Minute
|
||||
maxConnections = 1024
|
||||
)
|
||||
|
||||
type IGamePresence interface {
|
||||
GetGame(context.Context, int) (*game_model.Game, error)
|
||||
}
|
||||
type ITeamPresence interface {
|
||||
CheckPresence(context.Context, int, string) (int, error)
|
||||
}
|
||||
type IScreenPresence interface {
|
||||
Get(context.Context, int, string) (*screen_service.ScreenSession, error)
|
||||
}
|
||||
|
||||
// Heartbeat contains transient credentials; only the verified connection is retained.
|
||||
type Heartbeat struct {
|
||||
DeviceID string
|
||||
ConnectionID string
|
||||
OrganizerGameID *int
|
||||
TeamID *int
|
||||
Password string
|
||||
ScreenID string
|
||||
ScreenTokenHash string
|
||||
IP string
|
||||
UserAgent string
|
||||
IsAuthenticated bool
|
||||
CanManage bool
|
||||
}
|
||||
|
||||
type Connection struct {
|
||||
ID string
|
||||
DeviceID string
|
||||
FirstSeenAt time.Time
|
||||
LastSeenAt time.Time
|
||||
IP string
|
||||
UserAgent string
|
||||
Kind string
|
||||
GameID int
|
||||
TeamID int
|
||||
ScreenID int
|
||||
Connected bool
|
||||
}
|
||||
|
||||
type Device struct {
|
||||
ID string
|
||||
FirstSeenAt time.Time
|
||||
LastSeenAt time.Time
|
||||
Connected bool
|
||||
Connections []Connection
|
||||
}
|
||||
|
||||
type DevicePresenceService struct {
|
||||
enabled bool
|
||||
games IGamePresence
|
||||
teams ITeamPresence
|
||||
screens IScreenPresence
|
||||
now func() time.Time
|
||||
mu sync.Mutex
|
||||
connections map[string]Connection
|
||||
}
|
||||
|
||||
func NewDevicePresenceService(enabled bool, games IGamePresence, teams ITeamPresence, screens IScreenPresence) *DevicePresenceService {
|
||||
return &DevicePresenceService{
|
||||
enabled: enabled,
|
||||
games: games,
|
||||
teams: teams,
|
||||
screens: screens,
|
||||
now: time.Now,
|
||||
connections: make(map[string]Connection),
|
||||
}
|
||||
}
|
||||
|
||||
func (s *DevicePresenceService) Heartbeat(ctx context.Context, input Heartbeat) (bool, time.Time, error) {
|
||||
if err := ctx.Err(); err != nil {
|
||||
return false, time.Time{}, err
|
||||
}
|
||||
if !s.enabled {
|
||||
return false, s.now(), nil
|
||||
}
|
||||
connection, err := s.verify(ctx, input)
|
||||
if err != nil {
|
||||
return true, time.Time{}, err
|
||||
}
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
now := s.now()
|
||||
s.prune(now)
|
||||
old, exists := s.connections[connection.ID]
|
||||
if exists && old.DeviceID != connection.DeviceID {
|
||||
return true, now, ErrInvalid
|
||||
}
|
||||
if !exists && len(s.connections) >= maxConnections {
|
||||
return true, now, ErrFull
|
||||
}
|
||||
connection.FirstSeenAt = now
|
||||
if exists {
|
||||
connection.FirstSeenAt = old.FirstSeenAt
|
||||
}
|
||||
connection.LastSeenAt = now
|
||||
s.connections[connection.ID] = connection
|
||||
return true, now, nil
|
||||
}
|
||||
|
||||
func (s *DevicePresenceService) List(ctx context.Context, gameID *int, canManage bool) ([]Device, time.Time, error) {
|
||||
if err := ctx.Err(); err != nil {
|
||||
return nil, time.Time{}, err
|
||||
}
|
||||
if !s.enabled {
|
||||
return nil, time.Time{}, ErrDisabled
|
||||
}
|
||||
if !canManage {
|
||||
return nil, time.Time{}, ErrPermission
|
||||
}
|
||||
if gameID != nil && *gameID <= 0 {
|
||||
return nil, time.Time{}, ErrInvalid
|
||||
}
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
now := s.now()
|
||||
s.prune(now)
|
||||
devices := make(map[string]*Device)
|
||||
for _, connection := range s.connections {
|
||||
if gameID != nil && connection.GameID != *gameID {
|
||||
continue
|
||||
}
|
||||
connection.Connected = now.Sub(connection.LastSeenAt) < connectedFor
|
||||
device := devices[connection.DeviceID]
|
||||
if device == nil {
|
||||
device = &Device{
|
||||
ID: connection.DeviceID,
|
||||
FirstSeenAt: connection.FirstSeenAt,
|
||||
LastSeenAt: connection.LastSeenAt,
|
||||
}
|
||||
devices[connection.DeviceID] = device
|
||||
}
|
||||
if connection.FirstSeenAt.Before(device.FirstSeenAt) {
|
||||
device.FirstSeenAt = connection.FirstSeenAt
|
||||
}
|
||||
if connection.LastSeenAt.After(device.LastSeenAt) {
|
||||
device.LastSeenAt = connection.LastSeenAt
|
||||
}
|
||||
device.Connected = device.Connected || connection.Connected
|
||||
device.Connections = append(device.Connections, connection)
|
||||
}
|
||||
result := make([]Device, 0, len(devices))
|
||||
for _, device := range devices {
|
||||
sort.Slice(device.Connections, func(i, j int) bool { return device.Connections[i].ID < device.Connections[j].ID })
|
||||
result = append(result, *device)
|
||||
}
|
||||
sort.Slice(result, func(i, j int) bool { return result[i].ID < result[j].ID })
|
||||
return result, now, nil
|
||||
}
|
||||
|
||||
func (s *DevicePresenceService) prune(now time.Time) {
|
||||
for id, connection := range s.connections {
|
||||
if now.Sub(connection.LastSeenAt) >= retainedFor {
|
||||
delete(s.connections, id)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (s *DevicePresenceService) verify(ctx context.Context, input Heartbeat) (Connection, error) {
|
||||
deviceID, err := parseUUID(input.DeviceID)
|
||||
if err != nil {
|
||||
return Connection{}, err
|
||||
}
|
||||
connectionID, err := parseUUID(input.ConnectionID)
|
||||
if err != nil {
|
||||
return Connection{}, err
|
||||
}
|
||||
organizer := input.OrganizerGameID != nil
|
||||
team := input.TeamID != nil || input.Password != ""
|
||||
screen := input.ScreenID != "" || input.ScreenTokenHash != ""
|
||||
contexts := 0
|
||||
for _, present := range []bool{organizer, team, screen} {
|
||||
if present {
|
||||
contexts++
|
||||
}
|
||||
}
|
||||
if contexts > 1 || organizer && *input.OrganizerGameID <= 0 || team && (input.TeamID == nil || *input.TeamID <= 0 || input.Password == "") || screen && (input.ScreenID == "" || input.ScreenTokenHash == "") {
|
||||
return Connection{}, ErrInvalid
|
||||
}
|
||||
connection := Connection{
|
||||
ID: connectionID,
|
||||
DeviceID: deviceID,
|
||||
Kind: "browser",
|
||||
IP: input.IP,
|
||||
UserAgent: input.UserAgent,
|
||||
}
|
||||
// Bound untrusted metadata as well as the number of records.
|
||||
if len(connection.UserAgent) > 1024 {
|
||||
connection.UserAgent = connection.UserAgent[:1024]
|
||||
}
|
||||
connection.UserAgent = strings.ToValidUTF8(connection.UserAgent, "")
|
||||
if organizer {
|
||||
if !input.IsAuthenticated {
|
||||
return Connection{}, ErrUnauthorized
|
||||
}
|
||||
if !input.CanManage {
|
||||
return Connection{}, ErrPermission
|
||||
}
|
||||
if _, err := s.games.GetGame(ctx, *input.OrganizerGameID); err != nil {
|
||||
return Connection{}, err
|
||||
}
|
||||
connection.Kind = "organizer"
|
||||
connection.GameID = *input.OrganizerGameID
|
||||
}
|
||||
if team {
|
||||
gameID, err := s.teams.CheckPresence(ctx, *input.TeamID, input.Password)
|
||||
if err != nil {
|
||||
return Connection{}, err
|
||||
}
|
||||
connection.Kind = "team"
|
||||
connection.TeamID = *input.TeamID
|
||||
connection.GameID = gameID
|
||||
}
|
||||
if screen {
|
||||
id, err := strconv.Atoi(input.ScreenID)
|
||||
if err != nil || id <= 0 || id > 2147483647 {
|
||||
return Connection{}, ErrInvalid
|
||||
}
|
||||
session, err := s.screens.Get(ctx, id, input.ScreenTokenHash)
|
||||
if err != nil {
|
||||
return Connection{}, err
|
||||
}
|
||||
connection.Kind = "screen"
|
||||
connection.ScreenID = session.ID
|
||||
if session.GameID != nil {
|
||||
connection.GameID = *session.GameID
|
||||
}
|
||||
}
|
||||
return connection, nil
|
||||
}
|
||||
|
||||
func parseUUID(value string) (string, error) {
|
||||
parsed, err := uuid.Parse(value)
|
||||
if err != nil || len(value) != 36 || parsed == uuid.Nil {
|
||||
return "", ErrInvalid
|
||||
}
|
||||
return parsed.String(), nil
|
||||
}
|
||||
@@ -0,0 +1,258 @@
|
||||
package device_presence_service
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"strings"
|
||||
"sync"
|
||||
"testing"
|
||||
"time"
|
||||
"unicode/utf8"
|
||||
|
||||
"evening_detective_server/internal/services/game_model"
|
||||
"evening_detective_server/internal/services/game_service"
|
||||
"evening_detective_server/internal/services/screen_service"
|
||||
"evening_detective_server/internal/services/team_service"
|
||||
|
||||
"github.com/google/uuid"
|
||||
)
|
||||
|
||||
type presenceFixture struct{}
|
||||
|
||||
func (presenceFixture) GetGame(_ context.Context, id int) (*game_model.Game, error) {
|
||||
if id != 7 {
|
||||
return nil, game_service.ErrGameNotFound
|
||||
}
|
||||
return &game_model.Game{}, nil
|
||||
}
|
||||
func (presenceFixture) CheckPresence(_ context.Context, id int, password string) (int, error) {
|
||||
if id != 2 || password != "secret" {
|
||||
return 0, team_service.ErrTeamNotFound
|
||||
}
|
||||
return 7, nil
|
||||
}
|
||||
func (presenceFixture) Get(_ context.Context, id int, hash string) (*screen_service.ScreenSession, error) {
|
||||
if id != 3 || hash != "hash" {
|
||||
return nil, screen_service.ErrScreenNotFound
|
||||
}
|
||||
gameID := 7
|
||||
return &screen_service.ScreenSession{ID: 3, GameID: &gameID}, nil
|
||||
}
|
||||
func newFixture() (*DevicePresenceService, Heartbeat) {
|
||||
fixture := presenceFixture{}
|
||||
return NewDevicePresenceService(true, fixture, fixture, fixture), Heartbeat{DeviceID: uuid.NewString(), ConnectionID: uuid.NewString()}
|
||||
}
|
||||
func pointer(id int) *int { return &id }
|
||||
func send(t *testing.T, s *DevicePresenceService, input Heartbeat) {
|
||||
t.Helper()
|
||||
enabled, _, err := s.Heartbeat(context.Background(), input)
|
||||
if !enabled || err != nil {
|
||||
t.Fatalf("heartbeat: %v %v", enabled, err)
|
||||
}
|
||||
}
|
||||
func list(t *testing.T, s *DevicePresenceService, gameID *int) []Device {
|
||||
t.Helper()
|
||||
devices, _, err := s.List(context.Background(), gameID, true)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
return devices
|
||||
}
|
||||
|
||||
func TestRegistrationAndContext(t *testing.T) {
|
||||
s, input := newFixture()
|
||||
now := time.Date(2026, 10, 11, 0, 0, 0, 0, time.UTC)
|
||||
s.now = func() time.Time { return now }
|
||||
input.TeamID, input.Password = pointer(2), "secret"
|
||||
send(t, s, input)
|
||||
now = now.Add(time.Second)
|
||||
input.IP, input.UserAgent = "192.0.2.1", "updated"
|
||||
send(t, s, input)
|
||||
devices := list(t, s, pointer(7))
|
||||
if len(devices) != 1 || len(devices[0].Connections) != 1 {
|
||||
t.Fatal(devices)
|
||||
}
|
||||
connection := devices[0].Connections[0]
|
||||
if connection.IP != input.IP || connection.TeamID != 2 || connection.FirstSeenAt.Equal(connection.LastSeenAt) {
|
||||
t.Fatal(connection)
|
||||
}
|
||||
tab := input
|
||||
tab.ConnectionID = uuid.NewString()
|
||||
tab.TeamID, tab.Password = nil, ""
|
||||
tab.OrganizerGameID, tab.IsAuthenticated, tab.CanManage = pointer(7), true, true
|
||||
send(t, s, tab)
|
||||
if len(list(t, s, nil)[0].Connections) != 2 {
|
||||
t.Fatal("tabs not grouped")
|
||||
}
|
||||
input.TeamID, input.Password = nil, ""
|
||||
input.ScreenID, input.ScreenTokenHash = "3", "hash"
|
||||
send(t, s, input)
|
||||
connection = s.connections[input.ConnectionID]
|
||||
if connection.TeamID != 0 || connection.ScreenID != 3 || connection.GameID != 7 {
|
||||
t.Fatal(connection)
|
||||
}
|
||||
input.ScreenID, input.ScreenTokenHash = "", ""
|
||||
send(t, s, input)
|
||||
connection = s.connections[input.ConnectionID]
|
||||
if connection.TeamID != 0 || connection.ScreenID != 0 || connection.GameID != 0 || connection.Kind != "browser" {
|
||||
t.Fatal(connection)
|
||||
}
|
||||
if len(list(t, s, pointer(7))[0].Connections) != 1 {
|
||||
t.Fatal("filter includes other contexts")
|
||||
}
|
||||
devices[0].Connections[0].GameID = 99
|
||||
if s.connections[input.ConnectionID].GameID != 0 {
|
||||
t.Fatal("list exposes registry memory")
|
||||
}
|
||||
}
|
||||
|
||||
func TestTimeBoundaries(t *testing.T) {
|
||||
s, input := newFixture()
|
||||
start := time.Now()
|
||||
now := start
|
||||
s.now = func() time.Time { return now }
|
||||
send(t, s, input)
|
||||
for _, test := range []struct {
|
||||
age time.Duration
|
||||
exists, connected bool
|
||||
}{
|
||||
{time.Minute - time.Nanosecond, true, true},
|
||||
{time.Minute, true, false},
|
||||
{15*time.Minute - time.Nanosecond, true, false},
|
||||
{15 * time.Minute, false, false},
|
||||
} {
|
||||
now = start.Add(test.age)
|
||||
devices := list(t, s, nil)
|
||||
if (len(devices) > 0) != test.exists {
|
||||
t.Fatalf("retention at %v: %v", test.age, devices)
|
||||
}
|
||||
if test.exists && (devices[0].Connected != test.connected || devices[0].Connections[0].Connected != test.connected) {
|
||||
t.Fatalf("connection at %v", test.age)
|
||||
}
|
||||
}
|
||||
send(t, s, input)
|
||||
now = now.Add(59 * time.Second)
|
||||
tab := input
|
||||
tab.ConnectionID = uuid.NewString()
|
||||
send(t, s, tab)
|
||||
now = now.Add(time.Second)
|
||||
if !list(t, s, nil)[0].Connected {
|
||||
t.Fatal("live tab must keep device connected")
|
||||
}
|
||||
}
|
||||
|
||||
func TestCapacityAndPruning(t *testing.T) {
|
||||
s, input := newFixture()
|
||||
now := time.Now()
|
||||
s.now = func() time.Time { return now }
|
||||
for range maxConnections {
|
||||
input.ConnectionID = uuid.NewString()
|
||||
send(t, s, input)
|
||||
}
|
||||
send(t, s, input)
|
||||
extra := input
|
||||
extra.ConnectionID = uuid.NewString()
|
||||
_, _, err := s.Heartbeat(context.Background(), extra)
|
||||
if !errors.Is(err, ErrFull) || len(s.connections) != maxConnections {
|
||||
t.Fatal(err)
|
||||
}
|
||||
now = now.Add(retainedFor)
|
||||
send(t, s, extra)
|
||||
if len(s.connections) != 1 {
|
||||
t.Fatal("expired records not pruned on update")
|
||||
}
|
||||
}
|
||||
|
||||
func TestInvalidContextDoesNotOverwrite(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
mutate func(*Heartbeat)
|
||||
want error
|
||||
}{
|
||||
{"device UUID", func(h *Heartbeat) { h.DeviceID = "invalid" }, ErrInvalid},
|
||||
{"connection UUID", func(h *Heartbeat) { h.ConnectionID = "" }, ErrInvalid},
|
||||
{"nil UUID", func(h *Heartbeat) { h.DeviceID = uuid.Nil.String() }, ErrInvalid},
|
||||
{"device reassignment", func(h *Heartbeat) { h.DeviceID = uuid.NewString() }, ErrInvalid},
|
||||
{"mixed", func(h *Heartbeat) { h.TeamID = pointer(2); h.Password = "secret"; h.OrganizerGameID = pointer(7) }, ErrInvalid},
|
||||
{"password only", func(h *Heartbeat) { h.Password = "secret" }, ErrInvalid},
|
||||
{"team only", func(h *Heartbeat) { h.TeamID = pointer(2) }, ErrInvalid},
|
||||
{"screen only", func(h *Heartbeat) { h.ScreenID = "3" }, ErrInvalid},
|
||||
{"token only", func(h *Heartbeat) { h.ScreenTokenHash = "hash" }, ErrInvalid},
|
||||
{"screen ID", func(h *Heartbeat) { h.ScreenID = "oops"; h.ScreenTokenHash = "hash" }, ErrInvalid},
|
||||
{"negative game", func(h *Heartbeat) { h.OrganizerGameID = pointer(-1) }, ErrInvalid},
|
||||
{"anonymous organizer", func(h *Heartbeat) { h.OrganizerGameID = pointer(7) }, ErrUnauthorized},
|
||||
{"user organizer", func(h *Heartbeat) { h.OrganizerGameID = pointer(7); h.IsAuthenticated = true }, ErrPermission},
|
||||
{"missing game", func(h *Heartbeat) { h.OrganizerGameID = pointer(8); h.IsAuthenticated = true; h.CanManage = true }, game_service.ErrGameNotFound},
|
||||
{"bad team", func(h *Heartbeat) { h.TeamID = pointer(2); h.Password = "bad" }, team_service.ErrTeamNotFound},
|
||||
{"bad screen", func(h *Heartbeat) { h.ScreenID = "3"; h.ScreenTokenHash = "bad" }, screen_service.ErrScreenNotFound},
|
||||
}
|
||||
for _, test := range tests {
|
||||
t.Run(test.name, func(t *testing.T) {
|
||||
s, input := newFixture()
|
||||
send(t, s, input)
|
||||
before := s.connections[input.ConnectionID]
|
||||
test.mutate(&input)
|
||||
_, _, err := s.Heartbeat(context.Background(), input)
|
||||
if !errors.Is(err, test.want) {
|
||||
t.Fatalf("got %v, want %v", err, test.want)
|
||||
}
|
||||
if s.connections[before.ID] != before || len(s.connections) != 1 {
|
||||
t.Fatal("invalid request modified registry")
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestModeAndReadAccess(t *testing.T) {
|
||||
s, input := newFixture()
|
||||
if _, _, err := s.List(context.Background(), nil, false); !errors.Is(err, ErrPermission) {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if _, _, err := s.List(context.Background(), pointer(0), true); !errors.Is(err, ErrInvalid) {
|
||||
t.Fatal(err)
|
||||
}
|
||||
s.enabled = false
|
||||
enabled, now, err := s.Heartbeat(context.Background(), Heartbeat{})
|
||||
if enabled || now.IsZero() || err != nil || len(s.connections) != 0 {
|
||||
t.Fatal("prod registered device")
|
||||
}
|
||||
if _, _, err = s.List(context.Background(), nil, true); !errors.Is(err, ErrDisabled) {
|
||||
t.Fatal(err)
|
||||
}
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
cancel()
|
||||
if _, _, err = s.Heartbeat(ctx, input); !errors.Is(err, context.Canceled) {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestConcurrentAccess(t *testing.T) {
|
||||
s, input := newFixture()
|
||||
var wg sync.WaitGroup
|
||||
for range 20 {
|
||||
wg.Go(func() {
|
||||
tab := input
|
||||
tab.ConnectionID = uuid.NewString()
|
||||
for range 50 {
|
||||
send(t, s, tab)
|
||||
list(t, s, nil)
|
||||
}
|
||||
})
|
||||
}
|
||||
wg.Wait()
|
||||
if len(list(t, s, nil)[0].Connections) != 20 {
|
||||
t.Fatal("lost connections")
|
||||
}
|
||||
}
|
||||
|
||||
func TestMetadataUTF8(t *testing.T) {
|
||||
for _, agent := range []string{strings.Repeat("x", 1023) + "я", "bad\xffagent"} {
|
||||
s, input := newFixture()
|
||||
input.UserAgent = agent
|
||||
send(t, s, input)
|
||||
stored := list(t, s, nil)[0].Connections[0].UserAgent
|
||||
if len(stored) > 1024 || !utf8.ValidString(stored) {
|
||||
t.Fatal("metadata must serialize safely")
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -377,7 +377,7 @@ func verifyExportedGameReads(t *testing.T, f serviceFixture, worker *game_export
|
||||
games := game_service.NewGameService(f.games, f.teams, f.scenarios, nil, "", actions_repo.NewActionsRepo(f.pool), applications_repo.NewApplicationsRepo(f.pool))
|
||||
mapRepo := game_map_repo.NewGameMapRepo(f.pool)
|
||||
server := app.NewServer(nil, nil, nil, nil, games, nil, nil, nil, nil, nil, nil,
|
||||
screen_service.NewScreenService(screen_sessions_repo.NewScreenSessionsRepo(f.pool)), nil, nil, map_service.NewMapService(mapRepo), nil, worker, nil, nil, nil)
|
||||
screen_service.NewScreenService(screen_sessions_repo.NewScreenSessionsRepo(f.pool)), nil, nil, map_service.NewMapService(mapRepo), nil, worker, nil, nil, nil, nil)
|
||||
ctx := processor_jwt.ContextWithClaims(context.Background(), &processor_jwt.JWTClaims{UserID: f.userID, Roles: []string{"organizer"}})
|
||||
game, err := server.GetGame(ctx, &proto.GetGameReq{ID: int32(f.gameID)})
|
||||
check(t, err)
|
||||
|
||||
@@ -166,3 +166,18 @@ func (s *TeamService) DownloadTeamQR(ctx context.Context, id int) ([]byte, strin
|
||||
data, err := team_qr_pdf.Generate(cards)
|
||||
return data, filename, err
|
||||
}
|
||||
|
||||
// CheckPresence validates team credentials without loading game progress or a scenario.
|
||||
func (s *TeamService) CheckPresence(ctx context.Context, teamID int, password string) (int, error) {
|
||||
team, err := s.teamsRepo.GetTeamByID(ctx, teamID)
|
||||
if errors.Is(err, teams_repo.ErrTeamNotFound) {
|
||||
return 0, ErrTeamNotFound
|
||||
}
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
if team.Password != password || team.ParticipationStatus == "cancelled" {
|
||||
return 0, ErrTeamNotFound
|
||||
}
|
||||
return team.GameID, nil
|
||||
}
|
||||
|
||||
+755
-246
File diff suppressed because it is too large
Load Diff
@@ -36,6 +36,68 @@ var (
|
||||
_ = metadata.Join
|
||||
)
|
||||
|
||||
func request_EveningDetectiveServer_DeviceHeartbeat_0(ctx context.Context, marshaler runtime.Marshaler, client EveningDetectiveServerClient, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) {
|
||||
var (
|
||||
protoReq DeviceHeartbeatReq
|
||||
metadata runtime.ServerMetadata
|
||||
)
|
||||
if err := marshaler.NewDecoder(req.Body).Decode(&protoReq); err != nil && !errors.Is(err, io.EOF) {
|
||||
return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err)
|
||||
}
|
||||
if req.Body != nil {
|
||||
_, _ = io.Copy(io.Discard, req.Body)
|
||||
}
|
||||
msg, err := client.DeviceHeartbeat(ctx, &protoReq, grpc.Header(&metadata.HeaderMD), grpc.Trailer(&metadata.TrailerMD))
|
||||
return msg, metadata, err
|
||||
}
|
||||
|
||||
func local_request_EveningDetectiveServer_DeviceHeartbeat_0(ctx context.Context, marshaler runtime.Marshaler, server EveningDetectiveServerServer, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) {
|
||||
var (
|
||||
protoReq DeviceHeartbeatReq
|
||||
metadata runtime.ServerMetadata
|
||||
)
|
||||
if err := marshaler.NewDecoder(req.Body).Decode(&protoReq); err != nil && !errors.Is(err, io.EOF) {
|
||||
return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err)
|
||||
}
|
||||
msg, err := server.DeviceHeartbeat(ctx, &protoReq)
|
||||
return msg, metadata, err
|
||||
}
|
||||
|
||||
var filter_EveningDetectiveServer_GetDevices_0 = &utilities.DoubleArray{Encoding: map[string]int{}, Base: []int(nil), Check: []int(nil)}
|
||||
|
||||
func request_EveningDetectiveServer_GetDevices_0(ctx context.Context, marshaler runtime.Marshaler, client EveningDetectiveServerClient, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) {
|
||||
var (
|
||||
protoReq GetDevicesReq
|
||||
metadata runtime.ServerMetadata
|
||||
)
|
||||
if req.Body != nil {
|
||||
_, _ = io.Copy(io.Discard, req.Body)
|
||||
}
|
||||
if err := req.ParseForm(); err != nil {
|
||||
return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err)
|
||||
}
|
||||
if err := runtime.PopulateQueryParameters(&protoReq, req.Form, filter_EveningDetectiveServer_GetDevices_0); err != nil {
|
||||
return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err)
|
||||
}
|
||||
msg, err := client.GetDevices(ctx, &protoReq, grpc.Header(&metadata.HeaderMD), grpc.Trailer(&metadata.TrailerMD))
|
||||
return msg, metadata, err
|
||||
}
|
||||
|
||||
func local_request_EveningDetectiveServer_GetDevices_0(ctx context.Context, marshaler runtime.Marshaler, server EveningDetectiveServerServer, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) {
|
||||
var (
|
||||
protoReq GetDevicesReq
|
||||
metadata runtime.ServerMetadata
|
||||
)
|
||||
if err := req.ParseForm(); err != nil {
|
||||
return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err)
|
||||
}
|
||||
if err := runtime.PopulateQueryParameters(&protoReq, req.Form, filter_EveningDetectiveServer_GetDevices_0); err != nil {
|
||||
return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err)
|
||||
}
|
||||
msg, err := server.GetDevices(ctx, &protoReq)
|
||||
return msg, metadata, err
|
||||
}
|
||||
|
||||
func request_EveningDetectiveServer_GetTasks_0(ctx context.Context, marshaler runtime.Marshaler, client EveningDetectiveServerClient, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) {
|
||||
var (
|
||||
protoReq GetTasksReq
|
||||
@@ -4235,6 +4297,46 @@ func local_request_EveningDetectiveServer_SetTeamAnswerScore_0(ctx context.Conte
|
||||
// Note that using this registration option will cause many gRPC library features to stop working. Consider using RegisterEveningDetectiveServerHandlerFromEndpoint instead.
|
||||
// GRPC interceptors will not work for this type of registration. To use interceptors, you must use the "runtime.WithMiddlewares" option in the "runtime.NewServeMux" call.
|
||||
func RegisterEveningDetectiveServerHandlerServer(ctx context.Context, mux *runtime.ServeMux, server EveningDetectiveServerServer) error {
|
||||
mux.Handle(http.MethodPost, pattern_EveningDetectiveServer_DeviceHeartbeat_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) {
|
||||
ctx, cancel := context.WithCancel(req.Context())
|
||||
defer cancel()
|
||||
var stream runtime.ServerTransportStream
|
||||
ctx = grpc.NewContextWithServerTransportStream(ctx, &stream)
|
||||
inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req)
|
||||
annotatedContext, err := runtime.AnnotateIncomingContext(ctx, mux, req, "/crabs.evening_detective_server.EveningDetectiveServer/DeviceHeartbeat", runtime.WithHTTPPathPattern("/api/devices/heartbeat"))
|
||||
if err != nil {
|
||||
runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err)
|
||||
return
|
||||
}
|
||||
resp, md, err := local_request_EveningDetectiveServer_DeviceHeartbeat_0(annotatedContext, inboundMarshaler, server, req, pathParams)
|
||||
md.HeaderMD, md.TrailerMD = metadata.Join(md.HeaderMD, stream.Header()), metadata.Join(md.TrailerMD, stream.Trailer())
|
||||
annotatedContext = runtime.NewServerMetadataContext(annotatedContext, md)
|
||||
if err != nil {
|
||||
runtime.HTTPError(annotatedContext, mux, outboundMarshaler, w, req, err)
|
||||
return
|
||||
}
|
||||
forward_EveningDetectiveServer_DeviceHeartbeat_0(annotatedContext, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...)
|
||||
})
|
||||
mux.Handle(http.MethodGet, pattern_EveningDetectiveServer_GetDevices_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) {
|
||||
ctx, cancel := context.WithCancel(req.Context())
|
||||
defer cancel()
|
||||
var stream runtime.ServerTransportStream
|
||||
ctx = grpc.NewContextWithServerTransportStream(ctx, &stream)
|
||||
inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req)
|
||||
annotatedContext, err := runtime.AnnotateIncomingContext(ctx, mux, req, "/crabs.evening_detective_server.EveningDetectiveServer/GetDevices", runtime.WithHTTPPathPattern("/api/devices"))
|
||||
if err != nil {
|
||||
runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err)
|
||||
return
|
||||
}
|
||||
resp, md, err := local_request_EveningDetectiveServer_GetDevices_0(annotatedContext, inboundMarshaler, server, req, pathParams)
|
||||
md.HeaderMD, md.TrailerMD = metadata.Join(md.HeaderMD, stream.Header()), metadata.Join(md.TrailerMD, stream.Trailer())
|
||||
annotatedContext = runtime.NewServerMetadataContext(annotatedContext, md)
|
||||
if err != nil {
|
||||
runtime.HTTPError(annotatedContext, mux, outboundMarshaler, w, req, err)
|
||||
return
|
||||
}
|
||||
forward_EveningDetectiveServer_GetDevices_0(annotatedContext, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...)
|
||||
})
|
||||
mux.Handle(http.MethodGet, pattern_EveningDetectiveServer_GetTasks_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) {
|
||||
ctx, cancel := context.WithCancel(req.Context())
|
||||
defer cancel()
|
||||
@@ -6455,6 +6557,40 @@ func RegisterEveningDetectiveServerHandler(ctx context.Context, mux *runtime.Ser
|
||||
// doesn't go through the normal gRPC flow (creating a gRPC client etc.) then it will be up to the passed in
|
||||
// "EveningDetectiveServerClient" to call the correct interceptors. This client ignores the HTTP middlewares.
|
||||
func RegisterEveningDetectiveServerHandlerClient(ctx context.Context, mux *runtime.ServeMux, client EveningDetectiveServerClient) error {
|
||||
mux.Handle(http.MethodPost, pattern_EveningDetectiveServer_DeviceHeartbeat_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) {
|
||||
ctx, cancel := context.WithCancel(req.Context())
|
||||
defer cancel()
|
||||
inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req)
|
||||
annotatedContext, err := runtime.AnnotateContext(ctx, mux, req, "/crabs.evening_detective_server.EveningDetectiveServer/DeviceHeartbeat", runtime.WithHTTPPathPattern("/api/devices/heartbeat"))
|
||||
if err != nil {
|
||||
runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err)
|
||||
return
|
||||
}
|
||||
resp, md, err := request_EveningDetectiveServer_DeviceHeartbeat_0(annotatedContext, inboundMarshaler, client, req, pathParams)
|
||||
annotatedContext = runtime.NewServerMetadataContext(annotatedContext, md)
|
||||
if err != nil {
|
||||
runtime.HTTPError(annotatedContext, mux, outboundMarshaler, w, req, err)
|
||||
return
|
||||
}
|
||||
forward_EveningDetectiveServer_DeviceHeartbeat_0(annotatedContext, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...)
|
||||
})
|
||||
mux.Handle(http.MethodGet, pattern_EveningDetectiveServer_GetDevices_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) {
|
||||
ctx, cancel := context.WithCancel(req.Context())
|
||||
defer cancel()
|
||||
inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req)
|
||||
annotatedContext, err := runtime.AnnotateContext(ctx, mux, req, "/crabs.evening_detective_server.EveningDetectiveServer/GetDevices", runtime.WithHTTPPathPattern("/api/devices"))
|
||||
if err != nil {
|
||||
runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err)
|
||||
return
|
||||
}
|
||||
resp, md, err := request_EveningDetectiveServer_GetDevices_0(annotatedContext, inboundMarshaler, client, req, pathParams)
|
||||
annotatedContext = runtime.NewServerMetadataContext(annotatedContext, md)
|
||||
if err != nil {
|
||||
runtime.HTTPError(annotatedContext, mux, outboundMarshaler, w, req, err)
|
||||
return
|
||||
}
|
||||
forward_EveningDetectiveServer_GetDevices_0(annotatedContext, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...)
|
||||
})
|
||||
mux.Handle(http.MethodGet, pattern_EveningDetectiveServer_GetTasks_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) {
|
||||
ctx, cancel := context.WithCancel(req.Context())
|
||||
defer cancel()
|
||||
@@ -8312,6 +8448,8 @@ func RegisterEveningDetectiveServerHandlerClient(ctx context.Context, mux *runti
|
||||
}
|
||||
|
||||
var (
|
||||
pattern_EveningDetectiveServer_DeviceHeartbeat_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1, 2, 2}, []string{"api", "devices", "heartbeat"}, ""))
|
||||
pattern_EveningDetectiveServer_GetDevices_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1}, []string{"api", "devices"}, ""))
|
||||
pattern_EveningDetectiveServer_GetTasks_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1}, []string{"api", "tasks"}, ""))
|
||||
pattern_EveningDetectiveServer_GetTaskAssignees_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1, 2, 2}, []string{"api", "tasks", "assignees"}, ""))
|
||||
pattern_EveningDetectiveServer_CreateTask_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1}, []string{"api", "tasks"}, ""))
|
||||
@@ -8424,6 +8562,8 @@ var (
|
||||
)
|
||||
|
||||
var (
|
||||
forward_EveningDetectiveServer_DeviceHeartbeat_0 = runtime.ForwardResponseMessage
|
||||
forward_EveningDetectiveServer_GetDevices_0 = runtime.ForwardResponseMessage
|
||||
forward_EveningDetectiveServer_GetTasks_0 = runtime.ForwardResponseMessage
|
||||
forward_EveningDetectiveServer_GetTaskAssignees_0 = runtime.ForwardResponseMessage
|
||||
forward_EveningDetectiveServer_CreateTask_0 = runtime.ForwardResponseMessage
|
||||
|
||||
@@ -26,6 +26,8 @@ import (
|
||||
const _ = grpc.SupportPackageIsVersion9
|
||||
|
||||
const (
|
||||
EveningDetectiveServer_DeviceHeartbeat_FullMethodName = "/crabs.evening_detective_server.EveningDetectiveServer/DeviceHeartbeat"
|
||||
EveningDetectiveServer_GetDevices_FullMethodName = "/crabs.evening_detective_server.EveningDetectiveServer/GetDevices"
|
||||
EveningDetectiveServer_GetTasks_FullMethodName = "/crabs.evening_detective_server.EveningDetectiveServer/GetTasks"
|
||||
EveningDetectiveServer_GetTaskAssignees_FullMethodName = "/crabs.evening_detective_server.EveningDetectiveServer/GetTaskAssignees"
|
||||
EveningDetectiveServer_CreateTask_FullMethodName = "/crabs.evening_detective_server.EveningDetectiveServer/CreateTask"
|
||||
@@ -141,6 +143,8 @@ const (
|
||||
//
|
||||
// For semantics around ctx use and closing/ending streaming RPCs, please refer to https://pkg.go.dev/google.golang.org/grpc/?tab=doc#ClientConn.NewStream.
|
||||
type EveningDetectiveServerClient interface {
|
||||
DeviceHeartbeat(ctx context.Context, in *DeviceHeartbeatReq, opts ...grpc.CallOption) (*DeviceHeartbeatRsp, error)
|
||||
GetDevices(ctx context.Context, in *GetDevicesReq, opts ...grpc.CallOption) (*GetDevicesRsp, error)
|
||||
GetTasks(ctx context.Context, in *GetTasksReq, opts ...grpc.CallOption) (*GetTasksRsp, error)
|
||||
GetTaskAssignees(ctx context.Context, in *GetTaskAssigneesReq, opts ...grpc.CallOption) (*GetTaskAssigneesRsp, error)
|
||||
CreateTask(ctx context.Context, in *CreateTaskReq, opts ...grpc.CallOption) (*TaskRsp, error)
|
||||
@@ -272,6 +276,26 @@ func NewEveningDetectiveServerClient(cc grpc.ClientConnInterface) EveningDetecti
|
||||
return &eveningDetectiveServerClient{cc}
|
||||
}
|
||||
|
||||
func (c *eveningDetectiveServerClient) DeviceHeartbeat(ctx context.Context, in *DeviceHeartbeatReq, opts ...grpc.CallOption) (*DeviceHeartbeatRsp, error) {
|
||||
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
|
||||
out := new(DeviceHeartbeatRsp)
|
||||
err := c.cc.Invoke(ctx, EveningDetectiveServer_DeviceHeartbeat_FullMethodName, in, out, cOpts...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
|
||||
func (c *eveningDetectiveServerClient) GetDevices(ctx context.Context, in *GetDevicesReq, opts ...grpc.CallOption) (*GetDevicesRsp, error) {
|
||||
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
|
||||
out := new(GetDevicesRsp)
|
||||
err := c.cc.Invoke(ctx, EveningDetectiveServer_GetDevices_FullMethodName, in, out, cOpts...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
|
||||
func (c *eveningDetectiveServerClient) GetTasks(ctx context.Context, in *GetTasksReq, opts ...grpc.CallOption) (*GetTasksRsp, error) {
|
||||
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
|
||||
out := new(GetTasksRsp)
|
||||
@@ -1366,6 +1390,8 @@ func (c *eveningDetectiveServerClient) SetTeamAnswerScore(ctx context.Context, i
|
||||
// All implementations must embed UnimplementedEveningDetectiveServerServer
|
||||
// for forward compatibility.
|
||||
type EveningDetectiveServerServer interface {
|
||||
DeviceHeartbeat(context.Context, *DeviceHeartbeatReq) (*DeviceHeartbeatRsp, error)
|
||||
GetDevices(context.Context, *GetDevicesReq) (*GetDevicesRsp, error)
|
||||
GetTasks(context.Context, *GetTasksReq) (*GetTasksRsp, error)
|
||||
GetTaskAssignees(context.Context, *GetTaskAssigneesReq) (*GetTaskAssigneesRsp, error)
|
||||
CreateTask(context.Context, *CreateTaskReq) (*TaskRsp, error)
|
||||
@@ -1497,6 +1523,12 @@ type EveningDetectiveServerServer interface {
|
||||
// pointer dereference when methods are called.
|
||||
type UnimplementedEveningDetectiveServerServer struct{}
|
||||
|
||||
func (UnimplementedEveningDetectiveServerServer) DeviceHeartbeat(context.Context, *DeviceHeartbeatReq) (*DeviceHeartbeatRsp, error) {
|
||||
return nil, status.Error(codes.Unimplemented, "method DeviceHeartbeat not implemented")
|
||||
}
|
||||
func (UnimplementedEveningDetectiveServerServer) GetDevices(context.Context, *GetDevicesReq) (*GetDevicesRsp, error) {
|
||||
return nil, status.Error(codes.Unimplemented, "method GetDevices not implemented")
|
||||
}
|
||||
func (UnimplementedEveningDetectiveServerServer) GetTasks(context.Context, *GetTasksReq) (*GetTasksRsp, error) {
|
||||
return nil, status.Error(codes.Unimplemented, "method GetTasks not implemented")
|
||||
}
|
||||
@@ -1846,6 +1878,42 @@ func RegisterEveningDetectiveServerServer(s grpc.ServiceRegistrar, srv EveningDe
|
||||
s.RegisterService(&EveningDetectiveServer_ServiceDesc, srv)
|
||||
}
|
||||
|
||||
func _EveningDetectiveServer_DeviceHeartbeat_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
|
||||
in := new(DeviceHeartbeatReq)
|
||||
if err := dec(in); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if interceptor == nil {
|
||||
return srv.(EveningDetectiveServerServer).DeviceHeartbeat(ctx, in)
|
||||
}
|
||||
info := &grpc.UnaryServerInfo{
|
||||
Server: srv,
|
||||
FullMethod: EveningDetectiveServer_DeviceHeartbeat_FullMethodName,
|
||||
}
|
||||
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
|
||||
return srv.(EveningDetectiveServerServer).DeviceHeartbeat(ctx, req.(*DeviceHeartbeatReq))
|
||||
}
|
||||
return interceptor(ctx, in, info, handler)
|
||||
}
|
||||
|
||||
func _EveningDetectiveServer_GetDevices_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
|
||||
in := new(GetDevicesReq)
|
||||
if err := dec(in); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if interceptor == nil {
|
||||
return srv.(EveningDetectiveServerServer).GetDevices(ctx, in)
|
||||
}
|
||||
info := &grpc.UnaryServerInfo{
|
||||
Server: srv,
|
||||
FullMethod: EveningDetectiveServer_GetDevices_FullMethodName,
|
||||
}
|
||||
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
|
||||
return srv.(EveningDetectiveServerServer).GetDevices(ctx, req.(*GetDevicesReq))
|
||||
}
|
||||
return interceptor(ctx, in, info, handler)
|
||||
}
|
||||
|
||||
func _EveningDetectiveServer_GetTasks_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
|
||||
in := new(GetTasksReq)
|
||||
if err := dec(in); err != nil {
|
||||
@@ -3815,6 +3883,14 @@ var EveningDetectiveServer_ServiceDesc = grpc.ServiceDesc{
|
||||
ServiceName: "crabs.evening_detective_server.EveningDetectiveServer",
|
||||
HandlerType: (*EveningDetectiveServerServer)(nil),
|
||||
Methods: []grpc.MethodDesc{
|
||||
{
|
||||
MethodName: "DeviceHeartbeat",
|
||||
Handler: _EveningDetectiveServer_DeviceHeartbeat_Handler,
|
||||
},
|
||||
{
|
||||
MethodName: "GetDevices",
|
||||
Handler: _EveningDetectiveServer_GetDevices_Handler,
|
||||
},
|
||||
{
|
||||
MethodName: "GetTasks",
|
||||
Handler: _EveningDetectiveServer_GetTasks_Handler,
|
||||
|
||||
Reference in New Issue
Block a user