ntfy/server/types.go

465 lines
14 KiB
Go
Raw Normal View History

package server
2021-10-29 17:58:14 +00:00
import (
2023-02-06 04:34:27 +00:00
"heckel.io/ntfy/log"
"heckel.io/ntfy/user"
2022-01-16 04:17:46 +00:00
"net/http"
"net/netip"
2021-10-29 17:58:14 +00:00
"time"
"heckel.io/ntfy/util"
2021-10-29 17:58:14 +00:00
)
// List of possible events
const (
openEvent = "open"
keepaliveEvent = "keepalive"
messageEvent = "message"
pollRequestEvent = "poll_request"
2021-10-29 17:58:14 +00:00
)
const (
2022-02-26 20:57:10 +00:00
messageIDLength = 12
)
// message represents a message published to a topic
type message struct {
2023-01-12 17:04:18 +00:00
ID string `json:"id"` // Random message ID
Time int64 `json:"time"` // Unix time in seconds
Expires int64 `json:"expires,omitempty"` // Unix time in seconds (not required for open/keepalive)
Event string `json:"event"` // One of the above
2022-01-02 22:56:12 +00:00
Topic string `json:"topic"`
2022-05-27 11:55:57 +00:00
Title string `json:"title,omitempty"`
Message string `json:"message,omitempty"`
2022-01-02 22:56:12 +00:00
Priority int `json:"priority,omitempty"`
Tags []string `json:"tags,omitempty"`
2022-01-04 23:25:49 +00:00
Click string `json:"click,omitempty"`
2022-09-11 20:31:39 +00:00
Icon string `json:"icon,omitempty"`
2022-04-19 13:14:32 +00:00
Actions []*action `json:"actions,omitempty"`
2022-01-04 23:25:49 +00:00
Attachment *attachment `json:"attachment,omitempty"`
2022-05-27 11:55:57 +00:00
PollID string `json:"poll_id,omitempty"`
2022-01-17 18:28:07 +00:00
Encoding string `json:"encoding,omitempty"` // empty for raw UTF-8, or "base64" for encoded bytes
2022-12-20 02:42:36 +00:00
Sender netip.Addr `json:"-"` // IP address of uploader, used for rate limiting
User string `json:"-"` // Username of the uploader, used to associated attachments
2022-01-02 22:56:12 +00:00
}
2023-02-06 04:34:27 +00:00
func (m *message) Context() log.Context {
2023-02-04 03:21:50 +00:00
fields := map[string]any{
2023-02-26 01:23:22 +00:00
"topic": m.Topic,
2023-02-04 03:21:50 +00:00
"message_id": m.ID,
"message_time": m.Time,
"message_event": m.Event,
"message_body_size": len(m.Message),
}
2023-02-08 03:10:51 +00:00
if m.Sender.IsValid() {
2023-02-04 03:21:50 +00:00
fields["message_sender"] = m.Sender.String()
}
if m.User != "" {
fields["message_user"] = m.User
}
return fields
}
2022-01-02 22:56:12 +00:00
type attachment struct {
Name string `json:"name"`
Type string `json:"type,omitempty"`
Size int64 `json:"size,omitempty"`
Expires int64 `json:"expires,omitempty"`
URL string `json:"url"`
}
2022-04-16 20:17:58 +00:00
type action struct {
2022-04-17 18:29:43 +00:00
ID string `json:"id"`
Action string `json:"action"` // "view", "broadcast", or "http"
Label string `json:"label"` // action button label
Clear bool `json:"clear"` // clear notification after successful execution
2022-04-19 13:14:32 +00:00
URL string `json:"url,omitempty"` // used in "view" and "http" actions
Method string `json:"method,omitempty"` // used in "http" action, default is POST (!)
Headers map[string]string `json:"headers,omitempty"` // used in "http" action
Body string `json:"body,omitempty"` // used in "http" action
2022-04-19 23:22:19 +00:00
Intent string `json:"intent,omitempty"` // used in "broadcast" action
2022-04-19 13:14:32 +00:00
Extras map[string]string `json:"extras,omitempty"` // used in "broadcast" action
2022-04-16 20:17:58 +00:00
}
2022-04-27 13:51:23 +00:00
func newAction() *action {
return &action{
Headers: make(map[string]string),
Extras: make(map[string]string),
}
}
2022-03-15 20:00:59 +00:00
// publishMessage is used as input when publishing as JSON
type publishMessage struct {
2022-03-16 18:16:54 +00: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"`
2022-09-11 20:31:39 +00:00
Icon string `json:"icon"`
2022-04-16 20:17:58 +00:00
Actions []action `json:"actions"`
2022-03-16 18:16:54 +00:00
Attach string `json:"attach"`
Filename string `json:"filename"`
Email string `json:"email"`
Call string `json:"call"`
Delay string `json:"delay"`
2022-03-15 20:00:59 +00: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 17:58:14 +00:00
func newMessage(event, topic, msg string) *message {
return &message{
2022-05-27 11:55:57 +00:00
ID: util.RandomString(messageIDLength),
Time: time.Now().Unix(),
Event: event,
Topic: topic,
Message: msg,
}
}
// newOpenMessage is a convenience method to create an open message
2021-10-29 17:58:14 +00:00
func newOpenMessage(topic string) *message {
return newMessage(openEvent, topic, "")
}
// newKeepaliveMessage is a convenience method to create a keepalive message
2021-10-29 17:58:14 +00:00
func newKeepaliveMessage(topic string) *message {
return newMessage(keepaliveEvent, topic, "")
}
// newDefaultMessage is a convenience method to create a notification message
2021-10-29 17:58:14 +00:00
func newDefaultMessage(topic, msg string) *message {
return newMessage(messageEvent, topic, msg)
}
2022-01-16 04:17:46 +00:00
2022-05-27 11:55:57 +00:00
// newPollRequestMessage is a convenience method to create a poll request message
func newPollRequestMessage(topic, pollID string) *message {
m := newMessage(pollRequestEvent, topic, newMessageBody)
m.PollID = pollID
return m
}
2022-02-26 20:57:10 +00:00
func validMessageID(s string) bool {
return util.ValidRandomString(s, messageIDLength)
}
type sinceMarker struct {
time time.Time
id string
}
2022-01-16 04:17:46 +00:00
2022-02-26 20:57:10 +00: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 04:17:46 +00:00
return t == sinceAllMessages
}
2022-02-26 20:57:10 +00:00
func (t sinceMarker) IsNone() bool {
2022-01-16 04:17:46 +00:00
return t == sinceNoMessages
}
2022-02-26 20:57:10 +00: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 04:17:46 +00:00
}
var (
2022-02-26 20:57:10 +00:00
sinceAllMessages = sinceMarker{time.Unix(0, 0), ""}
sinceNoMessages = sinceMarker{time.Unix(1, 0), ""}
2022-01-16 04:17:46 +00:00
)
type queryFilter struct {
2022-05-26 22:52:55 +00:00
ID string
2022-01-16 04:17:46 +00:00
Message string
Title string
Tags []string
Priority []int
}
func parseQueryFilters(r *http.Request) (*queryFilter, error) {
2022-05-26 22:52:55 +00:00
idFilter := readParam(r, "x-id", "id")
2022-01-16 04:17:46 +00:00
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 {
2022-07-01 13:28:42 +00:00
return nil, errHTTPBadRequestPriorityInvalid
2022-01-16 04:17:46 +00:00
}
priorityFilter = append(priorityFilter, priority)
}
return &queryFilter{
2022-05-26 22:52:55 +00:00
ID: idFilter,
2022-01-16 04:17:46 +00:00
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
2022-05-26 22:52:55 +00:00
} else if q.ID != "" && msg.ID != q.ID {
2022-01-16 04:17:46 +00:00
return false
2022-05-26 22:52:55 +00:00
} else if q.Message != "" && msg.Message != q.Message {
return false
} else if q.Title != "" && msg.Title != q.Title {
2022-01-16 04:17:46 +00:00
return false
}
messagePriority := msg.Priority
if messagePriority == 0 {
messagePriority = 3 // For query filters, default priority (3) is the same as "not set" (0)
}
2022-10-01 19:50:48 +00:00
if len(q.Priority) > 0 && !util.Contains(q.Priority, messagePriority) {
2022-01-16 04:17:46 +00:00
return false
}
2022-10-01 19:50:48 +00:00
if len(q.Tags) > 0 && !util.ContainsAll(msg.Tags, q.Tags) {
2022-01-16 04:17:46 +00:00
return false
}
return true
}
2022-12-15 04:11:22 +00:00
2022-12-24 17:22:54 +00:00
type apiHealthResponse struct {
Healthy bool `json:"healthy"`
}
2022-12-24 17:26:56 +00:00
2023-04-21 02:04:11 +00:00
type apiStatsResponse struct {
Messages int64 `json:"messages"`
MessagesRate float64 `json:"messages_rate"` // Average number of messages per second
}
2023-05-14 02:07:54 +00:00
type apiUserAddRequest struct {
Username string `json:"username"`
Password string `json:"password"`
Tier string `json:"tier"`
// Do not add 'role' here. We don't want to add admins via the API.
}
type apiUserResponse struct {
Username string `json:"username"`
Role string `json:"role"`
Tier string `json:"tier,omitempty"`
Grants []*apiUserGrantResponse `json:"grants,omitempty"`
}
type apiUserGrantResponse struct {
Topic string `json:"topic"` // This may be a pattern
Permission string `json:"permission"`
}
2023-05-14 02:07:54 +00:00
type apiUserDeleteRequest struct {
Username string `json:"username"`
}
2023-05-13 18:39:31 +00:00
type apiAccessAllowRequest struct {
Username string `json:"username"`
Topic string `json:"topic"` // This may be a pattern
2023-05-13 18:39:31 +00:00
Permission string `json:"permission"`
}
type apiAccessResetRequest struct {
Username string `json:"username"`
Topic string `json:"topic"`
}
2022-12-15 04:11:22 +00:00
type apiAccountCreateRequest struct {
Username string `json:"username"`
Password string `json:"password"`
}
2022-12-29 03:16:11 +00:00
type apiAccountPasswordChangeRequest struct {
Password string `json:"password"`
NewPassword string `json:"new_password"`
2022-12-29 03:16:11 +00:00
}
type apiAccountDeleteRequest struct {
Password string `json:"password"`
}
2023-01-28 04:10:59 +00:00
type apiAccountTokenIssueRequest struct {
Label *string `json:"label"`
Expires *int64 `json:"expires"` // Unix timestamp
}
type apiAccountTokenUpdateRequest struct {
Token string `json:"token"`
Label *string `json:"label"`
Expires *int64 `json:"expires"` // Unix timestamp
}
2022-12-15 04:11:22 +00:00
type apiAccountTokenResponse struct {
2023-01-29 01:29:06 +00:00
Token string `json:"token"`
Label string `json:"label,omitempty"`
LastAccess int64 `json:"last_access,omitempty"`
LastOrigin string `json:"last_origin,omitempty"`
Expires int64 `json:"expires,omitempty"` // Unix timestamp
2022-12-15 04:11:22 +00:00
}
2023-05-17 02:27:48 +00:00
type apiAccountPhoneNumberVerifyRequest struct {
Number string `json:"number"`
Channel string `json:"channel"`
}
type apiAccountPhoneNumberAddRequest struct {
2023-05-11 17:50:10 +00:00
Number string `json:"number"`
2023-05-17 14:39:15 +00:00
Code string `json:"code"` // Only set when adding a phone number
2023-05-11 17:50:10 +00:00
}
type apiAccountTier struct {
2023-01-09 20:40:46 +00:00
Code string `json:"code"`
Name string `json:"name"`
2022-12-17 20:17:52 +00:00
}
2022-12-19 14:59:32 +00:00
type apiAccountLimits struct {
Basis string `json:"basis,omitempty"` // "ip" or "tier"
Messages int64 `json:"messages"`
MessagesExpiryDuration int64 `json:"messages_expiry_duration"`
Emails int64 `json:"emails"`
2023-05-07 15:59:15 +00:00
Calls int64 `json:"calls"`
Reservations int64 `json:"reservations"`
AttachmentTotalSize int64 `json:"attachment_total_size"`
AttachmentFileSize int64 `json:"attachment_file_size"`
AttachmentExpiryDuration int64 `json:"attachment_expiry_duration"`
AttachmentBandwidth int64 `json:"attachment_bandwidth"`
2022-12-19 14:59:32 +00:00
}
type apiAccountStats struct {
2022-12-19 21:22:13 +00:00
Messages int64 `json:"messages"`
MessagesRemaining int64 `json:"messages_remaining"`
Emails int64 `json:"emails"`
EmailsRemaining int64 `json:"emails_remaining"`
2023-05-07 15:59:15 +00:00
Calls int64 `json:"calls"`
CallsRemaining int64 `json:"calls_remaining"`
Reservations int64 `json:"reservations"`
ReservationsRemaining int64 `json:"reservations_remaining"`
2022-12-19 21:22:13 +00:00
AttachmentTotalSize int64 `json:"attachment_total_size"`
AttachmentTotalSizeRemaining int64 `json:"attachment_total_size_remaining"`
2022-12-15 04:11:22 +00:00
}
2023-01-03 01:08:37 +00:00
type apiAccountReservation struct {
Topic string `json:"topic"`
Everyone string `json:"everyone"`
2023-01-01 20:21:43 +00:00
}
2023-01-16 04:29:46 +00:00
type apiAccountBilling struct {
Customer bool `json:"customer"`
Subscription bool `json:"subscription"`
Status string `json:"status,omitempty"`
2023-02-22 03:44:30 +00:00
Interval string `json:"interval,omitempty"`
2023-01-16 04:29:46 +00:00
PaidUntil int64 `json:"paid_until,omitempty"`
2023-01-16 15:35:12 +00:00
CancelAt int64 `json:"cancel_at,omitempty"`
2023-01-16 04:29:46 +00:00
}
2022-12-28 03:14:14 +00:00
type apiAccountResponse struct {
2023-05-13 01:47:41 +00:00
Username string `json:"username"`
Role string `json:"role,omitempty"`
SyncTopic string `json:"sync_topic,omitempty"`
Language string `json:"language,omitempty"`
Notification *user.NotificationPrefs `json:"notification,omitempty"`
Subscriptions []*user.Subscription `json:"subscriptions,omitempty"`
Reservations []*apiAccountReservation `json:"reservations,omitempty"`
Tokens []*apiAccountTokenResponse `json:"tokens,omitempty"`
PhoneNumbers []string `json:"phone_numbers,omitempty"`
Tier *apiAccountTier `json:"tier,omitempty"`
Limits *apiAccountLimits `json:"limits,omitempty"`
Stats *apiAccountStats `json:"stats,omitempty"`
Billing *apiAccountBilling `json:"billing,omitempty"`
2022-12-15 04:11:22 +00:00
}
2022-12-30 19:20:48 +00:00
2023-01-12 17:04:18 +00:00
type apiAccountReservationRequest struct {
Topic string `json:"topic"`
Everyone string `json:"everyone"`
2022-12-30 19:20:48 +00:00
}
2023-01-05 01:34:22 +00:00
type apiConfigResponse struct {
2023-01-11 03:51:51 +00:00
BaseURL string `json:"base_url"`
AppRoot string `json:"app_root"`
EnableLogin bool `json:"enable_login"`
EnableSignup bool `json:"enable_signup"`
EnablePayments bool `json:"enable_payments"`
EnableCalls bool `json:"enable_calls"`
2023-05-17 14:58:28 +00:00
EnableEmails bool `json:"enable_emails"`
2023-01-11 03:51:51 +00:00
EnableReservations bool `json:"enable_reservations"`
2023-02-28 19:38:31 +00:00
BillingContact string `json:"billing_contact"`
2023-01-11 03:51:51 +00:00
DisallowedTopics []string `json:"disallowed_topics"`
2023-01-05 01:34:22 +00:00
}
2023-01-14 11:43:44 +00:00
2023-02-22 03:44:30 +00:00
type apiAccountBillingPrices struct {
Month int64 `json:"month"`
Year int64 `json:"year"`
}
2023-01-17 15:09:37 +00:00
type apiAccountBillingTier struct {
2023-02-22 03:44:30 +00:00
Code string `json:"code,omitempty"`
Name string `json:"name,omitempty"`
Prices *apiAccountBillingPrices `json:"prices,omitempty"`
Limits *apiAccountLimits `json:"limits"`
2023-01-14 11:43:44 +00:00
}
2023-01-17 15:09:37 +00:00
type apiAccountBillingSubscriptionCreateResponse struct {
2023-01-14 11:43:44 +00:00
RedirectURL string `json:"redirect_url"`
}
2023-01-17 15:09:37 +00:00
type apiAccountBillingSubscriptionChangeRequest struct {
2023-02-22 03:44:30 +00:00
Tier string `json:"tier"`
Interval string `json:"interval"`
2023-01-17 15:09:37 +00:00
}
2023-01-14 11:43:44 +00:00
type apiAccountBillingPortalRedirectResponse struct {
RedirectURL string `json:"redirect_url"`
}
type apiAccountSyncTopicResponse struct {
Event string `json:"event"`
}
2023-01-17 15:09:37 +00:00
type apiSuccessResponse struct {
Success bool `json:"success"`
}
func newSuccessResponse() *apiSuccessResponse {
return &apiSuccessResponse{
Success: true,
}
}
2023-01-18 20:50:06 +00:00
type apiStripeSubscriptionUpdatedEvent struct {
ID string `json:"id"`
Customer string `json:"customer"`
Status string `json:"status"`
CurrentPeriodEnd int64 `json:"current_period_end"`
CancelAt int64 `json:"cancel_at"`
Items *struct {
Data []*struct {
Price *struct {
2023-02-22 03:44:30 +00:00
ID string `json:"id"`
Recurring *struct {
Interval string `json:"interval"`
} `json:"recurring"`
2023-01-18 20:50:06 +00:00
} `json:"price"`
} `json:"data"`
} `json:"items"`
}
type apiStripeSubscriptionDeletedEvent struct {
2023-01-23 03:21:30 +00:00
ID string `json:"id"`
2023-01-18 20:50:06 +00:00
Customer string `json:"customer"`
}