autobrr/pkg/jsonrpc/jsonrpc.go
ze0s 1ae8624e05
build(deps): bump Go to 1.24 (#2060)
* build(deps): bump Go to 1.24
2025-05-01 16:36:55 +02:00

267 lines
6.5 KiB
Go

// Copyright (c) 2021 - 2025, Ludvig Lundgren and the autobrr contributors.
// SPDX-License-Identifier: GPL-2.0-or-later
package jsonrpc
import (
"bytes"
"context"
"encoding/json"
"net/http"
"reflect"
"strconv"
"github.com/autobrr/autobrr/pkg/errors"
)
type Client interface {
Call(method string, params ...interface{}) (*RPCResponse, error)
CallCtx(ctx context.Context, method string, params ...interface{}) (*RPCResponse, error)
}
type RPCRequest struct {
JsonRPC string `json:"jsonrpc"`
Method string `json:"method"`
Params interface{} `json:"params"`
ID int `json:"id"`
}
func NewRequest(method string, params ...interface{}) *RPCRequest {
return &RPCRequest{
JsonRPC: "2.0",
Method: method,
Params: Params(params...),
ID: 0,
}
}
type RPCResponse struct {
JsonRPC string `json:"jsonrpc"`
Result interface{} `json:"result,omitempty"`
Error *RPCError `json:"error,omitempty"`
ID int `json:"id"`
}
type RPCError struct {
Code int `json:"code"`
Message string `json:"message"`
Data interface{} `json:"data,omitempty"`
}
func (e *RPCError) Error() string {
return strconv.Itoa(e.Code) + ":" + e.Message
}
type HTTPError struct {
Code int
err error
}
func (e *HTTPError) Error() string {
return e.err.Error()
}
type rpcClient struct {
endpoint string
httpClient *http.Client
headers map[string]string
// HTTP Basic auth username
basicUser string
// HTTP Basic auth password
basicPass string
}
type ClientOpts struct {
HTTPClient *http.Client
Headers map[string]string
// HTTP Basic auth username
BasicUser string
// HTTP Basic auth password
BasicPass string
}
type RPCResponses []*RPCResponse
func NewClient(endpoint string) Client {
return NewClientWithOpts(endpoint, nil)
}
func NewClientWithOpts(endpoint string, opts *ClientOpts) Client {
c := &rpcClient{
endpoint: endpoint,
httpClient: &http.Client{},
headers: make(map[string]string),
}
if opts == nil {
return c
}
if opts.HTTPClient != nil {
c.httpClient = opts.HTTPClient
}
if opts.Headers != nil {
for k, v := range opts.Headers {
c.headers[k] = v
}
}
c.basicUser = opts.BasicUser
c.basicPass = opts.BasicPass
return c
}
func (c *rpcClient) Call(method string, params ...interface{}) (*RPCResponse, error) {
request := RPCRequest{
ID: 1,
JsonRPC: "2.0",
Method: method,
Params: Params(params...),
}
return c.doCall(context.TODO(), request)
}
func (c *rpcClient) CallCtx(ctx context.Context, method string, params ...interface{}) (*RPCResponse, error) {
request := RPCRequest{
ID: 1,
JsonRPC: "2.0",
Method: method,
Params: Params(params...),
}
return c.doCall(ctx, request)
}
func (c *rpcClient) newRequest(ctx context.Context, req interface{}) (*http.Request, error) {
body, err := json.Marshal(req)
if err != nil {
return nil, errors.Wrap(err, "could not marshal request")
}
request, err := http.NewRequestWithContext(ctx, "POST", c.endpoint, bytes.NewReader(body))
if err != nil {
return nil, errors.Wrap(err, "error creating request")
}
request.Header.Set("Content-Type", "application/json")
request.Header.Set("Accept", "application/json")
// set basic auth
if c.basicUser != "" && c.basicPass != "" {
request.SetBasicAuth(c.basicUser, c.basicPass)
}
for k, v := range c.headers {
request.Header.Set(k, v)
}
return request, nil
}
func (c *rpcClient) doCall(ctx context.Context, request RPCRequest) (*RPCResponse, error) {
httpRequest, err := c.newRequest(ctx, request)
if err != nil {
return nil, errors.Wrap(err, "could not create rpc http request")
}
httpResponse, err := c.httpClient.Do(httpRequest)
if err != nil {
return nil, errors.Wrap(err, "error during rpc http request")
}
defer httpResponse.Body.Close()
var rpcResponse *RPCResponse
decoder := json.NewDecoder(httpResponse.Body)
decoder.DisallowUnknownFields()
decoder.UseNumber()
err = decoder.Decode(&rpcResponse)
if err != nil {
if httpResponse.StatusCode >= 400 {
return nil, errors.Wrap(err, "rpc call %v() on %v status code: %v. Could not decode body to rpc response", request.Method, httpRequest.URL.String(), httpResponse.StatusCode)
}
// if res.StatusCode == http.StatusUnauthorized {
// return nil, errors.New("unauthorized: bad credentials")
// } else if res.StatusCode == http.StatusForbidden {
// return nil, nil
// } else if res.StatusCode == http.StatusTooManyRequests {
// return nil, nil
// } else if res.StatusCode == http.StatusBadRequest {
// return nil, nil
// } else if res.StatusCode == http.StatusNotFound {
// return nil, nil
// } else if res.StatusCode == http.StatusServiceUnavailable {
// return nil, nil
// }
}
if rpcResponse == nil {
return nil, errors.New("rpc call %v() on %v status code: %v. rpc response missing", request.Method, httpRequest.URL.String(), httpResponse.StatusCode)
}
return rpcResponse, nil
}
func Params(params ...interface{}) interface{} {
var finalParams interface{}
// if params was nil skip this and p stays nil
if params != nil {
switch len(params) {
case 0: // no parameters were provided, do nothing so finalParam is nil and will be omitted
case 1: // one param was provided, use it directly as is, or wrap primitive types in array
if params[0] != nil {
var typeOf reflect.Type
// traverse until nil or not a pointer type
for typeOf = reflect.TypeOf(params[0]); typeOf != nil && typeOf.Kind() == reflect.Ptr; typeOf = typeOf.Elem() {
}
if typeOf != nil {
// now check if we can directly marshal the type or if it must be wrapped in an array
switch typeOf.Kind() {
// for these types we just do nothing, since value of p is already unwrapped from the array params
case reflect.Struct:
finalParams = params[0]
case reflect.Array:
finalParams = params[0]
case reflect.Slice:
finalParams = params[0]
case reflect.Interface:
finalParams = params[0]
case reflect.Map:
finalParams = params[0]
default: // everything else must stay in an array (int, string, etc)
finalParams = params
}
}
} else {
finalParams = params
}
default: // if more than one parameter was provided it should be treated as an array
finalParams = params
}
}
return finalParams
}
func (r *RPCResponse) GetObject(toType interface{}) error {
js, err := json.Marshal(r.Result)
if err != nil {
return errors.Wrap(err, "could not marshal object")
}
if err = json.Unmarshal(js, toType); err != nil {
return errors.Wrap(err, "could not unmarshal object")
}
return nil
}