Merge branch 'main' into attachments
This commit is contained in:
		
						commit
						24eb27d41c
					
				
					 8 changed files with 194 additions and 20 deletions
				
			
		|  | @ -6,7 +6,8 @@ | ||||||
| [](https://github.com/binwiederhier/ntfy/actions) | [](https://github.com/binwiederhier/ntfy/actions) | ||||||
| [](https://goreportcard.com/report/github.com/binwiederhier/ntfy) | [](https://goreportcard.com/report/github.com/binwiederhier/ntfy) | ||||||
| [](https://codecov.io/gh/binwiederhier/ntfy) | [](https://codecov.io/gh/binwiederhier/ntfy) | ||||||
| [](https://discord.gg/cT7ECsZj9w) | [](https://discord.gg/cT7ECsZj9w) | ||||||
|  | [](https://matrix.to/#/#ntfy:matrix.org) | ||||||
| [](https://ntfy.statuspage.io/) | [](https://ntfy.statuspage.io/) | ||||||
| 
 | 
 | ||||||
| **ntfy** (pronounce: *notify*) is a simple HTTP-based [pub-sub](https://en.wikipedia.org/wiki/Publish%E2%80%93subscribe_pattern) notification service. | **ntfy** (pronounce: *notify*) is a simple HTTP-based [pub-sub](https://en.wikipedia.org/wiki/Publish%E2%80%93subscribe_pattern) notification service. | ||||||
|  | @ -36,8 +37,9 @@ too. | ||||||
| I welcome any and all contributions. Just create a PR or an issue. | I welcome any and all contributions. Just create a PR or an issue. | ||||||
| 
 | 
 | ||||||
| ## Contact me | ## Contact me | ||||||
| You can directly contact me **[on Discord](https://discord.gg/cT7ECsZj9w)**, or via the [GitHub issues](https://github.com/binwiederhier/ntfy/issues), | You can directly contact me **[on Discord](https://discord.gg/cT7ECsZj9w)** or [on Matrix](https://matrix.to/#/#ntfy:matrix.org)  | ||||||
| or find more contact information [on my website](https://heckel.io/about). | (bridged from Discord), or via the [GitHub issues](https://github.com/binwiederhier/ntfy/issues), or find more contact information | ||||||
|  | [on my website](https://heckel.io/about). | ||||||
| 
 | 
 | ||||||
| ## License | ## License | ||||||
| Made with ❤️ by [Philipp C. Heckel](https://heckel.io).    | Made with ❤️ by [Philipp C. Heckel](https://heckel.io).    | ||||||
|  |  | ||||||
|  | @ -45,6 +45,11 @@ func WithDelay(delay string) PublishOption { | ||||||
| 	return WithHeader("X-Delay", delay) | 	return WithHeader("X-Delay", delay) | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
|  | // WithClick makes the notification action open the given URL as opposed to entering the detail view | ||||||
|  | func WithClick(url string) PublishOption { | ||||||
|  | 	return WithHeader("X-Click", url) | ||||||
|  | } | ||||||
|  | 
 | ||||||
| // WithEmail instructs the server to also send the message to the given e-mail address | // WithEmail instructs the server to also send the message to the given e-mail address | ||||||
| func WithEmail(email string) PublishOption { | func WithEmail(email string) PublishOption { | ||||||
| 	return WithHeader("X-Email", email) | 	return WithHeader("X-Email", email) | ||||||
|  |  | ||||||
|  | @ -20,6 +20,7 @@ var cmdPublish = &cli.Command{ | ||||||
| 		&cli.StringFlag{Name: "priority", Aliases: []string{"p"}, Usage: "priority of the message (1=min, 2=low, 3=default, 4=high, 5=max)"}, | 		&cli.StringFlag{Name: "priority", Aliases: []string{"p"}, Usage: "priority of the message (1=min, 2=low, 3=default, 4=high, 5=max)"}, | ||||||
| 		&cli.StringFlag{Name: "tags", Aliases: []string{"tag", "T"}, Usage: "comma separated list of tags and emojis"}, | 		&cli.StringFlag{Name: "tags", Aliases: []string{"tag", "T"}, Usage: "comma separated list of tags and emojis"}, | ||||||
| 		&cli.StringFlag{Name: "delay", Aliases: []string{"at", "in", "D"}, Usage: "delay/schedule message"}, | 		&cli.StringFlag{Name: "delay", Aliases: []string{"at", "in", "D"}, Usage: "delay/schedule message"}, | ||||||
|  | 		&cli.StringFlag{Name: "click", Aliases: []string{"U"}, Usage: "URL to open when notification is clicked"}, | ||||||
| 		&cli.StringFlag{Name: "email", Aliases: []string{"e-mail", "mail", "e"}, Usage: "also send to e-mail address"}, | 		&cli.StringFlag{Name: "email", Aliases: []string{"e-mail", "mail", "e"}, Usage: "also send to e-mail address"}, | ||||||
| 		&cli.BoolFlag{Name: "no-cache", Aliases: []string{"C"}, Usage: "do not cache message server-side"}, | 		&cli.BoolFlag{Name: "no-cache", Aliases: []string{"C"}, Usage: "do not cache message server-side"}, | ||||||
| 		&cli.BoolFlag{Name: "no-firebase", Aliases: []string{"F"}, Usage: "do not forward message to Firebase"}, | 		&cli.BoolFlag{Name: "no-firebase", Aliases: []string{"F"}, Usage: "do not forward message to Firebase"}, | ||||||
|  | @ -35,6 +36,7 @@ Examples: | ||||||
|   ntfy pub --delay=10s delayed_topic Laterzz              # Delay message by 10s |   ntfy pub --delay=10s delayed_topic Laterzz              # Delay message by 10s | ||||||
|   ntfy pub --at=8:30am delayed_topic Laterzz              # Send message at 8:30am |   ntfy pub --at=8:30am delayed_topic Laterzz              # Send message at 8:30am | ||||||
|   ntfy pub -e phil@example.com alerts 'App is down!'      # Also send email to phil@example.com |   ntfy pub -e phil@example.com alerts 'App is down!'      # Also send email to phil@example.com | ||||||
|  |   ntfy pub --click="https://reddit.com" redd 'New msg'    # Opens Reddit when notification is clicked | ||||||
|   ntfy trigger mywebhook                                  # Sending without message, useful for webhooks |   ntfy trigger mywebhook                                  # Sending without message, useful for webhooks | ||||||
| 
 | 
 | ||||||
| Please also check out the docs on publishing messages. Especially for the --tags and --delay options,  | Please also check out the docs on publishing messages. Especially for the --tags and --delay options,  | ||||||
|  | @ -56,6 +58,7 @@ func execPublish(c *cli.Context) error { | ||||||
| 	priority := c.String("priority") | 	priority := c.String("priority") | ||||||
| 	tags := c.String("tags") | 	tags := c.String("tags") | ||||||
| 	delay := c.String("delay") | 	delay := c.String("delay") | ||||||
|  | 	click := c.String("click") | ||||||
| 	email := c.String("email") | 	email := c.String("email") | ||||||
| 	noCache := c.Bool("no-cache") | 	noCache := c.Bool("no-cache") | ||||||
| 	noFirebase := c.Bool("no-firebase") | 	noFirebase := c.Bool("no-firebase") | ||||||
|  | @ -78,6 +81,9 @@ func execPublish(c *cli.Context) error { | ||||||
| 	if delay != "" { | 	if delay != "" { | ||||||
| 		options = append(options, client.WithDelay(delay)) | 		options = append(options, client.WithDelay(delay)) | ||||||
| 	} | 	} | ||||||
|  | 	if click != "" { | ||||||
|  | 		options = append(options, client.WithClick(email)) | ||||||
|  | 	} | ||||||
| 	if email != "" { | 	if email != "" { | ||||||
| 		options = append(options, client.WithEmail(email)) | 		options = append(options, client.WithEmail(email)) | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
|  | @ -592,6 +592,73 @@ 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'); |     file_get_contents('https://ntfy.sh/mywebhook/publish?message=Webhook+triggered&priority=high&tags=warning,skull'); | ||||||
|     ``` |     ``` | ||||||
| 
 | 
 | ||||||
|  | ## Click action | ||||||
|  | You can define which URL to open when a notification is clicked. This may be useful if your notification is related  | ||||||
|  | to a Zabbix alert or a transaction that you'd like to provide the deep-link for. Tapping the notification will open | ||||||
|  | the web browser (or the app) and open the website. | ||||||
|  | 
 | ||||||
|  | Here's an example that will open Reddit when the notification is clicked: | ||||||
|  | 
 | ||||||
|  | === "Command line (curl)" | ||||||
|  |     ``` | ||||||
|  |     curl \ | ||||||
|  |         -d "New messages on Reddit" \ | ||||||
|  |         -H "Click: https://www.reddit.com/message/messages" \ | ||||||
|  |         ntfy.sh/reddit_alerts | ||||||
|  |     ``` | ||||||
|  | 
 | ||||||
|  | === "ntfy CLI" | ||||||
|  |     ``` | ||||||
|  |     ntfy publish \ | ||||||
|  |         --click="https://www.reddit.com/message/messages" \ | ||||||
|  |         reddit_alerts "New messages on Reddit" | ||||||
|  |     ``` | ||||||
|  | 
 | ||||||
|  | === "HTTP" | ||||||
|  |     ``` http | ||||||
|  |     POST /reddit_alerts HTTP/1.1 | ||||||
|  |     Host: ntfy.sh | ||||||
|  |     Click: https://www.reddit.com/message/messages  | ||||||
|  | 
 | ||||||
|  |     New messages on Reddit | ||||||
|  |     ``` | ||||||
|  | 
 | ||||||
|  | === "JavaScript" | ||||||
|  |     ``` javascript | ||||||
|  |     fetch('https://ntfy.sh/reddit_alerts', { | ||||||
|  |         method: 'POST', | ||||||
|  |         body: 'New messages on Reddit', | ||||||
|  |         headers: { 'Click': 'https://www.reddit.com/message/messages' } | ||||||
|  |     }) | ||||||
|  |     ``` | ||||||
|  | 
 | ||||||
|  | === "Go" | ||||||
|  |     ``` go | ||||||
|  |     req, _ := http.NewRequest("POST", "https://ntfy.sh/reddit_alerts", strings.NewReader("New messages on Reddit")) | ||||||
|  |     req.Header.Set("Click", "https://www.reddit.com/message/messages") | ||||||
|  |     http.DefaultClient.Do(req) | ||||||
|  |     ``` | ||||||
|  | 
 | ||||||
|  | === "Python" | ||||||
|  |     ``` python | ||||||
|  |     requests.post("https://ntfy.sh/reddit_alerts", | ||||||
|  |         data="New messages on Reddit", | ||||||
|  |         headers={ "Click": "https://www.reddit.com/message/messages" }) | ||||||
|  |     ``` | ||||||
|  | 
 | ||||||
|  | === "PHP" | ||||||
|  |     ``` php-inline | ||||||
|  |     file_get_contents('https://ntfy.sh/reddit_alerts', false, stream_context_create([ | ||||||
|  |         'http' => [ | ||||||
|  |             'method' => 'POST', | ||||||
|  |             'header' => | ||||||
|  |                 "Content-Type: text/plain\r\n" . | ||||||
|  |                 "Click: https://www.reddit.com/message/messages", | ||||||
|  |             'content' => 'New messages on Reddit' | ||||||
|  |         ] | ||||||
|  |     ])); | ||||||
|  |     ``` | ||||||
|  | 
 | ||||||
| ## Send files + URLs | ## Send files + URLs | ||||||
| ``` | ``` | ||||||
| curl -T image.jpg ntfy.sh/howdy | curl -T image.jpg ntfy.sh/howdy | ||||||
|  | @ -903,6 +970,7 @@ and can be passed as **HTTP headers** or **query parameters in the URL**. They a | ||||||
| | `X-Priority` | `Priority`, `prio`, `p` | [Message priority](#message-priority) | | | `X-Priority` | `Priority`, `prio`, `p` | [Message priority](#message-priority) | | ||||||
| | `X-Tags` | `Tags`, `Tag`, `ta` | [Tags and emojis](#tags-emojis) | | | `X-Tags` | `Tags`, `Tag`, `ta` | [Tags and emojis](#tags-emojis) | | ||||||
| | `X-Delay` | `Delay`, `X-At`, `At`, `X-In`, `In` | Timestamp or duration for [delayed delivery](#scheduled-delivery) | | | `X-Delay` | `Delay`, `X-At`, `At`, `X-In`, `In` | Timestamp or duration for [delayed delivery](#scheduled-delivery) | | ||||||
|  | | `X-Click` | `Click` | URL to open when [notification is clicked](#click-action) | | ||||||
| | `X-Filename` | `Filename`, `file`, `f` | XXXXXXXXXXXXXXXX | | | `X-Filename` | `Filename`, `file`, `f` | XXXXXXXXXXXXXXXX | | ||||||
| | `X-Email` | `X-E-Mail`, `Email`, `E-Mail`, `mail`, `e` | E-mail address for [e-mail notifications](#e-mail-notifications) | | | `X-Email` | `X-E-Mail`, `Email`, `E-Mail`, `mail`, `e` | E-mail address for [e-mail notifications](#e-mail-notifications) | | ||||||
| | `X-Cache` | `Cache` | Allows disabling [message caching](#message-caching) | | | `X-Cache` | `Cache` | Allows disabling [message caching](#message-caching) | | ||||||
|  |  | ||||||
|  | @ -22,6 +22,7 @@ const ( | ||||||
| 			title TEXT NOT NULL, | 			title TEXT NOT NULL, | ||||||
| 			priority INT NOT NULL, | 			priority INT NOT NULL, | ||||||
| 			tags TEXT NOT NULL, | 			tags TEXT NOT NULL, | ||||||
|  | 			click TEXT NOT NULL, | ||||||
| 			attachment_name TEXT NOT NULL, | 			attachment_name TEXT NOT NULL, | ||||||
| 			attachment_type TEXT NOT NULL, | 			attachment_type TEXT NOT NULL, | ||||||
| 			attachment_size INT NOT NULL, | 			attachment_size INT NOT NULL, | ||||||
|  | @ -34,24 +35,24 @@ const ( | ||||||
| 		COMMIT; | 		COMMIT; | ||||||
| 	` | 	` | ||||||
| 	insertMessageQuery = ` | 	insertMessageQuery = ` | ||||||
| 		INSERT INTO messages (id, time, topic, message, title, priority, tags, attachment_name, attachment_type, attachment_size, attachment_expires, attachment_preview_url, attachment_url, published)  | 		INSERT INTO messages (id, time, topic, message, title, priority, tags, click, attachment_name, attachment_type, attachment_size, attachment_expires, attachment_preview_url, attachment_url, published)  | ||||||
| 		VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) | 		VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) | ||||||
| 	` | 	` | ||||||
| 	pruneMessagesQuery           = `DELETE FROM messages WHERE time < ? AND published = 1` | 	pruneMessagesQuery           = `DELETE FROM messages WHERE time < ? AND published = 1` | ||||||
| 	selectMessagesSinceTimeQuery = ` | 	selectMessagesSinceTimeQuery = ` | ||||||
| 		SELECT id, time, topic, message, title, priority, tags, attachment_name, attachment_type, attachment_size, attachment_expires, attachment_preview_url, attachment_url | 		SELECT id, time, topic, message, title, priority, tags, click, attachment_name, attachment_type, attachment_size, attachment_expires, attachment_preview_url, attachment_url | ||||||
| 		FROM messages  | 		FROM messages  | ||||||
| 		WHERE topic = ? AND time >= ? AND published = 1 | 		WHERE topic = ? AND time >= ? AND published = 1 | ||||||
| 		ORDER BY time ASC | 		ORDER BY time ASC | ||||||
| 	` | 	` | ||||||
| 	selectMessagesSinceTimeIncludeScheduledQuery = ` | 	selectMessagesSinceTimeIncludeScheduledQuery = ` | ||||||
| 		SELECT id, time, topic, message, title, priority, tags, attachment_name, attachment_type, attachment_size, attachment_expires, attachment_preview_url, attachment_url | 		SELECT id, time, topic, message, title, priority, tags, click, attachment_name, attachment_type, attachment_size, attachment_expires, attachment_preview_url, attachment_url | ||||||
| 		FROM messages  | 		FROM messages  | ||||||
| 		WHERE topic = ? AND time >= ? | 		WHERE topic = ? AND time >= ? | ||||||
| 		ORDER BY time ASC | 		ORDER BY time ASC | ||||||
| 	` | 	` | ||||||
| 	selectMessagesDueQuery = ` | 	selectMessagesDueQuery = ` | ||||||
| 		SELECT id, time, topic, message, title, priority, tags, attachment_name, attachment_type, attachment_size, attachment_expires, attachment_preview_url, attachment_url | 		SELECT id, time, topic, message, title, priority, tags, click, attachment_name, attachment_type, attachment_size, attachment_expires, attachment_preview_url, attachment_url | ||||||
| 		FROM messages  | 		FROM messages  | ||||||
| 		WHERE time <= ? AND published = 0 | 		WHERE time <= ? AND published = 0 | ||||||
| 	` | 	` | ||||||
|  | @ -91,11 +92,13 @@ const ( | ||||||
| 	// 2 -> 3 | 	// 2 -> 3 | ||||||
| 	migrate2To3AlterMessagesTableQuery = ` | 	migrate2To3AlterMessagesTableQuery = ` | ||||||
| 		BEGIN; | 		BEGIN; | ||||||
| 		ALTER TABLE messages ADD COLUMN attachment_name TEXT NOT NULL; | 		ALTER TABLE messages ADD COLUMN click TEXT NOT NULL DEFAULT(''); | ||||||
| 		ALTER TABLE messages ADD COLUMN attachment_type TEXT NOT NULL; | 		ALTER TABLE messages ADD COLUMN attachment_name TEXT NOT NULL DEFAULT(''); | ||||||
| 		ALTER TABLE messages ADD COLUMN attachment_size INT NOT NULL; | 		ALTER TABLE messages ADD COLUMN attachment_type TEXT NOT NULL DEFAULT(''); | ||||||
| 		ALTER TABLE messages ADD COLUMN attachment_expires INT NOT NULL; | 		ALTER TABLE messages ADD COLUMN attachment_size INT NOT NULL DEFAULT('0'); | ||||||
| 		ALTER TABLE messages ADD COLUMN attachment_url TEXT NOT NULL; | 		ALTER TABLE messages ADD COLUMN attachment_expires INT NOT NULL DEFAULT('0'); | ||||||
|  | 		ALTER TABLE messages ADD COLUMN attachment_preview_url TEXT NOT NULL DEFAULT(''); | ||||||
|  | 		ALTER TABLE messages ADD COLUMN attachment_url TEXT NOT NULL DEFAULT(''); | ||||||
| 		COMMIT; | 		COMMIT; | ||||||
| 	` | 	` | ||||||
| ) | ) | ||||||
|  | @ -144,6 +147,7 @@ func (c *sqliteCache) AddMessage(m *message) error { | ||||||
| 		m.Title, | 		m.Title, | ||||||
| 		m.Priority, | 		m.Priority, | ||||||
| 		tags, | 		tags, | ||||||
|  | 		m.Click, | ||||||
| 		attachmentName, | 		attachmentName, | ||||||
| 		attachmentType, | 		attachmentType, | ||||||
| 		attachmentSize, | 		attachmentSize, | ||||||
|  | @ -234,8 +238,8 @@ func readMessages(rows *sql.Rows) ([]*message, error) { | ||||||
| 	for rows.Next() { | 	for rows.Next() { | ||||||
| 		var timestamp, attachmentSize, attachmentExpires int64 | 		var timestamp, attachmentSize, attachmentExpires int64 | ||||||
| 		var priority int | 		var priority int | ||||||
| 		var id, topic, msg, title, tagsStr, attachmentName, attachmentType, attachmentPreviewURL, attachmentURL string | 		var id, topic, msg, title, tagsStr, click, attachmentName, attachmentType, attachmentPreviewURL, attachmentURL string | ||||||
| 		if err := rows.Scan(&id, ×tamp, &topic, &msg, &title, &priority, &tagsStr, &attachmentName, &attachmentType, &attachmentSize, &attachmentExpires, &attachmentPreviewURL, &attachmentURL); err != nil { | 		if err := rows.Scan(&id, ×tamp, &topic, &msg, &title, &priority, &tagsStr, &click, &attachmentName, &attachmentType, &attachmentSize, &attachmentExpires, &attachmentPreviewURL, &attachmentURL); err != nil { | ||||||
| 			return nil, err | 			return nil, err | ||||||
| 		} | 		} | ||||||
| 		var tags []string | 		var tags []string | ||||||
|  | @ -262,6 +266,7 @@ func readMessages(rows *sql.Rows) ([]*message, error) { | ||||||
| 			Title:      title, | 			Title:      title, | ||||||
| 			Priority:   priority, | 			Priority:   priority, | ||||||
| 			Tags:       tags, | 			Tags:       tags, | ||||||
|  | 			Click:      click, | ||||||
| 			Attachment: att, | 			Attachment: att, | ||||||
| 		}) | 		}) | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
|  | @ -24,9 +24,10 @@ type message struct { | ||||||
| 	Topic      string      `json:"topic"` | 	Topic      string      `json:"topic"` | ||||||
| 	Priority   int         `json:"priority,omitempty"` | 	Priority   int         `json:"priority,omitempty"` | ||||||
| 	Tags       []string    `json:"tags,omitempty"` | 	Tags       []string    `json:"tags,omitempty"` | ||||||
|  | 	Click      string      `json:"click,omitempty"` | ||||||
|  | 	Attachment *attachment `json:"attachment,omitempty"` | ||||||
| 	Title      string      `json:"title,omitempty"` | 	Title      string      `json:"title,omitempty"` | ||||||
| 	Message    string      `json:"message,omitempty"` | 	Message    string      `json:"message,omitempty"` | ||||||
| 	Attachment *attachment `json:"attachment,omitempty"` |  | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| type attachment struct { | type attachment struct { | ||||||
|  |  | ||||||
|  | @ -148,6 +148,7 @@ var ( | ||||||
| const ( | const ( | ||||||
| 	firebaseControlTopic = "~control" // See Android if changed | 	firebaseControlTopic = "~control" // See Android if changed | ||||||
| 	emptyMessageBody     = "triggered" | 	emptyMessageBody     = "triggered" | ||||||
|  | 	fcmMessageLimit      = 4000 // see maybeTruncateFCMMessage for details | ||||||
| ) | ) | ||||||
| 
 | 
 | ||||||
| // New instantiates a new Server. It creates the cache and adds a Firebase | // New instantiates a new Server. It creates the cache and adds a Firebase | ||||||
|  | @ -224,6 +225,7 @@ func createFirebaseSubscriber(conf *Config) (subscriber, error) { | ||||||
| 				"topic":    m.Topic, | 				"topic":    m.Topic, | ||||||
| 				"priority": fmt.Sprintf("%d", m.Priority), | 				"priority": fmt.Sprintf("%d", m.Priority), | ||||||
| 				"tags":     strings.Join(m.Tags, ","), | 				"tags":     strings.Join(m.Tags, ","), | ||||||
|  | 				"click":    m.Click, | ||||||
| 				"title":    m.Title, | 				"title":    m.Title, | ||||||
| 				"message":  m.Message, | 				"message":  m.Message, | ||||||
| 			} | 			} | ||||||
|  | @ -236,14 +238,40 @@ func createFirebaseSubscriber(conf *Config) (subscriber, error) { | ||||||
| 				data["attachment_url"] = m.Attachment.URL | 				data["attachment_url"] = m.Attachment.URL | ||||||
| 			} | 			} | ||||||
| 		} | 		} | ||||||
| 		_, err := msg.Send(context.Background(), &messaging.Message{ | 		var androidConfig *messaging.AndroidConfig | ||||||
| 			Topic: m.Topic, | 		if m.Priority >= 4 { | ||||||
| 			Data:  data, | 			androidConfig = &messaging.AndroidConfig{ | ||||||
| 		}) | 				Priority: "high", | ||||||
|  | 			} | ||||||
|  | 		} | ||||||
|  | 		_, err := msg.Send(context.Background(), maybeTruncateFCMMessage(&messaging.Message{ | ||||||
|  | 			Topic:   m.Topic, | ||||||
|  | 			Data:    data, | ||||||
|  | 			Android: androidConfig, | ||||||
|  | 		})) | ||||||
| 		return err | 		return err | ||||||
| 	}, nil | 	}, nil | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
|  | // maybeTruncateFCMMessage performs best-effort truncation of FCM messages. | ||||||
|  | // The docs say the limit is 4000 characters, but during testing it wasn't quite clear | ||||||
|  | // what fields matter; so we're just capping the serialized JSON to 4000 bytes. | ||||||
|  | func maybeTruncateFCMMessage(m *messaging.Message) *messaging.Message { | ||||||
|  | 	s, err := json.Marshal(m) | ||||||
|  | 	if err != nil { | ||||||
|  | 		return m | ||||||
|  | 	} | ||||||
|  | 	if len(s) > fcmMessageLimit { | ||||||
|  | 		over := len(s) - fcmMessageLimit + 16 // = len("truncated":"1",), sigh ... | ||||||
|  | 		message, ok := m.Data["message"] | ||||||
|  | 		if ok && len(message) > over { | ||||||
|  | 			m.Data["truncated"] = "1" | ||||||
|  | 			m.Data["message"] = message[:len(message)-over] | ||||||
|  | 		} | ||||||
|  | 	} | ||||||
|  | 	return m | ||||||
|  | } | ||||||
|  | 
 | ||||||
| // Run executes the main server. It listens on HTTP (+ HTTPS, if configured), and starts | // Run executes the main server. It listens on HTTP (+ HTTPS, if configured), and starts | ||||||
| // a manager go routine to print stats and prune messages. | // a manager go routine to print stats and prune messages. | ||||||
| func (s *Server) Run() error { | func (s *Server) Run() error { | ||||||
|  | @ -514,6 +542,7 @@ func (s *Server) parsePublishParams(r *http.Request, m *message) (cache bool, fi | ||||||
| 	firebase = readParam(r, "x-firebase", "firebase") != "no" | 	firebase = readParam(r, "x-firebase", "firebase") != "no" | ||||||
| 	email = readParam(r, "x-email", "x-e-mail", "email", "e-mail", "mail", "e") | 	email = readParam(r, "x-email", "x-e-mail", "email", "e-mail", "mail", "e") | ||||||
| 	m.Title = readParam(r, "x-title", "title", "t") | 	m.Title = readParam(r, "x-title", "title", "t") | ||||||
|  | 	m.Click = readParam(r, "x-click", "click") | ||||||
| 	messageStr := readParam(r, "x-message", "message", "m") | 	messageStr := readParam(r, "x-message", "message", "m") | ||||||
| 	if messageStr != "" { | 	if messageStr != "" { | ||||||
| 		m.Message = messageStr | 		m.Message = messageStr | ||||||
|  |  | ||||||
|  | @ -4,6 +4,7 @@ import ( | ||||||
| 	"bufio" | 	"bufio" | ||||||
| 	"context" | 	"context" | ||||||
| 	"encoding/json" | 	"encoding/json" | ||||||
|  | 	"firebase.google.com/go/messaging" | ||||||
| 	"fmt" | 	"fmt" | ||||||
| 	"github.com/stretchr/testify/require" | 	"github.com/stretchr/testify/require" | ||||||
| 	"net/http" | 	"net/http" | ||||||
|  | @ -582,6 +583,63 @@ func TestServer_UnifiedPushDiscovery(t *testing.T) { | ||||||
| 	require.Equal(t, `{"unifiedpush":{"version":1}}`+"\n", response.Body.String()) | 	require.Equal(t, `{"unifiedpush":{"version":1}}`+"\n", response.Body.String()) | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
|  | func TestServer_MaybeTruncateFCMMessage(t *testing.T) { | ||||||
|  | 	origMessage := strings.Repeat("this is a long string", 300) | ||||||
|  | 	origFCMMessage := &messaging.Message{ | ||||||
|  | 		Topic: "mytopic", | ||||||
|  | 		Data: map[string]string{ | ||||||
|  | 			"id":       "abcdefg", | ||||||
|  | 			"time":     "1641324761", | ||||||
|  | 			"event":    "message", | ||||||
|  | 			"topic":    "mytopic", | ||||||
|  | 			"priority": "0", | ||||||
|  | 			"tags":     "", | ||||||
|  | 			"title":    "", | ||||||
|  | 			"message":  origMessage, | ||||||
|  | 		}, | ||||||
|  | 		Android: &messaging.AndroidConfig{ | ||||||
|  | 			Priority: "high", | ||||||
|  | 		}, | ||||||
|  | 	} | ||||||
|  | 	origMessageLength := len(origFCMMessage.Data["message"]) | ||||||
|  | 	serializedOrigFCMMessage, _ := json.Marshal(origFCMMessage) | ||||||
|  | 	require.Greater(t, len(serializedOrigFCMMessage), fcmMessageLimit) // Pre-condition | ||||||
|  | 
 | ||||||
|  | 	truncatedFCMMessage := maybeTruncateFCMMessage(origFCMMessage) | ||||||
|  | 	truncatedMessageLength := len(truncatedFCMMessage.Data["message"]) | ||||||
|  | 	serializedTruncatedFCMMessage, _ := json.Marshal(truncatedFCMMessage) | ||||||
|  | 	require.Equal(t, fcmMessageLimit, len(serializedTruncatedFCMMessage)) | ||||||
|  | 	require.Equal(t, "1", truncatedFCMMessage.Data["truncated"]) | ||||||
|  | 	require.NotEqual(t, origMessageLength, truncatedMessageLength) | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | func TestServer_MaybeTruncateFCMMessage_NotTooLong(t *testing.T) { | ||||||
|  | 	origMessage := "not really a long string" | ||||||
|  | 	origFCMMessage := &messaging.Message{ | ||||||
|  | 		Topic: "mytopic", | ||||||
|  | 		Data: map[string]string{ | ||||||
|  | 			"id":       "abcdefg", | ||||||
|  | 			"time":     "1641324761", | ||||||
|  | 			"event":    "message", | ||||||
|  | 			"topic":    "mytopic", | ||||||
|  | 			"priority": "0", | ||||||
|  | 			"tags":     "", | ||||||
|  | 			"title":    "", | ||||||
|  | 			"message":  origMessage, | ||||||
|  | 		}, | ||||||
|  | 	} | ||||||
|  | 	origMessageLength := len(origFCMMessage.Data["message"]) | ||||||
|  | 	serializedOrigFCMMessage, _ := json.Marshal(origFCMMessage) | ||||||
|  | 	require.LessOrEqual(t, len(serializedOrigFCMMessage), fcmMessageLimit) // Pre-condition | ||||||
|  | 
 | ||||||
|  | 	notTruncatedFCMMessage := maybeTruncateFCMMessage(origFCMMessage) | ||||||
|  | 	notTruncatedMessageLength := len(notTruncatedFCMMessage.Data["message"]) | ||||||
|  | 	serializedNotTruncatedFCMMessage, _ := json.Marshal(notTruncatedFCMMessage) | ||||||
|  | 	require.Equal(t, origMessageLength, notTruncatedMessageLength) | ||||||
|  | 	require.Equal(t, len(serializedOrigFCMMessage), len(serializedNotTruncatedFCMMessage)) | ||||||
|  | 	require.Equal(t, "", notTruncatedFCMMessage.Data["truncated"]) | ||||||
|  | } | ||||||
|  | 
 | ||||||
| func newTestConfig(t *testing.T) *Config { | func newTestConfig(t *testing.T) *Config { | ||||||
| 	conf := NewConfig() | 	conf := NewConfig() | ||||||
| 	conf.CacheFile = filepath.Join(t.TempDir(), "cache.db") | 	conf.CacheFile = filepath.Join(t.TempDir(), "cache.db") | ||||||
|  |  | ||||||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue