mirror of https://github.com/glanceapp/glance.git
Add custom API widget
parent
2dd5b29303
commit
84a7f90129
@ -0,0 +1,7 @@
|
|||||||
|
{{ template "widget-base.html" . }}
|
||||||
|
|
||||||
|
{{ define "widget-content-classes" }}{{ if .Frameless }}widget-content-frameless{{ end }}{{ end }}
|
||||||
|
|
||||||
|
{{ define "widget-content" }}
|
||||||
|
{{ .CompiledHTML }}
|
||||||
|
{{ end }}
|
||||||
@ -0,0 +1,148 @@
|
|||||||
|
package feed
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"errors"
|
||||||
|
"html/template"
|
||||||
|
"io"
|
||||||
|
"log/slog"
|
||||||
|
"net/http"
|
||||||
|
|
||||||
|
"github.com/glanceapp/glance/internal/assets"
|
||||||
|
"github.com/tidwall/gjson"
|
||||||
|
)
|
||||||
|
|
||||||
|
func FetchAndParseCustomAPI(req *http.Request, tmpl *template.Template) (template.HTML, error) {
|
||||||
|
emptyBody := template.HTML("")
|
||||||
|
|
||||||
|
resp, err := defaultClient.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 := string(bodyBytes)
|
||||||
|
|
||||||
|
if !gjson.Valid(body) {
|
||||||
|
truncatedBody, isTruncated := limitStringLength(body, 100)
|
||||||
|
if isTruncated {
|
||||||
|
truncatedBody += "... <truncated>"
|
||||||
|
}
|
||||||
|
|
||||||
|
slog.Error("invalid response JSON in custom API widget", "URL", req.URL.String(), "body", truncatedBody)
|
||||||
|
return emptyBody, errors.New("invalid response JSON")
|
||||||
|
}
|
||||||
|
|
||||||
|
var templateBuffer bytes.Buffer
|
||||||
|
|
||||||
|
data := CustomAPITemplateData{
|
||||||
|
JSON: DecoratedGJSONResult{gjson.Parse(body)},
|
||||||
|
Response: resp,
|
||||||
|
}
|
||||||
|
|
||||||
|
err = tmpl.Execute(&templateBuffer, &data)
|
||||||
|
if err != nil {
|
||||||
|
return emptyBody, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return template.HTML(templateBuffer.String()), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
type DecoratedGJSONResult struct {
|
||||||
|
gjson.Result
|
||||||
|
}
|
||||||
|
|
||||||
|
type CustomAPITemplateData struct {
|
||||||
|
JSON DecoratedGJSONResult
|
||||||
|
Response *http.Response
|
||||||
|
}
|
||||||
|
|
||||||
|
func GJsonResultArrayToDecoratedResultArray(results []gjson.Result) []DecoratedGJSONResult {
|
||||||
|
decoratedResults := make([]DecoratedGJSONResult, len(results))
|
||||||
|
|
||||||
|
for i, result := range results {
|
||||||
|
decoratedResults[i] = DecoratedGJSONResult{result}
|
||||||
|
}
|
||||||
|
|
||||||
|
return decoratedResults
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *DecoratedGJSONResult) Array(key string) []DecoratedGJSONResult {
|
||||||
|
if key == "" {
|
||||||
|
return GJsonResultArrayToDecoratedResultArray(r.Result.Array())
|
||||||
|
}
|
||||||
|
|
||||||
|
return GJsonResultArrayToDecoratedResultArray(r.Get(key).Array())
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *DecoratedGJSONResult) String(key string) string {
|
||||||
|
if key == "" {
|
||||||
|
return r.Result.String()
|
||||||
|
}
|
||||||
|
|
||||||
|
return r.Get(key).String()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *DecoratedGJSONResult) Int(key string) int64 {
|
||||||
|
if key == "" {
|
||||||
|
return r.Result.Int()
|
||||||
|
}
|
||||||
|
|
||||||
|
return r.Get(key).Int()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *DecoratedGJSONResult) Float(key string) float64 {
|
||||||
|
if key == "" {
|
||||||
|
return r.Result.Float()
|
||||||
|
}
|
||||||
|
|
||||||
|
return r.Get(key).Float()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *DecoratedGJSONResult) Bool(key string) bool {
|
||||||
|
if key == "" {
|
||||||
|
return r.Result.Bool()
|
||||||
|
}
|
||||||
|
|
||||||
|
return r.Get(key).Bool()
|
||||||
|
}
|
||||||
|
|
||||||
|
var CustomAPITemplateFuncs = func() template.FuncMap {
|
||||||
|
funcs := template.FuncMap{
|
||||||
|
"toFloat": func(a int64) float64 {
|
||||||
|
return float64(a)
|
||||||
|
},
|
||||||
|
"toInt": func(a float64) int64 {
|
||||||
|
return int64(a)
|
||||||
|
},
|
||||||
|
"mathexpr": func(left float64, op string, right float64) float64 {
|
||||||
|
if right == 0 {
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
switch op {
|
||||||
|
case "+":
|
||||||
|
return left + right
|
||||||
|
case "-":
|
||||||
|
return left - right
|
||||||
|
case "*":
|
||||||
|
return left * right
|
||||||
|
case "/":
|
||||||
|
return left / right
|
||||||
|
default:
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for key, value := range assets.GlobalTemplateFunctions {
|
||||||
|
funcs[key] = value
|
||||||
|
}
|
||||||
|
|
||||||
|
return funcs
|
||||||
|
}()
|
||||||
@ -0,0 +1,70 @@
|
|||||||
|
package widget
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"html/template"
|
||||||
|
"net/http"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/glanceapp/glance/internal/assets"
|
||||||
|
"github.com/glanceapp/glance/internal/feed"
|
||||||
|
)
|
||||||
|
|
||||||
|
type CustomApi struct {
|
||||||
|
widgetBase `yaml:",inline"`
|
||||||
|
URL string `yaml:"url"`
|
||||||
|
Template string `yaml:"template"`
|
||||||
|
Frameless bool `yaml:"frameless"`
|
||||||
|
Headers map[string]OptionalEnvString `yaml:"headers"`
|
||||||
|
APIRequest *http.Request `yaml:"-"`
|
||||||
|
compiledTemplate *template.Template `yaml:"-"`
|
||||||
|
CompiledHTML template.HTML `yaml:"-"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (widget *CustomApi) Initialize() error {
|
||||||
|
widget.withTitle("Custom API").withCacheDuration(1 * time.Hour)
|
||||||
|
|
||||||
|
if widget.URL == "" {
|
||||||
|
return errors.New("URL is required for the custom API widget")
|
||||||
|
}
|
||||||
|
|
||||||
|
if widget.Template == "" {
|
||||||
|
return errors.New("template is required for the custom API widget")
|
||||||
|
}
|
||||||
|
|
||||||
|
compiledTemplate, err := template.New("").Funcs(feed.CustomAPITemplateFuncs).Parse(widget.Template)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed parsing custom API widget template: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
widget.compiledTemplate = compiledTemplate
|
||||||
|
|
||||||
|
req, err := http.NewRequest(http.MethodGet, widget.URL, nil)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
for key, value := range widget.Headers {
|
||||||
|
req.Header.Add(key, value.String())
|
||||||
|
}
|
||||||
|
|
||||||
|
widget.APIRequest = req
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (widget *CustomApi) Update(ctx context.Context) {
|
||||||
|
compiledHTML, err := feed.FetchAndParseCustomAPI(widget.APIRequest, widget.compiledTemplate)
|
||||||
|
if !widget.canContinueUpdateAfterHandlingErr(err) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
widget.CompiledHTML = compiledHTML
|
||||||
|
}
|
||||||
|
|
||||||
|
func (widget *CustomApi) Render() template.HTML {
|
||||||
|
return widget.render(widget, assets.CustomAPITemplate)
|
||||||
|
}
|
||||||
Loading…
Reference in New Issue