2021-12-17 04:33:01 +03:00
|
|
|
package client
|
|
|
|
|
|
|
|
import (
|
|
|
|
"bufio"
|
|
|
|
"context"
|
|
|
|
"encoding/json"
|
|
|
|
"fmt"
|
|
|
|
"log"
|
|
|
|
"net/http"
|
|
|
|
"strings"
|
|
|
|
"sync"
|
|
|
|
"time"
|
|
|
|
)
|
|
|
|
|
|
|
|
const (
|
|
|
|
MessageEvent = "message"
|
|
|
|
KeepaliveEvent = "keepalive"
|
|
|
|
OpenEvent = "open"
|
|
|
|
)
|
|
|
|
|
|
|
|
type Client struct {
|
|
|
|
Messages chan *Message
|
2021-12-18 22:43:27 +03:00
|
|
|
config *Config
|
2021-12-17 04:33:01 +03:00
|
|
|
subscriptions map[string]*subscription
|
|
|
|
mu sync.Mutex
|
|
|
|
}
|
|
|
|
|
|
|
|
type Message struct {
|
|
|
|
ID string
|
|
|
|
Event string
|
|
|
|
Time int64
|
|
|
|
Topic string
|
2021-12-17 17:32:59 +03:00
|
|
|
TopicURL string
|
2021-12-17 04:33:01 +03:00
|
|
|
Message string
|
|
|
|
Title string
|
|
|
|
Priority int
|
|
|
|
Tags []string
|
|
|
|
Raw string
|
|
|
|
}
|
|
|
|
|
|
|
|
type subscription struct {
|
|
|
|
cancel context.CancelFunc
|
|
|
|
}
|
|
|
|
|
2021-12-18 22:43:27 +03:00
|
|
|
func New(config *Config) *Client {
|
2021-12-17 04:33:01 +03:00
|
|
|
return &Client{
|
|
|
|
Messages: make(chan *Message),
|
2021-12-18 22:43:27 +03:00
|
|
|
config: config,
|
2021-12-17 04:33:01 +03:00
|
|
|
subscriptions: make(map[string]*subscription),
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
func (c *Client) Publish(topicURL, message string, options ...PublishOption) error {
|
|
|
|
req, _ := http.NewRequest("POST", topicURL, strings.NewReader(message))
|
|
|
|
for _, option := range options {
|
|
|
|
if err := option(req); err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
}
|
|
|
|
resp, err := http.DefaultClient.Do(req)
|
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
if resp.StatusCode != http.StatusOK {
|
|
|
|
return fmt.Errorf("unexpected response %d from server", resp.StatusCode)
|
|
|
|
}
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
2021-12-18 22:43:27 +03:00
|
|
|
func (c *Client) Poll(topic string, options ...SubscribeOption) ([]*Message, error) {
|
2021-12-17 17:32:59 +03:00
|
|
|
ctx := context.Background()
|
|
|
|
messages := make([]*Message, 0)
|
|
|
|
msgChan := make(chan *Message)
|
|
|
|
errChan := make(chan error)
|
2021-12-18 22:43:27 +03:00
|
|
|
topicURL := c.expandTopicURL(topic)
|
2021-12-17 17:32:59 +03:00
|
|
|
go func() {
|
|
|
|
err := performSubscribeRequest(ctx, msgChan, topicURL, options...)
|
|
|
|
close(msgChan)
|
|
|
|
errChan <- err
|
|
|
|
}()
|
|
|
|
for m := range msgChan {
|
|
|
|
messages = append(messages, m)
|
|
|
|
}
|
|
|
|
return messages, <-errChan
|
|
|
|
}
|
|
|
|
|
2021-12-18 22:43:27 +03:00
|
|
|
func (c *Client) Subscribe(topic string, options ...SubscribeOption) string {
|
2021-12-17 04:33:01 +03:00
|
|
|
c.mu.Lock()
|
|
|
|
defer c.mu.Unlock()
|
2021-12-18 22:43:27 +03:00
|
|
|
topicURL := c.expandTopicURL(topic)
|
2021-12-17 04:33:01 +03:00
|
|
|
if _, ok := c.subscriptions[topicURL]; ok {
|
2021-12-18 22:43:27 +03:00
|
|
|
return topicURL
|
2021-12-17 04:33:01 +03:00
|
|
|
}
|
|
|
|
ctx, cancel := context.WithCancel(context.Background())
|
|
|
|
c.subscriptions[topicURL] = &subscription{cancel}
|
2021-12-17 17:32:59 +03:00
|
|
|
go handleSubscribeConnLoop(ctx, c.Messages, topicURL, options...)
|
2021-12-18 22:43:27 +03:00
|
|
|
return topicURL
|
2021-12-17 04:33:01 +03:00
|
|
|
}
|
|
|
|
|
2021-12-18 22:43:27 +03:00
|
|
|
func (c *Client) Unsubscribe(topic string) {
|
2021-12-17 04:33:01 +03:00
|
|
|
c.mu.Lock()
|
|
|
|
defer c.mu.Unlock()
|
2021-12-18 22:43:27 +03:00
|
|
|
topicURL := c.expandTopicURL(topic)
|
2021-12-17 04:33:01 +03:00
|
|
|
sub, ok := c.subscriptions[topicURL]
|
|
|
|
if !ok {
|
|
|
|
return
|
|
|
|
}
|
|
|
|
sub.cancel()
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
2021-12-18 22:43:27 +03:00
|
|
|
func (c *Client) expandTopicURL(topic string) string {
|
|
|
|
if strings.HasPrefix(topic, "http://") || strings.HasPrefix(topic, "https://") {
|
|
|
|
return topic
|
|
|
|
} else if strings.Contains(topic, "/") {
|
|
|
|
return fmt.Sprintf("https://%s", topic)
|
|
|
|
}
|
|
|
|
return fmt.Sprintf("%s/%s", c.config.DefaultHost, topic)
|
|
|
|
}
|
|
|
|
|
2021-12-17 17:32:59 +03:00
|
|
|
func handleSubscribeConnLoop(ctx context.Context, msgChan chan *Message, topicURL string, options ...SubscribeOption) {
|
2021-12-17 04:33:01 +03:00
|
|
|
for {
|
2021-12-17 17:32:59 +03:00
|
|
|
if err := performSubscribeRequest(ctx, msgChan, topicURL, options...); err != nil {
|
|
|
|
log.Printf("Connection to %s failed: %s", topicURL, err.Error())
|
2021-12-17 04:33:01 +03:00
|
|
|
}
|
|
|
|
select {
|
|
|
|
case <-ctx.Done():
|
2021-12-17 17:32:59 +03:00
|
|
|
log.Printf("Connection to %s exited", topicURL)
|
2021-12-17 04:33:01 +03:00
|
|
|
return
|
|
|
|
case <-time.After(5 * time.Second):
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2021-12-17 17:32:59 +03:00
|
|
|
func performSubscribeRequest(ctx context.Context, msgChan chan *Message, topicURL string, options ...SubscribeOption) error {
|
2021-12-17 04:33:01 +03:00
|
|
|
req, err := http.NewRequestWithContext(ctx, http.MethodGet, fmt.Sprintf("%s/json", topicURL), nil)
|
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
2021-12-17 17:32:59 +03:00
|
|
|
for _, option := range options {
|
|
|
|
if err := option(req); err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
}
|
2021-12-17 04:33:01 +03:00
|
|
|
resp, err := http.DefaultClient.Do(req)
|
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
defer resp.Body.Close()
|
|
|
|
scanner := bufio.NewScanner(resp.Body)
|
|
|
|
for scanner.Scan() {
|
|
|
|
var m *Message
|
|
|
|
line := scanner.Text()
|
|
|
|
if err := json.NewDecoder(strings.NewReader(line)).Decode(&m); err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
m.TopicURL = topicURL
|
|
|
|
m.Raw = line
|
|
|
|
msgChan <- m
|
|
|
|
}
|
|
|
|
return nil
|
|
|
|
}
|