瀏覽代碼

Add extension widget

Svilen Markov 1 年之前
父節點
當前提交
342ef90cbe

+ 1 - 0
internal/assets/templates.go

@@ -36,6 +36,7 @@ var (
 	TwitchChannelsTemplate        = compileTemplate("twitch-channels.html", "widget-base.html")
 	RepositoryTemplate            = compileTemplate("repository.html", "widget-base.html")
 	SearchTemplate                = compileTemplate("search.html", "widget-base.html")
+	ExtensionTemplate             = compileTemplate("extension.html", "widget-base.html")
 )
 
 var globalTemplateFunctions = template.FuncMap{

+ 5 - 0
internal/assets/templates/extension.html

@@ -0,0 +1,5 @@
+{{ template "widget-base.html" . }}
+
+{{ define "widget-content" }}
+{{ .Extension.Content }}
+{{ end }}

+ 97 - 0
internal/feed/extension.go

@@ -0,0 +1,97 @@
+package feed
+
+import (
+	"fmt"
+	"html"
+	"html/template"
+	"io"
+	"log/slog"
+	"net/http"
+	"net/url"
+)
+
+type ExtensionType int
+
+const (
+	ExtensionContentHTML    ExtensionType = iota
+	ExtensionContentUnknown               = iota
+)
+
+var ExtensionStringToType = map[string]ExtensionType{
+	"html": ExtensionContentHTML,
+}
+
+const (
+	ExtensionHeaderTitle       = "Widget-Title"
+	ExtensionHeaderContentType = "Widget-Content-Type"
+)
+
+type ExtensionRequestOptions struct {
+	URL        string            `yaml:"url"`
+	Parameters map[string]string `yaml:"parameters"`
+	AllowHtml  bool              `yaml:"allow-potentially-dangerous-html"`
+}
+
+type Extension struct {
+	Title   string
+	Content template.HTML
+}
+
+func convertExtensionContent(options ExtensionRequestOptions, content []byte, contentType ExtensionType) template.HTML {
+	switch contentType {
+	case ExtensionContentHTML:
+		if options.AllowHtml {
+			return template.HTML(content)
+		}
+
+		fallthrough
+	default:
+		return template.HTML(html.EscapeString(string(content)))
+	}
+}
+
+func FetchExtension(options ExtensionRequestOptions) (Extension, error) {
+	request, _ := http.NewRequest("GET", options.URL, nil)
+
+	query := url.Values{}
+
+	for key, value := range options.Parameters {
+		query.Set(key, value)
+	}
+
+	request.URL.RawQuery = query.Encode()
+
+	response, err := http.DefaultClient.Do(request)
+
+	if err != nil {
+		slog.Error("failed fetching extension", "error", err, "url", options.URL)
+		return Extension{}, fmt.Errorf("%w: request failed: %w", ErrNoContent, err)
+	}
+
+	defer response.Body.Close()
+
+	body, err := io.ReadAll(response.Body)
+
+	if err != nil {
+		slog.Error("failed reading response body of extension", "error", err, "url", options.URL)
+		return Extension{}, fmt.Errorf("%w: could not read body: %w", ErrNoContent, err)
+	}
+
+	extension := Extension{}
+
+	if response.Header.Get(ExtensionHeaderTitle) == "" {
+		extension.Title = "Extension"
+	} else {
+		extension.Title = response.Header.Get(ExtensionHeaderTitle)
+	}
+
+	contentType, ok := ExtensionStringToType[response.Header.Get(ExtensionHeaderContentType)]
+
+	if !ok {
+		contentType = ExtensionContentUnknown
+	}
+
+	extension.Content = convertExtensionContent(options, body, contentType)
+
+	return extension, nil
+}

+ 59 - 0
internal/widget/extension.go

@@ -0,0 +1,59 @@
+package widget
+
+import (
+	"context"
+	"errors"
+	"html/template"
+	"net/url"
+	"time"
+
+	"github.com/glanceapp/glance/internal/assets"
+	"github.com/glanceapp/glance/internal/feed"
+)
+
+type Extension struct {
+	widgetBase `yaml:",inline"`
+	URL        string            `yaml:"url"`
+	Parameters map[string]string `yaml:"parameters"`
+	AllowHtml  bool              `yaml:"allow-potentially-dangerous-html"`
+	Extension  feed.Extension    `yaml:"-"`
+	cachedHTML template.HTML     `yaml:"-"`
+}
+
+func (widget *Extension) Initialize() error {
+	widget.withTitle("Extension").withCacheDuration(time.Minute * 30)
+
+	if widget.URL == "" {
+		return errors.New("no extension URL specified")
+	}
+
+	_, err := url.Parse(widget.URL)
+
+	if err != nil {
+		return err
+	}
+
+	return nil
+}
+
+func (widget *Extension) Update(ctx context.Context) {
+	extension, err := feed.FetchExtension(feed.ExtensionRequestOptions{
+		URL:        widget.URL,
+		Parameters: widget.Parameters,
+		AllowHtml:  widget.AllowHtml,
+	})
+
+	widget.canContinueUpdateAfterHandlingErr(err)
+
+	widget.Extension = extension
+
+	if extension.Title != "" {
+		widget.Title = extension.Title
+	}
+
+	widget.cachedHTML = widget.render(widget, assets.ExtensionTemplate)
+}
+
+func (widget *Extension) Render() template.HTML {
+	return widget.cachedHTML
+}

+ 2 - 0
internal/widget/widget.go

@@ -53,6 +53,8 @@ func New(widgetType string) (Widget, error) {
 		return &Repository{}, nil
 	case "search":
 		return &Search{}, nil
+	case "extension":
+		return &Extension{}, nil
 	default:
 		return nil, fmt.Errorf("unknown widget type: %s", widgetType)
 	}