2021-10-27 20:56:17 +02:00
|
|
|
package server
|
|
|
|
|
2021-10-29 19:58:14 +02:00
|
|
|
import (
|
|
|
|
"heckel.io/ntfy/util"
|
2022-01-16 05:17:46 +01:00
|
|
|
"net/http"
|
2021-10-29 19:58:14 +02:00
|
|
|
"time"
|
|
|
|
)
|
2021-10-27 20:56:17 +02:00
|
|
|
|
|
|
|
// List of possible events
|
|
|
|
const (
|
2022-02-01 01:33:22 +01:00
|
|
|
openEvent = "open"
|
|
|
|
keepaliveEvent = "keepalive"
|
|
|
|
messageEvent = "message"
|
|
|
|
pollRequestEvent = "poll_request"
|
2021-10-29 19:58:14 +02:00
|
|
|
)
|
|
|
|
|
|
|
|
const (
|
2022-02-26 21:57:10 +01:00
|
|
|
messageIDLength = 12
|
2021-10-27 20:56:17 +02:00
|
|
|
)
|
|
|
|
|
|
|
|
// message represents a message published to a topic
|
|
|
|
type message struct {
|
2022-01-02 23:56:12 +01:00
|
|
|
ID string `json:"id"` // Random message ID
|
|
|
|
Time int64 `json:"time"` // Unix time in seconds
|
|
|
|
Event string `json:"event"` // One of the above
|
|
|
|
Topic string `json:"topic"`
|
|
|
|
Priority int `json:"priority,omitempty"`
|
|
|
|
Tags []string `json:"tags,omitempty"`
|
2022-01-05 00:25:49 +01:00
|
|
|
Click string `json:"click,omitempty"`
|
|
|
|
Attachment *attachment `json:"attachment,omitempty"`
|
2022-01-02 23:56:12 +01:00
|
|
|
Title string `json:"title,omitempty"`
|
|
|
|
Message string `json:"message,omitempty"`
|
2022-03-23 21:39:22 +01:00
|
|
|
Encoding string `json:"encoding,omitempty"` // Empty for raw UTF-8, or "base64" for encoded bytes
|
|
|
|
Updated int64 `json:"updated,omitempty"` // Set if updated, unix time in seconds
|
|
|
|
Deleted int64 `json:"deleted,omitempty"` // Set if deleted, unix time in seconds
|
2022-01-02 23:56:12 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
type attachment struct {
|
2022-01-07 14:49:28 +01:00
|
|
|
Name string `json:"name"`
|
|
|
|
Type string `json:"type,omitempty"`
|
|
|
|
Size int64 `json:"size,omitempty"`
|
|
|
|
Expires int64 `json:"expires,omitempty"`
|
|
|
|
URL string `json:"url"`
|
|
|
|
Owner string `json:"-"` // IP address of uploader, used for rate limiting
|
2021-10-27 20:56:17 +02:00
|
|
|
}
|
|
|
|
|
2022-03-15 21:00:59 +01:00
|
|
|
// publishMessage is used as input when publishing as JSON
|
|
|
|
type publishMessage struct {
|
2022-03-16 19:16:54 +01:00
|
|
|
Topic string `json:"topic"`
|
|
|
|
Title string `json:"title"`
|
|
|
|
Message string `json:"message"`
|
|
|
|
Priority int `json:"priority"`
|
|
|
|
Tags []string `json:"tags"`
|
|
|
|
Click string `json:"click"`
|
|
|
|
Attach string `json:"attach"`
|
|
|
|
Filename string `json:"filename"`
|
2022-03-15 21:00:59 +01:00
|
|
|
}
|
|
|
|
|
2021-10-27 20:56:17 +02:00
|
|
|
// messageEncoder is a function that knows how to encode a message
|
|
|
|
type messageEncoder func(msg *message) (string, error)
|
|
|
|
|
|
|
|
// newMessage creates a new message with the current timestamp
|
2021-10-29 19:58:14 +02:00
|
|
|
func newMessage(event, topic, msg string) *message {
|
2021-10-27 20:56:17 +02:00
|
|
|
return &message{
|
2021-11-27 22:12:08 +01:00
|
|
|
ID: util.RandomString(messageIDLength),
|
|
|
|
Time: time.Now().Unix(),
|
|
|
|
Event: event,
|
|
|
|
Topic: topic,
|
|
|
|
Priority: 0,
|
|
|
|
Tags: nil,
|
|
|
|
Title: "",
|
|
|
|
Message: msg,
|
2021-10-27 20:56:17 +02:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// newOpenMessage is a convenience method to create an open message
|
2021-10-29 19:58:14 +02:00
|
|
|
func newOpenMessage(topic string) *message {
|
|
|
|
return newMessage(openEvent, topic, "")
|
2021-10-27 20:56:17 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
// newKeepaliveMessage is a convenience method to create a keepalive message
|
2021-10-29 19:58:14 +02:00
|
|
|
func newKeepaliveMessage(topic string) *message {
|
|
|
|
return newMessage(keepaliveEvent, topic, "")
|
2021-10-27 20:56:17 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
// newDefaultMessage is a convenience method to create a notification message
|
2021-10-29 19:58:14 +02:00
|
|
|
func newDefaultMessage(topic, msg string) *message {
|
|
|
|
return newMessage(messageEvent, topic, msg)
|
2021-10-27 20:56:17 +02:00
|
|
|
}
|
2022-01-16 05:17:46 +01:00
|
|
|
|
2022-02-26 21:57:10 +01:00
|
|
|
func validMessageID(s string) bool {
|
|
|
|
return util.ValidRandomString(s, messageIDLength)
|
|
|
|
}
|
|
|
|
|
|
|
|
type sinceMarker struct {
|
|
|
|
time time.Time
|
|
|
|
id string
|
|
|
|
}
|
2022-01-16 05:17:46 +01:00
|
|
|
|
2022-02-26 21:57:10 +01:00
|
|
|
func newSinceTime(timestamp int64) sinceMarker {
|
|
|
|
return sinceMarker{time.Unix(timestamp, 0), ""}
|
|
|
|
}
|
|
|
|
|
|
|
|
func newSinceID(id string) sinceMarker {
|
|
|
|
return sinceMarker{time.Unix(0, 0), id}
|
|
|
|
}
|
|
|
|
|
|
|
|
func (t sinceMarker) IsAll() bool {
|
2022-01-16 05:17:46 +01:00
|
|
|
return t == sinceAllMessages
|
|
|
|
}
|
|
|
|
|
2022-02-26 21:57:10 +01:00
|
|
|
func (t sinceMarker) IsNone() bool {
|
2022-01-16 05:17:46 +01:00
|
|
|
return t == sinceNoMessages
|
|
|
|
}
|
|
|
|
|
2022-02-26 21:57:10 +01:00
|
|
|
func (t sinceMarker) IsID() bool {
|
|
|
|
return t.id != ""
|
|
|
|
}
|
|
|
|
|
|
|
|
func (t sinceMarker) Time() time.Time {
|
|
|
|
return t.time
|
|
|
|
}
|
|
|
|
|
|
|
|
func (t sinceMarker) ID() string {
|
|
|
|
return t.id
|
2022-01-16 05:17:46 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
var (
|
2022-02-26 21:57:10 +01:00
|
|
|
sinceAllMessages = sinceMarker{time.Unix(0, 0), ""}
|
|
|
|
sinceNoMessages = sinceMarker{time.Unix(1, 0), ""}
|
2022-01-16 05:17:46 +01:00
|
|
|
)
|
|
|
|
|
|
|
|
type queryFilter struct {
|
|
|
|
Message string
|
|
|
|
Title string
|
|
|
|
Tags []string
|
|
|
|
Priority []int
|
|
|
|
}
|
|
|
|
|
|
|
|
func parseQueryFilters(r *http.Request) (*queryFilter, error) {
|
|
|
|
messageFilter := readParam(r, "x-message", "message", "m")
|
|
|
|
titleFilter := readParam(r, "x-title", "title", "t")
|
|
|
|
tagsFilter := util.SplitNoEmpty(readParam(r, "x-tags", "tags", "tag", "ta"), ",")
|
|
|
|
priorityFilter := make([]int, 0)
|
|
|
|
for _, p := range util.SplitNoEmpty(readParam(r, "x-priority", "priority", "prio", "p"), ",") {
|
|
|
|
priority, err := util.ParsePriority(p)
|
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
priorityFilter = append(priorityFilter, priority)
|
|
|
|
}
|
|
|
|
return &queryFilter{
|
|
|
|
Message: messageFilter,
|
|
|
|
Title: titleFilter,
|
|
|
|
Tags: tagsFilter,
|
|
|
|
Priority: priorityFilter,
|
|
|
|
}, nil
|
|
|
|
}
|
|
|
|
|
|
|
|
func (q *queryFilter) Pass(msg *message) bool {
|
|
|
|
if msg.Event != messageEvent {
|
|
|
|
return true // filters only apply to messages
|
|
|
|
}
|
|
|
|
if q.Message != "" && msg.Message != q.Message {
|
|
|
|
return false
|
|
|
|
}
|
|
|
|
if q.Title != "" && msg.Title != q.Title {
|
|
|
|
return false
|
|
|
|
}
|
|
|
|
messagePriority := msg.Priority
|
|
|
|
if messagePriority == 0 {
|
|
|
|
messagePriority = 3 // For query filters, default priority (3) is the same as "not set" (0)
|
|
|
|
}
|
|
|
|
if len(q.Priority) > 0 && !util.InIntList(q.Priority, messagePriority) {
|
|
|
|
return false
|
|
|
|
}
|
|
|
|
if len(q.Tags) > 0 && !util.InStringListAll(msg.Tags, q.Tags) {
|
|
|
|
return false
|
|
|
|
}
|
|
|
|
return true
|
|
|
|
}
|