From 342ef90cbe8cd3cd90c57063d7660857961415ae Mon Sep 17 00:00:00 2001 From: Svilen Markov <7613769+svilenmarkov@users.noreply.github.com> Date: Fri, 31 May 2024 02:03:21 +0100 Subject: [PATCH] Add extension widget --- internal/assets/templates.go | 1 + internal/assets/templates/extension.html | 5 ++ internal/feed/extension.go | 97 ++++++++++++++++++++++++ internal/widget/extension.go | 59 ++++++++++++++ internal/widget/widget.go | 2 + 5 files changed, 164 insertions(+) create mode 100644 internal/assets/templates/extension.html create mode 100644 internal/feed/extension.go create mode 100644 internal/widget/extension.go diff --git a/internal/assets/templates.go b/internal/assets/templates.go index cc91b49..53ae871 100644 --- a/internal/assets/templates.go +++ b/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{ diff --git a/internal/assets/templates/extension.html b/internal/assets/templates/extension.html new file mode 100644 index 0000000..e5794c8 --- /dev/null +++ b/internal/assets/templates/extension.html @@ -0,0 +1,5 @@ +{{ template "widget-base.html" . }} + +{{ define "widget-content" }} +{{ .Extension.Content }} +{{ end }} diff --git a/internal/feed/extension.go b/internal/feed/extension.go new file mode 100644 index 0000000..3aa499a --- /dev/null +++ b/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 +} diff --git a/internal/widget/extension.go b/internal/widget/extension.go new file mode 100644 index 0000000..547bbfe --- /dev/null +++ b/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 +} diff --git a/internal/widget/widget.go b/internal/widget/widget.go index bee61ec..e16af92 100644 --- a/internal/widget/widget.go +++ b/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) }