website/pkg/templates/template_funcs.go
Noah Petherbridge 20d04fc370 Admin Transparency Page
* Add a transparency page where regular user accounts can list the roles and
  permissions that an admin user has access to. It is available by clicking on
  the "Admin" badge on that user's profile page.
* Add additional admin scopes to lock down more functionality:
  * User feedback and reports
  * Change logs
  * User notes and admin notes
* Add friendly descriptions to what all the scopes mean in practice.
* Don't show admin notification badges to admins who aren't allowed to act on
  those notifications.
* Update the admin dashboard page and documentation for admins.
2024-05-09 15:50:46 -07:00

323 lines
8.7 KiB
Go

package templates
import (
"bytes"
"encoding/json"
"fmt"
"html/template"
"net/http"
"net/url"
"sort"
"strings"
"time"
"code.nonshy.com/nonshy/website/pkg/config"
"code.nonshy.com/nonshy/website/pkg/markdown"
"code.nonshy.com/nonshy/website/pkg/models"
"code.nonshy.com/nonshy/website/pkg/photo"
"code.nonshy.com/nonshy/website/pkg/session"
"code.nonshy.com/nonshy/website/pkg/utility"
"golang.org/x/text/language"
"golang.org/x/text/message"
)
// Generics
type Number interface {
int | int64 | uint64 | float32 | float64
}
// TemplateFuncs available to all pages.
func TemplateFuncs(r *http.Request) template.FuncMap {
return template.FuncMap{
"InputCSRF": InputCSRF(r),
"SincePrettyCoarse": SincePrettyCoarse(),
"FormatNumberShort": FormatNumberShort(),
"FormatNumberCommas": FormatNumberCommas(),
"ComputeAge": utility.Age,
"Split": strings.Split,
"ToMarkdown": ToMarkdown,
"ToJSON": ToJSON,
"ToHTML": ToHTML,
"PhotoURL": photo.URLPath,
"Now": time.Now,
"RunTime": RunTime,
"PrettyTitle": func() template.HTML {
return template.HTML(fmt.Sprintf(
`<strong style="color: #0077FF">non</strong>` +
`<strong style="color: #FF77FF">shy</strong>`,
))
},
"PrettyTitleShort": func() template.HTML {
return template.HTML(`<strong style="color: #0077FF">n</strong>` +
`<strong style="color: #FF77FF">s</strong>`,
)
},
"PrettyCircle": func() template.HTML {
return template.HTML(
`<span style="color: #0077ff">I</span><span style="color: #1c77ff">n</span><span style="color: #3877ff">n</span><span style="color: #5477ff">e</span><span style="color: #7077ff">r</span><span style="color: #8c77ff"> </span><span style="color: #aa77ff">c</span><span style="color: #b877ff">i</span><span style="color: #c677ff">r</span><span style="color: #d477ff">c</span><span style="color: #e277ff">l</span><span style="color: #f077ff">e</span>`,
)
},
"Pluralize": Pluralize[int],
"Pluralize64": Pluralize[int64],
"PluralizeU64": Pluralize[uint64],
"Substring": Substring,
"TrimEllipses": TrimEllipses,
"IterRange": IterRange,
"SubtractInt": SubtractInt,
"UrlEncode": UrlEncode,
"QueryPlus": QueryPlus(r),
"SimplePager": SimplePager(r),
"HasSuffix": strings.HasSuffix,
// Test if a photo should be blurred ({{BlurExplicit .Photo}})
"BlurExplicit": BlurExplicit(r),
// Get a description for an admin scope (e.g. for transparency page).
"AdminScopeDescription": config.AdminScopeDescription,
}
}
// InputCSRF returns the HTML snippet for a CSRF token hidden input field.
func InputCSRF(r *http.Request) func() template.HTML {
return func() template.HTML {
ctx := r.Context()
if token, ok := ctx.Value(session.CSRFKey).(string); ok {
return template.HTML(fmt.Sprintf(
`<input type="hidden" name="%s" value="%s">`,
config.CSRFInputName,
token,
))
} else {
return template.HTML(`[CSRF middleware error]`)
}
}
}
// RunTime returns the elapsed time between the HTTP request start and now, as a formatted string.
func RunTime(r *http.Request) string {
if rt, ok := r.Context().Value(session.RequestTimeKey).(time.Time); ok {
duration := time.Since(rt)
return duration.Round(time.Millisecond).String()
}
return "ERROR"
}
// BlurExplicit returns true if the current user has the blur_explicit setting on and the given Photo is Explicit.
func BlurExplicit(r *http.Request) func(*models.Photo) bool {
return func(photo *models.Photo) bool {
if !photo.Explicit {
return false
}
currentUser, err := session.CurrentUser(r)
if err != nil {
return false
}
return currentUser.GetProfileField("blur_explicit") == "true"
}
}
// SincePrettyCoarse formats a time.Duration in plain English. Intended for "joined 2 months ago" type
// strings - returns the coarsest level of granularity.
func SincePrettyCoarse() func(time.Time) template.HTML {
return func(since time.Time) template.HTML {
return template.HTML(utility.FormatDurationCoarse(time.Since(since)))
}
}
// FormatNumberShort will format any integer number into a short representation.
func FormatNumberShort() func(v interface{}) template.HTML {
return func(v interface{}) template.HTML {
var number int64
switch t := v.(type) {
case int:
number = int64(t)
case int64:
number = int64(t)
case uint:
number = int64(t)
case uint64:
number = int64(t)
case float32:
number = int64(t)
case float64:
number = int64(t)
default:
return template.HTML("#INVALID#")
}
return template.HTML(utility.FormatNumberShort(number))
}
}
// FormatNumberCommas will pretty print a long number by adding commas.
func FormatNumberCommas() func(v interface{}) template.HTML {
return func(v interface{}) template.HTML {
var number int64
switch t := v.(type) {
case int:
number = int64(t)
case int64:
number = int64(t)
case uint:
number = int64(t)
case uint64:
number = int64(t)
case float32:
number = int64(t)
case float64:
number = int64(t)
default:
return template.HTML("#INVALID#")
}
p := message.NewPrinter(language.English)
return template.HTML(p.Sprintf("%d", number))
}
}
// ToMarkdown renders input text as Markdown.
func ToMarkdown(input string) template.HTML {
return template.HTML(markdown.Render(input))
}
// ToHTML renders input text as trusted HTML code.
func ToHTML(input string) template.HTML {
return template.HTML(input)
}
// ToJSON will stringify any json-serializable object.
func ToJSON(v any) template.JS {
bin, err := json.Marshal(v)
if err != nil {
return template.JS(err.Error())
}
return template.JS(string(bin))
}
// Pluralize text based on a quantity number. Provide up to 2 labels for the
// singular and plural cases, or the defaults are "", "s"
func Pluralize[V Number](count V, labels ...string) string {
if len(labels) < 2 {
labels = []string{"", "s"}
}
if count == 1 {
return labels[0]
}
return labels[1]
}
// Substring safely returns the first N characters of a string.
func Substring(value string, n int) string {
if n > len(value) {
return value
}
return value[:n]
}
// TrimEllipses is like Substring but will add an ellipses if truncated.
func TrimEllipses(value string, n int) string {
if n > len(value) {
return value
}
return value[:n] + "…"
}
// IterRange returns a list of integers useful for pagination.
func IterRange(start, n int) []int {
var result = []int{}
for i := start; i <= n; i++ {
result = append(result, i)
}
return result
}
// SubtractInt subtracts two numbers.
func SubtractInt(a, b int) int {
return a - b
}
// UrlEncode escapes a series of values (joined with no delimiter)
func UrlEncode(values ...interface{}) string {
var result string
for _, value := range values {
result += url.QueryEscape(fmt.Sprintf("%v", value))
}
return result
}
// QueryPlus takes the current request's query parameters and upserts them with new values.
//
// Use it like: {{QueryPlus "page" .NextPage}}
//
// Returns the query string sans the ? prefix, like "key1=value1&key2=value2"
func QueryPlus(r *http.Request) func(...interface{}) template.URL {
return func(upsert ...interface{}) template.URL {
// Get current parameters.
// Note: COPY them from r.Form so we don't accidentally modify r.Form.
var params = map[string][]string{}
for k, v := range r.Form {
params[k] = v
}
// Mix in the incoming fields.
for i := 0; i < len(upsert); i += 2 {
var (
key = fmt.Sprintf("%v", upsert[i])
value interface{}
)
if len(upsert) > i {
value = upsert[i+1]
}
params[key] = []string{fmt.Sprintf("%v", value)}
}
// Assemble and return the query string.
var parts = []string{}
for k, vs := range params {
for _, v := range vs {
parts = append(parts,
fmt.Sprintf("%s=%s", url.QueryEscape(k), url.QueryEscape(v)),
)
}
}
// Sort them deterministically.
sort.Strings(parts)
return template.URL(strings.Join(parts, "&"))
}
}
// SimplePager creates a paginator row (partial template).
//
// Use it like: {{SimplePager .Pager}}
//
// It runs the template partial 'simple_pager.html' to customize it for the site theme.
func SimplePager(r *http.Request) func(*models.Pagination) template.HTML {
return func(pager *models.Pagination) template.HTML {
tmpl, err := template.New("index").Funcs(template.FuncMap{
"QueryPlus": QueryPlus(r),
}).ParseFiles(config.TemplatePath + "/partials/simple_pager.html")
if err != nil {
return template.HTML(err.Error())
}
var (
vars = struct {
Pager *models.Pagination
Request *http.Request
}{pager, r}
buf = bytes.NewBuffer([]byte{})
)
err = tmpl.ExecuteTemplate(buf, "SimplePager", vars)
if err != nil {
return template.HTML(err.Error())
}
return template.HTML(buf.String())
}
}