notification icons

This commit is contained in:
Hunter Kehoe 2022-07-16 13:31:03 -06:00
parent cbcd0e3f0d
commit d519fd999b
14 changed files with 197 additions and 13 deletions

View file

@ -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"}

View file

@ -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,
&timestamp,
@ -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
}

View file

@ -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 {

View file

@ -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

View file

@ -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"}}]`,

View file

@ -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)

View file

@ -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"`