mirror of
https://github.com/akyaiy/GoSally-mvp.git
synced 2026-01-03 09:12:25 +00:00
Compare commits
7 Commits
0151c3f68a
...
bf5e136dc9
| Author | SHA1 | Date | |
|---|---|---|---|
| bf5e136dc9 | |||
| 86cdc9adf2 | |||
| f09afdb850 | |||
| efbca43f27 | |||
| a0451aa8a0 | |||
| 7608bcfed3 | |||
| c62710a7d0 |
@@ -13,14 +13,11 @@ type serversApiVer string
|
|||||||
|
|
||||||
type ServerApiContract interface {
|
type ServerApiContract interface {
|
||||||
GetVersion() string
|
GetVersion() string
|
||||||
Handle(w http.ResponseWriter, r *http.Request, req rpc.RPCRequest)
|
Handle(r *http.Request, req *rpc.RPCRequest) *rpc.RPCResponse
|
||||||
}
|
}
|
||||||
|
|
||||||
// GeneralServer implements the GeneralServerApiContract and serves as a router for different API versions.
|
// GeneralServer implements the GeneralServerApiContract and serves as a router for different API versions.
|
||||||
type GatewayServer struct {
|
type GatewayServer struct {
|
||||||
w http.ResponseWriter
|
|
||||||
r *http.Request
|
|
||||||
|
|
||||||
// servers holds the registered servers by their API version.
|
// servers holds the registered servers by their API version.
|
||||||
// The key is the version string, and the value is the server implementing GeneralServerApi
|
// The key is the version string, and the value is the server implementing GeneralServerApi
|
||||||
servers map[serversApiVer]ServerApiContract
|
servers map[serversApiVer]ServerApiContract
|
||||||
|
|||||||
@@ -5,16 +5,17 @@ import (
|
|||||||
"io"
|
"io"
|
||||||
"log/slog"
|
"log/slog"
|
||||||
"net/http"
|
"net/http"
|
||||||
"net/http/httptest"
|
"sync"
|
||||||
|
|
||||||
"github.com/akyaiy/GoSally-mvp/internal/server/rpc"
|
"github.com/akyaiy/GoSally-mvp/internal/server/rpc"
|
||||||
)
|
)
|
||||||
|
|
||||||
func (gs *GatewayServer) Handle(w http.ResponseWriter, r *http.Request) {
|
func (gs *GatewayServer) Handle(w http.ResponseWriter, r *http.Request) {
|
||||||
var req rpc.RPCRequest
|
w.Header().Set("Content-Type", "application/json")
|
||||||
body, err := io.ReadAll(r.Body)
|
body, err := io.ReadAll(r.Body)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
rpc.WriteRouterError(w, http.StatusBadRequest, &rpc.RPCError{
|
w.WriteHeader(http.StatusBadRequest)
|
||||||
|
rpc.WriteError(w, &rpc.RPCResponse{
|
||||||
JSONRPC: rpc.JSONRPCVersion,
|
JSONRPC: rpc.JSONRPCVersion,
|
||||||
ID: nil,
|
ID: nil,
|
||||||
Error: map[string]any{
|
Error: map[string]any{
|
||||||
@@ -26,55 +27,70 @@ func (gs *GatewayServer) Handle(w http.ResponseWriter, r *http.Request) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := json.Unmarshal(body, &req); err != nil {
|
// determine if the JSON-RPC request is a batch
|
||||||
rpc.WriteRouterError(w, http.StatusBadRequest, &rpc.RPCError{
|
var batch []rpc.RPCRequest
|
||||||
JSONRPC: rpc.JSONRPCVersion,
|
json.Unmarshal(body, &batch)
|
||||||
ID: nil,
|
var single rpc.RPCRequest
|
||||||
Error: map[string]any{
|
if batch == nil {
|
||||||
"code": rpc.ErrParseError,
|
if err := json.Unmarshal(body, &single); err != nil {
|
||||||
"message": rpc.ErrParseErrorS,
|
w.WriteHeader(http.StatusBadRequest)
|
||||||
},
|
rpc.WriteError(w, &rpc.RPCResponse{
|
||||||
})
|
JSONRPC: rpc.JSONRPCVersion,
|
||||||
gs.log.Info("invalid request received", slog.String("issue", rpc.ErrParseErrorS))
|
ID: nil,
|
||||||
|
Error: map[string]any{
|
||||||
|
"code": rpc.ErrParseError,
|
||||||
|
"message": rpc.ErrParseErrorS,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
gs.log.Info("invalid request received", slog.String("issue", rpc.ErrParseErrorS))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
resp := gs.Route(r, &single)
|
||||||
|
rpc.WriteResponse(w, resp)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if req.JSONRPC != rpc.JSONRPCVersion {
|
// handle batch
|
||||||
rpc.WriteRouterError(w, http.StatusBadRequest, &rpc.RPCError{
|
responses := make(chan rpc.RPCResponse, len(batch))
|
||||||
JSONRPC: rpc.JSONRPCVersion,
|
var wg sync.WaitGroup
|
||||||
ID: req.ID,
|
for _, m := range batch {
|
||||||
Error: map[string]any{
|
wg.Add(1)
|
||||||
"code": rpc.ErrInvalidRequest,
|
go func(req rpc.RPCRequest) {
|
||||||
"message": rpc.ErrInvalidRequestS,
|
defer wg.Done()
|
||||||
},
|
res := gs.Route(r, &req)
|
||||||
})
|
if res != nil {
|
||||||
gs.log.Info("invalid request received", slog.String("issue", rpc.ErrInvalidRequestS), slog.String("requested-version", req.JSONRPC))
|
responses <- *res
|
||||||
return
|
}
|
||||||
|
}(m)
|
||||||
}
|
}
|
||||||
|
wg.Wait()
|
||||||
|
close(responses)
|
||||||
|
|
||||||
gs.Route(w, r, req)
|
var result []rpc.RPCResponse
|
||||||
|
for res := range responses {
|
||||||
|
result = append(result, res)
|
||||||
|
}
|
||||||
|
if len(result) > 0 {
|
||||||
|
json.NewEncoder(w).Encode(result)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (gs *GatewayServer) Route(w http.ResponseWriter, r *http.Request, req rpc.RPCRequest) {
|
func (gs *GatewayServer) Route(r *http.Request, req *rpc.RPCRequest) *rpc.RPCResponse {
|
||||||
server, ok := gs.servers[serversApiVer(req.Params.ContextVersion)]
|
if req.JSONRPC != rpc.JSONRPCVersion {
|
||||||
if !ok {
|
gs.log.Info("invalid request received", slog.String("issue", rpc.ErrInvalidRequestS), slog.String("requested-version", req.JSONRPC))
|
||||||
rpc.WriteRouterError(w, http.StatusBadRequest, &rpc.RPCError{
|
return rpc.NewError(rpc.ErrInvalidRequest, rpc.ErrInvalidRequestS, req.ID)
|
||||||
JSONRPC: rpc.JSONRPCVersion,
|
|
||||||
ID: req.ID,
|
|
||||||
Error: map[string]any{
|
|
||||||
"code": rpc.ErrContextVersion,
|
|
||||||
"message": rpc.ErrContextVersionS,
|
|
||||||
},
|
|
||||||
})
|
|
||||||
gs.log.Info("invalid request received", slog.String("issue", rpc.ErrContextVersionS), slog.String("requested-version", req.Params.ContextVersion))
|
|
||||||
return
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
server, ok := gs.servers[serversApiVer(req.ContextVersion)]
|
||||||
|
if !ok {
|
||||||
|
gs.log.Info("invalid request received", slog.String("issue", rpc.ErrContextVersionS), slog.String("requested-version", req.ContextVersion))
|
||||||
|
return rpc.NewError(rpc.ErrContextVersion, rpc.ErrContextVersionS, req.ID)
|
||||||
|
}
|
||||||
|
|
||||||
|
resp := server.Handle(r, req)
|
||||||
// checks if request is notification
|
// checks if request is notification
|
||||||
if req.ID == nil {
|
if req.ID == nil {
|
||||||
rr := httptest.NewRecorder()
|
return nil
|
||||||
server.Handle(rr, r, req)
|
|
||||||
return
|
|
||||||
}
|
}
|
||||||
server.Handle(w, r, req)
|
return resp
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,27 +1,20 @@
|
|||||||
package rpc
|
package rpc
|
||||||
|
|
||||||
type RPCRequest struct {
|
import "encoding/json"
|
||||||
JSONRPC string `json:"jsonrpc"`
|
|
||||||
ID any `json:"id"`
|
|
||||||
Method string `json:"method"`
|
|
||||||
Params RPCRequestParams `json:"params"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type RPCRequestParams struct {
|
type RPCRequest struct {
|
||||||
ContextVersion string `json:"context-version"`
|
JSONRPC string `json:"jsonrpc"`
|
||||||
Method map[string]any `json:"method-params"`
|
ID *json.RawMessage `json:"id,omitempty"`
|
||||||
|
Method string `json:"method"`
|
||||||
|
Params any `json:"params,omitempty"`
|
||||||
|
ContextVersion string `json:"context-version,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type RPCResponse struct {
|
type RPCResponse struct {
|
||||||
JSONRPC string `json:"jsonrpc"`
|
JSONRPC string `json:"jsonrpc"`
|
||||||
ID any `json:"id"`
|
ID *json.RawMessage `json:"id"`
|
||||||
Result any `json:"result"`
|
Result any `json:"result,omitempty"`
|
||||||
}
|
Error any `json:"error,omitempty"`
|
||||||
|
|
||||||
type RPCError struct {
|
|
||||||
JSONRPC string `json:"jsonrpc"`
|
|
||||||
ID any `json:"id"`
|
|
||||||
Error any `json:"error"`
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const (
|
const (
|
||||||
|
|||||||
22
internal/server/rpc/responsers.go
Normal file
22
internal/server/rpc/responsers.go
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
package rpc
|
||||||
|
|
||||||
|
import "encoding/json"
|
||||||
|
|
||||||
|
func NewError(code int, message string, id *json.RawMessage) *RPCResponse {
|
||||||
|
return &RPCResponse{
|
||||||
|
JSONRPC: JSONRPCVersion,
|
||||||
|
ID: id,
|
||||||
|
Error: map[string]any{
|
||||||
|
"code": code,
|
||||||
|
"message": message,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewResponse(result any, id *json.RawMessage) *RPCResponse {
|
||||||
|
return &RPCResponse{
|
||||||
|
JSONRPC: JSONRPCVersion,
|
||||||
|
ID: id,
|
||||||
|
Result: result,
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -2,32 +2,22 @@ package rpc
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"fmt"
|
|
||||||
"net/http"
|
"net/http"
|
||||||
)
|
)
|
||||||
|
|
||||||
func write(w http.ResponseWriter, status int, msg any) error {
|
func write(w http.ResponseWriter, msg *RPCResponse) error {
|
||||||
w.Header().Set("Content-Type", "application/json")
|
data, err := json.Marshal(msg)
|
||||||
w.WriteHeader(status)
|
if err != nil {
|
||||||
|
|
||||||
switch m := msg.(type) {
|
|
||||||
case RPCError, *RPCError,
|
|
||||||
RPCResponse, *RPCResponse:
|
|
||||||
data, err := json.Marshal(m)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
_, err = w.Write(data)
|
|
||||||
return err
|
return err
|
||||||
default:
|
|
||||||
return fmt.Errorf("invalid RPC structure: %T", msg)
|
|
||||||
}
|
}
|
||||||
|
_, err = w.Write(data)
|
||||||
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
func WriteRouterError(w http.ResponseWriter, status int, errm *RPCError) error {
|
func WriteError(w http.ResponseWriter, errm *RPCResponse) error {
|
||||||
return write(w, status, errm)
|
return write(w, errm)
|
||||||
}
|
}
|
||||||
|
|
||||||
func WriteResponse(w http.ResponseWriter, response *RPCResponse) error {
|
func WriteResponse(w http.ResponseWriter, response *RPCResponse) error {
|
||||||
return write(w, http.StatusOK, response)
|
return write(w, response)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,12 +6,8 @@ import (
|
|||||||
"github.com/akyaiy/GoSally-mvp/internal/server/rpc"
|
"github.com/akyaiy/GoSally-mvp/internal/server/rpc"
|
||||||
)
|
)
|
||||||
|
|
||||||
func (h *HandlerV1) Handle(w http.ResponseWriter, r *http.Request, req rpc.RPCRequest) {
|
func (h *HandlerV1) Handle(r *http.Request, req *rpc.RPCRequest) *rpc.RPCResponse {
|
||||||
rpc.WriteResponse(w, &rpc.RPCResponse{
|
return rpc.NewResponse("Hi", req.ID) // test answer to make sure everything works
|
||||||
JSONRPC: rpc.JSONRPCVersion,
|
|
||||||
ID: req.ID,
|
|
||||||
Result: "Hi",
|
|
||||||
}) // test answer to make sure everything works
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// func (h *HandlerV1) Handle(w http.ResponseWriter, r *http.Request) {
|
// func (h *HandlerV1) Handle(w http.ResponseWriter, r *http.Request) {
|
||||||
|
|||||||
@@ -1,133 +0,0 @@
|
|||||||
package sv1
|
|
||||||
|
|
||||||
/*
|
|
||||||
import (
|
|
||||||
"encoding/json"
|
|
||||||
"log/slog"
|
|
||||||
"net/http"
|
|
||||||
"os"
|
|
||||||
"path/filepath"
|
|
||||||
"strings"
|
|
||||||
|
|
||||||
"github.com/akyaiy/GoSally-mvp/core/config"
|
|
||||||
"github.com/akyaiy/GoSally-mvp/core/corestate"
|
|
||||||
"github.com/akyaiy/GoSally-mvp/core/utils"
|
|
||||||
"github.com/go-chi/chi/v5"
|
|
||||||
)
|
|
||||||
|
|
||||||
// The function processes the HTTP request and returns a list of available commands.
|
|
||||||
func (h *HandlerV1) HandleList(w http.ResponseWriter, r *http.Request) {
|
|
||||||
uuid16, err := utils.NewUUID(int(config.UUIDLength))
|
|
||||||
if err != nil {
|
|
||||||
h.log.Error("Failed to generate UUID",
|
|
||||||
slog.String("error", err.Error()))
|
|
||||||
|
|
||||||
if err := utils.WriteJSONError(w, http.StatusInternalServerError, "failed to generate UUID: "+err.Error()); err != nil {
|
|
||||||
h.log.Error("Failed to write JSON", slog.String("err", err.Error()))
|
|
||||||
}
|
|
||||||
return
|
|
||||||
}
|
|
||||||
log := h.log.With(
|
|
||||||
slog.Group("request",
|
|
||||||
slog.String("version", h.GetVersion()),
|
|
||||||
slog.String("url", r.URL.String()),
|
|
||||||
slog.String("method", r.Method),
|
|
||||||
),
|
|
||||||
slog.Group("connection",
|
|
||||||
slog.String("connection-uuid", uuid16),
|
|
||||||
slog.String("remote", r.RemoteAddr),
|
|
||||||
),
|
|
||||||
)
|
|
||||||
log.Info("Received request")
|
|
||||||
type ComMeta struct {
|
|
||||||
Description string `json:"Description"`
|
|
||||||
Arguments map[string]string `json:"Arguments,omitempty"`
|
|
||||||
}
|
|
||||||
var (
|
|
||||||
files []os.DirEntry
|
|
||||||
commands = make(map[string]ComMeta)
|
|
||||||
cmdsProcessed = make(map[string]bool)
|
|
||||||
)
|
|
||||||
|
|
||||||
if files, err = os.ReadDir(h.cfg.ComDir); err != nil {
|
|
||||||
log.Error("Failed to read commands directory",
|
|
||||||
slog.String("error", err.Error()))
|
|
||||||
|
|
||||||
if err := utils.WriteJSONError(w, http.StatusInternalServerError, "failed to read commands directory: "+err.Error()); err != nil {
|
|
||||||
h.log.Error("Failed to write JSON", slog.String("err", err.Error()))
|
|
||||||
}
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
apiVer := chi.URLParam(r, "ver")
|
|
||||||
|
|
||||||
// Сначала ищем версионные
|
|
||||||
for _, file := range files {
|
|
||||||
if file.IsDir() || filepath.Ext(file.Name()) != ".lua" {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
cmdFull := file.Name()[:len(file.Name())-4]
|
|
||||||
cmdParts := strings.SplitN(cmdFull, "?", 2)
|
|
||||||
cmdName := cmdParts[0]
|
|
||||||
|
|
||||||
if !h.allowedCmd.MatchString(string([]rune(cmdName)[0])) {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
if !h.listAllowedCmd.MatchString(cmdName) {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
if len(cmdParts) == 2 && cmdParts[1] == apiVer {
|
|
||||||
description, _ := h.extractDescriptionStatic(filepath.Join(h.cfg.ComDir, file.Name()))
|
|
||||||
if description == "" {
|
|
||||||
description = "description missing"
|
|
||||||
}
|
|
||||||
commands[cmdName] = ComMeta{Description: description}
|
|
||||||
cmdsProcessed[cmdName] = true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Потом фоллбеки
|
|
||||||
for _, file := range files {
|
|
||||||
if file.IsDir() || filepath.Ext(file.Name()) != ".lua" {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
cmdFull := file.Name()[:len(file.Name())-4]
|
|
||||||
cmdParts := strings.SplitN(cmdFull, "?", 2)
|
|
||||||
cmdName := cmdParts[0]
|
|
||||||
|
|
||||||
if !h.allowedCmd.MatchString(string([]rune(cmdName)[0])) {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
if !h.listAllowedCmd.MatchString(cmdName) {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
if cmdsProcessed[cmdName] {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
if len(cmdParts) == 1 {
|
|
||||||
description, _ := h.extractDescriptionStatic(filepath.Join(h.cfg.ComDir, file.Name()))
|
|
||||||
if description == "" {
|
|
||||||
description = "description missing"
|
|
||||||
}
|
|
||||||
commands[cmdName] = ComMeta{Description: description}
|
|
||||||
cmdsProcessed[cmdName] = true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
log.Debug("Command list prepared")
|
|
||||||
|
|
||||||
log.Info("Session completed")
|
|
||||||
uuid32, _ := corestate.GetNodeUUID(filepath.Join(config.MetaDir, "uuid"))
|
|
||||||
response := ResponseFormat{
|
|
||||||
ResponsibleAgentUUID: uuid32,
|
|
||||||
RequestedCommand: "list",
|
|
||||||
Response: commands,
|
|
||||||
}
|
|
||||||
w.Header().Set("Content-Type", "application/json")
|
|
||||||
if err := json.NewEncoder(w).Encode(response); err != nil {
|
|
||||||
h.log.Error("Failed to write JSON error response",
|
|
||||||
slog.String("error", err.Error()))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
*/
|
|
||||||
@@ -1,20 +0,0 @@
|
|||||||
package sv1
|
|
||||||
|
|
||||||
// PETTI - Go Sally Protocol for Exchanging Technical Tasks and Information
|
|
||||||
|
|
||||||
type PettiRequest struct {
|
|
||||||
PettiVer string `json:"PettiVer"`
|
|
||||||
PackageType struct {
|
|
||||||
Request string `json:"Request"`
|
|
||||||
} `json:"PackageType"`
|
|
||||||
Payload map[string]any `json:"Payload"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type PettiResponse struct {
|
|
||||||
PettiVer string `json:"PettiVer"`
|
|
||||||
PackageType struct {
|
|
||||||
AnswerOf string `json:"AnswerOf"`
|
|
||||||
} `json:"PackageType"`
|
|
||||||
ResponsibleAgentUUID string `json:"ResponsibleAgentUUID"`
|
|
||||||
Payload map[string]any `json:"Payload"`
|
|
||||||
}
|
|
||||||
@@ -5,29 +5,6 @@ import (
|
|||||||
"os"
|
"os"
|
||||||
)
|
)
|
||||||
|
|
||||||
// func (h *HandlerV1) errNotFound(w http.ResponseWriter, r *http.Request) {
|
|
||||||
// utils.WriteJSONError(h.w, http.StatusBadRequest, "invalid request")
|
|
||||||
// h.log.Error("HTTP request error",
|
|
||||||
// slog.String("remote", h.r.RemoteAddr),
|
|
||||||
// slog.String("method", h.r.Method),
|
|
||||||
// slog.String("url", h.r.URL.String()),
|
|
||||||
// slog.Int("status", http.StatusBadRequest))
|
|
||||||
// }
|
|
||||||
|
|
||||||
// func (h *HandlerV1) extractDescriptionStatic(path string) (string, error) {
|
|
||||||
// data, err := os.ReadFile(path)
|
|
||||||
// if err != nil {
|
|
||||||
// return "", err
|
|
||||||
// }
|
|
||||||
|
|
||||||
// re := regexp.MustCompile(`---\s*#description\s*=\s*"([^"]+)"`)
|
|
||||||
// m := re.FindStringSubmatch(string(data))
|
|
||||||
// if len(m) <= 0 {
|
|
||||||
// return "", nil
|
|
||||||
// }
|
|
||||||
// return m[1], nil
|
|
||||||
// }
|
|
||||||
|
|
||||||
func (h *HandlerV1) comMatch(ver string, comName string) string {
|
func (h *HandlerV1) comMatch(ver string, comName string) string {
|
||||||
files, err := os.ReadDir(h.cfg.ComDir)
|
files, err := os.ReadDir(h.cfg.ComDir)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|||||||
Reference in New Issue
Block a user