Docblocks, a handful of tests, but not enough
parent
fa9d6444f5
commit
e3dfea1991
|
@ -1,3 +1,4 @@
|
||||||
|
// Package client provides a ntfy client to publish and subscribe to topics
|
||||||
package client
|
package client
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
@ -12,12 +13,14 @@ import (
|
||||||
"time"
|
"time"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// Event type constants
|
||||||
const (
|
const (
|
||||||
MessageEvent = "message"
|
MessageEvent = "message"
|
||||||
KeepaliveEvent = "keepalive"
|
KeepaliveEvent = "keepalive"
|
||||||
OpenEvent = "open"
|
OpenEvent = "open"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// Client is the ntfy client that can be used to publish and subscribe to ntfy topics
|
||||||
type Client struct {
|
type Client struct {
|
||||||
Messages chan *Message
|
Messages chan *Message
|
||||||
config *Config
|
config *Config
|
||||||
|
@ -25,6 +28,7 @@ type Client struct {
|
||||||
mu sync.Mutex
|
mu sync.Mutex
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Message is a struct that represents a ntfy message
|
||||||
type Message struct {
|
type Message struct {
|
||||||
ID string
|
ID string
|
||||||
Event string
|
Event string
|
||||||
|
@ -42,6 +46,7 @@ type subscription struct {
|
||||||
cancel context.CancelFunc
|
cancel context.CancelFunc
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// New creates a new Client using a given Config
|
||||||
func New(config *Config) *Client {
|
func New(config *Config) *Client {
|
||||||
return &Client{
|
return &Client{
|
||||||
Messages: make(chan *Message),
|
Messages: make(chan *Message),
|
||||||
|
@ -50,6 +55,14 @@ func New(config *Config) *Client {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Publish sends a message to a specific topic, optionally using options.
|
||||||
|
//
|
||||||
|
// A topic can be either a full URL (e.g. https://myhost.lan/mytopic), a short URL which is then prepended https://
|
||||||
|
// (e.g. myhost.lan -> https://myhost.lan), or a short name which is expanded using the default host in the
|
||||||
|
// config (e.g. mytopic -> https://ntfy.sh/mytopic).
|
||||||
|
//
|
||||||
|
// To pass title, priority and tags, check out WithTitle, WithPriority, WithTagsList, WithDelay, WithNoCache,
|
||||||
|
// WithNoFirebase, and the generic WithHeader.
|
||||||
func (c *Client) Publish(topic, message string, options ...PublishOption) error {
|
func (c *Client) Publish(topic, message string, options ...PublishOption) error {
|
||||||
topicURL := c.expandTopicURL(topic)
|
topicURL := c.expandTopicURL(topic)
|
||||||
req, _ := http.NewRequest("POST", topicURL, strings.NewReader(message))
|
req, _ := http.NewRequest("POST", topicURL, strings.NewReader(message))
|
||||||
|
@ -68,6 +81,15 @@ func (c *Client) Publish(topic, message string, options ...PublishOption) error
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Poll queries a topic for all (or a limited set) of messages. Unlike Subscribe, this method only polls for
|
||||||
|
// messages and does not subscribe to messages that arrive after this call.
|
||||||
|
//
|
||||||
|
// A topic can be either a full URL (e.g. https://myhost.lan/mytopic), a short URL which is then prepended https://
|
||||||
|
// (e.g. myhost.lan -> https://myhost.lan), or a short name which is expanded using the default host in the
|
||||||
|
// config (e.g. mytopic -> https://ntfy.sh/mytopic).
|
||||||
|
//
|
||||||
|
// By default, all messages will be returned, but you can change this behavior using a SubscribeOption.
|
||||||
|
// See WithSince, WithSinceAll, WithSinceUnixTime, WithScheduled, and the generic WithQueryParam.
|
||||||
func (c *Client) Poll(topic string, options ...SubscribeOption) ([]*Message, error) {
|
func (c *Client) Poll(topic string, options ...SubscribeOption) ([]*Message, error) {
|
||||||
ctx := context.Background()
|
ctx := context.Background()
|
||||||
messages := make([]*Message, 0)
|
messages := make([]*Message, 0)
|
||||||
|
@ -85,6 +107,22 @@ func (c *Client) Poll(topic string, options ...SubscribeOption) ([]*Message, err
|
||||||
return messages, <-errChan
|
return messages, <-errChan
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Subscribe subscribes to a topic to listen for newly incoming messages. The method starts a connection in the
|
||||||
|
// background and returns new messages via the Messages channel.
|
||||||
|
//
|
||||||
|
// A topic can be either a full URL (e.g. https://myhost.lan/mytopic), a short URL which is then prepended https://
|
||||||
|
// (e.g. myhost.lan -> https://myhost.lan), or a short name which is expanded using the default host in the
|
||||||
|
// config (e.g. mytopic -> https://ntfy.sh/mytopic).
|
||||||
|
//
|
||||||
|
// By default, only new messages will be returned, but you can change this behavior using a SubscribeOption.
|
||||||
|
// See WithSince, WithSinceAll, WithSinceUnixTime, WithScheduled, and the generic WithQueryParam.
|
||||||
|
//
|
||||||
|
// Example:
|
||||||
|
// c := client.New(client.NewConfig())
|
||||||
|
// c.Subscribe("mytopic")
|
||||||
|
// for m := range c.Messages {
|
||||||
|
// fmt.Printf("New message: %s", m.Message)
|
||||||
|
// }
|
||||||
func (c *Client) Subscribe(topic string, options ...SubscribeOption) string {
|
func (c *Client) Subscribe(topic string, options ...SubscribeOption) string {
|
||||||
c.mu.Lock()
|
c.mu.Lock()
|
||||||
defer c.mu.Unlock()
|
defer c.mu.Unlock()
|
||||||
|
@ -98,6 +136,11 @@ func (c *Client) Subscribe(topic string, options ...SubscribeOption) string {
|
||||||
return topicURL
|
return topicURL
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Unsubscribe unsubscribes from a topic that has been previously subscribed with Subscribe.
|
||||||
|
//
|
||||||
|
// A topic can be either a full URL (e.g. https://myhost.lan/mytopic), a short URL which is then prepended https://
|
||||||
|
// (e.g. myhost.lan -> https://myhost.lan), or a short name which is expanded using the default host in the
|
||||||
|
// config (e.g. mytopic -> https://ntfy.sh/mytopic).
|
||||||
func (c *Client) Unsubscribe(topic string) {
|
func (c *Client) Unsubscribe(topic string) {
|
||||||
c.mu.Lock()
|
c.mu.Lock()
|
||||||
defer c.mu.Unlock()
|
defer c.mu.Unlock()
|
||||||
|
@ -107,7 +150,6 @@ func (c *Client) Unsubscribe(topic string) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
sub.cancel()
|
sub.cancel()
|
||||||
return
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *Client) expandTopicURL(topic string) string {
|
func (c *Client) expandTopicURL(topic string) string {
|
||||||
|
@ -128,7 +170,7 @@ func handleSubscribeConnLoop(ctx context.Context, msgChan chan *Message, topicUR
|
||||||
case <-ctx.Done():
|
case <-ctx.Done():
|
||||||
log.Printf("Connection to %s exited", topicURL)
|
log.Printf("Connection to %s exited", topicURL)
|
||||||
return
|
return
|
||||||
case <-time.After(5 * time.Second):
|
case <-time.After(10 * time.Second): // TODO Add incremental backoff
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,9 +1,11 @@
|
||||||
package client
|
package client
|
||||||
|
|
||||||
const (
|
const (
|
||||||
|
// DefaultBaseURL is the base URL used to expand short topic names
|
||||||
DefaultBaseURL = "https://ntfy.sh"
|
DefaultBaseURL = "https://ntfy.sh"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// Config is the config struct for a Client
|
||||||
type Config struct {
|
type Config struct {
|
||||||
DefaultHost string
|
DefaultHost string
|
||||||
Subscribe []struct {
|
Subscribe []struct {
|
||||||
|
@ -12,6 +14,7 @@ type Config struct {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// NewConfig creates a new Config struct for a Client
|
||||||
func NewConfig() *Config {
|
func NewConfig() *Config {
|
||||||
return &Config{
|
return &Config{
|
||||||
DefaultHost: DefaultBaseURL,
|
DefaultHost: DefaultBaseURL,
|
||||||
|
|
|
@ -1,49 +1,94 @@
|
||||||
package client
|
package client
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"fmt"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
)
|
)
|
||||||
|
|
||||||
type RequestOption func(r *http.Request) error
|
// RequestOption is a generic request option that can be added to Client calls
|
||||||
|
type RequestOption = func(r *http.Request) error
|
||||||
|
|
||||||
|
// PublishOption is an option that can be passed to the Client.Publish call
|
||||||
type PublishOption = RequestOption
|
type PublishOption = RequestOption
|
||||||
|
|
||||||
|
// SubscribeOption is an option that can be passed to a Client.Subscribe or Client.Poll call
|
||||||
type SubscribeOption = RequestOption
|
type SubscribeOption = RequestOption
|
||||||
|
|
||||||
|
// WithTitle adds a title to a message
|
||||||
func WithTitle(title string) PublishOption {
|
func WithTitle(title string) PublishOption {
|
||||||
return WithHeader("X-Title", title)
|
return WithHeader("X-Title", title)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// WithPriority adds a priority to a message. The priority can be either a number (1=min, 5=max),
|
||||||
|
// or the corresponding names (see util.ParsePriority).
|
||||||
func WithPriority(priority string) PublishOption {
|
func WithPriority(priority string) PublishOption {
|
||||||
return WithHeader("X-Priority", priority)
|
return WithHeader("X-Priority", priority)
|
||||||
}
|
}
|
||||||
|
|
||||||
func WithTags(tags string) PublishOption {
|
// WithTagsList adds a list of tags to a message. The tags parameter must be a comma-separated list
|
||||||
|
// of tags. To use a slice, use WithTags instead
|
||||||
|
func WithTagsList(tags string) PublishOption {
|
||||||
return WithHeader("X-Tags", tags)
|
return WithHeader("X-Tags", tags)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// WithTags adds a list of a tags to a message
|
||||||
|
func WithTags(tags []string) PublishOption {
|
||||||
|
return WithTagsList(strings.Join(tags, ","))
|
||||||
|
}
|
||||||
|
|
||||||
|
// WithDelay instructs the server to send the message at a later date. The delay parameter can be a
|
||||||
|
// Unix timestamp, a duration string or a natural langage string. See https://ntfy.sh/docs/publish/#scheduled-delivery
|
||||||
|
// for details.
|
||||||
func WithDelay(delay string) PublishOption {
|
func WithDelay(delay string) PublishOption {
|
||||||
return WithHeader("X-Delay", delay)
|
return WithHeader("X-Delay", delay)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// WithNoCache instructs the server not to cache the message server-side
|
||||||
func WithNoCache() PublishOption {
|
func WithNoCache() PublishOption {
|
||||||
return WithHeader("X-Cache", "no")
|
return WithHeader("X-Cache", "no")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// WithNoFirebase instructs the server not to forward the message to Firebase
|
||||||
func WithNoFirebase() PublishOption {
|
func WithNoFirebase() PublishOption {
|
||||||
return WithHeader("X-Firebase", "no")
|
return WithHeader("X-Firebase", "no")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// WithSince limits the number of messages returned from the server. The parameter since can be a Unix
|
||||||
|
// timestamp (see WithSinceUnixTime), a duration (WithSinceDuration) the word "all" (see WithSinceAll).
|
||||||
func WithSince(since string) SubscribeOption {
|
func WithSince(since string) SubscribeOption {
|
||||||
return WithQueryParam("since", since)
|
return WithQueryParam("since", since)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// WithSinceAll instructs the server to return all messages for the given topic from the server
|
||||||
|
func WithSinceAll() SubscribeOption {
|
||||||
|
return WithSince("all")
|
||||||
|
}
|
||||||
|
|
||||||
|
// WithSinceDuration instructs the server to return all messages since the given duration ago
|
||||||
|
func WithSinceDuration(since time.Duration) SubscribeOption {
|
||||||
|
return WithSinceUnixTime(time.Now().Add(-1 * since).Unix())
|
||||||
|
}
|
||||||
|
|
||||||
|
// WithSinceUnixTime instructs the server to return only messages newer or equal to the given timestamp
|
||||||
|
func WithSinceUnixTime(since int64) SubscribeOption {
|
||||||
|
return WithSince(fmt.Sprintf("%d", since))
|
||||||
|
}
|
||||||
|
|
||||||
|
// WithPoll instructs the server to close the connection after messages have been returned. Don't use this option
|
||||||
|
// directly. Use Client.Poll instead.
|
||||||
func WithPoll() SubscribeOption {
|
func WithPoll() SubscribeOption {
|
||||||
return WithQueryParam("poll", "1")
|
return WithQueryParam("poll", "1")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// WithScheduled instructs the server to also return messages that have not been sent yet, i.e. delayed/scheduled
|
||||||
|
// messages (see WithDelay). The messages will have a future date.
|
||||||
func WithScheduled() SubscribeOption {
|
func WithScheduled() SubscribeOption {
|
||||||
return WithQueryParam("scheduled", "1")
|
return WithQueryParam("scheduled", "1")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// WithHeader is a generic option to add headers to a request
|
||||||
func WithHeader(header, value string) RequestOption {
|
func WithHeader(header, value string) RequestOption {
|
||||||
return func(r *http.Request) error {
|
return func(r *http.Request) error {
|
||||||
if value != "" {
|
if value != "" {
|
||||||
|
@ -53,6 +98,7 @@ func WithHeader(header, value string) RequestOption {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// WithQueryParam is a generic option to add query parameters to a request
|
||||||
func WithQueryParam(param, value string) RequestOption {
|
func WithQueryParam(param, value string) RequestOption {
|
||||||
return func(r *http.Request) error {
|
return func(r *http.Request) error {
|
||||||
if value != "" {
|
if value != "" {
|
||||||
|
|
|
@ -59,7 +59,7 @@ func execPublish(c *cli.Context) error {
|
||||||
options = append(options, client.WithPriority(priority))
|
options = append(options, client.WithPriority(priority))
|
||||||
}
|
}
|
||||||
if tags != "" {
|
if tags != "" {
|
||||||
options = append(options, client.WithTags(tags))
|
options = append(options, client.WithTagsList(tags))
|
||||||
}
|
}
|
||||||
if delay != "" {
|
if delay != "" {
|
||||||
options = append(options, client.WithDelay(delay))
|
options = append(options, client.WithDelay(delay))
|
||||||
|
|
|
@ -4,7 +4,7 @@ set -e
|
||||||
# Delete the config if package is purged
|
# Delete the config if package is purged
|
||||||
if [ "$1" = "purge" ]; then
|
if [ "$1" = "purge" ]; then
|
||||||
id ntfy >/dev/null 2>&1 && userdel ntfy
|
id ntfy >/dev/null 2>&1 && userdel ntfy
|
||||||
rm -f /etc/ntfy/server.yml
|
rm -f /etc/ntfy/server.yml /etc/ntfy/client.yml
|
||||||
rmdir /etc/ntfy || true
|
rmdir /etc/ntfy || true
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
|
|
@ -51,7 +51,7 @@ type Config struct {
|
||||||
BehindProxy bool
|
BehindProxy bool
|
||||||
}
|
}
|
||||||
|
|
||||||
// New instantiates a default new config
|
// NewConfig instantiates a default new server config
|
||||||
func NewConfig(listenHTTP string) *Config {
|
func NewConfig(listenHTTP string) *Config {
|
||||||
return &Config{
|
return &Config{
|
||||||
ListenHTTP: listenHTTP,
|
ListenHTTP: listenHTTP,
|
||||||
|
|
|
@ -6,7 +6,6 @@ import (
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
"github.com/stretchr/testify/require"
|
"github.com/stretchr/testify/require"
|
||||||
"heckel.io/ntfy/config"
|
|
||||||
"net/http"
|
"net/http"
|
||||||
"net/http/httptest"
|
"net/http/httptest"
|
||||||
"os"
|
"os"
|
||||||
|
@ -393,13 +392,13 @@ func TestServer_PublishFirebase(t *testing.T) {
|
||||||
time.Sleep(500 * time.Millisecond) // Time for sends
|
time.Sleep(500 * time.Millisecond) // Time for sends
|
||||||
}
|
}
|
||||||
|
|
||||||
func newTestConfig(t *testing.T) *config.Config {
|
func newTestConfig(t *testing.T) *Config {
|
||||||
conf := config.New(":80")
|
conf := NewConfig(":80")
|
||||||
conf.CacheFile = filepath.Join(t.TempDir(), "cache.db")
|
conf.CacheFile = filepath.Join(t.TempDir(), "cache.db")
|
||||||
return conf
|
return conf
|
||||||
}
|
}
|
||||||
|
|
||||||
func newTestServer(t *testing.T, config *config.Config) *Server {
|
func newTestServer(t *testing.T, config *Config) *Server {
|
||||||
server, err := New(config)
|
server, err := New(config)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
|
|
|
@ -80,8 +80,9 @@ func DurationToHuman(d time.Duration) (str string) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ParsePriority parses a priority string into its equivalent integer value
|
||||||
func ParsePriority(priority string) (int, error) {
|
func ParsePriority(priority string) (int, error) {
|
||||||
switch strings.ToLower(priority) {
|
switch strings.TrimSpace(strings.ToLower(priority)) {
|
||||||
case "":
|
case "":
|
||||||
return 0, nil
|
return 0, nil
|
||||||
case "1", "min":
|
case "1", "min":
|
||||||
|
|
|
@ -63,3 +63,21 @@ func TestExpandHome_WithTilde(t *testing.T) {
|
||||||
func TestExpandHome_NoTilde(t *testing.T) {
|
func TestExpandHome_NoTilde(t *testing.T) {
|
||||||
require.Equal(t, "/this/is/an/absolute/path", ExpandHome("/this/is/an/absolute/path"))
|
require.Equal(t, "/this/is/an/absolute/path", ExpandHome("/this/is/an/absolute/path"))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestParsePriority(t *testing.T) {
|
||||||
|
priorities := []string{"", "1", "2", "3", "4", "5", "min", "LOW", " default ", "HIgh", "max", "urgent"}
|
||||||
|
expected := []int{0, 1, 2, 3, 4, 5, 1, 2, 3, 4, 5, 5}
|
||||||
|
for i, priority := range priorities {
|
||||||
|
actual, err := ParsePriority(priority)
|
||||||
|
require.Nil(t, err)
|
||||||
|
require.Equal(t, expected[i], actual)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestParsePriority_Invalid(t *testing.T) {
|
||||||
|
priorities := []string{"-1", "6", "aa", "-"}
|
||||||
|
for _, priority := range priorities {
|
||||||
|
_, err := ParsePriority(priority)
|
||||||
|
require.Equal(t, errInvalidPriority, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
Loading…
Reference in New Issue