package main import ( "bytes" "embed" "encoding/json" "fmt" "io/fs" "strconv" "text/template" ) //go:embed template/*.tmpl var templatesFS embed.FS type templateRenderer struct { templates map[string]*template.Template } type templateExtra struct { Name string Value string } type backupTemplateData struct { Title string Asset string BackupSizeInMB float64 Extra []templateExtra } type updateTemplateData struct { Host string Asset string Time float64 } type errorTemplateData struct { Caller string Message string Critical bool Extra []templateExtra } func newTemplateRenderer() (*templateRenderer, error) { return newTemplateRendererFromFS(templatesFS) } func newTemplateRendererFromFS(templateFS fs.FS) (*templateRenderer, error) { functions := template.FuncMap{ "formatSize": formatSize, "formatSeconds": formatSeconds, "json": jsonLiteral, } templates := make(map[string]*template.Template, 3) for _, name := range []string{"backup", "update", "error"} { contents, err := fs.ReadFile(templateFS, "template/"+name+".tmpl") if err != nil { return nil, err } parsed, err := template.New(name).Funcs(functions).Parse(string(contents)) if err != nil { return nil, err } templates[name] = parsed } return &templateRenderer{templates: templates}, nil } func (r *templateRenderer) renderBackup(request backupRequest) (discordPayload, error) { return r.render("backup", backupTemplateData{ Title: *request.Title, Asset: *request.Asset, BackupSizeInMB: *request.BackupSizeInMB, Extra: templateExtras(request.Extra), }) } func (r *templateRenderer) renderUpdate(request updateRequest) (discordPayload, error) { return r.render("update", updateTemplateData{ Host: *request.Host, Asset: *request.Asset, Time: *request.Time, }) } func (r *templateRenderer) renderError(request errorRequest) (discordPayload, error) { return r.render("error", errorTemplateData{ Caller: *request.Caller, Message: *request.Message, Critical: *request.Critical, Extra: templateExtras(request.Extra), }) } func (r *templateRenderer) render(name string, data any) (discordPayload, error) { parsed, ok := r.templates[name] if !ok { return discordPayload{}, fmt.Errorf("unknown template %q", name) } var rendered bytes.Buffer if err := parsed.Execute(&rendered, data); err != nil { return discordPayload{}, err } var payload discordPayload if err := json.Unmarshal(rendered.Bytes(), &payload); err != nil { return discordPayload{}, fmt.Errorf("template produced invalid JSON: %w", err) } normalizeDiscordPayload(&payload) return payload, nil } func templateExtras(extra extraRequests) []templateExtra { result := make([]templateExtra, 0, len(extra)) for _, field := range extra { result = append(result, templateExtra{Name: *field.Name, Value: *field.Value}) } return result } func jsonLiteral(value any) (string, error) { encoded, err := json.Marshal(value) return string(encoded), err } func formatSize(size float64) string { if size >= 1024 { return fmt.Sprintf("%.2f GiB", size/1024) } return fmt.Sprintf("%.2f MiB", size) } func formatSeconds(seconds float64) string { return strconv.FormatFloat(seconds, 'f', -1, 64) + " seconds" }