adding api/v2 endpoints
All checks were successful
Check scripts syntax / check-scripts-syntax (push) Successful in 3s
Haven Notify Build and Deploy / Test Haven Notify (push) Successful in 46s
Haven Notify Build and Deploy / Build Haven Notify Image (PR) (push) Has been skipped
Haven Notify Build and Deploy / Build Haven Notify Image (push) Successful in 20s
Haven Notify Build and Deploy / Deploy Haven Notify (internal) (push) Successful in 3s
All checks were successful
Check scripts syntax / check-scripts-syntax (push) Successful in 3s
Haven Notify Build and Deploy / Test Haven Notify (push) Successful in 46s
Haven Notify Build and Deploy / Build Haven Notify Image (PR) (push) Has been skipped
Haven Notify Build and Deploy / Build Haven Notify Image (push) Successful in 20s
Haven Notify Build and Deploy / Deploy Haven Notify (internal) (push) Successful in 3s
This commit is contained in:
306
haven-notify/v2.go
Normal file
306
haven-notify/v2.go
Normal file
@@ -0,0 +1,306 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"math"
|
||||
"net/http"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
"unicode/utf8"
|
||||
)
|
||||
|
||||
const (
|
||||
v2ColorBrand = 0x5865F2
|
||||
v2ColorSuccess = 0x57F287
|
||||
v2ColorUpdate = 0x3498DB
|
||||
v2ColorWarning = 0xFEE75C
|
||||
v2ColorError = 0xED4245
|
||||
v2ColorCritical = 0x992D22
|
||||
|
||||
v2InlineFieldLimit = 80
|
||||
)
|
||||
|
||||
type v2CommonRequest struct {
|
||||
Source *string `json:"source"`
|
||||
OccurredAt *string `json:"occurredAt"`
|
||||
Extra extraRequests `json:"extra"`
|
||||
}
|
||||
|
||||
type v2MessageRequest struct {
|
||||
v2CommonRequest
|
||||
Title *string `json:"title"`
|
||||
Message *string `json:"message"`
|
||||
}
|
||||
|
||||
type v2BackupRequest struct {
|
||||
v2CommonRequest
|
||||
Title *string `json:"title"`
|
||||
Asset *string `json:"asset"`
|
||||
SizeBytes *int64 `json:"sizeBytes"`
|
||||
DurationSeconds *float64 `json:"durationSeconds"`
|
||||
}
|
||||
|
||||
type v2UpdateRequest struct {
|
||||
v2CommonRequest
|
||||
Host *string `json:"host"`
|
||||
Asset *string `json:"asset"`
|
||||
DurationSeconds *float64 `json:"durationSeconds"`
|
||||
FromVersion *string `json:"fromVersion"`
|
||||
ToVersion *string `json:"toVersion"`
|
||||
}
|
||||
|
||||
type v2ErrorRequest struct {
|
||||
v2CommonRequest
|
||||
Message *string `json:"message"`
|
||||
Severity *string `json:"severity"`
|
||||
ErrorCode *string `json:"errorCode"`
|
||||
}
|
||||
|
||||
func (a *application) v2MessageHandler(w http.ResponseWriter, r *http.Request) {
|
||||
var request v2MessageRequest
|
||||
if err := decodeRequest(w, r, &request, "title", "message", "source", "occurredat", "extra"); err != nil {
|
||||
a.writeDecodeError(w, r, err)
|
||||
return
|
||||
}
|
||||
when, valid := a.validV2Common(request.v2CommonRequest)
|
||||
if !valid || !requiredString(request.Message) || !validOptionalString(request.Title) {
|
||||
a.writeDecodeError(w, r, errInvalidPayload)
|
||||
return
|
||||
}
|
||||
|
||||
if !a.send(w, r, buildV2MessagePayload(request, when)) {
|
||||
return
|
||||
}
|
||||
writeText(w, http.StatusOK, "Notification sent")
|
||||
}
|
||||
|
||||
func (a *application) v2BackupHandler(w http.ResponseWriter, r *http.Request) {
|
||||
var request v2BackupRequest
|
||||
if err := decodeRequest(w, r, &request, "title", "asset", "sizebytes", "durationseconds", "source", "occurredat", "extra"); err != nil {
|
||||
a.writeDecodeError(w, r, err)
|
||||
return
|
||||
}
|
||||
when, valid := a.validV2Common(request.v2CommonRequest)
|
||||
if !valid || !requiredString(request.Asset) || !validOptionalString(request.Title) || request.SizeBytes == nil || *request.SizeBytes < 0 || !validOptionalNumber(request.DurationSeconds) {
|
||||
a.writeDecodeError(w, r, errInvalidPayload)
|
||||
return
|
||||
}
|
||||
|
||||
if !a.send(w, r, buildV2BackupPayload(request, when)) {
|
||||
return
|
||||
}
|
||||
writeText(w, http.StatusOK, "Notification sent")
|
||||
}
|
||||
|
||||
func (a *application) v2UpdateHandler(w http.ResponseWriter, r *http.Request) {
|
||||
var request v2UpdateRequest
|
||||
if err := decodeRequest(w, r, &request, "host", "asset", "durationseconds", "fromversion", "toversion", "source", "occurredat", "extra"); err != nil {
|
||||
a.writeDecodeError(w, r, err)
|
||||
return
|
||||
}
|
||||
when, valid := a.validV2Common(request.v2CommonRequest)
|
||||
if !valid || !requiredString(request.Host) || !requiredString(request.Asset) || !validNumber(request.DurationSeconds) || !validOptionalString(request.FromVersion) || !validOptionalString(request.ToVersion) {
|
||||
a.writeDecodeError(w, r, errInvalidPayload)
|
||||
return
|
||||
}
|
||||
|
||||
if !a.send(w, r, buildV2UpdatePayload(request, when)) {
|
||||
return
|
||||
}
|
||||
writeText(w, http.StatusOK, "Notification sent")
|
||||
}
|
||||
|
||||
func (a *application) v2ErrorHandler(w http.ResponseWriter, r *http.Request) {
|
||||
var request v2ErrorRequest
|
||||
if err := decodeRequest(w, r, &request, "source", "message", "severity", "errorcode", "occurredat", "extra"); err != nil {
|
||||
a.writeDecodeError(w, r, err)
|
||||
return
|
||||
}
|
||||
when, valid := a.validV2Common(request.v2CommonRequest)
|
||||
if !valid || !requiredString(request.Source) || !requiredString(request.Message) || !validV2Severity(request.Severity) || !validOptionalString(request.ErrorCode) {
|
||||
a.writeDecodeError(w, r, errInvalidPayload)
|
||||
return
|
||||
}
|
||||
|
||||
if !a.send(w, r, buildV2ErrorPayload(request, when)) {
|
||||
return
|
||||
}
|
||||
writeText(w, http.StatusOK, "Notification sent")
|
||||
}
|
||||
|
||||
func (a *application) validV2Common(request v2CommonRequest) (time.Time, bool) {
|
||||
if !validOptionalString(request.Source) || !validExtra(request.Extra) {
|
||||
return time.Time{}, false
|
||||
}
|
||||
if request.OccurredAt == nil {
|
||||
return a.now().UTC(), true
|
||||
}
|
||||
if !requiredString(request.OccurredAt) {
|
||||
return time.Time{}, false
|
||||
}
|
||||
when, err := time.Parse(time.RFC3339, *request.OccurredAt)
|
||||
if err != nil {
|
||||
return time.Time{}, false
|
||||
}
|
||||
return when.UTC(), true
|
||||
}
|
||||
|
||||
func validOptionalString(value *string) bool {
|
||||
return value == nil || requiredString(value)
|
||||
}
|
||||
|
||||
func validNumber(value *float64) bool {
|
||||
return value != nil && *value >= 0 && !math.IsNaN(*value) && !math.IsInf(*value, 0)
|
||||
}
|
||||
|
||||
func validOptionalNumber(value *float64) bool {
|
||||
return value == nil || validNumber(value)
|
||||
}
|
||||
|
||||
func validV2Severity(value *string) bool {
|
||||
if value == nil {
|
||||
return false
|
||||
}
|
||||
switch *value {
|
||||
case "warning", "error", "critical":
|
||||
return true
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
func buildV2MessagePayload(request v2MessageRequest, when time.Time) discordPayload {
|
||||
title := "Message"
|
||||
if request.Title != nil {
|
||||
title = *request.Title
|
||||
}
|
||||
embed := newV2Embed("💬 "+title, *request.Message, v2ColorBrand, when)
|
||||
embed.Fields = appendV2SourceAndExtras(embed.Fields, request.Source, request.Extra)
|
||||
return discordPayload{Embeds: []discordEmbed{embed}}
|
||||
}
|
||||
|
||||
func buildV2BackupPayload(request v2BackupRequest, when time.Time) discordPayload {
|
||||
title := "Backup completed"
|
||||
if request.Title != nil {
|
||||
title = *request.Title
|
||||
}
|
||||
embed := newV2Embed("📦 "+title, fmt.Sprintf("**%s** was backed up successfully.", *request.Asset), v2ColorSuccess, when)
|
||||
embed.Fields = append(embed.Fields, v2Field("Size", formatBytes(*request.SizeBytes)))
|
||||
if request.DurationSeconds != nil {
|
||||
embed.Fields = append(embed.Fields, v2Field("Duration", formatV2Duration(*request.DurationSeconds)))
|
||||
}
|
||||
embed.Fields = appendV2SourceAndExtras(embed.Fields, request.Source, request.Extra)
|
||||
return discordPayload{Embeds: []discordEmbed{embed}}
|
||||
}
|
||||
|
||||
func buildV2UpdatePayload(request v2UpdateRequest, when time.Time) discordPayload {
|
||||
embed := newV2Embed("🔄 Update completed", fmt.Sprintf("**%s** was updated on **%s**.", *request.Asset, *request.Host), v2ColorUpdate, when)
|
||||
switch {
|
||||
case request.FromVersion != nil && request.ToVersion != nil:
|
||||
embed.Fields = append(embed.Fields, v2Field("Version", *request.FromVersion+" → "+*request.ToVersion))
|
||||
case request.FromVersion != nil:
|
||||
embed.Fields = append(embed.Fields, v2Field("Previous version", *request.FromVersion))
|
||||
case request.ToVersion != nil:
|
||||
embed.Fields = append(embed.Fields, v2Field("Installed version", *request.ToVersion))
|
||||
}
|
||||
embed.Fields = append(embed.Fields, v2Field("Duration", formatV2Duration(*request.DurationSeconds)))
|
||||
embed.Fields = appendV2SourceAndExtras(embed.Fields, request.Source, request.Extra)
|
||||
return discordPayload{Embeds: []discordEmbed{embed}}
|
||||
}
|
||||
|
||||
func buildV2ErrorPayload(request v2ErrorRequest, when time.Time) discordPayload {
|
||||
title, color := v2ErrorStyle(*request.Severity)
|
||||
embed := newV2Embed(title, *request.Message, color, when)
|
||||
embed.Fields = append(embed.Fields, v2Field("Source", *request.Source))
|
||||
if request.ErrorCode != nil {
|
||||
embed.Fields = append(embed.Fields, v2Field("Error code", *request.ErrorCode))
|
||||
}
|
||||
embed.Fields = appendV2Extras(embed.Fields, request.Extra)
|
||||
return discordPayload{Embeds: []discordEmbed{embed}}
|
||||
}
|
||||
|
||||
func newV2Embed(title, description string, color int, when time.Time) discordEmbed {
|
||||
return discordEmbed{
|
||||
Title: title,
|
||||
Description: description,
|
||||
Color: color,
|
||||
Timestamp: when.UTC().Format(time.RFC3339),
|
||||
Footer: &discordEmbedFooter{Text: "Haven Notify"},
|
||||
}
|
||||
}
|
||||
|
||||
func v2ErrorStyle(severity string) (string, int) {
|
||||
switch severity {
|
||||
case "warning":
|
||||
return "⚠️ Warning", v2ColorWarning
|
||||
case "critical":
|
||||
return "🛑 Critical error", v2ColorCritical
|
||||
default:
|
||||
return "❌ Error", v2ColorError
|
||||
}
|
||||
}
|
||||
|
||||
func appendV2SourceAndExtras(fields []discordEmbedField, source *string, extra extraRequests) []discordEmbedField {
|
||||
if source != nil {
|
||||
fields = append(fields, v2Field("Source", *source))
|
||||
}
|
||||
return appendV2Extras(fields, extra)
|
||||
}
|
||||
|
||||
func appendV2Extras(fields []discordEmbedField, extra extraRequests) []discordEmbedField {
|
||||
for _, field := range extra {
|
||||
value := *field.Value
|
||||
fields = append(fields, discordEmbedField{
|
||||
Name: *field.Name,
|
||||
Value: value,
|
||||
Inline: !strings.ContainsAny(value, "\r\n") && utf8.RuneCountInString(value) <= v2InlineFieldLimit,
|
||||
})
|
||||
}
|
||||
return fields
|
||||
}
|
||||
|
||||
func v2Field(name, value string) discordEmbedField {
|
||||
return discordEmbedField{Name: name, Value: value, Inline: true}
|
||||
}
|
||||
|
||||
func formatBytes(bytes int64) string {
|
||||
if bytes < 1024 {
|
||||
return strconv.FormatInt(bytes, 10) + " B"
|
||||
}
|
||||
value := float64(bytes)
|
||||
for _, unit := range []string{"KiB", "MiB", "GiB", "TiB", "PiB", "EiB"} {
|
||||
value /= 1024
|
||||
if value < 1024 || unit == "EiB" {
|
||||
return fmt.Sprintf("%.2f %s", value, unit)
|
||||
}
|
||||
}
|
||||
return strconv.FormatInt(bytes, 10) + " B"
|
||||
}
|
||||
|
||||
func formatV2Duration(seconds float64) string {
|
||||
if seconds < 60 {
|
||||
return formatV2Number(seconds) + "s"
|
||||
}
|
||||
|
||||
hours := math.Floor(seconds / 3600)
|
||||
seconds -= hours * 3600
|
||||
minutes := math.Floor(seconds / 60)
|
||||
seconds -= minutes * 60
|
||||
parts := make([]string, 0, 3)
|
||||
if hours > 0 {
|
||||
parts = append(parts, formatV2Number(hours)+"h")
|
||||
}
|
||||
if minutes > 0 {
|
||||
parts = append(parts, formatV2Number(minutes)+"m")
|
||||
}
|
||||
if seconds > 0 {
|
||||
parts = append(parts, formatV2Number(seconds)+"s")
|
||||
}
|
||||
return strings.Join(parts, " ")
|
||||
}
|
||||
|
||||
func formatV2Number(value float64) string {
|
||||
formatted := strconv.FormatFloat(value, 'f', 3, 64)
|
||||
return strings.TrimRight(strings.TrimRight(formatted, "0"), ".")
|
||||
}
|
||||
Reference in New Issue
Block a user