diff --git a/client/client.go b/client/client.go index 8b05a39..1f0862f 100644 --- a/client/client.go +++ b/client/client.go @@ -47,6 +47,7 @@ type Message struct { // TODO combine with server.message Priority int Tags []string Click string + Icon *Icon Attachment *Attachment // Additional fields @@ -65,6 +66,13 @@ type Attachment struct { Owner string `json:"-"` // IP address of uploader, used for rate limiting } +// Icon represents a message icon +type Icon struct { + Url string `json:"url"` + Type string `json:"type,omitempty"` + Size int64 `json:"size,omitempty"` +} + type subscription struct { ID string topicURL string diff --git a/client/options.go b/client/options.go index 7d59969..fdcbe1d 100644 --- a/client/options.go +++ b/client/options.go @@ -56,6 +56,11 @@ func WithClick(url string) PublishOption { return WithHeader("X-Click", url) } +// WithIcon makes the notification use the given URL as its icon +func WithIcon(icon string) PublishOption { + return WithHeader("X-Icon", icon) +} + // WithActions adds custom user actions to the notification. The value can be either a JSON array or the // simple format definition. See https://ntfy.sh/docs/publish/#action-buttons for details. func WithActions(value string) PublishOption { diff --git a/cmd/publish.go b/cmd/publish.go index 4d6ec78..c6b4a05 100644 --- a/cmd/publish.go +++ b/cmd/publish.go @@ -28,6 +28,7 @@ var flagsPublish = append( &cli.StringFlag{Name: "tags", Aliases: []string{"tag", "T"}, EnvVars: []string{"NTFY_TAGS"}, Usage: "comma separated list of tags and emojis"}, &cli.StringFlag{Name: "delay", Aliases: []string{"at", "in", "D"}, EnvVars: []string{"NTFY_DELAY"}, Usage: "delay/schedule message"}, &cli.StringFlag{Name: "click", Aliases: []string{"U"}, EnvVars: []string{"NTFY_CLICK"}, Usage: "URL to open when notification is clicked"}, + &cli.StringFlag{Name: "icon", Aliases: []string{"i"}, EnvVars: []string{"NTFY_ICON"}, Usage: "URL to use as notification icon"}, &cli.StringFlag{Name: "actions", Aliases: []string{"A"}, EnvVars: []string{"NTFY_ACTIONS"}, Usage: "actions JSON array or simple definition"}, &cli.StringFlag{Name: "attach", Aliases: []string{"a"}, EnvVars: []string{"NTFY_ATTACH"}, Usage: "URL to send as an external attachment"}, &cli.StringFlag{Name: "filename", Aliases: []string{"name", "n"}, EnvVars: []string{"NTFY_FILENAME"}, Usage: "filename for the attachment"}, @@ -64,6 +65,7 @@ Examples: 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 --click="https://reddit.com" redd 'New msg' # Opens Reddit when notification is clicked + ntfy pub --icon="http://some.tld/icon.png" 'Icon!' # Send notification with custom icon ntfy pub --attach="http://some.tld/file.zip" files # Send ZIP archive from URL as attachment ntfy pub --file=flower.jpg flowers 'Nice!' # Send image.jpg as attachment ntfy pub -u phil:mypass secret Psst # Publish with username/password @@ -90,6 +92,7 @@ func execPublish(c *cli.Context) error { tags := c.String("tags") delay := c.String("delay") click := c.String("click") + icon := c.String("icon") actions := c.String("actions") attach := c.String("attach") filename := c.String("filename") @@ -120,6 +123,9 @@ func execPublish(c *cli.Context) error { if click != "" { options = append(options, client.WithClick(click)) } + if icon != "" { + options = append(options, client.WithIcon(icon)) + } if actions != "" { options = append(options, client.WithActions(strings.ReplaceAll(actions, "\n", " "))) } diff --git a/cmd/publish_test.go b/cmd/publish_test.go index 2b9ad3f..ee8b116 100644 --- a/cmd/publish_test.go +++ b/cmd/publish_test.go @@ -52,6 +52,7 @@ func TestCLI_Publish_All_The_Things(t *testing.T) { "--tags", "tag1,tag2", // No --delay, --email "--click", "https://ntfy.sh", + "--icon", "https://ntfy.sh/static/img/ntfy.png", "--attach", "https://f-droid.org/F-Droid.apk", "--filename", "fdroid.apk", "--no-cache", diff --git a/docs/publish.md b/docs/publish.md index b1bb09f..390a376 100644 --- a/docs/publish.md +++ b/docs/publish.md @@ -2349,6 +2349,84 @@ Here's an example showing how to attach an APK file:
File attachment sent from an external URL
+## Icons +_Supported on:_ :material-android: + +You can include an icon that will appear next to the text of the notification. Simply pass the `X-Icon` header or query +parameter (or its alias `Icon`) to specify the URL that the icon is located at. The client will automatically download +the icon (up to 300KB) and show it in the notification. Only jpeg and png images are supported at this time. + +Here's an example showing how to include an icon: + +=== "Command line (curl)" + ``` + curl \ + -X POST \ + -H "Icon: https://ntfy.sh/docs/static/img/ntfy.png" \ + ntfy.sh/customIcons + ``` + +=== "ntfy CLI" + ``` + ntfy publish \ + --icon="https://ntfy.sh/docs/static/img/ntfy.png" \ + customIcons + ``` + +=== "HTTP" + ``` http + POST /customIcons HTTP/1.1 + Host: ntfy.sh + Icon: https://ntfy.sh/docs/static/img/ntfy.png + ``` + +=== "JavaScript" + ``` javascript + fetch('https://ntfy.sh/customIcons', { + method: 'POST', + headers: { 'Icon': 'https://ntfy.sh/docs/static/img/ntfy.png' } + }) + ``` + +=== "Go" + ``` go + req, _ := http.NewRequest("POST", "https://ntfy.sh/customIcons", file) + req.Header.Set("Icon", "https://ntfy.sh/docs/static/img/ntfy.png") + http.DefaultClient.Do(req) + ``` + +=== "PowerShell" + ``` powershell + $uri = "https://ntfy.sh/customIcons" + $headers = @{ Icon="https://ntfy.sh/docs/static/img/ntfy.png" } + Invoke-RestMethod -Method 'Post' -Uri $uri -Headers $headers -UseBasicParsing + ``` + +=== "Python" + ``` python + requests.put("https://ntfy.sh/customIcons", + headers={ "Icon": "https://ntfy.sh/docs/static/img/ntfy.png" }) + ``` + +=== "PHP" + ``` php-inline + file_get_contents('https://ntfy.sh/customIcons', false, stream_context_create([ + 'http' => [ + 'method' => 'PUT', + 'header' => + "Content-Type: text/plain\r\n" . // Does not matter + "Icon: https://ntfy.sh/docs/static/img/ntfy.png", + ] + ])); + ``` + +Here's an example of how it will look on Android: + +
+ ![file attachment](static/img/android-screenshot-icon.png){ width=500 } +
Custom icon from an external URL
+
+ ## E-mail notifications _Supported on:_ :material-android: :material-apple: :material-firefox: @@ -2804,6 +2882,7 @@ and can be passed as **HTTP headers** or **query parameters in the URL**. They a | `X-Actions` | `Actions`, `Action` | JSON array or short format of [user actions](#action-buttons) | | `X-Click` | `Click` | URL to open when [notification is clicked](#click-action) | | `X-Attach` | `Attach`, `a` | URL to send as an [attachment](#attachments), as an alternative to PUT/POST-ing an attachment | +| `X-Icon` | `Icon` | URL to use as notification [icon](#icons) | | `X-Filename` | `Filename`, `file`, `f` | Optional [attachment](#attachments) filename, as it appears in the client | | `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) | diff --git a/docs/releases.md b/docs/releases.md index 3ae44d0..b615b03 100644 --- a/docs/releases.md +++ b/docs/releases.md @@ -13,6 +13,7 @@ and the [ntfy Android app](https://github.com/binwiederhier/ntfy-android/release * Polling is now done with `since=` API, which makes deduping easier ([#165](https://github.com/binwiederhier/ntfy/issues/165)) * Turned JSON stream deprecation banner into "Use WebSockets" banner (no ticket) * Move action buttons in notification cards ([#236](https://github.com/binwiederhier/ntfy/issues/236), thanks to [@wunter8](https://github.com/wunter8)) +* Icons can be set for each individual notification ([#126](https://github.com/binwiederhier/ntfy/issues/126), thanks to [@wunter8](https://github.com/wunter8)) **Bugs:** @@ -41,12 +42,12 @@ Thank you to [@wunter8](https://github.com/wunter8) for proactively picking up s * `ntfy user` commands don't work with `auth_file` but works with `auth-file` ([#344](https://github.com/binwiederhier/ntfy/issues/344), thanks to [@Histalek](https://github.com/Histalek) for reporting) * Ignore new draft HTTP `Priority` header ([#351](https://github.com/binwiederhier/ntfy/issues/351), thanks to [@ksurl](https://github.com/ksurl) for reporting) * Delete expired attachments based on mod time instead of DB entry to avoid races (no ticket) +* Icons can be set for each individual notification ([#126](https://github.com/binwiederhier/ntfy/issues/126), thanks to [@wunter8](https://github.com/wunter8)) **Documentation:** * Fix some PowerShell publish docs ([#345](https://github.com/binwiederhier/ntfy/pull/345), thanks to [@noahpeltier](https://github.com/noahpeltier)) ---> ## ntfy server v1.27.2 Released June 23, 2022 diff --git a/docs/static/img/android-screenshot-icon.png b/docs/static/img/android-screenshot-icon.png new file mode 100644 index 0000000..9d9ffb2 Binary files /dev/null and b/docs/static/img/android-screenshot-icon.png differ diff --git a/server/errors.go b/server/errors.go index 28dbca3..460138a 100644 --- a/server/errors.go +++ b/server/errors.go @@ -52,6 +52,7 @@ var ( errHTTPBadRequestActionsInvalid = &errHTTP{40018, http.StatusBadRequest, "invalid request: actions invalid", "https://ntfy.sh/docs/publish/#action-buttons"} errHTTPBadRequestMatrixMessageInvalid = &errHTTP{40019, http.StatusBadRequest, "invalid request: Matrix JSON invalid", "https://ntfy.sh/docs/publish/#matrix-gateway"} errHTTPBadRequestMatrixPushkeyBaseURLMismatch = &errHTTP{40020, http.StatusBadRequest, "invalid request: push key must be prefixed with base URL", "https://ntfy.sh/docs/publish/#matrix-gateway"} + errHTTPBadRequestIconURLInvalid = &errHTTP{40021, http.StatusBadRequest, "invalid request: icon URL is invalid", "https://ntfy.sh/docs/publish/#icons"} errHTTPNotFound = &errHTTP{40401, http.StatusNotFound, "page not found", ""} errHTTPUnauthorized = &errHTTP{40101, http.StatusUnauthorized, "unauthorized", "https://ntfy.sh/docs/publish/#authentication"} errHTTPForbidden = &errHTTP{40301, http.StatusForbidden, "forbidden", "https://ntfy.sh/docs/publish/#authentication"} diff --git a/server/message_cache.go b/server/message_cache.go index 2e9c577..0905e00 100644 --- a/server/message_cache.go +++ b/server/message_cache.go @@ -38,44 +38,47 @@ const ( attachment_url TEXT NOT NULL, sender TEXT NOT NULL, encoding TEXT NOT NULL, - published INT NOT NULL + published INT NOT NULL, + icon_url TEXT NOT NULL, + icon_type TEXT NOT NULL, + icon_size INT NOT NULL ); CREATE INDEX IF NOT EXISTS idx_mid ON messages (mid); CREATE INDEX IF NOT EXISTS idx_topic ON messages (topic); COMMIT; ` insertMessageQuery = ` - INSERT INTO messages (mid, time, topic, message, title, priority, tags, click, actions, attachment_name, attachment_type, attachment_size, attachment_expires, attachment_url, sender, encoding, published) - VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) + INSERT INTO messages (mid, time, topic, message, title, priority, tags, click, actions, attachment_name, attachment_type, attachment_size, attachment_expires, attachment_url, sender, encoding, published, icon_url, icon_type, icon_size) + VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) ` pruneMessagesQuery = `DELETE FROM messages WHERE time < ? AND published = 1` selectRowIDFromMessageID = `SELECT id FROM messages WHERE mid = ?` // Do not include topic, see #336 and TestServer_PollSinceID_MultipleTopics selectMessagesSinceTimeQuery = ` - SELECT mid, time, topic, message, title, priority, tags, click, actions, attachment_name, attachment_type, attachment_size, attachment_expires, attachment_url, sender, encoding + SELECT mid, time, topic, message, title, priority, tags, click, actions, attachment_name, attachment_type, attachment_size, attachment_expires, attachment_url, sender, encoding, icon_url, icon_type, icon_size FROM messages WHERE topic = ? AND time >= ? AND published = 1 ORDER BY time, id ` selectMessagesSinceTimeIncludeScheduledQuery = ` - SELECT mid, time, topic, message, title, priority, tags, click, actions, attachment_name, attachment_type, attachment_size, attachment_expires, attachment_url, sender, encoding + SELECT mid, time, topic, message, title, priority, tags, click, actions, attachment_name, attachment_type, attachment_size, attachment_expires, attachment_url, sender, encoding, icon_url, icon_type, icon_size FROM messages WHERE topic = ? AND time >= ? ORDER BY time, id ` selectMessagesSinceIDQuery = ` - SELECT mid, time, topic, message, title, priority, tags, click, actions, attachment_name, attachment_type, attachment_size, attachment_expires, attachment_url, sender, encoding + SELECT mid, time, topic, message, title, priority, tags, click, actions, attachment_name, attachment_type, attachment_size, attachment_expires, attachment_url, sender, encoding, icon_url, icon_type, icon_size FROM messages WHERE topic = ? AND id > ? AND published = 1 ORDER BY time, id ` selectMessagesSinceIDIncludeScheduledQuery = ` - SELECT mid, time, topic, message, title, priority, tags, click, actions, attachment_name, attachment_type, attachment_size, attachment_expires, attachment_url, sender, encoding + SELECT mid, time, topic, message, title, priority, tags, click, actions, attachment_name, attachment_type, attachment_size, attachment_expires, attachment_url, sender, encoding, icon_url, icon_type, icon_size FROM messages WHERE topic = ? AND (id > ? OR published = 0) ORDER BY time, id ` selectMessagesDueQuery = ` - SELECT mid, time, topic, message, title, priority, tags, click, actions, attachment_name, attachment_type, attachment_size, attachment_expires, attachment_url, sender, encoding + SELECT mid, time, topic, message, title, priority, tags, click, actions, attachment_name, attachment_type, attachment_size, attachment_expires, attachment_url, sender, encoding, icon_url, icon_type, icon_size FROM messages WHERE time <= ? AND published = 0 ORDER BY time, id @@ -89,7 +92,7 @@ const ( // Schema management queries const ( - currentSchemaVersion = 7 + currentSchemaVersion = 8 createSchemaVersionTableQuery = ` CREATE TABLE IF NOT EXISTS schemaVersion ( id INT PRIMARY KEY, @@ -177,6 +180,13 @@ const ( migrate6To7AlterMessagesTableQuery = ` ALTER TABLE messages RENAME COLUMN attachment_owner TO sender; ` + + // 7 -> 8 + migrate7To8AlterMessagesTableQuery = ` + ALTER TABLE messages ADD COLUMN icon_url TEXT NOT NULL DEFAULT(''); + ALTER TABLE messages ADD COLUMN icon_type TEXT NOT NULL DEFAULT(''); + ALTER TABLE messages ADD COLUMN icon_size INT NOT NULL DEFAULT('0'); + ` ) type messageCache struct { @@ -248,6 +258,13 @@ func (c *messageCache) addMessages(ms []*message) error { attachmentExpires = m.Attachment.Expires attachmentURL = m.Attachment.URL } + var iconURL, iconType string + var iconSize int64 + if m.Icon != nil { + iconURL = m.Icon.URL + iconType = m.Icon.Type + iconSize = m.Icon.Size + } var actionsStr string if len(m.Actions) > 0 { actionsBytes, err := json.Marshal(m.Actions) @@ -275,6 +292,9 @@ func (c *messageCache) addMessages(ms []*message) error { m.Sender, m.Encoding, published, + iconURL, + iconType, + iconSize, ) if err != nil { return err @@ -412,9 +432,9 @@ func readMessages(rows *sql.Rows) ([]*message, error) { defer rows.Close() messages := make([]*message, 0) for rows.Next() { - var timestamp, attachmentSize, attachmentExpires int64 + var timestamp, attachmentSize, attachmentExpires, iconSize int64 var priority int - var id, topic, msg, title, tagsStr, click, actionsStr, attachmentName, attachmentType, attachmentURL, sender, encoding string + var id, topic, msg, title, tagsStr, click, actionsStr, attachmentName, attachmentType, attachmentURL, sender, encoding, iconURL, iconType string err := rows.Scan( &id, ×tamp, @@ -432,6 +452,9 @@ func readMessages(rows *sql.Rows) ([]*message, error) { &attachmentURL, &sender, &encoding, + &iconURL, + &iconType, + &iconSize, ) if err != nil { return nil, err @@ -456,6 +479,14 @@ func readMessages(rows *sql.Rows) ([]*message, error) { URL: attachmentURL, } } + var ico *icon + if iconURL != "" { + ico = &icon{ + URL: iconURL, + Type: iconType, + Size: iconSize, + } + } messages = append(messages, &message{ ID: id, Time: timestamp, @@ -466,6 +497,7 @@ func readMessages(rows *sql.Rows) ([]*message, error) { Priority: priority, Tags: tags, Click: click, + Icon: ico, Actions: actions, Attachment: att, Sender: sender, @@ -524,6 +556,8 @@ func setupCacheDB(db *sql.DB, startupQueries string) error { return migrateFrom5(db) } else if schemaVersion == 6 { return migrateFrom6(db) + } else if schemaVersion == 7 { + return migrateFrom7(db) } return fmt.Errorf("unexpected schema version found: %d", schemaVersion) } @@ -618,5 +652,16 @@ func migrateFrom6(db *sql.DB) error { if _, err := db.Exec(updateSchemaVersion, 7); err != nil { return err } + return migrateFrom7(db) +} + +func migrateFrom7(db *sql.DB) error { + log.Info("Migrating cache database schema: from 7 to 8") + if _, err := db.Exec(migrate7To8AlterMessagesTableQuery); err != nil { + return err + } + if _, err := db.Exec(updateSchemaVersion, 8); err != nil { + return err + } return nil // Update this when a new version is added } diff --git a/server/server.go b/server/server.go index 94f3580..6d25e2d 100644 --- a/server/server.go +++ b/server/server.go @@ -75,6 +75,7 @@ var ( fileRegex = regexp.MustCompile(`^/file/([-_A-Za-z0-9]{1,64})(?:\.[A-Za-z0-9]{1,16})?$`) disallowedTopics = []string{"docs", "static", "file", "app", "settings"} // If updated, also update in Android app attachURLRegex = regexp.MustCompile(`^https?://`) + iconURLRegex = regexp.MustCompile(`^https?://`) //go:embed site webFs embed.FS @@ -568,6 +569,7 @@ func (s *Server) parsePublishParams(r *http.Request, v *visitor, m *message) (ca firebase = readBoolParam(r, true, "x-firebase", "firebase") m.Title = readParam(r, "x-title", "title", "t") m.Click = readParam(r, "x-click", "click") + ico := readParam(r, "x-icon", "icon") filename := readParam(r, "x-filename", "filename", "file", "f") attach := readParam(r, "x-attach", "attach", "a") if attach != "" || filename != "" { @@ -594,6 +596,13 @@ func (s *Server) parsePublishParams(r *http.Request, v *visitor, m *message) (ca m.Attachment.Name = "attachment" } } + if ico != "" { + m.Icon = &icon{} + if !iconURLRegex.MatchString(ico) { + return false, false, "", false, errHTTPBadRequestIconURLInvalid + } + m.Icon.URL = ico + } email = readParam(r, "x-email", "x-e-mail", "email", "e-mail", "mail", "e") if email != "" { if err := v.EmailAllowed(); err != nil { @@ -1336,6 +1345,9 @@ func (s *Server) transformBodyJSON(next handleFunc) handleFunc { if m.Click != "" { r.Header.Set("X-Click", m.Click) } + if m.Icon != "" { + r.Header.Set("X-Icon", m.Icon) + } if len(m.Actions) > 0 { actionsStr, err := json.Marshal(m.Actions) if err != nil { diff --git a/server/server_firebase.go b/server/server_firebase.go index 99f0ba1..016f54b 100644 --- a/server/server_firebase.go +++ b/server/server_firebase.go @@ -166,6 +166,11 @@ func toFirebaseMessage(m *message, auther auth.Auther) (*messaging.Message, erro data["attachment_expires"] = fmt.Sprintf("%d", m.Attachment.Expires) data["attachment_url"] = m.Attachment.URL } + if m.Icon != nil { + data["icon_url"] = m.Icon.URL + data["icon_type"] = m.Icon.Type + data["icon_size"] = fmt.Sprintf("%d", m.Icon.Size) + } apnsConfig = createAPNSAlertConfig(m, data) } else { // If anonymous read for a topic is not allowed, we cannot send the message along diff --git a/server/server_firebase_test.go b/server/server_firebase_test.go index b004b0f..7301057 100644 --- a/server/server_firebase_test.go +++ b/server/server_firebase_test.go @@ -123,6 +123,11 @@ func TestToFirebaseMessage_Message_Normal_Allowed(t *testing.T) { m.Priority = 4 m.Tags = []string{"tag 1", "tag2"} m.Click = "https://google.com" + m.Icon = &icon{ + URL: "https://ntfy.sh/static/img/ntfy.png", + Type: "image/jpeg", + Size: 4567, + } m.Title = "some title" m.Actions = []*action{ { @@ -173,6 +178,9 @@ func TestToFirebaseMessage_Message_Normal_Allowed(t *testing.T) { "priority": "4", "tags": strings.Join(m.Tags, ","), "click": "https://google.com", + "icon_url": "https://ntfy.sh/static/img/ntfy.png", + "icon_type": "image/jpeg", + "icon_size": "4567", "title": "some title", "message": "this is a message", "actions": `[{"id":"123","action":"view","label":"Open page","clear":true,"url":"https://ntfy.sh"},{"id":"456","action":"http","label":"Close door","clear":false,"url":"https://door.com/close","method":"PUT","headers":{"really":"yes"}}]`, @@ -193,6 +201,9 @@ func TestToFirebaseMessage_Message_Normal_Allowed(t *testing.T) { "priority": "4", "tags": strings.Join(m.Tags, ","), "click": "https://google.com", + "icon_url": "https://ntfy.sh/static/img/ntfy.png", + "icon_type": "image/jpeg", + "icon_size": "4567", "title": "some title", "message": "this is a message", "actions": `[{"id":"123","action":"view","label":"Open page","clear":true,"url":"https://ntfy.sh"},{"id":"456","action":"http","label":"Close door","clear":false,"url":"https://door.com/close","method":"PUT","headers":{"really":"yes"}}]`, diff --git a/server/server_test.go b/server/server_test.go index d68cfa1..13e5137 100644 --- a/server/server_test.go +++ b/server/server_test.go @@ -1046,7 +1046,7 @@ func TestServer_PublishAsJSON(t *testing.T) { s := newTestServer(t, newTestConfig(t)) body := `{"topic":"mytopic","message":"A message","title":"a title\nwith lines","tags":["tag1","tag 2"],` + `"not-a-thing":"ok", "attach":"http://google.com","filename":"google.pdf", "click":"http://ntfy.sh","priority":4,` + - `"delay":"30min"}` + `"icon":"https://ntfy.sh/static/img/ntfy.png", "delay":"30min"}` response := request(t, s, "PUT", "/", body, nil) require.Equal(t, 200, response.Code) @@ -1058,6 +1058,8 @@ func TestServer_PublishAsJSON(t *testing.T) { require.Equal(t, "http://google.com", m.Attachment.URL) require.Equal(t, "google.pdf", m.Attachment.Name) require.Equal(t, "http://ntfy.sh", m.Click) + require.Equal(t, "https://ntfy.sh/static/img/ntfy.png", m.Icon.URL) + require.Equal(t, 4, m.Priority) require.True(t, m.Time > time.Now().Unix()+29*60) require.True(t, m.Time < time.Now().Unix()+31*60) diff --git a/server/types.go b/server/types.go index 44fe9e9..3a5e9fa 100644 --- a/server/types.go +++ b/server/types.go @@ -31,6 +31,7 @@ type message struct { Click string `json:"click,omitempty"` Actions []*action `json:"actions,omitempty"` Attachment *attachment `json:"attachment,omitempty"` + Icon *icon `json:"icon,omitempty"` PollID string `json:"poll_id,omitempty"` Sender string `json:"-"` // IP address of uploader, used for rate limiting Encoding string `json:"encoding,omitempty"` // empty for raw UTF-8, or "base64" for encoded bytes @@ -44,6 +45,12 @@ type attachment struct { URL string `json:"url"` } +type icon struct { + URL string `json:"url"` + Type string `json:"type,omitempty"` + Size int64 `json:"size,omitempty"` +} + type action struct { ID string `json:"id"` Action string `json:"action"` // "view", "broadcast", or "http" @@ -74,6 +81,7 @@ type publishMessage struct { Click string `json:"click"` Actions []action `json:"actions"` Attach string `json:"attach"` + Icon string `json:"icon"` Filename string `json:"filename"` Email string `json:"email"` Delay string `json:"delay"`