From df7d6baec59e17b3ac07b2846d067bc17fb94133 Mon Sep 17 00:00:00 2001 From: Hunter Kehoe Date: Sun, 17 Mar 2024 21:55:50 -0600 Subject: [PATCH 01/14] add templating for title and message fields --- docs/publish.md | 23 ++++++++ docs/releases.md | 6 +++ go.mod | 3 ++ go.sum | 6 +++ server/server.go | 76 ++++++++++++++++++-------- server/server_test.go | 120 ++++++++++++++++++++++++++++++++++++++++++ 6 files changed, 212 insertions(+), 22 deletions(-) diff --git a/docs/publish.md b/docs/publish.md index 5239bbc6..cd67ed69 100644 --- a/docs/publish.md +++ b/docs/publish.md @@ -3557,6 +3557,29 @@ ntfy server plays the role of the Push Gateway, as well as the Push Provider. Un !!! info This is not a generic Matrix Push Gateway. It only works in combination with UnifiedPush and ntfy. +### Message and Title Templates +Some services let you specify a webhook URL but do not let you modify the webhook body (e.g., Grafana). Instead of using a separate +bridge program to parse the webhook body into the format ntfy expects, you can include a message template and/or a title template +which will be populated based on the fields of the webhook body (so long as the webhook body is valid JSON). + +Send the message template with the header `X-Template-Message`, `Template-Message`, or `tpl-m`. Send the title template with the +header `X-Template-Title`, `Template-Title`, or `tpl-t`. (No other fields can be filled with a template at this time). + +In the template, include paths to the appropriate JSON fields surrounded by `${` and `}`. See an example below. +See [GJSON docs](https://github.com/tidwall/gjson/blob/master/SYNTAX.md) for supported JSON path syntax. + +=== "HTTP" + ``` http + POST /mytopic HTTP/1.1 + Host: ntfy.sh + X-Template-Message: Error message: ${error.desc} + X-Template-Title: ${hostname}: A ${error.level} error has occurred + + {"hostname": "philipp-pc", "error": {"level": "severe", "desc": "Disk has run out of space"}} + ``` + +The example above would send a notification with a title "philipp-pc: A severe error has occurred" and a message "Error message: Disk has run out of space". + ## Public topics Obviously all topics on ntfy.sh are public, but there are a few designated topics that are used in examples, and topics that you can use to try out what [authentication and access control](#authentication) looks like. diff --git a/docs/releases.md b/docs/releases.md index 9bdfef34..c82560ac 100644 --- a/docs/releases.md +++ b/docs/releases.md @@ -1338,6 +1338,12 @@ and the [ntfy Android app](https://github.com/binwiederhier/ntfy-android/release ## Not released yet +### ntfy server v2.9.1 (UNRELEASED) + +**Features:** + +* You can now include a message and/or title template that will be filled with values from a JSON body, great for services that let you specify a webhook URL but do not let you change the webhook body (such as Grafana). ([#724](https://github.com/binwiederhier/ntfy/issues/724), thanks to [@wunter8](https://github.com/wunter8) for implementing) + ### ntfy Android app v1.16.1 (UNRELEASED) **Features:** diff --git a/go.mod b/go.mod index 1a5ecf76..a63f2ab8 100644 --- a/go.mod +++ b/go.mod @@ -69,6 +69,9 @@ require ( github.com/prometheus/procfs v0.13.0 // indirect github.com/russross/blackfriday/v2 v2.1.0 // indirect github.com/stretchr/objx v0.5.0 // indirect + github.com/tidwall/gjson v1.17.1 // indirect + github.com/tidwall/match v1.1.1 // indirect + github.com/tidwall/pretty v1.2.0 // indirect github.com/xrash/smetrics v0.0.0-20231213231151-1d8dd44e695e // indirect go.opencensus.io v0.24.0 // indirect go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.49.0 // indirect diff --git a/go.sum b/go.sum index bdd68ab7..47c8b8c5 100644 --- a/go.sum +++ b/go.sum @@ -143,6 +143,12 @@ github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcU github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= github.com/stripe/stripe-go/v74 v74.30.0 h1:0Kf0KkeFnY7iRhOwvTerX0Ia1BRw+eV1CVJ51mGYAUY= github.com/stripe/stripe-go/v74 v74.30.0/go.mod h1:f9L6LvaXa35ja7eyvP6GQswoaIPaBRvGAimAO+udbBw= +github.com/tidwall/gjson v1.17.1 h1:wlYEnwqAHgzmhNUFfw7Xalt2JzQvsMx2Se4PcoFCT/U= +github.com/tidwall/gjson v1.17.1/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk= +github.com/tidwall/match v1.1.1 h1:+Ho715JplO36QYgwN9PGYNhgZvoUSc9X2c80KVTi+GA= +github.com/tidwall/match v1.1.1/go.mod h1:eRSPERbgtNPcGhD8UCthc6PmLEQXEWd3PRB5JTxsfmM= +github.com/tidwall/pretty v1.2.0 h1:RWIZEg2iJ8/g6fDDYzMpobmaoGh5OLl4AXtGUGPcqCs= +github.com/tidwall/pretty v1.2.0/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU= github.com/urfave/cli/v2 v2.27.1 h1:8xSQ6szndafKVRmfyeUMxkNUJQMjL1F2zmsZ+qHpfho= github.com/urfave/cli/v2 v2.27.1/go.mod h1:8qnjx1vcq5s2/wpsqoZFndg2CE5tNFyrTvS6SinrnYQ= github.com/xrash/smetrics v0.0.0-20231213231151-1d8dd44e695e h1:+SOyEddqYF09QP7vr7CgJ1eti3pY9Fn3LHO1M1r/0sI= diff --git a/server/server.go b/server/server.go index f6e39be3..5af493e6 100644 --- a/server/server.go +++ b/server/server.go @@ -29,6 +29,7 @@ import ( "github.com/emersion/go-smtp" "github.com/gorilla/websocket" "github.com/prometheus/client_golang/prometheus/promhttp" + "github.com/tidwall/gjson" "golang.org/x/sync/errgroup" "heckel.io/ntfy/v2/log" "heckel.io/ntfy/v2/user" @@ -738,7 +739,7 @@ func (s *Server) handlePublishInternal(r *http.Request, v *visitor) (*message, e return nil, err } m := newDefaultMessage(t.ID, "") - cache, firebase, email, call, unifiedpush, e := s.parsePublishParams(r, m) + cache, firebase, email, call, messageTemplate, titleTemplate, unifiedpush, e := s.parsePublishParams(r, m) if e != nil { return nil, e.With(t) } @@ -769,7 +770,7 @@ func (s *Server) handlePublishInternal(r *http.Request, v *visitor) (*message, e if cache { m.Expires = time.Unix(m.Time, 0).Add(v.Limits().MessageExpiryDuration).Unix() } - if err := s.handlePublishBody(r, v, m, body, unifiedpush); err != nil { + if err := s.handlePublishBody(r, v, m, body, messageTemplate, titleTemplate, unifiedpush); err != nil { return nil, err } if m.Message == "" { @@ -924,7 +925,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, unifiedpush bool, err *errHTTP) { +func (s *Server) parsePublishParams(r *http.Request, m *message) (cache bool, firebase bool, email, call string, messageTemplate string, titleTemplate string, 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") @@ -940,7 +941,7 @@ func (s *Server) parsePublishParams(r *http.Request, m *message) (cache bool, fi } if attach != "" { if !urlRegex.MatchString(attach) { - return false, false, "", "", false, errHTTPBadRequestAttachmentURLInvalid + return false, false, "", "", "", "", false, errHTTPBadRequestAttachmentURLInvalid } m.Attachment.URL = attach if m.Attachment.Name == "" { @@ -958,19 +959,20 @@ func (s *Server) parsePublishParams(r *http.Request, m *message) (cache bool, fi } if icon != "" { if !urlRegex.MatchString(icon) { - return 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, errHTTPBadRequestEmailDisabled + return false, false, "", "", "", "", false, errHTTPBadRequestEmailDisabled } call = readParam(r, "x-call", "call") if call != "" && (s.config.TwilioAccount == "" || s.userManager == nil) { - return false, false, "", "", false, errHTTPBadRequestPhoneCallsDisabled + print("call: %s", call) + return false, false, "", "", "", "", false, errHTTPBadRequestPhoneCallsDisabled } else if call != "" && !isBoolValue(call) && !phoneNumberRegex.MatchString(call) { - return false, false, "", "", false, errHTTPBadRequestPhoneNumberInvalid + return false, false, "", "", "", "", false, errHTTPBadRequestPhoneNumberInvalid } messageStr := strings.ReplaceAll(readParam(r, "x-message", "message", "m"), "\\n", "\n") if messageStr != "" { @@ -979,27 +981,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, 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, errHTTPBadRequestDelayNoCache + return false, false, "", "", "", "", false, errHTTPBadRequestDelayNoCache } if email != "" { - return 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, 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, errHTTPBadRequestDelayCannotParse + return false, false, "", "", "", "", false, errHTTPBadRequestDelayCannotParse } else if delay.Unix() < time.Now().Add(s.config.MessageDelayMin).Unix() { - return false, false, "", "", false, errHTTPBadRequestDelayTooSmall + return false, false, "", "", "", "", false, errHTTPBadRequestDelayTooSmall } else if delay.Unix() > time.Now().Add(s.config.MessageDelayMax).Unix() { - return false, false, "", "", false, errHTTPBadRequestDelayTooLarge + return false, false, "", "", "", "", false, errHTTPBadRequestDelayTooLarge } m.Time = delay.Unix() } @@ -1007,13 +1009,15 @@ 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, errHTTPBadRequestActionsInvalid.Wrap(e.Error()) + return false, false, "", "", "", "", false, errHTTPBadRequestActionsInvalid.Wrap(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" } + messageTemplate = readParam(r, "x-template-message", "template-message", "tpl-m") + titleTemplate = readParam(r, "x-template-title", "template-title", "tpl-t") unifiedpush = readBoolParam(r, false, "x-unifiedpush", "unifiedpush", "up") // see GET too! if unifiedpush { firebase = false @@ -1025,7 +1029,7 @@ func (s *Server) parsePublishParams(r *http.Request, m *message) (cache bool, fi cache = false email = "" } - return cache, firebase, email, call, unifiedpush, nil + return cache, firebase, email, call, messageTemplate, titleTemplate, unifiedpush, nil } // handlePublishBody consumes the PUT/POST body and decides whether the body is an attachment or the message. @@ -1042,17 +1046,17 @@ 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 // 6. curl -T file.txt ntfy.sh/mytopic // 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, unifiedpush bool) error { +func (s *Server) handlePublishBody(r *http.Request, v *visitor, m *message, body *util.PeekedReadCloser, messageTemplate string, titleTemplate string, unifiedpush bool) error { if m.Event == pollRequestEvent { // Case 1 return s.handleBodyDiscard(body) } else if unifiedpush { return s.handleBodyAsMessageAutoDetect(m, body) // Case 2 } else if m.Attachment != nil && m.Attachment.URL != "" { - return s.handleBodyAsTextMessage(m, body) // Case 3 + return s.handleBodyAsTextMessage(m, body, messageTemplate, titleTemplate) // Case 3 } else if m.Attachment != nil && m.Attachment.Name != "" { return s.handleBodyAsAttachment(r, v, m, body) // Case 4 } else if !body.LimitReached && utf8.Valid(body.PeekedBytes) { - return s.handleBodyAsTextMessage(m, body) // Case 5 + return s.handleBodyAsTextMessage(m, body, messageTemplate, titleTemplate) // Case 5 } return s.handleBodyAsAttachment(r, v, m, body) // Case 6 } @@ -1073,12 +1077,40 @@ func (s *Server) handleBodyAsMessageAutoDetect(m *message, body *util.PeekedRead return nil } -func (s *Server) handleBodyAsTextMessage(m *message, body *util.PeekedReadCloser) error { +func (s *Server) handleBodyAsTextMessage(m *message, body *util.PeekedReadCloser, messageTemplate string, titleTemplate string) error { if !utf8.Valid(body.PeekedBytes) { return errHTTPBadRequestMessageNotUTF8.With(m) } if len(body.PeekedBytes) > 0 { // Empty body should not override message (publish via GET!) - m.Message = strings.TrimSpace(string(body.PeekedBytes)) // Truncates the message to the peek limit if required + peakedBody := strings.TrimSpace(string(body.PeekedBytes)) // Truncates the message to the peek limit if required + // Replace JSON paths in messageTemplate + if messageTemplate != "" && gjson.Valid(peakedBody) { + m.Message = messageTemplate + r := regexp.MustCompile(`\${([^}]+)}`) + messageMatches := r.FindAllStringSubmatch(messageTemplate, -1) + for _, v := range messageMatches { + query := v[1] + result := gjson.Get(peakedBody, query) + if result.Exists() { + m.Message = strings.ReplaceAll(m.Message, fmt.Sprintf("${%s}", query), result.String()) + } + } + } else { + m.Message = peakedBody + } + // Replace JSON paths in titleTemplate + if titleTemplate != "" && gjson.Valid(peakedBody) { + m.Title = titleTemplate + r := regexp.MustCompile(`\${([^}]+)}`) + titleMatches := r.FindAllStringSubmatch(titleTemplate, -1) + for _, v := range titleMatches { + query := v[1] + result := gjson.Get(peakedBody, query) + if result.Exists() { + m.Title = strings.ReplaceAll(m.Title, fmt.Sprintf("${%s}", query), result.String()) + } + } + } } if m.Attachment != nil && m.Attachment.Name != "" && m.Message == "" { m.Message = fmt.Sprintf(defaultAttachmentMessage, m.Attachment.Name) diff --git a/server/server_test.go b/server/server_test.go index 8d965153..4f634360 100644 --- a/server/server_test.go +++ b/server/server_test.go @@ -2625,6 +2625,126 @@ func TestServer_UpstreamBaseURL_DoNotForwardUnifiedPush(t *testing.T) { time.Sleep(500 * time.Millisecond) } +func TestServer_MessageTemplate(t *testing.T) { + s := newTestServer(t, newTestConfig(t)) + response := request(t, s, "PUT", "/mytopic", `{"foo":"bar", "nested":{"title":"here"}}`, map[string]string{ + "X-Template-Message": "${foo}", + "X-Template-Title": "${nested.title}", + }) + + require.Equal(t, 200, response.Code) + m := toMessage(t, response.Body.String()) + require.Equal(t, "bar", m.Message) + require.Equal(t, "here", m.Title) +} + +func TestServer_MessageTemplate_RepeatPlaceholder(t *testing.T) { + s := newTestServer(t, newTestConfig(t)) + response := request(t, s, "PUT", "/mytopic", `{"foo":"bar", "nested":{"title":"here"}}`, map[string]string{ + "Template-Message": "${foo} is ${foo}", + "Template-Title": "${nested.title} is ${nested.title}", + }) + + require.Equal(t, 200, response.Code) + m := toMessage(t, response.Body.String()) + require.Equal(t, "bar is bar", m.Message) + require.Equal(t, "here is here", m.Title) +} + +func TestServer_MessageTemplate_JSONBody(t *testing.T) { + s := newTestServer(t, newTestConfig(t)) + body := `{"topic": "mytopic", "message": "{\"foo\":\"bar\",\"nested\":{\"title\":\"here\"}}"}` + response := request(t, s, "PUT", "/", body, map[string]string{ + "tpl-m": "${foo}", + "tpl-t": "${nested.title}", + }) + + require.Equal(t, 200, response.Code) + m := toMessage(t, response.Body.String()) + require.Equal(t, "bar", m.Message) + require.Equal(t, "here", m.Title) +} + +func TestServer_MessageTemplate_MalformedJSONBody(t *testing.T) { + s := newTestServer(t, newTestConfig(t)) + body := `{"topic": "mytopic", "message": "{\"foo\":\"bar\",\"nested\":{\"title\":\"here\"INVALID"}` + response := request(t, s, "PUT", "/", body, map[string]string{ + "X-Template-Message": "${foo}", + "X-Template-Title": "${nested.title}", + }) + + require.Equal(t, 200, response.Code, "Got %s", response) + m := toMessage(t, response.Body.String()) + require.Equal(t, "{\"foo\":\"bar\",\"nested\":{\"title\":\"here\"INVALID", m.Message) + require.Equal(t, "", m.Title) +} + +func TestServer_MessageTemplate_PlaceholderTypo(t *testing.T) { + s := newTestServer(t, newTestConfig(t)) + response := request(t, s, "PUT", "/mytopic", `{"foo":"bar", "nested":{"title":"here"}}`, map[string]string{ + "X-Template-Message": "${food}", + "X-Template-Title": "${nested.titl}", + }) + + require.Equal(t, 200, response.Code) + m := toMessage(t, response.Body.String()) + require.Equal(t, "${food}", m.Message) + require.Equal(t, "${nested.titl}", m.Title) +} + +func TestServer_MessageTemplate_MultiplePlaceholders(t *testing.T) { + s := newTestServer(t, newTestConfig(t)) + response := request(t, s, "PUT", "/mytopic", `{"foo":"bar", "nested":{"title":"here"}}`, map[string]string{ + "X-Template-Message": "${foo} is ${nested.title}", + }) + + require.Equal(t, 200, response.Code) + m := toMessage(t, response.Body.String()) + require.Equal(t, "bar is here", m.Message) +} + +func TestServer_MessageTemplate_NestedPlaceholders(t *testing.T) { + // not intended to work recursively for now + // i.e., ${${nested.bar}} should NOT evaluate to ${foo} and then to "bar" + s := newTestServer(t, newTestConfig(t)) + response := request(t, s, "PUT", "/mytopic", `{"foo":"bar", "nested":{"title":"here","bar":"foo"}}`, map[string]string{ + "X-Template-Message": "${${nested.bar}}", + }) + + require.Equal(t, 200, response.Code) + m := toMessage(t, response.Body.String()) + require.Equal(t, "${${nested.bar}}", m.Message) +} + +func TestServer_MessageTemplate_NestedPlaceholdersFunky(t *testing.T) { + // The above example can technically work + // ${${nested.bar}} would be interpreted as a nested GJSON path with key "${nested" then key "bar" + // so you would probably expect the output to be "works!", BUT the second } in the placeholder is not + // included by the regex, so it is still there after replacing the placeholder, thus giving you "works!}" + s := newTestServer(t, newTestConfig(t)) + response := request(t, s, "PUT", "/mytopic", `{"foo":"bar", "nested":{"title":"here","bar":"foo"}, "${nested":{"bar":"works!"}}`, map[string]string{ + "X-Template-Message": "${${nested.bar}}", + }) + + require.Equal(t, 200, response.Code) + m := toMessage(t, response.Body.String()) + require.Equal(t, "works!}", m.Message) +} + +func TestServer_MessageTemplate_FancyGJSON(t *testing.T) { + s := newTestServer(t, newTestConfig(t)) + jsonBody := `{"foo": "bar", "errors": [{"level": "severe", "url": "https://severe1.com"},{"level": "warning", "url": "https://warning.com"},{"level": "severe", "url": "https://severe2.com"}]}` + response := request(t, s, "PUT", "/mytopic", jsonBody, map[string]string{ + "X-Template-Message": `${errors.#(level=="severe")#.url}`, + "X-Template-Title": `${errors.#(level=="severe")#|#} Severe Errors`, + }) + + require.Equal(t, 200, response.Code) + m := toMessage(t, response.Body.String()) + require.Equal(t, `["https://severe1.com","https://severe2.com"]`, m.Message) + require.Equal(t, `2 Severe Errors`, m.Title) +} + func newTestConfig(t *testing.T) *Config { conf := NewConfig() conf.BaseURL = "http://127.0.0.1:12345" From b2eb5b94bdc8efd76516aae9b5db4a10d2360bad Mon Sep 17 00:00:00 2001 From: Hunter Kehoe Date: Mon, 18 Mar 2024 20:04:40 -0600 Subject: [PATCH 02/14] use existing message and title fields for templates --- server/server.go | 70 ++++++++++++++++++++----------------------- server/server_test.go | 41 +++++++++++++++---------- 2 files changed, 58 insertions(+), 53 deletions(-) diff --git a/server/server.go b/server/server.go index 5af493e6..56afa8ba 100644 --- a/server/server.go +++ b/server/server.go @@ -739,7 +739,7 @@ func (s *Server) handlePublishInternal(r *http.Request, v *visitor) (*message, e return nil, err } m := newDefaultMessage(t.ID, "") - cache, firebase, email, call, messageTemplate, titleTemplate, unifiedpush, e := s.parsePublishParams(r, m) + cache, firebase, email, call, template, unifiedpush, e := s.parsePublishParams(r, m) if e != nil { return nil, e.With(t) } @@ -770,7 +770,7 @@ func (s *Server) handlePublishInternal(r *http.Request, v *visitor) (*message, e if cache { m.Expires = time.Unix(m.Time, 0).Add(v.Limits().MessageExpiryDuration).Unix() } - if err := s.handlePublishBody(r, v, m, body, messageTemplate, titleTemplate, unifiedpush); err != nil { + if err := s.handlePublishBody(r, v, m, body, template, unifiedpush); err != nil { return nil, err } if m.Message == "" { @@ -925,7 +925,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, messageTemplate string, titleTemplate string, unifiedpush bool, err *errHTTP) { +func (s *Server) parsePublishParams(r *http.Request, m *message) (cache bool, firebase bool, email, call string, template bool, 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") @@ -941,7 +941,7 @@ func (s *Server) parsePublishParams(r *http.Request, m *message) (cache bool, fi } if attach != "" { if !urlRegex.MatchString(attach) { - return false, false, "", "", "", "", false, errHTTPBadRequestAttachmentURLInvalid + return false, false, "", "", false, false, errHTTPBadRequestAttachmentURLInvalid } m.Attachment.URL = attach if m.Attachment.Name == "" { @@ -959,20 +959,20 @@ func (s *Server) parsePublishParams(r *http.Request, m *message) (cache bool, fi } if icon != "" { if !urlRegex.MatchString(icon) { - return false, false, "", "", "", "", false, errHTTPBadRequestIconURLInvalid + return false, 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, errHTTPBadRequestEmailDisabled + return false, false, "", "", false, false, errHTTPBadRequestEmailDisabled } call = readParam(r, "x-call", "call") if call != "" && (s.config.TwilioAccount == "" || s.userManager == nil) { print("call: %s", call) - return false, false, "", "", "", "", false, errHTTPBadRequestPhoneCallsDisabled + return false, false, "", "", false, false, errHTTPBadRequestPhoneCallsDisabled } else if call != "" && !isBoolValue(call) && !phoneNumberRegex.MatchString(call) { - return false, false, "", "", "", "", false, errHTTPBadRequestPhoneNumberInvalid + return false, false, "", "", false, false, errHTTPBadRequestPhoneNumberInvalid } messageStr := strings.ReplaceAll(readParam(r, "x-message", "message", "m"), "\\n", "\n") if messageStr != "" { @@ -981,27 +981,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, errHTTPBadRequestPriorityInvalid + return false, 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, errHTTPBadRequestDelayNoCache + return false, false, "", "", false, false, errHTTPBadRequestDelayNoCache } if email != "" { - return false, false, "", "", "", "", false, errHTTPBadRequestDelayNoEmail // we cannot store the email address (yet) + return false, false, "", "", false, false, errHTTPBadRequestDelayNoEmail // we cannot store the email address (yet) } if call != "" { - return false, false, "", "", "", "", false, errHTTPBadRequestDelayNoCall // we cannot store the phone number (yet) + return false, 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, errHTTPBadRequestDelayCannotParse + return false, false, "", "", false, false, errHTTPBadRequestDelayCannotParse } else if delay.Unix() < time.Now().Add(s.config.MessageDelayMin).Unix() { - return false, false, "", "", "", "", false, errHTTPBadRequestDelayTooSmall + return false, false, "", "", false, false, errHTTPBadRequestDelayTooSmall } else if delay.Unix() > time.Now().Add(s.config.MessageDelayMax).Unix() { - return false, false, "", "", "", "", false, errHTTPBadRequestDelayTooLarge + return false, false, "", "", false, false, errHTTPBadRequestDelayTooLarge } m.Time = delay.Unix() } @@ -1009,15 +1009,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, errHTTPBadRequestActionsInvalid.Wrap(e.Error()) + return false, false, "", "", false, false, errHTTPBadRequestActionsInvalid.Wrap(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" } - messageTemplate = readParam(r, "x-template-message", "template-message", "tpl-m") - titleTemplate = readParam(r, "x-template-title", "template-title", "tpl-t") + template = readBoolParam(r, false, "x-template", "template", "tpl") unifiedpush = readBoolParam(r, false, "x-unifiedpush", "unifiedpush", "up") // see GET too! if unifiedpush { firebase = false @@ -1029,7 +1028,7 @@ func (s *Server) parsePublishParams(r *http.Request, m *message) (cache bool, fi cache = false email = "" } - return cache, firebase, email, call, messageTemplate, titleTemplate, unifiedpush, nil + return cache, firebase, email, call, template, unifiedpush, nil } // handlePublishBody consumes the PUT/POST body and decides whether the body is an attachment or the message. @@ -1046,17 +1045,17 @@ 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 // 6. curl -T file.txt ntfy.sh/mytopic // 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, messageTemplate string, titleTemplate string, unifiedpush bool) error { +func (s *Server) handlePublishBody(r *http.Request, v *visitor, m *message, body *util.PeekedReadCloser, template bool, unifiedpush bool) error { if m.Event == pollRequestEvent { // Case 1 return s.handleBodyDiscard(body) } else if unifiedpush { return s.handleBodyAsMessageAutoDetect(m, body) // Case 2 } else if m.Attachment != nil && m.Attachment.URL != "" { - return s.handleBodyAsTextMessage(m, body, messageTemplate, titleTemplate) // Case 3 + return s.handleBodyAsTextMessage(m, body, template) // Case 3 } else if m.Attachment != nil && m.Attachment.Name != "" { return s.handleBodyAsAttachment(r, v, m, body) // Case 4 } else if !body.LimitReached && utf8.Valid(body.PeekedBytes) { - return s.handleBodyAsTextMessage(m, body, messageTemplate, titleTemplate) // Case 5 + return s.handleBodyAsTextMessage(m, body, template) // Case 5 } return s.handleBodyAsAttachment(r, v, m, body) // Case 6 } @@ -1077,39 +1076,36 @@ func (s *Server) handleBodyAsMessageAutoDetect(m *message, body *util.PeekedRead return nil } -func (s *Server) handleBodyAsTextMessage(m *message, body *util.PeekedReadCloser, messageTemplate string, titleTemplate string) error { +func (s *Server) handleBodyAsTextMessage(m *message, body *util.PeekedReadCloser, template bool) error { if !utf8.Valid(body.PeekedBytes) { return errHTTPBadRequestMessageNotUTF8.With(m) } if len(body.PeekedBytes) > 0 { // Empty body should not override message (publish via GET!) peakedBody := strings.TrimSpace(string(body.PeekedBytes)) // Truncates the message to the peek limit if required - // Replace JSON paths in messageTemplate - if messageTemplate != "" && gjson.Valid(peakedBody) { - m.Message = messageTemplate + if template && gjson.Valid(peakedBody) { + // Replace JSON paths in message r := regexp.MustCompile(`\${([^}]+)}`) - messageMatches := r.FindAllStringSubmatch(messageTemplate, -1) - for _, v := range messageMatches { + matches := r.FindAllStringSubmatch(m.Message, -1) + for _, v := range matches { query := v[1] result := gjson.Get(peakedBody, query) if result.Exists() { m.Message = strings.ReplaceAll(m.Message, fmt.Sprintf("${%s}", query), result.String()) } } - } else { - m.Message = peakedBody - } - // Replace JSON paths in titleTemplate - if titleTemplate != "" && gjson.Valid(peakedBody) { - m.Title = titleTemplate - r := regexp.MustCompile(`\${([^}]+)}`) - titleMatches := r.FindAllStringSubmatch(titleTemplate, -1) - for _, v := range titleMatches { + + // Replace JSON paths in title + r = regexp.MustCompile(`\${([^}]+)}`) + matches = r.FindAllStringSubmatch(m.Title, -1) + for _, v := range matches { query := v[1] result := gjson.Get(peakedBody, query) if result.Exists() { m.Title = strings.ReplaceAll(m.Title, fmt.Sprintf("${%s}", query), result.String()) } } + } else { + m.Message = peakedBody } } if m.Attachment != nil && m.Attachment.Name != "" && m.Message == "" { diff --git a/server/server_test.go b/server/server_test.go index 4f634360..35ea2c54 100644 --- a/server/server_test.go +++ b/server/server_test.go @@ -2628,8 +2628,9 @@ func TestServer_UpstreamBaseURL_DoNotForwardUnifiedPush(t *testing.T) { func TestServer_MessageTemplate(t *testing.T) { s := newTestServer(t, newTestConfig(t)) response := request(t, s, "PUT", "/mytopic", `{"foo":"bar", "nested":{"title":"here"}}`, map[string]string{ - "X-Template-Message": "${foo}", - "X-Template-Title": "${nested.title}", + "X-Message": "${foo}", + "X-Title": "${nested.title}", + "X-Template": "1", }) require.Equal(t, 200, response.Code) @@ -2641,8 +2642,9 @@ func TestServer_MessageTemplate(t *testing.T) { func TestServer_MessageTemplate_RepeatPlaceholder(t *testing.T) { s := newTestServer(t, newTestConfig(t)) response := request(t, s, "PUT", "/mytopic", `{"foo":"bar", "nested":{"title":"here"}}`, map[string]string{ - "Template-Message": "${foo} is ${foo}", - "Template-Title": "${nested.title} is ${nested.title}", + "Message": "${foo} is ${foo}", + "Title": "${nested.title} is ${nested.title}", + "Template": "1", }) require.Equal(t, 200, response.Code) @@ -2655,8 +2657,9 @@ func TestServer_MessageTemplate_JSONBody(t *testing.T) { s := newTestServer(t, newTestConfig(t)) body := `{"topic": "mytopic", "message": "{\"foo\":\"bar\",\"nested\":{\"title\":\"here\"}}"}` response := request(t, s, "PUT", "/", body, map[string]string{ - "tpl-m": "${foo}", - "tpl-t": "${nested.title}", + "m": "${foo}", + "t": "${nested.title}", + "tpl": "1", }) require.Equal(t, 200, response.Code) @@ -2669,21 +2672,23 @@ func TestServer_MessageTemplate_MalformedJSONBody(t *testing.T) { s := newTestServer(t, newTestConfig(t)) body := `{"topic": "mytopic", "message": "{\"foo\":\"bar\",\"nested\":{\"title\":\"here\"INVALID"}` response := request(t, s, "PUT", "/", body, map[string]string{ - "X-Template-Message": "${foo}", - "X-Template-Title": "${nested.title}", + "X-Message": "${foo}", + "X-Title": "${nested.title}", + "X-Template": "1", }) require.Equal(t, 200, response.Code, "Got %s", response) m := toMessage(t, response.Body.String()) require.Equal(t, "{\"foo\":\"bar\",\"nested\":{\"title\":\"here\"INVALID", m.Message) - require.Equal(t, "", m.Title) + require.Equal(t, "${nested.title}", m.Title) } func TestServer_MessageTemplate_PlaceholderTypo(t *testing.T) { s := newTestServer(t, newTestConfig(t)) response := request(t, s, "PUT", "/mytopic", `{"foo":"bar", "nested":{"title":"here"}}`, map[string]string{ - "X-Template-Message": "${food}", - "X-Template-Title": "${nested.titl}", + "X-Message": "${food}", + "X-Title": "${nested.titl}", + "X-Template": "1", }) require.Equal(t, 200, response.Code) @@ -2695,7 +2700,8 @@ func TestServer_MessageTemplate_PlaceholderTypo(t *testing.T) { func TestServer_MessageTemplate_MultiplePlaceholders(t *testing.T) { s := newTestServer(t, newTestConfig(t)) response := request(t, s, "PUT", "/mytopic", `{"foo":"bar", "nested":{"title":"here"}}`, map[string]string{ - "X-Template-Message": "${foo} is ${nested.title}", + "X-Message": "${foo} is ${nested.title}", + "X-Template": "1", }) require.Equal(t, 200, response.Code) @@ -2708,7 +2714,8 @@ func TestServer_MessageTemplate_NestedPlaceholders(t *testing.T) { // i.e., ${${nested.bar}} should NOT evaluate to ${foo} and then to "bar" s := newTestServer(t, newTestConfig(t)) response := request(t, s, "PUT", "/mytopic", `{"foo":"bar", "nested":{"title":"here","bar":"foo"}}`, map[string]string{ - "X-Template-Message": "${${nested.bar}}", + "X-Message": "${${nested.bar}}", + "X-Template": "1", }) require.Equal(t, 200, response.Code) @@ -2723,7 +2730,8 @@ func TestServer_MessageTemplate_NestedPlaceholdersFunky(t *testing.T) { // included by the regex, so it is still there after replacing the placeholder, thus giving you "works!}" s := newTestServer(t, newTestConfig(t)) response := request(t, s, "PUT", "/mytopic", `{"foo":"bar", "nested":{"title":"here","bar":"foo"}, "${nested":{"bar":"works!"}}`, map[string]string{ - "X-Template-Message": "${${nested.bar}}", + "X-Message": "${${nested.bar}}", + "X-Template": "1", }) require.Equal(t, 200, response.Code) @@ -2735,8 +2743,9 @@ func TestServer_MessageTemplate_FancyGJSON(t *testing.T) { s := newTestServer(t, newTestConfig(t)) jsonBody := `{"foo": "bar", "errors": [{"level": "severe", "url": "https://severe1.com"},{"level": "warning", "url": "https://warning.com"},{"level": "severe", "url": "https://severe2.com"}]}` response := request(t, s, "PUT", "/mytopic", jsonBody, map[string]string{ - "X-Template-Message": `${errors.#(level=="severe")#.url}`, - "X-Template-Title": `${errors.#(level=="severe")#|#} Severe Errors`, + "X-Message": `${errors.#(level=="severe")#.url}`, + "X-Title": `${errors.#(level=="severe")#|#} Severe Errors`, + "X-Template": "1", }) require.Equal(t, 200, response.Code) From 867cf2808068fd50678b6588f8780497afb88a83 Mon Sep 17 00:00:00 2001 From: Hunter Kehoe Date: Tue, 19 Mar 2024 20:21:45 -0600 Subject: [PATCH 03/14] refactor gjson parsing code --- server/server.go | 41 +++++++++++++++++------------------------ 1 file changed, 17 insertions(+), 24 deletions(-) diff --git a/server/server.go b/server/server.go index 56afa8ba..8be6ce75 100644 --- a/server/server.go +++ b/server/server.go @@ -110,6 +110,7 @@ var ( fileRegex = regexp.MustCompile(`^/file/([-_A-Za-z0-9]{1,64})(?:\.[A-Za-z0-9]{1,16})?$`) urlRegex = regexp.MustCompile(`^https?://`) phoneNumberRegex = regexp.MustCompile(`^\+\d{1,100}$`) + templateVarRegex = regexp.MustCompile(`\${([^}]+)}`) //go:embed site webFs embed.FS @@ -1076,36 +1077,28 @@ func (s *Server) handleBodyAsMessageAutoDetect(m *message, body *util.PeekedRead return nil } +func replaceGJSONTemplate(template string, source string) string { + matches := templateVarRegex.FindAllStringSubmatch(template, -1) + for _, v := range matches { + query := v[1] + if result := gjson.Get(source, query); result.Exists() { + template = strings.ReplaceAll(template, fmt.Sprintf("${%s}", query), result.String()) + } + } + return template +} + func (s *Server) handleBodyAsTextMessage(m *message, body *util.PeekedReadCloser, template bool) error { if !utf8.Valid(body.PeekedBytes) { return errHTTPBadRequestMessageNotUTF8.With(m) } if len(body.PeekedBytes) > 0 { // Empty body should not override message (publish via GET!) - peakedBody := strings.TrimSpace(string(body.PeekedBytes)) // Truncates the message to the peek limit if required - if template && gjson.Valid(peakedBody) { - // Replace JSON paths in message - r := regexp.MustCompile(`\${([^}]+)}`) - matches := r.FindAllStringSubmatch(m.Message, -1) - for _, v := range matches { - query := v[1] - result := gjson.Get(peakedBody, query) - if result.Exists() { - m.Message = strings.ReplaceAll(m.Message, fmt.Sprintf("${%s}", query), result.String()) - } - } - - // Replace JSON paths in title - r = regexp.MustCompile(`\${([^}]+)}`) - matches = r.FindAllStringSubmatch(m.Title, -1) - for _, v := range matches { - query := v[1] - result := gjson.Get(peakedBody, query) - if result.Exists() { - m.Title = strings.ReplaceAll(m.Title, fmt.Sprintf("${%s}", query), result.String()) - } - } + peekedBody := strings.TrimSpace(string(body.PeekedBytes)) // Truncates the message to the peek limit if required + if template && gjson.Valid(peekedBody) { + m.Message = replaceGJSONTemplate(m.Message, peekedBody) + m.Title = replaceGJSONTemplate(m.Title, peekedBody) } else { - m.Message = peakedBody + m.Message = peekedBody } } if m.Attachment != nil && m.Attachment.Name != "" && m.Message == "" { From 03737dbf5c521a6e87d2f37509822bcf4f853de2 Mon Sep 17 00:00:00 2001 From: Hunter Kehoe Date: Tue, 19 Mar 2024 20:55:36 -0600 Subject: [PATCH 04/14] update docs --- docs/publish.md | 19 ++++++++++++------- 1 file changed, 12 insertions(+), 7 deletions(-) diff --git a/docs/publish.md b/docs/publish.md index cd67ed69..672f2fa3 100644 --- a/docs/publish.md +++ b/docs/publish.md @@ -3559,27 +3559,32 @@ ntfy server plays the role of the Push Gateway, as well as the Push Provider. Un ### Message and Title Templates Some services let you specify a webhook URL but do not let you modify the webhook body (e.g., Grafana). Instead of using a separate -bridge program to parse the webhook body into the format ntfy expects, you can include a message template and/or a title template +bridge program to parse the webhook body into the format ntfy expects, you can include a templated message and/or a templated title which will be populated based on the fields of the webhook body (so long as the webhook body is valid JSON). -Send the message template with the header `X-Template-Message`, `Template-Message`, or `tpl-m`. Send the title template with the -header `X-Template-Title`, `Template-Title`, or `tpl-t`. (No other fields can be filled with a template at this time). - -In the template, include paths to the appropriate JSON fields surrounded by `${` and `}`. See an example below. +Enable templating by setting the `X-Template` header (or its aliases `Template` or `tpl`) to "yes". Then, include templates +in your message and/or title (no other fields can be filled with a template at this time) by including paths to the +appropriate JSON fields surrounded by `${` and `}`. See an example below. See [GJSON docs](https://github.com/tidwall/gjson/blob/master/SYNTAX.md) for supported JSON path syntax. +[https://gjson.dev/](https://gjson.dev/) is a great resource for testing your templates. === "HTTP" ``` http POST /mytopic HTTP/1.1 Host: ntfy.sh - X-Template-Message: Error message: ${error.desc} - X-Template-Title: ${hostname}: A ${error.level} error has occurred + X-Message: Error message: ${error.desc} + X-Title: ${hostname}: A ${error.level} error has occurred + X-Template: yes {"hostname": "philipp-pc", "error": {"level": "severe", "desc": "Disk has run out of space"}} ``` The example above would send a notification with a title "philipp-pc: A severe error has occurred" and a message "Error message: Disk has run out of space". +For Grafana webhooks, you might find it helpful to use the headers `X-Title: Grafana alert: ${title}` and `X-Message: ${message}`. +Alternatively, you can include the params in the webhook URL. For example, by +appending `?template=yes&title=Grafana alert: ${title}&message=${message}` to the URL. + ## Public topics Obviously all topics on ntfy.sh are public, but there are a few designated topics that are used in examples, and topics that you can use to try out what [authentication and access control](#authentication) looks like. From 7fd5f0b29d4d0b9cd34bd9d5c1e3759d48a6916b Mon Sep 17 00:00:00 2001 From: Hunter Kehoe Date: Tue, 19 Mar 2024 21:56:55 -0600 Subject: [PATCH 05/14] allow large HTTP body so long as resulting message is small --- server/errors.go | 1 + server/server.go | 21 ++++++++++++++++++--- server/server_test.go | 29 +++++++++++++++++++++++++++++ 3 files changed, 48 insertions(+), 3 deletions(-) diff --git a/server/errors.go b/server/errors.go index 072bdc01..05adeb66 100644 --- a/server/errors.go +++ b/server/errors.go @@ -117,6 +117,7 @@ var ( errHTTPBadRequestWebPushSubscriptionInvalid = &errHTTP{40038, http.StatusBadRequest, "invalid request: web push payload malformed", "", nil} errHTTPBadRequestWebPushEndpointUnknown = &errHTTP{40039, http.StatusBadRequest, "invalid request: web push endpoint unknown", "", nil} errHTTPBadRequestWebPushTopicCountTooHigh = &errHTTP{40040, http.StatusBadRequest, "invalid request: too many web push topic subscriptions", "", nil} + errHTTPBadRequestTemplatedMessageTooLarge = &errHTTP{40041, http.StatusBadRequest, "invalid request: message is too large after replacing template", "", 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} diff --git a/server/server.go b/server/server.go index 8be6ce75..e6b1b88e 100644 --- a/server/server.go +++ b/server/server.go @@ -1044,8 +1044,11 @@ func (s *Server) parsePublishParams(r *http.Request, m *message) (cache bool, fi // Body must be attachment, because we passed a filename // 5. curl -T file.txt ntfy.sh/mytopic // If file.txt is <= 4096 (message limit) and valid UTF-8, treat it as a message -// 6. curl -T file.txt ntfy.sh/mytopic -// If file.txt is > message limit, treat it as an attachment +// 6. curl -H "Template: yes" -T file.txt ntfy.sh/mytopic +// If file.txt is < 4096*2 (message limit*2) and a template is used, try parsing under the assumption +// that the message generated by the template will be less than 4096 +// 7. curl -T file.txt ntfy.sh/mytopic +// If file.txt is > message limit or template && file.txt > message limit*2, treat it as an attachment func (s *Server) handlePublishBody(r *http.Request, v *visitor, m *message, body *util.PeekedReadCloser, template bool, unifiedpush bool) error { if m.Event == pollRequestEvent { // Case 1 return s.handleBodyDiscard(body) @@ -1057,8 +1060,16 @@ func (s *Server) handlePublishBody(r *http.Request, v *visitor, m *message, body return s.handleBodyAsAttachment(r, v, m, body) // Case 4 } else if !body.LimitReached && utf8.Valid(body.PeekedBytes) { return s.handleBodyAsTextMessage(m, body, template) // Case 5 + } else if template { + templateBody, err := util.Peek(body, s.config.MessageSizeLimit*2) + if err != nil { + return err + } + if !templateBody.LimitReached { + return s.handleBodyAsTextMessage(m, templateBody, template) // Case 6 + } } - return s.handleBodyAsAttachment(r, v, m, body) // Case 6 + return s.handleBodyAsAttachment(r, v, m, body) // Case 7 } func (s *Server) handleBodyDiscard(body *util.PeekedReadCloser) error { @@ -1104,6 +1115,10 @@ func (s *Server) handleBodyAsTextMessage(m *message, body *util.PeekedReadCloser if m.Attachment != nil && m.Attachment.Name != "" && m.Message == "" { m.Message = fmt.Sprintf(defaultAttachmentMessage, m.Attachment.Name) } + // Ensure message is less than message limit after templating + if len(m.Message) > s.config.MessageSizeLimit { + return errHTTPBadRequestTemplatedMessageTooLarge + } return nil } diff --git a/server/server_test.go b/server/server_test.go index 35ea2c54..99f4c4de 100644 --- a/server/server_test.go +++ b/server/server_test.go @@ -2754,6 +2754,35 @@ func TestServer_MessageTemplate_FancyGJSON(t *testing.T) { require.Equal(t, `2 Severe Errors`, m.Title) } +func TestServer_MessageTemplate_ExceedMessageSize_TemplatedMessageOK(t *testing.T) { + c := newTestConfig(t) + c.MessageSizeLimit = 25 // 25 < len(HTTP body) < 25*2 && len(m.Message) < 25 + s := newTestServer(t, c) + response := request(t, s, "PUT", "/mytopic", `{"foo":"bar", "nested":{"title":"here"}}`, map[string]string{ + "X-Message": "${foo}", + "X-Title": "${nested.title}", + "X-Template": "1", + }) + + require.Equal(t, 200, response.Code) + m := toMessage(t, response.Body.String()) + require.Equal(t, "bar", m.Message) + require.Equal(t, "here", m.Title) +} + +func TestServer_MessageTemplate_ExceedMessageSize_TemplatedMessageTooLong(t *testing.T) { + c := newTestConfig(t) + c.MessageSizeLimit = 21 // 21 < len(HTTP body) < 21*2 && !len(m.Message) < 21 + s := newTestServer(t, c) + response := request(t, s, "PUT", "/mytopic", `{"foo":"This is a long message"}`, map[string]string{ + "X-Message": "${foo}", + "X-Template": "1", + }) + + require.Equal(t, 400, response.Code) + require.Equal(t, 40041, toHTTPError(t, response.Body.String()).Code) +} + func newTestConfig(t *testing.T) *Config { conf := NewConfig() conf.BaseURL = "http://127.0.0.1:12345" From 83356f565e5327b2bb20c94530c12e17af2796da Mon Sep 17 00:00:00 2001 From: wunter8 Date: Wed, 20 Mar 2024 10:54:41 -0600 Subject: [PATCH 06/14] remove debug print statement --- server/server.go | 1 - 1 file changed, 1 deletion(-) diff --git a/server/server.go b/server/server.go index e6b1b88e..337a3d16 100644 --- a/server/server.go +++ b/server/server.go @@ -970,7 +970,6 @@ func (s *Server) parsePublishParams(r *http.Request, m *message) (cache bool, fi } call = readParam(r, "x-call", "call") if call != "" && (s.config.TwilioAccount == "" || s.userManager == nil) { - print("call: %s", call) return false, false, "", "", false, false, errHTTPBadRequestPhoneCallsDisabled } else if call != "" && !isBoolValue(call) && !phoneNumberRegex.MatchString(call) { return false, false, "", "", false, false, errHTTPBadRequestPhoneNumberInvalid From de65d0751803306d07081f41501688cb0e9b5120 Mon Sep 17 00:00:00 2001 From: binwiederhier Date: Wed, 20 Mar 2024 21:33:54 -0400 Subject: [PATCH 07/14] Simplify(?) templating cases --- cmd/access_test.go | 2 + cmd/config_loader_test.go | 1 + cmd/publish_test.go | 3 ++ server/errors.go | 3 +- server/server.go | 84 ++++++++++++++++++++------------------- server/server_account.go | 24 +++++------ server/server_admin.go | 17 ++++---- server/server_payments.go | 6 +-- server/server_test.go | 38 ++++++++++++++---- server/server_webpush.go | 4 +- server/util.go | 5 ++- test/server.go | 2 +- util/peek.go | 5 ++- 13 files changed, 115 insertions(+), 79 deletions(-) diff --git a/cmd/access_test.go b/cmd/access_test.go index 81c9f2b9..47aa9dae 100644 --- a/cmd/access_test.go +++ b/cmd/access_test.go @@ -10,6 +10,7 @@ import ( ) func TestCLI_Access_Show(t *testing.T) { + t.Parallel() s, conf, port := newTestServerWithAuth(t) defer test.StopServer(t, s, port) @@ -19,6 +20,7 @@ func TestCLI_Access_Show(t *testing.T) { } func TestCLI_Access_Grant_And_Publish(t *testing.T) { + t.Parallel() s, conf, port := newTestServerWithAuth(t) defer test.StopServer(t, s, port) diff --git a/cmd/config_loader_test.go b/cmd/config_loader_test.go index 7a7f2bf1..67a4bcbe 100644 --- a/cmd/config_loader_test.go +++ b/cmd/config_loader_test.go @@ -8,6 +8,7 @@ import ( ) func TestNewYamlSourceFromFile(t *testing.T) { + t.Parallel() filename := filepath.Join(t.TempDir(), "server.yml") contents := ` # Normal options diff --git a/cmd/publish_test.go b/cmd/publish_test.go index 31d01cb5..e03ae1dc 100644 --- a/cmd/publish_test.go +++ b/cmd/publish_test.go @@ -17,6 +17,7 @@ import ( ) func TestCLI_Publish_Subscribe_Poll_Real_Server(t *testing.T) { + t.Parallel() testMessage := util.RandomString(10) app, _, _, _ := newTestApp() require.Nil(t, app.Run([]string{"ntfy", "publish", "ntfytest", "ntfy unit test " + testMessage})) @@ -35,6 +36,7 @@ func TestCLI_Publish_Subscribe_Poll_Real_Server(t *testing.T) { } func TestCLI_Publish_Subscribe_Poll(t *testing.T) { + t.Parallel() s, port := test.StartServer(t) defer test.StopServer(t, s, port) topic := fmt.Sprintf("http://127.0.0.1:%d/mytopic", port) @@ -51,6 +53,7 @@ func TestCLI_Publish_Subscribe_Poll(t *testing.T) { } func TestCLI_Publish_All_The_Things(t *testing.T) { + t.Parallel() s, port := test.StartServer(t) defer test.StopServer(t, s, port) topic := fmt.Sprintf("http://127.0.0.1:%d/mytopic", port) diff --git a/server/errors.go b/server/errors.go index 05adeb66..92ea0ee6 100644 --- a/server/errors.go +++ b/server/errors.go @@ -117,7 +117,8 @@ var ( errHTTPBadRequestWebPushSubscriptionInvalid = &errHTTP{40038, http.StatusBadRequest, "invalid request: web push payload malformed", "", nil} errHTTPBadRequestWebPushEndpointUnknown = &errHTTP{40039, http.StatusBadRequest, "invalid request: web push endpoint unknown", "", nil} errHTTPBadRequestWebPushTopicCountTooHigh = &errHTTP{40040, http.StatusBadRequest, "invalid request: too many web push topic subscriptions", "", nil} - errHTTPBadRequestTemplatedMessageTooLarge = &errHTTP{40041, http.StatusBadRequest, "invalid request: message is too large after replacing template", "", nil} + errHTTPBadRequestTemplatedMessageTooLarge = &errHTTP{40041, http.StatusBadRequest, "invalid request: message or title is too large after replacing template", "", nil} + errHTTPBadRequestTemplatedMessageNotJSON = &errHTTP{40042, http.StatusBadRequest, "invalid request: message body must be JSON if templating is enabled", "", 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} diff --git a/server/server.go b/server/server.go index 337a3d16..1c1950d1 100644 --- a/server/server.go +++ b/server/server.go @@ -111,6 +111,7 @@ var ( urlRegex = regexp.MustCompile(`^https?://`) phoneNumberRegex = regexp.MustCompile(`^\+\d{1,100}$`) templateVarRegex = regexp.MustCompile(`\${([^}]+)}`) + templateVarFormat = "${%s}" //go:embed site webFs embed.FS @@ -125,12 +126,12 @@ var ( const ( firebaseControlTopic = "~control" // See Android if changed - firebasePollTopic = "~poll" // See iOS if changed + firebasePollTopic = "~poll" // See iOS if changed (DISABLED for now) emptyMessageBody = "triggered" // Used if message body is empty 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 = 16384 // Max number of bytes for a JSON request body + httpBodyBytesLimit = 32768 // 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 @@ -675,7 +676,7 @@ func (s *Server) handleFile(w http.ResponseWriter, r *http.Request, v *visitor) // - avoid abuse (e.g. 1 uploader, 1k downloaders) // - and also uses the higher bandwidth limits of a paying user m, err := s.messageCache.Message(messageID) - if err == errMessageNotFound { + if errors.Is(err, errMessageNotFound) { if s.config.CacheBatchTimeout > 0 { // Strange edge case: If we immediately after upload request the file (the web app does this for images), // and messages are persisted asynchronously, retry fetching from the database @@ -874,7 +875,7 @@ func (s *Server) sendToFirebase(v *visitor, m *message) { logvm(v, m).Tag(tagFirebase).Debug("Publishing to Firebase") if err := s.firebaseClient.Send(v, m); err != nil { minc(metricFirebasePublishedFailure) - if err == errFirebaseTemporarilyBanned { + if errors.Is(err, errFirebaseTemporarilyBanned) { logvm(v, m).Tag(tagFirebase).Err(err).Debug("Unable to publish to Firebase: %v", err.Error()) } else { logvm(v, m).Tag(tagFirebase).Err(err).Warn("Unable to publish to Firebase: %v", err.Error()) @@ -1036,37 +1037,30 @@ func (s *Server) parsePublishParams(r *http.Request, m *message) (cache bool, fi // 1. curl -X POST -H "Poll: 1234" ntfy.sh/... // If a message is flagged as poll request, the body does not matter and is discarded // 2. curl -T somebinarydata.bin "ntfy.sh/mytopic?up=1" -// If body is binary, encode as base64, if not do not encode +// If UnifiedPush is enabled, encode as base64 if body is binary, and do not trim // 3. curl -H "Attach: http://example.com/file.jpg" ntfy.sh/mytopic // Body must be a message, because we attached an external URL // 4. curl -T short.txt -H "Filename: short.txt" ntfy.sh/mytopic // Body must be attachment, because we passed a filename -// 5. curl -T file.txt ntfy.sh/mytopic +// 5. curl -H "Template: yes" -T file.txt ntfy.sh/mytopic +// If templating is enabled, read up to 32k and treat message body as JSON +// 6. curl -T file.txt ntfy.sh/mytopic // If file.txt is <= 4096 (message limit) and valid UTF-8, treat it as a message -// 6. curl -H "Template: yes" -T file.txt ntfy.sh/mytopic -// If file.txt is < 4096*2 (message limit*2) and a template is used, try parsing under the assumption -// that the message generated by the template will be less than 4096 // 7. curl -T file.txt ntfy.sh/mytopic // If file.txt is > message limit or template && file.txt > message limit*2, treat it as an attachment -func (s *Server) handlePublishBody(r *http.Request, v *visitor, m *message, body *util.PeekedReadCloser, template bool, unifiedpush bool) error { +func (s *Server) handlePublishBody(r *http.Request, v *visitor, m *message, body *util.PeekedReadCloser, template, unifiedpush bool) error { if m.Event == pollRequestEvent { // Case 1 return s.handleBodyDiscard(body) } else if unifiedpush { return s.handleBodyAsMessageAutoDetect(m, body) // Case 2 } else if m.Attachment != nil && m.Attachment.URL != "" { - return s.handleBodyAsTextMessage(m, body, template) // Case 3 + 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 !body.LimitReached && utf8.Valid(body.PeekedBytes) { - return s.handleBodyAsTextMessage(m, body, template) // Case 5 } else if template { - templateBody, err := util.Peek(body, s.config.MessageSizeLimit*2) - if err != nil { - return err - } - if !templateBody.LimitReached { - return s.handleBodyAsTextMessage(m, templateBody, template) // Case 6 - } + return s.handleBodyAsTemplatedTextMessage(m, body) // Case 5 + } else if !body.LimitReached && utf8.Valid(body.PeekedBytes) { + return s.handleBodyAsTextMessage(m, body) // Case 6 } return s.handleBodyAsAttachment(r, v, m, body) // Case 7 } @@ -1087,34 +1081,32 @@ func (s *Server) handleBodyAsMessageAutoDetect(m *message, body *util.PeekedRead return nil } -func replaceGJSONTemplate(template string, source string) string { - matches := templateVarRegex.FindAllStringSubmatch(template, -1) - for _, v := range matches { - query := v[1] - if result := gjson.Get(source, query); result.Exists() { - template = strings.ReplaceAll(template, fmt.Sprintf("${%s}", query), result.String()) - } - } - return template -} - -func (s *Server) handleBodyAsTextMessage(m *message, body *util.PeekedReadCloser, template bool) error { +func (s *Server) handleBodyAsTextMessage(m *message, body *util.PeekedReadCloser) error { if !utf8.Valid(body.PeekedBytes) { return errHTTPBadRequestMessageNotUTF8.With(m) } if len(body.PeekedBytes) > 0 { // Empty body should not override message (publish via GET!) - peekedBody := strings.TrimSpace(string(body.PeekedBytes)) // Truncates the message to the peek limit if required - if template && gjson.Valid(peekedBody) { - m.Message = replaceGJSONTemplate(m.Message, peekedBody) - m.Title = replaceGJSONTemplate(m.Title, peekedBody) - } else { - m.Message = peekedBody - } + m.Message = strings.TrimSpace(string(body.PeekedBytes)) // Truncates the message to the peek limit if required } if m.Attachment != nil && m.Attachment.Name != "" && m.Message == "" { m.Message = fmt.Sprintf(defaultAttachmentMessage, m.Attachment.Name) } - // Ensure message is less than message limit after templating + return nil +} + +func (s *Server) handleBodyAsTemplatedTextMessage(m *message, body *util.PeekedReadCloser) error { + body, err := util.Peek(body, httpBodyBytesLimit) + if err != nil { + return err + } else if body.LimitReached { + return errHTTPEntityTooLargeJSONBody + } + peekedBody := strings.TrimSpace(string(body.PeekedBytes)) + if !gjson.Valid(peekedBody) { + return errHTTPBadRequestTemplatedMessageNotJSON + } + m.Message = replaceGJSONTemplate(m.Message, peekedBody) + m.Title = replaceGJSONTemplate(m.Title, peekedBody) if len(m.Message) > s.config.MessageSizeLimit { return errHTTPBadRequestTemplatedMessageTooLarge } @@ -1163,7 +1155,7 @@ func (s *Server) handleBodyAsAttachment(r *http.Request, v *visitor, m *message, util.NewFixedLimiter(vinfo.Stats.AttachmentTotalSizeRemaining), } m.Attachment.Size, err = s.fileCache.Write(m.ID, body, limiters...) - if err == util.ErrLimitReached { + if errors.Is(err, util.ErrLimitReached) { return errHTTPEntityTooLargeAttachment.With(m) } else if err != nil { return err @@ -1171,6 +1163,16 @@ func (s *Server) handleBodyAsAttachment(r *http.Request, v *visitor, m *message, return nil } +func replaceGJSONTemplate(template string, source string) string { + matches := templateVarRegex.FindAllStringSubmatch(template, -1) + for _, m := range matches { + if result := gjson.Get(source, m[1]); result.Exists() { + template = strings.ReplaceAll(template, fmt.Sprintf(templateVarFormat, m[1]), result.String()) + } + } + return template +} + func (s *Server) handleSubscribeJSON(w http.ResponseWriter, r *http.Request, v *visitor) error { encoder := func(msg *message) (string, error) { var buf bytes.Buffer diff --git a/server/server_account.go b/server/server_account.go index cb841d07..e457464d 100644 --- a/server/server_account.go +++ b/server/server_account.go @@ -28,7 +28,7 @@ func (s *Server) handleAccountCreate(w http.ResponseWriter, r *http.Request, v * return errHTTPTooManyRequestsLimitAccountCreation } } - newAccount, err := readJSONWithLimit[apiAccountCreateRequest](r.Body, jsonBodyBytesLimit, false) + newAccount, err := readJSONWithLimit[apiAccountCreateRequest](r.Body, httpBodyBytesLimit, false) if err != nil { return err } @@ -160,7 +160,7 @@ func (s *Server) handleAccountGet(w http.ResponseWriter, r *http.Request, v *vis } func (s *Server) handleAccountDelete(w http.ResponseWriter, r *http.Request, v *visitor) error { - req, err := readJSONWithLimit[apiAccountDeleteRequest](r.Body, jsonBodyBytesLimit, false) + req, err := readJSONWithLimit[apiAccountDeleteRequest](r.Body, httpBodyBytesLimit, false) if err != nil { return err } else if req.Password == "" { @@ -192,7 +192,7 @@ func (s *Server) handleAccountDelete(w http.ResponseWriter, r *http.Request, v * } func (s *Server) handleAccountPasswordChange(w http.ResponseWriter, r *http.Request, v *visitor) error { - req, err := readJSONWithLimit[apiAccountPasswordChangeRequest](r.Body, jsonBodyBytesLimit, false) + req, err := readJSONWithLimit[apiAccountPasswordChangeRequest](r.Body, httpBodyBytesLimit, false) if err != nil { return err } else if req.Password == "" || req.NewPassword == "" { @@ -210,7 +210,7 @@ func (s *Server) handleAccountPasswordChange(w http.ResponseWriter, r *http.Requ } func (s *Server) handleAccountTokenCreate(w http.ResponseWriter, r *http.Request, v *visitor) error { - req, err := readJSONWithLimit[apiAccountTokenIssueRequest](r.Body, jsonBodyBytesLimit, true) // Allow empty body! + req, err := readJSONWithLimit[apiAccountTokenIssueRequest](r.Body, httpBodyBytesLimit, true) // Allow empty body! if err != nil { return err } @@ -246,7 +246,7 @@ func (s *Server) handleAccountTokenCreate(w http.ResponseWriter, r *http.Request func (s *Server) handleAccountTokenUpdate(w http.ResponseWriter, r *http.Request, v *visitor) error { u := v.User() - req, err := readJSONWithLimit[apiAccountTokenUpdateRequest](r.Body, jsonBodyBytesLimit, true) // Allow empty body! + req, err := readJSONWithLimit[apiAccountTokenUpdateRequest](r.Body, httpBodyBytesLimit, true) // Allow empty body! if err != nil { return err } else if req.Token == "" { @@ -302,7 +302,7 @@ func (s *Server) handleAccountTokenDelete(w http.ResponseWriter, r *http.Request } func (s *Server) handleAccountSettingsChange(w http.ResponseWriter, r *http.Request, v *visitor) error { - newPrefs, err := readJSONWithLimit[user.Prefs](r.Body, jsonBodyBytesLimit, false) + newPrefs, err := readJSONWithLimit[user.Prefs](r.Body, httpBodyBytesLimit, false) if err != nil { return err } @@ -336,7 +336,7 @@ func (s *Server) handleAccountSettingsChange(w http.ResponseWriter, r *http.Requ } func (s *Server) handleAccountSubscriptionAdd(w http.ResponseWriter, r *http.Request, v *visitor) error { - newSubscription, err := readJSONWithLimit[user.Subscription](r.Body, jsonBodyBytesLimit, false) + newSubscription, err := readJSONWithLimit[user.Subscription](r.Body, httpBodyBytesLimit, false) if err != nil { return err } @@ -359,7 +359,7 @@ func (s *Server) handleAccountSubscriptionAdd(w http.ResponseWriter, r *http.Req } func (s *Server) handleAccountSubscriptionChange(w http.ResponseWriter, r *http.Request, v *visitor) error { - updatedSubscription, err := readJSONWithLimit[user.Subscription](r.Body, jsonBodyBytesLimit, false) + updatedSubscription, err := readJSONWithLimit[user.Subscription](r.Body, httpBodyBytesLimit, false) if err != nil { return err } @@ -417,7 +417,7 @@ func (s *Server) handleAccountSubscriptionDelete(w http.ResponseWriter, r *http. // it is already reserved by someone else. func (s *Server) handleAccountReservationAdd(w http.ResponseWriter, r *http.Request, v *visitor) error { u := v.User() - req, err := readJSONWithLimit[apiAccountReservationRequest](r.Body, jsonBodyBytesLimit, false) + req, err := readJSONWithLimit[apiAccountReservationRequest](r.Body, httpBodyBytesLimit, false) if err != nil { return err } @@ -532,7 +532,7 @@ func (s *Server) maybeRemoveMessagesAndExcessReservations(r *http.Request, v *vi func (s *Server) handleAccountPhoneNumberVerify(w http.ResponseWriter, r *http.Request, v *visitor) error { u := v.User() - req, err := readJSONWithLimit[apiAccountPhoneNumberVerifyRequest](r.Body, jsonBodyBytesLimit, false) + req, err := readJSONWithLimit[apiAccountPhoneNumberVerifyRequest](r.Body, httpBodyBytesLimit, false) if err != nil { return err } else if !phoneNumberRegex.MatchString(req.Number) { @@ -563,7 +563,7 @@ func (s *Server) handleAccountPhoneNumberVerify(w http.ResponseWriter, r *http.R func (s *Server) handleAccountPhoneNumberAdd(w http.ResponseWriter, r *http.Request, v *visitor) error { u := v.User() - req, err := readJSONWithLimit[apiAccountPhoneNumberAddRequest](r.Body, jsonBodyBytesLimit, false) + req, err := readJSONWithLimit[apiAccountPhoneNumberAddRequest](r.Body, httpBodyBytesLimit, false) if err != nil { return err } @@ -582,7 +582,7 @@ func (s *Server) handleAccountPhoneNumberAdd(w http.ResponseWriter, r *http.Requ func (s *Server) handleAccountPhoneNumberDelete(w http.ResponseWriter, r *http.Request, v *visitor) error { u := v.User() - req, err := readJSONWithLimit[apiAccountPhoneNumberAddRequest](r.Body, jsonBodyBytesLimit, false) + req, err := readJSONWithLimit[apiAccountPhoneNumberAddRequest](r.Body, httpBodyBytesLimit, false) if err != nil { return err } diff --git a/server/server_admin.go b/server/server_admin.go index fc9dfed1..ec0b69b6 100644 --- a/server/server_admin.go +++ b/server/server_admin.go @@ -1,6 +1,7 @@ package server import ( + "errors" "heckel.io/ntfy/v2/user" "net/http" ) @@ -38,14 +39,14 @@ func (s *Server) handleUsersGet(w http.ResponseWriter, r *http.Request, v *visit } func (s *Server) handleUsersAdd(w http.ResponseWriter, r *http.Request, v *visitor) error { - req, err := readJSONWithLimit[apiUserAddRequest](r.Body, jsonBodyBytesLimit, false) + req, err := readJSONWithLimit[apiUserAddRequest](r.Body, httpBodyBytesLimit, false) if err != nil { return err } else if !user.AllowedUsername(req.Username) || req.Password == "" { return errHTTPBadRequest.Wrap("username invalid, or password missing") } u, err := s.userManager.User(req.Username) - if err != nil && err != user.ErrUserNotFound { + if err != nil && !errors.Is(err, user.ErrUserNotFound) { return err } else if u != nil { return errHTTPConflictUserExists @@ -53,7 +54,7 @@ func (s *Server) handleUsersAdd(w http.ResponseWriter, r *http.Request, v *visit var tier *user.Tier if req.Tier != "" { tier, err = s.userManager.Tier(req.Tier) - if err == user.ErrTierNotFound { + if errors.Is(err, user.ErrTierNotFound) { return errHTTPBadRequestTierInvalid } else if err != nil { return err @@ -71,12 +72,12 @@ func (s *Server) handleUsersAdd(w http.ResponseWriter, r *http.Request, v *visit } func (s *Server) handleUsersDelete(w http.ResponseWriter, r *http.Request, v *visitor) error { - req, err := readJSONWithLimit[apiUserDeleteRequest](r.Body, jsonBodyBytesLimit, false) + req, err := readJSONWithLimit[apiUserDeleteRequest](r.Body, httpBodyBytesLimit, false) if err != nil { return err } u, err := s.userManager.User(req.Username) - if err == user.ErrUserNotFound { + if errors.Is(err, user.ErrUserNotFound) { return errHTTPBadRequestUserNotFound } else if err != nil { return err @@ -93,12 +94,12 @@ func (s *Server) handleUsersDelete(w http.ResponseWriter, r *http.Request, v *vi } func (s *Server) handleAccessAllow(w http.ResponseWriter, r *http.Request, v *visitor) error { - req, err := readJSONWithLimit[apiAccessAllowRequest](r.Body, jsonBodyBytesLimit, false) + req, err := readJSONWithLimit[apiAccessAllowRequest](r.Body, httpBodyBytesLimit, false) if err != nil { return err } _, err = s.userManager.User(req.Username) - if err == user.ErrUserNotFound { + if errors.Is(err, user.ErrUserNotFound) { return errHTTPBadRequestUserNotFound } else if err != nil { return err @@ -114,7 +115,7 @@ func (s *Server) handleAccessAllow(w http.ResponseWriter, r *http.Request, v *vi } func (s *Server) handleAccessReset(w http.ResponseWriter, r *http.Request, v *visitor) error { - req, err := readJSONWithLimit[apiAccessResetRequest](r.Body, jsonBodyBytesLimit, false) + req, err := readJSONWithLimit[apiAccessResetRequest](r.Body, httpBodyBytesLimit, false) if err != nil { return err } diff --git a/server/server_payments.go b/server/server_payments.go index 334301bb..2fb42d31 100644 --- a/server/server_payments.go +++ b/server/server_payments.go @@ -115,7 +115,7 @@ func (s *Server) handleAccountBillingSubscriptionCreate(w http.ResponseWriter, r if u.Billing.StripeSubscriptionID != "" { return errHTTPBadRequestBillingSubscriptionExists } - req, err := readJSONWithLimit[apiAccountBillingSubscriptionChangeRequest](r.Body, jsonBodyBytesLimit, false) + req, err := readJSONWithLimit[apiAccountBillingSubscriptionChangeRequest](r.Body, httpBodyBytesLimit, false) if err != nil { return err } @@ -245,7 +245,7 @@ func (s *Server) handleAccountBillingSubscriptionUpdate(w http.ResponseWriter, r if u.Billing.StripeSubscriptionID == "" { return errNoBillingSubscription } - req, err := readJSONWithLimit[apiAccountBillingSubscriptionChangeRequest](r.Body, jsonBodyBytesLimit, false) + req, err := readJSONWithLimit[apiAccountBillingSubscriptionChangeRequest](r.Body, httpBodyBytesLimit, false) if err != nil { return err } @@ -342,7 +342,7 @@ func (s *Server) handleAccountBillingWebhook(_ http.ResponseWriter, r *http.Requ if stripeSignature == "" { return errHTTPBadRequestBillingRequestInvalid } - body, err := util.Peek(r.Body, jsonBodyBytesLimit) + body, err := util.Peek(r.Body, httpBodyBytesLimit) if err != nil { return err } else if body.LimitReached { diff --git a/server/server_test.go b/server/server_test.go index 99f4c4de..66c54fd0 100644 --- a/server/server_test.go +++ b/server/server_test.go @@ -2669,6 +2669,7 @@ func TestServer_MessageTemplate_JSONBody(t *testing.T) { } func TestServer_MessageTemplate_MalformedJSONBody(t *testing.T) { + t.Parallel() s := newTestServer(t, newTestConfig(t)) body := `{"topic": "mytopic", "message": "{\"foo\":\"bar\",\"nested\":{\"title\":\"here\"INVALID"}` response := request(t, s, "PUT", "/", body, map[string]string{ @@ -2677,13 +2678,12 @@ func TestServer_MessageTemplate_MalformedJSONBody(t *testing.T) { "X-Template": "1", }) - require.Equal(t, 200, response.Code, "Got %s", response) - m := toMessage(t, response.Body.String()) - require.Equal(t, "{\"foo\":\"bar\",\"nested\":{\"title\":\"here\"INVALID", m.Message) - require.Equal(t, "${nested.title}", m.Title) + require.Equal(t, 400, response.Code) + require.Equal(t, 40042, toHTTPError(t, response.Body.String()).Code) } func TestServer_MessageTemplate_PlaceholderTypo(t *testing.T) { + t.Parallel() s := newTestServer(t, newTestConfig(t)) response := request(t, s, "PUT", "/mytopic", `{"foo":"bar", "nested":{"title":"here"}}`, map[string]string{ "X-Message": "${food}", @@ -2756,12 +2756,12 @@ func TestServer_MessageTemplate_FancyGJSON(t *testing.T) { func TestServer_MessageTemplate_ExceedMessageSize_TemplatedMessageOK(t *testing.T) { c := newTestConfig(t) - c.MessageSizeLimit = 25 // 25 < len(HTTP body) < 25*2 && len(m.Message) < 25 + c.MessageSizeLimit = 25 // 25 < len(HTTP body) < 32k, and len(m.Message) < 25 s := newTestServer(t, c) response := request(t, s, "PUT", "/mytopic", `{"foo":"bar", "nested":{"title":"here"}}`, map[string]string{ "X-Message": "${foo}", "X-Title": "${nested.title}", - "X-Template": "1", + "X-Template": "yes", }) require.Equal(t, 200, response.Code) @@ -2772,7 +2772,7 @@ func TestServer_MessageTemplate_ExceedMessageSize_TemplatedMessageOK(t *testing. func TestServer_MessageTemplate_ExceedMessageSize_TemplatedMessageTooLong(t *testing.T) { c := newTestConfig(t) - c.MessageSizeLimit = 21 // 21 < len(HTTP body) < 21*2 && !len(m.Message) < 21 + c.MessageSizeLimit = 21 // 21 < len(HTTP body) < 32k, but !len(m.Message) < 21 s := newTestServer(t, c) response := request(t, s, "PUT", "/mytopic", `{"foo":"This is a long message"}`, map[string]string{ "X-Message": "${foo}", @@ -2783,6 +2783,30 @@ func TestServer_MessageTemplate_ExceedMessageSize_TemplatedMessageTooLong(t *tes require.Equal(t, 40041, toHTTPError(t, response.Body.String()).Code) } +func TestServer_MessageTemplate_Grafana(t *testing.T) { + c := newTestConfig(t) + s := newTestServer(t, c) + body := `{"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\nAnnotations:\n - summary = 15m load average too high\nSource: localhost:3000/alerting/grafana/NW9oDw-4z/view\nSilence: 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\n"}` + response := request(t, s, "PUT", "/mytopic?tpl=yes&title=Grafana+alert:+${title}&message=${message}", body, nil) + + require.Equal(t, 200, response.Code) + m := toMessage(t, response.Body.String()) + require.Equal(t, "Grafana alert: [RESOLVED] Load avg 15m too high Node alerts (10.108.0.2:9100 node-exporter)", m.Title) + require.Equal(t, `**Resolved** + +Value: B=18.98211314475876, C=0 +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 +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) +} + func newTestConfig(t *testing.T) *Config { conf := NewConfig() conf.BaseURL = "http://127.0.0.1:12345" diff --git a/server/server_webpush.go b/server/server_webpush.go index cd41759d..cf4929a9 100644 --- a/server/server_webpush.go +++ b/server/server_webpush.go @@ -38,7 +38,7 @@ func init() { } func (s *Server) handleWebPushUpdate(w http.ResponseWriter, r *http.Request, v *visitor) error { - req, err := readJSONWithLimit[apiWebPushUpdateSubscriptionRequest](r.Body, jsonBodyBytesLimit, false) + req, err := readJSONWithLimit[apiWebPushUpdateSubscriptionRequest](r.Body, httpBodyBytesLimit, false) if err != nil || req.Endpoint == "" || req.P256dh == "" || req.Auth == "" { return errHTTPBadRequestWebPushSubscriptionInvalid } else if !webPushAllowedEndpointsRegex.MatchString(req.Endpoint) { @@ -66,7 +66,7 @@ func (s *Server) handleWebPushUpdate(w http.ResponseWriter, r *http.Request, v * } func (s *Server) handleWebPushDelete(w http.ResponseWriter, r *http.Request, _ *visitor) error { - req, err := readJSONWithLimit[apiWebPushUpdateSubscriptionRequest](r.Body, jsonBodyBytesLimit, false) + req, err := readJSONWithLimit[apiWebPushUpdateSubscriptionRequest](r.Body, httpBodyBytesLimit, false) if err != nil || req.Endpoint == "" { return errHTTPBadRequestWebPushSubscriptionInvalid } diff --git a/server/util.go b/server/util.go index fe5b3ea3..bcfe3037 100644 --- a/server/util.go +++ b/server/util.go @@ -2,6 +2,7 @@ package server import ( "context" + "errors" "fmt" "heckel.io/ntfy/v2/util" "io" @@ -104,9 +105,9 @@ func extractIPAddress(r *http.Request, behindProxy bool) netip.Addr { func readJSONWithLimit[T any](r io.ReadCloser, limit int, allowEmpty bool) (*T, error) { obj, err := util.UnmarshalJSONWithLimit[T](r, limit, allowEmpty) - if err == util.ErrUnmarshalJSON { + if errors.Is(err, util.ErrUnmarshalJSON) { return nil, errHTTPBadRequestJSONInvalid - } else if err == util.ErrTooLargeJSON { + } else if errors.Is(err, util.ErrTooLargeJSON) { return nil, errHTTPEntityTooLargeJSONBody } else if err != nil { return nil, err diff --git a/test/server.go b/test/server.go index 9d75a2c7..5398cf9e 100644 --- a/test/server.go +++ b/test/server.go @@ -16,7 +16,7 @@ func StartServer(t *testing.T) (*server.Server, int) { // StartServerWithConfig starts a server.Server with a random port and waits for the server to be up func StartServerWithConfig(t *testing.T, conf *server.Config) (*server.Server, int) { - port := 10000 + rand.Intn(20000) + port := 10000 + rand.Intn(30000) conf.ListenHTTP = fmt.Sprintf(":%d", port) conf.AttachmentCacheDir = t.TempDir() conf.CacheFile = filepath.Join(t.TempDir(), "cache.db") diff --git a/util/peek.go b/util/peek.go index 40150cbc..03d2e20a 100644 --- a/util/peek.go +++ b/util/peek.go @@ -2,6 +2,7 @@ package util import ( "bytes" + "errors" "io" "strings" ) @@ -26,7 +27,7 @@ func Peek(underlying io.ReadCloser, limit int) (*PeekedReadCloser, error) { } peeked := make([]byte, limit) read, err := io.ReadFull(underlying, peeked) - if err != nil && err != io.ErrUnexpectedEOF && err != io.EOF { + if err != nil && !errors.Is(err, io.ErrUnexpectedEOF) && err != io.EOF { return nil, err } return &PeekedReadCloser{ @@ -44,7 +45,7 @@ func (r *PeekedReadCloser) Read(p []byte) (n int, err error) { return 0, io.EOF } n, err = r.peeked.Read(p) - if err == io.EOF { + if errors.Is(err, io.EOF) { return r.underlying.Read(p) } else if err != nil { return 0, err From 9247dac50db7d5a4617079755fde479b50907f80 Mon Sep 17 00:00:00 2001 From: binwiederhier Date: Wed, 20 Mar 2024 21:39:17 -0400 Subject: [PATCH 08/14] Move things, revert naming --- server/server.go | 26 +++++++++++++------------- server/server_account.go | 24 ++++++++++++------------ server/server_admin.go | 8 ++++---- server/server_payments.go | 6 +++--- server/server_webpush.go | 4 ++-- 5 files changed, 34 insertions(+), 34 deletions(-) diff --git a/server/server.go b/server/server.go index 1c1950d1..975e2dac 100644 --- a/server/server.go +++ b/server/server.go @@ -131,7 +131,7 @@ 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 - httpBodyBytesLimit = 32768 // Max number of bytes for a request bodys (unless MessageLimit is higher) + jsonBodyBytesLimit = 32768 // 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 @@ -1047,7 +1047,7 @@ func (s *Server) parsePublishParams(r *http.Request, m *message) (cache bool, fi // 6. curl -T file.txt ntfy.sh/mytopic // If file.txt is <= 4096 (message limit) and valid UTF-8, treat it as a message // 7. curl -T file.txt ntfy.sh/mytopic -// If file.txt is > message limit or template && file.txt > message limit*2, treat it as an attachment +// 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 { if m.Event == pollRequestEvent { // Case 1 return s.handleBodyDiscard(body) @@ -1095,7 +1095,7 @@ func (s *Server) handleBodyAsTextMessage(m *message, body *util.PeekedReadCloser } func (s *Server) handleBodyAsTemplatedTextMessage(m *message, body *util.PeekedReadCloser) error { - body, err := util.Peek(body, httpBodyBytesLimit) + body, err := util.Peek(body, jsonBodyBytesLimit) if err != nil { return err } else if body.LimitReached { @@ -1113,6 +1113,16 @@ func (s *Server) handleBodyAsTemplatedTextMessage(m *message, body *util.PeekedR return nil } +func replaceGJSONTemplate(template string, source string) string { + matches := templateVarRegex.FindAllStringSubmatch(template, -1) + for _, m := range matches { + if result := gjson.Get(source, m[1]); result.Exists() { + template = strings.ReplaceAll(template, fmt.Sprintf(templateVarFormat, m[1]), result.String()) + } + } + return template +} + func (s *Server) handleBodyAsAttachment(r *http.Request, v *visitor, m *message, body *util.PeekedReadCloser) error { if s.fileCache == nil || s.config.BaseURL == "" || s.config.AttachmentCacheDir == "" { return errHTTPBadRequestAttachmentsDisallowed.With(m) @@ -1163,16 +1173,6 @@ func (s *Server) handleBodyAsAttachment(r *http.Request, v *visitor, m *message, return nil } -func replaceGJSONTemplate(template string, source string) string { - matches := templateVarRegex.FindAllStringSubmatch(template, -1) - for _, m := range matches { - if result := gjson.Get(source, m[1]); result.Exists() { - template = strings.ReplaceAll(template, fmt.Sprintf(templateVarFormat, m[1]), result.String()) - } - } - return template -} - func (s *Server) handleSubscribeJSON(w http.ResponseWriter, r *http.Request, v *visitor) error { encoder := func(msg *message) (string, error) { var buf bytes.Buffer diff --git a/server/server_account.go b/server/server_account.go index e457464d..cb841d07 100644 --- a/server/server_account.go +++ b/server/server_account.go @@ -28,7 +28,7 @@ func (s *Server) handleAccountCreate(w http.ResponseWriter, r *http.Request, v * return errHTTPTooManyRequestsLimitAccountCreation } } - newAccount, err := readJSONWithLimit[apiAccountCreateRequest](r.Body, httpBodyBytesLimit, false) + newAccount, err := readJSONWithLimit[apiAccountCreateRequest](r.Body, jsonBodyBytesLimit, false) if err != nil { return err } @@ -160,7 +160,7 @@ func (s *Server) handleAccountGet(w http.ResponseWriter, r *http.Request, v *vis } func (s *Server) handleAccountDelete(w http.ResponseWriter, r *http.Request, v *visitor) error { - req, err := readJSONWithLimit[apiAccountDeleteRequest](r.Body, httpBodyBytesLimit, false) + req, err := readJSONWithLimit[apiAccountDeleteRequest](r.Body, jsonBodyBytesLimit, false) if err != nil { return err } else if req.Password == "" { @@ -192,7 +192,7 @@ func (s *Server) handleAccountDelete(w http.ResponseWriter, r *http.Request, v * } func (s *Server) handleAccountPasswordChange(w http.ResponseWriter, r *http.Request, v *visitor) error { - req, err := readJSONWithLimit[apiAccountPasswordChangeRequest](r.Body, httpBodyBytesLimit, false) + req, err := readJSONWithLimit[apiAccountPasswordChangeRequest](r.Body, jsonBodyBytesLimit, false) if err != nil { return err } else if req.Password == "" || req.NewPassword == "" { @@ -210,7 +210,7 @@ func (s *Server) handleAccountPasswordChange(w http.ResponseWriter, r *http.Requ } func (s *Server) handleAccountTokenCreate(w http.ResponseWriter, r *http.Request, v *visitor) error { - req, err := readJSONWithLimit[apiAccountTokenIssueRequest](r.Body, httpBodyBytesLimit, true) // Allow empty body! + req, err := readJSONWithLimit[apiAccountTokenIssueRequest](r.Body, jsonBodyBytesLimit, true) // Allow empty body! if err != nil { return err } @@ -246,7 +246,7 @@ func (s *Server) handleAccountTokenCreate(w http.ResponseWriter, r *http.Request func (s *Server) handleAccountTokenUpdate(w http.ResponseWriter, r *http.Request, v *visitor) error { u := v.User() - req, err := readJSONWithLimit[apiAccountTokenUpdateRequest](r.Body, httpBodyBytesLimit, true) // Allow empty body! + req, err := readJSONWithLimit[apiAccountTokenUpdateRequest](r.Body, jsonBodyBytesLimit, true) // Allow empty body! if err != nil { return err } else if req.Token == "" { @@ -302,7 +302,7 @@ func (s *Server) handleAccountTokenDelete(w http.ResponseWriter, r *http.Request } func (s *Server) handleAccountSettingsChange(w http.ResponseWriter, r *http.Request, v *visitor) error { - newPrefs, err := readJSONWithLimit[user.Prefs](r.Body, httpBodyBytesLimit, false) + newPrefs, err := readJSONWithLimit[user.Prefs](r.Body, jsonBodyBytesLimit, false) if err != nil { return err } @@ -336,7 +336,7 @@ func (s *Server) handleAccountSettingsChange(w http.ResponseWriter, r *http.Requ } func (s *Server) handleAccountSubscriptionAdd(w http.ResponseWriter, r *http.Request, v *visitor) error { - newSubscription, err := readJSONWithLimit[user.Subscription](r.Body, httpBodyBytesLimit, false) + newSubscription, err := readJSONWithLimit[user.Subscription](r.Body, jsonBodyBytesLimit, false) if err != nil { return err } @@ -359,7 +359,7 @@ func (s *Server) handleAccountSubscriptionAdd(w http.ResponseWriter, r *http.Req } func (s *Server) handleAccountSubscriptionChange(w http.ResponseWriter, r *http.Request, v *visitor) error { - updatedSubscription, err := readJSONWithLimit[user.Subscription](r.Body, httpBodyBytesLimit, false) + updatedSubscription, err := readJSONWithLimit[user.Subscription](r.Body, jsonBodyBytesLimit, false) if err != nil { return err } @@ -417,7 +417,7 @@ func (s *Server) handleAccountSubscriptionDelete(w http.ResponseWriter, r *http. // it is already reserved by someone else. func (s *Server) handleAccountReservationAdd(w http.ResponseWriter, r *http.Request, v *visitor) error { u := v.User() - req, err := readJSONWithLimit[apiAccountReservationRequest](r.Body, httpBodyBytesLimit, false) + req, err := readJSONWithLimit[apiAccountReservationRequest](r.Body, jsonBodyBytesLimit, false) if err != nil { return err } @@ -532,7 +532,7 @@ func (s *Server) maybeRemoveMessagesAndExcessReservations(r *http.Request, v *vi func (s *Server) handleAccountPhoneNumberVerify(w http.ResponseWriter, r *http.Request, v *visitor) error { u := v.User() - req, err := readJSONWithLimit[apiAccountPhoneNumberVerifyRequest](r.Body, httpBodyBytesLimit, false) + req, err := readJSONWithLimit[apiAccountPhoneNumberVerifyRequest](r.Body, jsonBodyBytesLimit, false) if err != nil { return err } else if !phoneNumberRegex.MatchString(req.Number) { @@ -563,7 +563,7 @@ func (s *Server) handleAccountPhoneNumberVerify(w http.ResponseWriter, r *http.R func (s *Server) handleAccountPhoneNumberAdd(w http.ResponseWriter, r *http.Request, v *visitor) error { u := v.User() - req, err := readJSONWithLimit[apiAccountPhoneNumberAddRequest](r.Body, httpBodyBytesLimit, false) + req, err := readJSONWithLimit[apiAccountPhoneNumberAddRequest](r.Body, jsonBodyBytesLimit, false) if err != nil { return err } @@ -582,7 +582,7 @@ func (s *Server) handleAccountPhoneNumberAdd(w http.ResponseWriter, r *http.Requ func (s *Server) handleAccountPhoneNumberDelete(w http.ResponseWriter, r *http.Request, v *visitor) error { u := v.User() - req, err := readJSONWithLimit[apiAccountPhoneNumberAddRequest](r.Body, httpBodyBytesLimit, false) + req, err := readJSONWithLimit[apiAccountPhoneNumberAddRequest](r.Body, jsonBodyBytesLimit, false) if err != nil { return err } diff --git a/server/server_admin.go b/server/server_admin.go index ec0b69b6..ac295718 100644 --- a/server/server_admin.go +++ b/server/server_admin.go @@ -39,7 +39,7 @@ func (s *Server) handleUsersGet(w http.ResponseWriter, r *http.Request, v *visit } func (s *Server) handleUsersAdd(w http.ResponseWriter, r *http.Request, v *visitor) error { - req, err := readJSONWithLimit[apiUserAddRequest](r.Body, httpBodyBytesLimit, false) + req, err := readJSONWithLimit[apiUserAddRequest](r.Body, jsonBodyBytesLimit, false) if err != nil { return err } else if !user.AllowedUsername(req.Username) || req.Password == "" { @@ -72,7 +72,7 @@ func (s *Server) handleUsersAdd(w http.ResponseWriter, r *http.Request, v *visit } func (s *Server) handleUsersDelete(w http.ResponseWriter, r *http.Request, v *visitor) error { - req, err := readJSONWithLimit[apiUserDeleteRequest](r.Body, httpBodyBytesLimit, false) + req, err := readJSONWithLimit[apiUserDeleteRequest](r.Body, jsonBodyBytesLimit, false) if err != nil { return err } @@ -94,7 +94,7 @@ func (s *Server) handleUsersDelete(w http.ResponseWriter, r *http.Request, v *vi } func (s *Server) handleAccessAllow(w http.ResponseWriter, r *http.Request, v *visitor) error { - req, err := readJSONWithLimit[apiAccessAllowRequest](r.Body, httpBodyBytesLimit, false) + req, err := readJSONWithLimit[apiAccessAllowRequest](r.Body, jsonBodyBytesLimit, false) if err != nil { return err } @@ -115,7 +115,7 @@ func (s *Server) handleAccessAllow(w http.ResponseWriter, r *http.Request, v *vi } func (s *Server) handleAccessReset(w http.ResponseWriter, r *http.Request, v *visitor) error { - req, err := readJSONWithLimit[apiAccessResetRequest](r.Body, httpBodyBytesLimit, false) + req, err := readJSONWithLimit[apiAccessResetRequest](r.Body, jsonBodyBytesLimit, false) if err != nil { return err } diff --git a/server/server_payments.go b/server/server_payments.go index 2fb42d31..334301bb 100644 --- a/server/server_payments.go +++ b/server/server_payments.go @@ -115,7 +115,7 @@ func (s *Server) handleAccountBillingSubscriptionCreate(w http.ResponseWriter, r if u.Billing.StripeSubscriptionID != "" { return errHTTPBadRequestBillingSubscriptionExists } - req, err := readJSONWithLimit[apiAccountBillingSubscriptionChangeRequest](r.Body, httpBodyBytesLimit, false) + req, err := readJSONWithLimit[apiAccountBillingSubscriptionChangeRequest](r.Body, jsonBodyBytesLimit, false) if err != nil { return err } @@ -245,7 +245,7 @@ func (s *Server) handleAccountBillingSubscriptionUpdate(w http.ResponseWriter, r if u.Billing.StripeSubscriptionID == "" { return errNoBillingSubscription } - req, err := readJSONWithLimit[apiAccountBillingSubscriptionChangeRequest](r.Body, httpBodyBytesLimit, false) + req, err := readJSONWithLimit[apiAccountBillingSubscriptionChangeRequest](r.Body, jsonBodyBytesLimit, false) if err != nil { return err } @@ -342,7 +342,7 @@ func (s *Server) handleAccountBillingWebhook(_ http.ResponseWriter, r *http.Requ if stripeSignature == "" { return errHTTPBadRequestBillingRequestInvalid } - body, err := util.Peek(r.Body, httpBodyBytesLimit) + body, err := util.Peek(r.Body, jsonBodyBytesLimit) if err != nil { return err } else if body.LimitReached { diff --git a/server/server_webpush.go b/server/server_webpush.go index cf4929a9..cd41759d 100644 --- a/server/server_webpush.go +++ b/server/server_webpush.go @@ -38,7 +38,7 @@ func init() { } func (s *Server) handleWebPushUpdate(w http.ResponseWriter, r *http.Request, v *visitor) error { - req, err := readJSONWithLimit[apiWebPushUpdateSubscriptionRequest](r.Body, httpBodyBytesLimit, false) + req, err := readJSONWithLimit[apiWebPushUpdateSubscriptionRequest](r.Body, jsonBodyBytesLimit, false) if err != nil || req.Endpoint == "" || req.P256dh == "" || req.Auth == "" { return errHTTPBadRequestWebPushSubscriptionInvalid } else if !webPushAllowedEndpointsRegex.MatchString(req.Endpoint) { @@ -66,7 +66,7 @@ func (s *Server) handleWebPushUpdate(w http.ResponseWriter, r *http.Request, v * } func (s *Server) handleWebPushDelete(w http.ResponseWriter, r *http.Request, _ *visitor) error { - req, err := readJSONWithLimit[apiWebPushUpdateSubscriptionRequest](r.Body, httpBodyBytesLimit, false) + req, err := readJSONWithLimit[apiWebPushUpdateSubscriptionRequest](r.Body, jsonBodyBytesLimit, false) if err != nil || req.Endpoint == "" { return errHTTPBadRequestWebPushSubscriptionInvalid } From a04f2f9c9ac35320810198ced538ae365b5df26d Mon Sep 17 00:00:00 2001 From: binwiederhier Date: Fri, 22 Mar 2024 20:45:16 -0400 Subject: [PATCH 09/14] Bla --- docs/publish.md | 59 +++++++++++++++++++++++-------------------- server/server.go | 39 ++++++++++++++++++---------- server/server_test.go | 29 +++++++++++++++++++++ 3 files changed, 85 insertions(+), 42 deletions(-) diff --git a/docs/publish.md b/docs/publish.md index 672f2fa3..08bb7f74 100644 --- a/docs/publish.md +++ b/docs/publish.md @@ -938,6 +938,37 @@ Here's an example with a custom message, tags and a priority: file_get_contents('https://ntfy.sh/mywebhook/publish?message=Webhook+triggered&priority=high&tags=warning,skull'); ``` + +## JSON templating +Some services let you specify a webhook URL but do not let you modify the webhook body (e.g. GitHub, Grafana). Instead of using a separate +bridge program to parse the webhook body into the format ntfy expects, you can include a templated message and/or a templated title +which will be populated based on the fields of the webhook body (so long as the webhook body is valid JSON). + +Enable templating by setting the `X-Template` header (or its aliases `Template` or `tpl`) to `yes`, or (more appropriately for webhooks) +by setting the `?template=yes` query parameter. Then, include templates in your message and/or title by including paths to the +appropriate JSON fields surrounded by `${` and `}`, e.g. `${alert.title}` or `${error.desc}`, depending on your JSON payload. + +Please refer to the [GJSON docs](https://github.com/tidwall/gjson/blob/master/SYNTAX.md) for supported JSON path syntax, as well as +[gjson.dev](https://gjson.dev/) to test your templates. + +=== "HTTP" + ``` http + POST /mytopic HTTP/1.1 + Host: ntfy.sh + X-Message: Error message: ${error.desc} + X-Title: ${hostname}: A ${error.level} error has occurred + X-Template: yes + + {"hostname": "philipp-pc", "error": {"level": "severe", "desc": "Disk has run out of space"}} + ``` + +The example above would send a notification with a title "philipp-pc: A severe error has occurred" and a message "Error message: Disk has run out of space". + +For Grafana webhooks, you might find it helpful to use the headers `X-Title: Grafana alert: ${title}` and `X-Message: ${message}`. +Alternatively, you can include the params in the webhook URL. For example, by +appending `?template=yes&title=Grafana alert: ${title}&message=${message}` to the URL. + + ## Publish as JSON _Supported on:_ :material-android: :material-apple: :material-firefox: @@ -3557,34 +3588,6 @@ ntfy server plays the role of the Push Gateway, as well as the Push Provider. Un !!! info This is not a generic Matrix Push Gateway. It only works in combination with UnifiedPush and ntfy. -### Message and Title Templates -Some services let you specify a webhook URL but do not let you modify the webhook body (e.g., Grafana). Instead of using a separate -bridge program to parse the webhook body into the format ntfy expects, you can include a templated message and/or a templated title -which will be populated based on the fields of the webhook body (so long as the webhook body is valid JSON). - -Enable templating by setting the `X-Template` header (or its aliases `Template` or `tpl`) to "yes". Then, include templates -in your message and/or title (no other fields can be filled with a template at this time) by including paths to the -appropriate JSON fields surrounded by `${` and `}`. See an example below. -See [GJSON docs](https://github.com/tidwall/gjson/blob/master/SYNTAX.md) for supported JSON path syntax. -[https://gjson.dev/](https://gjson.dev/) is a great resource for testing your templates. - -=== "HTTP" - ``` http - POST /mytopic HTTP/1.1 - Host: ntfy.sh - X-Message: Error message: ${error.desc} - X-Title: ${hostname}: A ${error.level} error has occurred - X-Template: yes - - {"hostname": "philipp-pc", "error": {"level": "severe", "desc": "Disk has run out of space"}} - ``` - -The example above would send a notification with a title "philipp-pc: A severe error has occurred" and a message "Error message: Disk has run out of space". - -For Grafana webhooks, you might find it helpful to use the headers `X-Title: Grafana alert: ${title}` and `X-Message: ${message}`. -Alternatively, you can include the params in the webhook URL. For example, by -appending `?template=yes&title=Grafana alert: ${title}&message=${message}` to the URL. - ## Public topics Obviously all topics on ntfy.sh are public, but there are a few designated topics that are used in examples, and topics that you can use to try out what [authentication and access control](#authentication) looks like. diff --git a/server/server.go b/server/server.go index 975e2dac..21269125 100644 --- a/server/server.go +++ b/server/server.go @@ -23,13 +23,13 @@ import ( "strconv" "strings" "sync" + "text/template" "time" "unicode/utf8" "github.com/emersion/go-smtp" "github.com/gorilla/websocket" "github.com/prometheus/client_golang/prometheus/promhttp" - "github.com/tidwall/gjson" "golang.org/x/sync/errgroup" "heckel.io/ntfy/v2/log" "heckel.io/ntfy/v2/user" @@ -1095,32 +1095,43 @@ func (s *Server) handleBodyAsTextMessage(m *message, body *util.PeekedReadCloser } func (s *Server) handleBodyAsTemplatedTextMessage(m *message, body *util.PeekedReadCloser) error { - body, err := util.Peek(body, jsonBodyBytesLimit) + body, err := util.Peek(body, max(s.config.MessageSizeLimit, jsonBodyBytesLimit)) if err != nil { return err } else if body.LimitReached { return errHTTPEntityTooLargeJSONBody } peekedBody := strings.TrimSpace(string(body.PeekedBytes)) - if !gjson.Valid(peekedBody) { - return errHTTPBadRequestTemplatedMessageNotJSON - } - m.Message = replaceGJSONTemplate(m.Message, peekedBody) - m.Title = replaceGJSONTemplate(m.Title, peekedBody) + m.Message = replaceTemplate(m.Message, peekedBody) + m.Title = replaceTemplate(m.Title, peekedBody) if len(m.Message) > s.config.MessageSizeLimit { return errHTTPBadRequestTemplatedMessageTooLarge } return nil } -func replaceGJSONTemplate(template string, source string) string { - matches := templateVarRegex.FindAllStringSubmatch(template, -1) - for _, m := range matches { - if result := gjson.Get(source, m[1]); result.Exists() { - template = strings.ReplaceAll(template, fmt.Sprintf(templateVarFormat, m[1]), result.String()) - } +func replaceTemplate(tpl string, source string) string { + rendered, err := replaceTemplateInternal(tpl, source) + if err != nil { + return "" } - return template + return rendered +} + +func replaceTemplateInternal(tpl string, source string) (string, error) { + var data any + if err := json.Unmarshal([]byte(source), &data); err != nil { + return "", err + } + t, err := template.New("").Parse(tpl) + if err != nil { + return "", err + } + var buf bytes.Buffer + if err := t.Execute(&buf, data); err != nil { + return "", err + } + return buf.String(), nil } func (s *Server) handleBodyAsAttachment(r *http.Request, v *visitor, m *message, body *util.PeekedReadCloser) error { diff --git a/server/server_test.go b/server/server_test.go index 66c54fd0..6c76ff2f 100644 --- a/server/server_test.go +++ b/server/server_test.go @@ -2494,6 +2494,7 @@ func TestServer_MessageHistoryAndStatsEndpoint(t *testing.T) { } func TestServer_MessageHistoryMaxSize(t *testing.T) { + t.Parallel() s := newTestServer(t, newTestConfig(t)) for i := 0; i < 20; i++ { s.messages = int64(i) @@ -2503,6 +2504,7 @@ func TestServer_MessageHistoryMaxSize(t *testing.T) { } func TestServer_MessageCountPersistence(t *testing.T) { + t.Parallel() c := newTestConfig(t) s := newTestServer(t, c) s.messages = 1234 @@ -2518,6 +2520,7 @@ func TestServer_MessageCountPersistence(t *testing.T) { } func TestServer_PublishWithUTF8MimeHeader(t *testing.T) { + t.Parallel() s := newTestServer(t, newTestConfig(t)) response := request(t, s, "POST", "/mytopic", "some attachment", map[string]string{ @@ -2543,6 +2546,7 @@ func TestServer_PublishWithUTF8MimeHeader(t *testing.T) { } func TestServer_UpstreamBaseURL_Success(t *testing.T) { + t.Parallel() var pollID atomic.Pointer[string] upstreamServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { body, err := io.ReadAll(r.Body) @@ -2572,6 +2576,7 @@ func TestServer_UpstreamBaseURL_Success(t *testing.T) { } func TestServer_UpstreamBaseURL_With_Access_Token_Success(t *testing.T) { + t.Parallel() var pollID atomic.Pointer[string] upstreamServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { body, err := io.ReadAll(r.Body) @@ -2603,6 +2608,7 @@ func TestServer_UpstreamBaseURL_With_Access_Token_Success(t *testing.T) { } func TestServer_UpstreamBaseURL_DoNotForwardUnifiedPush(t *testing.T) { + t.Parallel() upstreamServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { t.Fatal("UnifiedPush messages should not be forwarded") })) @@ -2626,6 +2632,7 @@ func TestServer_UpstreamBaseURL_DoNotForwardUnifiedPush(t *testing.T) { } func TestServer_MessageTemplate(t *testing.T) { + t.Parallel() s := newTestServer(t, newTestConfig(t)) response := request(t, s, "PUT", "/mytopic", `{"foo":"bar", "nested":{"title":"here"}}`, map[string]string{ "X-Message": "${foo}", @@ -2640,6 +2647,7 @@ func TestServer_MessageTemplate(t *testing.T) { } func TestServer_MessageTemplate_RepeatPlaceholder(t *testing.T) { + t.Parallel() s := newTestServer(t, newTestConfig(t)) response := request(t, s, "PUT", "/mytopic", `{"foo":"bar", "nested":{"title":"here"}}`, map[string]string{ "Message": "${foo} is ${foo}", @@ -2654,6 +2662,7 @@ func TestServer_MessageTemplate_RepeatPlaceholder(t *testing.T) { } func TestServer_MessageTemplate_JSONBody(t *testing.T) { + t.Parallel() s := newTestServer(t, newTestConfig(t)) body := `{"topic": "mytopic", "message": "{\"foo\":\"bar\",\"nested\":{\"title\":\"here\"}}"}` response := request(t, s, "PUT", "/", body, map[string]string{ @@ -2698,6 +2707,7 @@ func TestServer_MessageTemplate_PlaceholderTypo(t *testing.T) { } func TestServer_MessageTemplate_MultiplePlaceholders(t *testing.T) { + t.Parallel() s := newTestServer(t, newTestConfig(t)) response := request(t, s, "PUT", "/mytopic", `{"foo":"bar", "nested":{"title":"here"}}`, map[string]string{ "X-Message": "${foo} is ${nested.title}", @@ -2710,6 +2720,7 @@ func TestServer_MessageTemplate_MultiplePlaceholders(t *testing.T) { } func TestServer_MessageTemplate_NestedPlaceholders(t *testing.T) { + t.Parallel() // not intended to work recursively for now // i.e., ${${nested.bar}} should NOT evaluate to ${foo} and then to "bar" s := newTestServer(t, newTestConfig(t)) @@ -2724,6 +2735,7 @@ func TestServer_MessageTemplate_NestedPlaceholders(t *testing.T) { } func TestServer_MessageTemplate_NestedPlaceholdersFunky(t *testing.T) { + t.Parallel() // The above example can technically work // ${${nested.bar}} would be interpreted as a nested GJSON path with key "${nested" then key "bar" // so you would probably expect the output to be "works!", BUT the second } in the placeholder is not @@ -2740,6 +2752,7 @@ func TestServer_MessageTemplate_NestedPlaceholdersFunky(t *testing.T) { } func TestServer_MessageTemplate_FancyGJSON(t *testing.T) { + t.Parallel() s := newTestServer(t, newTestConfig(t)) jsonBody := `{"foo": "bar", "errors": [{"level": "severe", "url": "https://severe1.com"},{"level": "warning", "url": "https://warning.com"},{"level": "severe", "url": "https://severe2.com"}]}` response := request(t, s, "PUT", "/mytopic", jsonBody, map[string]string{ @@ -2755,6 +2768,7 @@ func TestServer_MessageTemplate_FancyGJSON(t *testing.T) { } func TestServer_MessageTemplate_ExceedMessageSize_TemplatedMessageOK(t *testing.T) { + t.Parallel() c := newTestConfig(t) c.MessageSizeLimit = 25 // 25 < len(HTTP body) < 32k, and len(m.Message) < 25 s := newTestServer(t, c) @@ -2771,6 +2785,7 @@ func TestServer_MessageTemplate_ExceedMessageSize_TemplatedMessageOK(t *testing. } func TestServer_MessageTemplate_ExceedMessageSize_TemplatedMessageTooLong(t *testing.T) { + t.Parallel() c := newTestConfig(t) c.MessageSizeLimit = 21 // 21 < len(HTTP body) < 32k, but !len(m.Message) < 21 s := newTestServer(t, c) @@ -2784,6 +2799,7 @@ func TestServer_MessageTemplate_ExceedMessageSize_TemplatedMessageTooLong(t *tes } func TestServer_MessageTemplate_Grafana(t *testing.T) { + t.Parallel() c := newTestConfig(t) s := newTestServer(t, c) body := `{"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\nAnnotations:\n - summary = 15m load average too high\nSource: localhost:3000/alerting/grafana/NW9oDw-4z/view\nSilence: 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\n"}` @@ -2807,6 +2823,19 @@ Silence: localhost:3000/alerting/silence/new?alertmanager=grafana&matcher=alertn `, m.Message) } +func TestServer_MessageTemplate_GitHub(t *testing.T) { + t.Parallel() + c := newTestConfig(t) + s := newTestServer(t, c) + body := `{"action":"opened","number":1,"pull_request":{"url":"https://api.github.com/repos/binwiederhier/dabble/pulls/1","id":1783420972,"node_id":"PR_kwDOHAbdo85qTNgs","html_url":"https://github.com/binwiederhier/dabble/pull/1","diff_url":"https://github.com/binwiederhier/dabble/pull/1.diff","patch_url":"https://github.com/binwiederhier/dabble/pull/1.patch","issue_url":"https://api.github.com/repos/binwiederhier/dabble/issues/1","number":1,"state":"open","locked":false,"title":"A sample PR from Phil","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","site_admin":false},"body":null,"created_at":"2024-03-21T02:52:09Z","updated_at":"2024-03-21T02:52:09Z","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/dabble/pulls/1/commits","review_comments_url":"https://api.github.com/repos/binwiederhier/dabble/pulls/1/comments","review_comment_url":"https://api.github.com/repos/binwiederhier/dabble/pulls/comments{/number}","comments_url":"https://api.github.com/repos/binwiederhier/dabble/issues/1/comments","statuses_url":"https://api.github.com/repos/binwiederhier/dabble/statuses/5703842cc5715ed1e358d23ebb693db09747ae9b","head":{"label":"binwiederhier:aa","ref":"aa","sha":"5703842cc5715ed1e358d23ebb693db09747ae9b","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","site_admin":false},"repo":{"id":470212003,"node_id":"R_kgDOHAbdow","name":"dabble","full_name":"binwiederhier/dabble","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","site_admin":false},"html_url":"https://github.com/binwiederhier/dabble","description":"A repo for dabbling","fork":false,"url":"https://api.github.com/repos/binwiederhier/dabble","forks_url":"https://api.github.com/repos/binwiederhier/dabble/forks","keys_url":"https://api.github.com/repos/binwiederhier/dabble/keys{/key_id}","collaborators_url":"https://api.github.com/repos/binwiederhier/dabble/collaborators{/collaborator}","teams_url":"https://api.github.com/repos/binwiederhier/dabble/teams","hooks_url":"https://api.github.com/repos/binwiederhier/dabble/hooks","issue_events_url":"https://api.github.com/repos/binwiederhier/dabble/issues/events{/number}","events_url":"https://api.github.com/repos/binwiederhier/dabble/events","assignees_url":"https://api.github.com/repos/binwiederhier/dabble/assignees{/user}","branches_url":"https://api.github.com/repos/binwiederhier/dabble/branches{/branch}","tags_url":"https://api.github.com/repos/binwiederhier/dabble/tags","blobs_url":"https://api.github.com/repos/binwiederhier/dabble/git/blobs{/sha}","git_tags_url":"https://api.github.com/repos/binwiederhier/dabble/git/tags{/sha}","git_refs_url":"https://api.github.com/repos/binwiederhier/dabble/git/refs{/sha}","trees_url":"https://api.github.com/repos/binwiederhier/dabble/git/trees{/sha}","statuses_url":"https://api.github.com/repos/binwiederhier/dabble/statuses/{sha}","languages_url":"https://api.github.com/repos/binwiederhier/dabble/languages","stargazers_url":"https://api.github.com/repos/binwiederhier/dabble/stargazers","contributors_url":"https://api.github.com/repos/binwiederhier/dabble/contributors","subscribers_url":"https://api.github.com/repos/binwiederhier/dabble/subscribers","subscription_url":"https://api.github.com/repos/binwiederhier/dabble/subscription","commits_url":"https://api.github.com/repos/binwiederhier/dabble/commits{/sha}","git_commits_url":"https://api.github.com/repos/binwiederhier/dabble/git/commits{/sha}","comments_url":"https://api.github.com/repos/binwiederhier/dabble/comments{/number}","issue_comment_url":"https://api.github.com/repos/binwiederhier/dabble/issues/comments{/number}","contents_url":"https://api.github.com/repos/binwiederhier/dabble/contents/{+path}","compare_url":"https://api.github.com/repos/binwiederhier/dabble/compare/{base}...{head}","merges_url":"https://api.github.com/repos/binwiederhier/dabble/merges","archive_url":"https://api.github.com/repos/binwiederhier/dabble/{archive_format}{/ref}","downloads_url":"https://api.github.com/repos/binwiederhier/dabble/downloads","issues_url":"https://api.github.com/repos/binwiederhier/dabble/issues{/number}","pulls_url":"https://api.github.com/repos/binwiederhier/dabble/pulls{/number}","milestones_url":"https://api.github.com/repos/binwiederhier/dabble/milestones{/number}","notifications_url":"https://api.github.com/repos/binwiederhier/dabble/notifications{?since,all,participating}","labels_url":"https://api.github.com/repos/binwiederhier/dabble/labels{/name}","releases_url":"https://api.github.com/repos/binwiederhier/dabble/releases{/id}","deployments_url":"https://api.github.com/repos/binwiederhier/dabble/deployments","created_at":"2022-03-15T15:06:17Z","updated_at":"2022-03-15T15:06:17Z","pushed_at":"2024-03-21T02:52:10Z","git_url":"git://github.com/binwiederhier/dabble.git","ssh_url":"git@github.com:binwiederhier/dabble.git","clone_url":"https://github.com/binwiederhier/dabble.git","svn_url":"https://github.com/binwiederhier/dabble","homepage":null,"size":1,"stargazers_count":0,"watchers_count":0,"language":null,"has_issues":true,"has_projects":true,"has_downloads":true,"has_wiki":true,"has_pages":false,"has_discussions":false,"forks_count":0,"mirror_url":null,"archived":false,"disabled":false,"open_issues_count":1,"license":null,"allow_forking":true,"is_template":false,"web_commit_signoff_required":false,"topics":[],"visibility":"public","forks":0,"open_issues":1,"watchers":0,"default_branch":"main","allow_squash_merge":true,"allow_merge_commit":true,"allow_rebase_merge":true,"allow_auto_merge":false,"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":"72d931a20bb83d123ab45accaf761150c8b01211","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","site_admin":false},"repo":{"id":470212003,"node_id":"R_kgDOHAbdow","name":"dabble","full_name":"binwiederhier/dabble","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","site_admin":false},"html_url":"https://github.com/binwiederhier/dabble","description":"A repo for dabbling","fork":false,"url":"https://api.github.com/repos/binwiederhier/dabble","forks_url":"https://api.github.com/repos/binwiederhier/dabble/forks","keys_url":"https://api.github.com/repos/binwiederhier/dabble/keys{/key_id}","collaborators_url":"https://api.github.com/repos/binwiederhier/dabble/collaborators{/collaborator}","teams_url":"https://api.github.com/repos/binwiederhier/dabble/teams","hooks_url":"https://api.github.com/repos/binwiederhier/dabble/hooks","issue_events_url":"https://api.github.com/repos/binwiederhier/dabble/issues/events{/number}","events_url":"https://api.github.com/repos/binwiederhier/dabble/events","assignees_url":"https://api.github.com/repos/binwiederhier/dabble/assignees{/user}","branches_url":"https://api.github.com/repos/binwiederhier/dabble/branches{/branch}","tags_url":"https://api.github.com/repos/binwiederhier/dabble/tags","blobs_url":"https://api.github.com/repos/binwiederhier/dabble/git/blobs{/sha}","git_tags_url":"https://api.github.com/repos/binwiederhier/dabble/git/tags{/sha}","git_refs_url":"https://api.github.com/repos/binwiederhier/dabble/git/refs{/sha}","trees_url":"https://api.github.com/repos/binwiederhier/dabble/git/trees{/sha}","statuses_url":"https://api.github.com/repos/binwiederhier/dabble/statuses/{sha}","languages_url":"https://api.github.com/repos/binwiederhier/dabble/languages","stargazers_url":"https://api.github.com/repos/binwiederhier/dabble/stargazers","contributors_url":"https://api.github.com/repos/binwiederhier/dabble/contributors","subscribers_url":"https://api.github.com/repos/binwiederhier/dabble/subscribers","subscription_url":"https://api.github.com/repos/binwiederhier/dabble/subscription","commits_url":"https://api.github.com/repos/binwiederhier/dabble/commits{/sha}","git_commits_url":"https://api.github.com/repos/binwiederhier/dabble/git/commits{/sha}","comments_url":"https://api.github.com/repos/binwiederhier/dabble/comments{/number}","issue_comment_url":"https://api.github.com/repos/binwiederhier/dabble/issues/comments{/number}","contents_url":"https://api.github.com/repos/binwiederhier/dabble/contents/{+path}","compare_url":"https://api.github.com/repos/binwiederhier/dabble/compare/{base}...{head}","merges_url":"https://api.github.com/repos/binwiederhier/dabble/merges","archive_url":"https://api.github.com/repos/binwiederhier/dabble/{archive_format}{/ref}","downloads_url":"https://api.github.com/repos/binwiederhier/dabble/downloads","issues_url":"https://api.github.com/repos/binwiederhier/dabble/issues{/number}","pulls_url":"https://api.github.com/repos/binwiederhier/dabble/pulls{/number}","milestones_url":"https://api.github.com/repos/binwiederhier/dabble/milestones{/number}","notifications_url":"https://api.github.com/repos/binwiederhier/dabble/notifications{?since,all,participating}","labels_url":"https://api.github.com/repos/binwiederhier/dabble/labels{/name}","releases_url":"https://api.github.com/repos/binwiederhier/dabble/releases{/id}","deployments_url":"https://api.github.com/repos/binwiederhier/dabble/deployments","created_at":"2022-03-15T15:06:17Z","updated_at":"2022-03-15T15:06:17Z","pushed_at":"2024-03-21T02:52:10Z","git_url":"git://github.com/binwiederhier/dabble.git","ssh_url":"git@github.com:binwiederhier/dabble.git","clone_url":"https://github.com/binwiederhier/dabble.git","svn_url":"https://github.com/binwiederhier/dabble","homepage":null,"size":1,"stargazers_count":0,"watchers_count":0,"language":null,"has_issues":true,"has_projects":true,"has_downloads":true,"has_wiki":true,"has_pages":false,"has_discussions":false,"forks_count":0,"mirror_url":null,"archived":false,"disabled":false,"open_issues_count":1,"license":null,"allow_forking":true,"is_template":false,"web_commit_signoff_required":false,"topics":[],"visibility":"public","forks":0,"open_issues":1,"watchers":0,"default_branch":"main","allow_squash_merge":true,"allow_merge_commit":true,"allow_rebase_merge":true,"allow_auto_merge":false,"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/dabble/pulls/1"},"html":{"href":"https://github.com/binwiederhier/dabble/pull/1"},"issue":{"href":"https://api.github.com/repos/binwiederhier/dabble/issues/1"},"comments":{"href":"https://api.github.com/repos/binwiederhier/dabble/issues/1/comments"},"review_comments":{"href":"https://api.github.com/repos/binwiederhier/dabble/pulls/1/comments"},"review_comment":{"href":"https://api.github.com/repos/binwiederhier/dabble/pulls/comments{/number}"},"commits":{"href":"https://api.github.com/repos/binwiederhier/dabble/pulls/1/commits"},"statuses":{"href":"https://api.github.com/repos/binwiederhier/dabble/statuses/5703842cc5715ed1e358d23ebb693db09747ae9b"}},"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":1,"additions":1,"deletions":1,"changed_files":1},"repository":{"id":470212003,"node_id":"R_kgDOHAbdow","name":"dabble","full_name":"binwiederhier/dabble","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","site_admin":false},"html_url":"https://github.com/binwiederhier/dabble","description":"A repo for dabbling","fork":false,"url":"https://api.github.com/repos/binwiederhier/dabble","forks_url":"https://api.github.com/repos/binwiederhier/dabble/forks","keys_url":"https://api.github.com/repos/binwiederhier/dabble/keys{/key_id}","collaborators_url":"https://api.github.com/repos/binwiederhier/dabble/collaborators{/collaborator}","teams_url":"https://api.github.com/repos/binwiederhier/dabble/teams","hooks_url":"https://api.github.com/repos/binwiederhier/dabble/hooks","issue_events_url":"https://api.github.com/repos/binwiederhier/dabble/issues/events{/number}","events_url":"https://api.github.com/repos/binwiederhier/dabble/events","assignees_url":"https://api.github.com/repos/binwiederhier/dabble/assignees{/user}","branches_url":"https://api.github.com/repos/binwiederhier/dabble/branches{/branch}","tags_url":"https://api.github.com/repos/binwiederhier/dabble/tags","blobs_url":"https://api.github.com/repos/binwiederhier/dabble/git/blobs{/sha}","git_tags_url":"https://api.github.com/repos/binwiederhier/dabble/git/tags{/sha}","git_refs_url":"https://api.github.com/repos/binwiederhier/dabble/git/refs{/sha}","trees_url":"https://api.github.com/repos/binwiederhier/dabble/git/trees{/sha}","statuses_url":"https://api.github.com/repos/binwiederhier/dabble/statuses/{sha}","languages_url":"https://api.github.com/repos/binwiederhier/dabble/languages","stargazers_url":"https://api.github.com/repos/binwiederhier/dabble/stargazers","contributors_url":"https://api.github.com/repos/binwiederhier/dabble/contributors","subscribers_url":"https://api.github.com/repos/binwiederhier/dabble/subscribers","subscription_url":"https://api.github.com/repos/binwiederhier/dabble/subscription","commits_url":"https://api.github.com/repos/binwiederhier/dabble/commits{/sha}","git_commits_url":"https://api.github.com/repos/binwiederhier/dabble/git/commits{/sha}","comments_url":"https://api.github.com/repos/binwiederhier/dabble/comments{/number}","issue_comment_url":"https://api.github.com/repos/binwiederhier/dabble/issues/comments{/number}","contents_url":"https://api.github.com/repos/binwiederhier/dabble/contents/{+path}","compare_url":"https://api.github.com/repos/binwiederhier/dabble/compare/{base}...{head}","merges_url":"https://api.github.com/repos/binwiederhier/dabble/merges","archive_url":"https://api.github.com/repos/binwiederhier/dabble/{archive_format}{/ref}","downloads_url":"https://api.github.com/repos/binwiederhier/dabble/downloads","issues_url":"https://api.github.com/repos/binwiederhier/dabble/issues{/number}","pulls_url":"https://api.github.com/repos/binwiederhier/dabble/pulls{/number}","milestones_url":"https://api.github.com/repos/binwiederhier/dabble/milestones{/number}","notifications_url":"https://api.github.com/repos/binwiederhier/dabble/notifications{?since,all,participating}","labels_url":"https://api.github.com/repos/binwiederhier/dabble/labels{/name}","releases_url":"https://api.github.com/repos/binwiederhier/dabble/releases{/id}","deployments_url":"https://api.github.com/repos/binwiederhier/dabble/deployments","created_at":"2022-03-15T15:06:17Z","updated_at":"2022-03-15T15:06:17Z","pushed_at":"2024-03-21T02:52:10Z","git_url":"git://github.com/binwiederhier/dabble.git","ssh_url":"git@github.com:binwiederhier/dabble.git","clone_url":"https://github.com/binwiederhier/dabble.git","svn_url":"https://github.com/binwiederhier/dabble","homepage":null,"size":1,"stargazers_count":0,"watchers_count":0,"language":null,"has_issues":true,"has_projects":true,"has_downloads":true,"has_wiki":true,"has_pages":false,"has_discussions":false,"forks_count":0,"mirror_url":null,"archived":false,"disabled":false,"open_issues_count":1,"license":null,"allow_forking":true,"is_template":false,"web_commit_signoff_required":false,"topics":[],"visibility":"public","forks":0,"open_issues":1,"watchers":0,"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","site_admin":false}}` + response := request(t, s, "PUT", `/mytopic?tpl=yes&message=[{{.pull_request.head.repo.full_name}}]+Pull+request+{{if+eq+.action+"opened"}}OPENED{{else}}CLOSED{{end}}:+{{.pull_request.title}}`, body, nil) + + require.Equal(t, 200, response.Code) + m := toMessage(t, response.Body.String()) + require.Equal(t, "", m.Title) + require.Equal(t, `[binwiederhier/dabble] Pull request OPENED: A sample PR from Phil`, m.Message) +} + func newTestConfig(t *testing.T) *Config { conf := NewConfig() conf.BaseURL = "http://127.0.0.1:12345" From b9c176ddbad6f3812af98dc164725432fb11428e Mon Sep 17 00:00:00 2001 From: binwiederhier Date: Fri, 22 Mar 2024 22:01:41 -0400 Subject: [PATCH 10/14] Tests --- server/errors.go | 2 + server/server.go | 27 ++++++------- server/server_test.go | 88 ++++++++++++++++-------------------------- util/timeout_writer.go | 34 ++++++++++++++++ 4 files changed, 81 insertions(+), 70 deletions(-) create mode 100644 util/timeout_writer.go diff --git a/server/errors.go b/server/errors.go index 92ea0ee6..3309e4b9 100644 --- a/server/errors.go +++ b/server/errors.go @@ -119,6 +119,8 @@ var ( errHTTPBadRequestWebPushTopicCountTooHigh = &errHTTP{40040, http.StatusBadRequest, "invalid request: too many web push topic subscriptions", "", nil} errHTTPBadRequestTemplatedMessageTooLarge = &errHTTP{40041, http.StatusBadRequest, "invalid request: message or title is too large after replacing template", "", nil} errHTTPBadRequestTemplatedMessageNotJSON = &errHTTP{40042, http.StatusBadRequest, "invalid request: message body must be JSON if templating is enabled", "", nil} + errHTTPBadRequestTemplateInvalid = &errHTTP{40043, http.StatusBadRequest, "invalid request: could not parse template", "", nil} + errHTTPBadRequestTemplateExecutionFailed = &errHTTP{40044, http.StatusBadRequest, "invalid request: template execution failed", "", 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} diff --git a/server/server.go b/server/server.go index 21269125..6c0a9f19 100644 --- a/server/server.go +++ b/server/server.go @@ -135,6 +135,7 @@ const ( 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 ) // WebSocket constants @@ -1102,34 +1103,30 @@ func (s *Server) handleBodyAsTemplatedTextMessage(m *message, body *util.PeekedR return errHTTPEntityTooLargeJSONBody } peekedBody := strings.TrimSpace(string(body.PeekedBytes)) - m.Message = replaceTemplate(m.Message, peekedBody) - m.Title = replaceTemplate(m.Title, peekedBody) + if m.Message, err = replaceTemplate(m.Message, peekedBody); err != nil { + return err + } + if m.Title, err = replaceTemplate(m.Title, peekedBody); err != nil { + return err + } if len(m.Message) > s.config.MessageSizeLimit { return errHTTPBadRequestTemplatedMessageTooLarge } return nil } -func replaceTemplate(tpl string, source string) string { - rendered, err := replaceTemplateInternal(tpl, source) - if err != nil { - return "" - } - return rendered -} - -func replaceTemplateInternal(tpl string, source string) (string, error) { +func replaceTemplate(tpl string, source string) (string, error) { var data any if err := json.Unmarshal([]byte(source), &data); err != nil { - return "", err + return "", errHTTPBadRequestTemplatedMessageNotJSON } t, err := template.New("").Parse(tpl) if err != nil { - return "", err + return "", errHTTPBadRequestTemplateInvalid } var buf bytes.Buffer - if err := t.Execute(&buf, data); err != nil { - return "", err + if err := t.Execute(util.NewTimeoutWriter(&buf, templateMaxExecutionTime), data); err != nil { + return "", errHTTPBadRequestTemplateExecutionFailed } return buf.String(), nil } diff --git a/server/server_test.go b/server/server_test.go index 6c76ff2f..713e097e 100644 --- a/server/server_test.go +++ b/server/server_test.go @@ -2635,8 +2635,8 @@ func TestServer_MessageTemplate(t *testing.T) { t.Parallel() s := newTestServer(t, newTestConfig(t)) response := request(t, s, "PUT", "/mytopic", `{"foo":"bar", "nested":{"title":"here"}}`, map[string]string{ - "X-Message": "${foo}", - "X-Title": "${nested.title}", + "X-Message": "{{.foo}}", + "X-Title": "{{.nested.title}}", "X-Template": "1", }) @@ -2650,8 +2650,8 @@ func TestServer_MessageTemplate_RepeatPlaceholder(t *testing.T) { t.Parallel() s := newTestServer(t, newTestConfig(t)) response := request(t, s, "PUT", "/mytopic", `{"foo":"bar", "nested":{"title":"here"}}`, map[string]string{ - "Message": "${foo} is ${foo}", - "Title": "${nested.title} is ${nested.title}", + "Message": "{{.foo}} is {{.foo}}", + "Title": "{{.nested.title}} is {{.nested.title}}", "Template": "1", }) @@ -2666,8 +2666,8 @@ func TestServer_MessageTemplate_JSONBody(t *testing.T) { s := newTestServer(t, newTestConfig(t)) body := `{"topic": "mytopic", "message": "{\"foo\":\"bar\",\"nested\":{\"title\":\"here\"}}"}` response := request(t, s, "PUT", "/", body, map[string]string{ - "m": "${foo}", - "t": "${nested.title}", + "m": "{{.foo}}", + "t": "{{.nested.title}}", "tpl": "1", }) @@ -2682,8 +2682,8 @@ func TestServer_MessageTemplate_MalformedJSONBody(t *testing.T) { s := newTestServer(t, newTestConfig(t)) body := `{"topic": "mytopic", "message": "{\"foo\":\"bar\",\"nested\":{\"title\":\"here\"INVALID"}` response := request(t, s, "PUT", "/", body, map[string]string{ - "X-Message": "${foo}", - "X-Title": "${nested.title}", + "X-Message": "{{.foo}}", + "X-Title": "{{.nested.title}}", "X-Template": "1", }) @@ -2695,22 +2695,22 @@ func TestServer_MessageTemplate_PlaceholderTypo(t *testing.T) { t.Parallel() s := newTestServer(t, newTestConfig(t)) response := request(t, s, "PUT", "/mytopic", `{"foo":"bar", "nested":{"title":"here"}}`, map[string]string{ - "X-Message": "${food}", - "X-Title": "${nested.titl}", + "X-Message": "{{.food}}", + "X-Title": "{{.neste.title}}", "X-Template": "1", }) require.Equal(t, 200, response.Code) m := toMessage(t, response.Body.String()) - require.Equal(t, "${food}", m.Message) - require.Equal(t, "${nested.titl}", m.Title) + require.Equal(t, "", m.Message) + require.Equal(t, "", m.Title) } func TestServer_MessageTemplate_MultiplePlaceholders(t *testing.T) { t.Parallel() s := newTestServer(t, newTestConfig(t)) response := request(t, s, "PUT", "/mytopic", `{"foo":"bar", "nested":{"title":"here"}}`, map[string]string{ - "X-Message": "${foo} is ${nested.title}", + "X-Message": "{{.foo}} is {{.nested.title}}", "X-Template": "1", }) @@ -2719,52 +2719,18 @@ func TestServer_MessageTemplate_MultiplePlaceholders(t *testing.T) { require.Equal(t, "bar is here", m.Message) } -func TestServer_MessageTemplate_NestedPlaceholders(t *testing.T) { - t.Parallel() - // not intended to work recursively for now - // i.e., ${${nested.bar}} should NOT evaluate to ${foo} and then to "bar" - s := newTestServer(t, newTestConfig(t)) - response := request(t, s, "PUT", "/mytopic", `{"foo":"bar", "nested":{"title":"here","bar":"foo"}}`, map[string]string{ - "X-Message": "${${nested.bar}}", - "X-Template": "1", - }) - - require.Equal(t, 200, response.Code) - m := toMessage(t, response.Body.String()) - require.Equal(t, "${${nested.bar}}", m.Message) -} - -func TestServer_MessageTemplate_NestedPlaceholdersFunky(t *testing.T) { - t.Parallel() - // The above example can technically work - // ${${nested.bar}} would be interpreted as a nested GJSON path with key "${nested" then key "bar" - // so you would probably expect the output to be "works!", BUT the second } in the placeholder is not - // included by the regex, so it is still there after replacing the placeholder, thus giving you "works!}" - s := newTestServer(t, newTestConfig(t)) - response := request(t, s, "PUT", "/mytopic", `{"foo":"bar", "nested":{"title":"here","bar":"foo"}, "${nested":{"bar":"works!"}}`, map[string]string{ - "X-Message": "${${nested.bar}}", - "X-Template": "1", - }) - - require.Equal(t, 200, response.Code) - m := toMessage(t, response.Body.String()) - require.Equal(t, "works!}", m.Message) -} - -func TestServer_MessageTemplate_FancyGJSON(t *testing.T) { +func TestServer_MessageTemplate_Range(t *testing.T) { t.Parallel() s := newTestServer(t, newTestConfig(t)) jsonBody := `{"foo": "bar", "errors": [{"level": "severe", "url": "https://severe1.com"},{"level": "warning", "url": "https://warning.com"},{"level": "severe", "url": "https://severe2.com"}]}` response := request(t, s, "PUT", "/mytopic", jsonBody, map[string]string{ - "X-Message": `${errors.#(level=="severe")#.url}`, - "X-Title": `${errors.#(level=="severe")#|#} Severe Errors`, + "X-Message": `Severe URLs:\n{{range .errors}}{{if eq .level "severe"}}- {{.url}}\n{{end}}{{end}}`, "X-Template": "1", }) require.Equal(t, 200, response.Code) m := toMessage(t, response.Body.String()) - require.Equal(t, `["https://severe1.com","https://severe2.com"]`, m.Message) - require.Equal(t, `2 Severe Errors`, m.Title) + require.Equal(t, "Severe URLs:\n- https://severe1.com\n- https://severe2.com\n", m.Message) } func TestServer_MessageTemplate_ExceedMessageSize_TemplatedMessageOK(t *testing.T) { @@ -2773,8 +2739,8 @@ func TestServer_MessageTemplate_ExceedMessageSize_TemplatedMessageOK(t *testing. c.MessageSizeLimit = 25 // 25 < len(HTTP body) < 32k, and len(m.Message) < 25 s := newTestServer(t, c) response := request(t, s, "PUT", "/mytopic", `{"foo":"bar", "nested":{"title":"here"}}`, map[string]string{ - "X-Message": "${foo}", - "X-Title": "${nested.title}", + "X-Message": "{{.foo}}", + "X-Title": "{{.nested.title}}", "X-Template": "yes", }) @@ -2790,7 +2756,7 @@ func TestServer_MessageTemplate_ExceedMessageSize_TemplatedMessageTooLong(t *tes c.MessageSizeLimit = 21 // 21 < len(HTTP body) < 32k, but !len(m.Message) < 21 s := newTestServer(t, c) response := request(t, s, "PUT", "/mytopic", `{"foo":"This is a long message"}`, map[string]string{ - "X-Message": "${foo}", + "X-Message": "{{.foo}}", "X-Template": "1", }) @@ -2803,7 +2769,7 @@ func TestServer_MessageTemplate_Grafana(t *testing.T) { c := newTestConfig(t) s := newTestServer(t, c) body := `{"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\nAnnotations:\n - summary = 15m load average too high\nSource: localhost:3000/alerting/grafana/NW9oDw-4z/view\nSilence: 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\n"}` - response := request(t, s, "PUT", "/mytopic?tpl=yes&title=Grafana+alert:+${title}&message=${message}", body, nil) + response := request(t, s, "PUT", "/mytopic?tpl=yes&title=Grafana+alert:+{{.title}}&message={{.message}}", body, nil) require.Equal(t, 200, response.Code) m := toMessage(t, response.Body.String()) @@ -2829,13 +2795,25 @@ func TestServer_MessageTemplate_GitHub(t *testing.T) { s := newTestServer(t, c) body := `{"action":"opened","number":1,"pull_request":{"url":"https://api.github.com/repos/binwiederhier/dabble/pulls/1","id":1783420972,"node_id":"PR_kwDOHAbdo85qTNgs","html_url":"https://github.com/binwiederhier/dabble/pull/1","diff_url":"https://github.com/binwiederhier/dabble/pull/1.diff","patch_url":"https://github.com/binwiederhier/dabble/pull/1.patch","issue_url":"https://api.github.com/repos/binwiederhier/dabble/issues/1","number":1,"state":"open","locked":false,"title":"A sample PR from Phil","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","site_admin":false},"body":null,"created_at":"2024-03-21T02:52:09Z","updated_at":"2024-03-21T02:52:09Z","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/dabble/pulls/1/commits","review_comments_url":"https://api.github.com/repos/binwiederhier/dabble/pulls/1/comments","review_comment_url":"https://api.github.com/repos/binwiederhier/dabble/pulls/comments{/number}","comments_url":"https://api.github.com/repos/binwiederhier/dabble/issues/1/comments","statuses_url":"https://api.github.com/repos/binwiederhier/dabble/statuses/5703842cc5715ed1e358d23ebb693db09747ae9b","head":{"label":"binwiederhier:aa","ref":"aa","sha":"5703842cc5715ed1e358d23ebb693db09747ae9b","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","site_admin":false},"repo":{"id":470212003,"node_id":"R_kgDOHAbdow","name":"dabble","full_name":"binwiederhier/dabble","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","site_admin":false},"html_url":"https://github.com/binwiederhier/dabble","description":"A repo for dabbling","fork":false,"url":"https://api.github.com/repos/binwiederhier/dabble","forks_url":"https://api.github.com/repos/binwiederhier/dabble/forks","keys_url":"https://api.github.com/repos/binwiederhier/dabble/keys{/key_id}","collaborators_url":"https://api.github.com/repos/binwiederhier/dabble/collaborators{/collaborator}","teams_url":"https://api.github.com/repos/binwiederhier/dabble/teams","hooks_url":"https://api.github.com/repos/binwiederhier/dabble/hooks","issue_events_url":"https://api.github.com/repos/binwiederhier/dabble/issues/events{/number}","events_url":"https://api.github.com/repos/binwiederhier/dabble/events","assignees_url":"https://api.github.com/repos/binwiederhier/dabble/assignees{/user}","branches_url":"https://api.github.com/repos/binwiederhier/dabble/branches{/branch}","tags_url":"https://api.github.com/repos/binwiederhier/dabble/tags","blobs_url":"https://api.github.com/repos/binwiederhier/dabble/git/blobs{/sha}","git_tags_url":"https://api.github.com/repos/binwiederhier/dabble/git/tags{/sha}","git_refs_url":"https://api.github.com/repos/binwiederhier/dabble/git/refs{/sha}","trees_url":"https://api.github.com/repos/binwiederhier/dabble/git/trees{/sha}","statuses_url":"https://api.github.com/repos/binwiederhier/dabble/statuses/{sha}","languages_url":"https://api.github.com/repos/binwiederhier/dabble/languages","stargazers_url":"https://api.github.com/repos/binwiederhier/dabble/stargazers","contributors_url":"https://api.github.com/repos/binwiederhier/dabble/contributors","subscribers_url":"https://api.github.com/repos/binwiederhier/dabble/subscribers","subscription_url":"https://api.github.com/repos/binwiederhier/dabble/subscription","commits_url":"https://api.github.com/repos/binwiederhier/dabble/commits{/sha}","git_commits_url":"https://api.github.com/repos/binwiederhier/dabble/git/commits{/sha}","comments_url":"https://api.github.com/repos/binwiederhier/dabble/comments{/number}","issue_comment_url":"https://api.github.com/repos/binwiederhier/dabble/issues/comments{/number}","contents_url":"https://api.github.com/repos/binwiederhier/dabble/contents/{+path}","compare_url":"https://api.github.com/repos/binwiederhier/dabble/compare/{base}...{head}","merges_url":"https://api.github.com/repos/binwiederhier/dabble/merges","archive_url":"https://api.github.com/repos/binwiederhier/dabble/{archive_format}{/ref}","downloads_url":"https://api.github.com/repos/binwiederhier/dabble/downloads","issues_url":"https://api.github.com/repos/binwiederhier/dabble/issues{/number}","pulls_url":"https://api.github.com/repos/binwiederhier/dabble/pulls{/number}","milestones_url":"https://api.github.com/repos/binwiederhier/dabble/milestones{/number}","notifications_url":"https://api.github.com/repos/binwiederhier/dabble/notifications{?since,all,participating}","labels_url":"https://api.github.com/repos/binwiederhier/dabble/labels{/name}","releases_url":"https://api.github.com/repos/binwiederhier/dabble/releases{/id}","deployments_url":"https://api.github.com/repos/binwiederhier/dabble/deployments","created_at":"2022-03-15T15:06:17Z","updated_at":"2022-03-15T15:06:17Z","pushed_at":"2024-03-21T02:52:10Z","git_url":"git://github.com/binwiederhier/dabble.git","ssh_url":"git@github.com:binwiederhier/dabble.git","clone_url":"https://github.com/binwiederhier/dabble.git","svn_url":"https://github.com/binwiederhier/dabble","homepage":null,"size":1,"stargazers_count":0,"watchers_count":0,"language":null,"has_issues":true,"has_projects":true,"has_downloads":true,"has_wiki":true,"has_pages":false,"has_discussions":false,"forks_count":0,"mirror_url":null,"archived":false,"disabled":false,"open_issues_count":1,"license":null,"allow_forking":true,"is_template":false,"web_commit_signoff_required":false,"topics":[],"visibility":"public","forks":0,"open_issues":1,"watchers":0,"default_branch":"main","allow_squash_merge":true,"allow_merge_commit":true,"allow_rebase_merge":true,"allow_auto_merge":false,"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":"72d931a20bb83d123ab45accaf761150c8b01211","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","site_admin":false},"repo":{"id":470212003,"node_id":"R_kgDOHAbdow","name":"dabble","full_name":"binwiederhier/dabble","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","site_admin":false},"html_url":"https://github.com/binwiederhier/dabble","description":"A repo for dabbling","fork":false,"url":"https://api.github.com/repos/binwiederhier/dabble","forks_url":"https://api.github.com/repos/binwiederhier/dabble/forks","keys_url":"https://api.github.com/repos/binwiederhier/dabble/keys{/key_id}","collaborators_url":"https://api.github.com/repos/binwiederhier/dabble/collaborators{/collaborator}","teams_url":"https://api.github.com/repos/binwiederhier/dabble/teams","hooks_url":"https://api.github.com/repos/binwiederhier/dabble/hooks","issue_events_url":"https://api.github.com/repos/binwiederhier/dabble/issues/events{/number}","events_url":"https://api.github.com/repos/binwiederhier/dabble/events","assignees_url":"https://api.github.com/repos/binwiederhier/dabble/assignees{/user}","branches_url":"https://api.github.com/repos/binwiederhier/dabble/branches{/branch}","tags_url":"https://api.github.com/repos/binwiederhier/dabble/tags","blobs_url":"https://api.github.com/repos/binwiederhier/dabble/git/blobs{/sha}","git_tags_url":"https://api.github.com/repos/binwiederhier/dabble/git/tags{/sha}","git_refs_url":"https://api.github.com/repos/binwiederhier/dabble/git/refs{/sha}","trees_url":"https://api.github.com/repos/binwiederhier/dabble/git/trees{/sha}","statuses_url":"https://api.github.com/repos/binwiederhier/dabble/statuses/{sha}","languages_url":"https://api.github.com/repos/binwiederhier/dabble/languages","stargazers_url":"https://api.github.com/repos/binwiederhier/dabble/stargazers","contributors_url":"https://api.github.com/repos/binwiederhier/dabble/contributors","subscribers_url":"https://api.github.com/repos/binwiederhier/dabble/subscribers","subscription_url":"https://api.github.com/repos/binwiederhier/dabble/subscription","commits_url":"https://api.github.com/repos/binwiederhier/dabble/commits{/sha}","git_commits_url":"https://api.github.com/repos/binwiederhier/dabble/git/commits{/sha}","comments_url":"https://api.github.com/repos/binwiederhier/dabble/comments{/number}","issue_comment_url":"https://api.github.com/repos/binwiederhier/dabble/issues/comments{/number}","contents_url":"https://api.github.com/repos/binwiederhier/dabble/contents/{+path}","compare_url":"https://api.github.com/repos/binwiederhier/dabble/compare/{base}...{head}","merges_url":"https://api.github.com/repos/binwiederhier/dabble/merges","archive_url":"https://api.github.com/repos/binwiederhier/dabble/{archive_format}{/ref}","downloads_url":"https://api.github.com/repos/binwiederhier/dabble/downloads","issues_url":"https://api.github.com/repos/binwiederhier/dabble/issues{/number}","pulls_url":"https://api.github.com/repos/binwiederhier/dabble/pulls{/number}","milestones_url":"https://api.github.com/repos/binwiederhier/dabble/milestones{/number}","notifications_url":"https://api.github.com/repos/binwiederhier/dabble/notifications{?since,all,participating}","labels_url":"https://api.github.com/repos/binwiederhier/dabble/labels{/name}","releases_url":"https://api.github.com/repos/binwiederhier/dabble/releases{/id}","deployments_url":"https://api.github.com/repos/binwiederhier/dabble/deployments","created_at":"2022-03-15T15:06:17Z","updated_at":"2022-03-15T15:06:17Z","pushed_at":"2024-03-21T02:52:10Z","git_url":"git://github.com/binwiederhier/dabble.git","ssh_url":"git@github.com:binwiederhier/dabble.git","clone_url":"https://github.com/binwiederhier/dabble.git","svn_url":"https://github.com/binwiederhier/dabble","homepage":null,"size":1,"stargazers_count":0,"watchers_count":0,"language":null,"has_issues":true,"has_projects":true,"has_downloads":true,"has_wiki":true,"has_pages":false,"has_discussions":false,"forks_count":0,"mirror_url":null,"archived":false,"disabled":false,"open_issues_count":1,"license":null,"allow_forking":true,"is_template":false,"web_commit_signoff_required":false,"topics":[],"visibility":"public","forks":0,"open_issues":1,"watchers":0,"default_branch":"main","allow_squash_merge":true,"allow_merge_commit":true,"allow_rebase_merge":true,"allow_auto_merge":false,"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/dabble/pulls/1"},"html":{"href":"https://github.com/binwiederhier/dabble/pull/1"},"issue":{"href":"https://api.github.com/repos/binwiederhier/dabble/issues/1"},"comments":{"href":"https://api.github.com/repos/binwiederhier/dabble/issues/1/comments"},"review_comments":{"href":"https://api.github.com/repos/binwiederhier/dabble/pulls/1/comments"},"review_comment":{"href":"https://api.github.com/repos/binwiederhier/dabble/pulls/comments{/number}"},"commits":{"href":"https://api.github.com/repos/binwiederhier/dabble/pulls/1/commits"},"statuses":{"href":"https://api.github.com/repos/binwiederhier/dabble/statuses/5703842cc5715ed1e358d23ebb693db09747ae9b"}},"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":1,"additions":1,"deletions":1,"changed_files":1},"repository":{"id":470212003,"node_id":"R_kgDOHAbdow","name":"dabble","full_name":"binwiederhier/dabble","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","site_admin":false},"html_url":"https://github.com/binwiederhier/dabble","description":"A repo for dabbling","fork":false,"url":"https://api.github.com/repos/binwiederhier/dabble","forks_url":"https://api.github.com/repos/binwiederhier/dabble/forks","keys_url":"https://api.github.com/repos/binwiederhier/dabble/keys{/key_id}","collaborators_url":"https://api.github.com/repos/binwiederhier/dabble/collaborators{/collaborator}","teams_url":"https://api.github.com/repos/binwiederhier/dabble/teams","hooks_url":"https://api.github.com/repos/binwiederhier/dabble/hooks","issue_events_url":"https://api.github.com/repos/binwiederhier/dabble/issues/events{/number}","events_url":"https://api.github.com/repos/binwiederhier/dabble/events","assignees_url":"https://api.github.com/repos/binwiederhier/dabble/assignees{/user}","branches_url":"https://api.github.com/repos/binwiederhier/dabble/branches{/branch}","tags_url":"https://api.github.com/repos/binwiederhier/dabble/tags","blobs_url":"https://api.github.com/repos/binwiederhier/dabble/git/blobs{/sha}","git_tags_url":"https://api.github.com/repos/binwiederhier/dabble/git/tags{/sha}","git_refs_url":"https://api.github.com/repos/binwiederhier/dabble/git/refs{/sha}","trees_url":"https://api.github.com/repos/binwiederhier/dabble/git/trees{/sha}","statuses_url":"https://api.github.com/repos/binwiederhier/dabble/statuses/{sha}","languages_url":"https://api.github.com/repos/binwiederhier/dabble/languages","stargazers_url":"https://api.github.com/repos/binwiederhier/dabble/stargazers","contributors_url":"https://api.github.com/repos/binwiederhier/dabble/contributors","subscribers_url":"https://api.github.com/repos/binwiederhier/dabble/subscribers","subscription_url":"https://api.github.com/repos/binwiederhier/dabble/subscription","commits_url":"https://api.github.com/repos/binwiederhier/dabble/commits{/sha}","git_commits_url":"https://api.github.com/repos/binwiederhier/dabble/git/commits{/sha}","comments_url":"https://api.github.com/repos/binwiederhier/dabble/comments{/number}","issue_comment_url":"https://api.github.com/repos/binwiederhier/dabble/issues/comments{/number}","contents_url":"https://api.github.com/repos/binwiederhier/dabble/contents/{+path}","compare_url":"https://api.github.com/repos/binwiederhier/dabble/compare/{base}...{head}","merges_url":"https://api.github.com/repos/binwiederhier/dabble/merges","archive_url":"https://api.github.com/repos/binwiederhier/dabble/{archive_format}{/ref}","downloads_url":"https://api.github.com/repos/binwiederhier/dabble/downloads","issues_url":"https://api.github.com/repos/binwiederhier/dabble/issues{/number}","pulls_url":"https://api.github.com/repos/binwiederhier/dabble/pulls{/number}","milestones_url":"https://api.github.com/repos/binwiederhier/dabble/milestones{/number}","notifications_url":"https://api.github.com/repos/binwiederhier/dabble/notifications{?since,all,participating}","labels_url":"https://api.github.com/repos/binwiederhier/dabble/labels{/name}","releases_url":"https://api.github.com/repos/binwiederhier/dabble/releases{/id}","deployments_url":"https://api.github.com/repos/binwiederhier/dabble/deployments","created_at":"2022-03-15T15:06:17Z","updated_at":"2022-03-15T15:06:17Z","pushed_at":"2024-03-21T02:52:10Z","git_url":"git://github.com/binwiederhier/dabble.git","ssh_url":"git@github.com:binwiederhier/dabble.git","clone_url":"https://github.com/binwiederhier/dabble.git","svn_url":"https://github.com/binwiederhier/dabble","homepage":null,"size":1,"stargazers_count":0,"watchers_count":0,"language":null,"has_issues":true,"has_projects":true,"has_downloads":true,"has_wiki":true,"has_pages":false,"has_discussions":false,"forks_count":0,"mirror_url":null,"archived":false,"disabled":false,"open_issues_count":1,"license":null,"allow_forking":true,"is_template":false,"web_commit_signoff_required":false,"topics":[],"visibility":"public","forks":0,"open_issues":1,"watchers":0,"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","site_admin":false}}` response := request(t, s, "PUT", `/mytopic?tpl=yes&message=[{{.pull_request.head.repo.full_name}}]+Pull+request+{{if+eq+.action+"opened"}}OPENED{{else}}CLOSED{{end}}:+{{.pull_request.title}}`, body, nil) - require.Equal(t, 200, response.Code) m := toMessage(t, response.Body.String()) require.Equal(t, "", m.Title) require.Equal(t, `[binwiederhier/dabble] Pull request OPENED: A sample PR from Phil`, m.Message) } +func TestServer_MessageTemplate_GitHub2(t *testing.T) { + t.Parallel() + c := newTestConfig(t) + s := newTestServer(t, c) + body := `{"action":"opened","number":1,"pull_request":{"url":"https://api.github.com/repos/binwiederhier/dabble/pulls/1","id":1783420972,"node_id":"PR_kwDOHAbdo85qTNgs","html_url":"https://github.com/binwiederhier/dabble/pull/1","diff_url":"https://github.com/binwiederhier/dabble/pull/1.diff","patch_url":"https://github.com/binwiederhier/dabble/pull/1.patch","issue_url":"https://api.github.com/repos/binwiederhier/dabble/issues/1","number":1,"state":"open","locked":false,"title":"A sample PR from Phil","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","site_admin":false},"body":null,"created_at":"2024-03-21T02:52:09Z","updated_at":"2024-03-21T02:52:09Z","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/dabble/pulls/1/commits","review_comments_url":"https://api.github.com/repos/binwiederhier/dabble/pulls/1/comments","review_comment_url":"https://api.github.com/repos/binwiederhier/dabble/pulls/comments{/number}","comments_url":"https://api.github.com/repos/binwiederhier/dabble/issues/1/comments","statuses_url":"https://api.github.com/repos/binwiederhier/dabble/statuses/5703842cc5715ed1e358d23ebb693db09747ae9b","head":{"label":"binwiederhier:aa","ref":"aa","sha":"5703842cc5715ed1e358d23ebb693db09747ae9b","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","site_admin":false},"repo":{"id":470212003,"node_id":"R_kgDOHAbdow","name":"dabble","full_name":"binwiederhier/dabble","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","site_admin":false},"html_url":"https://github.com/binwiederhier/dabble","description":"A repo for dabbling","fork":false,"url":"https://api.github.com/repos/binwiederhier/dabble","forks_url":"https://api.github.com/repos/binwiederhier/dabble/forks","keys_url":"https://api.github.com/repos/binwiederhier/dabble/keys{/key_id}","collaborators_url":"https://api.github.com/repos/binwiederhier/dabble/collaborators{/collaborator}","teams_url":"https://api.github.com/repos/binwiederhier/dabble/teams","hooks_url":"https://api.github.com/repos/binwiederhier/dabble/hooks","issue_events_url":"https://api.github.com/repos/binwiederhier/dabble/issues/events{/number}","events_url":"https://api.github.com/repos/binwiederhier/dabble/events","assignees_url":"https://api.github.com/repos/binwiederhier/dabble/assignees{/user}","branches_url":"https://api.github.com/repos/binwiederhier/dabble/branches{/branch}","tags_url":"https://api.github.com/repos/binwiederhier/dabble/tags","blobs_url":"https://api.github.com/repos/binwiederhier/dabble/git/blobs{/sha}","git_tags_url":"https://api.github.com/repos/binwiederhier/dabble/git/tags{/sha}","git_refs_url":"https://api.github.com/repos/binwiederhier/dabble/git/refs{/sha}","trees_url":"https://api.github.com/repos/binwiederhier/dabble/git/trees{/sha}","statuses_url":"https://api.github.com/repos/binwiederhier/dabble/statuses/{sha}","languages_url":"https://api.github.com/repos/binwiederhier/dabble/languages","stargazers_url":"https://api.github.com/repos/binwiederhier/dabble/stargazers","contributors_url":"https://api.github.com/repos/binwiederhier/dabble/contributors","subscribers_url":"https://api.github.com/repos/binwiederhier/dabble/subscribers","subscription_url":"https://api.github.com/repos/binwiederhier/dabble/subscription","commits_url":"https://api.github.com/repos/binwiederhier/dabble/commits{/sha}","git_commits_url":"https://api.github.com/repos/binwiederhier/dabble/git/commits{/sha}","comments_url":"https://api.github.com/repos/binwiederhier/dabble/comments{/number}","issue_comment_url":"https://api.github.com/repos/binwiederhier/dabble/issues/comments{/number}","contents_url":"https://api.github.com/repos/binwiederhier/dabble/contents/{+path}","compare_url":"https://api.github.com/repos/binwiederhier/dabble/compare/{base}...{head}","merges_url":"https://api.github.com/repos/binwiederhier/dabble/merges","archive_url":"https://api.github.com/repos/binwiederhier/dabble/{archive_format}{/ref}","downloads_url":"https://api.github.com/repos/binwiederhier/dabble/downloads","issues_url":"https://api.github.com/repos/binwiederhier/dabble/issues{/number}","pulls_url":"https://api.github.com/repos/binwiederhier/dabble/pulls{/number}","milestones_url":"https://api.github.com/repos/binwiederhier/dabble/milestones{/number}","notifications_url":"https://api.github.com/repos/binwiederhier/dabble/notifications{?since,all,participating}","labels_url":"https://api.github.com/repos/binwiederhier/dabble/labels{/name}","releases_url":"https://api.github.com/repos/binwiederhier/dabble/releases{/id}","deployments_url":"https://api.github.com/repos/binwiederhier/dabble/deployments","created_at":"2022-03-15T15:06:17Z","updated_at":"2022-03-15T15:06:17Z","pushed_at":"2024-03-21T02:52:10Z","git_url":"git://github.com/binwiederhier/dabble.git","ssh_url":"git@github.com:binwiederhier/dabble.git","clone_url":"https://github.com/binwiederhier/dabble.git","svn_url":"https://github.com/binwiederhier/dabble","homepage":null,"size":1,"stargazers_count":0,"watchers_count":0,"language":null,"has_issues":true,"has_projects":true,"has_downloads":true,"has_wiki":true,"has_pages":false,"has_discussions":false,"forks_count":0,"mirror_url":null,"archived":false,"disabled":false,"open_issues_count":1,"license":null,"allow_forking":true,"is_template":false,"web_commit_signoff_required":false,"topics":[],"visibility":"public","forks":0,"open_issues":1,"watchers":0,"default_branch":"main","allow_squash_merge":true,"allow_merge_commit":true,"allow_rebase_merge":true,"allow_auto_merge":false,"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":"72d931a20bb83d123ab45accaf761150c8b01211","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","site_admin":false},"repo":{"id":470212003,"node_id":"R_kgDOHAbdow","name":"dabble","full_name":"binwiederhier/dabble","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","site_admin":false},"html_url":"https://github.com/binwiederhier/dabble","description":"A repo for dabbling","fork":false,"url":"https://api.github.com/repos/binwiederhier/dabble","forks_url":"https://api.github.com/repos/binwiederhier/dabble/forks","keys_url":"https://api.github.com/repos/binwiederhier/dabble/keys{/key_id}","collaborators_url":"https://api.github.com/repos/binwiederhier/dabble/collaborators{/collaborator}","teams_url":"https://api.github.com/repos/binwiederhier/dabble/teams","hooks_url":"https://api.github.com/repos/binwiederhier/dabble/hooks","issue_events_url":"https://api.github.com/repos/binwiederhier/dabble/issues/events{/number}","events_url":"https://api.github.com/repos/binwiederhier/dabble/events","assignees_url":"https://api.github.com/repos/binwiederhier/dabble/assignees{/user}","branches_url":"https://api.github.com/repos/binwiederhier/dabble/branches{/branch}","tags_url":"https://api.github.com/repos/binwiederhier/dabble/tags","blobs_url":"https://api.github.com/repos/binwiederhier/dabble/git/blobs{/sha}","git_tags_url":"https://api.github.com/repos/binwiederhier/dabble/git/tags{/sha}","git_refs_url":"https://api.github.com/repos/binwiederhier/dabble/git/refs{/sha}","trees_url":"https://api.github.com/repos/binwiederhier/dabble/git/trees{/sha}","statuses_url":"https://api.github.com/repos/binwiederhier/dabble/statuses/{sha}","languages_url":"https://api.github.com/repos/binwiederhier/dabble/languages","stargazers_url":"https://api.github.com/repos/binwiederhier/dabble/stargazers","contributors_url":"https://api.github.com/repos/binwiederhier/dabble/contributors","subscribers_url":"https://api.github.com/repos/binwiederhier/dabble/subscribers","subscription_url":"https://api.github.com/repos/binwiederhier/dabble/subscription","commits_url":"https://api.github.com/repos/binwiederhier/dabble/commits{/sha}","git_commits_url":"https://api.github.com/repos/binwiederhier/dabble/git/commits{/sha}","comments_url":"https://api.github.com/repos/binwiederhier/dabble/comments{/number}","issue_comment_url":"https://api.github.com/repos/binwiederhier/dabble/issues/comments{/number}","contents_url":"https://api.github.com/repos/binwiederhier/dabble/contents/{+path}","compare_url":"https://api.github.com/repos/binwiederhier/dabble/compare/{base}...{head}","merges_url":"https://api.github.com/repos/binwiederhier/dabble/merges","archive_url":"https://api.github.com/repos/binwiederhier/dabble/{archive_format}{/ref}","downloads_url":"https://api.github.com/repos/binwiederhier/dabble/downloads","issues_url":"https://api.github.com/repos/binwiederhier/dabble/issues{/number}","pulls_url":"https://api.github.com/repos/binwiederhier/dabble/pulls{/number}","milestones_url":"https://api.github.com/repos/binwiederhier/dabble/milestones{/number}","notifications_url":"https://api.github.com/repos/binwiederhier/dabble/notifications{?since,all,participating}","labels_url":"https://api.github.com/repos/binwiederhier/dabble/labels{/name}","releases_url":"https://api.github.com/repos/binwiederhier/dabble/releases{/id}","deployments_url":"https://api.github.com/repos/binwiederhier/dabble/deployments","created_at":"2022-03-15T15:06:17Z","updated_at":"2022-03-15T15:06:17Z","pushed_at":"2024-03-21T02:52:10Z","git_url":"git://github.com/binwiederhier/dabble.git","ssh_url":"git@github.com:binwiederhier/dabble.git","clone_url":"https://github.com/binwiederhier/dabble.git","svn_url":"https://github.com/binwiederhier/dabble","homepage":null,"size":1,"stargazers_count":0,"watchers_count":0,"language":null,"has_issues":true,"has_projects":true,"has_downloads":true,"has_wiki":true,"has_pages":false,"has_discussions":false,"forks_count":0,"mirror_url":null,"archived":false,"disabled":false,"open_issues_count":1,"license":null,"allow_forking":true,"is_template":false,"web_commit_signoff_required":false,"topics":[],"visibility":"public","forks":0,"open_issues":1,"watchers":0,"default_branch":"main","allow_squash_merge":true,"allow_merge_commit":true,"allow_rebase_merge":true,"allow_auto_merge":false,"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/dabble/pulls/1"},"html":{"href":"https://github.com/binwiederhier/dabble/pull/1"},"issue":{"href":"https://api.github.com/repos/binwiederhier/dabble/issues/1"},"comments":{"href":"https://api.github.com/repos/binwiederhier/dabble/issues/1/comments"},"review_comments":{"href":"https://api.github.com/repos/binwiederhier/dabble/pulls/1/comments"},"review_comment":{"href":"https://api.github.com/repos/binwiederhier/dabble/pulls/comments{/number}"},"commits":{"href":"https://api.github.com/repos/binwiederhier/dabble/pulls/1/commits"},"statuses":{"href":"https://api.github.com/repos/binwiederhier/dabble/statuses/5703842cc5715ed1e358d23ebb693db09747ae9b"}},"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":1,"additions":1,"deletions":1,"changed_files":1},"repository":{"id":470212003,"node_id":"R_kgDOHAbdow","name":"dabble","full_name":"binwiederhier/dabble","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","site_admin":false},"html_url":"https://github.com/binwiederhier/dabble","description":"A repo for dabbling","fork":false,"url":"https://api.github.com/repos/binwiederhier/dabble","forks_url":"https://api.github.com/repos/binwiederhier/dabble/forks","keys_url":"https://api.github.com/repos/binwiederhier/dabble/keys{/key_id}","collaborators_url":"https://api.github.com/repos/binwiederhier/dabble/collaborators{/collaborator}","teams_url":"https://api.github.com/repos/binwiederhier/dabble/teams","hooks_url":"https://api.github.com/repos/binwiederhier/dabble/hooks","issue_events_url":"https://api.github.com/repos/binwiederhier/dabble/issues/events{/number}","events_url":"https://api.github.com/repos/binwiederhier/dabble/events","assignees_url":"https://api.github.com/repos/binwiederhier/dabble/assignees{/user}","branches_url":"https://api.github.com/repos/binwiederhier/dabble/branches{/branch}","tags_url":"https://api.github.com/repos/binwiederhier/dabble/tags","blobs_url":"https://api.github.com/repos/binwiederhier/dabble/git/blobs{/sha}","git_tags_url":"https://api.github.com/repos/binwiederhier/dabble/git/tags{/sha}","git_refs_url":"https://api.github.com/repos/binwiederhier/dabble/git/refs{/sha}","trees_url":"https://api.github.com/repos/binwiederhier/dabble/git/trees{/sha}","statuses_url":"https://api.github.com/repos/binwiederhier/dabble/statuses/{sha}","languages_url":"https://api.github.com/repos/binwiederhier/dabble/languages","stargazers_url":"https://api.github.com/repos/binwiederhier/dabble/stargazers","contributors_url":"https://api.github.com/repos/binwiederhier/dabble/contributors","subscribers_url":"https://api.github.com/repos/binwiederhier/dabble/subscribers","subscription_url":"https://api.github.com/repos/binwiederhier/dabble/subscription","commits_url":"https://api.github.com/repos/binwiederhier/dabble/commits{/sha}","git_commits_url":"https://api.github.com/repos/binwiederhier/dabble/git/commits{/sha}","comments_url":"https://api.github.com/repos/binwiederhier/dabble/comments{/number}","issue_comment_url":"https://api.github.com/repos/binwiederhier/dabble/issues/comments{/number}","contents_url":"https://api.github.com/repos/binwiederhier/dabble/contents/{+path}","compare_url":"https://api.github.com/repos/binwiederhier/dabble/compare/{base}...{head}","merges_url":"https://api.github.com/repos/binwiederhier/dabble/merges","archive_url":"https://api.github.com/repos/binwiederhier/dabble/{archive_format}{/ref}","downloads_url":"https://api.github.com/repos/binwiederhier/dabble/downloads","issues_url":"https://api.github.com/repos/binwiederhier/dabble/issues{/number}","pulls_url":"https://api.github.com/repos/binwiederhier/dabble/pulls{/number}","milestones_url":"https://api.github.com/repos/binwiederhier/dabble/milestones{/number}","notifications_url":"https://api.github.com/repos/binwiederhier/dabble/notifications{?since,all,participating}","labels_url":"https://api.github.com/repos/binwiederhier/dabble/labels{/name}","releases_url":"https://api.github.com/repos/binwiederhier/dabble/releases{/id}","deployments_url":"https://api.github.com/repos/binwiederhier/dabble/deployments","created_at":"2022-03-15T15:06:17Z","updated_at":"2022-03-15T15:06:17Z","pushed_at":"2024-03-21T02:52:10Z","git_url":"git://github.com/binwiederhier/dabble.git","ssh_url":"git@github.com:binwiederhier/dabble.git","clone_url":"https://github.com/binwiederhier/dabble.git","svn_url":"https://github.com/binwiederhier/dabble","homepage":null,"size":1,"stargazers_count":0,"watchers_count":0,"language":null,"has_issues":true,"has_projects":true,"has_downloads":true,"has_wiki":true,"has_pages":false,"has_discussions":false,"forks_count":0,"mirror_url":null,"archived":false,"disabled":false,"open_issues_count":1,"license":null,"allow_forking":true,"is_template":false,"web_commit_signoff_required":false,"topics":[],"visibility":"public","forks":0,"open_issues":1,"watchers":0,"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","site_admin":false}}` + response := request(t, s, "PUT", `/mytopic?tpl=yes&title={{if+eq+.action+"opened"}}New+PR:+%23{{.number}}+by+{{.pull_request.user.login}}{{else}}[{{.action}}]+PR:+%23{{.number}}+by+{{.pull_request.user.login}}{{end}}&message={{.pull_request.title}}+in+{{.repository.full_name}}.+View+more+at+{{.pull_request.html_url}}`, body, nil) + + require.Equal(t, 200, response.Code) + m := toMessage(t, response.Body.String()) + require.Equal(t, `New PR: #1 by binwiederhier`, m.Title) + require.Equal(t, `A sample PR from Phil in binwiederhier/dabble. View more at https://github.com/binwiederhier/dabble/pull/1`, m.Message) +} + func newTestConfig(t *testing.T) *Config { conf := NewConfig() conf.BaseURL = "http://127.0.0.1:12345" diff --git a/util/timeout_writer.go b/util/timeout_writer.go new file mode 100644 index 00000000..370068c4 --- /dev/null +++ b/util/timeout_writer.go @@ -0,0 +1,34 @@ +package util + +import ( + "errors" + "io" + "time" +) + +// ErrWriteTimeout is returned when a write timed out +var ErrWriteTimeout = errors.New("write operation failed due to timeout since creation") + +// TimeoutWriter wraps an io.Writer that will time out after the given timeout +type TimeoutWriter struct { + writer io.Writer + timeout time.Duration + start time.Time +} + +// NewTimeoutWriter creates a new TimeoutWriter +func NewTimeoutWriter(w io.Writer, timeout time.Duration) *TimeoutWriter { + return &TimeoutWriter{ + writer: w, + timeout: timeout, + start: time.Now(), + } +} + +// Write implements the io.Writer interface, failing if called after the timeout period from creation. +func (tw *TimeoutWriter) Write(p []byte) (n int, err error) { + if time.Since(tw.start) > tw.timeout { + return 0, errors.New("write operation failed due to timeout since creation") + } + return tw.writer.Write(p) +} From 547b09a7e5eeef22ed60a7754c764d1f68fe9ae0 Mon Sep 17 00:00:00 2001 From: binwiederhier Date: Sat, 23 Mar 2024 14:22:56 -0400 Subject: [PATCH 11/14] docs --- docs/publish.md | 71 +++++++++++++----- .../img/android-screenshot-template.jpg | Bin 0 -> 124374 bytes 2 files changed, 53 insertions(+), 18 deletions(-) create mode 100644 docs/static/img/android-screenshot-template.jpg diff --git a/docs/publish.md b/docs/publish.md index 08bb7f74..4d54f77e 100644 --- a/docs/publish.md +++ b/docs/publish.md @@ -940,34 +940,69 @@ Here's an example with a custom message, tags and a priority: ## JSON templating -Some services let you specify a webhook URL but do not let you modify the webhook body (e.g. GitHub, Grafana). Instead of using a separate -bridge program to parse the webhook body into the format ntfy expects, you can include a templated message and/or a templated title -which will be populated based on the fields of the webhook body (so long as the webhook body is valid JSON). +Templating lets you **format a JSON message body into human-friendly message and title text** using +[Go templates](https://pkg.go.dev/text/template) (see tutorials [here](https://blog.gopheracademy.com/advent-2017/using-go-templates/), +[here](https://www.digitalocean.com/community/tutorials/how-to-use-templates-in-go), and +[here](https://developer.hashicorp.com/nomad/tutorials/templates/go-template-syntax)). This is specifically useful when +**combined with webhooks** from services such as GitHub, Grafana, or other services that emit JSON webhooks. -Enable templating by setting the `X-Template` header (or its aliases `Template` or `tpl`) to `yes`, or (more appropriately for webhooks) -by setting the `?template=yes` query parameter. Then, include templates in your message and/or title by including paths to the -appropriate JSON fields surrounded by `${` and `}`, e.g. `${alert.title}` or `${error.desc}`, depending on your JSON payload. +Instead of using a separate bridge program to parse the webhook body into the format ntfy expects, you can include a templated +message and/or a templated title which will be populated based on the fields of the webhook body (so long as the webhook body +is valid JSON). -Please refer to the [GJSON docs](https://github.com/tidwall/gjson/blob/master/SYNTAX.md) for supported JSON path syntax, as well as -[gjson.dev](https://gjson.dev/) to test your templates. +Enable templating by setting the `X-Template` header (or its aliases `Template` or `tpl`) to `yes` or `1`, or (more appropriately +for webhooks) by setting the `?template=yes` query parameter. Then, include templates in your `message` and/or `title`, like so: + +* Variables,, e.g. `{{.alert.title}}` or `An error occurred: {{.error.desc}}` +* Conditionals (if/else, e.g. `{{if eq .action "opened"}}..{{else}}..{{end}}`, see [example](https://repeatit.io/#/share/eyJ0ZW1wbGF0ZSI6Ilt7ey5wdWxsX3JlcXVlc3QuaGVhZC5yZXBvLmZ1bGxfbmFtZX19XSBQdWxsIHJlcXVlc3Qge3tpZiBlcSAuYWN0aW9uIFwib3BlbmVkXCJ9fU9QRU5FRHt7ZWxzZX19Q0xPU0VEe3tlbmR9fToge3sucHVsbF9yZXF1ZXN0LnRpdGxlfX0iLCJpbnB1dCI6IntcbiAgXCJhY3Rpb25cIjogXCJvcGVuZWRcIixcbiAgXCJudW1iZXJcIjogMSxcbiAgXCJwdWxsX3JlcXVlc3RcIjoge1xuICAgIFwidXJsXCI6IFwiaHR0cHM6Ly9hcGkuZ2l0aHViLmNvbS9yZXBvcy9iaW53aWVkZXJoaWVyL2RhYmJsZS9wdWxscy8xXCIsXG4gICAgXCJpZFwiOiAxNzgzNDIwOTcyLFxuICAgIFwibm9kZV9pZFwiOiBcIlBSX2t3RE9IQWJkbzg1cVROZ3NcIixcbiAgICBcImh0bWxfdXJsXCI6IFwiaHR0cHM6Ly9naXRodWIuY29tL2JpbndpZWRlcmhpZXIvZGFiYmxlL3B1bGwvMVwiLFxuICAgIFwiZGlmZl91cmxcIjogXCJodHRwczovL2dpdGh1Yi5jb20vYmlud2llZGVyaGllci9kYWJibGUvcHVsbC8xLmRpZmZcIixcbiAgICBcInBhdGNoX3VybFwiOiBcImh0dHBzOi8vZ2l0aHViLmNvbS9iaW53aWVkZXJoaWVyL2RhYmJsZS9wdWxsLzEucGF0Y2hcIixcbiAgICBcImlzc3VlX3VybFwiOiBcImh0dHBzOi8vYXBpLmdpdGh1Yi5jb20vcmVwb3MvYmlud2llZGVyaGllci9kYWJibGUvaXNzdWVzLzFcIixcbiAgICBcIm51bWJlclwiOiAxLFxuICAgIFwic3RhdGVcIjogXCJvcGVuXCIsXG4gICAgXCJsb2NrZWRcIjogZmFsc2UsXG4gICAgXCJ0aXRsZVwiOiBcIkEgc2FtcGxlIFBSIGZyb20gUGhpbFwiLFxuICAgIFwidXNlclwiOiB7XG4gICAgICBcImxvZ2luXCI6IFwiYmlud2llZGVyaGllclwiLFxuICAgICAgXCJpZFwiOiA2NjQ1OTcsXG4gICAgICBcIm5vZGVfaWRcIjogXCJNRFE2VlhObGNqWTJORFU1Tnc9PVwiLFxuICAgICAgXCJhdmF0YXJfdXJsXCI6IFwiaHR0cHM6Ly9hdmF0YXJzLmdpdGh1YnVzZXJjb250ZW50LmNvbS91LzY2NDU5Nz92PTRcIixcbiAgICAgIFwiZ3JhdmF0YXJfaWRcIjogXCJcIixcbiAgICAgIFwidXJsXCI6IFwiaHR0cHM6Ly9hcGkuZ2l0aHViLmNvbS91c2Vycy9iaW53aWVkZXJoaWVyXCIsXG4gICAgICBcImh0bWxfdXJsXCI6IFwiaHR0cHM6Ly9naXRodWIuY29tL2JpbndpZWRlcmhpZXJcIixcbiAgICAgIFwiZm9sbG93ZXJzX3VybFwiOiBcImh0dHBzOi8vYXBpLmdpdGh1Yi5jb20vdXNlcnMvYmlud2llZGVyaGllci9mb2xsb3dlcnNcIixcbiAgICAgIFwiZm9sbG93aW5nX3VybFwiOiBcImh0dHBzOi8vYXBpLmdpdGh1Yi5jb20vdXNlcnMvYmlud2llZGVyaGllci9mb2xsb3dpbmd7L290aGVyX3VzZXJ9XCIsXG4gICAgICBcImdpc3RzX3VybFwiOiBcImh0dHBzOi8vYXBpLmdpdGh1Yi5jb20vdXNlcnMvYmlud2llZGVyaGllci9naXN0c3svZ2lzdF9pZH1cIixcbiAgICAgIFwic3RhcnJlZF91cmxcIjogXCJodHRwczovL2FwaS5naXRodWIuY29tL3VzZXJzL2JpbndpZWRlcmhpZXIvc3RhcnJlZHsvb3duZXJ9ey9yZXBvfVwiLFxuICAgICAgXCJzdWJzY3JpcHRpb25zX3VybFwiOiBcImh0dHBzOi8vYXBpLmdpdGh1Yi5jb20vdXNlcnMvYmlud2llZGVyaGllci9zdWJzY3JpcHRpb25zXCIsXG4gICAgICBcIm9yZ2FuaXphdGlvbnNfdXJsXCI6IFwiaHR0cHM6Ly9hcGkuZ2l0aHViLmNvbS91c2Vycy9iaW53aWVkZXJoaWVyL29yZ3NcIixcbiAgICAgIFwicmVwb3NfdXJsXCI6IFwiaHR0cHM6Ly9hcGkuZ2l0aHViLmNvbS91c2Vycy9iaW53aWVkZXJoaWVyL3JlcG9zXCIsXG4gICAgICBcImV2ZW50c191cmxcIjogXCJodHRwczovL2FwaS5naXRodWIuY29tL3VzZXJzL2JpbndpZWRlcmhpZXIvZXZlbnRzey9wcml2YWN5fVwiLFxuICAgICAgXCJyZWNlaXZlZF9ldmVudHNfdXJsXCI6IFwiaHR0cHM6Ly9hcGkuZ2l0aHViLmNvbS91c2Vycy9iaW53aWVkZXJoaWVyL3JlY2VpdmVkX2V2ZW50c1wiLFxuICAgICAgXCJ0eXBlXCI6IFwiVXNlclwiLFxuICAgICAgXCJzaXRlX2FkbWluXCI6IGZhbHNlXG4gICAgfSxcbiAgICBcImJvZHlcIjogbnVsbCxcbiAgICBcImNyZWF0ZWRfYXRcIjogXCIyMDI0LTAzLTIxVDAyOjUyOjA5WlwiLFxuICAgIFwidXBkYXRlZF9hdFwiOiBcIjIwMjQtMDMtMjFUMDI6NTI6MDlaXCIsXG4gICAgXCJjbG9zZWRfYXRcIjogbnVsbCxcbiAgICBcIm1lcmdlZF9hdFwiOiBudWxsLFxuICAgIFwibWVyZ2VfY29tbWl0X3NoYVwiOiBudWxsLFxuICAgIFwiYXNzaWduZWVcIjogbnVsbCxcbiAgICBcImFzc2lnbmVlc1wiOiBbXSxcbiAgICBcInJlcXVlc3RlZF9yZXZpZXdlcnNcIjogW10sXG4gICAgXCJyZXF1ZXN0ZWRfdGVhbXNcIjogW10sXG4gICAgXCJsYWJlbHNcIjogW10sXG4gICAgXCJtaWxlc3RvbmVcIjogbnVsbCxcbiAgICBcImRyYWZ0XCI6IGZhbHNlLFxuICAgIFwiY29tbWl0c191cmxcIjogXCJodHRwczovL2FwaS5naXRodWIuY29tL3JlcG9zL2JpbndpZWRlcmhpZXIvZGFiYmxlL3B1bGxzLzEvY29tbWl0c1wiLFxuICAgIFwicmV2aWV3X2NvbW1lbnRzX3VybFwiOiBcImh0dHBzOi8vYXBpLmdpdGh1Yi5jb20vcmVwb3MvYmlud2llZGVyaGllci9kYWJibGUvcHVsbHMvMS9jb21tZW50c1wiLFxuICAgIFwicmV2aWV3X2NvbW1lbnRfdXJsXCI6IFwiaHR0cHM6Ly9hcGkuZ2l0aHViLmNvbS9yZXBvcy9iaW53aWVkZXJoaWVyL2RhYmJsZS9wdWxscy9jb21tZW50c3svbnVtYmVyfVwiLFxuICAgIFwiY29tbWVudHNfdXJsXCI6IFwiaHR0cHM6Ly9hcGkuZ2l0aHViLmNvbS9yZXBvcy9iaW53aWVkZXJoaWVyL2RhYmJsZS9pc3N1ZXMvMS9jb21tZW50c1wiLFxuICAgIFwic3RhdHVzZXNfdXJsXCI6IFwiaHR0cHM6Ly9hcGkuZ2l0aHViLmNvbS9yZXBvcy9iaW53aWVkZXJoaWVyL2RhYmJsZS9zdGF0dXNlcy81NzAzODQyY2M1NzE1ZWQxZTM1OGQyM2ViYjY5M2RiMDk3NDdhZTliXCIsXG4gICAgXCJoZWFkXCI6IHtcbiAgICAgIFwibGFiZWxcIjogXCJiaW53aWVkZXJoaWVyOmFhXCIsXG4gICAgICBcInJlZlwiOiBcImFhXCIsXG4gICAgICBcInNoYVwiOiBcIjU3MDM4NDJjYzU3MTVlZDFlMzU4ZDIzZWJiNjkzZGIwOTc0N2FlOWJcIixcbiAgICAgIFwidXNlclwiOiB7XG4gICAgICAgIFwibG9naW5cIjogXCJiaW53aWVkZXJoaWVyXCIsXG4gICAgICAgIFwiaWRcIjogNjY0NTk3LFxuICAgICAgICBcIm5vZGVfaWRcIjogXCJNRFE2VlhObGNqWTJORFU1Tnc9PVwiLFxuICAgICAgICBcImF2YXRhcl91cmxcIjogXCJodHRwczovL2F2YXRhcnMuZ2l0aHVidXNlcmNvbnRlbnQuY29tL3UvNjY0NTk3P3Y9NFwiLFxuICAgICAgICBcImdyYXZhdGFyX2lkXCI6IFwiXCIsXG4gICAgICAgIFwidXJsXCI6IFwiaHR0cHM6Ly9hcGkuZ2l0aHViLmNvbS91c2Vycy9iaW53aWVkZXJoaWVyXCIsXG4gICAgICAgIFwiaHRtbF91cmxcIjogXCJodHRwczovL2dpdGh1Yi5jb20vYmlud2llZGVyaGllclwiLFxuICAgICAgICBcImZvbGxvd2Vyc191cmxcIjogXCJodHRwczovL2FwaS5naXRodWIuY29tL3VzZXJzL2JpbndpZWRlcmhpZXIvZm9sbG93ZXJzXCIsXG4gICAgICAgIFwiZm9sbG93aW5nX3VybFwiOiBcImh0dHBzOi8vYXBpLmdpdGh1Yi5jb20vdXNlcnMvYmlud2llZGVyaGllci9mb2xsb3dpbmd7L290aGVyX3VzZXJ9XCIsXG4gICAgICAgIFwiZ2lzdHNfdXJsXCI6IFwiaHR0cHM6Ly9hcGkuZ2l0aHViLmNvbS91c2Vycy9iaW53aWVkZXJoaWVyL2dpc3Rzey9naXN0X2lkfVwiLFxuICAgICAgICBcInN0YXJyZWRfdXJsXCI6IFwiaHR0cHM6Ly9hcGkuZ2l0aHViLmNvbS91c2Vycy9iaW53aWVkZXJoaWVyL3N0YXJyZWR7L293bmVyfXsvcmVwb31cIixcbiAgICAgICAgXCJzdWJzY3JpcHRpb25zX3VybFwiOiBcImh0dHBzOi8vYXBpLmdpdGh1Yi5jb20vdXNlcnMvYmlud2llZGVyaGllci9zdWJzY3JpcHRpb25zXCIsXG4gICAgICAgIFwib3JnYW5pemF0aW9uc191cmxcIjogXCJodHRwczovL2FwaS5naXRodWIuY29tL3VzZXJzL2JpbndpZWRlcmhpZXIvb3Jnc1wiLFxuICAgICAgICBcInJlcG9zX3VybFwiOiBcImh0dHBzOi8vYXBpLmdpdGh1Yi5jb20vdXNlcnMvYmlud2llZGVyaGllci9yZXBvc1wiLFxuICAgICAgICBcImV2ZW50c191cmxcIjogXCJodHRwczovL2FwaS5naXRodWIuY29tL3VzZXJzL2JpbndpZWRlcmhpZXIvZXZlbnRzey9wcml2YWN5fVwiLFxuICAgICAgICBcInJlY2VpdmVkX2V2ZW50c191cmxcIjogXCJodHRwczovL2FwaS5naXRodWIuY29tL3VzZXJzL2JpbndpZWRlcmhpZXIvcmVjZWl2ZWRfZXZlbnRzXCIsXG4gICAgICAgIFwidHlwZVwiOiBcIlVzZXJcIixcbiAgICAgICAgXCJzaXRlX2FkbWluXCI6IGZhbHNlXG4gICAgICB9LFxuICAgICAgXCJyZXBvXCI6IHtcbiAgICAgICAgXCJpZFwiOiA0NzAyMTIwMDMsXG4gICAgICAgIFwibm9kZV9pZFwiOiBcIlJfa2dET0hBYmRvd1wiLFxuICAgICAgICBcIm5hbWVcIjogXCJkYWJibGVcIixcbiAgICAgICAgXCJmdWxsX25hbWVcIjogXCJiaW53aWVkZXJoaWVyL2RhYmJsZVwiLFxuICAgICAgICBcInByaXZhdGVcIjogZmFsc2UsXG4gICAgICAgIFwib3duZXJcIjoge1xuICAgICAgICAgIFwibG9naW5cIjogXCJiaW53aWVkZXJoaWVyXCIsXG4gICAgICAgICAgXCJpZFwiOiA2NjQ1OTcsXG4gICAgICAgICAgXCJub2RlX2lkXCI6IFwiTURRNlZYTmxjalkyTkRVNU53PT1cIixcbiAgICAgICAgICBcImF2YXRhcl91cmxcIjogXCJodHRwczovL2F2YXRhcnMuZ2l0aHVidXNlcmNvbnRlbnQuY29tL3UvNjY0NTk3P3Y9NFwiLFxuICAgICAgICAgIFwiZ3JhdmF0YXJfaWRcIjogXCJcIixcbiAgICAgICAgICBcInVybFwiOiBcImh0dHBzOi8vYXBpLmdpdGh1Yi5jb20vdXNlcnMvYmlud2llZGVyaGllclwiLFxuICAgICAgICAgIFwiaHRtbF91cmxcIjogXCJodHRwczovL2dpdGh1Yi5jb20vYmlud2llZGVyaGllclwiLFxuICAgICAgICAgIFwiZm9sbG93ZXJzX3VybFwiOiBcImh0dHBzOi8vYXBpLmdpdGh1Yi5jb20vdXNlcnMvYmlud2llZGVyaGllci9mb2xsb3dlcnNcIixcbiAgICAgICAgICBcImZvbGxvd2luZ191cmxcIjogXCJodHRwczovL2FwaS5naXRodWIuY29tL3VzZXJzL2JpbndpZWRlcmhpZXIvZm9sbG93aW5ney9vdGhlcl91c2VyfVwiLFxuICAgICAgICAgIFwiZ2lzdHNfdXJsXCI6IFwiaHR0cHM6Ly9hcGkuZ2l0aHViLmNvbS91c2Vycy9iaW53aWVkZXJoaWVyL2dpc3Rzey9naXN0X2lkfVwiLFxuICAgICAgICAgIFwic3RhcnJlZF91cmxcIjogXCJodHRwczovL2FwaS5naXRodWIuY29tL3VzZXJzL2JpbndpZWRlcmhpZXIvc3RhcnJlZHsvb3duZXJ9ey9yZXBvfVwiLFxuICAgICAgICAgIFwic3Vic2NyaXB0aW9uc191cmxcIjogXCJodHRwczovL2FwaS5naXRodWIuY29tL3VzZXJzL2JpbndpZWRlcmhpZXIvc3Vic2NyaXB0aW9uc1wiLFxuICAgICAgICAgIFwib3JnYW5pemF0aW9uc191cmxcIjogXCJodHRwczovL2FwaS5naXRodWIuY29tL3VzZXJzL2JpbndpZWRlcmhpZXIvb3Jnc1wiLFxuICAgICAgICAgIFwicmVwb3NfdXJsXCI6IFwiaHR0cHM6Ly9hcGkuZ2l0aHViLmNvbS91c2Vycy9iaW53aWVkZXJoaWVyL3JlcG9zXCIsXG4gICAgICAgICAgXCJldmVudHNfdXJsXCI6IFwiaHR0cHM6Ly9hcGkuZ2l0aHViLmNvbS91c2Vycy9iaW53aWVkZXJoaWVyL2V2ZW50c3svcHJpdmFjeX1cIixcbiAgICAgICAgICBcInJlY2VpdmVkX2V2ZW50c191cmxcIjogXCJodHRwczovL2FwaS5naXRodWIuY29tL3VzZXJzL2JpbndpZWRlcmhpZXIvcmVjZWl2ZWRfZXZlbnRzXCIsXG4gICAgICAgICAgXCJ0eXBlXCI6IFwiVXNlclwiLFxuICAgICAgICAgIFwic2l0ZV9hZG1pblwiOiBmYWxzZVxuICAgICAgICB9LFxuICAgICAgICBcImh0bWxfdXJsXCI6IFwiaHR0cHM6Ly9naXRodWIuY29tL2JpbndpZWRlcmhpZXIvZGFiYmxlXCIsXG4gICAgICAgIFwiZGVzY3JpcHRpb25cIjogXCJBIHJlcG8gZm9yIGRhYmJsaW5nXCIsXG4gICAgICAgIFwiZm9ya1wiOiBmYWxzZSxcbiAgICAgICAgXCJ1cmxcIjogXCJodHRwczovL2FwaS5naXRodWIuY29tL3JlcG9zL2JpbndpZWRlcmhpZXIvZGFiYmxlXCIsXG4gICAgICAgIFwiZm9ya3NfdXJsXCI6IFwiaHR0cHM6Ly9hcGkuZ2l0aHViLmNvbS9yZXBvcy9iaW53aWVkZXJoaWVyL2RhYmJsZS9mb3Jrc1wiLFxuICAgICAgICBcImtleXNfdXJsXCI6IFwiaHR0cHM6Ly9hcGkuZ2l0aHViLmNvbS9yZXBvcy9iaW53aWVkZXJoaWVyL2RhYmJsZS9rZXlzey9rZXlfaWR9XCIsXG4gICAgICAgIFwiY29sbGFib3JhdG9yc191cmxcIjogXCJodHRwczovL2FwaS5naXRodWIuY29tL3JlcG9zL2JpbndpZWRlcmhpZXIvZGFiYmxlL2NvbGxhYm9yYXRvcnN7L2NvbGxhYm9yYXRvcn1cIixcbiAgICAgICAgXCJ0ZWFtc191cmxcIjogXCJodHRwczovL2FwaS5naXRodWIuY29tL3JlcG9zL2JpbndpZWRlcmhpZXIvZGFiYmxlL3RlYW1zXCIsXG4gICAgICAgIFwiaG9va3NfdXJsXCI6IFwiaHR0cHM6Ly9hcGkuZ2l0aHViLmNvbS9yZXBvcy9iaW53aWVkZXJoaWVyL2RhYmJsZS9ob29rc1wiLFxuICAgICAgICBcImlzc3VlX2V2ZW50c191cmxcIjogXCJodHRwczovL2FwaS5naXRodWIuY29tL3JlcG9zL2JpbndpZWRlcmhpZXIvZGFiYmxlL2lzc3Vlcy9ldmVudHN7L251bWJlcn1cIixcbiAgICAgICAgXCJldmVudHNfdXJsXCI6IFwiaHR0cHM6Ly9hcGkuZ2l0aHViLmNvbS9yZXBvcy9iaW53aWVkZXJoaWVyL2RhYmJsZS9ldmVudHNcIixcbiAgICAgICAgXCJhc3NpZ25lZXNfdXJsXCI6IFwiaHR0cHM6Ly9hcGkuZ2l0aHViLmNvbS9yZXBvcy9iaW53aWVkZXJoaWVyL2RhYmJsZS9hc3NpZ25lZXN7L3VzZXJ9XCIsXG4gICAgICAgIFwiYnJhbmNoZXNfdXJsXCI6IFwiaHR0cHM6Ly9hcGkuZ2l0aHViLmNvbS9yZXBvcy9iaW53aWVkZXJoaWVyL2RhYmJsZS9icmFuY2hlc3svYnJhbmNofVwiLFxuICAgICAgICBcInRhZ3NfdXJsXCI6IFwiaHR0cHM6Ly9hcGkuZ2l0aHViLmNvbS9yZXBvcy9iaW53aWVkZXJoaWVyL2RhYmJsZS90YWdzXCIsXG4gICAgICAgIFwiYmxvYnNfdXJsXCI6IFwiaHR0cHM6Ly9hcGkuZ2l0aHViLmNvbS9yZXBvcy9iaW53aWVkZXJoaWVyL2RhYmJsZS9naXQvYmxvYnN7L3NoYX1cIixcbiAgICAgICAgXCJnaXRfdGFnc191cmxcIjogXCJodHRwczovL2FwaS5naXRodWIuY29tL3JlcG9zL2JpbndpZWRlcmhpZXIvZGFiYmxlL2dpdC90YWdzey9zaGF9XCIsXG4gICAgICAgIFwiZ2l0X3JlZnNfdXJsXCI6IFwiaHR0cHM6Ly9hcGkuZ2l0aHViLmNvbS9yZXBvcy9iaW53aWVkZXJoaWVyL2RhYmJsZS9naXQvcmVmc3svc2hhfVwiLFxuICAgICAgICBcInRyZWVzX3VybFwiOiBcImh0dHBzOi8vYXBpLmdpdGh1Yi5jb20vcmVwb3MvYmlud2llZGVyaGllci9kYWJibGUvZ2l0L3RyZWVzey9zaGF9XCIsXG4gICAgICAgIFwic3RhdHVzZXNfdXJsXCI6IFwiaHR0cHM6Ly9hcGkuZ2l0aHViLmNvbS9yZXBvcy9iaW53aWVkZXJoaWVyL2RhYmJsZS9zdGF0dXNlcy97c2hhfVwiLFxuICAgICAgICBcImxhbmd1YWdlc191cmxcIjogXCJodHRwczovL2FwaS5naXRodWIuY29tL3JlcG9zL2JpbndpZWRlcmhpZXIvZGFiYmxlL2xhbmd1YWdlc1wiLFxuICAgICAgICBcInN0YXJnYXplcnNfdXJsXCI6IFwiaHR0cHM6Ly9hcGkuZ2l0aHViLmNvbS9yZXBvcy9iaW53aWVkZXJoaWVyL2RhYmJsZS9zdGFyZ2F6ZXJzXCIsXG4gICAgICAgIFwiY29udHJpYnV0b3JzX3VybFwiOiBcImh0dHBzOi8vYXBpLmdpdGh1Yi5jb20vcmVwb3MvYmlud2llZGVyaGllci9kYWJibGUvY29udHJpYnV0b3JzXCIsXG4gICAgICAgIFwic3Vic2NyaWJlcnNfdXJsXCI6IFwiaHR0cHM6Ly9hcGkuZ2l0aHViLmNvbS9yZXBvcy9iaW53aWVkZXJoaWVyL2RhYmJsZS9zdWJzY3JpYmVyc1wiLFxuICAgICAgICBcInN1YnNjcmlwdGlvbl91cmxcIjogXCJodHRwczovL2FwaS5naXRodWIuY29tL3JlcG9zL2JpbndpZWRlcmhpZXIvZGFiYmxlL3N1YnNjcmlwdGlvblwiLFxuICAgICAgICBcImNvbW1pdHNfdXJsXCI6IFwiaHR0cHM6Ly9hcGkuZ2l0aHViLmNvbS9yZXBvcy9iaW53aWVkZXJoaWVyL2RhYmJsZS9jb21taXRzey9zaGF9XCIsXG4gICAgICAgIFwiZ2l0X2NvbW1pdHNfdXJsXCI6IFwiaHR0cHM6Ly9hcGkuZ2l0aHViLmNvbS9yZXBvcy9iaW53aWVkZXJoaWVyL2RhYmJsZS9naXQvY29tbWl0c3svc2hhfVwiLFxuICAgICAgICBcImNvbW1lbnRzX3VybFwiOiBcImh0dHBzOi8vYXBpLmdpdGh1Yi5jb20vcmVwb3MvYmlud2llZGVyaGllci9kYWJibGUvY29tbWVudHN7L251bWJlcn1cIixcbiAgICAgICAgXCJpc3N1ZV9jb21tZW50X3VybFwiOiBcImh0dHBzOi8vYXBpLmdpdGh1Yi5jb20vcmVwb3MvYmlud2llZGVyaGllci9kYWJibGUvaXNzdWVzL2NvbW1lbnRzey9udW1iZXJ9XCIsXG4gICAgICAgIFwiY29udGVudHNfdXJsXCI6IFwiaHR0cHM6Ly9hcGkuZ2l0aHViLmNvbS9yZXBvcy9iaW53aWVkZXJoaWVyL2RhYmJsZS9jb250ZW50cy97K3BhdGh9XCIsXG4gICAgICAgIFwiY29tcGFyZV91cmxcIjogXCJodHRwczovL2FwaS5naXRodWIuY29tL3JlcG9zL2JpbndpZWRlcmhpZXIvZGFiYmxlL2NvbXBhcmUve2Jhc2V9Li4ue2hlYWR9XCIsXG4gICAgICAgIFwibWVyZ2VzX3VybFwiOiBcImh0dHBzOi8vYXBpLmdpdGh1Yi5jb20vcmVwb3MvYmlud2llZGVyaGllci9kYWJibGUvbWVyZ2VzXCIsXG4gICAgICAgIFwiYXJjaGl2ZV91cmxcIjogXCJodHRwczovL2FwaS5naXRodWIuY29tL3JlcG9zL2JpbndpZWRlcmhpZXIvZGFiYmxlL3thcmNoaXZlX2Zvcm1hdH17L3JlZn1cIixcbiAgICAgICAgXCJkb3dubG9hZHNfdXJsXCI6IFwiaHR0cHM6Ly9hcGkuZ2l0aHViLmNvbS9yZXBvcy9iaW53aWVkZXJoaWVyL2RhYmJsZS9kb3dubG9hZHNcIixcbiAgICAgICAgXCJpc3N1ZXNfdXJsXCI6IFwiaHR0cHM6Ly9hcGkuZ2l0aHViLmNvbS9yZXBvcy9iaW53aWVkZXJoaWVyL2RhYmJsZS9pc3N1ZXN7L251bWJlcn1cIixcbiAgICAgICAgXCJwdWxsc191cmxcIjogXCJodHRwczovL2FwaS5naXRodWIuY29tL3JlcG9zL2JpbndpZWRlcmhpZXIvZGFiYmxlL3B1bGxzey9udW1iZXJ9XCIsXG4gICAgICAgIFwibWlsZXN0b25lc191cmxcIjogXCJodHRwczovL2FwaS5naXRodWIuY29tL3JlcG9zL2JpbndpZWRlcmhpZXIvZGFiYmxlL21pbGVzdG9uZXN7L251bWJlcn1cIixcbiAgICAgICAgXCJub3RpZmljYXRpb25zX3VybFwiOiBcImh0dHBzOi8vYXBpLmdpdGh1Yi5jb20vcmVwb3MvYmlud2llZGVyaGllci9kYWJibGUvbm90aWZpY2F0aW9uc3s/c2luY2UsYWxsLHBhcnRpY2lwYXRpbmd9XCIsXG4gICAgICAgIFwibGFiZWxzX3VybFwiOiBcImh0dHBzOi8vYXBpLmdpdGh1Yi5jb20vcmVwb3MvYmlud2llZGVyaGllci9kYWJibGUvbGFiZWxzey9uYW1lfVwiLFxuICAgICAgICBcInJlbGVhc2VzX3VybFwiOiBcImh0dHBzOi8vYXBpLmdpdGh1Yi5jb20vcmVwb3MvYmlud2llZGVyaGllci9kYWJibGUvcmVsZWFzZXN7L2lkfVwiLFxuICAgICAgICBcImRlcGxveW1lbnRzX3VybFwiOiBcImh0dHBzOi8vYXBpLmdpdGh1Yi5jb20vcmVwb3MvYmlud2llZGVyaGllci9kYWJibGUvZGVwbG95bWVudHNcIixcbiAgICAgICAgXCJjcmVhdGVkX2F0XCI6IFwiMjAyMi0wMy0xNVQxNTowNjoxN1pcIixcbiAgICAgICAgXCJ1cGRhdGVkX2F0XCI6IFwiMjAyMi0wMy0xNVQxNTowNjoxN1pcIixcbiAgICAgICAgXCJwdXNoZWRfYXRcIjogXCIyMDI0LTAzLTIxVDAyOjUyOjEwWlwiLFxuICAgICAgICBcImdpdF91cmxcIjogXCJnaXQ6Ly9naXRodWIuY29tL2JpbndpZWRlcmhpZXIvZGFiYmxlLmdpdFwiLFxuICAgICAgICBcInNzaF91cmxcIjogXCJnaXRAZ2l0aHViLmNvbTpiaW53aWVkZXJoaWVyL2RhYmJsZS5naXRcIixcbiAgICAgICAgXCJjbG9uZV91cmxcIjogXCJodHRwczovL2dpdGh1Yi5jb20vYmlud2llZGVyaGllci9kYWJibGUuZ2l0XCIsXG4gICAgICAgIFwic3ZuX3VybFwiOiBcImh0dHBzOi8vZ2l0aHViLmNvbS9iaW53aWVkZXJoaWVyL2RhYmJsZVwiLFxuICAgICAgICBcImhvbWVwYWdlXCI6IG51bGwsXG4gICAgICAgIFwic2l6ZVwiOiAxLFxuICAgICAgICBcInN0YXJnYXplcnNfY291bnRcIjogMCxcbiAgICAgICAgXCJ3YXRjaGVyc19jb3VudFwiOiAwLFxuICAgICAgICBcImxhbmd1YWdlXCI6IG51bGwsXG4gICAgICAgIFwiaGFzX2lzc3Vlc1wiOiB0cnVlLFxuICAgICAgICBcImhhc19wcm9qZWN0c1wiOiB0cnVlLFxuICAgICAgICBcImhhc19kb3dubG9hZHNcIjogdHJ1ZSxcbiAgICAgICAgXCJoYXNfd2lraVwiOiB0cnVlLFxuICAgICAgICBcImhhc19wYWdlc1wiOiBmYWxzZSxcbiAgICAgICAgXCJoYXNfZGlzY3Vzc2lvbnNcIjogZmFsc2UsXG4gICAgICAgIFwiZm9ya3NfY291bnRcIjogMCxcbiAgICAgICAgXCJtaXJyb3JfdXJsXCI6IG51bGwsXG4gICAgICAgIFwiYXJjaGl2ZWRcIjogZmFsc2UsXG4gICAgICAgIFwiZGlzYWJsZWRcIjogZmFsc2UsXG4gICAgICAgIFwib3Blbl9pc3N1ZXNfY291bnRcIjogMSxcbiAgICAgICAgXCJsaWNlbnNlXCI6IG51bGwsXG4gICAgICAgIFwiYWxsb3dfZm9ya2luZ1wiOiB0cnVlLFxuICAgICAgICBcImlzX3RlbXBsYXRlXCI6IGZhbHNlLFxuICAgICAgICBcIndlYl9jb21taXRfc2lnbm9mZl9yZXF1aXJlZFwiOiBmYWxzZSxcbiAgICAgICAgXCJ0b3BpY3NcIjogW10sXG4gICAgICAgIFwidmlzaWJpbGl0eVwiOiBcInB1YmxpY1wiLFxuICAgICAgICBcImZvcmtzXCI6IDAsXG4gICAgICAgIFwib3Blbl9pc3N1ZXNcIjogMSxcbiAgICAgICAgXCJ3YXRjaGVyc1wiOiAwLFxuICAgICAgICBcImRlZmF1bHRfYnJhbmNoXCI6IFwibWFpblwiLFxuICAgICAgICBcImFsbG93X3NxdWFzaF9tZXJnZVwiOiB0cnVlLFxuICAgICAgICBcImFsbG93X21lcmdlX2NvbW1pdFwiOiB0cnVlLFxuICAgICAgICBcImFsbG93X3JlYmFzZV9tZXJnZVwiOiB0cnVlLFxuICAgICAgICBcImFsbG93X2F1dG9fbWVyZ2VcIjogZmFsc2UsXG4gICAgICAgIFwiZGVsZXRlX2JyYW5jaF9vbl9tZXJnZVwiOiBmYWxzZSxcbiAgICAgICAgXCJhbGxvd191cGRhdGVfYnJhbmNoXCI6IGZhbHNlLFxuICAgICAgICBcInVzZV9zcXVhc2hfcHJfdGl0bGVfYXNfZGVmYXVsdFwiOiBmYWxzZSxcbiAgICAgICAgXCJzcXVhc2hfbWVyZ2VfY29tbWl0X21lc3NhZ2VcIjogXCJDT01NSVRfTUVTU0FHRVNcIixcbiAgICAgICAgXCJzcXVhc2hfbWVyZ2VfY29tbWl0X3RpdGxlXCI6IFwiQ09NTUlUX09SX1BSX1RJVExFXCIsXG4gICAgICAgIFwibWVyZ2VfY29tbWl0X21lc3NhZ2VcIjogXCJQUl9USVRMRVwiLFxuICAgICAgICBcIm1lcmdlX2NvbW1pdF90aXRsZVwiOiBcIk1FUkdFX01FU1NBR0VcIlxuICAgICAgfVxuICAgIH0sXG4gICAgXCJiYXNlXCI6IHtcbiAgICAgIFwibGFiZWxcIjogXCJiaW53aWVkZXJoaWVyOm1haW5cIixcbiAgICAgIFwicmVmXCI6IFwibWFpblwiLFxuICAgICAgXCJzaGFcIjogXCI3MmQ5MzFhMjBiYjgzZDEyM2FiNDVhY2NhZjc2MTE1MGM4YjAxMjExXCIsXG4gICAgICBcInVzZXJcIjoge1xuICAgICAgICBcImxvZ2luXCI6IFwiYmlud2llZGVyaGllclwiLFxuICAgICAgICBcImlkXCI6IDY2NDU5NyxcbiAgICAgICAgXCJub2RlX2lkXCI6IFwiTURRNlZYTmxjalkyTkRVNU53PT1cIixcbiAgICAgICAgXCJhdmF0YXJfdXJsXCI6IFwiaHR0cHM6Ly9hdmF0YXJzLmdpdGh1YnVzZXJjb250ZW50LmNvbS91LzY2NDU5Nz92PTRcIixcbiAgICAgICAgXCJncmF2YXRhcl9pZFwiOiBcIlwiLFxuICAgICAgICBcInVybFwiOiBcImh0dHBzOi8vYXBpLmdpdGh1Yi5jb20vdXNlcnMvYmlud2llZGVyaGllclwiLFxuICAgICAgICBcImh0bWxfdXJsXCI6IFwiaHR0cHM6Ly9naXRodWIuY29tL2JpbndpZWRlcmhpZXJcIixcbiAgICAgICAgXCJmb2xsb3dlcnNfdXJsXCI6IFwiaHR0cHM6Ly9hcGkuZ2l0aHViLmNvbS91c2Vycy9iaW53aWVkZXJoaWVyL2ZvbGxvd2Vyc1wiLFxuICAgICAgICBcImZvbGxvd2luZ191cmxcIjogXCJodHRwczovL2FwaS5naXRodWIuY29tL3VzZXJzL2JpbndpZWRlcmhpZXIvZm9sbG93aW5ney9vdGhlcl91c2VyfVwiLFxuICAgICAgICBcImdpc3RzX3VybFwiOiBcImh0dHBzOi8vYXBpLmdpdGh1Yi5jb20vdXNlcnMvYmlud2llZGVyaGllci9naXN0c3svZ2lzdF9pZH1cIixcbiAgICAgICAgXCJzdGFycmVkX3VybFwiOiBcImh0dHBzOi8vYXBpLmdpdGh1Yi5jb20vdXNlcnMvYmlud2llZGVyaGllci9zdGFycmVkey9vd25lcn17L3JlcG99XCIsXG4gICAgICAgIFwic3Vic2NyaXB0aW9uc191cmxcIjogXCJodHRwczovL2FwaS5naXRodWIuY29tL3VzZXJzL2JpbndpZWRlcmhpZXIvc3Vic2NyaXB0aW9uc1wiLFxuICAgICAgICBcIm9yZ2FuaXphdGlvbnNfdXJsXCI6IFwiaHR0cHM6Ly9hcGkuZ2l0aHViLmNvbS91c2Vycy9iaW53aWVkZXJoaWVyL29yZ3NcIixcbiAgICAgICAgXCJyZXBvc191cmxcIjogXCJodHRwczovL2FwaS5naXRodWIuY29tL3VzZXJzL2JpbndpZWRlcmhpZXIvcmVwb3NcIixcbiAgICAgICAgXCJldmVudHNfdXJsXCI6IFwiaHR0cHM6Ly9hcGkuZ2l0aHViLmNvbS91c2Vycy9iaW53aWVkZXJoaWVyL2V2ZW50c3svcHJpdmFjeX1cIixcbiAgICAgICAgXCJyZWNlaXZlZF9ldmVudHNfdXJsXCI6IFwiaHR0cHM6Ly9hcGkuZ2l0aHViLmNvbS91c2Vycy9iaW53aWVkZXJoaWVyL3JlY2VpdmVkX2V2ZW50c1wiLFxuICAgICAgICBcInR5cGVcIjogXCJVc2VyXCIsXG4gICAgICAgIFwic2l0ZV9hZG1pblwiOiBmYWxzZVxuICAgICAgfSxcbiAgICAgIFwicmVwb1wiOiB7XG4gICAgICAgIFwiaWRcIjogNDcwMjEyMDAzLFxuICAgICAgICBcIm5vZGVfaWRcIjogXCJSX2tnRE9IQWJkb3dcIixcbiAgICAgICAgXCJuYW1lXCI6IFwiZGFiYmxlXCIsXG4gICAgICAgIFwiZnVsbF9uYW1lXCI6IFwiYmlud2llZGVyaGllci9kYWJibGVcIixcbiAgICAgICAgXCJwcml2YXRlXCI6IGZhbHNlLFxuICAgICAgICBcIm93bmVyXCI6IHtcbiAgICAgICAgICBcImxvZ2luXCI6IFwiYmlud2llZGVyaGllclwiLFxuICAgICAgICAgIFwiaWRcIjogNjY0NTk3LFxuICAgICAgICAgIFwibm9kZV9pZFwiOiBcIk1EUTZWWE5sY2pZMk5EVTVOdz09XCIsXG4gICAgICAgICAgXCJhdmF0YXJfdXJsXCI6IFwiaHR0cHM6Ly9hdmF0YXJzLmdpdGh1YnVzZXJjb250ZW50LmNvbS91LzY2NDU5Nz92PTRcIixcbiAgICAgICAgICBcImdyYXZhdGFyX2lkXCI6IFwiXCIsXG4gICAgICAgICAgXCJ1cmxcIjogXCJodHRwczovL2FwaS5naXRodWIuY29tL3VzZXJzL2JpbndpZWRlcmhpZXJcIixcbiAgICAgICAgICBcImh0bWxfdXJsXCI6IFwiaHR0cHM6Ly9naXRodWIuY29tL2JpbndpZWRlcmhpZXJcIixcbiAgICAgICAgICBcImZvbGxvd2Vyc191cmxcIjogXCJodHRwczovL2FwaS5naXRodWIuY29tL3VzZXJzL2JpbndpZWRlcmhpZXIvZm9sbG93ZXJzXCIsXG4gICAgICAgICAgXCJmb2xsb3dpbmdfdXJsXCI6IFwiaHR0cHM6Ly9hcGkuZ2l0aHViLmNvbS91c2Vycy9iaW53aWVkZXJoaWVyL2ZvbGxvd2luZ3svb3RoZXJfdXNlcn1cIixcbiAgICAgICAgICBcImdpc3RzX3VybFwiOiBcImh0dHBzOi8vYXBpLmdpdGh1Yi5jb20vdXNlcnMvYmlud2llZGVyaGllci9naXN0c3svZ2lzdF9pZH1cIixcbiAgICAgICAgICBcInN0YXJyZWRfdXJsXCI6IFwiaHR0cHM6Ly9hcGkuZ2l0aHViLmNvbS91c2Vycy9iaW53aWVkZXJoaWVyL3N0YXJyZWR7L293bmVyfXsvcmVwb31cIixcbiAgICAgICAgICBcInN1YnNjcmlwdGlvbnNfdXJsXCI6IFwiaHR0cHM6Ly9hcGkuZ2l0aHViLmNvbS91c2Vycy9iaW53aWVkZXJoaWVyL3N1YnNjcmlwdGlvbnNcIixcbiAgICAgICAgICBcIm9yZ2FuaXphdGlvbnNfdXJsXCI6IFwiaHR0cHM6Ly9hcGkuZ2l0aHViLmNvbS91c2Vycy9iaW53aWVkZXJoaWVyL29yZ3NcIixcbiAgICAgICAgICBcInJlcG9zX3VybFwiOiBcImh0dHBzOi8vYXBpLmdpdGh1Yi5jb20vdXNlcnMvYmlud2llZGVyaGllci9yZXBvc1wiLFxuICAgICAgICAgIFwiZXZlbnRzX3VybFwiOiBcImh0dHBzOi8vYXBpLmdpdGh1Yi5jb20vdXNlcnMvYmlud2llZGVyaGllci9ldmVudHN7L3ByaXZhY3l9XCIsXG4gICAgICAgICAgXCJyZWNlaXZlZF9ldmVudHNfdXJsXCI6IFwiaHR0cHM6Ly9hcGkuZ2l0aHViLmNvbS91c2Vycy9iaW53aWVkZXJoaWVyL3JlY2VpdmVkX2V2ZW50c1wiLFxuICAgICAgICAgIFwidHlwZVwiOiBcIlVzZXJcIixcbiAgICAgICAgICBcInNpdGVfYWRtaW5cIjogZmFsc2VcbiAgICAgICAgfSxcbiAgICAgICAgXCJodG1sX3VybFwiOiBcImh0dHBzOi8vZ2l0aHViLmNvbS9iaW53aWVkZXJoaWVyL2RhYmJsZVwiLFxuICAgICAgICBcImRlc2NyaXB0aW9uXCI6IFwiQSByZXBvIGZvciBkYWJibGluZ1wiLFxuICAgICAgICBcImZvcmtcIjogZmFsc2UsXG4gICAgICAgIFwidXJsXCI6IFwiaHR0cHM6Ly9hcGkuZ2l0aHViLmNvbS9yZXBvcy9iaW53aWVkZXJoaWVyL2RhYmJsZVwiLFxuICAgICAgICBcImZvcmtzX3VybFwiOiBcImh0dHBzOi8vYXBpLmdpdGh1Yi5jb20vcmVwb3MvYmlud2llZGVyaGllci9kYWJibGUvZm9ya3NcIixcbiAgICAgICAgXCJrZXlzX3VybFwiOiBcImh0dHBzOi8vYXBpLmdpdGh1Yi5jb20vcmVwb3MvYmlud2llZGVyaGllci9kYWJibGUva2V5c3sva2V5X2lkfVwiLFxuICAgICAgICBcImNvbGxhYm9yYXRvcnNfdXJsXCI6IFwiaHR0cHM6Ly9hcGkuZ2l0aHViLmNvbS9yZXBvcy9iaW53aWVkZXJoaWVyL2RhYmJsZS9jb2xsYWJvcmF0b3Jzey9jb2xsYWJvcmF0b3J9XCIsXG4gICAgICAgIFwidGVhbXNfdXJsXCI6IFwiaHR0cHM6Ly9hcGkuZ2l0aHViLmNvbS9yZXBvcy9iaW53aWVkZXJoaWVyL2RhYmJsZS90ZWFtc1wiLFxuICAgICAgICBcImhvb2tzX3VybFwiOiBcImh0dHBzOi8vYXBpLmdpdGh1Yi5jb20vcmVwb3MvYmlud2llZGVyaGllci9kYWJibGUvaG9va3NcIixcbiAgICAgICAgXCJpc3N1ZV9ldmVudHNfdXJsXCI6IFwiaHR0cHM6Ly9hcGkuZ2l0aHViLmNvbS9yZXBvcy9iaW53aWVkZXJoaWVyL2RhYmJsZS9pc3N1ZXMvZXZlbnRzey9udW1iZXJ9XCIsXG4gICAgICAgIFwiZXZlbnRzX3VybFwiOiBcImh0dHBzOi8vYXBpLmdpdGh1Yi5jb20vcmVwb3MvYmlud2llZGVyaGllci9kYWJibGUvZXZlbnRzXCIsXG4gICAgICAgIFwiYXNzaWduZWVzX3VybFwiOiBcImh0dHBzOi8vYXBpLmdpdGh1Yi5jb20vcmVwb3MvYmlud2llZGVyaGllci9kYWJibGUvYXNzaWduZWVzey91c2VyfVwiLFxuICAgICAgICBcImJyYW5jaGVzX3VybFwiOiBcImh0dHBzOi8vYXBpLmdpdGh1Yi5jb20vcmVwb3MvYmlud2llZGVyaGllci9kYWJibGUvYnJhbmNoZXN7L2JyYW5jaH1cIixcbiAgICAgICAgXCJ0YWdzX3VybFwiOiBcImh0dHBzOi8vYXBpLmdpdGh1Yi5jb20vcmVwb3MvYmlud2llZGVyaGllci9kYWJibGUvdGFnc1wiLFxuICAgICAgICBcImJsb2JzX3VybFwiOiBcImh0dHBzOi8vYXBpLmdpdGh1Yi5jb20vcmVwb3MvYmlud2llZGVyaGllci9kYWJibGUvZ2l0L2Jsb2Jzey9zaGF9XCIsXG4gICAgICAgIFwiZ2l0X3RhZ3NfdXJsXCI6IFwiaHR0cHM6Ly9hcGkuZ2l0aHViLmNvbS9yZXBvcy9iaW53aWVkZXJoaWVyL2RhYmJsZS9naXQvdGFnc3svc2hhfVwiLFxuICAgICAgICBcImdpdF9yZWZzX3VybFwiOiBcImh0dHBzOi8vYXBpLmdpdGh1Yi5jb20vcmVwb3MvYmlud2llZGVyaGllci9kYWJibGUvZ2l0L3JlZnN7L3NoYX1cIixcbiAgICAgICAgXCJ0cmVlc191cmxcIjogXCJodHRwczovL2FwaS5naXRodWIuY29tL3JlcG9zL2JpbndpZWRlcmhpZXIvZGFiYmxlL2dpdC90cmVlc3svc2hhfVwiLFxuICAgICAgICBcInN0YXR1c2VzX3VybFwiOiBcImh0dHBzOi8vYXBpLmdpdGh1Yi5jb20vcmVwb3MvYmlud2llZGVyaGllci9kYWJibGUvc3RhdHVzZXMve3NoYX1cIixcbiAgICAgICAgXCJsYW5ndWFnZXNfdXJsXCI6IFwiaHR0cHM6Ly9hcGkuZ2l0aHViLmNvbS9yZXBvcy9iaW53aWVkZXJoaWVyL2RhYmJsZS9sYW5ndWFnZXNcIixcbiAgICAgICAgXCJzdGFyZ2F6ZXJzX3VybFwiOiBcImh0dHBzOi8vYXBpLmdpdGh1Yi5jb20vcmVwb3MvYmlud2llZGVyaGllci9kYWJibGUvc3RhcmdhemVyc1wiLFxuICAgICAgICBcImNvbnRyaWJ1dG9yc191cmxcIjogXCJodHRwczovL2FwaS5naXRodWIuY29tL3JlcG9zL2JpbndpZWRlcmhpZXIvZGFiYmxlL2NvbnRyaWJ1dG9yc1wiLFxuICAgICAgICBcInN1YnNjcmliZXJzX3VybFwiOiBcImh0dHBzOi8vYXBpLmdpdGh1Yi5jb20vcmVwb3MvYmlud2llZGVyaGllci9kYWJibGUvc3Vic2NyaWJlcnNcIixcbiAgICAgICAgXCJzdWJzY3JpcHRpb25fdXJsXCI6IFwiaHR0cHM6Ly9hcGkuZ2l0aHViLmNvbS9yZXBvcy9iaW53aWVkZXJoaWVyL2RhYmJsZS9zdWJzY3JpcHRpb25cIixcbiAgICAgICAgXCJjb21taXRzX3VybFwiOiBcImh0dHBzOi8vYXBpLmdpdGh1Yi5jb20vcmVwb3MvYmlud2llZGVyaGllci9kYWJibGUvY29tbWl0c3svc2hhfVwiLFxuICAgICAgICBcImdpdF9jb21taXRzX3VybFwiOiBcImh0dHBzOi8vYXBpLmdpdGh1Yi5jb20vcmVwb3MvYmlud2llZGVyaGllci9kYWJibGUvZ2l0L2NvbW1pdHN7L3NoYX1cIixcbiAgICAgICAgXCJjb21tZW50c191cmxcIjogXCJodHRwczovL2FwaS5naXRodWIuY29tL3JlcG9zL2JpbndpZWRlcmhpZXIvZGFiYmxlL2NvbW1lbnRzey9udW1iZXJ9XCIsXG4gICAgICAgIFwiaXNzdWVfY29tbWVudF91cmxcIjogXCJodHRwczovL2FwaS5naXRodWIuY29tL3JlcG9zL2JpbndpZWRlcmhpZXIvZGFiYmxlL2lzc3Vlcy9jb21tZW50c3svbnVtYmVyfVwiLFxuICAgICAgICBcImNvbnRlbnRzX3VybFwiOiBcImh0dHBzOi8vYXBpLmdpdGh1Yi5jb20vcmVwb3MvYmlud2llZGVyaGllci9kYWJibGUvY29udGVudHMveytwYXRofVwiLFxuICAgICAgICBcImNvbXBhcmVfdXJsXCI6IFwiaHR0cHM6Ly9hcGkuZ2l0aHViLmNvbS9yZXBvcy9iaW53aWVkZXJoaWVyL2RhYmJsZS9jb21wYXJlL3tiYXNlfS4uLntoZWFkfVwiLFxuICAgICAgICBcIm1lcmdlc191cmxcIjogXCJodHRwczovL2FwaS5naXRodWIuY29tL3JlcG9zL2JpbndpZWRlcmhpZXIvZGFiYmxlL21lcmdlc1wiLFxuICAgICAgICBcImFyY2hpdmVfdXJsXCI6IFwiaHR0cHM6Ly9hcGkuZ2l0aHViLmNvbS9yZXBvcy9iaW53aWVkZXJoaWVyL2RhYmJsZS97YXJjaGl2ZV9mb3JtYXR9ey9yZWZ9XCIsXG4gICAgICAgIFwiZG93bmxvYWRzX3VybFwiOiBcImh0dHBzOi8vYXBpLmdpdGh1Yi5jb20vcmVwb3MvYmlud2llZGVyaGllci9kYWJibGUvZG93bmxvYWRzXCIsXG4gICAgICAgIFwiaXNzdWVzX3VybFwiOiBcImh0dHBzOi8vYXBpLmdpdGh1Yi5jb20vcmVwb3MvYmlud2llZGVyaGllci9kYWJibGUvaXNzdWVzey9udW1iZXJ9XCIsXG4gICAgICAgIFwicHVsbHNfdXJsXCI6IFwiaHR0cHM6Ly9hcGkuZ2l0aHViLmNvbS9yZXBvcy9iaW53aWVkZXJoaWVyL2RhYmJsZS9wdWxsc3svbnVtYmVyfVwiLFxuICAgICAgICBcIm1pbGVzdG9uZXNfdXJsXCI6IFwiaHR0cHM6Ly9hcGkuZ2l0aHViLmNvbS9yZXBvcy9iaW53aWVkZXJoaWVyL2RhYmJsZS9taWxlc3RvbmVzey9udW1iZXJ9XCIsXG4gICAgICAgIFwibm90aWZpY2F0aW9uc191cmxcIjogXCJodHRwczovL2FwaS5naXRodWIuY29tL3JlcG9zL2JpbndpZWRlcmhpZXIvZGFiYmxlL25vdGlmaWNhdGlvbnN7P3NpbmNlLGFsbCxwYXJ0aWNpcGF0aW5nfVwiLFxuICAgICAgICBcImxhYmVsc191cmxcIjogXCJodHRwczovL2FwaS5naXRodWIuY29tL3JlcG9zL2JpbndpZWRlcmhpZXIvZGFiYmxlL2xhYmVsc3svbmFtZX1cIixcbiAgICAgICAgXCJyZWxlYXNlc191cmxcIjogXCJodHRwczovL2FwaS5naXRodWIuY29tL3JlcG9zL2JpbndpZWRlcmhpZXIvZGFiYmxlL3JlbGVhc2Vzey9pZH1cIixcbiAgICAgICAgXCJkZXBsb3ltZW50c191cmxcIjogXCJodHRwczovL2FwaS5naXRodWIuY29tL3JlcG9zL2JpbndpZWRlcmhpZXIvZGFiYmxlL2RlcGxveW1lbnRzXCIsXG4gICAgICAgIFwiY3JlYXRlZF9hdFwiOiBcIjIwMjItMDMtMTVUMTU6MDY6MTdaXCIsXG4gICAgICAgIFwidXBkYXRlZF9hdFwiOiBcIjIwMjItMDMtMTVUMTU6MDY6MTdaXCIsXG4gICAgICAgIFwicHVzaGVkX2F0XCI6IFwiMjAyNC0wMy0yMVQwMjo1MjoxMFpcIixcbiAgICAgICAgXCJnaXRfdXJsXCI6IFwiZ2l0Oi8vZ2l0aHViLmNvbS9iaW53aWVkZXJoaWVyL2RhYmJsZS5naXRcIixcbiAgICAgICAgXCJzc2hfdXJsXCI6IFwiZ2l0QGdpdGh1Yi5jb206Ymlud2llZGVyaGllci9kYWJibGUuZ2l0XCIsXG4gICAgICAgIFwiY2xvbmVfdXJsXCI6IFwiaHR0cHM6Ly9naXRodWIuY29tL2JpbndpZWRlcmhpZXIvZGFiYmxlLmdpdFwiLFxuICAgICAgICBcInN2bl91cmxcIjogXCJodHRwczovL2dpdGh1Yi5jb20vYmlud2llZGVyaGllci9kYWJibGVcIixcbiAgICAgICAgXCJob21lcGFnZVwiOiBudWxsLFxuICAgICAgICBcInNpemVcIjogMSxcbiAgICAgICAgXCJzdGFyZ2F6ZXJzX2NvdW50XCI6IDAsXG4gICAgICAgIFwid2F0Y2hlcnNfY291bnRcIjogMCxcbiAgICAgICAgXCJsYW5ndWFnZVwiOiBudWxsLFxuICAgICAgICBcImhhc19pc3N1ZXNcIjogdHJ1ZSxcbiAgICAgICAgXCJoYXNfcHJvamVjdHNcIjogdHJ1ZSxcbiAgICAgICAgXCJoYXNfZG93bmxvYWRzXCI6IHRydWUsXG4gICAgICAgIFwiaGFzX3dpa2lcIjogdHJ1ZSxcbiAgICAgICAgXCJoYXNfcGFnZXNcIjogZmFsc2UsXG4gICAgICAgIFwiaGFzX2Rpc2N1c3Npb25zXCI6IGZhbHNlLFxuICAgICAgICBcImZvcmtzX2NvdW50XCI6IDAsXG4gICAgICAgIFwibWlycm9yX3VybFwiOiBudWxsLFxuICAgICAgICBcImFyY2hpdmVkXCI6IGZhbHNlLFxuICAgICAgICBcImRpc2FibGVkXCI6IGZhbHNlLFxuICAgICAgICBcIm9wZW5faXNzdWVzX2NvdW50XCI6IDEsXG4gICAgICAgIFwibGljZW5zZVwiOiBudWxsLFxuICAgICAgICBcImFsbG93X2ZvcmtpbmdcIjogdHJ1ZSxcbiAgICAgICAgXCJpc190ZW1wbGF0ZVwiOiBmYWxzZSxcbiAgICAgICAgXCJ3ZWJfY29tbWl0X3NpZ25vZmZfcmVxdWlyZWRcIjogZmFsc2UsXG4gICAgICAgIFwidG9waWNzXCI6IFtdLFxuICAgICAgICBcInZpc2liaWxpdHlcIjogXCJwdWJsaWNcIixcbiAgICAgICAgXCJmb3Jrc1wiOiAwLFxuICAgICAgICBcIm9wZW5faXNzdWVzXCI6IDEsXG4gICAgICAgIFwid2F0Y2hlcnNcIjogMCxcbiAgICAgICAgXCJkZWZhdWx0X2JyYW5jaFwiOiBcIm1haW5cIixcbiAgICAgICAgXCJhbGxvd19zcXVhc2hfbWVyZ2VcIjogdHJ1ZSxcbiAgICAgICAgXCJhbGxvd19tZXJnZV9jb21taXRcIjogdHJ1ZSxcbiAgICAgICAgXCJhbGxvd19yZWJhc2VfbWVyZ2VcIjogdHJ1ZSxcbiAgICAgICAgXCJhbGxvd19hdXRvX21lcmdlXCI6IGZhbHNlLFxuICAgICAgICBcImRlbGV0ZV9icmFuY2hfb25fbWVyZ2VcIjogZmFsc2UsXG4gICAgICAgIFwiYWxsb3dfdXBkYXRlX2JyYW5jaFwiOiBmYWxzZSxcbiAgICAgICAgXCJ1c2Vfc3F1YXNoX3ByX3RpdGxlX2FzX2RlZmF1bHRcIjogZmFsc2UsXG4gICAgICAgIFwic3F1YXNoX21lcmdlX2NvbW1pdF9tZXNzYWdlXCI6IFwiQ09NTUlUX01FU1NBR0VTXCIsXG4gICAgICAgIFwic3F1YXNoX21lcmdlX2NvbW1pdF90aXRsZVwiOiBcIkNPTU1JVF9PUl9QUl9USVRMRVwiLFxuICAgICAgICBcIm1lcmdlX2NvbW1pdF9tZXNzYWdlXCI6IFwiUFJfVElUTEVcIixcbiAgICAgICAgXCJtZXJnZV9jb21taXRfdGl0bGVcIjogXCJNRVJHRV9NRVNTQUdFXCJcbiAgICAgIH1cbiAgICB9LFxuICAgIFwiX2xpbmtzXCI6IHtcbiAgICAgIFwic2VsZlwiOiB7XG4gICAgICAgIFwiaHJlZlwiOiBcImh0dHBzOi8vYXBpLmdpdGh1Yi5jb20vcmVwb3MvYmlud2llZGVyaGllci9kYWJibGUvcHVsbHMvMVwiXG4gICAgICB9LFxuICAgICAgXCJodG1sXCI6IHtcbiAgICAgICAgXCJocmVmXCI6IFwiaHR0cHM6Ly9naXRodWIuY29tL2JpbndpZWRlcmhpZXIvZGFiYmxlL3B1bGwvMVwiXG4gICAgICB9LFxuICAgICAgXCJpc3N1ZVwiOiB7XG4gICAgICAgIFwiaHJlZlwiOiBcImh0dHBzOi8vYXBpLmdpdGh1Yi5jb20vcmVwb3MvYmlud2llZGVyaGllci9kYWJibGUvaXNzdWVzLzFcIlxuICAgICAgfSxcbiAgICAgIFwiY29tbWVudHNcIjoge1xuICAgICAgICBcImhyZWZcIjogXCJodHRwczovL2FwaS5naXRodWIuY29tL3JlcG9zL2JpbndpZWRlcmhpZXIvZGFiYmxlL2lzc3Vlcy8xL2NvbW1lbnRzXCJcbiAgICAgIH0sXG4gICAgICBcInJldmlld19jb21tZW50c1wiOiB7XG4gICAgICAgIFwiaHJlZlwiOiBcImh0dHBzOi8vYXBpLmdpdGh1Yi5jb20vcmVwb3MvYmlud2llZGVyaGllci9kYWJibGUvcHVsbHMvMS9jb21tZW50c1wiXG4gICAgICB9LFxuICAgICAgXCJyZXZpZXdfY29tbWVudFwiOiB7XG4gICAgICAgIFwiaHJlZlwiOiBcImh0dHBzOi8vYXBpLmdpdGh1Yi5jb20vcmVwb3MvYmlud2llZGVyaGllci9kYWJibGUvcHVsbHMvY29tbWVudHN7L251bWJlcn1cIlxuICAgICAgfSxcbiAgICAgIFwiY29tbWl0c1wiOiB7XG4gICAgICAgIFwiaHJlZlwiOiBcImh0dHBzOi8vYXBpLmdpdGh1Yi5jb20vcmVwb3MvYmlud2llZGVyaGllci9kYWJibGUvcHVsbHMvMS9jb21taXRzXCJcbiAgICAgIH0sXG4gICAgICBcInN0YXR1c2VzXCI6IHtcbiAgICAgICAgXCJocmVmXCI6IFwiaHR0cHM6Ly9hcGkuZ2l0aHViLmNvbS9yZXBvcy9iaW53aWVkZXJoaWVyL2RhYmJsZS9zdGF0dXNlcy81NzAzODQyY2M1NzE1ZWQxZTM1OGQyM2ViYjY5M2RiMDk3NDdhZTliXCJcbiAgICAgIH1cbiAgICB9LFxuICAgIFwiYXV0aG9yX2Fzc29jaWF0aW9uXCI6IFwiT1dORVJcIixcbiAgICBcImF1dG9fbWVyZ2VcIjogbnVsbCxcbiAgICBcImFjdGl2ZV9sb2NrX3JlYXNvblwiOiBudWxsLFxuICAgIFwibWVyZ2VkXCI6IGZhbHNlLFxuICAgIFwibWVyZ2VhYmxlXCI6IG51bGwsXG4gICAgXCJyZWJhc2VhYmxlXCI6IG51bGwsXG4gICAgXCJtZXJnZWFibGVfc3RhdGVcIjogXCJ1bmtub3duXCIsXG4gICAgXCJtZXJnZWRfYnlcIjogbnVsbCxcbiAgICBcImNvbW1lbnRzXCI6IDAsXG4gICAgXCJyZXZpZXdfY29tbWVudHNcIjogMCxcbiAgICBcIm1haW50YWluZXJfY2FuX21vZGlmeVwiOiBmYWxzZSxcbiAgICBcImNvbW1pdHNcIjogMSxcbiAgICBcImFkZGl0aW9uc1wiOiAxLFxuICAgIFwiZGVsZXRpb25zXCI6IDEsXG4gICAgXCJjaGFuZ2VkX2ZpbGVzXCI6IDFcbiAgfSxcbiAgXCJyZXBvc2l0b3J5XCI6IHtcbiAgICBcImlkXCI6IDQ3MDIxMjAwMyxcbiAgICBcIm5vZGVfaWRcIjogXCJSX2tnRE9IQWJkb3dcIixcbiAgICBcIm5hbWVcIjogXCJkYWJibGVcIixcbiAgICBcImZ1bGxfbmFtZVwiOiBcImJpbndpZWRlcmhpZXIvZGFiYmxlXCIsXG4gICAgXCJwcml2YXRlXCI6IGZhbHNlLFxuICAgIFwib3duZXJcIjoge1xuICAgICAgXCJsb2dpblwiOiBcImJpbndpZWRlcmhpZXJcIixcbiAgICAgIFwiaWRcIjogNjY0NTk3LFxuICAgICAgXCJub2RlX2lkXCI6IFwiTURRNlZYTmxjalkyTkRVNU53PT1cIixcbiAgICAgIFwiYXZhdGFyX3VybFwiOiBcImh0dHBzOi8vYXZhdGFycy5naXRodWJ1c2VyY29udGVudC5jb20vdS82NjQ1OTc/dj00XCIsXG4gICAgICBcImdyYXZhdGFyX2lkXCI6IFwiXCIsXG4gICAgICBcInVybFwiOiBcImh0dHBzOi8vYXBpLmdpdGh1Yi5jb20vdXNlcnMvYmlud2llZGVyaGllclwiLFxuICAgICAgXCJodG1sX3VybFwiOiBcImh0dHBzOi8vZ2l0aHViLmNvbS9iaW53aWVkZXJoaWVyXCIsXG4gICAgICBcImZvbGxvd2Vyc191cmxcIjogXCJodHRwczovL2FwaS5naXRodWIuY29tL3VzZXJzL2JpbndpZWRlcmhpZXIvZm9sbG93ZXJzXCIsXG4gICAgICBcImZvbGxvd2luZ191cmxcIjogXCJodHRwczovL2FwaS5naXRodWIuY29tL3VzZXJzL2JpbndpZWRlcmhpZXIvZm9sbG93aW5ney9vdGhlcl91c2VyfVwiLFxuICAgICAgXCJnaXN0c191cmxcIjogXCJodHRwczovL2FwaS5naXRodWIuY29tL3VzZXJzL2JpbndpZWRlcmhpZXIvZ2lzdHN7L2dpc3RfaWR9XCIsXG4gICAgICBcInN0YXJyZWRfdXJsXCI6IFwiaHR0cHM6Ly9hcGkuZ2l0aHViLmNvbS91c2Vycy9iaW53aWVkZXJoaWVyL3N0YXJyZWR7L293bmVyfXsvcmVwb31cIixcbiAgICAgIFwic3Vic2NyaXB0aW9uc191cmxcIjogXCJodHRwczovL2FwaS5naXRodWIuY29tL3VzZXJzL2JpbndpZWRlcmhpZXIvc3Vic2NyaXB0aW9uc1wiLFxuICAgICAgXCJvcmdhbml6YXRpb25zX3VybFwiOiBcImh0dHBzOi8vYXBpLmdpdGh1Yi5jb20vdXNlcnMvYmlud2llZGVyaGllci9vcmdzXCIsXG4gICAgICBcInJlcG9zX3VybFwiOiBcImh0dHBzOi8vYXBpLmdpdGh1Yi5jb20vdXNlcnMvYmlud2llZGVyaGllci9yZXBvc1wiLFxuICAgICAgXCJldmVudHNfdXJsXCI6IFwiaHR0cHM6Ly9hcGkuZ2l0aHViLmNvbS91c2Vycy9iaW53aWVkZXJoaWVyL2V2ZW50c3svcHJpdmFjeX1cIixcbiAgICAgIFwicmVjZWl2ZWRfZXZlbnRzX3VybFwiOiBcImh0dHBzOi8vYXBpLmdpdGh1Yi5jb20vdXNlcnMvYmlud2llZGVyaGllci9yZWNlaXZlZF9ldmVudHNcIixcbiAgICAgIFwidHlwZVwiOiBcIlVzZXJcIixcbiAgICAgIFwic2l0ZV9hZG1pblwiOiBmYWxzZVxuICAgIH0sXG4gICAgXCJodG1sX3VybFwiOiBcImh0dHBzOi8vZ2l0aHViLmNvbS9iaW53aWVkZXJoaWVyL2RhYmJsZVwiLFxuICAgIFwiZGVzY3JpcHRpb25cIjogXCJBIHJlcG8gZm9yIGRhYmJsaW5nXCIsXG4gICAgXCJmb3JrXCI6IGZhbHNlLFxuICAgIFwidXJsXCI6IFwiaHR0cHM6Ly9hcGkuZ2l0aHViLmNvbS9yZXBvcy9iaW53aWVkZXJoaWVyL2RhYmJsZVwiLFxuICAgIFwiZm9ya3NfdXJsXCI6IFwiaHR0cHM6Ly9hcGkuZ2l0aHViLmNvbS9yZXBvcy9iaW53aWVkZXJoaWVyL2RhYmJsZS9mb3Jrc1wiLFxuICAgIFwia2V5c191cmxcIjogXCJodHRwczovL2FwaS5naXRodWIuY29tL3JlcG9zL2JpbndpZWRlcmhpZXIvZGFiYmxlL2tleXN7L2tleV9pZH1cIixcbiAgICBcImNvbGxhYm9yYXRvcnNfdXJsXCI6IFwiaHR0cHM6Ly9hcGkuZ2l0aHViLmNvbS9yZXBvcy9iaW53aWVkZXJoaWVyL2RhYmJsZS9jb2xsYWJvcmF0b3Jzey9jb2xsYWJvcmF0b3J9XCIsXG4gICAgXCJ0ZWFtc191cmxcIjogXCJodHRwczovL2FwaS5naXRodWIuY29tL3JlcG9zL2JpbndpZWRlcmhpZXIvZGFiYmxlL3RlYW1zXCIsXG4gICAgXCJob29rc191cmxcIjogXCJodHRwczovL2FwaS5naXRodWIuY29tL3JlcG9zL2JpbndpZWRlcmhpZXIvZGFiYmxlL2hvb2tzXCIsXG4gICAgXCJpc3N1ZV9ldmVudHNfdXJsXCI6IFwiaHR0cHM6Ly9hcGkuZ2l0aHViLmNvbS9yZXBvcy9iaW53aWVkZXJoaWVyL2RhYmJsZS9pc3N1ZXMvZXZlbnRzey9udW1iZXJ9XCIsXG4gICAgXCJldmVudHNfdXJsXCI6IFwiaHR0cHM6Ly9hcGkuZ2l0aHViLmNvbS9yZXBvcy9iaW53aWVkZXJoaWVyL2RhYmJsZS9ldmVudHNcIixcbiAgICBcImFzc2lnbmVlc191cmxcIjogXCJodHRwczovL2FwaS5naXRodWIuY29tL3JlcG9zL2JpbndpZWRlcmhpZXIvZGFiYmxlL2Fzc2lnbmVlc3svdXNlcn1cIixcbiAgICBcImJyYW5jaGVzX3VybFwiOiBcImh0dHBzOi8vYXBpLmdpdGh1Yi5jb20vcmVwb3MvYmlud2llZGVyaGllci9kYWJibGUvYnJhbmNoZXN7L2JyYW5jaH1cIixcbiAgICBcInRhZ3NfdXJsXCI6IFwiaHR0cHM6Ly9hcGkuZ2l0aHViLmNvbS9yZXBvcy9iaW53aWVkZXJoaWVyL2RhYmJsZS90YWdzXCIsXG4gICAgXCJibG9ic191cmxcIjogXCJodHRwczovL2FwaS5naXRodWIuY29tL3JlcG9zL2JpbndpZWRlcmhpZXIvZGFiYmxlL2dpdC9ibG9ic3svc2hhfVwiLFxuICAgIFwiZ2l0X3RhZ3NfdXJsXCI6IFwiaHR0cHM6Ly9hcGkuZ2l0aHViLmNvbS9yZXBvcy9iaW53aWVkZXJoaWVyL2RhYmJsZS9naXQvdGFnc3svc2hhfVwiLFxuICAgIFwiZ2l0X3JlZnNfdXJsXCI6IFwiaHR0cHM6Ly9hcGkuZ2l0aHViLmNvbS9yZXBvcy9iaW53aWVkZXJoaWVyL2RhYmJsZS9naXQvcmVmc3svc2hhfVwiLFxuICAgIFwidHJlZXNfdXJsXCI6IFwiaHR0cHM6Ly9hcGkuZ2l0aHViLmNvbS9yZXBvcy9iaW53aWVkZXJoaWVyL2RhYmJsZS9naXQvdHJlZXN7L3NoYX1cIixcbiAgICBcInN0YXR1c2VzX3VybFwiOiBcImh0dHBzOi8vYXBpLmdpdGh1Yi5jb20vcmVwb3MvYmlud2llZGVyaGllci9kYWJibGUvc3RhdHVzZXMve3NoYX1cIixcbiAgICBcImxhbmd1YWdlc191cmxcIjogXCJodHRwczovL2FwaS5naXRodWIuY29tL3JlcG9zL2JpbndpZWRlcmhpZXIvZGFiYmxlL2xhbmd1YWdlc1wiLFxuICAgIFwic3RhcmdhemVyc191cmxcIjogXCJodHRwczovL2FwaS5naXRodWIuY29tL3JlcG9zL2JpbndpZWRlcmhpZXIvZGFiYmxlL3N0YXJnYXplcnNcIixcbiAgICBcImNvbnRyaWJ1dG9yc191cmxcIjogXCJodHRwczovL2FwaS5naXRodWIuY29tL3JlcG9zL2JpbndpZWRlcmhpZXIvZGFiYmxlL2NvbnRyaWJ1dG9yc1wiLFxuICAgIFwic3Vic2NyaWJlcnNfdXJsXCI6IFwiaHR0cHM6Ly9hcGkuZ2l0aHViLmNvbS9yZXBvcy9iaW53aWVkZXJoaWVyL2RhYmJsZS9zdWJzY3JpYmVyc1wiLFxuICAgIFwic3Vic2NyaXB0aW9uX3VybFwiOiBcImh0dHBzOi8vYXBpLmdpdGh1Yi5jb20vcmVwb3MvYmlud2llZGVyaGllci9kYWJibGUvc3Vic2NyaXB0aW9uXCIsXG4gICAgXCJjb21taXRzX3VybFwiOiBcImh0dHBzOi8vYXBpLmdpdGh1Yi5jb20vcmVwb3MvYmlud2llZGVyaGllci9kYWJibGUvY29tbWl0c3svc2hhfVwiLFxuICAgIFwiZ2l0X2NvbW1pdHNfdXJsXCI6IFwiaHR0cHM6Ly9hcGkuZ2l0aHViLmNvbS9yZXBvcy9iaW53aWVkZXJoaWVyL2RhYmJsZS9naXQvY29tbWl0c3svc2hhfVwiLFxuICAgIFwiY29tbWVudHNfdXJsXCI6IFwiaHR0cHM6Ly9hcGkuZ2l0aHViLmNvbS9yZXBvcy9iaW53aWVkZXJoaWVyL2RhYmJsZS9jb21tZW50c3svbnVtYmVyfVwiLFxuICAgIFwiaXNzdWVfY29tbWVudF91cmxcIjogXCJodHRwczovL2FwaS5naXRodWIuY29tL3JlcG9zL2JpbndpZWRlcmhpZXIvZGFiYmxlL2lzc3Vlcy9jb21tZW50c3svbnVtYmVyfVwiLFxuICAgIFwiY29udGVudHNfdXJsXCI6IFwiaHR0cHM6Ly9hcGkuZ2l0aHViLmNvbS9yZXBvcy9iaW53aWVkZXJoaWVyL2RhYmJsZS9jb250ZW50cy97K3BhdGh9XCIsXG4gICAgXCJjb21wYXJlX3VybFwiOiBcImh0dHBzOi8vYXBpLmdpdGh1Yi5jb20vcmVwb3MvYmlud2llZGVyaGllci9kYWJibGUvY29tcGFyZS97YmFzZX0uLi57aGVhZH1cIixcbiAgICBcIm1lcmdlc191cmxcIjogXCJodHRwczovL2FwaS5naXRodWIuY29tL3JlcG9zL2JpbndpZWRlcmhpZXIvZGFiYmxlL21lcmdlc1wiLFxuICAgIFwiYXJjaGl2ZV91cmxcIjogXCJodHRwczovL2FwaS5naXRodWIuY29tL3JlcG9zL2JpbndpZWRlcmhpZXIvZGFiYmxlL3thcmNoaXZlX2Zvcm1hdH17L3JlZn1cIixcbiAgICBcImRvd25sb2Fkc191cmxcIjogXCJodHRwczovL2FwaS5naXRodWIuY29tL3JlcG9zL2JpbndpZWRlcmhpZXIvZGFiYmxlL2Rvd25sb2Fkc1wiLFxuICAgIFwiaXNzdWVzX3VybFwiOiBcImh0dHBzOi8vYXBpLmdpdGh1Yi5jb20vcmVwb3MvYmlud2llZGVyaGllci9kYWJibGUvaXNzdWVzey9udW1iZXJ9XCIsXG4gICAgXCJwdWxsc191cmxcIjogXCJodHRwczovL2FwaS5naXRodWIuY29tL3JlcG9zL2JpbndpZWRlcmhpZXIvZGFiYmxlL3B1bGxzey9udW1iZXJ9XCIsXG4gICAgXCJtaWxlc3RvbmVzX3VybFwiOiBcImh0dHBzOi8vYXBpLmdpdGh1Yi5jb20vcmVwb3MvYmlud2llZGVyaGllci9kYWJibGUvbWlsZXN0b25lc3svbnVtYmVyfVwiLFxuICAgIFwibm90aWZpY2F0aW9uc191cmxcIjogXCJodHRwczovL2FwaS5naXRodWIuY29tL3JlcG9zL2JpbndpZWRlcmhpZXIvZGFiYmxlL25vdGlmaWNhdGlvbnN7P3NpbmNlLGFsbCxwYXJ0aWNpcGF0aW5nfVwiLFxuICAgIFwibGFiZWxzX3VybFwiOiBcImh0dHBzOi8vYXBpLmdpdGh1Yi5jb20vcmVwb3MvYmlud2llZGVyaGllci9kYWJibGUvbGFiZWxzey9uYW1lfVwiLFxuICAgIFwicmVsZWFzZXNfdXJsXCI6IFwiaHR0cHM6Ly9hcGkuZ2l0aHViLmNvbS9yZXBvcy9iaW53aWVkZXJoaWVyL2RhYmJsZS9yZWxlYXNlc3svaWR9XCIsXG4gICAgXCJkZXBsb3ltZW50c191cmxcIjogXCJodHRwczovL2FwaS5naXRodWIuY29tL3JlcG9zL2JpbndpZWRlcmhpZXIvZGFiYmxlL2RlcGxveW1lbnRzXCIsXG4gICAgXCJjcmVhdGVkX2F0XCI6IFwiMjAyMi0wMy0xNVQxNTowNjoxN1pcIixcbiAgICBcInVwZGF0ZWRfYXRcIjogXCIyMDIyLTAzLTE1VDE1OjA2OjE3WlwiLFxuICAgIFwicHVzaGVkX2F0XCI6IFwiMjAyNC0wMy0yMVQwMjo1MjoxMFpcIixcbiAgICBcImdpdF91cmxcIjogXCJnaXQ6Ly9naXRodWIuY29tL2JpbndpZWRlcmhpZXIvZGFiYmxlLmdpdFwiLFxuICAgIFwic3NoX3VybFwiOiBcImdpdEBnaXRodWIuY29tOmJpbndpZWRlcmhpZXIvZGFiYmxlLmdpdFwiLFxuICAgIFwiY2xvbmVfdXJsXCI6IFwiaHR0cHM6Ly9naXRodWIuY29tL2JpbndpZWRlcmhpZXIvZGFiYmxlLmdpdFwiLFxuICAgIFwic3ZuX3VybFwiOiBcImh0dHBzOi8vZ2l0aHViLmNvbS9iaW53aWVkZXJoaWVyL2RhYmJsZVwiLFxuICAgIFwiaG9tZXBhZ2VcIjogbnVsbCxcbiAgICBcInNpemVcIjogMSxcbiAgICBcInN0YXJnYXplcnNfY291bnRcIjogMCxcbiAgICBcIndhdGNoZXJzX2NvdW50XCI6IDAsXG4gICAgXCJsYW5ndWFnZVwiOiBudWxsLFxuICAgIFwiaGFzX2lzc3Vlc1wiOiB0cnVlLFxuICAgIFwiaGFzX3Byb2plY3RzXCI6IHRydWUsXG4gICAgXCJoYXNfZG93bmxvYWRzXCI6IHRydWUsXG4gICAgXCJoYXNfd2lraVwiOiB0cnVlLFxuICAgIFwiaGFzX3BhZ2VzXCI6IGZhbHNlLFxuICAgIFwiaGFzX2Rpc2N1c3Npb25zXCI6IGZhbHNlLFxuICAgIFwiZm9ya3NfY291bnRcIjogMCxcbiAgICBcIm1pcnJvcl91cmxcIjogbnVsbCxcbiAgICBcImFyY2hpdmVkXCI6IGZhbHNlLFxuICAgIFwiZGlzYWJsZWRcIjogZmFsc2UsXG4gICAgXCJvcGVuX2lzc3Vlc19jb3VudFwiOiAxLFxuICAgIFwibGljZW5zZVwiOiBudWxsLFxuICAgIFwiYWxsb3dfZm9ya2luZ1wiOiB0cnVlLFxuICAgIFwiaXNfdGVtcGxhdGVcIjogZmFsc2UsXG4gICAgXCJ3ZWJfY29tbWl0X3NpZ25vZmZfcmVxdWlyZWRcIjogZmFsc2UsXG4gICAgXCJ0b3BpY3NcIjogW10sXG4gICAgXCJ2aXNpYmlsaXR5XCI6IFwicHVibGljXCIsXG4gICAgXCJmb3Jrc1wiOiAwLFxuICAgIFwib3Blbl9pc3N1ZXNcIjogMSxcbiAgICBcIndhdGNoZXJzXCI6IDAsXG4gICAgXCJkZWZhdWx0X2JyYW5jaFwiOiBcIm1haW5cIlxuICB9LFxuICBcInNlbmRlclwiOiB7XG4gICAgXCJsb2dpblwiOiBcImJpbndpZWRlcmhpZXJcIixcbiAgICBcImlkXCI6IDY2NDU5NyxcbiAgICBcIm5vZGVfaWRcIjogXCJNRFE2VlhObGNqWTJORFU1Tnc9PVwiLFxuICAgIFwiYXZhdGFyX3VybFwiOiBcImh0dHBzOi8vYXZhdGFycy5naXRodWJ1c2VyY29udGVudC5jb20vdS82NjQ1OTc/dj00XCIsXG4gICAgXCJncmF2YXRhcl9pZFwiOiBcIlwiLFxuICAgIFwidXJsXCI6IFwiaHR0cHM6Ly9hcGkuZ2l0aHViLmNvbS91c2Vycy9iaW53aWVkZXJoaWVyXCIsXG4gICAgXCJodG1sX3VybFwiOiBcImh0dHBzOi8vZ2l0aHViLmNvbS9iaW53aWVkZXJoaWVyXCIsXG4gICAgXCJmb2xsb3dlcnNfdXJsXCI6IFwiaHR0cHM6Ly9hcGkuZ2l0aHViLmNvbS91c2Vycy9iaW53aWVkZXJoaWVyL2ZvbGxvd2Vyc1wiLFxuICAgIFwiZm9sbG93aW5nX3VybFwiOiBcImh0dHBzOi8vYXBpLmdpdGh1Yi5jb20vdXNlcnMvYmlud2llZGVyaGllci9mb2xsb3dpbmd7L290aGVyX3VzZXJ9XCIsXG4gICAgXCJnaXN0c191cmxcIjogXCJodHRwczovL2FwaS5naXRodWIuY29tL3VzZXJzL2JpbndpZWRlcmhpZXIvZ2lzdHN7L2dpc3RfaWR9XCIsXG4gICAgXCJzdGFycmVkX3VybFwiOiBcImh0dHBzOi8vYXBpLmdpdGh1Yi5jb20vdXNlcnMvYmlud2llZGVyaGllci9zdGFycmVkey9vd25lcn17L3JlcG99XCIsXG4gICAgXCJzdWJzY3JpcHRpb25zX3VybFwiOiBcImh0dHBzOi8vYXBpLmdpdGh1Yi5jb20vdXNlcnMvYmlud2llZGVyaGllci9zdWJzY3JpcHRpb25zXCIsXG4gICAgXCJvcmdhbml6YXRpb25zX3VybFwiOiBcImh0dHBzOi8vYXBpLmdpdGh1Yi5jb20vdXNlcnMvYmlud2llZGVyaGllci9vcmdzXCIsXG4gICAgXCJyZXBvc191cmxcIjogXCJodHRwczovL2FwaS5naXRodWIuY29tL3VzZXJzL2JpbndpZWRlcmhpZXIvcmVwb3NcIixcbiAgICBcImV2ZW50c191cmxcIjogXCJodHRwczovL2FwaS5naXRodWIuY29tL3VzZXJzL2JpbndpZWRlcmhpZXIvZXZlbnRzey9wcml2YWN5fVwiLFxuICAgIFwicmVjZWl2ZWRfZXZlbnRzX3VybFwiOiBcImh0dHBzOi8vYXBpLmdpdGh1Yi5jb20vdXNlcnMvYmlud2llZGVyaGllci9yZWNlaXZlZF9ldmVudHNcIixcbiAgICBcInR5cGVcIjogXCJVc2VyXCIsXG4gICAgXCJzaXRlX2FkbWluXCI6IGZhbHNlXG4gIH1cbn1cbiIsImNvbmZpZyI6eyJ0ZW1wbGF0ZSI6InRleHQiLCJmdWxsU2NyZWVuSFRNTCI6ZmFsc2UsImZ1bmN0aW9ucyI6WyJzcHJpZyJdLCJvcHRpb25zIjpbImxpdmUiXSwiaW5wdXRUeXBlIjoieWFtbCJ9fQ==)) +* Loops (e.g. `{{range .errors}}..{{end}}`, see [example](https://repeatit.io/#/share/eyJ0ZW1wbGF0ZSI6IlNldmVyZSBVUkxzOlxue3tyYW5nZSAuZXJyb3JzfX17e2lmIGVxIC5sZXZlbCBcInNldmVyZVwifX0tIHt7LnVybH19XG57e2VuZH19e3tlbmR9fSIsImlucHV0Ijoie1wiZm9vXCI6IFwiYmFyXCIsIFwiZXJyb3JzXCI6IFt7XCJsZXZlbFwiOiBcInNldmVyZVwiLCBcInVybFwiOiBcImh0dHBzOi8vc2V2ZXJlMS5jb21cIn0se1wibGV2ZWxcIjogXCJ3YXJuaW5nXCIsIFwidXJsXCI6IFwiaHR0cHM6Ly93YXJuaW5nLmNvbVwifSx7XCJsZXZlbFwiOiBcInNldmVyZVwiLCBcInVybFwiOiBcImh0dHBzOi8vc2V2ZXJlMi5jb21cIn1dfSIsImNvbmZpZyI6eyJ0ZW1wbGF0ZSI6InRleHQiLCJmdWxsU2NyZWVuSFRNTCI6ZmFsc2UsImZ1bmN0aW9ucyI6WyJzcHJpZyJdLCJvcHRpb25zIjpbImxpdmUiXSwiaW5wdXRUeXBlIjoieWFtbCJ9fQ==)) + +A good way to experiment with Go templates is the **[Go Template Playground](https://repeatit.io)**. It is _highly recommended_ to test +your templates there first ([example for Grafana alert](https://repeatit.io/#/share/eyJ0ZW1wbGF0ZSI6InRpdGxlPUdyYWZhbmErYWxlcnQ6K3t7LnRpdGxlfX0mbWVzc2FnZT17ey5tZXNzYWdlfX0iLCJpbnB1dCI6IntcbiAgXCJyZWNlaXZlclwiOiBcIm50ZnlcXFxcLmV4YW1wbGVcXFxcLmNvbS9hbGVydHNcIixcbiAgXCJzdGF0dXNcIjogXCJyZXNvbHZlZFwiLFxuICBcImFsZXJ0c1wiOiBbXG4gICAge1xuICAgICAgXCJzdGF0dXNcIjogXCJyZXNvbHZlZFwiLFxuICAgICAgXCJsYWJlbHNcIjoge1xuICAgICAgICBcImFsZXJ0bmFtZVwiOiBcIkxvYWQgYXZnIDE1bSB0b28gaGlnaFwiLFxuICAgICAgICBcImdyYWZhbmFfZm9sZGVyXCI6IFwiTm9kZSBhbGVydHNcIixcbiAgICAgICAgXCJpbnN0YW5jZVwiOiBcIjEwLjEwOC4wLjI6OTEwMFwiLFxuICAgICAgICBcImpvYlwiOiBcIm5vZGUtZXhwb3J0ZXJcIlxuICAgICAgfSxcbiAgICAgIFwiYW5ub3RhdGlvbnNcIjoge1xuICAgICAgICBcInN1bW1hcnlcIjogXCIxNW0gbG9hZCBhdmVyYWdlIHRvbyBoaWdoXCJcbiAgICAgIH0sXG4gICAgICBcInN0YXJ0c0F0XCI6IFwiMjAyNC0wMy0xNVQwMjoyODowMFpcIixcbiAgICAgIFwiZW5kc0F0XCI6IFwiMjAyNC0wMy0xNVQwMjo0MjowMFpcIixcbiAgICAgIFwiZ2VuZXJhdG9yVVJMXCI6IFwibG9jYWxob3N0OjMwMDAvYWxlcnRpbmcvZ3JhZmFuYS9OVzlvRHctNHovdmlld1wiLFxuICAgICAgXCJmaW5nZXJwcmludFwiOiBcImJlY2JmYjk0YmQ4MWVmNDhcIixcbiAgICAgIFwic2lsZW5jZVVSTFwiOiBcImxvY2FsaG9zdDozMDAwL2FsZXJ0aW5nL3NpbGVuY2UvbmV3P2FsZXJ0bWFuYWdlcj1ncmFmYW5hJm1hdGNoZXI9YWxlcnRuYW1lJTNETG9hZCthdmcrMTVtK3RvbytoaWdoJm1hdGNoZXI9Z3JhZmFuYV9mb2xkZXIlM0ROb2RlK2FsZXJ0cyZtYXRjaGVyPWluc3RhbmNlJTNEMTAuMTA4LjAuMiUzQTkxMDAmbWF0Y2hlcj1qb2IlM0Rub2RlLWV4cG9ydGVyXCIsXG4gICAgICBcImRhc2hib2FyZFVSTFwiOiBcIlwiLFxuICAgICAgXCJwYW5lbFVSTFwiOiBcIlwiLFxuICAgICAgXCJ2YWx1ZXNcIjoge1xuICAgICAgICBcIkJcIjogMTguOTgyMTEzMTQ0NzU4NzYsXG4gICAgICAgIFwiQ1wiOiAwXG4gICAgICB9LFxuICAgICAgXCJ2YWx1ZVN0cmluZ1wiOiBcIlsgdmFyPSdCJyBsYWJlbHM9e19fbmFtZV9fPW5vZGVfbG9hZDE1LCBpbnN0YW5jZT0xMC4xMDguMC4yOjkxMDAsIGpvYj1ub2RlLWV4cG9ydGVyfSB2YWx1ZT0xOC45ODIxMTMxNDQ3NTg3NiBdLCBbIHZhcj0nQycgbGFiZWxzPXtfX25hbWVfXz1ub2RlX2xvYWQxNSwgaW5zdGFuY2U9MTAuMTA4LjAuMjo5MTAwLCBqb2I9bm9kZS1leHBvcnRlcn0gdmFsdWU9MCBdXCJcbiAgICB9XG4gIF0sXG4gIFwiZ3JvdXBMYWJlbHNcIjoge1xuICAgIFwiYWxlcnRuYW1lXCI6IFwiTG9hZCBhdmcgMTVtIHRvbyBoaWdoXCIsXG4gICAgXCJncmFmYW5hX2ZvbGRlclwiOiBcIk5vZGUgYWxlcnRzXCJcbiAgfSxcbiAgXCJjb21tb25MYWJlbHNcIjoge1xuICAgIFwiYWxlcnRuYW1lXCI6IFwiTG9hZCBhdmcgMTVtIHRvbyBoaWdoXCIsXG4gICAgXCJncmFmYW5hX2ZvbGRlclwiOiBcIk5vZGUgYWxlcnRzXCIsXG4gICAgXCJpbnN0YW5jZVwiOiBcIjEwLjEwOC4wLjI6OTEwMFwiLFxuICAgIFwiam9iXCI6IFwibm9kZS1leHBvcnRlclwiXG4gIH0sXG4gIFwiY29tbW9uQW5ub3RhdGlvbnNcIjoge1xuICAgIFwic3VtbWFyeVwiOiBcIjE1bSBsb2FkIGF2ZXJhZ2UgdG9vIGhpZ2hcIlxuICB9LFxuICBcImV4dGVybmFsVVJMXCI6IFwibG9jYWxob3N0OjMwMDAvXCIsXG4gIFwidmVyc2lvblwiOiBcIjFcIixcbiAgXCJncm91cEtleVwiOiBcInt9OnthbGVydG5hbWU9XFxcIkxvYWQgYXZnIDE1bSB0b28gaGlnaFxcXCIsIGdyYWZhbmFfZm9sZGVyPVxcXCJOb2RlIGFsZXJ0c1xcXCJ9XCIsXG4gIFwidHJ1bmNhdGVkQWxlcnRzXCI6IDAsXG4gIFwib3JnSWRcIjogMSxcbiAgXCJ0aXRsZVwiOiBcIltSRVNPTFZFRF0gTG9hZCBhdmcgMTVtIHRvbyBoaWdoIE5vZGUgYWxlcnRzICgxMC4xMDguMC4yOjkxMDAgbm9kZS1leHBvcnRlcilcIixcbiAgXCJzdGF0ZVwiOiBcIm9rXCIsXG4gIFwibWVzc2FnZVwiOiBcIioqUmVzb2x2ZWQqKlxcblxcblZhbHVlOiBCPTE4Ljk4MjExMzE0NDc1ODc2LCBDPTBcXG5MYWJlbHM6XFxuIC0gYWxlcnRuYW1lID0gTG9hZCBhdmcgMTVtIHRvbyBoaWdoXFxuIC0gZ3JhZmFuYV9mb2xkZXIgPSBOb2RlIGFsZXJ0c1xcbiAtIGluc3RhbmNlID0gMTAuMTA4LjAuMjo5MTAwXFxuIC0gam9iID0gbm9kZS1leHBvcnRlclxcbkFubm90YXRpb25zOlxcbiAtIHN1bW1hcnkgPSAxNW0gbG9hZCBhdmVyYWdlIHRvbyBoaWdoXFxuU291cmNlOiBsb2NhbGhvc3Q6MzAwMC9hbGVydGluZy9ncmFmYW5hL05XOW9Edy00ei92aWV3XFxuU2lsZW5jZTogbG9jYWxob3N0OjMwMDAvYWxlcnRpbmcvc2lsZW5jZS9uZXc/YWxlcnRtYW5hZ2VyPWdyYWZhbmEmbWF0Y2hlcj1hbGVydG5hbWUlM0RMb2FkK2F2ZysxNW0rdG9vK2hpZ2gmbWF0Y2hlcj1ncmFmYW5hX2ZvbGRlciUzRE5vZGUrYWxlcnRzJm1hdGNoZXI9aW5zdGFuY2UlM0QxMC4xMDguMC4yJTNBOTEwMCZtYXRjaGVyPWpvYiUzRG5vZGUtZXhwb3J0ZXJcXG5cIlxufVxuIiwiY29uZmlnIjp7InRlbXBsYXRlIjoidGV4dCIsImZ1bGxTY3JlZW5IVE1MIjpmYWxzZSwiZnVuY3Rpb25zIjpbInNwcmlnIl0sIm9wdGlvbnMiOlsibGl2ZSJdLCJpbnB1dFR5cGUiOiJ5YW1sIn19)). + +!!! info + Please note that the Go templating language is quite terrible. My apologies for using it for this feature. It is the best option for Go-based + programs like ntfy. Stay calm and don't harm yourself or others in despair. **You can do it. I believe in you!** + +Here's an example for a Grafana alert: + +
+ ![notification with actions](static/img/android-screenshot-template.jpg){ width=500 } +
Grafana webhook, formatted using templates
+
+ +This was sent by configuring a webhook contact point in Grafana with the URL `https://nty.sh/mytpoic?tpl=1&t=%7B%7B.title%7D%7D&m=%7B%7Brange%20.alerts%7D%7D%7B%7B.annotations.summary%7D%7D%5Cn%5CnValues%3A%5Cn%7B%7Brange%20%24k%2C%24v%20%3A%3D%20.values%7D%7D-%20%7B%7B%24k%7D%7D%3D%7B%7B%24v%7D%7D%5Cn%7B%7Bend%7D%7D%7B%7Bend%7D%7D`. +The additional [URL encoding](https://www.urlencoder.org/) is necessary for Grafana, and may be required for other tools +too. Grafana then sent this JSON payload: + +``` +{"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\nAnnotations:\n - summary = 15m load average too high\nSource: localhost:3000/alerting/grafana/NW9oDw-4z/view\nSilence: 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\n"} +``` + +Here's an easier example with a shorter JSON payload. The example uses the `message`/`m` and `title`/`t` query parameters, +but obviously this also works with the corresponding `Message`/`Title` headers: + +=== "Command line (curl)" + ``` + # To use { and } in the URL without encoding, we need to turn of + # curl's globbing using --globoff + + curl \ + --globoff \ + -d '{"hostname": "philipp-pc", "error": {"level": "severe", "desc": "Disk has run out of space"}}' \ + 'ntfy.sh/mytopic?tpl=yes&t={{.hostname}}:+A+{{.error.level}}+error+has+occurred&m=Error+message:+{{.error.desc}}' + ``` === "HTTP" ``` http - POST /mytopic HTTP/1.1 + POST /mytopic?tpl=yes&t={{.hostname}}:+A+{{.error.level}}+error+has+occurred&m=Error+message:+{{.error.desc}} HTTP/1.1 Host: ntfy.sh - X-Message: Error message: ${error.desc} - X-Title: ${hostname}: A ${error.level} error has occurred - X-Template: yes {"hostname": "philipp-pc", "error": {"level": "severe", "desc": "Disk has run out of space"}} ``` -The example above would send a notification with a title "philipp-pc: A severe error has occurred" and a message "Error message: Disk has run out of space". - -For Grafana webhooks, you might find it helpful to use the headers `X-Title: Grafana alert: ${title}` and `X-Message: ${message}`. -Alternatively, you can include the params in the webhook URL. For example, by -appending `?template=yes&title=Grafana alert: ${title}&message=${message}` to the URL. - +The example above would send a notification with a title `philipp-pc: A severe error has occurred` and a message +`Error message: Disk has run out of space`. ## Publish as JSON _Supported on:_ :material-android: :material-apple: :material-firefox: diff --git a/docs/static/img/android-screenshot-template.jpg b/docs/static/img/android-screenshot-template.jpg new file mode 100644 index 0000000000000000000000000000000000000000..e37f7b1a513cc6095e85cc438c78f4eb34193dd9 GIT binary patch literal 124374 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

cz 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 H_ZIbU<*`8g literal 0 HcmV?d00001 From 5511812e309eb66f05e1ef28f6377f8ed3cc0844 Mon Sep 17 00:00:00 2001 From: binwiederhier Date: Sun, 24 Mar 2024 14:21:28 -0400 Subject: [PATCH 12/14] JSON templating --- docs/publish.md | 102 ++++++++++++++++++++++++++----- docs/releases.md | 2 +- docs/static/js/extra.js | 130 +++++++++++++++++++++------------------- go.mod | 3 - go.sum | 7 --- 5 files changed, 155 insertions(+), 89 deletions(-) diff --git a/docs/publish.md b/docs/publish.md index 4d54f77e..3076eb4c 100644 --- a/docs/publish.md +++ b/docs/publish.md @@ -938,8 +938,9 @@ Here's an example with a custom message, tags and a priority: file_get_contents('https://ntfy.sh/mywebhook/publish?message=Webhook+triggered&priority=high&tags=warning,skull'); ``` +## Message templating +_Supported on:_ :material-android: :material-apple: :material-firefox: -## JSON templating Templating lets you **format a JSON message body into human-friendly message and title text** using [Go templates](https://pkg.go.dev/text/template) (see tutorials [here](https://blog.gopheracademy.com/advent-2017/using-go-templates/), [here](https://www.digitalocean.com/community/tutorials/how-to-use-templates-in-go), and @@ -950,8 +951,8 @@ Instead of using a separate bridge program to parse the webhook body into the fo message and/or a templated title which will be populated based on the fields of the webhook body (so long as the webhook body is valid JSON). -Enable templating by setting the `X-Template` header (or its aliases `Template` or `tpl`) to `yes` or `1`, or (more appropriately -for webhooks) by setting the `?template=yes` query parameter. Then, include templates in your `message` and/or `title`, like so: +You can enable templating by setting the `X-Template` header (or its aliases `Template` or `tpl`) to `yes` or `1`, or (more appropriately +for webhooks) by setting the `?template=yes` query parameter. Then, include templates in your `message` and/or `title`, using the following stanzas (see [Go docs](https://pkg.go.dev/text/template) for detailed syntax): * Variables,, e.g. `{{.alert.title}}` or `An error occurred: {{.error.desc}}` * Conditionals (if/else, e.g. `{{if eq .action "opened"}}..{{else}}..{{end}}`, see [example](https://repeatit.io/#/share/eyJ0ZW1wbGF0ZSI6Ilt7ey5wdWxsX3JlcXVlc3QuaGVhZC5yZXBvLmZ1bGxfbmFtZX19XSBQdWxsIHJlcXVlc3Qge3tpZiBlcSAuYWN0aW9uIFwib3BlbmVkXCJ9fU9QRU5FRHt7ZWxzZX19Q0xPU0VEe3tlbmR9fToge3sucHVsbF9yZXF1ZXN0LnRpdGxlfX0iLCJpbnB1dCI6IntcbiAgXCJhY3Rpb25cIjogXCJvcGVuZWRcIixcbiAgXCJudW1iZXJcIjogMSxcbiAgXCJwdWxsX3JlcXVlc3RcIjoge1xuICAgIFwidXJsXCI6IFwiaHR0cHM6Ly9hcGkuZ2l0aHViLmNvbS9yZXBvcy9iaW53aWVkZXJoaWVyL2RhYmJsZS9wdWxscy8xXCIsXG4gICAgXCJpZFwiOiAxNzgzNDIwOTcyLFxuICAgIFwibm9kZV9pZFwiOiBcIlBSX2t3RE9IQWJkbzg1cVROZ3NcIixcbiAgICBcImh0bWxfdXJsXCI6IFwiaHR0cHM6Ly9naXRodWIuY29tL2JpbndpZWRlcmhpZXIvZGFiYmxlL3B1bGwvMVwiLFxuICAgIFwiZGlmZl91cmxcIjogXCJodHRwczovL2dpdGh1Yi5jb20vYmlud2llZGVyaGllci9kYWJibGUvcHVsbC8xLmRpZmZcIixcbiAgICBcInBhdGNoX3VybFwiOiBcImh0dHBzOi8vZ2l0aHViLmNvbS9iaW53aWVkZXJoaWVyL2RhYmJsZS9wdWxsLzEucGF0Y2hcIixcbiAgICBcImlzc3VlX3VybFwiOiBcImh0dHBzOi8vYXBpLmdpdGh1Yi5jb20vcmVwb3MvYmlud2llZGVyaGllci9kYWJibGUvaXNzdWVzLzFcIixcbiAgICBcIm51bWJlclwiOiAxLFxuICAgIFwic3RhdGVcIjogXCJvcGVuXCIsXG4gICAgXCJsb2NrZWRcIjogZmFsc2UsXG4gICAgXCJ0aXRsZVwiOiBcIkEgc2FtcGxlIFBSIGZyb20gUGhpbFwiLFxuICAgIFwidXNlclwiOiB7XG4gICAgICBcImxvZ2luXCI6IFwiYmlud2llZGVyaGllclwiLFxuICAgICAgXCJpZFwiOiA2NjQ1OTcsXG4gICAgICBcIm5vZGVfaWRcIjogXCJNRFE2VlhObGNqWTJORFU1Tnc9PVwiLFxuICAgICAgXCJhdmF0YXJfdXJsXCI6IFwiaHR0cHM6Ly9hdmF0YXJzLmdpdGh1YnVzZXJjb250ZW50LmNvbS91LzY2NDU5Nz92PTRcIixcbiAgICAgIFwiZ3JhdmF0YXJfaWRcIjogXCJcIixcbiAgICAgIFwidXJsXCI6IFwiaHR0cHM6Ly9hcGkuZ2l0aHViLmNvbS91c2Vycy9iaW53aWVkZXJoaWVyXCIsXG4gICAgICBcImh0bWxfdXJsXCI6IFwiaHR0cHM6Ly9naXRodWIuY29tL2JpbndpZWRlcmhpZXJcIixcbiAgICAgIFwiZm9sbG93ZXJzX3VybFwiOiBcImh0dHBzOi8vYXBpLmdpdGh1Yi5jb20vdXNlcnMvYmlud2llZGVyaGllci9mb2xsb3dlcnNcIixcbiAgICAgIFwiZm9sbG93aW5nX3VybFwiOiBcImh0dHBzOi8vYXBpLmdpdGh1Yi5jb20vdXNlcnMvYmlud2llZGVyaGllci9mb2xsb3dpbmd7L290aGVyX3VzZXJ9XCIsXG4gICAgICBcImdpc3RzX3VybFwiOiBcImh0dHBzOi8vYXBpLmdpdGh1Yi5jb20vdXNlcnMvYmlud2llZGVyaGllci9naXN0c3svZ2lzdF9pZH1cIixcbiAgICAgIFwic3RhcnJlZF91cmxcIjogXCJodHRwczovL2FwaS5naXRodWIuY29tL3VzZXJzL2JpbndpZWRlcmhpZXIvc3RhcnJlZHsvb3duZXJ9ey9yZXBvfVwiLFxuICAgICAgXCJzdWJzY3JpcHRpb25zX3VybFwiOiBcImh0dHBzOi8vYXBpLmdpdGh1Yi5jb20vdXNlcnMvYmlud2llZGVyaGllci9zdWJzY3JpcHRpb25zXCIsXG4gICAgICBcIm9yZ2FuaXphdGlvbnNfdXJsXCI6IFwiaHR0cHM6Ly9hcGkuZ2l0aHViLmNvbS91c2Vycy9iaW53aWVkZXJoaWVyL29yZ3NcIixcbiAgICAgIFwicmVwb3NfdXJsXCI6IFwiaHR0cHM6Ly9hcGkuZ2l0aHViLmNvbS91c2Vycy9iaW53aWVkZXJoaWVyL3JlcG9zXCIsXG4gICAgICBcImV2ZW50c191cmxcIjogXCJodHRwczovL2FwaS5naXRodWIuY29tL3VzZXJzL2JpbndpZWRlcmhpZXIvZXZlbnRzey9wcml2YWN5fVwiLFxuICAgICAgXCJyZWNlaXZlZF9ldmVudHNfdXJsXCI6IFwiaHR0cHM6Ly9hcGkuZ2l0aHViLmNvbS91c2Vycy9iaW53aWVkZXJoaWVyL3JlY2VpdmVkX2V2ZW50c1wiLFxuICAgICAgXCJ0eXBlXCI6IFwiVXNlclwiLFxuICAgICAgXCJzaXRlX2FkbWluXCI6IGZhbHNlXG4gICAgfSxcbiAgICBcImJvZHlcIjogbnVsbCxcbiAgICBcImNyZWF0ZWRfYXRcIjogXCIyMDI0LTAzLTIxVDAyOjUyOjA5WlwiLFxuICAgIFwidXBkYXRlZF9hdFwiOiBcIjIwMjQtMDMtMjFUMDI6NTI6MDlaXCIsXG4gICAgXCJjbG9zZWRfYXRcIjogbnVsbCxcbiAgICBcIm1lcmdlZF9hdFwiOiBudWxsLFxuICAgIFwibWVyZ2VfY29tbWl0X3NoYVwiOiBudWxsLFxuICAgIFwiYXNzaWduZWVcIjogbnVsbCxcbiAgICBcImFzc2lnbmVlc1wiOiBbXSxcbiAgICBcInJlcXVlc3RlZF9yZXZpZXdlcnNcIjogW10sXG4gICAgXCJyZXF1ZXN0ZWRfdGVhbXNcIjogW10sXG4gICAgXCJsYWJlbHNcIjogW10sXG4gICAgXCJtaWxlc3RvbmVcIjogbnVsbCxcbiAgICBcImRyYWZ0XCI6IGZhbHNlLFxuICAgIFwiY29tbWl0c191cmxcIjogXCJodHRwczovL2FwaS5naXRodWIuY29tL3JlcG9zL2JpbndpZWRlcmhpZXIvZGFiYmxlL3B1bGxzLzEvY29tbWl0c1wiLFxuICAgIFwicmV2aWV3X2NvbW1lbnRzX3VybFwiOiBcImh0dHBzOi8vYXBpLmdpdGh1Yi5jb20vcmVwb3MvYmlud2llZGVyaGllci9kYWJibGUvcHVsbHMvMS9jb21tZW50c1wiLFxuICAgIFwicmV2aWV3X2NvbW1lbnRfdXJsXCI6IFwiaHR0cHM6Ly9hcGkuZ2l0aHViLmNvbS9yZXBvcy9iaW53aWVkZXJoaWVyL2RhYmJsZS9wdWxscy9jb21tZW50c3svbnVtYmVyfVwiLFxuICAgIFwiY29tbWVudHNfdXJsXCI6IFwiaHR0cHM6Ly9hcGkuZ2l0aHViLmNvbS9yZXBvcy9iaW53aWVkZXJoaWVyL2RhYmJsZS9pc3N1ZXMvMS9jb21tZW50c1wiLFxuICAgIFwic3RhdHVzZXNfdXJsXCI6IFwiaHR0cHM6Ly9hcGkuZ2l0aHViLmNvbS9yZXBvcy9iaW53aWVkZXJoaWVyL2RhYmJsZS9zdGF0dXNlcy81NzAzODQyY2M1NzE1ZWQxZTM1OGQyM2ViYjY5M2RiMDk3NDdhZTliXCIsXG4gICAgXCJoZWFkXCI6IHtcbiAgICAgIFwibGFiZWxcIjogXCJiaW53aWVkZXJoaWVyOmFhXCIsXG4gICAgICBcInJlZlwiOiBcImFhXCIsXG4gICAgICBcInNoYVwiOiBcIjU3MDM4NDJjYzU3MTVlZDFlMzU4ZDIzZWJiNjkzZGIwOTc0N2FlOWJcIixcbiAgICAgIFwidXNlclwiOiB7XG4gICAgICAgIFwibG9naW5cIjogXCJiaW53aWVkZXJoaWVyXCIsXG4gICAgICAgIFwiaWRcIjogNjY0NTk3LFxuICAgICAgICBcIm5vZGVfaWRcIjogXCJNRFE2VlhObGNqWTJORFU1Tnc9PVwiLFxuICAgICAgICBcImF2YXRhcl91cmxcIjogXCJodHRwczovL2F2YXRhcnMuZ2l0aHVidXNlcmNvbnRlbnQuY29tL3UvNjY0NTk3P3Y9NFwiLFxuICAgICAgICBcImdyYXZhdGFyX2lkXCI6IFwiXCIsXG4gICAgICAgIFwidXJsXCI6IFwiaHR0cHM6Ly9hcGkuZ2l0aHViLmNvbS91c2Vycy9iaW53aWVkZXJoaWVyXCIsXG4gICAgICAgIFwiaHRtbF91cmxcIjogXCJodHRwczovL2dpdGh1Yi5jb20vYmlud2llZGVyaGllclwiLFxuICAgICAgICBcImZvbGxvd2Vyc191cmxcIjogXCJodHRwczovL2FwaS5naXRodWIuY29tL3VzZXJzL2JpbndpZWRlcmhpZXIvZm9sbG93ZXJzXCIsXG4gICAgICAgIFwiZm9sbG93aW5nX3VybFwiOiBcImh0dHBzOi8vYXBpLmdpdGh1Yi5jb20vdXNlcnMvYmlud2llZGVyaGllci9mb2xsb3dpbmd7L290aGVyX3VzZXJ9XCIsXG4gICAgICAgIFwiZ2lzdHNfdXJsXCI6IFwiaHR0cHM6Ly9hcGkuZ2l0aHViLmNvbS91c2Vycy9iaW53aWVkZXJoaWVyL2dpc3Rzey9naXN0X2lkfVwiLFxuICAgICAgICBcInN0YXJyZWRfdXJsXCI6IFwiaHR0cHM6Ly9hcGkuZ2l0aHViLmNvbS91c2Vycy9iaW53aWVkZXJoaWVyL3N0YXJyZWR7L293bmVyfXsvcmVwb31cIixcbiAgICAgICAgXCJzdWJzY3JpcHRpb25zX3VybFwiOiBcImh0dHBzOi8vYXBpLmdpdGh1Yi5jb20vdXNlcnMvYmlud2llZGVyaGllci9zdWJzY3JpcHRpb25zXCIsXG4gICAgICAgIFwib3JnYW5pemF0aW9uc191cmxcIjogXCJodHRwczovL2FwaS5naXRodWIuY29tL3VzZXJzL2JpbndpZWRlcmhpZXIvb3Jnc1wiLFxuICAgICAgICBcInJlcG9zX3VybFwiOiBcImh0dHBzOi8vYXBpLmdpdGh1Yi5jb20vdXNlcnMvYmlud2llZGVyaGllci9yZXBvc1wiLFxuICAgICAgICBcImV2ZW50c191cmxcIjogXCJodHRwczovL2FwaS5naXRodWIuY29tL3VzZXJzL2JpbndpZWRlcmhpZXIvZXZlbnRzey9wcml2YWN5fVwiLFxuICAgICAgICBcInJlY2VpdmVkX2V2ZW50c191cmxcIjogXCJodHRwczovL2FwaS5naXRodWIuY29tL3VzZXJzL2JpbndpZWRlcmhpZXIvcmVjZWl2ZWRfZXZlbnRzXCIsXG4gICAgICAgIFwidHlwZVwiOiBcIlVzZXJcIixcbiAgICAgICAgXCJzaXRlX2FkbWluXCI6IGZhbHNlXG4gICAgICB9LFxuICAgICAgXCJyZXBvXCI6IHtcbiAgICAgICAgXCJpZFwiOiA0NzAyMTIwMDMsXG4gICAgICAgIFwibm9kZV9pZFwiOiBcIlJfa2dET0hBYmRvd1wiLFxuICAgICAgICBcIm5hbWVcIjogXCJkYWJibGVcIixcbiAgICAgICAgXCJmdWxsX25hbWVcIjogXCJiaW53aWVkZXJoaWVyL2RhYmJsZVwiLFxuICAgICAgICBcInByaXZhdGVcIjogZmFsc2UsXG4gICAgICAgIFwib3duZXJcIjoge1xuICAgICAgICAgIFwibG9naW5cIjogXCJiaW53aWVkZXJoaWVyXCIsXG4gICAgICAgICAgXCJpZFwiOiA2NjQ1OTcsXG4gICAgICAgICAgXCJub2RlX2lkXCI6IFwiTURRNlZYTmxjalkyTkRVNU53PT1cIixcbiAgICAgICAgICBcImF2YXRhcl91cmxcIjogXCJodHRwczovL2F2YXRhcnMuZ2l0aHVidXNlcmNvbnRlbnQuY29tL3UvNjY0NTk3P3Y9NFwiLFxuICAgICAgICAgIFwiZ3JhdmF0YXJfaWRcIjogXCJcIixcbiAgICAgICAgICBcInVybFwiOiBcImh0dHBzOi8vYXBpLmdpdGh1Yi5jb20vdXNlcnMvYmlud2llZGVyaGllclwiLFxuICAgICAgICAgIFwiaHRtbF91cmxcIjogXCJodHRwczovL2dpdGh1Yi5jb20vYmlud2llZGVyaGllclwiLFxuICAgICAgICAgIFwiZm9sbG93ZXJzX3VybFwiOiBcImh0dHBzOi8vYXBpLmdpdGh1Yi5jb20vdXNlcnMvYmlud2llZGVyaGllci9mb2xsb3dlcnNcIixcbiAgICAgICAgICBcImZvbGxvd2luZ191cmxcIjogXCJodHRwczovL2FwaS5naXRodWIuY29tL3VzZXJzL2JpbndpZWRlcmhpZXIvZm9sbG93aW5ney9vdGhlcl91c2VyfVwiLFxuICAgICAgICAgIFwiZ2lzdHNfdXJsXCI6IFwiaHR0cHM6Ly9hcGkuZ2l0aHViLmNvbS91c2Vycy9iaW53aWVkZXJoaWVyL2dpc3Rzey9naXN0X2lkfVwiLFxuICAgICAgICAgIFwic3RhcnJlZF91cmxcIjogXCJodHRwczovL2FwaS5naXRodWIuY29tL3VzZXJzL2JpbndpZWRlcmhpZXIvc3RhcnJlZHsvb3duZXJ9ey9yZXBvfVwiLFxuICAgICAgICAgIFwic3Vic2NyaXB0aW9uc191cmxcIjogXCJodHRwczovL2FwaS5naXRodWIuY29tL3VzZXJzL2JpbndpZWRlcmhpZXIvc3Vic2NyaXB0aW9uc1wiLFxuICAgICAgICAgIFwib3JnYW5pemF0aW9uc191cmxcIjogXCJodHRwczovL2FwaS5naXRodWIuY29tL3VzZXJzL2JpbndpZWRlcmhpZXIvb3Jnc1wiLFxuICAgICAgICAgIFwicmVwb3NfdXJsXCI6IFwiaHR0cHM6Ly9hcGkuZ2l0aHViLmNvbS91c2Vycy9iaW53aWVkZXJoaWVyL3JlcG9zXCIsXG4gICAgICAgICAgXCJldmVudHNfdXJsXCI6IFwiaHR0cHM6Ly9hcGkuZ2l0aHViLmNvbS91c2Vycy9iaW53aWVkZXJoaWVyL2V2ZW50c3svcHJpdmFjeX1cIixcbiAgICAgICAgICBcInJlY2VpdmVkX2V2ZW50c191cmxcIjogXCJodHRwczovL2FwaS5naXRodWIuY29tL3VzZXJzL2JpbndpZWRlcmhpZXIvcmVjZWl2ZWRfZXZlbnRzXCIsXG4gICAgICAgICAgXCJ0eXBlXCI6IFwiVXNlclwiLFxuICAgICAgICAgIFwic2l0ZV9hZG1pblwiOiBmYWxzZVxuICAgICAgICB9LFxuICAgICAgICBcImh0bWxfdXJsXCI6IFwiaHR0cHM6Ly9naXRodWIuY29tL2JpbndpZWRlcmhpZXIvZGFiYmxlXCIsXG4gICAgICAgIFwiZGVzY3JpcHRpb25cIjogXCJBIHJlcG8gZm9yIGRhYmJsaW5nXCIsXG4gICAgICAgIFwiZm9ya1wiOiBmYWxzZSxcbiAgICAgICAgXCJ1cmxcIjogXCJodHRwczovL2FwaS5naXRodWIuY29tL3JlcG9zL2JpbndpZWRlcmhpZXIvZGFiYmxlXCIsXG4gICAgICAgIFwiZm9ya3NfdXJsXCI6IFwiaHR0cHM6Ly9hcGkuZ2l0aHViLmNvbS9yZXBvcy9iaW53aWVkZXJoaWVyL2RhYmJsZS9mb3Jrc1wiLFxuICAgICAgICBcImtleXNfdXJsXCI6IFwiaHR0cHM6Ly9hcGkuZ2l0aHViLmNvbS9yZXBvcy9iaW53aWVkZXJoaWVyL2RhYmJsZS9rZXlzey9rZXlfaWR9XCIsXG4gICAgICAgIFwiY29sbGFib3JhdG9yc191cmxcIjogXCJodHRwczovL2FwaS5naXRodWIuY29tL3JlcG9zL2JpbndpZWRlcmhpZXIvZGFiYmxlL2NvbGxhYm9yYXRvcnN7L2NvbGxhYm9yYXRvcn1cIixcbiAgICAgICAgXCJ0ZWFtc191cmxcIjogXCJodHRwczovL2FwaS5naXRodWIuY29tL3JlcG9zL2JpbndpZWRlcmhpZXIvZGFiYmxlL3RlYW1zXCIsXG4gICAgICAgIFwiaG9va3NfdXJsXCI6IFwiaHR0cHM6Ly9hcGkuZ2l0aHViLmNvbS9yZXBvcy9iaW53aWVkZXJoaWVyL2RhYmJsZS9ob29rc1wiLFxuICAgICAgICBcImlzc3VlX2V2ZW50c191cmxcIjogXCJodHRwczovL2FwaS5naXRodWIuY29tL3JlcG9zL2JpbndpZWRlcmhpZXIvZGFiYmxlL2lzc3Vlcy9ldmVudHN7L251bWJlcn1cIixcbiAgICAgICAgXCJldmVudHNfdXJsXCI6IFwiaHR0cHM6Ly9hcGkuZ2l0aHViLmNvbS9yZXBvcy9iaW53aWVkZXJoaWVyL2RhYmJsZS9ldmVudHNcIixcbiAgICAgICAgXCJhc3NpZ25lZXNfdXJsXCI6IFwiaHR0cHM6Ly9hcGkuZ2l0aHViLmNvbS9yZXBvcy9iaW53aWVkZXJoaWVyL2RhYmJsZS9hc3NpZ25lZXN7L3VzZXJ9XCIsXG4gICAgICAgIFwiYnJhbmNoZXNfdXJsXCI6IFwiaHR0cHM6Ly9hcGkuZ2l0aHViLmNvbS9yZXBvcy9iaW53aWVkZXJoaWVyL2RhYmJsZS9icmFuY2hlc3svYnJhbmNofVwiLFxuICAgICAgICBcInRhZ3NfdXJsXCI6IFwiaHR0cHM6Ly9hcGkuZ2l0aHViLmNvbS9yZXBvcy9iaW53aWVkZXJoaWVyL2RhYmJsZS90YWdzXCIsXG4gICAgICAgIFwiYmxvYnNfdXJsXCI6IFwiaHR0cHM6Ly9hcGkuZ2l0aHViLmNvbS9yZXBvcy9iaW53aWVkZXJoaWVyL2RhYmJsZS9naXQvYmxvYnN7L3NoYX1cIixcbiAgICAgICAgXCJnaXRfdGFnc191cmxcIjogXCJodHRwczovL2FwaS5naXRodWIuY29tL3JlcG9zL2JpbndpZWRlcmhpZXIvZGFiYmxlL2dpdC90YWdzey9zaGF9XCIsXG4gICAgICAgIFwiZ2l0X3JlZnNfdXJsXCI6IFwiaHR0cHM6Ly9hcGkuZ2l0aHViLmNvbS9yZXBvcy9iaW53aWVkZXJoaWVyL2RhYmJsZS9naXQvcmVmc3svc2hhfVwiLFxuICAgICAgICBcInRyZWVzX3VybFwiOiBcImh0dHBzOi8vYXBpLmdpdGh1Yi5jb20vcmVwb3MvYmlud2llZGVyaGllci9kYWJibGUvZ2l0L3RyZWVzey9zaGF9XCIsXG4gICAgICAgIFwic3RhdHVzZXNfdXJsXCI6IFwiaHR0cHM6Ly9hcGkuZ2l0aHViLmNvbS9yZXBvcy9iaW53aWVkZXJoaWVyL2RhYmJsZS9zdGF0dXNlcy97c2hhfVwiLFxuICAgICAgICBcImxhbmd1YWdlc191cmxcIjogXCJodHRwczovL2FwaS5naXRodWIuY29tL3JlcG9zL2JpbndpZWRlcmhpZXIvZGFiYmxlL2xhbmd1YWdlc1wiLFxuICAgICAgICBcInN0YXJnYXplcnNfdXJsXCI6IFwiaHR0cHM6Ly9hcGkuZ2l0aHViLmNvbS9yZXBvcy9iaW53aWVkZXJoaWVyL2RhYmJsZS9zdGFyZ2F6ZXJzXCIsXG4gICAgICAgIFwiY29udHJpYnV0b3JzX3VybFwiOiBcImh0dHBzOi8vYXBpLmdpdGh1Yi5jb20vcmVwb3MvYmlud2llZGVyaGllci9kYWJibGUvY29udHJpYnV0b3JzXCIsXG4gICAgICAgIFwic3Vic2NyaWJlcnNfdXJsXCI6IFwiaHR0cHM6Ly9hcGkuZ2l0aHViLmNvbS9yZXBvcy9iaW53aWVkZXJoaWVyL2RhYmJsZS9zdWJzY3JpYmVyc1wiLFxuICAgICAgICBcInN1YnNjcmlwdGlvbl91cmxcIjogXCJodHRwczovL2FwaS5naXRodWIuY29tL3JlcG9zL2JpbndpZWRlcmhpZXIvZGFiYmxlL3N1YnNjcmlwdGlvblwiLFxuICAgICAgICBcImNvbW1pdHNfdXJsXCI6IFwiaHR0cHM6Ly9hcGkuZ2l0aHViLmNvbS9yZXBvcy9iaW53aWVkZXJoaWVyL2RhYmJsZS9jb21taXRzey9zaGF9XCIsXG4gICAgICAgIFwiZ2l0X2NvbW1pdHNfdXJsXCI6IFwiaHR0cHM6Ly9hcGkuZ2l0aHViLmNvbS9yZXBvcy9iaW53aWVkZXJoaWVyL2RhYmJsZS9naXQvY29tbWl0c3svc2hhfVwiLFxuICAgICAgICBcImNvbW1lbnRzX3VybFwiOiBcImh0dHBzOi8vYXBpLmdpdGh1Yi5jb20vcmVwb3MvYmlud2llZGVyaGllci9kYWJibGUvY29tbWVudHN7L251bWJlcn1cIixcbiAgICAgICAgXCJpc3N1ZV9jb21tZW50X3VybFwiOiBcImh0dHBzOi8vYXBpLmdpdGh1Yi5jb20vcmVwb3MvYmlud2llZGVyaGllci9kYWJibGUvaXNzdWVzL2NvbW1lbnRzey9udW1iZXJ9XCIsXG4gICAgICAgIFwiY29udGVudHNfdXJsXCI6IFwiaHR0cHM6Ly9hcGkuZ2l0aHViLmNvbS9yZXBvcy9iaW53aWVkZXJoaWVyL2RhYmJsZS9jb250ZW50cy97K3BhdGh9XCIsXG4gICAgICAgIFwiY29tcGFyZV91cmxcIjogXCJodHRwczovL2FwaS5naXRodWIuY29tL3JlcG9zL2JpbndpZWRlcmhpZXIvZGFiYmxlL2NvbXBhcmUve2Jhc2V9Li4ue2hlYWR9XCIsXG4gICAgICAgIFwibWVyZ2VzX3VybFwiOiBcImh0dHBzOi8vYXBpLmdpdGh1Yi5jb20vcmVwb3MvYmlud2llZGVyaGllci9kYWJibGUvbWVyZ2VzXCIsXG4gICAgICAgIFwiYXJjaGl2ZV91cmxcIjogXCJodHRwczovL2FwaS5naXRodWIuY29tL3JlcG9zL2JpbndpZWRlcmhpZXIvZGFiYmxlL3thcmNoaXZlX2Zvcm1hdH17L3JlZn1cIixcbiAgICAgICAgXCJkb3dubG9hZHNfdXJsXCI6IFwiaHR0cHM6Ly9hcGkuZ2l0aHViLmNvbS9yZXBvcy9iaW53aWVkZXJoaWVyL2RhYmJsZS9kb3dubG9hZHNcIixcbiAgICAgICAgXCJpc3N1ZXNfdXJsXCI6IFwiaHR0cHM6Ly9hcGkuZ2l0aHViLmNvbS9yZXBvcy9iaW53aWVkZXJoaWVyL2RhYmJsZS9pc3N1ZXN7L251bWJlcn1cIixcbiAgICAgICAgXCJwdWxsc191cmxcIjogXCJodHRwczovL2FwaS5naXRodWIuY29tL3JlcG9zL2JpbndpZWRlcmhpZXIvZGFiYmxlL3B1bGxzey9udW1iZXJ9XCIsXG4gICAgICAgIFwibWlsZXN0b25lc191cmxcIjogXCJodHRwczovL2FwaS5naXRodWIuY29tL3JlcG9zL2JpbndpZWRlcmhpZXIvZGFiYmxlL21pbGVzdG9uZXN7L251bWJlcn1cIixcbiAgICAgICAgXCJub3RpZmljYXRpb25zX3VybFwiOiBcImh0dHBzOi8vYXBpLmdpdGh1Yi5jb20vcmVwb3MvYmlud2llZGVyaGllci9kYWJibGUvbm90aWZpY2F0aW9uc3s/c2luY2UsYWxsLHBhcnRpY2lwYXRpbmd9XCIsXG4gICAgICAgIFwibGFiZWxzX3VybFwiOiBcImh0dHBzOi8vYXBpLmdpdGh1Yi5jb20vcmVwb3MvYmlud2llZGVyaGllci9kYWJibGUvbGFiZWxzey9uYW1lfVwiLFxuICAgICAgICBcInJlbGVhc2VzX3VybFwiOiBcImh0dHBzOi8vYXBpLmdpdGh1Yi5jb20vcmVwb3MvYmlud2llZGVyaGllci9kYWJibGUvcmVsZWFzZXN7L2lkfVwiLFxuICAgICAgICBcImRlcGxveW1lbnRzX3VybFwiOiBcImh0dHBzOi8vYXBpLmdpdGh1Yi5jb20vcmVwb3MvYmlud2llZGVyaGllci9kYWJibGUvZGVwbG95bWVudHNcIixcbiAgICAgICAgXCJjcmVhdGVkX2F0XCI6IFwiMjAyMi0wMy0xNVQxNTowNjoxN1pcIixcbiAgICAgICAgXCJ1cGRhdGVkX2F0XCI6IFwiMjAyMi0wMy0xNVQxNTowNjoxN1pcIixcbiAgICAgICAgXCJwdXNoZWRfYXRcIjogXCIyMDI0LTAzLTIxVDAyOjUyOjEwWlwiLFxuICAgICAgICBcImdpdF91cmxcIjogXCJnaXQ6Ly9naXRodWIuY29tL2JpbndpZWRlcmhpZXIvZGFiYmxlLmdpdFwiLFxuICAgICAgICBcInNzaF91cmxcIjogXCJnaXRAZ2l0aHViLmNvbTpiaW53aWVkZXJoaWVyL2RhYmJsZS5naXRcIixcbiAgICAgICAgXCJjbG9uZV91cmxcIjogXCJodHRwczovL2dpdGh1Yi5jb20vYmlud2llZGVyaGllci9kYWJibGUuZ2l0XCIsXG4gICAgICAgIFwic3ZuX3VybFwiOiBcImh0dHBzOi8vZ2l0aHViLmNvbS9iaW53aWVkZXJoaWVyL2RhYmJsZVwiLFxuICAgICAgICBcImhvbWVwYWdlXCI6IG51bGwsXG4gICAgICAgIFwic2l6ZVwiOiAxLFxuICAgICAgICBcInN0YXJnYXplcnNfY291bnRcIjogMCxcbiAgICAgICAgXCJ3YXRjaGVyc19jb3VudFwiOiAwLFxuICAgICAgICBcImxhbmd1YWdlXCI6IG51bGwsXG4gICAgICAgIFwiaGFzX2lzc3Vlc1wiOiB0cnVlLFxuICAgICAgICBcImhhc19wcm9qZWN0c1wiOiB0cnVlLFxuICAgICAgICBcImhhc19kb3dubG9hZHNcIjogdHJ1ZSxcbiAgICAgICAgXCJoYXNfd2lraVwiOiB0cnVlLFxuICAgICAgICBcImhhc19wYWdlc1wiOiBmYWxzZSxcbiAgICAgICAgXCJoYXNfZGlzY3Vzc2lvbnNcIjogZmFsc2UsXG4gICAgICAgIFwiZm9ya3NfY291bnRcIjogMCxcbiAgICAgICAgXCJtaXJyb3JfdXJsXCI6IG51bGwsXG4gICAgICAgIFwiYXJjaGl2ZWRcIjogZmFsc2UsXG4gICAgICAgIFwiZGlzYWJsZWRcIjogZmFsc2UsXG4gICAgICAgIFwib3Blbl9pc3N1ZXNfY291bnRcIjogMSxcbiAgICAgICAgXCJsaWNlbnNlXCI6IG51bGwsXG4gICAgICAgIFwiYWxsb3dfZm9ya2luZ1wiOiB0cnVlLFxuICAgICAgICBcImlzX3RlbXBsYXRlXCI6IGZhbHNlLFxuICAgICAgICBcIndlYl9jb21taXRfc2lnbm9mZl9yZXF1aXJlZFwiOiBmYWxzZSxcbiAgICAgICAgXCJ0b3BpY3NcIjogW10sXG4gICAgICAgIFwidmlzaWJpbGl0eVwiOiBcInB1YmxpY1wiLFxuICAgICAgICBcImZvcmtzXCI6IDAsXG4gICAgICAgIFwib3Blbl9pc3N1ZXNcIjogMSxcbiAgICAgICAgXCJ3YXRjaGVyc1wiOiAwLFxuICAgICAgICBcImRlZmF1bHRfYnJhbmNoXCI6IFwibWFpblwiLFxuICAgICAgICBcImFsbG93X3NxdWFzaF9tZXJnZVwiOiB0cnVlLFxuICAgICAgICBcImFsbG93X21lcmdlX2NvbW1pdFwiOiB0cnVlLFxuICAgICAgICBcImFsbG93X3JlYmFzZV9tZXJnZVwiOiB0cnVlLFxuICAgICAgICBcImFsbG93X2F1dG9fbWVyZ2VcIjogZmFsc2UsXG4gICAgICAgIFwiZGVsZXRlX2JyYW5jaF9vbl9tZXJnZVwiOiBmYWxzZSxcbiAgICAgICAgXCJhbGxvd191cGRhdGVfYnJhbmNoXCI6IGZhbHNlLFxuICAgICAgICBcInVzZV9zcXVhc2hfcHJfdGl0bGVfYXNfZGVmYXVsdFwiOiBmYWxzZSxcbiAgICAgICAgXCJzcXVhc2hfbWVyZ2VfY29tbWl0X21lc3NhZ2VcIjogXCJDT01NSVRfTUVTU0FHRVNcIixcbiAgICAgICAgXCJzcXVhc2hfbWVyZ2VfY29tbWl0X3RpdGxlXCI6IFwiQ09NTUlUX09SX1BSX1RJVExFXCIsXG4gICAgICAgIFwibWVyZ2VfY29tbWl0X21lc3NhZ2VcIjogXCJQUl9USVRMRVwiLFxuICAgICAgICBcIm1lcmdlX2NvbW1pdF90aXRsZVwiOiBcIk1FUkdFX01FU1NBR0VcIlxuICAgICAgfVxuICAgIH0sXG4gICAgXCJiYXNlXCI6IHtcbiAgICAgIFwibGFiZWxcIjogXCJiaW53aWVkZXJoaWVyOm1haW5cIixcbiAgICAgIFwicmVmXCI6IFwibWFpblwiLFxuICAgICAgXCJzaGFcIjogXCI3MmQ5MzFhMjBiYjgzZDEyM2FiNDVhY2NhZjc2MTE1MGM4YjAxMjExXCIsXG4gICAgICBcInVzZXJcIjoge1xuICAgICAgICBcImxvZ2luXCI6IFwiYmlud2llZGVyaGllclwiLFxuICAgICAgICBcImlkXCI6IDY2NDU5NyxcbiAgICAgICAgXCJub2RlX2lkXCI6IFwiTURRNlZYTmxjalkyTkRVNU53PT1cIixcbiAgICAgICAgXCJhdmF0YXJfdXJsXCI6IFwiaHR0cHM6Ly9hdmF0YXJzLmdpdGh1YnVzZXJjb250ZW50LmNvbS91LzY2NDU5Nz92PTRcIixcbiAgICAgICAgXCJncmF2YXRhcl9pZFwiOiBcIlwiLFxuICAgICAgICBcInVybFwiOiBcImh0dHBzOi8vYXBpLmdpdGh1Yi5jb20vdXNlcnMvYmlud2llZGVyaGllclwiLFxuICAgICAgICBcImh0bWxfdXJsXCI6IFwiaHR0cHM6Ly9naXRodWIuY29tL2JpbndpZWRlcmhpZXJcIixcbiAgICAgICAgXCJmb2xsb3dlcnNfdXJsXCI6IFwiaHR0cHM6Ly9hcGkuZ2l0aHViLmNvbS91c2Vycy9iaW53aWVkZXJoaWVyL2ZvbGxvd2Vyc1wiLFxuICAgICAgICBcImZvbGxvd2luZ191cmxcIjogXCJodHRwczovL2FwaS5naXRodWIuY29tL3VzZXJzL2JpbndpZWRlcmhpZXIvZm9sbG93aW5ney9vdGhlcl91c2VyfVwiLFxuICAgICAgICBcImdpc3RzX3VybFwiOiBcImh0dHBzOi8vYXBpLmdpdGh1Yi5jb20vdXNlcnMvYmlud2llZGVyaGllci9naXN0c3svZ2lzdF9pZH1cIixcbiAgICAgICAgXCJzdGFycmVkX3VybFwiOiBcImh0dHBzOi8vYXBpLmdpdGh1Yi5jb20vdXNlcnMvYmlud2llZGVyaGllci9zdGFycmVkey9vd25lcn17L3JlcG99XCIsXG4gICAgICAgIFwic3Vic2NyaXB0aW9uc191cmxcIjogXCJodHRwczovL2FwaS5naXRodWIuY29tL3VzZXJzL2JpbndpZWRlcmhpZXIvc3Vic2NyaXB0aW9uc1wiLFxuICAgICAgICBcIm9yZ2FuaXphdGlvbnNfdXJsXCI6IFwiaHR0cHM6Ly9hcGkuZ2l0aHViLmNvbS91c2Vycy9iaW53aWVkZXJoaWVyL29yZ3NcIixcbiAgICAgICAgXCJyZXBvc191cmxcIjogXCJodHRwczovL2FwaS5naXRodWIuY29tL3VzZXJzL2JpbndpZWRlcmhpZXIvcmVwb3NcIixcbiAgICAgICAgXCJldmVudHNfdXJsXCI6IFwiaHR0cHM6Ly9hcGkuZ2l0aHViLmNvbS91c2Vycy9iaW53aWVkZXJoaWVyL2V2ZW50c3svcHJpdmFjeX1cIixcbiAgICAgICAgXCJyZWNlaXZlZF9ldmVudHNfdXJsXCI6IFwiaHR0cHM6Ly9hcGkuZ2l0aHViLmNvbS91c2Vycy9iaW53aWVkZXJoaWVyL3JlY2VpdmVkX2V2ZW50c1wiLFxuICAgICAgICBcInR5cGVcIjogXCJVc2VyXCIsXG4gICAgICAgIFwic2l0ZV9hZG1pblwiOiBmYWxzZVxuICAgICAgfSxcbiAgICAgIFwicmVwb1wiOiB7XG4gICAgICAgIFwiaWRcIjogNDcwMjEyMDAzLFxuICAgICAgICBcIm5vZGVfaWRcIjogXCJSX2tnRE9IQWJkb3dcIixcbiAgICAgICAgXCJuYW1lXCI6IFwiZGFiYmxlXCIsXG4gICAgICAgIFwiZnVsbF9uYW1lXCI6IFwiYmlud2llZGVyaGllci9kYWJibGVcIixcbiAgICAgICAgXCJwcml2YXRlXCI6IGZhbHNlLFxuICAgICAgICBcIm93bmVyXCI6IHtcbiAgICAgICAgICBcImxvZ2luXCI6IFwiYmlud2llZGVyaGllclwiLFxuICAgICAgICAgIFwiaWRcIjogNjY0NTk3LFxuICAgICAgICAgIFwibm9kZV9pZFwiOiBcIk1EUTZWWE5sY2pZMk5EVTVOdz09XCIsXG4gICAgICAgICAgXCJhdmF0YXJfdXJsXCI6IFwiaHR0cHM6Ly9hdmF0YXJzLmdpdGh1YnVzZXJjb250ZW50LmNvbS91LzY2NDU5Nz92PTRcIixcbiAgICAgICAgICBcImdyYXZhdGFyX2lkXCI6IFwiXCIsXG4gICAgICAgICAgXCJ1cmxcIjogXCJodHRwczovL2FwaS5naXRodWIuY29tL3VzZXJzL2JpbndpZWRlcmhpZXJcIixcbiAgICAgICAgICBcImh0bWxfdXJsXCI6IFwiaHR0cHM6Ly9naXRodWIuY29tL2JpbndpZWRlcmhpZXJcIixcbiAgICAgICAgICBcImZvbGxvd2Vyc191cmxcIjogXCJodHRwczovL2FwaS5naXRodWIuY29tL3VzZXJzL2JpbndpZWRlcmhpZXIvZm9sbG93ZXJzXCIsXG4gICAgICAgICAgXCJmb2xsb3dpbmdfdXJsXCI6IFwiaHR0cHM6Ly9hcGkuZ2l0aHViLmNvbS91c2Vycy9iaW53aWVkZXJoaWVyL2ZvbGxvd2luZ3svb3RoZXJfdXNlcn1cIixcbiAgICAgICAgICBcImdpc3RzX3VybFwiOiBcImh0dHBzOi8vYXBpLmdpdGh1Yi5jb20vdXNlcnMvYmlud2llZGVyaGllci9naXN0c3svZ2lzdF9pZH1cIixcbiAgICAgICAgICBcInN0YXJyZWRfdXJsXCI6IFwiaHR0cHM6Ly9hcGkuZ2l0aHViLmNvbS91c2Vycy9iaW53aWVkZXJoaWVyL3N0YXJyZWR7L293bmVyfXsvcmVwb31cIixcbiAgICAgICAgICBcInN1YnNjcmlwdGlvbnNfdXJsXCI6IFwiaHR0cHM6Ly9hcGkuZ2l0aHViLmNvbS91c2Vycy9iaW53aWVkZXJoaWVyL3N1YnNjcmlwdGlvbnNcIixcbiAgICAgICAgICBcIm9yZ2FuaXphdGlvbnNfdXJsXCI6IFwiaHR0cHM6Ly9hcGkuZ2l0aHViLmNvbS91c2Vycy9iaW53aWVkZXJoaWVyL29yZ3NcIixcbiAgICAgICAgICBcInJlcG9zX3VybFwiOiBcImh0dHBzOi8vYXBpLmdpdGh1Yi5jb20vdXNlcnMvYmlud2llZGVyaGllci9yZXBvc1wiLFxuICAgICAgICAgIFwiZXZlbnRzX3VybFwiOiBcImh0dHBzOi8vYXBpLmdpdGh1Yi5jb20vdXNlcnMvYmlud2llZGVyaGllci9ldmVudHN7L3ByaXZhY3l9XCIsXG4gICAgICAgICAgXCJyZWNlaXZlZF9ldmVudHNfdXJsXCI6IFwiaHR0cHM6Ly9hcGkuZ2l0aHViLmNvbS91c2Vycy9iaW53aWVkZXJoaWVyL3JlY2VpdmVkX2V2ZW50c1wiLFxuICAgICAgICAgIFwidHlwZVwiOiBcIlVzZXJcIixcbiAgICAgICAgICBcInNpdGVfYWRtaW5cIjogZmFsc2VcbiAgICAgICAgfSxcbiAgICAgICAgXCJodG1sX3VybFwiOiBcImh0dHBzOi8vZ2l0aHViLmNvbS9iaW53aWVkZXJoaWVyL2RhYmJsZVwiLFxuICAgICAgICBcImRlc2NyaXB0aW9uXCI6IFwiQSByZXBvIGZvciBkYWJibGluZ1wiLFxuICAgICAgICBcImZvcmtcIjogZmFsc2UsXG4gICAgICAgIFwidXJsXCI6IFwiaHR0cHM6Ly9hcGkuZ2l0aHViLmNvbS9yZXBvcy9iaW53aWVkZXJoaWVyL2RhYmJsZVwiLFxuICAgICAgICBcImZvcmtzX3VybFwiOiBcImh0dHBzOi8vYXBpLmdpdGh1Yi5jb20vcmVwb3MvYmlud2llZGVyaGllci9kYWJibGUvZm9ya3NcIixcbiAgICAgICAgXCJrZXlzX3VybFwiOiBcImh0dHBzOi8vYXBpLmdpdGh1Yi5jb20vcmVwb3MvYmlud2llZGVyaGllci9kYWJibGUva2V5c3sva2V5X2lkfVwiLFxuICAgICAgICBcImNvbGxhYm9yYXRvcnNfdXJsXCI6IFwiaHR0cHM6Ly9hcGkuZ2l0aHViLmNvbS9yZXBvcy9iaW53aWVkZXJoaWVyL2RhYmJsZS9jb2xsYWJvcmF0b3Jzey9jb2xsYWJvcmF0b3J9XCIsXG4gICAgICAgIFwidGVhbXNfdXJsXCI6IFwiaHR0cHM6Ly9hcGkuZ2l0aHViLmNvbS9yZXBvcy9iaW53aWVkZXJoaWVyL2RhYmJsZS90ZWFtc1wiLFxuICAgICAgICBcImhvb2tzX3VybFwiOiBcImh0dHBzOi8vYXBpLmdpdGh1Yi5jb20vcmVwb3MvYmlud2llZGVyaGllci9kYWJibGUvaG9va3NcIixcbiAgICAgICAgXCJpc3N1ZV9ldmVudHNfdXJsXCI6IFwiaHR0cHM6Ly9hcGkuZ2l0aHViLmNvbS9yZXBvcy9iaW53aWVkZXJoaWVyL2RhYmJsZS9pc3N1ZXMvZXZlbnRzey9udW1iZXJ9XCIsXG4gICAgICAgIFwiZXZlbnRzX3VybFwiOiBcImh0dHBzOi8vYXBpLmdpdGh1Yi5jb20vcmVwb3MvYmlud2llZGVyaGllci9kYWJibGUvZXZlbnRzXCIsXG4gICAgICAgIFwiYXNzaWduZWVzX3VybFwiOiBcImh0dHBzOi8vYXBpLmdpdGh1Yi5jb20vcmVwb3MvYmlud2llZGVyaGllci9kYWJibGUvYXNzaWduZWVzey91c2VyfVwiLFxuICAgICAgICBcImJyYW5jaGVzX3VybFwiOiBcImh0dHBzOi8vYXBpLmdpdGh1Yi5jb20vcmVwb3MvYmlud2llZGVyaGllci9kYWJibGUvYnJhbmNoZXN7L2JyYW5jaH1cIixcbiAgICAgICAgXCJ0YWdzX3VybFwiOiBcImh0dHBzOi8vYXBpLmdpdGh1Yi5jb20vcmVwb3MvYmlud2llZGVyaGllci9kYWJibGUvdGFnc1wiLFxuICAgICAgICBcImJsb2JzX3VybFwiOiBcImh0dHBzOi8vYXBpLmdpdGh1Yi5jb20vcmVwb3MvYmlud2llZGVyaGllci9kYWJibGUvZ2l0L2Jsb2Jzey9zaGF9XCIsXG4gICAgICAgIFwiZ2l0X3RhZ3NfdXJsXCI6IFwiaHR0cHM6Ly9hcGkuZ2l0aHViLmNvbS9yZXBvcy9iaW53aWVkZXJoaWVyL2RhYmJsZS9naXQvdGFnc3svc2hhfVwiLFxuICAgICAgICBcImdpdF9yZWZzX3VybFwiOiBcImh0dHBzOi8vYXBpLmdpdGh1Yi5jb20vcmVwb3MvYmlud2llZGVyaGllci9kYWJibGUvZ2l0L3JlZnN7L3NoYX1cIixcbiAgICAgICAgXCJ0cmVlc191cmxcIjogXCJodHRwczovL2FwaS5naXRodWIuY29tL3JlcG9zL2JpbndpZWRlcmhpZXIvZGFiYmxlL2dpdC90cmVlc3svc2hhfVwiLFxuICAgICAgICBcInN0YXR1c2VzX3VybFwiOiBcImh0dHBzOi8vYXBpLmdpdGh1Yi5jb20vcmVwb3MvYmlud2llZGVyaGllci9kYWJibGUvc3RhdHVzZXMve3NoYX1cIixcbiAgICAgICAgXCJsYW5ndWFnZXNfdXJsXCI6IFwiaHR0cHM6Ly9hcGkuZ2l0aHViLmNvbS9yZXBvcy9iaW53aWVkZXJoaWVyL2RhYmJsZS9sYW5ndWFnZXNcIixcbiAgICAgICAgXCJzdGFyZ2F6ZXJzX3VybFwiOiBcImh0dHBzOi8vYXBpLmdpdGh1Yi5jb20vcmVwb3MvYmlud2llZGVyaGllci9kYWJibGUvc3RhcmdhemVyc1wiLFxuICAgICAgICBcImNvbnRyaWJ1dG9yc191cmxcIjogXCJodHRwczovL2FwaS5naXRodWIuY29tL3JlcG9zL2JpbndpZWRlcmhpZXIvZGFiYmxlL2NvbnRyaWJ1dG9yc1wiLFxuICAgICAgICBcInN1YnNjcmliZXJzX3VybFwiOiBcImh0dHBzOi8vYXBpLmdpdGh1Yi5jb20vcmVwb3MvYmlud2llZGVyaGllci9kYWJibGUvc3Vic2NyaWJlcnNcIixcbiAgICAgICAgXCJzdWJzY3JpcHRpb25fdXJsXCI6IFwiaHR0cHM6Ly9hcGkuZ2l0aHViLmNvbS9yZXBvcy9iaW53aWVkZXJoaWVyL2RhYmJsZS9zdWJzY3JpcHRpb25cIixcbiAgICAgICAgXCJjb21taXRzX3VybFwiOiBcImh0dHBzOi8vYXBpLmdpdGh1Yi5jb20vcmVwb3MvYmlud2llZGVyaGllci9kYWJibGUvY29tbWl0c3svc2hhfVwiLFxuICAgICAgICBcImdpdF9jb21taXRzX3VybFwiOiBcImh0dHBzOi8vYXBpLmdpdGh1Yi5jb20vcmVwb3MvYmlud2llZGVyaGllci9kYWJibGUvZ2l0L2NvbW1pdHN7L3NoYX1cIixcbiAgICAgICAgXCJjb21tZW50c191cmxcIjogXCJodHRwczovL2FwaS5naXRodWIuY29tL3JlcG9zL2JpbndpZWRlcmhpZXIvZGFiYmxlL2NvbW1lbnRzey9udW1iZXJ9XCIsXG4gICAgICAgIFwiaXNzdWVfY29tbWVudF91cmxcIjogXCJodHRwczovL2FwaS5naXRodWIuY29tL3JlcG9zL2JpbndpZWRlcmhpZXIvZGFiYmxlL2lzc3Vlcy9jb21tZW50c3svbnVtYmVyfVwiLFxuICAgICAgICBcImNvbnRlbnRzX3VybFwiOiBcImh0dHBzOi8vYXBpLmdpdGh1Yi5jb20vcmVwb3MvYmlud2llZGVyaGllci9kYWJibGUvY29udGVudHMveytwYXRofVwiLFxuICAgICAgICBcImNvbXBhcmVfdXJsXCI6IFwiaHR0cHM6Ly9hcGkuZ2l0aHViLmNvbS9yZXBvcy9iaW53aWVkZXJoaWVyL2RhYmJsZS9jb21wYXJlL3tiYXNlfS4uLntoZWFkfVwiLFxuICAgICAgICBcIm1lcmdlc191cmxcIjogXCJodHRwczovL2FwaS5naXRodWIuY29tL3JlcG9zL2JpbndpZWRlcmhpZXIvZGFiYmxlL21lcmdlc1wiLFxuICAgICAgICBcImFyY2hpdmVfdXJsXCI6IFwiaHR0cHM6Ly9hcGkuZ2l0aHViLmNvbS9yZXBvcy9iaW53aWVkZXJoaWVyL2RhYmJsZS97YXJjaGl2ZV9mb3JtYXR9ey9yZWZ9XCIsXG4gICAgICAgIFwiZG93bmxvYWRzX3VybFwiOiBcImh0dHBzOi8vYXBpLmdpdGh1Yi5jb20vcmVwb3MvYmlud2llZGVyaGllci9kYWJibGUvZG93bmxvYWRzXCIsXG4gICAgICAgIFwiaXNzdWVzX3VybFwiOiBcImh0dHBzOi8vYXBpLmdpdGh1Yi5jb20vcmVwb3MvYmlud2llZGVyaGllci9kYWJibGUvaXNzdWVzey9udW1iZXJ9XCIsXG4gICAgICAgIFwicHVsbHNfdXJsXCI6IFwiaHR0cHM6Ly9hcGkuZ2l0aHViLmNvbS9yZXBvcy9iaW53aWVkZXJoaWVyL2RhYmJsZS9wdWxsc3svbnVtYmVyfVwiLFxuICAgICAgICBcIm1pbGVzdG9uZXNfdXJsXCI6IFwiaHR0cHM6Ly9hcGkuZ2l0aHViLmNvbS9yZXBvcy9iaW53aWVkZXJoaWVyL2RhYmJsZS9taWxlc3RvbmVzey9udW1iZXJ9XCIsXG4gICAgICAgIFwibm90aWZpY2F0aW9uc191cmxcIjogXCJodHRwczovL2FwaS5naXRodWIuY29tL3JlcG9zL2JpbndpZWRlcmhpZXIvZGFiYmxlL25vdGlmaWNhdGlvbnN7P3NpbmNlLGFsbCxwYXJ0aWNpcGF0aW5nfVwiLFxuICAgICAgICBcImxhYmVsc191cmxcIjogXCJodHRwczovL2FwaS5naXRodWIuY29tL3JlcG9zL2JpbndpZWRlcmhpZXIvZGFiYmxlL2xhYmVsc3svbmFtZX1cIixcbiAgICAgICAgXCJyZWxlYXNlc191cmxcIjogXCJodHRwczovL2FwaS5naXRodWIuY29tL3JlcG9zL2JpbndpZWRlcmhpZXIvZGFiYmxlL3JlbGVhc2Vzey9pZH1cIixcbiAgICAgICAgXCJkZXBsb3ltZW50c191cmxcIjogXCJodHRwczovL2FwaS5naXRodWIuY29tL3JlcG9zL2JpbndpZWRlcmhpZXIvZGFiYmxlL2RlcGxveW1lbnRzXCIsXG4gICAgICAgIFwiY3JlYXRlZF9hdFwiOiBcIjIwMjItMDMtMTVUMTU6MDY6MTdaXCIsXG4gICAgICAgIFwidXBkYXRlZF9hdFwiOiBcIjIwMjItMDMtMTVUMTU6MDY6MTdaXCIsXG4gICAgICAgIFwicHVzaGVkX2F0XCI6IFwiMjAyNC0wMy0yMVQwMjo1MjoxMFpcIixcbiAgICAgICAgXCJnaXRfdXJsXCI6IFwiZ2l0Oi8vZ2l0aHViLmNvbS9iaW53aWVkZXJoaWVyL2RhYmJsZS5naXRcIixcbiAgICAgICAgXCJzc2hfdXJsXCI6IFwiZ2l0QGdpdGh1Yi5jb206Ymlud2llZGVyaGllci9kYWJibGUuZ2l0XCIsXG4gICAgICAgIFwiY2xvbmVfdXJsXCI6IFwiaHR0cHM6Ly9naXRodWIuY29tL2JpbndpZWRlcmhpZXIvZGFiYmxlLmdpdFwiLFxuICAgICAgICBcInN2bl91cmxcIjogXCJodHRwczovL2dpdGh1Yi5jb20vYmlud2llZGVyaGllci9kYWJibGVcIixcbiAgICAgICAgXCJob21lcGFnZVwiOiBudWxsLFxuICAgICAgICBcInNpemVcIjogMSxcbiAgICAgICAgXCJzdGFyZ2F6ZXJzX2NvdW50XCI6IDAsXG4gICAgICAgIFwid2F0Y2hlcnNfY291bnRcIjogMCxcbiAgICAgICAgXCJsYW5ndWFnZVwiOiBudWxsLFxuICAgICAgICBcImhhc19pc3N1ZXNcIjogdHJ1ZSxcbiAgICAgICAgXCJoYXNfcHJvamVjdHNcIjogdHJ1ZSxcbiAgICAgICAgXCJoYXNfZG93bmxvYWRzXCI6IHRydWUsXG4gICAgICAgIFwiaGFzX3dpa2lcIjogdHJ1ZSxcbiAgICAgICAgXCJoYXNfcGFnZXNcIjogZmFsc2UsXG4gICAgICAgIFwiaGFzX2Rpc2N1c3Npb25zXCI6IGZhbHNlLFxuICAgICAgICBcImZvcmtzX2NvdW50XCI6IDAsXG4gICAgICAgIFwibWlycm9yX3VybFwiOiBudWxsLFxuICAgICAgICBcImFyY2hpdmVkXCI6IGZhbHNlLFxuICAgICAgICBcImRpc2FibGVkXCI6IGZhbHNlLFxuICAgICAgICBcIm9wZW5faXNzdWVzX2NvdW50XCI6IDEsXG4gICAgICAgIFwibGljZW5zZVwiOiBudWxsLFxuICAgICAgICBcImFsbG93X2ZvcmtpbmdcIjogdHJ1ZSxcbiAgICAgICAgXCJpc190ZW1wbGF0ZVwiOiBmYWxzZSxcbiAgICAgICAgXCJ3ZWJfY29tbWl0X3NpZ25vZmZfcmVxdWlyZWRcIjogZmFsc2UsXG4gICAgICAgIFwidG9waWNzXCI6IFtdLFxuICAgICAgICBcInZpc2liaWxpdHlcIjogXCJwdWJsaWNcIixcbiAgICAgICAgXCJmb3Jrc1wiOiAwLFxuICAgICAgICBcIm9wZW5faXNzdWVzXCI6IDEsXG4gICAgICAgIFwid2F0Y2hlcnNcIjogMCxcbiAgICAgICAgXCJkZWZhdWx0X2JyYW5jaFwiOiBcIm1haW5cIixcbiAgICAgICAgXCJhbGxvd19zcXVhc2hfbWVyZ2VcIjogdHJ1ZSxcbiAgICAgICAgXCJhbGxvd19tZXJnZV9jb21taXRcIjogdHJ1ZSxcbiAgICAgICAgXCJhbGxvd19yZWJhc2VfbWVyZ2VcIjogdHJ1ZSxcbiAgICAgICAgXCJhbGxvd19hdXRvX21lcmdlXCI6IGZhbHNlLFxuICAgICAgICBcImRlbGV0ZV9icmFuY2hfb25fbWVyZ2VcIjogZmFsc2UsXG4gICAgICAgIFwiYWxsb3dfdXBkYXRlX2JyYW5jaFwiOiBmYWxzZSxcbiAgICAgICAgXCJ1c2Vfc3F1YXNoX3ByX3RpdGxlX2FzX2RlZmF1bHRcIjogZmFsc2UsXG4gICAgICAgIFwic3F1YXNoX21lcmdlX2NvbW1pdF9tZXNzYWdlXCI6IFwiQ09NTUlUX01FU1NBR0VTXCIsXG4gICAgICAgIFwic3F1YXNoX21lcmdlX2NvbW1pdF90aXRsZVwiOiBcIkNPTU1JVF9PUl9QUl9USVRMRVwiLFxuICAgICAgICBcIm1lcmdlX2NvbW1pdF9tZXNzYWdlXCI6IFwiUFJfVElUTEVcIixcbiAgICAgICAgXCJtZXJnZV9jb21taXRfdGl0bGVcIjogXCJNRVJHRV9NRVNTQUdFXCJcbiAgICAgIH1cbiAgICB9LFxuICAgIFwiX2xpbmtzXCI6IHtcbiAgICAgIFwic2VsZlwiOiB7XG4gICAgICAgIFwiaHJlZlwiOiBcImh0dHBzOi8vYXBpLmdpdGh1Yi5jb20vcmVwb3MvYmlud2llZGVyaGllci9kYWJibGUvcHVsbHMvMVwiXG4gICAgICB9LFxuICAgICAgXCJodG1sXCI6IHtcbiAgICAgICAgXCJocmVmXCI6IFwiaHR0cHM6Ly9naXRodWIuY29tL2JpbndpZWRlcmhpZXIvZGFiYmxlL3B1bGwvMVwiXG4gICAgICB9LFxuICAgICAgXCJpc3N1ZVwiOiB7XG4gICAgICAgIFwiaHJlZlwiOiBcImh0dHBzOi8vYXBpLmdpdGh1Yi5jb20vcmVwb3MvYmlud2llZGVyaGllci9kYWJibGUvaXNzdWVzLzFcIlxuICAgICAgfSxcbiAgICAgIFwiY29tbWVudHNcIjoge1xuICAgICAgICBcImhyZWZcIjogXCJodHRwczovL2FwaS5naXRodWIuY29tL3JlcG9zL2JpbndpZWRlcmhpZXIvZGFiYmxlL2lzc3Vlcy8xL2NvbW1lbnRzXCJcbiAgICAgIH0sXG4gICAgICBcInJldmlld19jb21tZW50c1wiOiB7XG4gICAgICAgIFwiaHJlZlwiOiBcImh0dHBzOi8vYXBpLmdpdGh1Yi5jb20vcmVwb3MvYmlud2llZGVyaGllci9kYWJibGUvcHVsbHMvMS9jb21tZW50c1wiXG4gICAgICB9LFxuICAgICAgXCJyZXZpZXdfY29tbWVudFwiOiB7XG4gICAgICAgIFwiaHJlZlwiOiBcImh0dHBzOi8vYXBpLmdpdGh1Yi5jb20vcmVwb3MvYmlud2llZGVyaGllci9kYWJibGUvcHVsbHMvY29tbWVudHN7L251bWJlcn1cIlxuICAgICAgfSxcbiAgICAgIFwiY29tbWl0c1wiOiB7XG4gICAgICAgIFwiaHJlZlwiOiBcImh0dHBzOi8vYXBpLmdpdGh1Yi5jb20vcmVwb3MvYmlud2llZGVyaGllci9kYWJibGUvcHVsbHMvMS9jb21taXRzXCJcbiAgICAgIH0sXG4gICAgICBcInN0YXR1c2VzXCI6IHtcbiAgICAgICAgXCJocmVmXCI6IFwiaHR0cHM6Ly9hcGkuZ2l0aHViLmNvbS9yZXBvcy9iaW53aWVkZXJoaWVyL2RhYmJsZS9zdGF0dXNlcy81NzAzODQyY2M1NzE1ZWQxZTM1OGQyM2ViYjY5M2RiMDk3NDdhZTliXCJcbiAgICAgIH1cbiAgICB9LFxuICAgIFwiYXV0aG9yX2Fzc29jaWF0aW9uXCI6IFwiT1dORVJcIixcbiAgICBcImF1dG9fbWVyZ2VcIjogbnVsbCxcbiAgICBcImFjdGl2ZV9sb2NrX3JlYXNvblwiOiBudWxsLFxuICAgIFwibWVyZ2VkXCI6IGZhbHNlLFxuICAgIFwibWVyZ2VhYmxlXCI6IG51bGwsXG4gICAgXCJyZWJhc2VhYmxlXCI6IG51bGwsXG4gICAgXCJtZXJnZWFibGVfc3RhdGVcIjogXCJ1bmtub3duXCIsXG4gICAgXCJtZXJnZWRfYnlcIjogbnVsbCxcbiAgICBcImNvbW1lbnRzXCI6IDAsXG4gICAgXCJyZXZpZXdfY29tbWVudHNcIjogMCxcbiAgICBcIm1haW50YWluZXJfY2FuX21vZGlmeVwiOiBmYWxzZSxcbiAgICBcImNvbW1pdHNcIjogMSxcbiAgICBcImFkZGl0aW9uc1wiOiAxLFxuICAgIFwiZGVsZXRpb25zXCI6IDEsXG4gICAgXCJjaGFuZ2VkX2ZpbGVzXCI6IDFcbiAgfSxcbiAgXCJyZXBvc2l0b3J5XCI6IHtcbiAgICBcImlkXCI6IDQ3MDIxMjAwMyxcbiAgICBcIm5vZGVfaWRcIjogXCJSX2tnRE9IQWJkb3dcIixcbiAgICBcIm5hbWVcIjogXCJkYWJibGVcIixcbiAgICBcImZ1bGxfbmFtZVwiOiBcImJpbndpZWRlcmhpZXIvZGFiYmxlXCIsXG4gICAgXCJwcml2YXRlXCI6IGZhbHNlLFxuICAgIFwib3duZXJcIjoge1xuICAgICAgXCJsb2dpblwiOiBcImJpbndpZWRlcmhpZXJcIixcbiAgICAgIFwiaWRcIjogNjY0NTk3LFxuICAgICAgXCJub2RlX2lkXCI6IFwiTURRNlZYTmxjalkyTkRVNU53PT1cIixcbiAgICAgIFwiYXZhdGFyX3VybFwiOiBcImh0dHBzOi8vYXZhdGFycy5naXRodWJ1c2VyY29udGVudC5jb20vdS82NjQ1OTc/dj00XCIsXG4gICAgICBcImdyYXZhdGFyX2lkXCI6IFwiXCIsXG4gICAgICBcInVybFwiOiBcImh0dHBzOi8vYXBpLmdpdGh1Yi5jb20vdXNlcnMvYmlud2llZGVyaGllclwiLFxuICAgICAgXCJodG1sX3VybFwiOiBcImh0dHBzOi8vZ2l0aHViLmNvbS9iaW53aWVkZXJoaWVyXCIsXG4gICAgICBcImZvbGxvd2Vyc191cmxcIjogXCJodHRwczovL2FwaS5naXRodWIuY29tL3VzZXJzL2JpbndpZWRlcmhpZXIvZm9sbG93ZXJzXCIsXG4gICAgICBcImZvbGxvd2luZ191cmxcIjogXCJodHRwczovL2FwaS5naXRodWIuY29tL3VzZXJzL2JpbndpZWRlcmhpZXIvZm9sbG93aW5ney9vdGhlcl91c2VyfVwiLFxuICAgICAgXCJnaXN0c191cmxcIjogXCJodHRwczovL2FwaS5naXRodWIuY29tL3VzZXJzL2JpbndpZWRlcmhpZXIvZ2lzdHN7L2dpc3RfaWR9XCIsXG4gICAgICBcInN0YXJyZWRfdXJsXCI6IFwiaHR0cHM6Ly9hcGkuZ2l0aHViLmNvbS91c2Vycy9iaW53aWVkZXJoaWVyL3N0YXJyZWR7L293bmVyfXsvcmVwb31cIixcbiAgICAgIFwic3Vic2NyaXB0aW9uc191cmxcIjogXCJodHRwczovL2FwaS5naXRodWIuY29tL3VzZXJzL2JpbndpZWRlcmhpZXIvc3Vic2NyaXB0aW9uc1wiLFxuICAgICAgXCJvcmdhbml6YXRpb25zX3VybFwiOiBcImh0dHBzOi8vYXBpLmdpdGh1Yi5jb20vdXNlcnMvYmlud2llZGVyaGllci9vcmdzXCIsXG4gICAgICBcInJlcG9zX3VybFwiOiBcImh0dHBzOi8vYXBpLmdpdGh1Yi5jb20vdXNlcnMvYmlud2llZGVyaGllci9yZXBvc1wiLFxuICAgICAgXCJldmVudHNfdXJsXCI6IFwiaHR0cHM6Ly9hcGkuZ2l0aHViLmNvbS91c2Vycy9iaW53aWVkZXJoaWVyL2V2ZW50c3svcHJpdmFjeX1cIixcbiAgICAgIFwicmVjZWl2ZWRfZXZlbnRzX3VybFwiOiBcImh0dHBzOi8vYXBpLmdpdGh1Yi5jb20vdXNlcnMvYmlud2llZGVyaGllci9yZWNlaXZlZF9ldmVudHNcIixcbiAgICAgIFwidHlwZVwiOiBcIlVzZXJcIixcbiAgICAgIFwic2l0ZV9hZG1pblwiOiBmYWxzZVxuICAgIH0sXG4gICAgXCJodG1sX3VybFwiOiBcImh0dHBzOi8vZ2l0aHViLmNvbS9iaW53aWVkZXJoaWVyL2RhYmJsZVwiLFxuICAgIFwiZGVzY3JpcHRpb25cIjogXCJBIHJlcG8gZm9yIGRhYmJsaW5nXCIsXG4gICAgXCJmb3JrXCI6IGZhbHNlLFxuICAgIFwidXJsXCI6IFwiaHR0cHM6Ly9hcGkuZ2l0aHViLmNvbS9yZXBvcy9iaW53aWVkZXJoaWVyL2RhYmJsZVwiLFxuICAgIFwiZm9ya3NfdXJsXCI6IFwiaHR0cHM6Ly9hcGkuZ2l0aHViLmNvbS9yZXBvcy9iaW53aWVkZXJoaWVyL2RhYmJsZS9mb3Jrc1wiLFxuICAgIFwia2V5c191cmxcIjogXCJodHRwczovL2FwaS5naXRodWIuY29tL3JlcG9zL2JpbndpZWRlcmhpZXIvZGFiYmxlL2tleXN7L2tleV9pZH1cIixcbiAgICBcImNvbGxhYm9yYXRvcnNfdXJsXCI6IFwiaHR0cHM6Ly9hcGkuZ2l0aHViLmNvbS9yZXBvcy9iaW53aWVkZXJoaWVyL2RhYmJsZS9jb2xsYWJvcmF0b3Jzey9jb2xsYWJvcmF0b3J9XCIsXG4gICAgXCJ0ZWFtc191cmxcIjogXCJodHRwczovL2FwaS5naXRodWIuY29tL3JlcG9zL2JpbndpZWRlcmhpZXIvZGFiYmxlL3RlYW1zXCIsXG4gICAgXCJob29rc191cmxcIjogXCJodHRwczovL2FwaS5naXRodWIuY29tL3JlcG9zL2JpbndpZWRlcmhpZXIvZGFiYmxlL2hvb2tzXCIsXG4gICAgXCJpc3N1ZV9ldmVudHNfdXJsXCI6IFwiaHR0cHM6Ly9hcGkuZ2l0aHViLmNvbS9yZXBvcy9iaW53aWVkZXJoaWVyL2RhYmJsZS9pc3N1ZXMvZXZlbnRzey9udW1iZXJ9XCIsXG4gICAgXCJldmVudHNfdXJsXCI6IFwiaHR0cHM6Ly9hcGkuZ2l0aHViLmNvbS9yZXBvcy9iaW53aWVkZXJoaWVyL2RhYmJsZS9ldmVudHNcIixcbiAgICBcImFzc2lnbmVlc191cmxcIjogXCJodHRwczovL2FwaS5naXRodWIuY29tL3JlcG9zL2JpbndpZWRlcmhpZXIvZGFiYmxlL2Fzc2lnbmVlc3svdXNlcn1cIixcbiAgICBcImJyYW5jaGVzX3VybFwiOiBcImh0dHBzOi8vYXBpLmdpdGh1Yi5jb20vcmVwb3MvYmlud2llZGVyaGllci9kYWJibGUvYnJhbmNoZXN7L2JyYW5jaH1cIixcbiAgICBcInRhZ3NfdXJsXCI6IFwiaHR0cHM6Ly9hcGkuZ2l0aHViLmNvbS9yZXBvcy9iaW53aWVkZXJoaWVyL2RhYmJsZS90YWdzXCIsXG4gICAgXCJibG9ic191cmxcIjogXCJodHRwczovL2FwaS5naXRodWIuY29tL3JlcG9zL2JpbndpZWRlcmhpZXIvZGFiYmxlL2dpdC9ibG9ic3svc2hhfVwiLFxuICAgIFwiZ2l0X3RhZ3NfdXJsXCI6IFwiaHR0cHM6Ly9hcGkuZ2l0aHViLmNvbS9yZXBvcy9iaW53aWVkZXJoaWVyL2RhYmJsZS9naXQvdGFnc3svc2hhfVwiLFxuICAgIFwiZ2l0X3JlZnNfdXJsXCI6IFwiaHR0cHM6Ly9hcGkuZ2l0aHViLmNvbS9yZXBvcy9iaW53aWVkZXJoaWVyL2RhYmJsZS9naXQvcmVmc3svc2hhfVwiLFxuICAgIFwidHJlZXNfdXJsXCI6IFwiaHR0cHM6Ly9hcGkuZ2l0aHViLmNvbS9yZXBvcy9iaW53aWVkZXJoaWVyL2RhYmJsZS9naXQvdHJlZXN7L3NoYX1cIixcbiAgICBcInN0YXR1c2VzX3VybFwiOiBcImh0dHBzOi8vYXBpLmdpdGh1Yi5jb20vcmVwb3MvYmlud2llZGVyaGllci9kYWJibGUvc3RhdHVzZXMve3NoYX1cIixcbiAgICBcImxhbmd1YWdlc191cmxcIjogXCJodHRwczovL2FwaS5naXRodWIuY29tL3JlcG9zL2JpbndpZWRlcmhpZXIvZGFiYmxlL2xhbmd1YWdlc1wiLFxuICAgIFwic3RhcmdhemVyc191cmxcIjogXCJodHRwczovL2FwaS5naXRodWIuY29tL3JlcG9zL2JpbndpZWRlcmhpZXIvZGFiYmxlL3N0YXJnYXplcnNcIixcbiAgICBcImNvbnRyaWJ1dG9yc191cmxcIjogXCJodHRwczovL2FwaS5naXRodWIuY29tL3JlcG9zL2JpbndpZWRlcmhpZXIvZGFiYmxlL2NvbnRyaWJ1dG9yc1wiLFxuICAgIFwic3Vic2NyaWJlcnNfdXJsXCI6IFwiaHR0cHM6Ly9hcGkuZ2l0aHViLmNvbS9yZXBvcy9iaW53aWVkZXJoaWVyL2RhYmJsZS9zdWJzY3JpYmVyc1wiLFxuICAgIFwic3Vic2NyaXB0aW9uX3VybFwiOiBcImh0dHBzOi8vYXBpLmdpdGh1Yi5jb20vcmVwb3MvYmlud2llZGVyaGllci9kYWJibGUvc3Vic2NyaXB0aW9uXCIsXG4gICAgXCJjb21taXRzX3VybFwiOiBcImh0dHBzOi8vYXBpLmdpdGh1Yi5jb20vcmVwb3MvYmlud2llZGVyaGllci9kYWJibGUvY29tbWl0c3svc2hhfVwiLFxuICAgIFwiZ2l0X2NvbW1pdHNfdXJsXCI6IFwiaHR0cHM6Ly9hcGkuZ2l0aHViLmNvbS9yZXBvcy9iaW53aWVkZXJoaWVyL2RhYmJsZS9naXQvY29tbWl0c3svc2hhfVwiLFxuICAgIFwiY29tbWVudHNfdXJsXCI6IFwiaHR0cHM6Ly9hcGkuZ2l0aHViLmNvbS9yZXBvcy9iaW53aWVkZXJoaWVyL2RhYmJsZS9jb21tZW50c3svbnVtYmVyfVwiLFxuICAgIFwiaXNzdWVfY29tbWVudF91cmxcIjogXCJodHRwczovL2FwaS5naXRodWIuY29tL3JlcG9zL2JpbndpZWRlcmhpZXIvZGFiYmxlL2lzc3Vlcy9jb21tZW50c3svbnVtYmVyfVwiLFxuICAgIFwiY29udGVudHNfdXJsXCI6IFwiaHR0cHM6Ly9hcGkuZ2l0aHViLmNvbS9yZXBvcy9iaW53aWVkZXJoaWVyL2RhYmJsZS9jb250ZW50cy97K3BhdGh9XCIsXG4gICAgXCJjb21wYXJlX3VybFwiOiBcImh0dHBzOi8vYXBpLmdpdGh1Yi5jb20vcmVwb3MvYmlud2llZGVyaGllci9kYWJibGUvY29tcGFyZS97YmFzZX0uLi57aGVhZH1cIixcbiAgICBcIm1lcmdlc191cmxcIjogXCJodHRwczovL2FwaS5naXRodWIuY29tL3JlcG9zL2JpbndpZWRlcmhpZXIvZGFiYmxlL21lcmdlc1wiLFxuICAgIFwiYXJjaGl2ZV91cmxcIjogXCJodHRwczovL2FwaS5naXRodWIuY29tL3JlcG9zL2JpbndpZWRlcmhpZXIvZGFiYmxlL3thcmNoaXZlX2Zvcm1hdH17L3JlZn1cIixcbiAgICBcImRvd25sb2Fkc191cmxcIjogXCJodHRwczovL2FwaS5naXRodWIuY29tL3JlcG9zL2JpbndpZWRlcmhpZXIvZGFiYmxlL2Rvd25sb2Fkc1wiLFxuICAgIFwiaXNzdWVzX3VybFwiOiBcImh0dHBzOi8vYXBpLmdpdGh1Yi5jb20vcmVwb3MvYmlud2llZGVyaGllci9kYWJibGUvaXNzdWVzey9udW1iZXJ9XCIsXG4gICAgXCJwdWxsc191cmxcIjogXCJodHRwczovL2FwaS5naXRodWIuY29tL3JlcG9zL2JpbndpZWRlcmhpZXIvZGFiYmxlL3B1bGxzey9udW1iZXJ9XCIsXG4gICAgXCJtaWxlc3RvbmVzX3VybFwiOiBcImh0dHBzOi8vYXBpLmdpdGh1Yi5jb20vcmVwb3MvYmlud2llZGVyaGllci9kYWJibGUvbWlsZXN0b25lc3svbnVtYmVyfVwiLFxuICAgIFwibm90aWZpY2F0aW9uc191cmxcIjogXCJodHRwczovL2FwaS5naXRodWIuY29tL3JlcG9zL2JpbndpZWRlcmhpZXIvZGFiYmxlL25vdGlmaWNhdGlvbnN7P3NpbmNlLGFsbCxwYXJ0aWNpcGF0aW5nfVwiLFxuICAgIFwibGFiZWxzX3VybFwiOiBcImh0dHBzOi8vYXBpLmdpdGh1Yi5jb20vcmVwb3MvYmlud2llZGVyaGllci9kYWJibGUvbGFiZWxzey9uYW1lfVwiLFxuICAgIFwicmVsZWFzZXNfdXJsXCI6IFwiaHR0cHM6Ly9hcGkuZ2l0aHViLmNvbS9yZXBvcy9iaW53aWVkZXJoaWVyL2RhYmJsZS9yZWxlYXNlc3svaWR9XCIsXG4gICAgXCJkZXBsb3ltZW50c191cmxcIjogXCJodHRwczovL2FwaS5naXRodWIuY29tL3JlcG9zL2JpbndpZWRlcmhpZXIvZGFiYmxlL2RlcGxveW1lbnRzXCIsXG4gICAgXCJjcmVhdGVkX2F0XCI6IFwiMjAyMi0wMy0xNVQxNTowNjoxN1pcIixcbiAgICBcInVwZGF0ZWRfYXRcIjogXCIyMDIyLTAzLTE1VDE1OjA2OjE3WlwiLFxuICAgIFwicHVzaGVkX2F0XCI6IFwiMjAyNC0wMy0yMVQwMjo1MjoxMFpcIixcbiAgICBcImdpdF91cmxcIjogXCJnaXQ6Ly9naXRodWIuY29tL2JpbndpZWRlcmhpZXIvZGFiYmxlLmdpdFwiLFxuICAgIFwic3NoX3VybFwiOiBcImdpdEBnaXRodWIuY29tOmJpbndpZWRlcmhpZXIvZGFiYmxlLmdpdFwiLFxuICAgIFwiY2xvbmVfdXJsXCI6IFwiaHR0cHM6Ly9naXRodWIuY29tL2JpbndpZWRlcmhpZXIvZGFiYmxlLmdpdFwiLFxuICAgIFwic3ZuX3VybFwiOiBcImh0dHBzOi8vZ2l0aHViLmNvbS9iaW53aWVkZXJoaWVyL2RhYmJsZVwiLFxuICAgIFwiaG9tZXBhZ2VcIjogbnVsbCxcbiAgICBcInNpemVcIjogMSxcbiAgICBcInN0YXJnYXplcnNfY291bnRcIjogMCxcbiAgICBcIndhdGNoZXJzX2NvdW50XCI6IDAsXG4gICAgXCJsYW5ndWFnZVwiOiBudWxsLFxuICAgIFwiaGFzX2lzc3Vlc1wiOiB0cnVlLFxuICAgIFwiaGFzX3Byb2plY3RzXCI6IHRydWUsXG4gICAgXCJoYXNfZG93bmxvYWRzXCI6IHRydWUsXG4gICAgXCJoYXNfd2lraVwiOiB0cnVlLFxuICAgIFwiaGFzX3BhZ2VzXCI6IGZhbHNlLFxuICAgIFwiaGFzX2Rpc2N1c3Npb25zXCI6IGZhbHNlLFxuICAgIFwiZm9ya3NfY291bnRcIjogMCxcbiAgICBcIm1pcnJvcl91cmxcIjogbnVsbCxcbiAgICBcImFyY2hpdmVkXCI6IGZhbHNlLFxuICAgIFwiZGlzYWJsZWRcIjogZmFsc2UsXG4gICAgXCJvcGVuX2lzc3Vlc19jb3VudFwiOiAxLFxuICAgIFwibGljZW5zZVwiOiBudWxsLFxuICAgIFwiYWxsb3dfZm9ya2luZ1wiOiB0cnVlLFxuICAgIFwiaXNfdGVtcGxhdGVcIjogZmFsc2UsXG4gICAgXCJ3ZWJfY29tbWl0X3NpZ25vZmZfcmVxdWlyZWRcIjogZmFsc2UsXG4gICAgXCJ0b3BpY3NcIjogW10sXG4gICAgXCJ2aXNpYmlsaXR5XCI6IFwicHVibGljXCIsXG4gICAgXCJmb3Jrc1wiOiAwLFxuICAgIFwib3Blbl9pc3N1ZXNcIjogMSxcbiAgICBcIndhdGNoZXJzXCI6IDAsXG4gICAgXCJkZWZhdWx0X2JyYW5jaFwiOiBcIm1haW5cIlxuICB9LFxuICBcInNlbmRlclwiOiB7XG4gICAgXCJsb2dpblwiOiBcImJpbndpZWRlcmhpZXJcIixcbiAgICBcImlkXCI6IDY2NDU5NyxcbiAgICBcIm5vZGVfaWRcIjogXCJNRFE2VlhObGNqWTJORFU1Tnc9PVwiLFxuICAgIFwiYXZhdGFyX3VybFwiOiBcImh0dHBzOi8vYXZhdGFycy5naXRodWJ1c2VyY29udGVudC5jb20vdS82NjQ1OTc/dj00XCIsXG4gICAgXCJncmF2YXRhcl9pZFwiOiBcIlwiLFxuICAgIFwidXJsXCI6IFwiaHR0cHM6Ly9hcGkuZ2l0aHViLmNvbS91c2Vycy9iaW53aWVkZXJoaWVyXCIsXG4gICAgXCJodG1sX3VybFwiOiBcImh0dHBzOi8vZ2l0aHViLmNvbS9iaW53aWVkZXJoaWVyXCIsXG4gICAgXCJmb2xsb3dlcnNfdXJsXCI6IFwiaHR0cHM6Ly9hcGkuZ2l0aHViLmNvbS91c2Vycy9iaW53aWVkZXJoaWVyL2ZvbGxvd2Vyc1wiLFxuICAgIFwiZm9sbG93aW5nX3VybFwiOiBcImh0dHBzOi8vYXBpLmdpdGh1Yi5jb20vdXNlcnMvYmlud2llZGVyaGllci9mb2xsb3dpbmd7L290aGVyX3VzZXJ9XCIsXG4gICAgXCJnaXN0c191cmxcIjogXCJodHRwczovL2FwaS5naXRodWIuY29tL3VzZXJzL2JpbndpZWRlcmhpZXIvZ2lzdHN7L2dpc3RfaWR9XCIsXG4gICAgXCJzdGFycmVkX3VybFwiOiBcImh0dHBzOi8vYXBpLmdpdGh1Yi5jb20vdXNlcnMvYmlud2llZGVyaGllci9zdGFycmVkey9vd25lcn17L3JlcG99XCIsXG4gICAgXCJzdWJzY3JpcHRpb25zX3VybFwiOiBcImh0dHBzOi8vYXBpLmdpdGh1Yi5jb20vdXNlcnMvYmlud2llZGVyaGllci9zdWJzY3JpcHRpb25zXCIsXG4gICAgXCJvcmdhbml6YXRpb25zX3VybFwiOiBcImh0dHBzOi8vYXBpLmdpdGh1Yi5jb20vdXNlcnMvYmlud2llZGVyaGllci9vcmdzXCIsXG4gICAgXCJyZXBvc191cmxcIjogXCJodHRwczovL2FwaS5naXRodWIuY29tL3VzZXJzL2JpbndpZWRlcmhpZXIvcmVwb3NcIixcbiAgICBcImV2ZW50c191cmxcIjogXCJodHRwczovL2FwaS5naXRodWIuY29tL3VzZXJzL2JpbndpZWRlcmhpZXIvZXZlbnRzey9wcml2YWN5fVwiLFxuICAgIFwicmVjZWl2ZWRfZXZlbnRzX3VybFwiOiBcImh0dHBzOi8vYXBpLmdpdGh1Yi5jb20vdXNlcnMvYmlud2llZGVyaGllci9yZWNlaXZlZF9ldmVudHNcIixcbiAgICBcInR5cGVcIjogXCJVc2VyXCIsXG4gICAgXCJzaXRlX2FkbWluXCI6IGZhbHNlXG4gIH1cbn1cbiIsImNvbmZpZyI6eyJ0ZW1wbGF0ZSI6InRleHQiLCJmdWxsU2NyZWVuSFRNTCI6ZmFsc2UsImZ1bmN0aW9ucyI6WyJzcHJpZyJdLCJvcHRpb25zIjpbImxpdmUiXSwiaW5wdXRUeXBlIjoieWFtbCJ9fQ==)) @@ -964,23 +965,46 @@ your templates there first ([example for Grafana alert](https://repeatit.io/#/sh Please note that the Go templating language is quite terrible. My apologies for using it for this feature. It is the best option for Go-based programs like ntfy. Stay calm and don't harm yourself or others in despair. **You can do it. I believe in you!** -Here's an example for a Grafana alert: +Here's an **example for a Grafana alert**:

![notification with actions](static/img/android-screenshot-template.jpg){ width=500 }
Grafana webhook, formatted using templates
-This was sent by configuring a webhook contact point in Grafana with the URL `https://nty.sh/mytpoic?tpl=1&t=%7B%7B.title%7D%7D&m=%7B%7Brange%20.alerts%7D%7D%7B%7B.annotations.summary%7D%7D%5Cn%5CnValues%3A%5Cn%7B%7Brange%20%24k%2C%24v%20%3A%3D%20.values%7D%7D-%20%7B%7B%24k%7D%7D%3D%7B%7B%24v%7D%7D%5Cn%7B%7Bend%7D%7D%7B%7Bend%7D%7D`. -The additional [URL encoding](https://www.urlencoder.org/) is necessary for Grafana, and may be required for other tools -too. Grafana then sent this JSON payload: +This was sent using the following templates and payloads -``` -{"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\nAnnotations:\n - summary = 15m load average too high\nSource: localhost:3000/alerting/grafana/NW9oDw-4z/view\nSilence: 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\n"} -``` +=== "Message template" + ``` + {{range .alerts}} + {{.annotations.summary}} + + Values: + {{range $k,$v := .values}} + - {{$k}}={{$v}} + {{end}} + {{end}} + ``` -Here's an easier example with a shorter JSON payload. The example uses the `message`/`m` and `title`/`t` query parameters, -but obviously this also works with the corresponding `Message`/`Title` headers: +=== "Title template" + ``` + {{.title}} + ``` + +=== "Encoded webhook URL" + ``` + # Additional URL encoding (see https://www.urlencoder.org/) is necessary for Grafana, + # and may be required for other tools too + + https://ntfy.sh/mytopic?tpl=1&t=%7B%7B.title%7D%7D&m=%7B%7Brange%20.alerts%7D%7D%7B%7B.annotations.summary%7D%7D%5Cn%5CnValues%3A%5Cn%7B%7Brange%20%24k%2C%24v%20%3A%3D%20.values%7D%7D-%20%7B%7B%24k%7D%7D%3D%7B%7B%24v%7D%7D%5Cn%7B%7Bend%7D%7D%7B%7Bend%7D%7D + ``` + +=== "Grafana-sent payload" + ``` + {"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\nAnnotations:\n - summary = 15m load average too high\nSource: localhost:3000/alerting/grafana/NW9oDw-4z/view\nSilence: 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\n"} + ``` + +Here's an **easier example with a shorter JSON payload**: === "Command line (curl)" ``` @@ -989,7 +1013,7 @@ but obviously this also works with the corresponding `Message`/`Title` headers: curl \ --globoff \ - -d '{"hostname": "philipp-pc", "error": {"level": "severe", "desc": "Disk has run out of space"}}' \ + -d '{"hostname": "phil-pc", "error": {"level": "severe", "desc": "Disk has run out of space"}}' \ 'ntfy.sh/mytopic?tpl=yes&t={{.hostname}}:+A+{{.error.level}}+error+has+occurred&m=Error+message:+{{.error.desc}}' ``` @@ -998,10 +1022,58 @@ but obviously this also works with the corresponding `Message`/`Title` headers: POST /mytopic?tpl=yes&t={{.hostname}}:+A+{{.error.level}}+error+has+occurred&m=Error+message:+{{.error.desc}} HTTP/1.1 Host: ntfy.sh - {"hostname": "philipp-pc", "error": {"level": "severe", "desc": "Disk has run out of space"}} + {"hostname": "phil-pc", "error": {"level": "severe", "desc": "Disk has run out of space"}} ``` -The example above would send a notification with a title `philipp-pc: A severe error has occurred` and a message +=== "JavaScript" + ``` javascript + fetch('https://ntfy.sh/mytopic?tpl=yes&t={{.hostname}}:+A+{{.error.level}}+error+has+occurred&m=Error+message:+{{.error.desc}}', { + method: 'POST', + body: '{"hostname": "phil-pc", "error": {"level": "severe", "desc": "Disk has run out of space"}}' + }) + ``` + +=== "Go" + ``` go + body := `{"hostname": "phil-pc", "error": {"level": "severe", "desc": "Disk has run out of space"}}` + uri := "https://ntfy.sh/mytopic?tpl=yes&t={{.hostname}}:+A+{{.error.level}}+error+has+occurred&m=Error+message:+{{.error.desc}}" + req, _ := http.NewRequest("POST", uri, strings.NewReader(body)) + http.DefaultClient.Do(req) + ``` + + +=== "PowerShell" + ``` powershell + $Request = @{ + Method = "POST" + URI = "https://ntfy.sh/mytopic?tpl=yes&t={{.hostname}}:+A+{{.error.level}}+error+has+occurred&m=Error+message:+{{.error.desc}}" + Body = '{"hostname": "phil-pc", "error": {"level": "severe", "desc": "Disk has run out of space"}}' + ContentType = "application/json" + } + Invoke-RestMethod @Request + ``` + +=== "Python" + ``` python + requests.post( + "https://ntfy.sh/mytopic?tpl=yes&t={{.hostname}}:+A+{{.error.level}}+error+has+occurred&m=Error+message:+{{.error.desc}}", + data='{"hostname": "phil-pc", "error": {"level": "severe", "desc": "Disk has run out of space"}}' + ) + ``` + +=== "PHP" + ``` php-inline + file_get_contents("https://ntfy.sh/mytopic?tpl=yes&t={{.hostname}}:+A+{{.error.level}}+error+has+occurred&m=Error+message:+{{.error.desc}}", false, stream_context_create([ + 'http' => [ + 'method' => 'POST', + 'header' => "Content-Type: application/json", + 'content' => '{"hostname": "phil-pc", "error": {"level": "severe", "desc": "Disk has run out of space"}}' + ] + ])); + ``` + +This example uses the `message`/`m` and `title`/`t` query parameters, but obviously this also works with the corresponding +`Message`/`Title` headers. It will send a notification with a title `phil-pc: A severe error has occurred` and a message `Error message: Disk has run out of space`. ## Publish as JSON diff --git a/docs/releases.md b/docs/releases.md index c82560ac..7d95a3cc 100644 --- a/docs/releases.md +++ b/docs/releases.md @@ -1342,7 +1342,7 @@ and the [ntfy Android app](https://github.com/binwiederhier/ntfy-android/release **Features:** -* You can now include a message and/or title template that will be filled with values from a JSON body, great for services that let you specify a webhook URL but do not let you change the webhook body (such as Grafana). ([#724](https://github.com/binwiederhier/ntfy/issues/724), thanks to [@wunter8](https://github.com/wunter8) for implementing) +* [Message templating](publish.md#message-templating): You can now include a message and/or title template that will be filled with values from a JSON body (e.g. `curl -gd '{"alert":"Disk space low"}' "ntfy.sh/mytopic?tpl=1&m={{.alert}}"`), which is great for services that let you specify a webhook URL but do not let you change the webhook body (such as GitHub, or Grafana). ([#724](https://github.com/binwiederhier/ntfy/issues/724), thanks to [@wunter8](https://github.com/wunter8) for implementing) ### ntfy Android app v1.16.1 (UNRELEASED) diff --git a/docs/static/js/extra.js b/docs/static/js/extra.js index 6ddf07a9..f9bc1e2b 100644 --- a/docs/static/js/extra.js +++ b/docs/static/js/extra.js @@ -1,99 +1,103 @@ // Link tabs, as per https://facelessuser.github.io/pymdown-extensions/extensions/tabbed/#linked-tabs -const savedCodeTab = localStorage.getItem('savedTab') -const codeTabs = document.querySelectorAll(".tabbed-set > input") +const savedCodeTab = localStorage.getItem("savedTab"); +const codeTabs = document.querySelectorAll(".tabbed-set > input"); for (const tab of codeTabs) { - tab.addEventListener("click", () => { - const current = document.querySelector(`label[for=${tab.id}]`) - const pos = current.getBoundingClientRect().top - const labelContent = current.innerHTML - const labels = document.querySelectorAll('.tabbed-set > label, .tabbed-alternate > .tabbed-labels > label') - for (const label of labels) { - if (label.innerHTML === labelContent) { - document.querySelector(`input[id=${label.getAttribute('for')}]`).checked = true - } - } - - // Preserve scroll position - const delta = (current.getBoundingClientRect().top) - pos - window.scrollBy(0, delta) - - // Save - localStorage.setItem('savedTab', labelContent) - }) - - // Select saved tab - const current = document.querySelector(`label[for=${tab.id}]`) - const labelContent = current.innerHTML - if (savedCodeTab === labelContent) { - tab.checked = true + tab.addEventListener("click", () => { + const current = document.querySelector(`label[for=${tab.id}]`); + const pos = current.getBoundingClientRect().top; + const labelContent = current.innerHTML; + const labels = document.querySelectorAll(".tabbed-set > label, .tabbed-alternate > .tabbed-labels > label"); + for (const label of labels) { + if (label.innerHTML === labelContent) { + document.querySelector(`input[id=${label.getAttribute("for")}]`).checked = true; + } } + + // Preserve scroll position + const delta = (current.getBoundingClientRect().top) - pos; + window.scrollBy(0, delta); + + // Save + localStorage.setItem("savedTab", labelContent); + }); + + // Select saved tab + const current = document.querySelector(`label[for=${tab.id}]`); + const labelContent = current.innerHTML; + if (savedCodeTab === labelContent) { + tab.checked = true; + } } // Lightbox for screenshot -const lightbox = document.createElement('div'); -lightbox.classList.add('lightbox'); +const lightbox = document.createElement("div"); +lightbox.classList.add("lightbox"); document.body.appendChild(lightbox); const showScreenshotOverlay = (e, el, group, index) => { - lightbox.classList.add('show'); - document.addEventListener('keydown', nextScreenshotKeyboardListener); - return showScreenshot(e, group, index); + lightbox.classList.add("show"); + document.addEventListener("keydown", nextScreenshotKeyboardListener); + return showScreenshot(e, group, index); }; const showScreenshot = (e, group, index) => { - const actualIndex = resolveScreenshotIndex(group, index); - lightbox.innerHTML = '
' + screenshots[group][actualIndex].innerHTML; - lightbox.querySelector('img').onclick = (e) => { return showScreenshot(e, group, actualIndex+1); }; - currentScreenshotGroup = group; - currentScreenshotIndex = actualIndex; - e.stopPropagation(); - return false; + const actualIndex = resolveScreenshotIndex(group, index); + lightbox.innerHTML = "
" + screenshots[group][actualIndex].innerHTML; + lightbox.querySelector("img").onclick = (e) => { + return showScreenshot(e, group, actualIndex + 1); + }; + currentScreenshotGroup = group; + currentScreenshotIndex = actualIndex; + e.stopPropagation(); + return false; }; const nextScreenshot = (e) => { - return showScreenshot(e, currentScreenshotGroup, currentScreenshotIndex+1); + return showScreenshot(e, currentScreenshotGroup, currentScreenshotIndex + 1); }; const previousScreenshot = (e) => { - return showScreenshot(e, currentScreenshotGroup, currentScreenshotIndex-1); + return showScreenshot(e, currentScreenshotGroup, currentScreenshotIndex - 1); }; const resolveScreenshotIndex = (group, index) => { - if (index < 0) { - return screenshots[group].length - 1; - } else if (index > screenshots[group].length - 1) { - return 0; - } - return index; + if (index < 0) { + return screenshots[group].length - 1; + } else if (index > screenshots[group].length - 1) { + return 0; + } + return index; }; const hideScreenshotOverlay = (e) => { - lightbox.classList.remove('show'); - document.removeEventListener('keydown', nextScreenshotKeyboardListener); + lightbox.classList.remove("show"); + document.removeEventListener("keydown", nextScreenshotKeyboardListener); }; const nextScreenshotKeyboardListener = (e) => { - switch (e.keyCode) { - case 37: - previousScreenshot(e); - break; - case 39: - nextScreenshot(e); - break; - } + switch (e.keyCode) { + case 37: + previousScreenshot(e); + break; + case 39: + nextScreenshot(e); + break; + } }; -let currentScreenshotGroup = ''; +let currentScreenshotGroup = ""; let currentScreenshotIndex = 0; let screenshots = {}; -Array.from(document.getElementsByClassName('screenshots')).forEach((sg) => { - const group = sg.id; - screenshots[group] = [...sg.querySelectorAll('a')]; - screenshots[group].forEach((el, index) => { - el.onclick = (e) => { return showScreenshotOverlay(e, el, group, index); }; - }); +Array.from(document.getElementsByClassName("screenshots")).forEach((sg) => { + const group = sg.id; + screenshots[group] = [...sg.querySelectorAll("a")]; + screenshots[group].forEach((el, index) => { + el.onclick = (e) => { + return showScreenshotOverlay(e, el, group, index); + }; + }); }); lightbox.onclick = hideScreenshotOverlay; diff --git a/go.mod b/go.mod index d1106ce9..8735b47a 100644 --- a/go.mod +++ b/go.mod @@ -35,7 +35,6 @@ require ( github.com/microcosm-cc/bluemonday v1.0.26 github.com/prometheus/client_golang v1.19.0 github.com/stripe/stripe-go/v74 v74.30.0 - github.com/tidwall/gjson v1.17.1 ) require ( @@ -70,8 +69,6 @@ require ( github.com/prometheus/procfs v0.13.0 // indirect github.com/russross/blackfriday/v2 v2.1.0 // indirect github.com/stretchr/objx v0.5.0 // indirect - github.com/tidwall/match v1.1.1 // indirect - github.com/tidwall/pretty v1.2.1 // indirect github.com/xrash/smetrics v0.0.0-20240312152122-5f08fbb34913 // indirect go.opencensus.io v0.24.0 // indirect go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.49.0 // indirect diff --git a/go.sum b/go.sum index 582a71f9..bde4bc8a 100644 --- a/go.sum +++ b/go.sum @@ -143,13 +143,6 @@ github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcU github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= github.com/stripe/stripe-go/v74 v74.30.0 h1:0Kf0KkeFnY7iRhOwvTerX0Ia1BRw+eV1CVJ51mGYAUY= github.com/stripe/stripe-go/v74 v74.30.0/go.mod h1:f9L6LvaXa35ja7eyvP6GQswoaIPaBRvGAimAO+udbBw= -github.com/tidwall/gjson v1.17.1 h1:wlYEnwqAHgzmhNUFfw7Xalt2JzQvsMx2Se4PcoFCT/U= -github.com/tidwall/gjson v1.17.1/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk= -github.com/tidwall/match v1.1.1 h1:+Ho715JplO36QYgwN9PGYNhgZvoUSc9X2c80KVTi+GA= -github.com/tidwall/match v1.1.1/go.mod h1:eRSPERbgtNPcGhD8UCthc6PmLEQXEWd3PRB5JTxsfmM= -github.com/tidwall/pretty v1.2.0/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU= -github.com/tidwall/pretty v1.2.1 h1:qjsOFOWWQl+N3RsoF5/ssm1pHmJJwhjlSbZ51I6wMl4= -github.com/tidwall/pretty v1.2.1/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU= github.com/urfave/cli/v2 v2.27.1 h1:8xSQ6szndafKVRmfyeUMxkNUJQMjL1F2zmsZ+qHpfho= github.com/urfave/cli/v2 v2.27.1/go.mod h1:8qnjx1vcq5s2/wpsqoZFndg2CE5tNFyrTvS6SinrnYQ= github.com/xrash/smetrics v0.0.0-20240312152122-5f08fbb34913 h1:+qGGcbkzsfDQNPPe9UDgpxAWQrhbbBXOYJFQDq/dtJw= From af16542d02c247a6f7f9786583b922a36ab68f78 Mon Sep 17 00:00:00 2001 From: binwiederhier Date: Sun, 24 Mar 2024 14:28:10 -0400 Subject: [PATCH 13/14] Removed unused vars --- server/server.go | 2 -- 1 file changed, 2 deletions(-) diff --git a/server/server.go b/server/server.go index 6c0a9f19..02e56e34 100644 --- a/server/server.go +++ b/server/server.go @@ -110,8 +110,6 @@ var ( fileRegex = regexp.MustCompile(`^/file/([-_A-Za-z0-9]{1,64})(?:\.[A-Za-z0-9]{1,16})?$`) urlRegex = regexp.MustCompile(`^https?://`) phoneNumberRegex = regexp.MustCompile(`^\+\d{1,100}$`) - templateVarRegex = regexp.MustCompile(`\${([^}]+)}`) - templateVarFormat = "${%s}" //go:embed site webFs embed.FS From 4692ca7b7fe6d6ec9154ba8cf551154cdaad7c5a Mon Sep 17 00:00:00 2001 From: binwiederhier Date: Sun, 24 Mar 2024 14:36:14 -0400 Subject: [PATCH 14/14] REvert parallel tests --- cmd/access_test.go | 2 -- cmd/config_loader_test.go | 1 - cmd/publish_test.go | 3 --- 3 files changed, 6 deletions(-) diff --git a/cmd/access_test.go b/cmd/access_test.go index 47aa9dae..81c9f2b9 100644 --- a/cmd/access_test.go +++ b/cmd/access_test.go @@ -10,7 +10,6 @@ import ( ) func TestCLI_Access_Show(t *testing.T) { - t.Parallel() s, conf, port := newTestServerWithAuth(t) defer test.StopServer(t, s, port) @@ -20,7 +19,6 @@ func TestCLI_Access_Show(t *testing.T) { } func TestCLI_Access_Grant_And_Publish(t *testing.T) { - t.Parallel() s, conf, port := newTestServerWithAuth(t) defer test.StopServer(t, s, port) diff --git a/cmd/config_loader_test.go b/cmd/config_loader_test.go index 67a4bcbe..7a7f2bf1 100644 --- a/cmd/config_loader_test.go +++ b/cmd/config_loader_test.go @@ -8,7 +8,6 @@ import ( ) func TestNewYamlSourceFromFile(t *testing.T) { - t.Parallel() filename := filepath.Join(t.TempDir(), "server.yml") contents := ` # Normal options diff --git a/cmd/publish_test.go b/cmd/publish_test.go index e03ae1dc..31d01cb5 100644 --- a/cmd/publish_test.go +++ b/cmd/publish_test.go @@ -17,7 +17,6 @@ import ( ) func TestCLI_Publish_Subscribe_Poll_Real_Server(t *testing.T) { - t.Parallel() testMessage := util.RandomString(10) app, _, _, _ := newTestApp() require.Nil(t, app.Run([]string{"ntfy", "publish", "ntfytest", "ntfy unit test " + testMessage})) @@ -36,7 +35,6 @@ func TestCLI_Publish_Subscribe_Poll_Real_Server(t *testing.T) { } func TestCLI_Publish_Subscribe_Poll(t *testing.T) { - t.Parallel() s, port := test.StartServer(t) defer test.StopServer(t, s, port) topic := fmt.Sprintf("http://127.0.0.1:%d/mytopic", port) @@ -53,7 +51,6 @@ func TestCLI_Publish_Subscribe_Poll(t *testing.T) { } func TestCLI_Publish_All_The_Things(t *testing.T) { - t.Parallel() s, port := test.StartServer(t) defer test.StopServer(t, s, port) topic := fmt.Sprintf("http://127.0.0.1:%d/mytopic", port)