mirror of
https://github.com/binwiederhier/ntfy.git
synced 2024-12-27 12:12:28 +01:00
WIP: Since ID, works
This commit is contained in:
parent
ce9e9f3e0d
commit
4af9c07577
7 changed files with 153 additions and 33 deletions
|
@ -14,7 +14,7 @@ var (
|
||||||
// i.e. message structs with the Event messageEvent.
|
// i.e. message structs with the Event messageEvent.
|
||||||
type cache interface {
|
type cache interface {
|
||||||
AddMessage(m *message) error
|
AddMessage(m *message) error
|
||||||
Messages(topic string, since sinceTime, scheduled bool) ([]*message, error)
|
Messages(topic string, since sinceMarker, scheduled bool) ([]*message, error)
|
||||||
MessagesDue() ([]*message, error)
|
MessagesDue() ([]*message, error)
|
||||||
MessageCount(topic string) (int, error)
|
MessageCount(topic string) (int, error)
|
||||||
Topics() (map[string]*topic, error)
|
Topics() (map[string]*topic, error)
|
||||||
|
|
|
@ -54,7 +54,7 @@ func (c *memCache) AddMessage(m *message) error {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *memCache) Messages(topic string, since sinceTime, scheduled bool) ([]*message, error) {
|
func (c *memCache) Messages(topic string, since sinceMarker, scheduled bool) ([]*message, error) {
|
||||||
c.mu.Lock()
|
c.mu.Lock()
|
||||||
defer c.mu.Unlock()
|
defer c.mu.Unlock()
|
||||||
if _, ok := c.messages[topic]; !ok || since.IsNone() {
|
if _, ok := c.messages[topic]; !ok || since.IsNone() {
|
||||||
|
|
|
@ -15,7 +15,8 @@ const (
|
||||||
createMessagesTableQuery = `
|
createMessagesTableQuery = `
|
||||||
BEGIN;
|
BEGIN;
|
||||||
CREATE TABLE IF NOT EXISTS messages (
|
CREATE TABLE IF NOT EXISTS messages (
|
||||||
id TEXT PRIMARY KEY,
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||||
|
mid TEXT NOT NULL,
|
||||||
time INT NOT NULL,
|
time INT NOT NULL,
|
||||||
topic TEXT NOT NULL,
|
topic TEXT NOT NULL,
|
||||||
message TEXT NOT NULL,
|
message TEXT NOT NULL,
|
||||||
|
@ -32,42 +33,59 @@ const (
|
||||||
encoding TEXT NOT NULL,
|
encoding TEXT NOT NULL,
|
||||||
published INT NOT NULL
|
published INT NOT NULL
|
||||||
);
|
);
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_mid ON messages (mid);
|
||||||
CREATE INDEX IF NOT EXISTS idx_topic ON messages (topic);
|
CREATE INDEX IF NOT EXISTS idx_topic ON messages (topic);
|
||||||
COMMIT;
|
COMMIT;
|
||||||
`
|
`
|
||||||
insertMessageQuery = `
|
insertMessageQuery = `
|
||||||
INSERT INTO messages (id, time, topic, message, title, priority, tags, click, attachment_name, attachment_type, attachment_size, attachment_expires, attachment_url, attachment_owner, encoding, published)
|
INSERT INTO messages (mid, time, topic, message, title, priority, tags, click, attachment_name, attachment_type, attachment_size, attachment_expires, attachment_url, attachment_owner, encoding, 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, click, attachment_name, attachment_type, attachment_size, attachment_expires, attachment_url, attachment_owner, encoding
|
SELECT mid, time, topic, message, title, priority, tags, click, attachment_name, attachment_type, attachment_size, attachment_expires, attachment_url, attachment_owner, encoding
|
||||||
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, id
|
||||||
`
|
`
|
||||||
selectMessagesSinceTimeIncludeScheduledQuery = `
|
selectMessagesSinceTimeIncludeScheduledQuery = `
|
||||||
SELECT id, time, topic, message, title, priority, tags, click, attachment_name, attachment_type, attachment_size, attachment_expires, attachment_url, attachment_owner, encoding
|
SELECT mid, time, topic, message, title, priority, tags, click, attachment_name, attachment_type, attachment_size, attachment_expires, attachment_url, attachment_owner, encoding
|
||||||
FROM messages
|
FROM messages
|
||||||
WHERE topic = ? AND time >= ?
|
WHERE topic = ? AND time >= ?
|
||||||
ORDER BY time ASC
|
ORDER BY time, id
|
||||||
|
`
|
||||||
|
selectMessagesSinceIDQuery = `
|
||||||
|
SELECT mid, time, topic, message, title, priority, tags, click, attachment_name, attachment_type, attachment_size, attachment_expires, attachment_url, attachment_owner, encoding
|
||||||
|
FROM messages
|
||||||
|
WHERE topic = ?
|
||||||
|
AND published = 1
|
||||||
|
AND id > (SELECT IFNULL(id,0) FROM messages WHERE mid = ?)
|
||||||
|
ORDER BY time, id
|
||||||
|
`
|
||||||
|
selectMessagesSinceIDIncludeScheduledQuery = `
|
||||||
|
SELECT mid, time, topic, message, title, priority, tags, click, attachment_name, attachment_type, attachment_size, attachment_expires, attachment_url, attachment_owner, encoding
|
||||||
|
FROM messages
|
||||||
|
WHERE topic = ?
|
||||||
|
AND id > (SELECT IFNULL(id,0) FROM messages WHERE mid = ?)
|
||||||
|
ORDER BY time, id
|
||||||
`
|
`
|
||||||
selectMessagesDueQuery = `
|
selectMessagesDueQuery = `
|
||||||
SELECT id, time, topic, message, title, priority, tags, click, attachment_name, attachment_type, attachment_size, attachment_expires, attachment_url, attachment_owner, encoding
|
SELECT mid, time, topic, message, title, priority, tags, click, attachment_name, attachment_type, attachment_size, attachment_expires, attachment_url, attachment_owner, encoding
|
||||||
FROM messages
|
FROM messages
|
||||||
WHERE time <= ? AND published = 0
|
WHERE time <= ? AND published = 0
|
||||||
|
ORDER BY time, id
|
||||||
`
|
`
|
||||||
updateMessagePublishedQuery = `UPDATE messages SET published = 1 WHERE id = ?`
|
updateMessagePublishedQuery = `UPDATE messages SET published = 1 WHERE mid = ?`
|
||||||
selectMessagesCountQuery = `SELECT COUNT(*) FROM messages`
|
selectMessagesCountQuery = `SELECT COUNT(*) FROM messages`
|
||||||
selectMessageCountForTopicQuery = `SELECT COUNT(*) FROM messages WHERE topic = ?`
|
selectMessageCountForTopicQuery = `SELECT COUNT(*) FROM messages WHERE topic = ?`
|
||||||
selectTopicsQuery = `SELECT topic FROM messages GROUP BY topic`
|
selectTopicsQuery = `SELECT topic FROM messages GROUP BY topic`
|
||||||
selectAttachmentsSizeQuery = `SELECT IFNULL(SUM(attachment_size), 0) FROM messages WHERE attachment_owner = ? AND attachment_expires >= ?`
|
selectAttachmentsSizeQuery = `SELECT IFNULL(SUM(attachment_size), 0) FROM messages WHERE attachment_owner = ? AND attachment_expires >= ?`
|
||||||
selectAttachmentsExpiredQuery = `SELECT id FROM messages WHERE attachment_expires > 0 AND attachment_expires < ?`
|
selectAttachmentsExpiredQuery = `SELECT mid FROM messages WHERE attachment_expires > 0 AND attachment_expires < ?`
|
||||||
)
|
)
|
||||||
|
|
||||||
// Schema management queries
|
// Schema management queries
|
||||||
const (
|
const (
|
||||||
currentSchemaVersion = 4
|
currentSchemaVersion = 5
|
||||||
createSchemaVersionTableQuery = `
|
createSchemaVersionTableQuery = `
|
||||||
CREATE TABLE IF NOT EXISTS schemaVersion (
|
CREATE TABLE IF NOT EXISTS schemaVersion (
|
||||||
id INT PRIMARY KEY,
|
id INT PRIMARY KEY,
|
||||||
|
@ -108,6 +126,43 @@ const (
|
||||||
migrate3To4AlterMessagesTableQuery = `
|
migrate3To4AlterMessagesTableQuery = `
|
||||||
ALTER TABLE messages ADD COLUMN encoding TEXT NOT NULL DEFAULT('');
|
ALTER TABLE messages ADD COLUMN encoding TEXT NOT NULL DEFAULT('');
|
||||||
`
|
`
|
||||||
|
|
||||||
|
// 4 -> 5
|
||||||
|
migrate4To5AlterMessagesTableQuery = `
|
||||||
|
BEGIN;
|
||||||
|
CREATE TABLE IF NOT EXISTS messages_new (
|
||||||
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||||
|
mid TEXT NOT NULL,
|
||||||
|
time INT NOT NULL,
|
||||||
|
topic TEXT NOT NULL,
|
||||||
|
message TEXT NOT NULL,
|
||||||
|
title TEXT NOT NULL,
|
||||||
|
priority INT NOT NULL,
|
||||||
|
tags TEXT NOT NULL,
|
||||||
|
click TEXT NOT NULL,
|
||||||
|
attachment_name TEXT NOT NULL,
|
||||||
|
attachment_type TEXT NOT NULL,
|
||||||
|
attachment_size INT NOT NULL,
|
||||||
|
attachment_expires INT NOT NULL,
|
||||||
|
attachment_url TEXT NOT NULL,
|
||||||
|
attachment_owner TEXT NOT NULL,
|
||||||
|
encoding TEXT NOT NULL,
|
||||||
|
published INT NOT NULL
|
||||||
|
);
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_mid ON messages_new (mid);
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_topic ON messages_new (topic);
|
||||||
|
INSERT
|
||||||
|
INTO messages_new (
|
||||||
|
mid, time, topic, message, title, priority, tags, click, attachment_name, attachment_type,
|
||||||
|
attachment_size, attachment_expires, attachment_url, attachment_owner, encoding, published)
|
||||||
|
SELECT
|
||||||
|
id, time, topic, message, title, priority, tags, click, attachment_name, attachment_type,
|
||||||
|
attachment_size, attachment_expires, attachment_url, attachment_owner, encoding, published
|
||||||
|
FROM messages;
|
||||||
|
DROP TABLE messages;
|
||||||
|
ALTER TABLE messages_new RENAME TO messages;
|
||||||
|
COMMIT;
|
||||||
|
`
|
||||||
)
|
)
|
||||||
|
|
||||||
type sqliteCache struct {
|
type sqliteCache struct {
|
||||||
|
@ -167,17 +222,25 @@ func (c *sqliteCache) AddMessage(m *message) error {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *sqliteCache) Messages(topic string, since sinceTime, scheduled bool) ([]*message, error) {
|
func (c *sqliteCache) Messages(topic string, since sinceMarker, scheduled bool) ([]*message, error) {
|
||||||
if since.IsNone() {
|
if since.IsNone() {
|
||||||
return make([]*message, 0), nil
|
return make([]*message, 0), nil
|
||||||
}
|
}
|
||||||
var rows *sql.Rows
|
var rows *sql.Rows
|
||||||
var err error
|
var err error
|
||||||
|
if since.IsID() {
|
||||||
|
if scheduled {
|
||||||
|
rows, err = c.db.Query(selectMessagesSinceIDIncludeScheduledQuery, topic, since.ID())
|
||||||
|
} else {
|
||||||
|
rows, err = c.db.Query(selectMessagesSinceIDQuery, topic, since.ID())
|
||||||
|
}
|
||||||
|
} else {
|
||||||
if scheduled {
|
if scheduled {
|
||||||
rows, err = c.db.Query(selectMessagesSinceTimeIncludeScheduledQuery, topic, since.Time().Unix())
|
rows, err = c.db.Query(selectMessagesSinceTimeIncludeScheduledQuery, topic, since.Time().Unix())
|
||||||
} else {
|
} else {
|
||||||
rows, err = c.db.Query(selectMessagesSinceTimeQuery, topic, since.Time().Unix())
|
rows, err = c.db.Query(selectMessagesSinceTimeQuery, topic, since.Time().Unix())
|
||||||
}
|
}
|
||||||
|
}
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
@ -373,6 +436,8 @@ func setupCacheDB(db *sql.DB) error {
|
||||||
return migrateFrom2(db)
|
return migrateFrom2(db)
|
||||||
} else if schemaVersion == 3 {
|
} else if schemaVersion == 3 {
|
||||||
return migrateFrom3(db)
|
return migrateFrom3(db)
|
||||||
|
} else if schemaVersion == 4 {
|
||||||
|
return migrateFrom4(db)
|
||||||
}
|
}
|
||||||
return fmt.Errorf("unexpected schema version found: %d", schemaVersion)
|
return fmt.Errorf("unexpected schema version found: %d", schemaVersion)
|
||||||
}
|
}
|
||||||
|
@ -434,5 +499,16 @@ func migrateFrom3(db *sql.DB) error {
|
||||||
if _, err := db.Exec(updateSchemaVersion, 4); err != nil {
|
if _, err := db.Exec(updateSchemaVersion, 4); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
return migrateFrom4(db)
|
||||||
|
}
|
||||||
|
|
||||||
|
func migrateFrom4(db *sql.DB) error {
|
||||||
|
log.Print("Migrating cache database schema: from 4 to 5")
|
||||||
|
if _, err := db.Exec(migrate4To5AlterMessagesTableQuery); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if _, err := db.Exec(updateSchemaVersion, 5); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
return nil // Update this when a new version is added
|
return nil // Update this when a new version is added
|
||||||
}
|
}
|
||||||
|
|
|
@ -42,7 +42,7 @@ func testCacheMessages(t *testing.T, c cache) {
|
||||||
require.Empty(t, messages)
|
require.Empty(t, messages)
|
||||||
|
|
||||||
// mytopic: since 2
|
// mytopic: since 2
|
||||||
messages, _ = c.Messages("mytopic", sinceTime(time.Unix(2, 0)), false)
|
messages, _ = c.Messages("mytopic", newSinceTime(2), false)
|
||||||
require.Equal(t, 1, len(messages))
|
require.Equal(t, 1, len(messages))
|
||||||
require.Equal(t, "my other message", messages[0].Message)
|
require.Equal(t, "my other message", messages[0].Message)
|
||||||
|
|
||||||
|
|
|
@ -805,7 +805,7 @@ func (s *Server) handleSubscribeWS(w http.ResponseWriter, r *http.Request, v *vi
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
func parseSubscribeParams(r *http.Request) (poll bool, since sinceTime, scheduled bool, filters *queryFilter, err error) {
|
func parseSubscribeParams(r *http.Request) (poll bool, since sinceMarker, scheduled bool, filters *queryFilter, err error) {
|
||||||
poll = readBoolParam(r, false, "x-poll", "poll", "po")
|
poll = readBoolParam(r, false, "x-poll", "poll", "po")
|
||||||
scheduled = readBoolParam(r, false, "x-scheduled", "scheduled", "sched")
|
scheduled = readBoolParam(r, false, "x-scheduled", "scheduled", "sched")
|
||||||
since, err = parseSince(r, poll)
|
since, err = parseSince(r, poll)
|
||||||
|
@ -819,7 +819,7 @@ func parseSubscribeParams(r *http.Request) (poll bool, since sinceTime, schedule
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *Server) sendOldMessages(topics []*topic, since sinceTime, scheduled bool, sub subscriber) error {
|
func (s *Server) sendOldMessages(topics []*topic, since sinceMarker, scheduled bool, sub subscriber) error {
|
||||||
if since.IsNone() {
|
if since.IsNone() {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
@ -841,20 +841,28 @@ func (s *Server) sendOldMessages(topics []*topic, since sinceTime, scheduled boo
|
||||||
//
|
//
|
||||||
// Values in the "since=..." parameter can be either a unix timestamp or a duration (e.g. 12h), or
|
// Values in the "since=..." parameter can be either a unix timestamp or a duration (e.g. 12h), or
|
||||||
// "all" for all messages.
|
// "all" for all messages.
|
||||||
func parseSince(r *http.Request, poll bool) (sinceTime, error) {
|
func parseSince(r *http.Request, poll bool) (sinceMarker, error) {
|
||||||
since := readParam(r, "x-since", "since", "si")
|
since := readParam(r, "x-since", "since", "si")
|
||||||
|
|
||||||
|
// Easy cases (empty, all, none)
|
||||||
if since == "" {
|
if since == "" {
|
||||||
if poll {
|
if poll {
|
||||||
return sinceAllMessages, nil
|
return sinceAllMessages, nil
|
||||||
}
|
}
|
||||||
return sinceNoMessages, nil
|
return sinceNoMessages, nil
|
||||||
}
|
} else if since == "all" {
|
||||||
if since == "all" {
|
|
||||||
return sinceAllMessages, nil
|
return sinceAllMessages, nil
|
||||||
|
} else if since == "none" {
|
||||||
|
return sinceNoMessages, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// ID, timestamp, duration
|
||||||
|
if validMessageID(since) {
|
||||||
|
return newSinceID(since), nil
|
||||||
} else if s, err := strconv.ParseInt(since, 10, 64); err == nil {
|
} else if s, err := strconv.ParseInt(since, 10, 64); err == nil {
|
||||||
return sinceTime(time.Unix(s, 0)), nil
|
return newSinceTime(s), nil
|
||||||
} else if d, err := time.ParseDuration(since); err == nil {
|
} else if d, err := time.ParseDuration(since); err == nil {
|
||||||
return sinceTime(time.Now().Add(-1 * d)), nil
|
return newSinceTime(time.Now().Add(-1 * d).Unix()), nil
|
||||||
}
|
}
|
||||||
return sinceNoMessages, errHTTPBadRequestSinceInvalid
|
return sinceNoMessages, errHTTPBadRequestSinceInvalid
|
||||||
}
|
}
|
||||||
|
|
|
@ -15,7 +15,7 @@ const (
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
messageIDLength = 10
|
messageIDLength = 12
|
||||||
)
|
)
|
||||||
|
|
||||||
// message represents a message published to a topic
|
// message represents a message published to a topic
|
||||||
|
@ -74,23 +74,46 @@ func newDefaultMessage(topic, msg string) *message {
|
||||||
return newMessage(messageEvent, topic, msg)
|
return newMessage(messageEvent, topic, msg)
|
||||||
}
|
}
|
||||||
|
|
||||||
type sinceTime time.Time
|
func validMessageID(s string) bool {
|
||||||
|
return util.ValidRandomString(s, messageIDLength)
|
||||||
|
}
|
||||||
|
|
||||||
func (t sinceTime) IsAll() bool {
|
type sinceMarker struct {
|
||||||
|
time time.Time
|
||||||
|
id string
|
||||||
|
}
|
||||||
|
|
||||||
|
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 {
|
||||||
return t == sinceAllMessages
|
return t == sinceAllMessages
|
||||||
}
|
}
|
||||||
|
|
||||||
func (t sinceTime) IsNone() bool {
|
func (t sinceMarker) IsNone() bool {
|
||||||
return t == sinceNoMessages
|
return t == sinceNoMessages
|
||||||
}
|
}
|
||||||
|
|
||||||
func (t sinceTime) Time() time.Time {
|
func (t sinceMarker) IsID() bool {
|
||||||
return time.Time(t)
|
return t.id != ""
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t sinceMarker) Time() time.Time {
|
||||||
|
return t.time
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t sinceMarker) ID() string {
|
||||||
|
return t.id
|
||||||
}
|
}
|
||||||
|
|
||||||
var (
|
var (
|
||||||
sinceAllMessages = sinceTime(time.Unix(0, 0))
|
sinceAllMessages = sinceMarker{time.Unix(0, 0), ""}
|
||||||
sinceNoMessages = sinceTime(time.Unix(1, 0))
|
sinceNoMessages = sinceMarker{time.Unix(1, 0), ""}
|
||||||
)
|
)
|
||||||
|
|
||||||
type queryFilter struct {
|
type queryFilter struct {
|
||||||
|
|
15
util/util.go
15
util/util.go
|
@ -88,7 +88,20 @@ func RandomString(length int) string {
|
||||||
return string(b)
|
return string(b)
|
||||||
}
|
}
|
||||||
|
|
||||||
// DurationToHuman converts a duration to a human readable format
|
// ValidRandomString returns true if the given string matches the format created by RandomString
|
||||||
|
func ValidRandomString(s string, length int) bool {
|
||||||
|
if len(s) != length {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
for _, c := range strings.Split(s, "") {
|
||||||
|
if !strings.Contains(randomStringCharset, c) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
// DurationToHuman converts a duration to a human-readable format
|
||||||
func DurationToHuman(d time.Duration) (str string) {
|
func DurationToHuman(d time.Duration) (str string) {
|
||||||
if d == 0 {
|
if d == 0 {
|
||||||
return "0"
|
return "0"
|
||||||
|
|
Loading…
Reference in a new issue