custom-api.go 2.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148
  1. package feed
  2. import (
  3. "bytes"
  4. "errors"
  5. "html/template"
  6. "io"
  7. "log/slog"
  8. "net/http"
  9. "github.com/glanceapp/glance/internal/assets"
  10. "github.com/tidwall/gjson"
  11. )
  12. func FetchAndParseCustomAPI(req *http.Request, tmpl *template.Template) (template.HTML, error) {
  13. emptyBody := template.HTML("")
  14. resp, err := defaultClient.Do(req)
  15. if err != nil {
  16. return emptyBody, err
  17. }
  18. defer resp.Body.Close()
  19. bodyBytes, err := io.ReadAll(resp.Body)
  20. if err != nil {
  21. return emptyBody, err
  22. }
  23. body := string(bodyBytes)
  24. if !gjson.Valid(body) {
  25. truncatedBody, isTruncated := limitStringLength(body, 100)
  26. if isTruncated {
  27. truncatedBody += "... <truncated>"
  28. }
  29. slog.Error("invalid response JSON in custom API widget", "URL", req.URL.String(), "body", truncatedBody)
  30. return emptyBody, errors.New("invalid response JSON")
  31. }
  32. var templateBuffer bytes.Buffer
  33. data := CustomAPITemplateData{
  34. JSON: DecoratedGJSONResult{gjson.Parse(body)},
  35. Response: resp,
  36. }
  37. err = tmpl.Execute(&templateBuffer, &data)
  38. if err != nil {
  39. return emptyBody, err
  40. }
  41. return template.HTML(templateBuffer.String()), nil
  42. }
  43. type DecoratedGJSONResult struct {
  44. gjson.Result
  45. }
  46. type CustomAPITemplateData struct {
  47. JSON DecoratedGJSONResult
  48. Response *http.Response
  49. }
  50. func GJsonResultArrayToDecoratedResultArray(results []gjson.Result) []DecoratedGJSONResult {
  51. decoratedResults := make([]DecoratedGJSONResult, len(results))
  52. for i, result := range results {
  53. decoratedResults[i] = DecoratedGJSONResult{result}
  54. }
  55. return decoratedResults
  56. }
  57. func (r *DecoratedGJSONResult) Array(key string) []DecoratedGJSONResult {
  58. if key == "" {
  59. return GJsonResultArrayToDecoratedResultArray(r.Result.Array())
  60. }
  61. return GJsonResultArrayToDecoratedResultArray(r.Get(key).Array())
  62. }
  63. func (r *DecoratedGJSONResult) String(key string) string {
  64. if key == "" {
  65. return r.Result.String()
  66. }
  67. return r.Get(key).String()
  68. }
  69. func (r *DecoratedGJSONResult) Int(key string) int64 {
  70. if key == "" {
  71. return r.Result.Int()
  72. }
  73. return r.Get(key).Int()
  74. }
  75. func (r *DecoratedGJSONResult) Float(key string) float64 {
  76. if key == "" {
  77. return r.Result.Float()
  78. }
  79. return r.Get(key).Float()
  80. }
  81. func (r *DecoratedGJSONResult) Bool(key string) bool {
  82. if key == "" {
  83. return r.Result.Bool()
  84. }
  85. return r.Get(key).Bool()
  86. }
  87. var CustomAPITemplateFuncs = func() template.FuncMap {
  88. funcs := template.FuncMap{
  89. "toFloat": func(a int64) float64 {
  90. return float64(a)
  91. },
  92. "toInt": func(a float64) int64 {
  93. return int64(a)
  94. },
  95. "mathexpr": func(left float64, op string, right float64) float64 {
  96. if right == 0 {
  97. return 0
  98. }
  99. switch op {
  100. case "+":
  101. return left + right
  102. case "-":
  103. return left - right
  104. case "*":
  105. return left * right
  106. case "/":
  107. return left / right
  108. default:
  109. return 0
  110. }
  111. },
  112. }
  113. for key, value := range assets.GlobalTemplateFunctions {
  114. funcs[key] = value
  115. }
  116. return funcs
  117. }()