feat: custom-api multiple API queries

This commit is contained in:
Ralph Ocdol 2025-03-03 02:51:17 +08:00
parent 6c8859863a
commit 8baa07d440
3 changed files with 142 additions and 45 deletions

View file

@ -1290,11 +1290,34 @@ Examples:
#### Properties
| Name | Type | Required | Default |
| ---- | ---- | -------- | ------- |
| url | string | yes | |
| url | string | yes, unless `api-queries` is set | |
| headers | key (string) & value (string) | no | |
| frameless | boolean | no | false |
| template | string | yes | |
| parameters | key & value | no | |
| api-queries | list of urls, parameters & headers | no | |
> [!NOTE]
>
> `api-queries` will override `url`, `headers` and `parameters`
> since it also provides its own options
##### `api-queries`
A list of API queries, the name set will be the name of the json returned
```yaml
api-queries:
sample-data1:
url: https://domain.com/api
parameters:
foo: bar
headers:
x-api-key: your-api-key
Accept: application/json
sample-data2:
url: https://another-domain.com/api
```
see [custom-api docs](./custom-api.md#api-queries)
##### `url`
The URL to fetch the data from. It must be accessible from the server that Glance is running on.

View file

@ -240,6 +240,28 @@ Output:
Other operations include `add`, `mul`, and `div`.
<hr>
#### API-Queries
JSON response
```json
{
"sample-data1": {
"title": "My Title",
"content": "My Content"
},
"sample-data2": [
{
"name": "John Doe"
},
{
"name": "Jane Doe"
}
]
}
```
<hr>
In some instances, you may want to know the status code of the response. This can be done using the following:

View file

@ -3,6 +3,7 @@ package glance
import (
"bytes"
"context"
"encoding/json"
"errors"
"fmt"
"html/template"
@ -17,28 +18,70 @@ import (
)
var customAPIWidgetTemplate = mustParseTemplate("custom-api.html", "widget-base.html")
var customRandomKeyForSingleRequest = fmt.Sprintf("%x", time.Now().UnixNano())
type customAPIWidget struct {
widgetBase `yaml:",inline"`
ApiQueries map[string]apiQueries `yaml:"api-queries"`
URL string `yaml:"url"`
Template string `yaml:"template"`
Frameless bool `yaml:"frameless"`
Headers map[string]string `yaml:"headers"`
Parameters queryParametersField `yaml:"parameters"`
APIRequest map[string]*http.Request `yaml:"-"`
compiledTemplate *template.Template `yaml:"-"`
CompiledHTML template.HTML `yaml:"-"`
}
type apiQueries struct {
URL string `yaml:"url"`
Template string `yaml:"template"`
Frameless bool `yaml:"frameless"`
Headers map[string]string `yaml:"headers"`
Parameters queryParametersField `yaml:"parameters"`
APIRequest *http.Request `yaml:"-"`
compiledTemplate *template.Template `yaml:"-"`
CompiledHTML template.HTML `yaml:"-"`
}
func (widget *customAPIWidget) initialize() error {
widget.withTitle("Custom API").withCacheDuration(1 * time.Hour)
if widget.URL == "" {
return errors.New("URL is required")
}
widget.APIRequest = make(map[string]*http.Request)
if len(widget.ApiQueries) != 0 {
for object, query := range widget.ApiQueries {
if query.URL == "" {
return errors.New("URL for each query is required")
}
req, err := http.NewRequest(http.MethodGet, query.URL, nil)
if err != nil {
return err
}
if widget.Template == "" {
return errors.New("template is required")
req.URL.RawQuery = query.Parameters.toQueryString()
for key, value := range query.Headers {
req.Header.Add(key, value)
}
widget.APIRequest[object] = req
}
} else {
if widget.URL == "" {
return errors.New("URL is required")
}
if widget.Template == "" {
return errors.New("template is required")
}
req, err := http.NewRequest(http.MethodGet, widget.URL, nil)
if err != nil {
return err
}
req.URL.RawQuery = widget.Parameters.toQueryString()
for key, value := range widget.Headers {
req.Header.Add(key, value)
}
widget.APIRequest[customRandomKeyForSingleRequest] = req
}
compiledTemplate, err := template.New("").Funcs(customAPITemplateFuncs).Parse(widget.Template)
@ -48,19 +91,6 @@ func (widget *customAPIWidget) initialize() error {
widget.compiledTemplate = compiledTemplate
req, err := http.NewRequest(http.MethodGet, widget.URL, nil)
if err != nil {
return err
}
req.URL.RawQuery = widget.Parameters.toQueryString()
for key, value := range widget.Headers {
req.Header.Add(key, value)
}
widget.APIRequest = req
return nil
}
@ -77,36 +107,58 @@ func (widget *customAPIWidget) Render() template.HTML {
return widget.renderTemplate(widget, customAPIWidgetTemplate)
}
func fetchAndParseCustomAPI(req *http.Request, tmpl *template.Template) (template.HTML, error) {
func fetchAndParseCustomAPI(requests map[string]*http.Request, tmpl *template.Template) (template.HTML, error) {
emptyBody := template.HTML("")
resp, err := defaultHTTPClient.Do(req)
if err != nil {
return emptyBody, err
}
defer resp.Body.Close()
bodyBytes, err := io.ReadAll(resp.Body)
if err != nil {
return emptyBody, err
}
body := strings.TrimSpace(string(bodyBytes))
if body != "" && !gjson.Valid(body) {
truncatedBody, isTruncated := limitStringLength(body, 100)
if isTruncated {
truncatedBody += "... <truncated>"
var resp *http.Response
var err error
body := make(map[string]string)
mergedBody := "{}"
for key, req := range requests {
resp, err = defaultHTTPClient.Do(req)
if err != nil {
return emptyBody, err
}
defer resp.Body.Close()
slog.Error("Invalid response JSON in custom API widget", "url", req.URL.String(), "body", truncatedBody)
return emptyBody, errors.New("invalid response JSON")
bodyBytes, err := io.ReadAll(resp.Body)
if err != nil {
return emptyBody, err
}
body[key] = strings.TrimSpace(string(bodyBytes))
if body[key] != "" && !gjson.Valid(body[key]) {
truncatedBody, isTruncated := limitStringLength(body[key], 100)
if isTruncated {
truncatedBody += "... <truncated>"
}
slog.Error("Invalid response JSON in custom API widget", "url", req.URL.String(), key, truncatedBody)
return emptyBody, errors.New("invalid response JSON")
}
}
if jsonBody, exists := body[customRandomKeyForSingleRequest]; exists {
mergedBody = jsonBody
} else {
mergedMap := make(map[string]json.RawMessage)
for key, jsonBody := range body {
if !gjson.Valid(jsonBody) {
continue
}
mergedMap[key] = json.RawMessage(jsonBody)
}
if len(mergedMap) > 0 {
bytes, _ := json.Marshal(mergedMap)
mergedBody = string(bytes)
}
}
var templateBuffer bytes.Buffer
data := customAPITemplateData{
JSON: decoratedGJSONResult{gjson.Parse(body)},
JSON: decoratedGJSONResult{gjson.Parse(mergedBody)},
Response: resp,
}