300 lines
6.6 KiB
Go
300 lines
6.6 KiB
Go
package server
|
|
|
|
import (
|
|
"encoding/json"
|
|
"fmt"
|
|
"html/template"
|
|
"math/big"
|
|
"net/http"
|
|
"runtime/debug"
|
|
"strconv"
|
|
"strings"
|
|
"time"
|
|
|
|
"github.com/golang/glog"
|
|
"github.com/trezor/blockbook/api"
|
|
"github.com/trezor/blockbook/common"
|
|
)
|
|
|
|
type tpl int
|
|
|
|
const (
|
|
noTpl = tpl(iota)
|
|
errorTpl
|
|
errorInternalTpl
|
|
)
|
|
|
|
// htmlTemplateHandler is a handle to public http server
|
|
type htmlTemplates[TD any] struct {
|
|
metrics *common.Metrics
|
|
templates []*template.Template
|
|
debug bool
|
|
newTemplateData func(r *http.Request) *TD
|
|
newTemplateDataWithError func(error *api.APIError, r *http.Request) *TD
|
|
parseTemplates func() []*template.Template
|
|
postHtmlTemplateHandler func(data *TD, w http.ResponseWriter, r *http.Request)
|
|
}
|
|
|
|
func (s *htmlTemplates[TD]) htmlTemplateHandler(handler func(w http.ResponseWriter, r *http.Request) (tpl, *TD, error)) func(w http.ResponseWriter, r *http.Request) {
|
|
handlerName := getFunctionName(handler)
|
|
return func(w http.ResponseWriter, r *http.Request) {
|
|
var t tpl
|
|
var data *TD
|
|
var err error
|
|
defer func() {
|
|
if e := recover(); e != nil {
|
|
glog.Error(handlerName, " recovered from panic: ", e)
|
|
debug.PrintStack()
|
|
t = errorInternalTpl
|
|
if s.debug {
|
|
data = s.newTemplateDataWithError(&api.APIError{Text: fmt.Sprint("Internal server error: recovered from panic ", e)}, r)
|
|
} else {
|
|
data = s.newTemplateDataWithError(&api.APIError{Text: "Internal server error"}, r)
|
|
}
|
|
}
|
|
// noTpl means the handler completely handled the request
|
|
if t != noTpl {
|
|
w.Header().Set("Content-Type", "text/html; charset=utf-8")
|
|
// return 500 Internal Server Error with errorInternalTpl
|
|
if t == errorInternalTpl {
|
|
w.WriteHeader(http.StatusInternalServerError)
|
|
}
|
|
if err := s.templates[t].ExecuteTemplate(w, "base.html", data); err != nil {
|
|
glog.Error(err)
|
|
}
|
|
}
|
|
if s.metrics != nil {
|
|
s.metrics.ExplorerPendingRequests.With((common.Labels{"method": handlerName})).Dec()
|
|
}
|
|
}()
|
|
if s.metrics != nil {
|
|
s.metrics.ExplorerPendingRequests.With((common.Labels{"method": handlerName})).Inc()
|
|
}
|
|
if s.debug {
|
|
// reload templates on each request
|
|
// to reflect changes during development
|
|
s.templates = s.parseTemplates()
|
|
}
|
|
t, data, err = handler(w, r)
|
|
if err != nil || (data == nil && t != noTpl) {
|
|
t = errorInternalTpl
|
|
if apiErr, ok := err.(*api.APIError); ok {
|
|
data = s.newTemplateDataWithError(apiErr, r)
|
|
if apiErr.Public {
|
|
t = errorTpl
|
|
}
|
|
} else {
|
|
if err != nil {
|
|
glog.Error(handlerName, " error: ", err)
|
|
}
|
|
if s.debug {
|
|
data = s.newTemplateDataWithError(&api.APIError{Text: fmt.Sprintf("Internal server error: %v, data %+v", err, data)}, r)
|
|
} else {
|
|
data = s.newTemplateDataWithError(&api.APIError{Text: "Internal server error"}, r)
|
|
}
|
|
}
|
|
}
|
|
if s.postHtmlTemplateHandler != nil {
|
|
s.postHtmlTemplateHandler(data, w, r)
|
|
}
|
|
|
|
}
|
|
}
|
|
|
|
func relativeTimeUnit(d int64) string {
|
|
var u string
|
|
if d < 60 {
|
|
if d == 1 {
|
|
u = " sec"
|
|
} else {
|
|
u = " secs"
|
|
}
|
|
} else if d < 3600 {
|
|
d /= 60
|
|
if d == 1 {
|
|
u = " min"
|
|
} else {
|
|
u = " mins"
|
|
}
|
|
} else if d < 3600*24 {
|
|
d /= 3600
|
|
if d == 1 {
|
|
u = " hour"
|
|
} else {
|
|
u = " hours"
|
|
}
|
|
} else {
|
|
d /= 3600 * 24
|
|
if d == 1 {
|
|
u = " day"
|
|
} else {
|
|
u = " days"
|
|
}
|
|
}
|
|
return strconv.FormatInt(d, 10) + u
|
|
}
|
|
|
|
func relativeTime(d int64) string {
|
|
r := relativeTimeUnit(d)
|
|
if d > 3600*24 {
|
|
d = d % (3600 * 24)
|
|
if d >= 3600 {
|
|
r += " " + relativeTimeUnit(d)
|
|
}
|
|
} else if d > 3600 {
|
|
d = d % 3600
|
|
if d >= 60 {
|
|
r += " " + relativeTimeUnit(d)
|
|
}
|
|
}
|
|
return r
|
|
}
|
|
|
|
func unixTimeSpan(ut int64) template.HTML {
|
|
t := time.Unix(ut, 0)
|
|
return timeSpan(&t)
|
|
}
|
|
|
|
var timeNow = time.Now
|
|
|
|
func timeSpan(t *time.Time) template.HTML {
|
|
if t == nil {
|
|
return ""
|
|
}
|
|
u := t.Unix()
|
|
if u <= 0 {
|
|
return ""
|
|
}
|
|
d := timeNow().Unix() - u
|
|
f := t.UTC().Format("2006-01-02 15:04:05")
|
|
if d < 0 {
|
|
return template.HTML(f)
|
|
}
|
|
r := relativeTime(d)
|
|
return template.HTML(`<span tt="` + f + `">` + r + " ago</span>")
|
|
}
|
|
|
|
func toJSON(data interface{}) string {
|
|
json, err := json.Marshal(data)
|
|
if err != nil {
|
|
return ""
|
|
}
|
|
return string(json)
|
|
}
|
|
|
|
func formatAmountWithDecimals(a *api.Amount, d int) string {
|
|
if a == nil {
|
|
return "0"
|
|
}
|
|
return a.DecimalString(d)
|
|
}
|
|
|
|
func appendAmountSpan(rv *strings.Builder, class, amount, shortcut, txDate string) {
|
|
rv.WriteString(`<span`)
|
|
if class != "" {
|
|
rv.WriteString(` class="`)
|
|
rv.WriteString(class)
|
|
rv.WriteString(`"`)
|
|
}
|
|
if txDate != "" {
|
|
rv.WriteString(` tm="`)
|
|
rv.WriteString(txDate)
|
|
rv.WriteString(`"`)
|
|
}
|
|
rv.WriteString(">")
|
|
i := strings.IndexByte(amount, '.')
|
|
if i < 0 {
|
|
appendSeparatedNumberSpans(rv, amount, "nc")
|
|
} else {
|
|
appendSeparatedNumberSpans(rv, amount[:i], "nc")
|
|
rv.WriteString(`.`)
|
|
rv.WriteString(`<span class="amt-dec">`)
|
|
appendLeftSeparatedNumberSpans(rv, amount[i+1:], "ns")
|
|
rv.WriteString("</span>")
|
|
}
|
|
if shortcut != "" {
|
|
rv.WriteString(" ")
|
|
rv.WriteString(shortcut)
|
|
}
|
|
rv.WriteString("</span>")
|
|
}
|
|
|
|
func appendAmountWrapperSpan(rv *strings.Builder, primary, symbol, classes string) {
|
|
rv.WriteString(`<span class="amt`)
|
|
if classes != "" {
|
|
rv.WriteString(` `)
|
|
rv.WriteString(classes)
|
|
}
|
|
rv.WriteString(`" cc="`)
|
|
rv.WriteString(primary)
|
|
rv.WriteString(" ")
|
|
rv.WriteString(symbol)
|
|
rv.WriteString(`">`)
|
|
}
|
|
|
|
func formatInt(i int) template.HTML {
|
|
return formatInt64(int64(i))
|
|
}
|
|
|
|
func formatUint32(i uint32) template.HTML {
|
|
return formatInt64(int64(i))
|
|
}
|
|
|
|
func appendSeparatedNumberSpans(rv *strings.Builder, s, separatorClass string) {
|
|
if len(s) > 0 && s[0] == '-' {
|
|
s = s[1:]
|
|
rv.WriteByte('-')
|
|
}
|
|
t := (len(s) - 1) / 3
|
|
if t <= 0 {
|
|
rv.WriteString(s)
|
|
} else {
|
|
t *= 3
|
|
rv.WriteString(s[:len(s)-t])
|
|
for i := len(s) - t; i < len(s); i += 3 {
|
|
rv.WriteString(`<span class="`)
|
|
rv.WriteString(separatorClass)
|
|
rv.WriteString(`">`)
|
|
rv.WriteString(s[i : i+3])
|
|
rv.WriteString("</span>")
|
|
}
|
|
}
|
|
}
|
|
|
|
func appendLeftSeparatedNumberSpans(rv *strings.Builder, s, separatorClass string) {
|
|
l := len(s)
|
|
if l <= 3 {
|
|
rv.WriteString(s)
|
|
} else {
|
|
rv.WriteString(s[:3])
|
|
for i := 3; i < len(s); i += 3 {
|
|
rv.WriteString(`<span class="`)
|
|
rv.WriteString(separatorClass)
|
|
rv.WriteString(`">`)
|
|
e := i + 3
|
|
if e > l {
|
|
e = l
|
|
}
|
|
rv.WriteString(s[i:e])
|
|
rv.WriteString("</span>")
|
|
}
|
|
}
|
|
}
|
|
|
|
func formatInt64(i int64) template.HTML {
|
|
s := strconv.FormatInt(i, 10)
|
|
var rv strings.Builder
|
|
appendSeparatedNumberSpans(&rv, s, "ns")
|
|
return template.HTML(rv.String())
|
|
}
|
|
|
|
func formatBigInt(i *big.Int) template.HTML {
|
|
if i == nil {
|
|
return ""
|
|
}
|
|
s := i.String()
|
|
var rv strings.Builder
|
|
appendSeparatedNumberSpans(&rv, s, "ns")
|
|
return template.HTML(rv.String())
|
|
}
|