171 lines
4.9 KiB
Go
171 lines
4.9 KiB
Go
package discord
|
|
|
|
import (
|
|
"bytes"
|
|
"context"
|
|
"encoding/json"
|
|
"fmt"
|
|
"net/http"
|
|
"time"
|
|
|
|
"sanity/internal/backup"
|
|
)
|
|
|
|
type Client struct {
|
|
webhookURL string
|
|
httpClient *http.Client
|
|
serverName string
|
|
notifyRoleID string
|
|
}
|
|
|
|
type webhookPayload struct {
|
|
Content string `json:"content,omitempty"`
|
|
AllowedMentions allowedMentions `json:"allowed_mentions,omitempty"`
|
|
Embeds []embed `json:"embeds"`
|
|
}
|
|
|
|
type allowedMentions struct {
|
|
Roles []string `json:"roles,omitempty"`
|
|
}
|
|
|
|
type embed struct {
|
|
Title string `json:"title"`
|
|
Description string `json:"description,omitempty"`
|
|
Color int `json:"color"`
|
|
Timestamp string `json:"timestamp,omitempty"`
|
|
Fields []embedField `json:"fields,omitempty"`
|
|
Footer *embedFooter `json:"footer,omitempty"`
|
|
}
|
|
|
|
type embedField struct {
|
|
Name string `json:"name"`
|
|
Value string `json:"value"`
|
|
Inline bool `json:"inline"`
|
|
}
|
|
|
|
type embedFooter struct {
|
|
Text string `json:"text"`
|
|
}
|
|
|
|
func New(serverName string, webhookURL string, notifyRoleID string, timeout time.Duration) *Client {
|
|
return &Client{
|
|
serverName: serverName,
|
|
webhookURL: webhookURL,
|
|
httpClient: &http.Client{Timeout: timeout},
|
|
notifyRoleID: notifyRoleID,
|
|
}
|
|
}
|
|
|
|
func (c *Client) SendSuccess(ctx context.Context, result backup.Result, interval time.Duration) error {
|
|
fields := []embedField{
|
|
{Name: "Server", Value: c.serverName, Inline: true},
|
|
{Name: "Source", Value: result.SourceFile, Inline: false},
|
|
{Name: "Artifact", Value: result.ArtifactPath, Inline: false},
|
|
{Name: "Artifact size", Value: formatBytes(result.SizeBytes), Inline: true},
|
|
{Name: "Backup dir total", Value: formatOptionalBytes(result.TotalBackupDirSizeBytes), Inline: true},
|
|
{Name: "Duration", Value: result.Duration.Round(time.Millisecond).String(), Inline: true},
|
|
{Name: "Next run", Value: formatSummaryInterval(interval), Inline: true},
|
|
{Name: "Timestamp", Value: formatDiscordTime(result.Timestamp), Inline: true},
|
|
}
|
|
return c.send(ctx, webhookPayload{Embeds: []embed{{
|
|
Title: fmt.Sprintf("sanity backup successful - %s", c.serverName),
|
|
Description: "A scheduled backup completed successfully.",
|
|
Color: 0x27AE60,
|
|
Fields: fields,
|
|
Footer: &embedFooter{Text: fmt.Sprintf("sanity - %s", c.serverName)},
|
|
}}})
|
|
}
|
|
|
|
func (c *Client) SendFailure(ctx context.Context, timestamp time.Time, reason string, sourceFile string, outputDir string, totalBackupDirSizeBytes int64) error {
|
|
fields := []embedField{
|
|
{Name: "Server", Value: c.serverName, Inline: true},
|
|
{Name: "Source", Value: sourceFile, Inline: false},
|
|
{Name: "Output dir", Value: outputDir, Inline: false},
|
|
{Name: "Backup dir total", Value: formatOptionalBytes(totalBackupDirSizeBytes), Inline: true},
|
|
{Name: "Timestamp", Value: formatDiscordTime(timestamp), Inline: true},
|
|
{Name: "Reason", Value: reason, Inline: false},
|
|
}
|
|
payload := webhookPayload{Embeds: []embed{{
|
|
Title: fmt.Sprintf("sanity backup failed - %s", c.serverName),
|
|
Description: "A scheduled backup run failed.",
|
|
Color: 0xE74C3C,
|
|
Timestamp: timestamp.Format(time.RFC3339),
|
|
Fields: fields,
|
|
}}}
|
|
if c.notifyRoleID != "" {
|
|
payload.Content = fmt.Sprintf("<@&%s>", c.notifyRoleID)
|
|
payload.AllowedMentions = allowedMentions{Roles: []string{c.notifyRoleID}}
|
|
}
|
|
return c.send(ctx, payload)
|
|
}
|
|
|
|
func formatDiscordTime(timestamp time.Time) string {
|
|
unixSeconds := timestamp.Unix()
|
|
return fmt.Sprintf("<t:%d:f>", unixSeconds)
|
|
}
|
|
|
|
func formatSummaryInterval(interval time.Duration) string {
|
|
if interval%time.Hour == 0 {
|
|
return fmt.Sprintf("%dh", interval/time.Hour)
|
|
}
|
|
if interval%time.Minute == 0 {
|
|
return fmt.Sprintf("%dm", interval/time.Minute)
|
|
}
|
|
return interval.String()
|
|
}
|
|
|
|
func formatOptionalBytes(sizeBytes int64) string {
|
|
if sizeBytes < 0 {
|
|
return "unknown"
|
|
}
|
|
return formatBytes(sizeBytes)
|
|
}
|
|
|
|
func formatBytes(sizeBytes int64) string {
|
|
const (
|
|
kib = 1024
|
|
mib = 1024 * 1024
|
|
gib = 1024 * 1024 * 1024
|
|
tib = 1024 * 1024 * 1024 * 1024
|
|
)
|
|
if sizeBytes <= 0 {
|
|
return "0 B"
|
|
}
|
|
if sizeBytes < kib {
|
|
return fmt.Sprintf("%d B", sizeBytes)
|
|
}
|
|
if sizeBytes < mib {
|
|
return fmt.Sprintf("%.2f KB", float64(sizeBytes)/kib)
|
|
}
|
|
if sizeBytes < gib {
|
|
return fmt.Sprintf("%.2f MB", float64(sizeBytes)/mib)
|
|
}
|
|
if sizeBytes < tib {
|
|
return fmt.Sprintf("%.2f GB", float64(sizeBytes)/gib)
|
|
}
|
|
return fmt.Sprintf("%.2f TB", float64(sizeBytes)/tib)
|
|
}
|
|
|
|
func (c *Client) send(ctx context.Context, payload webhookPayload) error {
|
|
buffer := &bytes.Buffer{}
|
|
if err := json.NewEncoder(buffer).Encode(payload); err != nil {
|
|
return err
|
|
}
|
|
|
|
req, err := http.NewRequestWithContext(ctx, http.MethodPost, c.webhookURL, buffer)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
req.Header.Set("Content-Type", "application/json")
|
|
|
|
resp, err := c.httpClient.Do(req)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
defer resp.Body.Close()
|
|
if resp.StatusCode < 200 || resp.StatusCode >= 300 {
|
|
return fmt.Errorf("discord webhook returned %s", resp.Status)
|
|
}
|
|
return nil
|
|
}
|