Merge branch 'main' of github.com:binwiederhier/ntfy into predefined-users
This commit is contained in:
@@ -11,6 +11,8 @@ import (
|
||||
// Defines default config settings (excluding limits, see below)
|
||||
const (
|
||||
DefaultListenHTTP = ":80"
|
||||
DefaultConfigFile = "/etc/ntfy/server.yml"
|
||||
DefaultTemplateDir = "/etc/ntfy/templates"
|
||||
DefaultCacheDuration = 12 * time.Hour
|
||||
DefaultCacheBatchTimeout = time.Duration(0)
|
||||
DefaultKeepaliveInterval = 45 * time.Second // Not too frequently to save battery (Android read timeout used to be 77s!)
|
||||
@@ -101,6 +103,7 @@ type Config struct {
|
||||
AttachmentTotalSizeLimit int64
|
||||
AttachmentFileSizeLimit int64
|
||||
AttachmentExpiryDuration time.Duration
|
||||
TemplateDir string // Directory to load named templates from
|
||||
KeepaliveInterval time.Duration
|
||||
ManagerInterval time.Duration
|
||||
DisallowedTopics []string
|
||||
@@ -174,7 +177,7 @@ type Config struct {
|
||||
// NewConfig instantiates a default new server config
|
||||
func NewConfig() *Config {
|
||||
return &Config{
|
||||
File: "", // Only used for testing
|
||||
File: DefaultConfigFile, // Only used for testing
|
||||
BaseURL: "",
|
||||
ListenHTTP: DefaultListenHTTP,
|
||||
ListenHTTPS: "",
|
||||
@@ -197,6 +200,7 @@ func NewConfig() *Config {
|
||||
AttachmentTotalSizeLimit: DefaultAttachmentTotalSizeLimit,
|
||||
AttachmentFileSizeLimit: DefaultAttachmentFileSizeLimit,
|
||||
AttachmentExpiryDuration: DefaultAttachmentExpiryDuration,
|
||||
TemplateDir: DefaultTemplateDir,
|
||||
KeepaliveInterval: DefaultKeepaliveInterval,
|
||||
ManagerInterval: DefaultManagerInterval,
|
||||
DisallowedTopics: DefaultDisallowedTopics,
|
||||
|
||||
@@ -123,6 +123,8 @@ var (
|
||||
errHTTPBadRequestTemplateDisallowedFunctionCalls = &errHTTP{40044, http.StatusBadRequest, "invalid request: template contains disallowed function calls, e.g. template, call, or define", "https://ntfy.sh/docs/publish/#message-templating", nil}
|
||||
errHTTPBadRequestTemplateExecuteFailed = &errHTTP{40045, http.StatusBadRequest, "invalid request: template execution failed", "https://ntfy.sh/docs/publish/#message-templating", nil}
|
||||
errHTTPBadRequestInvalidUsername = &errHTTP{40046, http.StatusBadRequest, "invalid request: invalid username", "", nil}
|
||||
errHTTPBadRequestTemplateFileNotFound = &errHTTP{40047, http.StatusBadRequest, "invalid request: template file not found", "https://ntfy.sh/docs/publish/#message-templating", nil}
|
||||
errHTTPBadRequestTemplateFileInvalid = &errHTTP{40048, http.StatusBadRequest, "invalid request: template file invalid", "https://ntfy.sh/docs/publish/#message-templating", nil}
|
||||
errHTTPNotFound = &errHTTP{40401, http.StatusNotFound, "page not found", "", nil}
|
||||
errHTTPUnauthorized = &errHTTP{40101, http.StatusUnauthorized, "unauthorized", "https://ntfy.sh/docs/publish/#authentication", nil}
|
||||
errHTTPForbidden = &errHTTP{40301, http.StatusForbidden, "forbidden", "https://ntfy.sh/docs/publish/#authentication", nil}
|
||||
|
||||
136
server/server.go
136
server/server.go
@@ -9,6 +9,7 @@ import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"gopkg.in/yaml.v2"
|
||||
"io"
|
||||
"net"
|
||||
"net/http"
|
||||
@@ -34,6 +35,7 @@ import (
|
||||
"heckel.io/ntfy/v2/log"
|
||||
"heckel.io/ntfy/v2/user"
|
||||
"heckel.io/ntfy/v2/util"
|
||||
"heckel.io/ntfy/v2/util/sprig"
|
||||
)
|
||||
|
||||
// Server is the main server, providing the UI and API for ntfy
|
||||
@@ -120,6 +122,15 @@ var (
|
||||
//go:embed docs
|
||||
docsStaticFs embed.FS
|
||||
docsStaticCached = &util.CachingEmbedFS{ModTime: time.Now(), FS: docsStaticFs}
|
||||
|
||||
//go:embed templates
|
||||
templatesFs embed.FS // Contains template config files (e.g. grafana.yml, github.yml, ...)
|
||||
templatesDir = "templates"
|
||||
|
||||
// templateDisallowedRegex tests a template for disallowed expressions. While not really dangerous, they
|
||||
// are not useful, and seem potentially troublesome.
|
||||
templateDisallowedRegex = regexp.MustCompile(`(?m)\{\{-?\s*(call|template|define)\b`)
|
||||
templateNameRegex = regexp.MustCompile(`^[-_A-Za-z0-9]+$`)
|
||||
)
|
||||
|
||||
const (
|
||||
@@ -129,17 +140,13 @@ const (
|
||||
newMessageBody = "New message" // Used in poll requests as generic message
|
||||
defaultAttachmentMessage = "You received a file: %s" // Used if message body is empty, and there is an attachment
|
||||
encodingBase64 = "base64" // Used mainly for binary UnifiedPush messages
|
||||
jsonBodyBytesLimit = 32768 // Max number of bytes for a request bodys (unless MessageLimit is higher)
|
||||
jsonBodyBytesLimit = 131072 // Max number of bytes for a request bodys (unless MessageLimit is higher)
|
||||
unifiedPushTopicPrefix = "up" // Temporarily, we rate limit all "up*" topics based on the subscriber
|
||||
unifiedPushTopicLength = 14 // Length of UnifiedPush topics, including the "up" part
|
||||
messagesHistoryMax = 10 // Number of message count values to keep in memory
|
||||
templateMaxExecutionTime = 100 * time.Millisecond
|
||||
)
|
||||
|
||||
var (
|
||||
// templateDisallowedRegex tests a template for disallowed expressions. While not really dangerous, they
|
||||
// are not useful, and seem potentially troublesome.
|
||||
templateDisallowedRegex = regexp.MustCompile(`(?m)\{\{-?\s*(call|template|define)\b`)
|
||||
templateMaxExecutionTime = 100 * time.Millisecond // Maximum time a template can take to execute, used to prevent DoS attacks
|
||||
templateMaxOutputBytes = 1024 * 1024 // Maximum number of bytes a template can output, used to prevent DoS attacks
|
||||
templateFileExtension = ".yml" // Template files must end with this extension
|
||||
)
|
||||
|
||||
// WebSocket constants
|
||||
@@ -945,7 +952,7 @@ func (s *Server) forwardPollRequest(v *visitor, m *message) {
|
||||
}
|
||||
}
|
||||
|
||||
func (s *Server) parsePublishParams(r *http.Request, m *message) (cache bool, firebase bool, email, call string, template bool, unifiedpush bool, err *errHTTP) {
|
||||
func (s *Server) parsePublishParams(r *http.Request, m *message) (cache bool, firebase bool, email, call string, template templateMode, unifiedpush bool, err *errHTTP) {
|
||||
cache = readBoolParam(r, true, "x-cache", "cache")
|
||||
firebase = readBoolParam(r, true, "x-firebase", "firebase")
|
||||
m.Title = readParam(r, "x-title", "title", "t")
|
||||
@@ -961,7 +968,7 @@ func (s *Server) parsePublishParams(r *http.Request, m *message) (cache bool, fi
|
||||
}
|
||||
if attach != "" {
|
||||
if !urlRegex.MatchString(attach) {
|
||||
return false, false, "", "", false, false, errHTTPBadRequestAttachmentURLInvalid
|
||||
return false, false, "", "", "", false, errHTTPBadRequestAttachmentURLInvalid
|
||||
}
|
||||
m.Attachment.URL = attach
|
||||
if m.Attachment.Name == "" {
|
||||
@@ -979,19 +986,19 @@ func (s *Server) parsePublishParams(r *http.Request, m *message) (cache bool, fi
|
||||
}
|
||||
if icon != "" {
|
||||
if !urlRegex.MatchString(icon) {
|
||||
return false, false, "", "", false, false, errHTTPBadRequestIconURLInvalid
|
||||
return false, false, "", "", "", false, errHTTPBadRequestIconURLInvalid
|
||||
}
|
||||
m.Icon = icon
|
||||
}
|
||||
email = readParam(r, "x-email", "x-e-mail", "email", "e-mail", "mail", "e")
|
||||
if s.smtpSender == nil && email != "" {
|
||||
return false, false, "", "", false, false, errHTTPBadRequestEmailDisabled
|
||||
return false, false, "", "", "", false, errHTTPBadRequestEmailDisabled
|
||||
}
|
||||
call = readParam(r, "x-call", "call")
|
||||
if call != "" && (s.config.TwilioAccount == "" || s.userManager == nil) {
|
||||
return false, false, "", "", false, false, errHTTPBadRequestPhoneCallsDisabled
|
||||
return false, false, "", "", "", false, errHTTPBadRequestPhoneCallsDisabled
|
||||
} else if call != "" && !isBoolValue(call) && !phoneNumberRegex.MatchString(call) {
|
||||
return false, false, "", "", false, false, errHTTPBadRequestPhoneNumberInvalid
|
||||
return false, false, "", "", "", false, errHTTPBadRequestPhoneNumberInvalid
|
||||
}
|
||||
messageStr := strings.ReplaceAll(readParam(r, "x-message", "message", "m"), "\\n", "\n")
|
||||
if messageStr != "" {
|
||||
@@ -1000,27 +1007,27 @@ func (s *Server) parsePublishParams(r *http.Request, m *message) (cache bool, fi
|
||||
var e error
|
||||
m.Priority, e = util.ParsePriority(readParam(r, "x-priority", "priority", "prio", "p"))
|
||||
if e != nil {
|
||||
return false, false, "", "", false, false, errHTTPBadRequestPriorityInvalid
|
||||
return false, false, "", "", "", false, errHTTPBadRequestPriorityInvalid
|
||||
}
|
||||
m.Tags = readCommaSeparatedParam(r, "x-tags", "tags", "tag", "ta")
|
||||
delayStr := readParam(r, "x-delay", "delay", "x-at", "at", "x-in", "in")
|
||||
if delayStr != "" {
|
||||
if !cache {
|
||||
return false, false, "", "", false, false, errHTTPBadRequestDelayNoCache
|
||||
return false, false, "", "", "", false, errHTTPBadRequestDelayNoCache
|
||||
}
|
||||
if email != "" {
|
||||
return false, false, "", "", false, false, errHTTPBadRequestDelayNoEmail // we cannot store the email address (yet)
|
||||
return false, false, "", "", "", false, errHTTPBadRequestDelayNoEmail // we cannot store the email address (yet)
|
||||
}
|
||||
if call != "" {
|
||||
return false, false, "", "", false, false, errHTTPBadRequestDelayNoCall // we cannot store the phone number (yet)
|
||||
return false, false, "", "", "", false, errHTTPBadRequestDelayNoCall // we cannot store the phone number (yet)
|
||||
}
|
||||
delay, err := util.ParseFutureTime(delayStr, time.Now())
|
||||
if err != nil {
|
||||
return false, false, "", "", false, false, errHTTPBadRequestDelayCannotParse
|
||||
return false, false, "", "", "", false, errHTTPBadRequestDelayCannotParse
|
||||
} else if delay.Unix() < time.Now().Add(s.config.MessageDelayMin).Unix() {
|
||||
return false, false, "", "", false, false, errHTTPBadRequestDelayTooSmall
|
||||
return false, false, "", "", "", false, errHTTPBadRequestDelayTooSmall
|
||||
} else if delay.Unix() > time.Now().Add(s.config.MessageDelayMax).Unix() {
|
||||
return false, false, "", "", false, false, errHTTPBadRequestDelayTooLarge
|
||||
return false, false, "", "", "", false, errHTTPBadRequestDelayTooLarge
|
||||
}
|
||||
m.Time = delay.Unix()
|
||||
}
|
||||
@@ -1028,14 +1035,14 @@ func (s *Server) parsePublishParams(r *http.Request, m *message) (cache bool, fi
|
||||
if actionsStr != "" {
|
||||
m.Actions, e = parseActions(actionsStr)
|
||||
if e != nil {
|
||||
return false, false, "", "", false, false, errHTTPBadRequestActionsInvalid.Wrap("%s", e.Error())
|
||||
return false, false, "", "", "", false, errHTTPBadRequestActionsInvalid.Wrap("%s", e.Error())
|
||||
}
|
||||
}
|
||||
contentType, markdown := readParam(r, "content-type", "content_type"), readBoolParam(r, false, "x-markdown", "markdown", "md")
|
||||
if markdown || strings.ToLower(contentType) == "text/markdown" {
|
||||
m.ContentType = "text/markdown"
|
||||
}
|
||||
template = readBoolParam(r, false, "x-template", "template", "tpl")
|
||||
template = templateMode(readParam(r, "x-template", "template", "tpl"))
|
||||
unifiedpush = readBoolParam(r, false, "x-unifiedpush", "unifiedpush", "up") // see GET too!
|
||||
contentEncoding := readParam(r, "content-encoding")
|
||||
if unifiedpush || contentEncoding == "aes128gcm" {
|
||||
@@ -1067,7 +1074,7 @@ func (s *Server) parsePublishParams(r *http.Request, m *message) (cache bool, fi
|
||||
// If file.txt is <= 4096 (message limit) and valid UTF-8, treat it as a message
|
||||
// 7. curl -T file.txt ntfy.sh/mytopic
|
||||
// In all other cases, mostly if file.txt is > message limit, treat it as an attachment
|
||||
func (s *Server) handlePublishBody(r *http.Request, v *visitor, m *message, body *util.PeekedReadCloser, template, unifiedpush bool) error {
|
||||
func (s *Server) handlePublishBody(r *http.Request, v *visitor, m *message, body *util.PeekedReadCloser, template templateMode, unifiedpush bool) error {
|
||||
if m.Event == pollRequestEvent { // Case 1
|
||||
return s.handleBodyDiscard(body)
|
||||
} else if unifiedpush {
|
||||
@@ -1076,8 +1083,8 @@ func (s *Server) handlePublishBody(r *http.Request, v *visitor, m *message, body
|
||||
return s.handleBodyAsTextMessage(m, body) // Case 3
|
||||
} else if m.Attachment != nil && m.Attachment.Name != "" {
|
||||
return s.handleBodyAsAttachment(r, v, m, body) // Case 4
|
||||
} else if template {
|
||||
return s.handleBodyAsTemplatedTextMessage(m, body) // Case 5
|
||||
} else if template.Enabled() {
|
||||
return s.handleBodyAsTemplatedTextMessage(m, template, body) // Case 5
|
||||
} else if !body.LimitReached && utf8.Valid(body.PeekedBytes) {
|
||||
return s.handleBodyAsTextMessage(m, body) // Case 6
|
||||
}
|
||||
@@ -1113,7 +1120,7 @@ func (s *Server) handleBodyAsTextMessage(m *message, body *util.PeekedReadCloser
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *Server) handleBodyAsTemplatedTextMessage(m *message, body *util.PeekedReadCloser) error {
|
||||
func (s *Server) handleBodyAsTemplatedTextMessage(m *message, template templateMode, body *util.PeekedReadCloser) error {
|
||||
body, err := util.Peek(body, max(s.config.MessageSizeLimit, jsonBodyBytesLimit))
|
||||
if err != nil {
|
||||
return err
|
||||
@@ -1121,19 +1128,69 @@ func (s *Server) handleBodyAsTemplatedTextMessage(m *message, body *util.PeekedR
|
||||
return errHTTPEntityTooLargeJSONBody
|
||||
}
|
||||
peekedBody := strings.TrimSpace(string(body.PeekedBytes))
|
||||
if m.Message, err = replaceTemplate(m.Message, peekedBody); err != nil {
|
||||
return err
|
||||
if templateName := template.Name(); templateName != "" {
|
||||
if err := s.renderTemplateFromFile(m, templateName, peekedBody); err != nil {
|
||||
return err
|
||||
}
|
||||
} else {
|
||||
if err := s.renderTemplateFromParams(m, peekedBody); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
if m.Title, err = replaceTemplate(m.Title, peekedBody); err != nil {
|
||||
return err
|
||||
}
|
||||
if len(m.Message) > s.config.MessageSizeLimit {
|
||||
if len(m.Title) > s.config.MessageSizeLimit || len(m.Message) > s.config.MessageSizeLimit {
|
||||
return errHTTPBadRequestTemplateMessageTooLarge
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func replaceTemplate(tpl string, source string) (string, error) {
|
||||
// renderTemplateFromFile transforms the JSON message body according to a template from the filesystem.
|
||||
// The template file must be in the templates directory, or in the configured template directory.
|
||||
func (s *Server) renderTemplateFromFile(m *message, templateName, peekedBody string) error {
|
||||
if !templateNameRegex.MatchString(templateName) {
|
||||
return errHTTPBadRequestTemplateFileNotFound
|
||||
}
|
||||
templateContent, _ := templatesFs.ReadFile(filepath.Join(templatesDir, templateName+templateFileExtension)) // Read from the embedded filesystem first
|
||||
if s.config.TemplateDir != "" {
|
||||
if b, _ := os.ReadFile(filepath.Join(s.config.TemplateDir, templateName+templateFileExtension)); len(b) > 0 {
|
||||
templateContent = b
|
||||
}
|
||||
}
|
||||
if len(templateContent) == 0 {
|
||||
return errHTTPBadRequestTemplateFileNotFound
|
||||
}
|
||||
var tpl templateFile
|
||||
if err := yaml.Unmarshal(templateContent, &tpl); err != nil {
|
||||
return errHTTPBadRequestTemplateFileInvalid
|
||||
}
|
||||
var err error
|
||||
if tpl.Message != nil {
|
||||
if m.Message, err = s.renderTemplate(*tpl.Message, peekedBody); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
if tpl.Title != nil {
|
||||
if m.Title, err = s.renderTemplate(*tpl.Title, peekedBody); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// renderTemplateFromParams transforms the JSON message body according to the inline template in the
|
||||
// message and title parameters.
|
||||
func (s *Server) renderTemplateFromParams(m *message, peekedBody string) error {
|
||||
var err error
|
||||
if m.Message, err = s.renderTemplate(m.Message, peekedBody); err != nil {
|
||||
return err
|
||||
}
|
||||
if m.Title, err = s.renderTemplate(m.Title, peekedBody); err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// renderTemplate renders a template with the given JSON source data.
|
||||
func (s *Server) renderTemplate(tpl string, source string) (string, error) {
|
||||
if templateDisallowedRegex.MatchString(tpl) {
|
||||
return "", errHTTPBadRequestTemplateDisallowedFunctionCalls
|
||||
}
|
||||
@@ -1141,15 +1198,16 @@ func replaceTemplate(tpl string, source string) (string, error) {
|
||||
if err := json.Unmarshal([]byte(source), &data); err != nil {
|
||||
return "", errHTTPBadRequestTemplateMessageNotJSON
|
||||
}
|
||||
t, err := template.New("").Parse(tpl)
|
||||
t, err := template.New("").Funcs(sprig.TxtFuncMap()).Parse(tpl)
|
||||
if err != nil {
|
||||
return "", errHTTPBadRequestTemplateInvalid
|
||||
return "", errHTTPBadRequestTemplateInvalid.Wrap("%s", err.Error())
|
||||
}
|
||||
var buf bytes.Buffer
|
||||
if err := t.Execute(util.NewTimeoutWriter(&buf, templateMaxExecutionTime), data); err != nil {
|
||||
return "", errHTTPBadRequestTemplateExecuteFailed
|
||||
limitWriter := util.NewLimitWriter(util.NewTimeoutWriter(&buf, templateMaxExecutionTime), util.NewFixedLimiter(templateMaxOutputBytes))
|
||||
if err := t.Execute(limitWriter, data); err != nil {
|
||||
return "", errHTTPBadRequestTemplateExecuteFailed.Wrap("%s", err.Error())
|
||||
}
|
||||
return buf.String(), nil
|
||||
return strings.TrimSpace(buf.String()), nil
|
||||
}
|
||||
|
||||
func (s *Server) handleBodyAsAttachment(r *http.Request, v *visitor, m *message, body *util.PeekedReadCloser) error {
|
||||
|
||||
@@ -126,6 +126,26 @@
|
||||
# attachment-file-size-limit: "15M"
|
||||
# attachment-expiry-duration: "3h"
|
||||
|
||||
# Template directory for message templates.
|
||||
#
|
||||
# When "X-Template: <name>" (aliases: "Template: <name>", "Tpl: <name>") or "?template=<name>" is set, transform the message
|
||||
# based on one of the built-in pre-defined templates, or on a template defined in the "template-dir" directory.
|
||||
#
|
||||
# Template files must have the ".yml" extension and must be formatted as YAML. They may contain "title" and "message" keys,
|
||||
# which are interpreted as Go templates.
|
||||
#
|
||||
# Example template file (e.g. /etc/ntfy/templates/grafana.yml):
|
||||
# title: |
|
||||
# {{- if eq .status "firing" }}
|
||||
# {{ .title | default "Alert firing" }}
|
||||
# {{- else if eq .status "resolved" }}
|
||||
# {{ .title | default "Alert resolved" }}
|
||||
# {{- end }}
|
||||
# message: |
|
||||
# {{ .message | trunc 2000 }}
|
||||
#
|
||||
# template-dir: "/etc/ntfy/templates"
|
||||
|
||||
# If enabled, allow outgoing e-mail notifications via the 'X-Email' header. If this header is set,
|
||||
# messages will additionally be sent out as e-mail using an external SMTP server.
|
||||
#
|
||||
|
||||
@@ -4,6 +4,7 @@ import (
|
||||
"bufio"
|
||||
"context"
|
||||
"crypto/rand"
|
||||
_ "embed"
|
||||
"encoding/base64"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
@@ -2917,7 +2918,7 @@ func TestServer_MessageTemplate_Range(t *testing.T) {
|
||||
|
||||
require.Equal(t, 200, response.Code)
|
||||
m := toMessage(t, response.Body.String())
|
||||
require.Equal(t, "Severe URLs:\n- https://severe1.com\n- https://severe2.com\n", m.Message)
|
||||
require.Equal(t, "Severe URLs:\n- https://severe1.com\n- https://severe2.com", m.Message)
|
||||
}
|
||||
|
||||
func TestServer_MessageTemplate_ExceedMessageSize_TemplatedMessageOK(t *testing.T) {
|
||||
@@ -2970,8 +2971,7 @@ Labels:
|
||||
Annotations:
|
||||
- summary = 15m load average too high
|
||||
Source: localhost:3000/alerting/grafana/NW9oDw-4z/view
|
||||
Silence: localhost:3000/alerting/silence/new?alertmanager=grafana&matcher=alertname%3DLoad+avg+15m+too+high&matcher=grafana_folder%3DNode+alerts&matcher=instance%3D10.108.0.2%3A9100&matcher=job%3Dnode-exporter
|
||||
`, m.Message)
|
||||
Silence: localhost:3000/alerting/silence/new?alertmanager=grafana&matcher=alertname%3DLoad+avg+15m+too+high&matcher=grafana_folder%3DNode+alerts&matcher=instance%3D10.108.0.2%3A9100&matcher=job%3Dnode-exporter`, m.Message)
|
||||
}
|
||||
|
||||
func TestServer_MessageTemplate_GitHub(t *testing.T) {
|
||||
@@ -3024,12 +3024,168 @@ template ""}}`,
|
||||
}
|
||||
}
|
||||
|
||||
func TestServer_MessageTemplate_SprigFunctions(t *testing.T) {
|
||||
t.Parallel()
|
||||
s := newTestServer(t, newTestConfig(t))
|
||||
bodies := []string{
|
||||
`{"foo":"bar","nested":{"title":"here"}}`,
|
||||
`{"topic":"ntfy-test"}`,
|
||||
`{"topic":"another-topic"}`,
|
||||
}
|
||||
templates := []string{
|
||||
`{{.foo | upper}} is {{.nested.title | repeat 3}}`,
|
||||
`{{if hasPrefix "ntfy-" .topic}}Topic: {{trimPrefix "ntfy-" .topic}}{{ else }}Topic: {{.topic}}{{end}}`,
|
||||
`{{if hasPrefix "ntfy-" .topic}}Topic: {{trimPrefix "ntfy-" .topic}}{{ else }}Topic: {{.topic}}{{end}}`,
|
||||
}
|
||||
targets := []string{
|
||||
`BAR is hereherehere`,
|
||||
`Topic: test`,
|
||||
`Topic: another-topic`,
|
||||
}
|
||||
for i, body := range bodies {
|
||||
template := templates[i]
|
||||
target := targets[i]
|
||||
t.Run(template, func(t *testing.T) {
|
||||
response := request(t, s, "PUT", `/mytopic`, body, map[string]string{
|
||||
"Template": "yes",
|
||||
"Message": template,
|
||||
})
|
||||
require.Equal(t, 200, response.Code)
|
||||
m := toMessage(t, response.Body.String())
|
||||
require.Equal(t, target, m.Message)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestServer_MessageTemplate_UnsafeSprigFunctions(t *testing.T) {
|
||||
t.Parallel()
|
||||
s := newTestServer(t, newTestConfig(t))
|
||||
response := request(t, s, "POST", "/mytopic", `{}`, map[string]string{
|
||||
"X-Message": `{{ env "PATH" }}`,
|
||||
"X-Template": "1",
|
||||
})
|
||||
|
||||
require.Equal(t, 400, response.Code)
|
||||
require.Equal(t, 40043, toHTTPError(t, response.Body.String()).Code)
|
||||
}
|
||||
|
||||
var (
|
||||
//go:embed testdata/webhook_github_comment_created.json
|
||||
githubCommentCreatedJSON string
|
||||
|
||||
//go:embed testdata/webhook_github_issue_opened.json
|
||||
githubIssueOpenedJSON string
|
||||
)
|
||||
|
||||
func TestServer_MessageTemplate_FromNamedTemplate_GitHubCommentCreated(t *testing.T) {
|
||||
t.Parallel()
|
||||
s := newTestServer(t, newTestConfig(t))
|
||||
response := request(t, s, "POST", "/mytopic?template=github", githubCommentCreatedJSON, nil)
|
||||
require.Equal(t, 200, response.Code)
|
||||
m := toMessage(t, response.Body.String())
|
||||
require.Equal(t, "💬 New comment on issue #1389 instant alerts without Pull to refresh", m.Title)
|
||||
require.Equal(t, `Commenter: https://github.com/wunter8
|
||||
Repository: https://github.com/binwiederhier/ntfy
|
||||
Comment link: https://github.com/binwiederhier/ntfy/issues/1389#issuecomment-3078214289
|
||||
|
||||
Comment:
|
||||
These are the things you need to do to get iOS push notifications to work:
|
||||
1. open a browser to the web app of your ntfy instance and copy the URL (including "http://" or "https://", your domain or IP address, and any ports, and excluding any trailing slashes)
|
||||
2. put the URL you copied in the ntfy `+"`"+`base-url`+"`"+` config in server.yml or NTFY_BASE_URL in env variables
|
||||
3. put the URL you copied in the default server URL setting in the iOS ntfy app
|
||||
4. set `+"`"+`upstream-base-url`+"`"+` in server.yml or NTFY_UPSTREAM_BASE_URL in env variables to "https://ntfy.sh" (without a trailing slash)`, m.Message)
|
||||
}
|
||||
|
||||
func TestServer_MessageTemplate_FromNamedTemplate_GitHubIssueOpened(t *testing.T) {
|
||||
t.Parallel()
|
||||
s := newTestServer(t, newTestConfig(t))
|
||||
response := request(t, s, "POST", "/mytopic?template=github", githubIssueOpenedJSON, nil)
|
||||
require.Equal(t, 200, response.Code)
|
||||
m := toMessage(t, response.Body.String())
|
||||
require.Equal(t, "🐛 Issue opened: #1391 http 500 error (ntfy error 50001)", m.Title)
|
||||
require.Equal(t, `Opened by: https://github.com/TheUser-dev
|
||||
Repository: https://github.com/binwiederhier/ntfy
|
||||
Issue link: https://github.com/binwiederhier/ntfy/issues/1391
|
||||
Labels: 🪲 bug
|
||||
|
||||
Description:
|
||||
:lady_beetle: **Describe the bug**
|
||||
When sending a notification (especially when it happens with multiple requests) this error occurs
|
||||
|
||||
:computer: **Components impacted**
|
||||
ntfy server 2.13.0 in docker, debian 12 arm64
|
||||
|
||||
:bulb: **Screenshots and/or logs**
|
||||
`+"```"+`
|
||||
closed with HTTP 500 (ntfy error 50001) (error=database table is locked, http_method=POST, http_path=/_matrix/push/v1/notify, tag=http, visitor_auth_limiter_limit=0.016666666666666666, visitor_auth_limiter_tokens=30, visitor_id=ip:<edited>, visitor_ip=<edited>, visitor_messages=448, visitor_messages_limit=17280, visitor_messages_remaining=16832, visitor_request_limiter_limit=0.2, visitor_request_limiter_tokens=57.049697891799994, visitor_seen=2025-07-16T15:06:35.429Z)
|
||||
`+"```"+`
|
||||
|
||||
:crystal_ball: **Additional context**
|
||||
Looks like this has already been fixed by #498, regression?`, m.Message)
|
||||
}
|
||||
|
||||
func TestServer_MessageTemplate_FromNamedTemplate_GitHubIssueOpened_OverrideConfigTemplate(t *testing.T) {
|
||||
t.Parallel()
|
||||
c := newTestConfig(t)
|
||||
c.TemplateDir = t.TempDir()
|
||||
require.NoError(t, os.WriteFile(filepath.Join(c.TemplateDir, "github.yml"), []byte(`
|
||||
title: |
|
||||
Custom title: action={{ .action }} trunctitle={{ .issue.title | trunc 10 }}
|
||||
message: |
|
||||
Custom message {{ .issue.number }}
|
||||
`), 0644))
|
||||
s := newTestServer(t, c)
|
||||
response := request(t, s, "POST", "/mytopic?template=github", githubIssueOpenedJSON, nil)
|
||||
fmt.Println(response.Body.String())
|
||||
require.Equal(t, 200, response.Code)
|
||||
m := toMessage(t, response.Body.String())
|
||||
require.Equal(t, "Custom title: action=opened trunctitle=http 500 e", m.Title)
|
||||
require.Equal(t, "Custom message 1391", m.Message)
|
||||
}
|
||||
|
||||
func TestServer_MessageTemplate_Repeat9999_TooLarge(t *testing.T) {
|
||||
t.Parallel()
|
||||
s := newTestServer(t, newTestConfig(t))
|
||||
response := request(t, s, "POST", "/mytopic", `{}`, map[string]string{
|
||||
"X-Message": `{{ repeat 9999 "mystring" }}`,
|
||||
"X-Template": "1",
|
||||
})
|
||||
require.Equal(t, 400, response.Code)
|
||||
require.Equal(t, 40041, toHTTPError(t, response.Body.String()).Code)
|
||||
require.Contains(t, toHTTPError(t, response.Body.String()).Message, "message or title is too large after replacing template")
|
||||
}
|
||||
|
||||
func TestServer_MessageTemplate_Repeat10001_TooLarge(t *testing.T) {
|
||||
t.Parallel()
|
||||
s := newTestServer(t, newTestConfig(t))
|
||||
response := request(t, s, "POST", "/mytopic", `{}`, map[string]string{
|
||||
"X-Message": `{{ repeat 10001 "mystring" }}`,
|
||||
"X-Template": "1",
|
||||
})
|
||||
require.Equal(t, 400, response.Code)
|
||||
require.Equal(t, 40045, toHTTPError(t, response.Body.String()).Code)
|
||||
require.Contains(t, toHTTPError(t, response.Body.String()).Message, "repeat count 10001 exceeds limit of 10000")
|
||||
}
|
||||
|
||||
func TestServer_MessageTemplate_Until100_000(t *testing.T) {
|
||||
t.Parallel()
|
||||
s := newTestServer(t, newTestConfig(t))
|
||||
response := request(t, s, "POST", "/mytopic", `{}`, map[string]string{
|
||||
"X-Message": `{{ range $i, $e := until 100_000 }}{{end}}`,
|
||||
"X-Template": "1",
|
||||
})
|
||||
require.Equal(t, 400, response.Code)
|
||||
require.Equal(t, 40045, toHTTPError(t, response.Body.String()).Code)
|
||||
require.Contains(t, toHTTPError(t, response.Body.String()).Message, "too many iterations")
|
||||
}
|
||||
|
||||
func newTestConfig(t *testing.T) *Config {
|
||||
conf := NewConfig()
|
||||
conf.BaseURL = "http://127.0.0.1:12345"
|
||||
conf.CacheFile = filepath.Join(t.TempDir(), "cache.db")
|
||||
conf.CacheStartupQueries = "pragma journal_mode = WAL; pragma synchronous = normal; pragma temp_store = memory;"
|
||||
conf.AttachmentCacheDir = t.TempDir()
|
||||
conf.TemplateDir = t.TempDir()
|
||||
return conf
|
||||
}
|
||||
|
||||
|
||||
@@ -192,12 +192,12 @@ func (s *smtpSession) publishMessage(m *message) error {
|
||||
// Call HTTP handler with fake HTTP request
|
||||
url := fmt.Sprintf("%s/%s", s.backend.config.BaseURL, m.Topic)
|
||||
req, err := http.NewRequest("POST", url, strings.NewReader(m.Message))
|
||||
req.RequestURI = "/" + m.Topic // just for the logs
|
||||
req.RemoteAddr = remoteAddr // rate limiting!!
|
||||
req.Header.Set(s.backend.config.ProxyForwardedHeader, remoteAddr) // Set X-Forwarded-For header
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
req.RequestURI = "/" + m.Topic // just for the logs
|
||||
req.RemoteAddr = remoteAddr // rate limiting!!
|
||||
req.Header.Set(s.backend.config.ProxyForwardedHeader, remoteAddr) // Set X-Forwarded-For header
|
||||
if m.Title != "" {
|
||||
req.Header.Set("Title", m.Title)
|
||||
}
|
||||
|
||||
27
server/templates/alertmanager.yml
Normal file
27
server/templates/alertmanager.yml
Normal file
@@ -0,0 +1,27 @@
|
||||
title: |
|
||||
{{- if eq .status "firing" }}
|
||||
🚨 Alert: {{ (first .alerts).labels.alertname }}
|
||||
{{- else if eq .status "resolved" }}
|
||||
✅ Resolved: {{ (first .alerts).labels.alertname }}
|
||||
{{- else }}
|
||||
{{ fail "Unsupported Alertmanager status." }}
|
||||
{{- end }}
|
||||
message: |
|
||||
Status: {{ .status | title }}
|
||||
Receiver: {{ .receiver }}
|
||||
|
||||
{{- range .alerts }}
|
||||
Alert: {{ .labels.alertname }}
|
||||
Instance: {{ .labels.instance }}
|
||||
Severity: {{ .labels.severity }}
|
||||
Starts at: {{ .startsAt }}
|
||||
{{- if .endsAt }}Ends at: {{ .endsAt }}{{ end }}
|
||||
{{- if .annotations.summary }}
|
||||
Summary: {{ .annotations.summary }}
|
||||
{{- end }}
|
||||
{{- if .annotations.description }}
|
||||
Description: {{ .annotations.description }}
|
||||
{{- end }}
|
||||
Source: {{ .generatorURL }}
|
||||
|
||||
{{ end }}
|
||||
57
server/templates/github.yml
Normal file
57
server/templates/github.yml
Normal file
@@ -0,0 +1,57 @@
|
||||
title: |
|
||||
{{- if and .starred_at (eq .action "created")}}
|
||||
⭐ {{ .sender.login }} starred {{ .repository.name }}
|
||||
|
||||
{{- else if and .repository (eq .action "started")}}
|
||||
👀 {{ .sender.login }} started watching {{ .repository.name }}
|
||||
|
||||
{{- else if and .comment (eq .action "created") }}
|
||||
💬 New comment on issue #{{ .issue.number }} {{ .issue.title }}
|
||||
|
||||
{{- else if .pull_request }}
|
||||
🔀 Pull request {{ .action }}: #{{ .pull_request.number }} {{ .pull_request.title }}
|
||||
|
||||
{{- else if .issue }}
|
||||
🐛 Issue {{ .action }}: #{{ .issue.number }} {{ .issue.title }}
|
||||
|
||||
{{- else }}
|
||||
{{ fail "Unsupported GitHub event type or action." }}
|
||||
{{- end }}
|
||||
message: |
|
||||
{{ if and .starred_at (eq .action "created")}}
|
||||
Stargazer: {{ .sender.html_url }}
|
||||
Repository: {{ .repository.html_url }}
|
||||
|
||||
{{- else if and .repository (eq .action "started")}}
|
||||
Watcher: {{ .sender.html_url }}
|
||||
Repository: {{ .repository.html_url }}
|
||||
|
||||
{{- else if and .comment (eq .action "created") }}
|
||||
Commenter: {{ .comment.user.html_url }}
|
||||
Repository: {{ .repository.html_url }}
|
||||
Comment link: {{ .comment.html_url }}
|
||||
{{ if .comment.body }}
|
||||
Comment:
|
||||
{{ .comment.body | trunc 2000 }}{{ end }}
|
||||
|
||||
{{- else if .pull_request }}
|
||||
Branch: {{ .pull_request.head.ref }} → {{ .pull_request.base.ref }}
|
||||
{{ .action | title }} by: {{ .pull_request.user.html_url }}
|
||||
Repository: {{ .repository.html_url }}
|
||||
Pull request: {{ .pull_request.html_url }}
|
||||
{{ if .pull_request.body }}
|
||||
Description:
|
||||
{{ .pull_request.body | trunc 2000 }}{{ end }}
|
||||
|
||||
{{- else if .issue }}
|
||||
{{ .action | title }} by: {{ .issue.user.html_url }}
|
||||
Repository: {{ .repository.html_url }}
|
||||
Issue link: {{ .issue.html_url }}
|
||||
{{ if .issue.labels }}Labels: {{ range .issue.labels }}{{ .name }} {{ end }}{{ end }}
|
||||
{{ if .issue.body }}
|
||||
Description:
|
||||
{{ .issue.body | trunc 2000 }}{{ end }}
|
||||
|
||||
{{- else }}
|
||||
{{ fail "Unsupported GitHub event type or action." }}
|
||||
{{- end }}
|
||||
10
server/templates/grafana.yml
Normal file
10
server/templates/grafana.yml
Normal file
@@ -0,0 +1,10 @@
|
||||
title: |
|
||||
{{- if eq .status "firing" }}
|
||||
🚨 {{ .title | default "Alert firing" }}
|
||||
{{- else if eq .status "resolved" }}
|
||||
✅ {{ .title | default "Alert resolved" }}
|
||||
{{- else }}
|
||||
⚠️ Unknown alert: {{ .title | default "Alert" }}
|
||||
{{- end }}
|
||||
message: |
|
||||
{{ .message | trunc 2000 }}
|
||||
33
server/testdata/webhook_alertmanager_firing.json
vendored
Normal file
33
server/testdata/webhook_alertmanager_firing.json
vendored
Normal file
@@ -0,0 +1,33 @@
|
||||
{
|
||||
"version": "4",
|
||||
"groupKey": "...",
|
||||
"status": "firing",
|
||||
"receiver": "webhook-receiver",
|
||||
"groupLabels": {
|
||||
"alertname": "HighCPUUsage"
|
||||
},
|
||||
"commonLabels": {
|
||||
"alertname": "HighCPUUsage",
|
||||
"instance": "server01",
|
||||
"severity": "critical"
|
||||
},
|
||||
"commonAnnotations": {
|
||||
"summary": "High CPU usage detected"
|
||||
},
|
||||
"alerts": [
|
||||
{
|
||||
"status": "firing",
|
||||
"labels": {
|
||||
"alertname": "HighCPUUsage",
|
||||
"instance": "server01",
|
||||
"severity": "critical"
|
||||
},
|
||||
"annotations": {
|
||||
"summary": "High CPU usage detected"
|
||||
},
|
||||
"startsAt": "2025-07-17T07:00:00Z",
|
||||
"endsAt": "0001-01-01T00:00:00Z",
|
||||
"generatorURL": "http://prometheus.local/graph?g0.expr=..."
|
||||
}
|
||||
]
|
||||
}
|
||||
261
server/testdata/webhook_github_comment_created.json
vendored
Normal file
261
server/testdata/webhook_github_comment_created.json
vendored
Normal file
@@ -0,0 +1,261 @@
|
||||
{
|
||||
"action": "created",
|
||||
"issue": {
|
||||
"url": "https://api.github.com/repos/binwiederhier/ntfy/issues/1389",
|
||||
"repository_url": "https://api.github.com/repos/binwiederhier/ntfy",
|
||||
"labels_url": "https://api.github.com/repos/binwiederhier/ntfy/issues/1389/labels{/name}",
|
||||
"comments_url": "https://api.github.com/repos/binwiederhier/ntfy/issues/1389/comments",
|
||||
"events_url": "https://api.github.com/repos/binwiederhier/ntfy/issues/1389/events",
|
||||
"html_url": "https://github.com/binwiederhier/ntfy/issues/1389",
|
||||
"id": 3230655753,
|
||||
"node_id": "I_kwDOGRBhi87Aj-UJ",
|
||||
"number": 1389,
|
||||
"title": "instant alerts without Pull to refresh",
|
||||
"user": {
|
||||
"login": "edbraunh",
|
||||
"id": 8795846,
|
||||
"node_id": "MDQ6VXNlcjg3OTU4NDY=",
|
||||
"avatar_url": "https://avatars.githubusercontent.com/u/8795846?v=4",
|
||||
"gravatar_id": "",
|
||||
"url": "https://api.github.com/users/edbraunh",
|
||||
"html_url": "https://github.com/edbraunh",
|
||||
"followers_url": "https://api.github.com/users/edbraunh/followers",
|
||||
"following_url": "https://api.github.com/users/edbraunh/following{/other_user}",
|
||||
"gists_url": "https://api.github.com/users/edbraunh/gists{/gist_id}",
|
||||
"starred_url": "https://api.github.com/users/edbraunh/starred{/owner}{/repo}",
|
||||
"subscriptions_url": "https://api.github.com/users/edbraunh/subscriptions",
|
||||
"organizations_url": "https://api.github.com/users/edbraunh/orgs",
|
||||
"repos_url": "https://api.github.com/users/edbraunh/repos",
|
||||
"events_url": "https://api.github.com/users/edbraunh/events{/privacy}",
|
||||
"received_events_url": "https://api.github.com/users/edbraunh/received_events",
|
||||
"type": "User",
|
||||
"user_view_type": "public",
|
||||
"site_admin": false
|
||||
},
|
||||
"labels": [
|
||||
{
|
||||
"id": 3480884105,
|
||||
"node_id": "LA_kwDOGRBhi87PehOJ",
|
||||
"url": "https://api.github.com/repos/binwiederhier/ntfy/labels/enhancement",
|
||||
"name": "enhancement",
|
||||
"color": "a2eeef",
|
||||
"default": true,
|
||||
"description": "New feature or request"
|
||||
}
|
||||
],
|
||||
"state": "open",
|
||||
"locked": false,
|
||||
"assignee": null,
|
||||
"assignees": [
|
||||
],
|
||||
"milestone": null,
|
||||
"comments": 3,
|
||||
"created_at": "2025-07-15T03:46:30Z",
|
||||
"updated_at": "2025-07-16T11:45:57Z",
|
||||
"closed_at": null,
|
||||
"author_association": "NONE",
|
||||
"active_lock_reason": null,
|
||||
"sub_issues_summary": {
|
||||
"total": 0,
|
||||
"completed": 0,
|
||||
"percent_completed": 0
|
||||
},
|
||||
"body": "Hello ntfy Team,\n\nFirst off, thank you for developing such a powerful and lightweight notification app — it’s been invaluable for receiving timely alerts.\n\nI’m a user who relies heavily on ntfy for real-time trading alerts and have noticed that while push notifications arrive instantly, the in-app alert list does not automatically refresh with new messages. Currently, I need to manually pull-to-refresh the alert list to see the latest alerts.\n\nWould it be possible to add a feature that enables automatic refreshing of the alert list as new notifications arrive? This would greatly enhance usability and streamline the user experience, especially for users monitoring time-sensitive information.\n\nThank you for considering this request. I appreciate your hard work and look forward to future updates!",
|
||||
"reactions": {
|
||||
"url": "https://api.github.com/repos/binwiederhier/ntfy/issues/1389/reactions",
|
||||
"total_count": 0,
|
||||
"+1": 0,
|
||||
"-1": 0,
|
||||
"laugh": 0,
|
||||
"hooray": 0,
|
||||
"confused": 0,
|
||||
"heart": 0,
|
||||
"rocket": 0,
|
||||
"eyes": 0
|
||||
},
|
||||
"timeline_url": "https://api.github.com/repos/binwiederhier/ntfy/issues/1389/timeline",
|
||||
"performed_via_github_app": null,
|
||||
"state_reason": null
|
||||
},
|
||||
"comment": {
|
||||
"url": "https://api.github.com/repos/binwiederhier/ntfy/issues/comments/3078214289",
|
||||
"html_url": "https://github.com/binwiederhier/ntfy/issues/1389#issuecomment-3078214289",
|
||||
"issue_url": "https://api.github.com/repos/binwiederhier/ntfy/issues/1389",
|
||||
"id": 3078214289,
|
||||
"node_id": "IC_kwDOGRBhi863edKR",
|
||||
"user": {
|
||||
"login": "wunter8",
|
||||
"id": 8421688,
|
||||
"node_id": "MDQ6VXNlcjg0MjE2ODg=",
|
||||
"avatar_url": "https://avatars.githubusercontent.com/u/8421688?v=4",
|
||||
"gravatar_id": "",
|
||||
"url": "https://api.github.com/users/wunter8",
|
||||
"html_url": "https://github.com/wunter8",
|
||||
"followers_url": "https://api.github.com/users/wunter8/followers",
|
||||
"following_url": "https://api.github.com/users/wunter8/following{/other_user}",
|
||||
"gists_url": "https://api.github.com/users/wunter8/gists{/gist_id}",
|
||||
"starred_url": "https://api.github.com/users/wunter8/starred{/owner}{/repo}",
|
||||
"subscriptions_url": "https://api.github.com/users/wunter8/subscriptions",
|
||||
"organizations_url": "https://api.github.com/users/wunter8/orgs",
|
||||
"repos_url": "https://api.github.com/users/wunter8/repos",
|
||||
"events_url": "https://api.github.com/users/wunter8/events{/privacy}",
|
||||
"received_events_url": "https://api.github.com/users/wunter8/received_events",
|
||||
"type": "User",
|
||||
"user_view_type": "public",
|
||||
"site_admin": false
|
||||
},
|
||||
"created_at": "2025-07-16T11:45:57Z",
|
||||
"updated_at": "2025-07-16T11:45:57Z",
|
||||
"author_association": "CONTRIBUTOR",
|
||||
"body": "These are the things you need to do to get iOS push notifications to work:\n1. open a browser to the web app of your ntfy instance and copy the URL (including \"http://\" or \"https://\", your domain or IP address, and any ports, and excluding any trailing slashes)\n2. put the URL you copied in the ntfy `base-url` config in server.yml or NTFY_BASE_URL in env variables\n3. put the URL you copied in the default server URL setting in the iOS ntfy app\n4. set `upstream-base-url` in server.yml or NTFY_UPSTREAM_BASE_URL in env variables to \"https://ntfy.sh\" (without a trailing slash)",
|
||||
"reactions": {
|
||||
"url": "https://api.github.com/repos/binwiederhier/ntfy/issues/comments/3078214289/reactions",
|
||||
"total_count": 0,
|
||||
"+1": 0,
|
||||
"-1": 0,
|
||||
"laugh": 0,
|
||||
"hooray": 0,
|
||||
"confused": 0,
|
||||
"heart": 0,
|
||||
"rocket": 0,
|
||||
"eyes": 0
|
||||
},
|
||||
"performed_via_github_app": null
|
||||
},
|
||||
"repository": {
|
||||
"id": 420503947,
|
||||
"node_id": "R_kgDOGRBhiw",
|
||||
"name": "ntfy",
|
||||
"full_name": "binwiederhier/ntfy",
|
||||
"private": false,
|
||||
"owner": {
|
||||
"login": "binwiederhier",
|
||||
"id": 664597,
|
||||
"node_id": "MDQ6VXNlcjY2NDU5Nw==",
|
||||
"avatar_url": "https://avatars.githubusercontent.com/u/664597?v=4",
|
||||
"gravatar_id": "",
|
||||
"url": "https://api.github.com/users/binwiederhier",
|
||||
"html_url": "https://github.com/binwiederhier",
|
||||
"followers_url": "https://api.github.com/users/binwiederhier/followers",
|
||||
"following_url": "https://api.github.com/users/binwiederhier/following{/other_user}",
|
||||
"gists_url": "https://api.github.com/users/binwiederhier/gists{/gist_id}",
|
||||
"starred_url": "https://api.github.com/users/binwiederhier/starred{/owner}{/repo}",
|
||||
"subscriptions_url": "https://api.github.com/users/binwiederhier/subscriptions",
|
||||
"organizations_url": "https://api.github.com/users/binwiederhier/orgs",
|
||||
"repos_url": "https://api.github.com/users/binwiederhier/repos",
|
||||
"events_url": "https://api.github.com/users/binwiederhier/events{/privacy}",
|
||||
"received_events_url": "https://api.github.com/users/binwiederhier/received_events",
|
||||
"type": "User",
|
||||
"user_view_type": "public",
|
||||
"site_admin": false
|
||||
},
|
||||
"html_url": "https://github.com/binwiederhier/ntfy",
|
||||
"description": "Send push notifications to your phone or desktop using PUT/POST",
|
||||
"fork": false,
|
||||
"url": "https://api.github.com/repos/binwiederhier/ntfy",
|
||||
"forks_url": "https://api.github.com/repos/binwiederhier/ntfy/forks",
|
||||
"keys_url": "https://api.github.com/repos/binwiederhier/ntfy/keys{/key_id}",
|
||||
"collaborators_url": "https://api.github.com/repos/binwiederhier/ntfy/collaborators{/collaborator}",
|
||||
"teams_url": "https://api.github.com/repos/binwiederhier/ntfy/teams",
|
||||
"hooks_url": "https://api.github.com/repos/binwiederhier/ntfy/hooks",
|
||||
"issue_events_url": "https://api.github.com/repos/binwiederhier/ntfy/issues/events{/number}",
|
||||
"events_url": "https://api.github.com/repos/binwiederhier/ntfy/events",
|
||||
"assignees_url": "https://api.github.com/repos/binwiederhier/ntfy/assignees{/user}",
|
||||
"branches_url": "https://api.github.com/repos/binwiederhier/ntfy/branches{/branch}",
|
||||
"tags_url": "https://api.github.com/repos/binwiederhier/ntfy/tags",
|
||||
"blobs_url": "https://api.github.com/repos/binwiederhier/ntfy/git/blobs{/sha}",
|
||||
"git_tags_url": "https://api.github.com/repos/binwiederhier/ntfy/git/tags{/sha}",
|
||||
"git_refs_url": "https://api.github.com/repos/binwiederhier/ntfy/git/refs{/sha}",
|
||||
"trees_url": "https://api.github.com/repos/binwiederhier/ntfy/git/trees{/sha}",
|
||||
"statuses_url": "https://api.github.com/repos/binwiederhier/ntfy/statuses/{sha}",
|
||||
"languages_url": "https://api.github.com/repos/binwiederhier/ntfy/languages",
|
||||
"stargazers_url": "https://api.github.com/repos/binwiederhier/ntfy/stargazers",
|
||||
"contributors_url": "https://api.github.com/repos/binwiederhier/ntfy/contributors",
|
||||
"subscribers_url": "https://api.github.com/repos/binwiederhier/ntfy/subscribers",
|
||||
"subscription_url": "https://api.github.com/repos/binwiederhier/ntfy/subscription",
|
||||
"commits_url": "https://api.github.com/repos/binwiederhier/ntfy/commits{/sha}",
|
||||
"git_commits_url": "https://api.github.com/repos/binwiederhier/ntfy/git/commits{/sha}",
|
||||
"comments_url": "https://api.github.com/repos/binwiederhier/ntfy/comments{/number}",
|
||||
"issue_comment_url": "https://api.github.com/repos/binwiederhier/ntfy/issues/comments{/number}",
|
||||
"contents_url": "https://api.github.com/repos/binwiederhier/ntfy/contents/{+path}",
|
||||
"compare_url": "https://api.github.com/repos/binwiederhier/ntfy/compare/{base}...{head}",
|
||||
"merges_url": "https://api.github.com/repos/binwiederhier/ntfy/merges",
|
||||
"archive_url": "https://api.github.com/repos/binwiederhier/ntfy/{archive_format}{/ref}",
|
||||
"downloads_url": "https://api.github.com/repos/binwiederhier/ntfy/downloads",
|
||||
"issues_url": "https://api.github.com/repos/binwiederhier/ntfy/issues{/number}",
|
||||
"pulls_url": "https://api.github.com/repos/binwiederhier/ntfy/pulls{/number}",
|
||||
"milestones_url": "https://api.github.com/repos/binwiederhier/ntfy/milestones{/number}",
|
||||
"notifications_url": "https://api.github.com/repos/binwiederhier/ntfy/notifications{?since,all,participating}",
|
||||
"labels_url": "https://api.github.com/repos/binwiederhier/ntfy/labels{/name}",
|
||||
"releases_url": "https://api.github.com/repos/binwiederhier/ntfy/releases{/id}",
|
||||
"deployments_url": "https://api.github.com/repos/binwiederhier/ntfy/deployments",
|
||||
"created_at": "2021-10-23T19:25:32Z",
|
||||
"updated_at": "2025-07-16T10:18:34Z",
|
||||
"pushed_at": "2025-07-13T13:56:19Z",
|
||||
"git_url": "git://github.com/binwiederhier/ntfy.git",
|
||||
"ssh_url": "git@github.com:binwiederhier/ntfy.git",
|
||||
"clone_url": "https://github.com/binwiederhier/ntfy.git",
|
||||
"svn_url": "https://github.com/binwiederhier/ntfy",
|
||||
"homepage": "https://ntfy.sh",
|
||||
"size": 36740,
|
||||
"stargazers_count": 25111,
|
||||
"watchers_count": 25111,
|
||||
"language": "Go",
|
||||
"has_issues": true,
|
||||
"has_projects": true,
|
||||
"has_downloads": true,
|
||||
"has_wiki": true,
|
||||
"has_pages": false,
|
||||
"has_discussions": false,
|
||||
"forks_count": 984,
|
||||
"mirror_url": null,
|
||||
"archived": false,
|
||||
"disabled": false,
|
||||
"open_issues_count": 367,
|
||||
"license": {
|
||||
"key": "apache-2.0",
|
||||
"name": "Apache License 2.0",
|
||||
"spdx_id": "Apache-2.0",
|
||||
"url": "https://api.github.com/licenses/apache-2.0",
|
||||
"node_id": "MDc6TGljZW5zZTI="
|
||||
},
|
||||
"allow_forking": true,
|
||||
"is_template": false,
|
||||
"web_commit_signoff_required": false,
|
||||
"topics": [
|
||||
"curl",
|
||||
"notifications",
|
||||
"ntfy",
|
||||
"ntfysh",
|
||||
"pubsub",
|
||||
"push-notifications",
|
||||
"rest-api"
|
||||
],
|
||||
"visibility": "public",
|
||||
"forks": 984,
|
||||
"open_issues": 367,
|
||||
"watchers": 25111,
|
||||
"default_branch": "main"
|
||||
},
|
||||
"sender": {
|
||||
"login": "wunter8",
|
||||
"id": 8421688,
|
||||
"node_id": "MDQ6VXNlcjg0MjE2ODg=",
|
||||
"avatar_url": "https://avatars.githubusercontent.com/u/8421688?v=4",
|
||||
"gravatar_id": "",
|
||||
"url": "https://api.github.com/users/wunter8",
|
||||
"html_url": "https://github.com/wunter8",
|
||||
"followers_url": "https://api.github.com/users/wunter8/followers",
|
||||
"following_url": "https://api.github.com/users/wunter8/following{/other_user}",
|
||||
"gists_url": "https://api.github.com/users/wunter8/gists{/gist_id}",
|
||||
"starred_url": "https://api.github.com/users/wunter8/starred{/owner}{/repo}",
|
||||
"subscriptions_url": "https://api.github.com/users/wunter8/subscriptions",
|
||||
"organizations_url": "https://api.github.com/users/wunter8/orgs",
|
||||
"repos_url": "https://api.github.com/users/wunter8/repos",
|
||||
"events_url": "https://api.github.com/users/wunter8/events{/privacy}",
|
||||
"received_events_url": "https://api.github.com/users/wunter8/received_events",
|
||||
"type": "User",
|
||||
"user_view_type": "public",
|
||||
"site_admin": false
|
||||
}
|
||||
}
|
||||
216
server/testdata/webhook_github_issue_opened.json
vendored
Normal file
216
server/testdata/webhook_github_issue_opened.json
vendored
Normal file
@@ -0,0 +1,216 @@
|
||||
{
|
||||
"action": "opened",
|
||||
"issue": {
|
||||
"url": "https://api.github.com/repos/binwiederhier/ntfy/issues/1391",
|
||||
"repository_url": "https://api.github.com/repos/binwiederhier/ntfy",
|
||||
"labels_url": "https://api.github.com/repos/binwiederhier/ntfy/issues/1391/labels{/name}",
|
||||
"comments_url": "https://api.github.com/repos/binwiederhier/ntfy/issues/1391/comments",
|
||||
"events_url": "https://api.github.com/repos/binwiederhier/ntfy/issues/1391/events",
|
||||
"html_url": "https://github.com/binwiederhier/ntfy/issues/1391",
|
||||
"id": 3236389051,
|
||||
"node_id": "I_kwDOGRBhi87A52C7",
|
||||
"number": 1391,
|
||||
"title": "http 500 error (ntfy error 50001)",
|
||||
"user": {
|
||||
"login": "TheUser-dev",
|
||||
"id": 213207407,
|
||||
"node_id": "U_kgDODLVJbw",
|
||||
"avatar_url": "https://avatars.githubusercontent.com/u/213207407?v=4",
|
||||
"gravatar_id": "",
|
||||
"url": "https://api.github.com/users/TheUser-dev",
|
||||
"html_url": "https://github.com/TheUser-dev",
|
||||
"followers_url": "https://api.github.com/users/TheUser-dev/followers",
|
||||
"following_url": "https://api.github.com/users/TheUser-dev/following{/other_user}",
|
||||
"gists_url": "https://api.github.com/users/TheUser-dev/gists{/gist_id}",
|
||||
"starred_url": "https://api.github.com/users/TheUser-dev/starred{/owner}{/repo}",
|
||||
"subscriptions_url": "https://api.github.com/users/TheUser-dev/subscriptions",
|
||||
"organizations_url": "https://api.github.com/users/TheUser-dev/orgs",
|
||||
"repos_url": "https://api.github.com/users/TheUser-dev/repos",
|
||||
"events_url": "https://api.github.com/users/TheUser-dev/events{/privacy}",
|
||||
"received_events_url": "https://api.github.com/users/TheUser-dev/received_events",
|
||||
"type": "User",
|
||||
"user_view_type": "public",
|
||||
"site_admin": false
|
||||
},
|
||||
"labels": [
|
||||
{
|
||||
"id": 3480884102,
|
||||
"node_id": "LA_kwDOGRBhi87PehOG",
|
||||
"url": "https://api.github.com/repos/binwiederhier/ntfy/labels/%F0%9F%AA%B2%20bug",
|
||||
"name": "🪲 bug",
|
||||
"color": "d73a4a",
|
||||
"default": false,
|
||||
"description": "Something isn't working"
|
||||
}
|
||||
],
|
||||
"state": "open",
|
||||
"locked": false,
|
||||
"assignee": null,
|
||||
"assignees": [
|
||||
],
|
||||
"milestone": null,
|
||||
"comments": 0,
|
||||
"created_at": "2025-07-16T15:20:56Z",
|
||||
"updated_at": "2025-07-16T15:20:56Z",
|
||||
"closed_at": null,
|
||||
"author_association": "NONE",
|
||||
"active_lock_reason": null,
|
||||
"sub_issues_summary": {
|
||||
"total": 0,
|
||||
"completed": 0,
|
||||
"percent_completed": 0
|
||||
},
|
||||
"body": ":lady_beetle: **Describe the bug**\nWhen sending a notification (especially when it happens with multiple requests) this error occurs\n\n:computer: **Components impacted**\nntfy server 2.13.0 in docker, debian 12 arm64\n\n:bulb: **Screenshots and/or logs**\n```\nclosed with HTTP 500 (ntfy error 50001) (error=database table is locked, http_method=POST, http_path=/_matrix/push/v1/notify, tag=http, visitor_auth_limiter_limit=0.016666666666666666, visitor_auth_limiter_tokens=30, visitor_id=ip:<edited>, visitor_ip=<edited>, visitor_messages=448, visitor_messages_limit=17280, visitor_messages_remaining=16832, visitor_request_limiter_limit=0.2, visitor_request_limiter_tokens=57.049697891799994, visitor_seen=2025-07-16T15:06:35.429Z)\n```\n\n:crystal_ball: **Additional context**\nLooks like this has already been fixed by #498, regression?\n",
|
||||
"reactions": {
|
||||
"url": "https://api.github.com/repos/binwiederhier/ntfy/issues/1391/reactions",
|
||||
"total_count": 0,
|
||||
"+1": 0,
|
||||
"-1": 0,
|
||||
"laugh": 0,
|
||||
"hooray": 0,
|
||||
"confused": 0,
|
||||
"heart": 0,
|
||||
"rocket": 0,
|
||||
"eyes": 0
|
||||
},
|
||||
"timeline_url": "https://api.github.com/repos/binwiederhier/ntfy/issues/1391/timeline",
|
||||
"performed_via_github_app": null,
|
||||
"state_reason": null
|
||||
},
|
||||
"repository": {
|
||||
"id": 420503947,
|
||||
"node_id": "R_kgDOGRBhiw",
|
||||
"name": "ntfy",
|
||||
"full_name": "binwiederhier/ntfy",
|
||||
"private": false,
|
||||
"owner": {
|
||||
"login": "binwiederhier",
|
||||
"id": 664597,
|
||||
"node_id": "MDQ6VXNlcjY2NDU5Nw==",
|
||||
"avatar_url": "https://avatars.githubusercontent.com/u/664597?v=4",
|
||||
"gravatar_id": "",
|
||||
"url": "https://api.github.com/users/binwiederhier",
|
||||
"html_url": "https://github.com/binwiederhier",
|
||||
"followers_url": "https://api.github.com/users/binwiederhier/followers",
|
||||
"following_url": "https://api.github.com/users/binwiederhier/following{/other_user}",
|
||||
"gists_url": "https://api.github.com/users/binwiederhier/gists{/gist_id}",
|
||||
"starred_url": "https://api.github.com/users/binwiederhier/starred{/owner}{/repo}",
|
||||
"subscriptions_url": "https://api.github.com/users/binwiederhier/subscriptions",
|
||||
"organizations_url": "https://api.github.com/users/binwiederhier/orgs",
|
||||
"repos_url": "https://api.github.com/users/binwiederhier/repos",
|
||||
"events_url": "https://api.github.com/users/binwiederhier/events{/privacy}",
|
||||
"received_events_url": "https://api.github.com/users/binwiederhier/received_events",
|
||||
"type": "User",
|
||||
"user_view_type": "public",
|
||||
"site_admin": false
|
||||
},
|
||||
"html_url": "https://github.com/binwiederhier/ntfy",
|
||||
"description": "Send push notifications to your phone or desktop using PUT/POST",
|
||||
"fork": false,
|
||||
"url": "https://api.github.com/repos/binwiederhier/ntfy",
|
||||
"forks_url": "https://api.github.com/repos/binwiederhier/ntfy/forks",
|
||||
"keys_url": "https://api.github.com/repos/binwiederhier/ntfy/keys{/key_id}",
|
||||
"collaborators_url": "https://api.github.com/repos/binwiederhier/ntfy/collaborators{/collaborator}",
|
||||
"teams_url": "https://api.github.com/repos/binwiederhier/ntfy/teams",
|
||||
"hooks_url": "https://api.github.com/repos/binwiederhier/ntfy/hooks",
|
||||
"issue_events_url": "https://api.github.com/repos/binwiederhier/ntfy/issues/events{/number}",
|
||||
"events_url": "https://api.github.com/repos/binwiederhier/ntfy/events",
|
||||
"assignees_url": "https://api.github.com/repos/binwiederhier/ntfy/assignees{/user}",
|
||||
"branches_url": "https://api.github.com/repos/binwiederhier/ntfy/branches{/branch}",
|
||||
"tags_url": "https://api.github.com/repos/binwiederhier/ntfy/tags",
|
||||
"blobs_url": "https://api.github.com/repos/binwiederhier/ntfy/git/blobs{/sha}",
|
||||
"git_tags_url": "https://api.github.com/repos/binwiederhier/ntfy/git/tags{/sha}",
|
||||
"git_refs_url": "https://api.github.com/repos/binwiederhier/ntfy/git/refs{/sha}",
|
||||
"trees_url": "https://api.github.com/repos/binwiederhier/ntfy/git/trees{/sha}",
|
||||
"statuses_url": "https://api.github.com/repos/binwiederhier/ntfy/statuses/{sha}",
|
||||
"languages_url": "https://api.github.com/repos/binwiederhier/ntfy/languages",
|
||||
"stargazers_url": "https://api.github.com/repos/binwiederhier/ntfy/stargazers",
|
||||
"contributors_url": "https://api.github.com/repos/binwiederhier/ntfy/contributors",
|
||||
"subscribers_url": "https://api.github.com/repos/binwiederhier/ntfy/subscribers",
|
||||
"subscription_url": "https://api.github.com/repos/binwiederhier/ntfy/subscription",
|
||||
"commits_url": "https://api.github.com/repos/binwiederhier/ntfy/commits{/sha}",
|
||||
"git_commits_url": "https://api.github.com/repos/binwiederhier/ntfy/git/commits{/sha}",
|
||||
"comments_url": "https://api.github.com/repos/binwiederhier/ntfy/comments{/number}",
|
||||
"issue_comment_url": "https://api.github.com/repos/binwiederhier/ntfy/issues/comments{/number}",
|
||||
"contents_url": "https://api.github.com/repos/binwiederhier/ntfy/contents/{+path}",
|
||||
"compare_url": "https://api.github.com/repos/binwiederhier/ntfy/compare/{base}...{head}",
|
||||
"merges_url": "https://api.github.com/repos/binwiederhier/ntfy/merges",
|
||||
"archive_url": "https://api.github.com/repos/binwiederhier/ntfy/{archive_format}{/ref}",
|
||||
"downloads_url": "https://api.github.com/repos/binwiederhier/ntfy/downloads",
|
||||
"issues_url": "https://api.github.com/repos/binwiederhier/ntfy/issues{/number}",
|
||||
"pulls_url": "https://api.github.com/repos/binwiederhier/ntfy/pulls{/number}",
|
||||
"milestones_url": "https://api.github.com/repos/binwiederhier/ntfy/milestones{/number}",
|
||||
"notifications_url": "https://api.github.com/repos/binwiederhier/ntfy/notifications{?since,all,participating}",
|
||||
"labels_url": "https://api.github.com/repos/binwiederhier/ntfy/labels{/name}",
|
||||
"releases_url": "https://api.github.com/repos/binwiederhier/ntfy/releases{/id}",
|
||||
"deployments_url": "https://api.github.com/repos/binwiederhier/ntfy/deployments",
|
||||
"created_at": "2021-10-23T19:25:32Z",
|
||||
"updated_at": "2025-07-16T14:54:16Z",
|
||||
"pushed_at": "2025-07-16T11:49:26Z",
|
||||
"git_url": "git://github.com/binwiederhier/ntfy.git",
|
||||
"ssh_url": "git@github.com:binwiederhier/ntfy.git",
|
||||
"clone_url": "https://github.com/binwiederhier/ntfy.git",
|
||||
"svn_url": "https://github.com/binwiederhier/ntfy",
|
||||
"homepage": "https://ntfy.sh",
|
||||
"size": 36831,
|
||||
"stargazers_count": 25112,
|
||||
"watchers_count": 25112,
|
||||
"language": "Go",
|
||||
"has_issues": true,
|
||||
"has_projects": true,
|
||||
"has_downloads": true,
|
||||
"has_wiki": true,
|
||||
"has_pages": false,
|
||||
"has_discussions": false,
|
||||
"forks_count": 984,
|
||||
"mirror_url": null,
|
||||
"archived": false,
|
||||
"disabled": false,
|
||||
"open_issues_count": 369,
|
||||
"license": {
|
||||
"key": "apache-2.0",
|
||||
"name": "Apache License 2.0",
|
||||
"spdx_id": "Apache-2.0",
|
||||
"url": "https://api.github.com/licenses/apache-2.0",
|
||||
"node_id": "MDc6TGljZW5zZTI="
|
||||
},
|
||||
"allow_forking": true,
|
||||
"is_template": false,
|
||||
"web_commit_signoff_required": false,
|
||||
"topics": [
|
||||
"curl",
|
||||
"notifications",
|
||||
"ntfy",
|
||||
"ntfysh",
|
||||
"pubsub",
|
||||
"push-notifications",
|
||||
"rest-api"
|
||||
],
|
||||
"visibility": "public",
|
||||
"forks": 984,
|
||||
"open_issues": 369,
|
||||
"watchers": 25112,
|
||||
"default_branch": "main"
|
||||
},
|
||||
"sender": {
|
||||
"login": "TheUser-dev",
|
||||
"id": 213207407,
|
||||
"node_id": "U_kgDODLVJbw",
|
||||
"avatar_url": "https://avatars.githubusercontent.com/u/213207407?v=4",
|
||||
"gravatar_id": "",
|
||||
"url": "https://api.github.com/users/TheUser-dev",
|
||||
"html_url": "https://github.com/TheUser-dev",
|
||||
"followers_url": "https://api.github.com/users/TheUser-dev/followers",
|
||||
"following_url": "https://api.github.com/users/TheUser-dev/following{/other_user}",
|
||||
"gists_url": "https://api.github.com/users/TheUser-dev/gists{/gist_id}",
|
||||
"starred_url": "https://api.github.com/users/TheUser-dev/starred{/owner}{/repo}",
|
||||
"subscriptions_url": "https://api.github.com/users/TheUser-dev/subscriptions",
|
||||
"organizations_url": "https://api.github.com/users/TheUser-dev/orgs",
|
||||
"repos_url": "https://api.github.com/users/TheUser-dev/repos",
|
||||
"events_url": "https://api.github.com/users/TheUser-dev/events{/privacy}",
|
||||
"received_events_url": "https://api.github.com/users/TheUser-dev/received_events",
|
||||
"type": "User",
|
||||
"user_view_type": "public",
|
||||
"site_admin": false
|
||||
}
|
||||
}
|
||||
541
server/testdata/webhook_github_pr_opened.json
vendored
Normal file
541
server/testdata/webhook_github_pr_opened.json
vendored
Normal file
@@ -0,0 +1,541 @@
|
||||
{
|
||||
"action": "opened",
|
||||
"number": 1390,
|
||||
"pull_request": {
|
||||
"url": "https://api.github.com/repos/binwiederhier/ntfy/pulls/1390",
|
||||
"id": 2670425869,
|
||||
"node_id": "PR_kwDOGRBhi86fK3cN",
|
||||
"html_url": "https://github.com/binwiederhier/ntfy/pull/1390",
|
||||
"diff_url": "https://github.com/binwiederhier/ntfy/pull/1390.diff",
|
||||
"patch_url": "https://github.com/binwiederhier/ntfy/pull/1390.patch",
|
||||
"issue_url": "https://api.github.com/repos/binwiederhier/ntfy/issues/1390",
|
||||
"number": 1390,
|
||||
"state": "open",
|
||||
"locked": false,
|
||||
"title": "WIP Template dir",
|
||||
"user": {
|
||||
"login": "binwiederhier",
|
||||
"id": 664597,
|
||||
"node_id": "MDQ6VXNlcjY2NDU5Nw==",
|
||||
"avatar_url": "https://avatars.githubusercontent.com/u/664597?v=4",
|
||||
"gravatar_id": "",
|
||||
"url": "https://api.github.com/users/binwiederhier",
|
||||
"html_url": "https://github.com/binwiederhier",
|
||||
"followers_url": "https://api.github.com/users/binwiederhier/followers",
|
||||
"following_url": "https://api.github.com/users/binwiederhier/following{/other_user}",
|
||||
"gists_url": "https://api.github.com/users/binwiederhier/gists{/gist_id}",
|
||||
"starred_url": "https://api.github.com/users/binwiederhier/starred{/owner}{/repo}",
|
||||
"subscriptions_url": "https://api.github.com/users/binwiederhier/subscriptions",
|
||||
"organizations_url": "https://api.github.com/users/binwiederhier/orgs",
|
||||
"repos_url": "https://api.github.com/users/binwiederhier/repos",
|
||||
"events_url": "https://api.github.com/users/binwiederhier/events{/privacy}",
|
||||
"received_events_url": "https://api.github.com/users/binwiederhier/received_events",
|
||||
"type": "User",
|
||||
"user_view_type": "public",
|
||||
"site_admin": false
|
||||
},
|
||||
"body": null,
|
||||
"created_at": "2025-07-16T11:49:31Z",
|
||||
"updated_at": "2025-07-16T11:49:31Z",
|
||||
"closed_at": null,
|
||||
"merged_at": null,
|
||||
"merge_commit_sha": null,
|
||||
"assignee": null,
|
||||
"assignees": [
|
||||
],
|
||||
"requested_reviewers": [
|
||||
],
|
||||
"requested_teams": [
|
||||
],
|
||||
"labels": [
|
||||
],
|
||||
"milestone": null,
|
||||
"draft": false,
|
||||
"commits_url": "https://api.github.com/repos/binwiederhier/ntfy/pulls/1390/commits",
|
||||
"review_comments_url": "https://api.github.com/repos/binwiederhier/ntfy/pulls/1390/comments",
|
||||
"review_comment_url": "https://api.github.com/repos/binwiederhier/ntfy/pulls/comments{/number}",
|
||||
"comments_url": "https://api.github.com/repos/binwiederhier/ntfy/issues/1390/comments",
|
||||
"statuses_url": "https://api.github.com/repos/binwiederhier/ntfy/statuses/b1e935da45365c5e7e731d544a1ad4c7ea3643cd",
|
||||
"head": {
|
||||
"label": "binwiederhier:template-dir",
|
||||
"ref": "template-dir",
|
||||
"sha": "b1e935da45365c5e7e731d544a1ad4c7ea3643cd",
|
||||
"user": {
|
||||
"login": "binwiederhier",
|
||||
"id": 664597,
|
||||
"node_id": "MDQ6VXNlcjY2NDU5Nw==",
|
||||
"avatar_url": "https://avatars.githubusercontent.com/u/664597?v=4",
|
||||
"gravatar_id": "",
|
||||
"url": "https://api.github.com/users/binwiederhier",
|
||||
"html_url": "https://github.com/binwiederhier",
|
||||
"followers_url": "https://api.github.com/users/binwiederhier/followers",
|
||||
"following_url": "https://api.github.com/users/binwiederhier/following{/other_user}",
|
||||
"gists_url": "https://api.github.com/users/binwiederhier/gists{/gist_id}",
|
||||
"starred_url": "https://api.github.com/users/binwiederhier/starred{/owner}{/repo}",
|
||||
"subscriptions_url": "https://api.github.com/users/binwiederhier/subscriptions",
|
||||
"organizations_url": "https://api.github.com/users/binwiederhier/orgs",
|
||||
"repos_url": "https://api.github.com/users/binwiederhier/repos",
|
||||
"events_url": "https://api.github.com/users/binwiederhier/events{/privacy}",
|
||||
"received_events_url": "https://api.github.com/users/binwiederhier/received_events",
|
||||
"type": "User",
|
||||
"user_view_type": "public",
|
||||
"site_admin": false
|
||||
},
|
||||
"repo": {
|
||||
"id": 420503947,
|
||||
"node_id": "R_kgDOGRBhiw",
|
||||
"name": "ntfy",
|
||||
"full_name": "binwiederhier/ntfy",
|
||||
"private": false,
|
||||
"owner": {
|
||||
"login": "binwiederhier",
|
||||
"id": 664597,
|
||||
"node_id": "MDQ6VXNlcjY2NDU5Nw==",
|
||||
"avatar_url": "https://avatars.githubusercontent.com/u/664597?v=4",
|
||||
"gravatar_id": "",
|
||||
"url": "https://api.github.com/users/binwiederhier",
|
||||
"html_url": "https://github.com/binwiederhier",
|
||||
"followers_url": "https://api.github.com/users/binwiederhier/followers",
|
||||
"following_url": "https://api.github.com/users/binwiederhier/following{/other_user}",
|
||||
"gists_url": "https://api.github.com/users/binwiederhier/gists{/gist_id}",
|
||||
"starred_url": "https://api.github.com/users/binwiederhier/starred{/owner}{/repo}",
|
||||
"subscriptions_url": "https://api.github.com/users/binwiederhier/subscriptions",
|
||||
"organizations_url": "https://api.github.com/users/binwiederhier/orgs",
|
||||
"repos_url": "https://api.github.com/users/binwiederhier/repos",
|
||||
"events_url": "https://api.github.com/users/binwiederhier/events{/privacy}",
|
||||
"received_events_url": "https://api.github.com/users/binwiederhier/received_events",
|
||||
"type": "User",
|
||||
"user_view_type": "public",
|
||||
"site_admin": false
|
||||
},
|
||||
"html_url": "https://github.com/binwiederhier/ntfy",
|
||||
"description": "Send push notifications to your phone or desktop using PUT/POST",
|
||||
"fork": false,
|
||||
"url": "https://api.github.com/repos/binwiederhier/ntfy",
|
||||
"forks_url": "https://api.github.com/repos/binwiederhier/ntfy/forks",
|
||||
"keys_url": "https://api.github.com/repos/binwiederhier/ntfy/keys{/key_id}",
|
||||
"collaborators_url": "https://api.github.com/repos/binwiederhier/ntfy/collaborators{/collaborator}",
|
||||
"teams_url": "https://api.github.com/repos/binwiederhier/ntfy/teams",
|
||||
"hooks_url": "https://api.github.com/repos/binwiederhier/ntfy/hooks",
|
||||
"issue_events_url": "https://api.github.com/repos/binwiederhier/ntfy/issues/events{/number}",
|
||||
"events_url": "https://api.github.com/repos/binwiederhier/ntfy/events",
|
||||
"assignees_url": "https://api.github.com/repos/binwiederhier/ntfy/assignees{/user}",
|
||||
"branches_url": "https://api.github.com/repos/binwiederhier/ntfy/branches{/branch}",
|
||||
"tags_url": "https://api.github.com/repos/binwiederhier/ntfy/tags",
|
||||
"blobs_url": "https://api.github.com/repos/binwiederhier/ntfy/git/blobs{/sha}",
|
||||
"git_tags_url": "https://api.github.com/repos/binwiederhier/ntfy/git/tags{/sha}",
|
||||
"git_refs_url": "https://api.github.com/repos/binwiederhier/ntfy/git/refs{/sha}",
|
||||
"trees_url": "https://api.github.com/repos/binwiederhier/ntfy/git/trees{/sha}",
|
||||
"statuses_url": "https://api.github.com/repos/binwiederhier/ntfy/statuses/{sha}",
|
||||
"languages_url": "https://api.github.com/repos/binwiederhier/ntfy/languages",
|
||||
"stargazers_url": "https://api.github.com/repos/binwiederhier/ntfy/stargazers",
|
||||
"contributors_url": "https://api.github.com/repos/binwiederhier/ntfy/contributors",
|
||||
"subscribers_url": "https://api.github.com/repos/binwiederhier/ntfy/subscribers",
|
||||
"subscription_url": "https://api.github.com/repos/binwiederhier/ntfy/subscription",
|
||||
"commits_url": "https://api.github.com/repos/binwiederhier/ntfy/commits{/sha}",
|
||||
"git_commits_url": "https://api.github.com/repos/binwiederhier/ntfy/git/commits{/sha}",
|
||||
"comments_url": "https://api.github.com/repos/binwiederhier/ntfy/comments{/number}",
|
||||
"issue_comment_url": "https://api.github.com/repos/binwiederhier/ntfy/issues/comments{/number}",
|
||||
"contents_url": "https://api.github.com/repos/binwiederhier/ntfy/contents/{+path}",
|
||||
"compare_url": "https://api.github.com/repos/binwiederhier/ntfy/compare/{base}...{head}",
|
||||
"merges_url": "https://api.github.com/repos/binwiederhier/ntfy/merges",
|
||||
"archive_url": "https://api.github.com/repos/binwiederhier/ntfy/{archive_format}{/ref}",
|
||||
"downloads_url": "https://api.github.com/repos/binwiederhier/ntfy/downloads",
|
||||
"issues_url": "https://api.github.com/repos/binwiederhier/ntfy/issues{/number}",
|
||||
"pulls_url": "https://api.github.com/repos/binwiederhier/ntfy/pulls{/number}",
|
||||
"milestones_url": "https://api.github.com/repos/binwiederhier/ntfy/milestones{/number}",
|
||||
"notifications_url": "https://api.github.com/repos/binwiederhier/ntfy/notifications{?since,all,participating}",
|
||||
"labels_url": "https://api.github.com/repos/binwiederhier/ntfy/labels{/name}",
|
||||
"releases_url": "https://api.github.com/repos/binwiederhier/ntfy/releases{/id}",
|
||||
"deployments_url": "https://api.github.com/repos/binwiederhier/ntfy/deployments",
|
||||
"created_at": "2021-10-23T19:25:32Z",
|
||||
"updated_at": "2025-07-16T10:18:34Z",
|
||||
"pushed_at": "2025-07-16T11:49:26Z",
|
||||
"git_url": "git://github.com/binwiederhier/ntfy.git",
|
||||
"ssh_url": "git@github.com:binwiederhier/ntfy.git",
|
||||
"clone_url": "https://github.com/binwiederhier/ntfy.git",
|
||||
"svn_url": "https://github.com/binwiederhier/ntfy",
|
||||
"homepage": "https://ntfy.sh",
|
||||
"size": 36740,
|
||||
"stargazers_count": 25111,
|
||||
"watchers_count": 25111,
|
||||
"language": "Go",
|
||||
"has_issues": true,
|
||||
"has_projects": true,
|
||||
"has_downloads": true,
|
||||
"has_wiki": true,
|
||||
"has_pages": false,
|
||||
"has_discussions": false,
|
||||
"forks_count": 984,
|
||||
"mirror_url": null,
|
||||
"archived": false,
|
||||
"disabled": false,
|
||||
"open_issues_count": 368,
|
||||
"license": {
|
||||
"key": "apache-2.0",
|
||||
"name": "Apache License 2.0",
|
||||
"spdx_id": "Apache-2.0",
|
||||
"url": "https://api.github.com/licenses/apache-2.0",
|
||||
"node_id": "MDc6TGljZW5zZTI="
|
||||
},
|
||||
"allow_forking": true,
|
||||
"is_template": false,
|
||||
"web_commit_signoff_required": false,
|
||||
"topics": [
|
||||
"curl",
|
||||
"notifications",
|
||||
"ntfy",
|
||||
"ntfysh",
|
||||
"pubsub",
|
||||
"push-notifications",
|
||||
"rest-api"
|
||||
],
|
||||
"visibility": "public",
|
||||
"forks": 984,
|
||||
"open_issues": 368,
|
||||
"watchers": 25111,
|
||||
"default_branch": "main",
|
||||
"allow_squash_merge": true,
|
||||
"allow_merge_commit": true,
|
||||
"allow_rebase_merge": true,
|
||||
"allow_auto_merge": true,
|
||||
"delete_branch_on_merge": false,
|
||||
"allow_update_branch": false,
|
||||
"use_squash_pr_title_as_default": false,
|
||||
"squash_merge_commit_message": "COMMIT_MESSAGES",
|
||||
"squash_merge_commit_title": "COMMIT_OR_PR_TITLE",
|
||||
"merge_commit_message": "PR_TITLE",
|
||||
"merge_commit_title": "MERGE_MESSAGE"
|
||||
}
|
||||
},
|
||||
"base": {
|
||||
"label": "binwiederhier:main",
|
||||
"ref": "main",
|
||||
"sha": "81a486adc11fe24efcbedefb28ae946028597c2f",
|
||||
"user": {
|
||||
"login": "binwiederhier",
|
||||
"id": 664597,
|
||||
"node_id": "MDQ6VXNlcjY2NDU5Nw==",
|
||||
"avatar_url": "https://avatars.githubusercontent.com/u/664597?v=4",
|
||||
"gravatar_id": "",
|
||||
"url": "https://api.github.com/users/binwiederhier",
|
||||
"html_url": "https://github.com/binwiederhier",
|
||||
"followers_url": "https://api.github.com/users/binwiederhier/followers",
|
||||
"following_url": "https://api.github.com/users/binwiederhier/following{/other_user}",
|
||||
"gists_url": "https://api.github.com/users/binwiederhier/gists{/gist_id}",
|
||||
"starred_url": "https://api.github.com/users/binwiederhier/starred{/owner}{/repo}",
|
||||
"subscriptions_url": "https://api.github.com/users/binwiederhier/subscriptions",
|
||||
"organizations_url": "https://api.github.com/users/binwiederhier/orgs",
|
||||
"repos_url": "https://api.github.com/users/binwiederhier/repos",
|
||||
"events_url": "https://api.github.com/users/binwiederhier/events{/privacy}",
|
||||
"received_events_url": "https://api.github.com/users/binwiederhier/received_events",
|
||||
"type": "User",
|
||||
"user_view_type": "public",
|
||||
"site_admin": false
|
||||
},
|
||||
"repo": {
|
||||
"id": 420503947,
|
||||
"node_id": "R_kgDOGRBhiw",
|
||||
"name": "ntfy",
|
||||
"full_name": "binwiederhier/ntfy",
|
||||
"private": false,
|
||||
"owner": {
|
||||
"login": "binwiederhier",
|
||||
"id": 664597,
|
||||
"node_id": "MDQ6VXNlcjY2NDU5Nw==",
|
||||
"avatar_url": "https://avatars.githubusercontent.com/u/664597?v=4",
|
||||
"gravatar_id": "",
|
||||
"url": "https://api.github.com/users/binwiederhier",
|
||||
"html_url": "https://github.com/binwiederhier",
|
||||
"followers_url": "https://api.github.com/users/binwiederhier/followers",
|
||||
"following_url": "https://api.github.com/users/binwiederhier/following{/other_user}",
|
||||
"gists_url": "https://api.github.com/users/binwiederhier/gists{/gist_id}",
|
||||
"starred_url": "https://api.github.com/users/binwiederhier/starred{/owner}{/repo}",
|
||||
"subscriptions_url": "https://api.github.com/users/binwiederhier/subscriptions",
|
||||
"organizations_url": "https://api.github.com/users/binwiederhier/orgs",
|
||||
"repos_url": "https://api.github.com/users/binwiederhier/repos",
|
||||
"events_url": "https://api.github.com/users/binwiederhier/events{/privacy}",
|
||||
"received_events_url": "https://api.github.com/users/binwiederhier/received_events",
|
||||
"type": "User",
|
||||
"user_view_type": "public",
|
||||
"site_admin": false
|
||||
},
|
||||
"html_url": "https://github.com/binwiederhier/ntfy",
|
||||
"description": "Send push notifications to your phone or desktop using PUT/POST",
|
||||
"fork": false,
|
||||
"url": "https://api.github.com/repos/binwiederhier/ntfy",
|
||||
"forks_url": "https://api.github.com/repos/binwiederhier/ntfy/forks",
|
||||
"keys_url": "https://api.github.com/repos/binwiederhier/ntfy/keys{/key_id}",
|
||||
"collaborators_url": "https://api.github.com/repos/binwiederhier/ntfy/collaborators{/collaborator}",
|
||||
"teams_url": "https://api.github.com/repos/binwiederhier/ntfy/teams",
|
||||
"hooks_url": "https://api.github.com/repos/binwiederhier/ntfy/hooks",
|
||||
"issue_events_url": "https://api.github.com/repos/binwiederhier/ntfy/issues/events{/number}",
|
||||
"events_url": "https://api.github.com/repos/binwiederhier/ntfy/events",
|
||||
"assignees_url": "https://api.github.com/repos/binwiederhier/ntfy/assignees{/user}",
|
||||
"branches_url": "https://api.github.com/repos/binwiederhier/ntfy/branches{/branch}",
|
||||
"tags_url": "https://api.github.com/repos/binwiederhier/ntfy/tags",
|
||||
"blobs_url": "https://api.github.com/repos/binwiederhier/ntfy/git/blobs{/sha}",
|
||||
"git_tags_url": "https://api.github.com/repos/binwiederhier/ntfy/git/tags{/sha}",
|
||||
"git_refs_url": "https://api.github.com/repos/binwiederhier/ntfy/git/refs{/sha}",
|
||||
"trees_url": "https://api.github.com/repos/binwiederhier/ntfy/git/trees{/sha}",
|
||||
"statuses_url": "https://api.github.com/repos/binwiederhier/ntfy/statuses/{sha}",
|
||||
"languages_url": "https://api.github.com/repos/binwiederhier/ntfy/languages",
|
||||
"stargazers_url": "https://api.github.com/repos/binwiederhier/ntfy/stargazers",
|
||||
"contributors_url": "https://api.github.com/repos/binwiederhier/ntfy/contributors",
|
||||
"subscribers_url": "https://api.github.com/repos/binwiederhier/ntfy/subscribers",
|
||||
"subscription_url": "https://api.github.com/repos/binwiederhier/ntfy/subscription",
|
||||
"commits_url": "https://api.github.com/repos/binwiederhier/ntfy/commits{/sha}",
|
||||
"git_commits_url": "https://api.github.com/repos/binwiederhier/ntfy/git/commits{/sha}",
|
||||
"comments_url": "https://api.github.com/repos/binwiederhier/ntfy/comments{/number}",
|
||||
"issue_comment_url": "https://api.github.com/repos/binwiederhier/ntfy/issues/comments{/number}",
|
||||
"contents_url": "https://api.github.com/repos/binwiederhier/ntfy/contents/{+path}",
|
||||
"compare_url": "https://api.github.com/repos/binwiederhier/ntfy/compare/{base}...{head}",
|
||||
"merges_url": "https://api.github.com/repos/binwiederhier/ntfy/merges",
|
||||
"archive_url": "https://api.github.com/repos/binwiederhier/ntfy/{archive_format}{/ref}",
|
||||
"downloads_url": "https://api.github.com/repos/binwiederhier/ntfy/downloads",
|
||||
"issues_url": "https://api.github.com/repos/binwiederhier/ntfy/issues{/number}",
|
||||
"pulls_url": "https://api.github.com/repos/binwiederhier/ntfy/pulls{/number}",
|
||||
"milestones_url": "https://api.github.com/repos/binwiederhier/ntfy/milestones{/number}",
|
||||
"notifications_url": "https://api.github.com/repos/binwiederhier/ntfy/notifications{?since,all,participating}",
|
||||
"labels_url": "https://api.github.com/repos/binwiederhier/ntfy/labels{/name}",
|
||||
"releases_url": "https://api.github.com/repos/binwiederhier/ntfy/releases{/id}",
|
||||
"deployments_url": "https://api.github.com/repos/binwiederhier/ntfy/deployments",
|
||||
"created_at": "2021-10-23T19:25:32Z",
|
||||
"updated_at": "2025-07-16T10:18:34Z",
|
||||
"pushed_at": "2025-07-16T11:49:26Z",
|
||||
"git_url": "git://github.com/binwiederhier/ntfy.git",
|
||||
"ssh_url": "git@github.com:binwiederhier/ntfy.git",
|
||||
"clone_url": "https://github.com/binwiederhier/ntfy.git",
|
||||
"svn_url": "https://github.com/binwiederhier/ntfy",
|
||||
"homepage": "https://ntfy.sh",
|
||||
"size": 36740,
|
||||
"stargazers_count": 25111,
|
||||
"watchers_count": 25111,
|
||||
"language": "Go",
|
||||
"has_issues": true,
|
||||
"has_projects": true,
|
||||
"has_downloads": true,
|
||||
"has_wiki": true,
|
||||
"has_pages": false,
|
||||
"has_discussions": false,
|
||||
"forks_count": 984,
|
||||
"mirror_url": null,
|
||||
"archived": false,
|
||||
"disabled": false,
|
||||
"open_issues_count": 368,
|
||||
"license": {
|
||||
"key": "apache-2.0",
|
||||
"name": "Apache License 2.0",
|
||||
"spdx_id": "Apache-2.0",
|
||||
"url": "https://api.github.com/licenses/apache-2.0",
|
||||
"node_id": "MDc6TGljZW5zZTI="
|
||||
},
|
||||
"allow_forking": true,
|
||||
"is_template": false,
|
||||
"web_commit_signoff_required": false,
|
||||
"topics": [
|
||||
"curl",
|
||||
"notifications",
|
||||
"ntfy",
|
||||
"ntfysh",
|
||||
"pubsub",
|
||||
"push-notifications",
|
||||
"rest-api"
|
||||
],
|
||||
"visibility": "public",
|
||||
"forks": 984,
|
||||
"open_issues": 368,
|
||||
"watchers": 25111,
|
||||
"default_branch": "main",
|
||||
"allow_squash_merge": true,
|
||||
"allow_merge_commit": true,
|
||||
"allow_rebase_merge": true,
|
||||
"allow_auto_merge": true,
|
||||
"delete_branch_on_merge": false,
|
||||
"allow_update_branch": false,
|
||||
"use_squash_pr_title_as_default": false,
|
||||
"squash_merge_commit_message": "COMMIT_MESSAGES",
|
||||
"squash_merge_commit_title": "COMMIT_OR_PR_TITLE",
|
||||
"merge_commit_message": "PR_TITLE",
|
||||
"merge_commit_title": "MERGE_MESSAGE"
|
||||
}
|
||||
},
|
||||
"_links": {
|
||||
"self": {
|
||||
"href": "https://api.github.com/repos/binwiederhier/ntfy/pulls/1390"
|
||||
},
|
||||
"html": {
|
||||
"href": "https://github.com/binwiederhier/ntfy/pull/1390"
|
||||
},
|
||||
"issue": {
|
||||
"href": "https://api.github.com/repos/binwiederhier/ntfy/issues/1390"
|
||||
},
|
||||
"comments": {
|
||||
"href": "https://api.github.com/repos/binwiederhier/ntfy/issues/1390/comments"
|
||||
},
|
||||
"review_comments": {
|
||||
"href": "https://api.github.com/repos/binwiederhier/ntfy/pulls/1390/comments"
|
||||
},
|
||||
"review_comment": {
|
||||
"href": "https://api.github.com/repos/binwiederhier/ntfy/pulls/comments{/number}"
|
||||
},
|
||||
"commits": {
|
||||
"href": "https://api.github.com/repos/binwiederhier/ntfy/pulls/1390/commits"
|
||||
},
|
||||
"statuses": {
|
||||
"href": "https://api.github.com/repos/binwiederhier/ntfy/statuses/b1e935da45365c5e7e731d544a1ad4c7ea3643cd"
|
||||
}
|
||||
},
|
||||
"author_association": "OWNER",
|
||||
"auto_merge": null,
|
||||
"active_lock_reason": null,
|
||||
"merged": false,
|
||||
"mergeable": null,
|
||||
"rebaseable": null,
|
||||
"mergeable_state": "unknown",
|
||||
"merged_by": null,
|
||||
"comments": 0,
|
||||
"review_comments": 0,
|
||||
"maintainer_can_modify": false,
|
||||
"commits": 7,
|
||||
"additions": 5506,
|
||||
"deletions": 42,
|
||||
"changed_files": 58
|
||||
},
|
||||
"repository": {
|
||||
"id": 420503947,
|
||||
"node_id": "R_kgDOGRBhiw",
|
||||
"name": "ntfy",
|
||||
"full_name": "binwiederhier/ntfy",
|
||||
"private": false,
|
||||
"owner": {
|
||||
"login": "binwiederhier",
|
||||
"id": 664597,
|
||||
"node_id": "MDQ6VXNlcjY2NDU5Nw==",
|
||||
"avatar_url": "https://avatars.githubusercontent.com/u/664597?v=4",
|
||||
"gravatar_id": "",
|
||||
"url": "https://api.github.com/users/binwiederhier",
|
||||
"html_url": "https://github.com/binwiederhier",
|
||||
"followers_url": "https://api.github.com/users/binwiederhier/followers",
|
||||
"following_url": "https://api.github.com/users/binwiederhier/following{/other_user}",
|
||||
"gists_url": "https://api.github.com/users/binwiederhier/gists{/gist_id}",
|
||||
"starred_url": "https://api.github.com/users/binwiederhier/starred{/owner}{/repo}",
|
||||
"subscriptions_url": "https://api.github.com/users/binwiederhier/subscriptions",
|
||||
"organizations_url": "https://api.github.com/users/binwiederhier/orgs",
|
||||
"repos_url": "https://api.github.com/users/binwiederhier/repos",
|
||||
"events_url": "https://api.github.com/users/binwiederhier/events{/privacy}",
|
||||
"received_events_url": "https://api.github.com/users/binwiederhier/received_events",
|
||||
"type": "User",
|
||||
"user_view_type": "public",
|
||||
"site_admin": false
|
||||
},
|
||||
"html_url": "https://github.com/binwiederhier/ntfy",
|
||||
"description": "Send push notifications to your phone or desktop using PUT/POST",
|
||||
"fork": false,
|
||||
"url": "https://api.github.com/repos/binwiederhier/ntfy",
|
||||
"forks_url": "https://api.github.com/repos/binwiederhier/ntfy/forks",
|
||||
"keys_url": "https://api.github.com/repos/binwiederhier/ntfy/keys{/key_id}",
|
||||
"collaborators_url": "https://api.github.com/repos/binwiederhier/ntfy/collaborators{/collaborator}",
|
||||
"teams_url": "https://api.github.com/repos/binwiederhier/ntfy/teams",
|
||||
"hooks_url": "https://api.github.com/repos/binwiederhier/ntfy/hooks",
|
||||
"issue_events_url": "https://api.github.com/repos/binwiederhier/ntfy/issues/events{/number}",
|
||||
"events_url": "https://api.github.com/repos/binwiederhier/ntfy/events",
|
||||
"assignees_url": "https://api.github.com/repos/binwiederhier/ntfy/assignees{/user}",
|
||||
"branches_url": "https://api.github.com/repos/binwiederhier/ntfy/branches{/branch}",
|
||||
"tags_url": "https://api.github.com/repos/binwiederhier/ntfy/tags",
|
||||
"blobs_url": "https://api.github.com/repos/binwiederhier/ntfy/git/blobs{/sha}",
|
||||
"git_tags_url": "https://api.github.com/repos/binwiederhier/ntfy/git/tags{/sha}",
|
||||
"git_refs_url": "https://api.github.com/repos/binwiederhier/ntfy/git/refs{/sha}",
|
||||
"trees_url": "https://api.github.com/repos/binwiederhier/ntfy/git/trees{/sha}",
|
||||
"statuses_url": "https://api.github.com/repos/binwiederhier/ntfy/statuses/{sha}",
|
||||
"languages_url": "https://api.github.com/repos/binwiederhier/ntfy/languages",
|
||||
"stargazers_url": "https://api.github.com/repos/binwiederhier/ntfy/stargazers",
|
||||
"contributors_url": "https://api.github.com/repos/binwiederhier/ntfy/contributors",
|
||||
"subscribers_url": "https://api.github.com/repos/binwiederhier/ntfy/subscribers",
|
||||
"subscription_url": "https://api.github.com/repos/binwiederhier/ntfy/subscription",
|
||||
"commits_url": "https://api.github.com/repos/binwiederhier/ntfy/commits{/sha}",
|
||||
"git_commits_url": "https://api.github.com/repos/binwiederhier/ntfy/git/commits{/sha}",
|
||||
"comments_url": "https://api.github.com/repos/binwiederhier/ntfy/comments{/number}",
|
||||
"issue_comment_url": "https://api.github.com/repos/binwiederhier/ntfy/issues/comments{/number}",
|
||||
"contents_url": "https://api.github.com/repos/binwiederhier/ntfy/contents/{+path}",
|
||||
"compare_url": "https://api.github.com/repos/binwiederhier/ntfy/compare/{base}...{head}",
|
||||
"merges_url": "https://api.github.com/repos/binwiederhier/ntfy/merges",
|
||||
"archive_url": "https://api.github.com/repos/binwiederhier/ntfy/{archive_format}{/ref}",
|
||||
"downloads_url": "https://api.github.com/repos/binwiederhier/ntfy/downloads",
|
||||
"issues_url": "https://api.github.com/repos/binwiederhier/ntfy/issues{/number}",
|
||||
"pulls_url": "https://api.github.com/repos/binwiederhier/ntfy/pulls{/number}",
|
||||
"milestones_url": "https://api.github.com/repos/binwiederhier/ntfy/milestones{/number}",
|
||||
"notifications_url": "https://api.github.com/repos/binwiederhier/ntfy/notifications{?since,all,participating}",
|
||||
"labels_url": "https://api.github.com/repos/binwiederhier/ntfy/labels{/name}",
|
||||
"releases_url": "https://api.github.com/repos/binwiederhier/ntfy/releases{/id}",
|
||||
"deployments_url": "https://api.github.com/repos/binwiederhier/ntfy/deployments",
|
||||
"created_at": "2021-10-23T19:25:32Z",
|
||||
"updated_at": "2025-07-16T10:18:34Z",
|
||||
"pushed_at": "2025-07-16T11:49:26Z",
|
||||
"git_url": "git://github.com/binwiederhier/ntfy.git",
|
||||
"ssh_url": "git@github.com:binwiederhier/ntfy.git",
|
||||
"clone_url": "https://github.com/binwiederhier/ntfy.git",
|
||||
"svn_url": "https://github.com/binwiederhier/ntfy",
|
||||
"homepage": "https://ntfy.sh",
|
||||
"size": 36740,
|
||||
"stargazers_count": 25111,
|
||||
"watchers_count": 25111,
|
||||
"language": "Go",
|
||||
"has_issues": true,
|
||||
"has_projects": true,
|
||||
"has_downloads": true,
|
||||
"has_wiki": true,
|
||||
"has_pages": false,
|
||||
"has_discussions": false,
|
||||
"forks_count": 984,
|
||||
"mirror_url": null,
|
||||
"archived": false,
|
||||
"disabled": false,
|
||||
"open_issues_count": 368,
|
||||
"license": {
|
||||
"key": "apache-2.0",
|
||||
"name": "Apache License 2.0",
|
||||
"spdx_id": "Apache-2.0",
|
||||
"url": "https://api.github.com/licenses/apache-2.0",
|
||||
"node_id": "MDc6TGljZW5zZTI="
|
||||
},
|
||||
"allow_forking": true,
|
||||
"is_template": false,
|
||||
"web_commit_signoff_required": false,
|
||||
"topics": [
|
||||
"curl",
|
||||
"notifications",
|
||||
"ntfy",
|
||||
"ntfysh",
|
||||
"pubsub",
|
||||
"push-notifications",
|
||||
"rest-api"
|
||||
],
|
||||
"visibility": "public",
|
||||
"forks": 984,
|
||||
"open_issues": 368,
|
||||
"watchers": 25111,
|
||||
"default_branch": "main"
|
||||
},
|
||||
"sender": {
|
||||
"login": "binwiederhier",
|
||||
"id": 664597,
|
||||
"node_id": "MDQ6VXNlcjY2NDU5Nw==",
|
||||
"avatar_url": "https://avatars.githubusercontent.com/u/664597?v=4",
|
||||
"gravatar_id": "",
|
||||
"url": "https://api.github.com/users/binwiederhier",
|
||||
"html_url": "https://github.com/binwiederhier",
|
||||
"followers_url": "https://api.github.com/users/binwiederhier/followers",
|
||||
"following_url": "https://api.github.com/users/binwiederhier/following{/other_user}",
|
||||
"gists_url": "https://api.github.com/users/binwiederhier/gists{/gist_id}",
|
||||
"starred_url": "https://api.github.com/users/binwiederhier/starred{/owner}{/repo}",
|
||||
"subscriptions_url": "https://api.github.com/users/binwiederhier/subscriptions",
|
||||
"organizations_url": "https://api.github.com/users/binwiederhier/orgs",
|
||||
"repos_url": "https://api.github.com/users/binwiederhier/repos",
|
||||
"events_url": "https://api.github.com/users/binwiederhier/events{/privacy}",
|
||||
"received_events_url": "https://api.github.com/users/binwiederhier/received_events",
|
||||
"type": "User",
|
||||
"user_view_type": "public",
|
||||
"site_admin": false
|
||||
}
|
||||
}
|
||||
141
server/testdata/webhook_github_star_created.json
vendored
Normal file
141
server/testdata/webhook_github_star_created.json
vendored
Normal file
@@ -0,0 +1,141 @@
|
||||
{
|
||||
"action": "created",
|
||||
"starred_at": "2025-07-16T12:57:43Z",
|
||||
"repository": {
|
||||
"id": 420503947,
|
||||
"node_id": "R_kgDOGRBhiw",
|
||||
"name": "ntfy",
|
||||
"full_name": "binwiederhier/ntfy",
|
||||
"private": false,
|
||||
"owner": {
|
||||
"login": "binwiederhier",
|
||||
"id": 664597,
|
||||
"node_id": "MDQ6VXNlcjY2NDU5Nw==",
|
||||
"avatar_url": "https://avatars.githubusercontent.com/u/664597?v=4",
|
||||
"gravatar_id": "",
|
||||
"url": "https://api.github.com/users/binwiederhier",
|
||||
"html_url": "https://github.com/binwiederhier",
|
||||
"followers_url": "https://api.github.com/users/binwiederhier/followers",
|
||||
"following_url": "https://api.github.com/users/binwiederhier/following{/other_user}",
|
||||
"gists_url": "https://api.github.com/users/binwiederhier/gists{/gist_id}",
|
||||
"starred_url": "https://api.github.com/users/binwiederhier/starred{/owner}{/repo}",
|
||||
"subscriptions_url": "https://api.github.com/users/binwiederhier/subscriptions",
|
||||
"organizations_url": "https://api.github.com/users/binwiederhier/orgs",
|
||||
"repos_url": "https://api.github.com/users/binwiederhier/repos",
|
||||
"events_url": "https://api.github.com/users/binwiederhier/events{/privacy}",
|
||||
"received_events_url": "https://api.github.com/users/binwiederhier/received_events",
|
||||
"type": "User",
|
||||
"user_view_type": "public",
|
||||
"site_admin": false
|
||||
},
|
||||
"html_url": "https://github.com/binwiederhier/ntfy",
|
||||
"description": "Send push notifications to your phone or desktop using PUT/POST",
|
||||
"fork": false,
|
||||
"url": "https://api.github.com/repos/binwiederhier/ntfy",
|
||||
"forks_url": "https://api.github.com/repos/binwiederhier/ntfy/forks",
|
||||
"keys_url": "https://api.github.com/repos/binwiederhier/ntfy/keys{/key_id}",
|
||||
"collaborators_url": "https://api.github.com/repos/binwiederhier/ntfy/collaborators{/collaborator}",
|
||||
"teams_url": "https://api.github.com/repos/binwiederhier/ntfy/teams",
|
||||
"hooks_url": "https://api.github.com/repos/binwiederhier/ntfy/hooks",
|
||||
"issue_events_url": "https://api.github.com/repos/binwiederhier/ntfy/issues/events{/number}",
|
||||
"events_url": "https://api.github.com/repos/binwiederhier/ntfy/events",
|
||||
"assignees_url": "https://api.github.com/repos/binwiederhier/ntfy/assignees{/user}",
|
||||
"branches_url": "https://api.github.com/repos/binwiederhier/ntfy/branches{/branch}",
|
||||
"tags_url": "https://api.github.com/repos/binwiederhier/ntfy/tags",
|
||||
"blobs_url": "https://api.github.com/repos/binwiederhier/ntfy/git/blobs{/sha}",
|
||||
"git_tags_url": "https://api.github.com/repos/binwiederhier/ntfy/git/tags{/sha}",
|
||||
"git_refs_url": "https://api.github.com/repos/binwiederhier/ntfy/git/refs{/sha}",
|
||||
"trees_url": "https://api.github.com/repos/binwiederhier/ntfy/git/trees{/sha}",
|
||||
"statuses_url": "https://api.github.com/repos/binwiederhier/ntfy/statuses/{sha}",
|
||||
"languages_url": "https://api.github.com/repos/binwiederhier/ntfy/languages",
|
||||
"stargazers_url": "https://api.github.com/repos/binwiederhier/ntfy/stargazers",
|
||||
"contributors_url": "https://api.github.com/repos/binwiederhier/ntfy/contributors",
|
||||
"subscribers_url": "https://api.github.com/repos/binwiederhier/ntfy/subscribers",
|
||||
"subscription_url": "https://api.github.com/repos/binwiederhier/ntfy/subscription",
|
||||
"commits_url": "https://api.github.com/repos/binwiederhier/ntfy/commits{/sha}",
|
||||
"git_commits_url": "https://api.github.com/repos/binwiederhier/ntfy/git/commits{/sha}",
|
||||
"comments_url": "https://api.github.com/repos/binwiederhier/ntfy/comments{/number}",
|
||||
"issue_comment_url": "https://api.github.com/repos/binwiederhier/ntfy/issues/comments{/number}",
|
||||
"contents_url": "https://api.github.com/repos/binwiederhier/ntfy/contents/{+path}",
|
||||
"compare_url": "https://api.github.com/repos/binwiederhier/ntfy/compare/{base}...{head}",
|
||||
"merges_url": "https://api.github.com/repos/binwiederhier/ntfy/merges",
|
||||
"archive_url": "https://api.github.com/repos/binwiederhier/ntfy/{archive_format}{/ref}",
|
||||
"downloads_url": "https://api.github.com/repos/binwiederhier/ntfy/downloads",
|
||||
"issues_url": "https://api.github.com/repos/binwiederhier/ntfy/issues{/number}",
|
||||
"pulls_url": "https://api.github.com/repos/binwiederhier/ntfy/pulls{/number}",
|
||||
"milestones_url": "https://api.github.com/repos/binwiederhier/ntfy/milestones{/number}",
|
||||
"notifications_url": "https://api.github.com/repos/binwiederhier/ntfy/notifications{?since,all,participating}",
|
||||
"labels_url": "https://api.github.com/repos/binwiederhier/ntfy/labels{/name}",
|
||||
"releases_url": "https://api.github.com/repos/binwiederhier/ntfy/releases{/id}",
|
||||
"deployments_url": "https://api.github.com/repos/binwiederhier/ntfy/deployments",
|
||||
"created_at": "2021-10-23T19:25:32Z",
|
||||
"updated_at": "2025-07-16T12:57:43Z",
|
||||
"pushed_at": "2025-07-16T11:49:26Z",
|
||||
"git_url": "git://github.com/binwiederhier/ntfy.git",
|
||||
"ssh_url": "git@github.com:binwiederhier/ntfy.git",
|
||||
"clone_url": "https://github.com/binwiederhier/ntfy.git",
|
||||
"svn_url": "https://github.com/binwiederhier/ntfy",
|
||||
"homepage": "https://ntfy.sh",
|
||||
"size": 36831,
|
||||
"stargazers_count": 25112,
|
||||
"watchers_count": 25112,
|
||||
"language": "Go",
|
||||
"has_issues": true,
|
||||
"has_projects": true,
|
||||
"has_downloads": true,
|
||||
"has_wiki": true,
|
||||
"has_pages": false,
|
||||
"has_discussions": false,
|
||||
"forks_count": 984,
|
||||
"mirror_url": null,
|
||||
"archived": false,
|
||||
"disabled": false,
|
||||
"open_issues_count": 368,
|
||||
"license": {
|
||||
"key": "apache-2.0",
|
||||
"name": "Apache License 2.0",
|
||||
"spdx_id": "Apache-2.0",
|
||||
"url": "https://api.github.com/licenses/apache-2.0",
|
||||
"node_id": "MDc6TGljZW5zZTI="
|
||||
},
|
||||
"allow_forking": true,
|
||||
"is_template": false,
|
||||
"web_commit_signoff_required": false,
|
||||
"topics": [
|
||||
"curl",
|
||||
"notifications",
|
||||
"ntfy",
|
||||
"ntfysh",
|
||||
"pubsub",
|
||||
"push-notifications",
|
||||
"rest-api"
|
||||
],
|
||||
"visibility": "public",
|
||||
"forks": 984,
|
||||
"open_issues": 368,
|
||||
"watchers": 25112,
|
||||
"default_branch": "main"
|
||||
},
|
||||
"sender": {
|
||||
"login": "mbilby",
|
||||
"id": 51273322,
|
||||
"node_id": "MDQ6VXNlcjUxMjczMzIy",
|
||||
"avatar_url": "https://avatars.githubusercontent.com/u/51273322?v=4",
|
||||
"gravatar_id": "",
|
||||
"url": "https://api.github.com/users/mbilby",
|
||||
"html_url": "https://github.com/mbilby",
|
||||
"followers_url": "https://api.github.com/users/mbilby/followers",
|
||||
"following_url": "https://api.github.com/users/mbilby/following{/other_user}",
|
||||
"gists_url": "https://api.github.com/users/mbilby/gists{/gist_id}",
|
||||
"starred_url": "https://api.github.com/users/mbilby/starred{/owner}{/repo}",
|
||||
"subscriptions_url": "https://api.github.com/users/mbilby/subscriptions",
|
||||
"organizations_url": "https://api.github.com/users/mbilby/orgs",
|
||||
"repos_url": "https://api.github.com/users/mbilby/repos",
|
||||
"events_url": "https://api.github.com/users/mbilby/events{/privacy}",
|
||||
"received_events_url": "https://api.github.com/users/mbilby/received_events",
|
||||
"type": "User",
|
||||
"user_view_type": "public",
|
||||
"site_admin": false
|
||||
}
|
||||
}
|
||||
|
||||
139
server/testdata/webhook_github_watch_created.json
vendored
Normal file
139
server/testdata/webhook_github_watch_created.json
vendored
Normal file
@@ -0,0 +1,139 @@
|
||||
{
|
||||
"action": "started",
|
||||
"repository": {
|
||||
"id": 420503947,
|
||||
"node_id": "R_kgDOGRBhiw",
|
||||
"name": "ntfy",
|
||||
"full_name": "binwiederhier/ntfy",
|
||||
"private": false,
|
||||
"owner": {
|
||||
"login": "binwiederhier",
|
||||
"id": 664597,
|
||||
"node_id": "MDQ6VXNlcjY2NDU5Nw==",
|
||||
"avatar_url": "https://avatars.githubusercontent.com/u/664597?v=4",
|
||||
"gravatar_id": "",
|
||||
"url": "https://api.github.com/users/binwiederhier",
|
||||
"html_url": "https://github.com/binwiederhier",
|
||||
"followers_url": "https://api.github.com/users/binwiederhier/followers",
|
||||
"following_url": "https://api.github.com/users/binwiederhier/following{/other_user}",
|
||||
"gists_url": "https://api.github.com/users/binwiederhier/gists{/gist_id}",
|
||||
"starred_url": "https://api.github.com/users/binwiederhier/starred{/owner}{/repo}",
|
||||
"subscriptions_url": "https://api.github.com/users/binwiederhier/subscriptions",
|
||||
"organizations_url": "https://api.github.com/users/binwiederhier/orgs",
|
||||
"repos_url": "https://api.github.com/users/binwiederhier/repos",
|
||||
"events_url": "https://api.github.com/users/binwiederhier/events{/privacy}",
|
||||
"received_events_url": "https://api.github.com/users/binwiederhier/received_events",
|
||||
"type": "User",
|
||||
"user_view_type": "public",
|
||||
"site_admin": false
|
||||
},
|
||||
"html_url": "https://github.com/binwiederhier/ntfy",
|
||||
"description": "Send push notifications to your phone or desktop using PUT/POST",
|
||||
"fork": false,
|
||||
"url": "https://api.github.com/repos/binwiederhier/ntfy",
|
||||
"forks_url": "https://api.github.com/repos/binwiederhier/ntfy/forks",
|
||||
"keys_url": "https://api.github.com/repos/binwiederhier/ntfy/keys{/key_id}",
|
||||
"collaborators_url": "https://api.github.com/repos/binwiederhier/ntfy/collaborators{/collaborator}",
|
||||
"teams_url": "https://api.github.com/repos/binwiederhier/ntfy/teams",
|
||||
"hooks_url": "https://api.github.com/repos/binwiederhier/ntfy/hooks",
|
||||
"issue_events_url": "https://api.github.com/repos/binwiederhier/ntfy/issues/events{/number}",
|
||||
"events_url": "https://api.github.com/repos/binwiederhier/ntfy/events",
|
||||
"assignees_url": "https://api.github.com/repos/binwiederhier/ntfy/assignees{/user}",
|
||||
"branches_url": "https://api.github.com/repos/binwiederhier/ntfy/branches{/branch}",
|
||||
"tags_url": "https://api.github.com/repos/binwiederhier/ntfy/tags",
|
||||
"blobs_url": "https://api.github.com/repos/binwiederhier/ntfy/git/blobs{/sha}",
|
||||
"git_tags_url": "https://api.github.com/repos/binwiederhier/ntfy/git/tags{/sha}",
|
||||
"git_refs_url": "https://api.github.com/repos/binwiederhier/ntfy/git/refs{/sha}",
|
||||
"trees_url": "https://api.github.com/repos/binwiederhier/ntfy/git/trees{/sha}",
|
||||
"statuses_url": "https://api.github.com/repos/binwiederhier/ntfy/statuses/{sha}",
|
||||
"languages_url": "https://api.github.com/repos/binwiederhier/ntfy/languages",
|
||||
"stargazers_url": "https://api.github.com/repos/binwiederhier/ntfy/stargazers",
|
||||
"contributors_url": "https://api.github.com/repos/binwiederhier/ntfy/contributors",
|
||||
"subscribers_url": "https://api.github.com/repos/binwiederhier/ntfy/subscribers",
|
||||
"subscription_url": "https://api.github.com/repos/binwiederhier/ntfy/subscription",
|
||||
"commits_url": "https://api.github.com/repos/binwiederhier/ntfy/commits{/sha}",
|
||||
"git_commits_url": "https://api.github.com/repos/binwiederhier/ntfy/git/commits{/sha}",
|
||||
"comments_url": "https://api.github.com/repos/binwiederhier/ntfy/comments{/number}",
|
||||
"issue_comment_url": "https://api.github.com/repos/binwiederhier/ntfy/issues/comments{/number}",
|
||||
"contents_url": "https://api.github.com/repos/binwiederhier/ntfy/contents/{+path}",
|
||||
"compare_url": "https://api.github.com/repos/binwiederhier/ntfy/compare/{base}...{head}",
|
||||
"merges_url": "https://api.github.com/repos/binwiederhier/ntfy/merges",
|
||||
"archive_url": "https://api.github.com/repos/binwiederhier/ntfy/{archive_format}{/ref}",
|
||||
"downloads_url": "https://api.github.com/repos/binwiederhier/ntfy/downloads",
|
||||
"issues_url": "https://api.github.com/repos/binwiederhier/ntfy/issues{/number}",
|
||||
"pulls_url": "https://api.github.com/repos/binwiederhier/ntfy/pulls{/number}",
|
||||
"milestones_url": "https://api.github.com/repos/binwiederhier/ntfy/milestones{/number}",
|
||||
"notifications_url": "https://api.github.com/repos/binwiederhier/ntfy/notifications{?since,all,participating}",
|
||||
"labels_url": "https://api.github.com/repos/binwiederhier/ntfy/labels{/name}",
|
||||
"releases_url": "https://api.github.com/repos/binwiederhier/ntfy/releases{/id}",
|
||||
"deployments_url": "https://api.github.com/repos/binwiederhier/ntfy/deployments",
|
||||
"created_at": "2021-10-23T19:25:32Z",
|
||||
"updated_at": "2025-07-16T12:57:43Z",
|
||||
"pushed_at": "2025-07-16T11:49:26Z",
|
||||
"git_url": "git://github.com/binwiederhier/ntfy.git",
|
||||
"ssh_url": "git@github.com:binwiederhier/ntfy.git",
|
||||
"clone_url": "https://github.com/binwiederhier/ntfy.git",
|
||||
"svn_url": "https://github.com/binwiederhier/ntfy",
|
||||
"homepage": "https://ntfy.sh",
|
||||
"size": 36831,
|
||||
"stargazers_count": 25112,
|
||||
"watchers_count": 25112,
|
||||
"language": "Go",
|
||||
"has_issues": true,
|
||||
"has_projects": true,
|
||||
"has_downloads": true,
|
||||
"has_wiki": true,
|
||||
"has_pages": false,
|
||||
"has_discussions": false,
|
||||
"forks_count": 984,
|
||||
"mirror_url": null,
|
||||
"archived": false,
|
||||
"disabled": false,
|
||||
"open_issues_count": 368,
|
||||
"license": {
|
||||
"key": "apache-2.0",
|
||||
"name": "Apache License 2.0",
|
||||
"spdx_id": "Apache-2.0",
|
||||
"url": "https://api.github.com/licenses/apache-2.0",
|
||||
"node_id": "MDc6TGljZW5zZTI="
|
||||
},
|
||||
"allow_forking": true,
|
||||
"is_template": false,
|
||||
"web_commit_signoff_required": false,
|
||||
"topics": [
|
||||
"curl",
|
||||
"notifications",
|
||||
"ntfy",
|
||||
"ntfysh",
|
||||
"pubsub",
|
||||
"push-notifications",
|
||||
"rest-api"
|
||||
],
|
||||
"visibility": "public",
|
||||
"forks": 984,
|
||||
"open_issues": 368,
|
||||
"watchers": 25112,
|
||||
"default_branch": "main"
|
||||
},
|
||||
"sender": {
|
||||
"login": "mbilby",
|
||||
"id": 51273322,
|
||||
"node_id": "MDQ6VXNlcjUxMjczMzIy",
|
||||
"avatar_url": "https://avatars.githubusercontent.com/u/51273322?v=4",
|
||||
"gravatar_id": "",
|
||||
"url": "https://api.github.com/users/mbilby",
|
||||
"html_url": "https://github.com/mbilby",
|
||||
"followers_url": "https://api.github.com/users/mbilby/followers",
|
||||
"following_url": "https://api.github.com/users/mbilby/following{/other_user}",
|
||||
"gists_url": "https://api.github.com/users/mbilby/gists{/gist_id}",
|
||||
"starred_url": "https://api.github.com/users/mbilby/starred{/owner}{/repo}",
|
||||
"subscriptions_url": "https://api.github.com/users/mbilby/subscriptions",
|
||||
"organizations_url": "https://api.github.com/users/mbilby/orgs",
|
||||
"repos_url": "https://api.github.com/users/mbilby/repos",
|
||||
"events_url": "https://api.github.com/users/mbilby/events{/privacy}",
|
||||
"received_events_url": "https://api.github.com/users/mbilby/received_events",
|
||||
"type": "User",
|
||||
"user_view_type": "public",
|
||||
"site_admin": false
|
||||
}
|
||||
}
|
||||
51
server/testdata/webhook_grafana_resolved.json
vendored
Normal file
51
server/testdata/webhook_grafana_resolved.json
vendored
Normal file
@@ -0,0 +1,51 @@
|
||||
{
|
||||
"receiver": "ntfy\\.example\\.com/alerts",
|
||||
"status": "resolved",
|
||||
"alerts": [
|
||||
{
|
||||
"status": "resolved",
|
||||
"labels": {
|
||||
"alertname": "Load avg 15m too high",
|
||||
"grafana_folder": "Node alerts",
|
||||
"instance": "10.108.0.2:9100",
|
||||
"job": "node-exporter"
|
||||
},
|
||||
"annotations": {
|
||||
"summary": "15m load average too high"
|
||||
},
|
||||
"startsAt": "2024-03-15T02:28:00Z",
|
||||
"endsAt": "2024-03-15T02:42:00Z",
|
||||
"generatorURL": "localhost:3000/alerting/grafana/NW9oDw-4z/view",
|
||||
"fingerprint": "becbfb94bd81ef48",
|
||||
"silenceURL": "localhost:3000/alerting/silence/new?alertmanager=grafana&matcher=alertname%3DLoad+avg+15m+too+high&matcher=grafana_folder%3DNode+alerts&matcher=instance%3D10.108.0.2%3A9100&matcher=job%3Dnode-exporter",
|
||||
"dashboardURL": "",
|
||||
"panelURL": "",
|
||||
"values": {
|
||||
"B": 18.98211314475876,
|
||||
"C": 0
|
||||
},
|
||||
"valueString": "[ var='B' labels={__name__=node_load15, instance=10.108.0.2:9100, job=node-exporter} value=18.98211314475876 ], [ var='C' labels={__name__=node_load15, instance=10.108.0.2:9100, job=node-exporter} value=0 ]"
|
||||
}
|
||||
],
|
||||
"groupLabels": {
|
||||
"alertname": "Load avg 15m too high",
|
||||
"grafana_folder": "Node alerts"
|
||||
},
|
||||
"commonLabels": {
|
||||
"alertname": "Load avg 15m too high",
|
||||
"grafana_folder": "Node alerts",
|
||||
"instance": "10.108.0.2:9100",
|
||||
"job": "node-exporter"
|
||||
},
|
||||
"commonAnnotations": {
|
||||
"summary": "15m load average too high"
|
||||
},
|
||||
"externalURL": "localhost:3000/",
|
||||
"version": "1",
|
||||
"groupKey": "{}:{alertname=\"Load avg 15m too high\", grafana_folder=\"Node alerts\"}",
|
||||
"truncatedAlerts": 0,
|
||||
"orgId": 1,
|
||||
"title": "[RESOLVED] Load avg 15m too high Node alerts (10.108.0.2:9100 node-exporter)",
|
||||
"state": "ok",
|
||||
"message": "**Resolved**\n\nValue: B=18.98211314475876, C=0\nLabels:\n - alertname = Load avg 15m too high\n - grafana_folder = Node alerts\n - instance = 10.108.0.2:9100\n - job = node-exporter\n"
|
||||
}
|
||||
@@ -7,7 +7,6 @@ import (
|
||||
|
||||
"heckel.io/ntfy/v2/log"
|
||||
"heckel.io/ntfy/v2/user"
|
||||
|
||||
"heckel.io/ntfy/v2/util"
|
||||
)
|
||||
|
||||
@@ -246,6 +245,24 @@ func (q *queryFilter) Pass(msg *message) bool {
|
||||
return true
|
||||
}
|
||||
|
||||
type templateMode string
|
||||
|
||||
func (t templateMode) Enabled() bool {
|
||||
return t != ""
|
||||
}
|
||||
|
||||
func (t templateMode) Name() string {
|
||||
if isBoolValue(string(t)) {
|
||||
return ""
|
||||
}
|
||||
return string(t)
|
||||
}
|
||||
|
||||
type templateFile struct {
|
||||
Title *string `yaml:"title"`
|
||||
Message *string `yaml:"message"`
|
||||
}
|
||||
|
||||
type apiHealthResponse struct {
|
||||
Healthy bool `json:"healthy"`
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user