├── .github └── FUNDING.yml ├── .gitignore ├── LICENSE ├── README.md ├── img └── go.png ├── sample └── main.go └── v2 ├── emitter.go ├── emitter_test.go ├── go.mod ├── go.sum ├── options.go ├── options_test.go ├── store.go ├── subtrie.go ├── subtrie_test.go ├── types.go └── types_test.go /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | github: [kelindar] 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Compiled Object files, Static and Dynamic libs (Shared Objects) 2 | *.o 3 | *.a 4 | *.so 5 | 6 | # Folders 7 | _obj 8 | _test 9 | debug 10 | 11 | # Architecture specific extensions/prefixes 12 | *.[568vq] 13 | [568vq].out 14 | 15 | *.cgo1.go 16 | *.cgo2.c 17 | _cgo_defun.c 18 | _cgo_gotypes.go 19 | _cgo_export.* 20 | 21 | _testmain.go 22 | 23 | *.exe 24 | *.test 25 | *.prof 26 | 27 | # Editor files 28 | .vscode/ 29 | *~ 30 | .idea/ 31 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Eclipse Public License - v 1.0 2 | 3 | THE ACCOMPANYING PROGRAM IS PROVIDED UNDER THE TERMS OF THIS ECLIPSE PUBLIC LICENSE ("AGREEMENT"). ANY USE, REPRODUCTION OR DISTRIBUTION OF THE PROGRAM CONSTITUTES RECIPIENT'S ACCEPTANCE OF THIS AGREEMENT. 4 | 5 | 1. DEFINITIONS 6 | 7 | "Contribution" means: 8 | 9 | a) in the case of the initial Contributor, the initial code and documentation distributed under this Agreement, and 10 | 11 | b) in the case of each subsequent Contributor: 12 | 13 | i) changes to the Program, and 14 | 15 | ii) additions to the Program; 16 | 17 | where such changes and/or additions to the Program originate from and are distributed by that particular Contributor. A Contribution 'originates' from a Contributor if it was added to the Program by such Contributor itself or anyone acting on such Contributor's behalf. Contributions do not include additions to the Program which: (i) are separate modules of software distributed in conjunction with the Program under their own license agreement, and (ii) are not derivative works of the Program. 18 | 19 | "Contributor" means any person or entity that distributes the Program. 20 | 21 | "Licensed Patents" mean patent claims licensable by a Contributor which are necessarily infringed by the use or sale of its Contribution alone or when combined with the Program. 22 | 23 | "Program" means the Contributions distributed in accordance with this Agreement. 24 | 25 | "Recipient" means anyone who receives the Program under this Agreement, including all Contributors. 26 | 27 | 2. GRANT OF RIGHTS 28 | 29 | a) Subject to the terms of this Agreement, each Contributor hereby grants Recipient a non-exclusive, worldwide, royalty-free copyright license to reproduce, prepare derivative works of, publicly display, publicly perform, distribute and sublicense the Contribution of such Contributor, if any, and such derivative works, in source code and object code form. 30 | 31 | b) Subject to the terms of this Agreement, each Contributor hereby grants Recipient a non-exclusive, worldwide, royalty-free patent license under Licensed Patents to make, use, sell, offer to sell, import and otherwise transfer the Contribution of such Contributor, if any, in source code and object code form. This patent license shall apply to the combination of the Contribution and the Program if, at the time the Contribution is added by the Contributor, such addition of the Contribution causes such combination to be covered by the Licensed Patents. The patent license shall not apply to any other combinations which include the Contribution. No hardware per se is licensed hereunder. 32 | 33 | c) Recipient understands that although each Contributor grants the licenses to its Contributions set forth herein, no assurances are provided by any Contributor that the Program does not infringe the patent or other intellectual property rights of any other entity. Each Contributor disclaims any liability to Recipient for claims brought by any other entity based on infringement of intellectual property rights or otherwise. As a condition to exercising the rights and licenses granted hereunder, each Recipient hereby assumes sole responsibility to secure any other intellectual property rights needed, if any. For example, if a third party patent license is required to allow Recipient to distribute the Program, it is Recipient's responsibility to acquire that license before distributing the Program. 34 | 35 | d) Each Contributor represents that to its knowledge it has sufficient copyright rights in its Contribution, if any, to grant the copyright license set forth in this Agreement. 36 | 37 | 3. REQUIREMENTS 38 | 39 | A Contributor may choose to distribute the Program in object code form under its own license agreement, provided that: 40 | 41 | a) it complies with the terms and conditions of this Agreement; and 42 | 43 | b) its license agreement: 44 | 45 | i) effectively disclaims on behalf of all Contributors all warranties and conditions, express and implied, including warranties or conditions of title and non-infringement, and implied warranties or conditions of merchantability and fitness for a particular purpose; 46 | 47 | ii) effectively excludes on behalf of all Contributors all liability for damages, including direct, indirect, special, incidental and consequential damages, such as lost profits; 48 | 49 | iii) states that any provisions which differ from this Agreement are offered by that Contributor alone and not by any other party; and 50 | 51 | iv) states that source code for the Program is available from such Contributor, and informs licensees how to obtain it in a reasonable manner on or through a medium customarily used for software exchange. 52 | 53 | When the Program is made available in source code form: 54 | 55 | a) it must be made available under this Agreement; and 56 | 57 | b) a copy of this Agreement must be included with each copy of the Program. 58 | 59 | Contributors may not remove or alter any copyright notices contained within the Program. 60 | 61 | Each Contributor must identify itself as the originator of its Contribution, if any, in a manner that reasonably allows subsequent Recipients to identify the originator of the Contribution. 62 | 63 | 4. COMMERCIAL DISTRIBUTION 64 | 65 | Commercial distributors of software may accept certain responsibilities with respect to end users, business partners and the like. While this license is intended to facilitate the commercial use of the Program, the Contributor who includes the Program in a commercial product offering should do so in a manner which does not create potential liability for other Contributors. Therefore, if a Contributor includes the Program in a commercial product offering, such Contributor ("Commercial Contributor") hereby agrees to defend and indemnify every other Contributor ("Indemnified Contributor") against any losses, damages and costs (collectively "Losses") arising from claims, lawsuits and other legal actions brought by a third party against the Indemnified Contributor to the extent caused by the acts or omissions of such Commercial Contributor in connection with its distribution of the Program in a commercial product offering. The obligations in this section do not apply to any claims or Losses relating to any actual or alleged intellectual property infringement. In order to qualify, an Indemnified Contributor must: a) promptly notify the Commercial Contributor in writing of such claim, and b) allow the Commercial Contributor to control, and cooperate with the Commercial Contributor in, the defense and any related settlement negotiations. The Indemnified Contributor may participate in any such claim at its own expense. 66 | 67 | For example, a Contributor might include the Program in a commercial product offering, Product X. That Contributor is then a Commercial Contributor. If that Commercial Contributor then makes performance claims, or offers warranties related to Product X, those performance claims and warranties are such Commercial Contributor's responsibility alone. Under this section, the Commercial Contributor would have to defend claims against the other Contributors related to those performance claims and warranties, and if a court requires any other Contributor to pay any damages as a result, the Commercial Contributor must pay those damages. 68 | 69 | 5. NO WARRANTY 70 | 71 | EXCEPT AS EXPRESSLY SET FORTH IN THIS AGREEMENT, THE PROGRAM IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, EITHER EXPRESS OR IMPLIED INCLUDING, WITHOUT LIMITATION, ANY WARRANTIES OR CONDITIONS OF TITLE, NON-INFRINGEMENT, MERCHANTABILITY OR FITNESS FOR A PARTICULAR PURPOSE. Each Recipient is solely responsible for determining the appropriateness of using and distributing the Program and assumes all risks associated with its exercise of rights under this Agreement , including but not limited to the risks and costs of program errors, compliance with applicable laws, damage to or loss of data, programs or equipment, and unavailability or interruption of operations. 72 | 73 | 6. DISCLAIMER OF LIABILITY 74 | 75 | EXCEPT AS EXPRESSLY SET FORTH IN THIS AGREEMENT, NEITHER RECIPIENT NOR ANY CONTRIBUTORS SHALL HAVE ANY LIABILITY FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING WITHOUT LIMITATION LOST PROFITS), HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OR DISTRIBUTION OF THE PROGRAM OR THE EXERCISE OF ANY RIGHTS GRANTED HEREUNDER, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGES. 76 | 77 | 7. GENERAL 78 | 79 | If any provision of this Agreement is invalid or unenforceable under applicable law, it shall not affect the validity or enforceability of the remainder of the terms of this Agreement, and without further action by the parties hereto, such provision shall be reformed to the minimum extent necessary to make such provision valid and enforceable. 80 | 81 | If Recipient institutes patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Program itself (excluding combinations of the Program with other software or hardware) infringes such Recipient's patent(s), then such Recipient's rights granted under Section 2(b) shall terminate as of the date such litigation is filed. 82 | 83 | All Recipient's rights under this Agreement shall terminate if it fails to comply with any of the material terms or conditions of this Agreement and does not cure such failure in a reasonable period of time after becoming aware of such noncompliance. If all Recipient's rights under this Agreement terminate, Recipient agrees to cease use and distribution of the Program as soon as reasonably practicable. However, Recipient's obligations under this Agreement and any licenses granted by Recipient relating to the Program shall continue and survive. 84 | 85 | Everyone is permitted to copy and distribute copies of this Agreement, but in order to avoid inconsistency the Agreement is copyrighted and may only be modified in the following manner. The Agreement Steward reserves the right to publish new versions (including revisions) of this Agreement from time to time. No one other than the Agreement Steward has the right to modify this Agreement. The Eclipse Foundation is the initial Agreement Steward. The Eclipse Foundation may assign the responsibility to serve as the Agreement Steward to a suitable separate entity. Each new version of the Agreement will be given a distinguishing version number. The Program (including Contributions) may always be distributed subject to the version of the Agreement under which it was received. In addition, after a new version of the Agreement is published, Contributor may elect to distribute the Program (including its Contributions) under the new version. Except as expressly stated in Sections 2(a) and 2(b) above, Recipient receives no rights or licenses to the intellectual property of any Contributor under this Agreement, whether expressly, by implication, estoppel or otherwise. All rights in the Program not expressly granted under this Agreement are reserved. 86 | 87 | This Agreement is governed by the laws of the State of New York and the intellectual property laws of the United States of America. No party to this Agreement will bring a legal action under this Agreement more than one year after the cause of action arose. Each party waives its rights to a jury trial in any resulting litigation. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Emitter Golang SDK [![api documentation](http://b.repl.ca/v1/api-documentation-green.png)](https://godoc.org/github.com/emitter-io/go) 2 | This repository contains Go/Golang client for [Emitter](https://emitter.io) (see also on [Emitter GitHub](https://github.com/emitter-io/emitter)). Emitter is an **open-source** real-time communication service for connecting online devices. At its core, emitter.io is a distributed, scalable and fault-tolerant publish-subscribe messaging platform based on MQTT protocol and featuring message storage. 3 | 4 | This library provides a nicer MQTT interface fine-tuned and extended with specific features provided by [Emitter](https://emitter.io). The code uses the [Eclipse Paho MQTT Go Client](https://github.com/eclipse/paho.mqtt.golang) for handling all the network communication and MQTT protocol, and is released under the same license (EPL v1). 5 | 6 | ## Usage 7 | 8 | This library aims to be as simple and straighforward as possible. First thing you'll need to do is to import it. 9 | 10 | ```go 11 | import emitter "github.com/emitter-io/go/v2" 12 | ``` 13 | 14 | Then, you can use the functions exposed by `Emitter` type - they are simple methods such as `Connect`, `Publish`, `Subscribe`, `Unsubscribe`, `GenerateKey`, `Presence`, etc. See the example below. 15 | 16 | ```go 17 | func main() { 18 | 19 | 20 | // Create the client and connect to the broker 21 | c, _ := emitter.Connect("", func(_ *emitter.Client, msg emitter.Message) { 22 | fmt.Printf("[emitter] -> [B] received: '%s' topic: '%s'\n", msg.Payload(), msg.Topic()) 23 | }) 24 | 25 | // Set the presence handler 26 | c.OnPresence(func(_ *emitter.Client, ev emitter.PresenceEvent) { 27 | fmt.Printf("[emitter] -> [B] presence event: %d subscriber(s) at topic: '%s'\n", len(ev.Who), ev.Channel) 28 | }) 29 | 30 | fmt.Println("[emitter] <- [B] querying own name") 31 | id := c.ID() 32 | fmt.Println("[emitter] -> [B] my name is " + id) 33 | 34 | // Subscribe to sdk-integration-test channel 35 | fmt.Println("[emitter] <- [B] subscribing to 'sdk-integration-test/'") 36 | c.Subscribe(key, "sdk-integration-test/", func(_ *emitter.Client, msg emitter.Message) { 37 | fmt.Printf("[emitter] -> [B] received on specific handler: '%s' topic: '%s'\n", msg.Payload(), msg.Topic()) 38 | }) 39 | 40 | // Ask for presence 41 | fmt.Println("[emitter] <- [B] asking for presence on 'sdk-integration-test/'") 42 | c.Presence(key, "sdk-integration-test/", true, false) 43 | 44 | // Publish to the channel 45 | fmt.Println("[emitter] <- [B] publishing to 'sdk-integration-test/'") 46 | c.Publish(key, "sdk-integration-test/", "hello") 47 | 48 | select {} // wait forever without busy loop - Ctrl-c to stop 49 | } 50 | ``` 51 | 52 | ## Installation and Build 53 | 54 | This client, similarly to the Eclipse Paho client is designed to work with the standard Go tools, so installation is as easy as: 55 | 56 | ```go 57 | go get -u github.com/emitter-io/go/v2 58 | ``` 59 | 60 | For usage, please refer to the `sample` sub-folder in this repository which provides a sample application on how to use the API. 61 | 62 | ## API Documentation 63 | 64 | The full API documentation of exported members is available on [godoc.org/github.com/emitter-io/go/v2](https://godoc.org/github.com/emitter-io/go/v2). 65 | 66 | ## License 67 | 68 | Licensed with EPL 1.0, similarly to Eclipse Paho MQTT Client. 69 | -------------------------------------------------------------------------------- /img/go.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/emitter-io/go/5df9a117322800e6ec1bdf485d58645921c9ba2e/img/go.png -------------------------------------------------------------------------------- /sample/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "time" 6 | 7 | emitter "github.com/emitter-io/go/v2" 8 | ) 9 | 10 | func main() { 11 | clientA() 12 | clientB() 13 | 14 | // stop after 10 seconds 15 | time.Sleep(1 * time.Second) 16 | } 17 | 18 | func clientA() { 19 | const key = "RUvY5GTEOUmIqFs_zfpJcfTqBUIKBhfs" // read on sdk-integration-test/#/ 20 | 21 | // Create the client and connect to the broker 22 | c, _ := emitter.Connect("", func(_ *emitter.Client, msg emitter.Message) { 23 | fmt.Printf("[emitter] -> [A] received: '%s' topic: '%s'\n", msg.Payload(), msg.Topic()) 24 | }) 25 | 26 | // Subscribe to sdk-integration-test channel 27 | fmt.Println("[emitter] <- [A] subscribing to 'sdk-integration-test/...'") 28 | c.Subscribe(key, "sdk-integration-test/", nil) 29 | } 30 | 31 | func clientB() { 32 | const key = "pGrtRRL6RrjAdExSArkMzBZOoWr2eB3L" // everything on sdk-integration-test/ 33 | 34 | // Create the client and connect to the broker 35 | c, _ := emitter.Connect("", func(_ *emitter.Client, msg emitter.Message) { 36 | fmt.Printf("[emitter] -> [B] received: '%s' topic: '%s'\n", msg.Payload(), msg.Topic()) 37 | }) 38 | 39 | // Set the presence handler 40 | c.OnPresence(func(_ *emitter.Client, ev emitter.PresenceEvent) { 41 | fmt.Printf("[emitter] -> [B] presence event: %d subscriber(s) at topic: '%s'\n", len(ev.Who), ev.Channel) 42 | }) 43 | 44 | fmt.Println("[emitter] <- [B] querying own name") 45 | id := c.ID() 46 | fmt.Println("[emitter] -> [B] my name is " + id) 47 | 48 | // Subscribe to sdk-integration-test channel 49 | fmt.Println("[emitter] <- [B] subscribing to 'sdk-integration-test/'") 50 | c.Subscribe(key, "sdk-integration-test/", func(_ *emitter.Client, msg emitter.Message) { 51 | fmt.Printf("[emitter] -> [B] received on specific handler: '%s' topic: '%s'\n", msg.Payload(), msg.Topic()) 52 | }) 53 | 54 | // Ask for presence 55 | fmt.Println("[emitter] <- [B] asking for presence on 'sdk-integration-test/'") 56 | c.Presence(key, "sdk-integration-test/", true, false) 57 | 58 | // Publish to the channel 59 | fmt.Println("[emitter] <- [B] publishing to 'sdk-integration-test/'") 60 | c.Publish(key, "sdk-integration-test/", "hello") 61 | } 62 | -------------------------------------------------------------------------------- /v2/emitter.go: -------------------------------------------------------------------------------- 1 | package emitter 2 | 3 | import ( 4 | "encoding/json" 5 | "errors" 6 | "fmt" 7 | "log" 8 | "strings" 9 | "sync" 10 | "time" 11 | 12 | mqtt "github.com/eclipse/paho.mqtt.golang" 13 | ) 14 | 15 | // Various emitter errors 16 | var ( 17 | ErrTimeout = errors.New("emitter: operation has timed out") 18 | ErrUnmarshal = errors.New("emitter: unable to unmarshal the response") 19 | ) 20 | 21 | // Message defines the externals that a message implementation must support 22 | // these are received messages that are passed to the callbacks, not internal 23 | // messages 24 | type Message interface { 25 | Topic() string 26 | Payload() []byte 27 | } 28 | 29 | // Client represents an emitter client which holds the connection. 30 | type Client struct { 31 | sync.RWMutex 32 | guid string // Emiter's client ID 33 | conn mqtt.Client // MQTT client 34 | opts *mqtt.ClientOptions // MQTT options 35 | store *store // In-flight requests store 36 | handlers *trie // The registry for handlers 37 | timeout time.Duration // Default timeout 38 | message MessageHandler // User-defined message handler 39 | connect ConnectHandler // User-defined connect handler 40 | disconnect DisconnectHandler // User-defined disconnect handler 41 | presence PresenceHandler // User-defined presence handler 42 | errors ErrorHandler // User-defined error handler 43 | } 44 | 45 | // Connect is a convenience function which sets a broker and connects to it. 46 | func Connect(host string, handler MessageHandler, options ...func(*Client)) (*Client, error) { 47 | if len(host) > 0 { 48 | options = append(options, WithBrokers(host)) 49 | } 50 | 51 | // Create the client and handlers 52 | client := NewClient(options...) 53 | client.OnMessage(handler) 54 | 55 | // Connect to the broker 56 | err := client.Connect() 57 | return client, err 58 | } 59 | 60 | // NewClient will create an MQTT v3.1.1 client with all of the options specified 61 | // in the provided ClientOptions. The client must have the Connect method called 62 | // on it before it may be used. This is to make sure resources (such as a net 63 | // connection) are created before the application is actually ready. 64 | func NewClient(options ...func(*Client)) *Client { 65 | c := &Client{ 66 | opts: mqtt.NewClientOptions(), 67 | timeout: 60 * time.Second, 68 | store: new(store), 69 | handlers: NewTrie(), 70 | } 71 | 72 | // Set handlers 73 | c.opts.SetOnConnectHandler(c.onConnect) 74 | c.opts.SetConnectionLostHandler(c.onConnectionLost) 75 | c.opts.SetDefaultPublishHandler(c.onMessage) 76 | c.opts.SetClientID(uuid()) 77 | c.opts.SetStore(c.store) 78 | 79 | // Apply default configuration 80 | WithBrokers("tcp://api.emitter.io:8080")(c) 81 | 82 | // Apply options 83 | for _, opt := range options { 84 | opt(c) 85 | } 86 | 87 | // Create the underlying MQTT client and set the options 88 | c.conn = mqtt.NewClient(c.opts) 89 | return c 90 | } 91 | 92 | // OnMessage sets the MessageHandler that will be called when a message 93 | // is received that does not match any known subscriptions. 94 | func (c *Client) OnMessage(handler MessageHandler) { 95 | c.message = handler 96 | } 97 | 98 | // OnConnect sets the function to be called when the client is connected. Both 99 | // at initial connection time and upon automatic reconnect. 100 | func (c *Client) OnConnect(handler ConnectHandler) { 101 | c.connect = handler 102 | } 103 | 104 | // OnDisconnect will set the function callback to be executed 105 | // in the case where the client unexpectedly loses connection with the MQTT broker. 106 | func (c *Client) OnDisconnect(handler DisconnectHandler) { 107 | c.disconnect = handler 108 | } 109 | 110 | // OnPresence sets the function that will be called when a presence event is received. 111 | func (c *Client) OnPresence(handler PresenceHandler) { 112 | c.presence = handler 113 | } 114 | 115 | // onConnect occurs when MQTT client is connected 116 | func (c *Client) onConnect(_ mqtt.Client) { 117 | if c.connect != nil { 118 | c.connect(c) 119 | } 120 | } 121 | 122 | // onConnectionLost occurs when MQTT client is disconnected 123 | func (c *Client) onConnectionLost(_ mqtt.Client, e error) { 124 | if c.disconnect != nil { 125 | c.disconnect(c, e) 126 | } else { 127 | log.Println("emitter: connection lost, due to", e.Error()) 128 | } 129 | } 130 | 131 | // OnError will set the function callback to be executed if an emitter-specific 132 | // error occurs. 133 | func (c *Client) OnError(handler ErrorHandler) { 134 | 135 | c.errors = handler 136 | } 137 | 138 | // onMessage occurs when MQTT client receives a message 139 | func (c *Client) onMessage(_ mqtt.Client, m mqtt.Message) { 140 | if !strings.HasPrefix(m.Topic(), "emitter/") { 141 | handlers := c.handlers.Lookup(m.Topic()) 142 | if len(handlers) == 0 && c.message != nil { // Invoke the default message handler 143 | c.message(c, m) 144 | } 145 | 146 | // Call each handler 147 | for _, h := range handlers { 148 | h(c, m) 149 | } 150 | return 151 | } 152 | 153 | // `onError` and `onResponse` read the callbacks store when calling 154 | // the `NotifyResponse`. See the comments in the `request` function. 155 | c.RLock() 156 | defer c.RUnlock() 157 | 158 | switch { 159 | 160 | // Dispatch errors handler 161 | case strings.HasPrefix(m.Topic(), "emitter/error/"): 162 | c.onError(m) 163 | 164 | // Dispatch presence handler 165 | case strings.HasPrefix(m.Topic(), "emitter/presence/"): 166 | var presenceResp presenceResponse 167 | if err := json.Unmarshal(m.Payload(), &presenceResp); err != nil { 168 | log.Println("emitter:", err.Error()) 169 | return 170 | } 171 | 172 | // If it's not the "status" response of the Presence RPC but a "change" event, we call the handler 173 | // and stop there. We locked the client to for nothing in this case. There is no other choice. 174 | r := PresenceEvent{presenceResp, make([]PresenceInfo, 0)} 175 | if c.presence != nil && presenceResp.Event != "" && presenceResp.Event != "status" { // If we didn't request a status the Event will be empty. 176 | r.Who = append(r.Who, PresenceInfo{}) 177 | if err := json.Unmarshal([]byte(presenceResp.Who), &r.Who[0]); err != nil { 178 | log.Println("emitter:", err.Error()) 179 | return 180 | } 181 | c.presence(c, r) 182 | } else if presenceResp.RequestID() > 0 { 183 | // In this case, we have a "status" response of the Presence RPC. And this could be an error. 184 | // Check if we've got an error response 185 | var errResponse Error 186 | if err := json.Unmarshal(m.Payload(), &errResponse); err == nil && errResponse.Error() != "" { 187 | c.store.NotifyResponse(errResponse.RequestID(), &errResponse) 188 | return 189 | } 190 | 191 | if err := json.Unmarshal([]byte(presenceResp.Who), &r.Who); err != nil { 192 | log.Println("emitter:", err.Error()) 193 | return 194 | } 195 | c.store.NotifyResponse(presenceResp.RequestID(), &r) 196 | } 197 | 198 | case strings.HasPrefix(m.Topic(), "emitter/keygen/"): 199 | c.onResponse(m, new(keyGenResponse)) 200 | 201 | // Dispatch keyban handler 202 | case strings.HasPrefix(m.Topic(), "emitter/keyban/"): 203 | c.onResponse(m, new(keyBanResponse)) 204 | 205 | // Dispatch link handler 206 | case strings.HasPrefix(m.Topic(), "emitter/link/"): 207 | c.onResponse(m, new(Link)) 208 | 209 | // Dispatch me handler 210 | case strings.HasPrefix(m.Topic(), "emitter/me/"): 211 | c.onResponse(m, new(meResponse)) 212 | 213 | // Dispatch history handler 214 | case strings.HasPrefix(m.Topic(), "emitter/history/"): 215 | c.onResponse(m, new(historyResponse)) 216 | 217 | default: 218 | 219 | } 220 | } 221 | 222 | // OnResponse handles the incoming response for emitter messages. 223 | func (c *Client) onResponse(m mqtt.Message, resp Response) bool { 224 | 225 | // Check if we've got an error response 226 | var errResponse Error 227 | if err := json.Unmarshal(m.Payload(), &errResponse); err == nil && errResponse.Error() != "" { 228 | return c.store.NotifyResponse(errResponse.RequestID(), &errResponse) 229 | } 230 | 231 | // If it's not an error, try to unmarshal the response 232 | if err := json.Unmarshal(m.Payload(), &resp); err == nil && resp.RequestID() > 0 { 233 | return c.store.NotifyResponse(resp.RequestID(), resp) 234 | } 235 | return false 236 | } 237 | 238 | // OnError handles the incoming error. 239 | func (c *Client) onError(m mqtt.Message) { 240 | var resp Error 241 | if err := json.Unmarshal(m.Payload(), &resp); err != nil { 242 | return 243 | } 244 | 245 | if c.errors == nil { 246 | log.Println("emitter:", resp.Error()) 247 | } 248 | 249 | if c.errors != nil && !c.store.NotifyResponse(resp.RequestID(), &resp) { 250 | c.errors(c, resp) 251 | } 252 | } 253 | 254 | // IsConnected returns a bool signifying whether the client is connected or not. 255 | func (c *Client) IsConnected() bool { 256 | return c.conn.IsConnected() 257 | } 258 | 259 | // Connect initiates a connection to the broker. 260 | func (c *Client) Connect() error { 261 | return c.do(c.conn.Connect()) 262 | } 263 | 264 | // ID retrieves information about the client. 265 | func (c *Client) ID() string { 266 | if c.guid != "" { 267 | return c.guid 268 | } 269 | 270 | // Query the remote GUID, cast the response and store it 271 | if resp, err := c.request("me", nil); err == nil { 272 | if result, ok := resp.(*meResponse); ok { 273 | c.guid = result.ID 274 | } 275 | } 276 | 277 | return c.guid 278 | } 279 | 280 | // Disconnect will end the connection with the server, but not before waiting 281 | // the specified number of milliseconds to wait for existing work to be 282 | // completed. 283 | func (c *Client) Disconnect(waitTime time.Duration) { 284 | c.conn.Disconnect(uint(waitTime.Nanoseconds() / 1000000)) 285 | } 286 | 287 | // Publish will publish a message with the specified QoS and content to the specified topic. 288 | // Returns a token to track delivery of the message to the broker 289 | func (c *Client) Publish(key string, channel string, payload interface{}, options ...Option) error { 290 | qos, retain := getHeader(options) 291 | token := c.conn.Publish(formatTopic(key, channel, options), qos, retain, payload) 292 | return c.do(token) 293 | } 294 | 295 | // PublishWithTTL publishes a message with a specified Time-To-Live option 296 | func (c *Client) PublishWithTTL(key string, channel string, payload interface{}, ttl int) error { 297 | return c.Publish(key, channel, payload, WithTTL(ttl)) 298 | } 299 | 300 | // PublishWithRetain publishes a message with a retain flag set to true 301 | func (c *Client) PublishWithRetain(key string, channel string, payload interface{}, options ...Option) error { 302 | options = append(options, WithRetain()) 303 | return c.Publish(key, channel, payload, options...) 304 | } 305 | 306 | // PublishWithLink publishes a message with a specified link name instead of a channel key. 307 | func (c *Client) PublishWithLink(name string, payload interface{}, options ...Option) error { 308 | qos, retain := getHeader(options) 309 | token := c.conn.Publish(name, qos, retain, payload) 310 | return c.do(token) 311 | } 312 | 313 | // Subscribe starts a new subscription. Provide a MessageHandler to be executed when 314 | // a message is published on the topic provided. 315 | func (c *Client) Subscribe(key string, channel string, optionalHandler MessageHandler, options ...Option) error { 316 | if optionalHandler != nil { 317 | c.handlers.AddHandler(channel, optionalHandler) 318 | } 319 | 320 | // https://github.com/eclipse/paho.mqtt.golang/blob/master/topic.go#L78 321 | topic := strings.ReplaceAll(formatTopic(key, channel, options), "#/", "#") 322 | 323 | // Issue subscribe 324 | token := c.conn.Subscribe(topic, 0, nil) 325 | return c.do(token) 326 | } 327 | 328 | // SubscribeWithGroup creates a shared subscription to a share group. 329 | func (c *Client) SubscribeWithGroup(key, channel, shareGroup string, optionalHandler MessageHandler, options ...Option) error { 330 | if optionalHandler != nil { 331 | c.handlers.AddHandler(channel, optionalHandler) 332 | } 333 | 334 | // Issue subscribe 335 | token := c.conn.Subscribe(formatShare(key, shareGroup, channel, options), 0, nil) 336 | return c.do(token) 337 | } 338 | 339 | // SubscribeWithHistory performs a subscribe with an option to retrieve the specified number 340 | // of messages that were already published in the channel. 341 | func (c *Client) SubscribeWithHistory(key string, channel string, last int, optionalHandler MessageHandler) error { 342 | return c.Subscribe(key, channel, optionalHandler, WithLast(last)) 343 | } 344 | 345 | // Unsubscribe will end the subscription from each of the topics provided. 346 | // Messages published to those topics from other clients will no longer be 347 | // received. 348 | func (c *Client) Unsubscribe(key string, channel string) error { 349 | 350 | // Remove the handler if we have one 351 | c.handlers.RemoveHandler(channel) 352 | 353 | // Issue the unsubscribe 354 | token := c.conn.Unsubscribe(formatTopic(key, channel, nil)) 355 | return c.do(token) 356 | } 357 | 358 | // Presence sends a presence request to the broker. 359 | func (c *Client) Presence(key, channel string, status, changes bool) (*PresenceEvent, error) { 360 | resp, err := c.request("presence", &presenceRequest{ 361 | Key: key, 362 | Channel: channel, 363 | Status: status, 364 | Changes: changes, 365 | }) 366 | if err != nil { 367 | return nil, err 368 | } 369 | 370 | // Cast the response and return it 371 | if result, ok := resp.(*PresenceEvent); ok { 372 | return result, nil 373 | } 374 | return nil, ErrUnmarshal 375 | //return c.do(c.conn.Publish("emitter/presence/", 1, false, req)) 376 | } 377 | 378 | // GenerateKey sends a key generation request to the broker 379 | func (c *Client) GenerateKey(key, channel, permissions string, ttl int) (string, string, error) { 380 | resp, err := c.request("keygen", &keygenRequest{ 381 | Key: key, 382 | Channel: channel, 383 | Type: permissions, 384 | TTL: ttl, 385 | }) 386 | if err != nil { 387 | return "", "", err 388 | } 389 | 390 | // Cast the response and return it 391 | if result, ok := resp.(*keyGenResponse); ok { 392 | return result.Key, result.Channel, nil 393 | } 394 | return "", "", ErrUnmarshal 395 | } 396 | 397 | // BlockKey sends a request to block a key. 398 | func (c *Client) BlockKey(secretKey, targetKey string) (bool, error) { 399 | resp, err := c.request("keyban", &keybanRequest{ 400 | Secret: secretKey, 401 | Target: targetKey, 402 | Banned: true, 403 | }) 404 | if err != nil { 405 | return false, err 406 | } 407 | 408 | // Cast the response and return it 409 | if result, ok := resp.(*keyBanResponse); ok { 410 | return result.Banned == true, nil 411 | } 412 | return false, ErrUnmarshal 413 | } 414 | 415 | // AllowKey sends a request to allow a previously blocked key. 416 | func (c *Client) AllowKey(secretKey, targetKey string) (bool, error) { 417 | resp, err := c.request("keyban", &keybanRequest{ 418 | Secret: secretKey, 419 | Target: targetKey, 420 | Banned: false, 421 | }) 422 | if err != nil { 423 | return false, err 424 | } 425 | 426 | // Cast the response and return it 427 | if result, ok := resp.(*keyBanResponse); ok { 428 | return result.Banned == false, nil 429 | } 430 | return false, ErrUnmarshal 431 | } 432 | 433 | // CreateLink sends a request to create a default link. 434 | func (c *Client) CreateLink(key, channel, name string, optionalHandler MessageHandler, options ...Option) (*Link, error) { 435 | resp, err := c.request("link", &linkRequest{ 436 | Name: name, 437 | Key: key, 438 | Channel: formatTopic("", channel, options), 439 | Subscribe: optionalHandler != nil, 440 | }) 441 | 442 | if err != nil { 443 | return nil, err 444 | } 445 | 446 | // Cast the response and return it 447 | if result, ok := resp.(*Link); ok { 448 | if optionalHandler != nil { 449 | c.handlers.AddHandler(result.Channel, optionalHandler) 450 | } 451 | 452 | return result, nil 453 | } 454 | return nil, ErrUnmarshal 455 | } 456 | 457 | func (c *Client) History(key, channel string, from, until int64, limit int) func(func(m HistoryMessage, err error) bool) { 458 | return func(yield func(m HistoryMessage, err error) bool) { 459 | { 460 | var startFromID MessageID = nil 461 | nMsgRetrieved := 0 462 | 463 | for { 464 | req := &historyRequest{ 465 | // As an MQTT message size is limited, we need to paginate the history request. 466 | // If we didn't receive all messages (limit) we request limit - nMsgRetrieved messages. 467 | // We also set the startFromID to the message ID of the last message we received to 468 | // continue retrieving them from this one. 469 | Channel: formatTopic(key, channel, []Option{WithLast(limit - nMsgRetrieved), WithFrom(time.Unix(from, 0)), WithUntil(time.Unix(until, 0))}), 470 | StartFromID: startFromID, 471 | } 472 | 473 | resp, err := c.request("history", req) 474 | if err != nil { 475 | yield(HistoryMessage{}, err) 476 | } 477 | 478 | // Cast the response. 479 | result, _ := resp.(*historyResponse) 480 | 481 | // If no messages left in the history then return. 482 | if len(result.Messages) == 0 { 483 | return 484 | } 485 | 486 | // Yield each message returned by the history request. 487 | for i := 0; i <= len(result.Messages)-1; i++ { 488 | if !yield(result.Messages[i], nil) { 489 | return 490 | } 491 | } 492 | 493 | nMsgRetrieved += len(result.Messages) 494 | // If we received the number of messages requested then return. 495 | if nMsgRetrieved >= limit { 496 | return 497 | } 498 | 499 | // As an MQTT message size is limited, we need to paginate the history request. 500 | // In case we have more messages to retrieve, set the startFromID to the message ID 501 | // of the last message we received to continue retrieving them from this one. 502 | startFromID = result.Messages[0].ID 503 | } 504 | } 505 | } 506 | } 507 | 508 | // Makes a request 509 | func (c *Client) request(operation string, req interface{}) (Response, error) { 510 | request, err := json.Marshal(req) 511 | if err != nil { 512 | panic("unable to encode the request") 513 | } 514 | 515 | // Publish and wait for an error, response or puback 516 | // The client is locked until the callback is stored, so the response 517 | // cannot arrive before and be lost 518 | c.Lock() 519 | token := c.conn.Publish(fmt.Sprintf("emitter/%s/", operation), 1, false, request) 520 | respChan := c.store.PutCallback(token.(*mqtt.PublishToken).MessageID()) 521 | c.Unlock() 522 | if err := c.do(token); err != nil { 523 | return nil, err 524 | } 525 | resp := <-respChan 526 | if err, ok := resp.(error); ok { 527 | return nil, err 528 | } 529 | return resp, nil 530 | } 531 | 532 | // do waits for the operation to complete 533 | func (c *Client) do(t mqtt.Token) error { 534 | if !t.WaitTimeout(c.timeout) { 535 | return ErrTimeout 536 | } 537 | 538 | return t.Error() 539 | } 540 | 541 | // Makes a topic name from the key/channel pair 542 | func formatTopic(key, channel string, options []Option) string { 543 | key = trim(key) 544 | channel = trim(channel) 545 | opts := formatOptions(options) 546 | if len(key) == 0 { 547 | return fmt.Sprintf("%s/%s", channel, opts) 548 | } 549 | 550 | return fmt.Sprintf("%s/%s/%s", key, channel, opts) 551 | } 552 | 553 | // formatShare creates a shared topic subscription 554 | func formatShare(key, shareGroup, channel string, options []Option) string { 555 | return fmt.Sprintf("%s/$share/%s/%s/%s", trim(key), trim(shareGroup), trim(channel), formatOptions(options)) 556 | } 557 | 558 | // getHeader gets the header fields from options. 559 | func getHeader(options []Option) (qos byte, retain bool) { 560 | for _, o := range options { 561 | switch o { 562 | case withRetain: 563 | retain = true 564 | case withQos0: 565 | qos = 0 566 | case withQos1: 567 | qos = 1 568 | } 569 | } 570 | return 571 | } 572 | 573 | // formatOptions formats a set of options, ignoring the reserved ones 574 | func formatOptions(options []Option) string { 575 | opts, hasOpts := "", false 576 | if options != nil && len(options) > 0 { 577 | for _, option := range options { 578 | opt := option.String() 579 | if opt[0] == '+' { 580 | continue 581 | } 582 | 583 | if !hasOpts { 584 | hasOpts = true 585 | opts += "?" 586 | } else { 587 | opts += "&" 588 | } 589 | 590 | opts += opt 591 | } 592 | } 593 | return opts 594 | } 595 | 596 | // Trim removes both suffix and prefix 597 | func trim(v string) string { 598 | return strings.TrimSuffix(strings.TrimPrefix(v, "/"), "/") 599 | } 600 | -------------------------------------------------------------------------------- /v2/emitter_test.go: -------------------------------------------------------------------------------- 1 | package emitter 2 | 3 | import ( 4 | "fmt" 5 | "testing" 6 | "time" 7 | 8 | "github.com/stretchr/testify/assert" 9 | ) 10 | 11 | func TestEndToEnd(t *testing.T) { 12 | clientA(t) 13 | clientB(t) 14 | 15 | // stop after 1 seconds 16 | time.Sleep(1 * time.Second) 17 | } 18 | 19 | func clientA(t *testing.T) { 20 | const key = "HMauuTysKrOZPyHc9ANkMfJfeAD_QDgQ" // read on sdk-integration-test/#/ 21 | 22 | // Create the client and connect to the broker 23 | c, _ := Connect("tcp://localhost:8080", func(_ *Client, msg Message) { 24 | fmt.Printf("[emitter] -> [A] received: '%s' topic: '%s'\n", msg.Payload(), msg.Topic()) 25 | }) 26 | 27 | // Subscribe to demo channel 28 | fmt.Println("[emitter] <- [A] subscribing to 'demo/...'") 29 | err := c.Subscribe(key, "sdk-integration-test/", nil) 30 | assert.NoError(t, err) 31 | } 32 | 33 | func clientB(t *testing.T) { 34 | const key = "HMauuTysKrOZPyHc9ANkMfJfeAD_QDgQ" // everything on sdk-integration-test/ 35 | 36 | // Create the client and connect to the broker 37 | c, _ := Connect("tcp://localhost:8080", func(_ *Client, msg Message) { 38 | fmt.Printf("[emitter] -> [B] received: '%s' topic: '%s'\n", msg.Payload(), msg.Topic()) 39 | }) 40 | 41 | c.OnPresence(func(_ *Client, ev PresenceEvent) { 42 | fmt.Printf("[emitter] -> [B] presence change event: '%s'. Topic: '%s'. ID: '%s'\n", ev.Event, ev.Channel, ev.Who[0].ID) 43 | }) 44 | 45 | fmt.Println("[emitter] <- [B] querying own name") 46 | id := c.ID() 47 | assert.NotEmpty(t, id) 48 | 49 | // Subscribe to demo channel 50 | c.Subscribe(key, "sdk-integration-test/", func(_ *Client, msg Message) { 51 | fmt.Printf("[emitter] -> [B] received on specific handler: '%s' topic: '%s'\n", msg.Payload(), msg.Topic()) 52 | }) 53 | 54 | // Ask for presence 55 | fmt.Println("[emitter] <- [B] asking for presence on 'sdk-integration-test/'") 56 | resp, err := c.Presence(key, "sdk-integration-test/", true, true) 57 | assert.NoError(t, err) 58 | assert.NotNil(t, resp) 59 | fmt.Printf("[emitter] -> [B] presence status response: %d subscriber(s) at topic: '%s'\n", len(resp.Who), resp.Channel) 60 | 61 | // Publish to the channel 62 | fmt.Println("[emitter] <- [B] publishing to 'sdk-integration-test/'") 63 | err = c.Publish(key, "sdk-integration-test/", "hello") 64 | assert.NoError(t, err) 65 | 66 | // Unsubscribe from the channel 67 | fmt.Println("[emitter] <- [B] unsubscribing from 'sdk-integration-test/'") 68 | err = c.Unsubscribe(key, "sdk-integration-test/") 69 | assert.NoError(t, err) 70 | 71 | time.Sleep(1 * time.Second) 72 | } 73 | 74 | func TestFormatTopic(t *testing.T) { 75 | tests := []struct { 76 | key string 77 | channel string 78 | options []Option 79 | result string 80 | }{ 81 | {channel: "a/b/c", result: "a/b/c/"}, 82 | {key: "key", channel: "channel", result: "key/channel/"}, 83 | {key: "key", channel: "a/b/c", result: "key/a/b/c/"}, 84 | {key: "key", channel: "a/b/c", options: []Option{WithoutEcho()}, result: "key/a/b/c/?me=0"}, 85 | {key: "key", channel: "a/b/c", options: []Option{WithoutEcho(), WithAtLeastOnce(), WithLast(100)}, result: "key/a/b/c/?me=0&last=100"}, 86 | {key: "key", channel: "a/b/c", options: []Option{WithAtLeastOnce(), WithoutEcho(), WithLast(100)}, result: "key/a/b/c/?me=0&last=100"}, 87 | {key: "key", channel: "a/b/c", options: []Option{WithoutEcho(), WithLast(100), WithAtLeastOnce()}, result: "key/a/b/c/?me=0&last=100"}, 88 | } 89 | 90 | for _, tc := range tests { 91 | topic := formatTopic(tc.key, tc.channel, tc.options) 92 | assert.Equal(t, tc.result, topic) 93 | } 94 | } 95 | 96 | func TestGetHeader(t *testing.T) { 97 | tests := []struct { 98 | options []Option 99 | qos byte 100 | retain bool 101 | }{ 102 | 103 | {options: []Option{WithoutEcho()}, qos: 0, retain: false}, 104 | {options: []Option{WithoutEcho(), WithAtLeastOnce(), WithLast(100)}, qos: 1, retain: false}, 105 | {options: []Option{WithAtLeastOnce(), WithoutEcho(), WithLast(100)}, qos: 1, retain: false}, 106 | {options: []Option{WithoutEcho(), WithLast(100), WithAtLeastOnce()}, qos: 1, retain: false}, 107 | {options: []Option{WithoutEcho(), WithRetain(), WithAtMostOnce()}, qos: 0, retain: true}, 108 | } 109 | 110 | for _, tc := range tests { 111 | qos, retain := getHeader(tc.options) 112 | assert.Equal(t, tc.qos, qos) 113 | assert.Equal(t, tc.retain, retain) 114 | } 115 | } 116 | 117 | func TestFormatShare(t *testing.T) { 118 | topic := formatShare("/key/", "share1", "/a/b/c/", []Option{WithoutEcho()}) 119 | assert.Equal(t, "key/$share/share1/a/b/c/?me=0", topic) 120 | } 121 | 122 | func TestPresence(t *testing.T) { 123 | c := NewClient() 124 | 125 | var events []PresenceEvent 126 | c.OnPresence(func(_ *Client, ev PresenceEvent) { 127 | events = append(events, ev) 128 | }) 129 | 130 | c.onMessage(nil, &message{ 131 | topic: "emitter/presence/", 132 | payload: ` {"time":1589626821,"event":"unsubscribe","channel":"retain-demo/","who":[{"id":"B"}, {"id":"C"}]}`, 133 | }) 134 | 135 | c.onMessage(nil, &message{ 136 | topic: "emitter/presence/", 137 | payload: ` {"time":1589626821,"event":"subscribe","channel":"retain-demo/","who":{"id":"A"}}`, 138 | }) 139 | 140 | assert.Equal(t, 2, len(events)) 141 | } 142 | 143 | /* 144 | func TestHistory(t *testing.T) { 145 | c, err := Connect("tcp://localhost:8080", nil) 146 | assert.NoError(t, err) 147 | 148 | for messageHistory, err := range c.History("JN8kaVOZQtG-G6QHnbFzcI-uyS_M3L5q", "test/", 1685608812, 1757293928, 5) { 149 | if err != nil { 150 | fmt.Println(err) 151 | break 152 | } 153 | fmt.Println(messageHistory) 154 | if messageHistory.Payload == "Hello World3" { 155 | break 156 | } 157 | } 158 | 159 | }*/ 160 | -------------------------------------------------------------------------------- /v2/go.mod: -------------------------------------------------------------------------------- 1 | module github.com/emitter-io/go/v2 2 | 3 | go 1.23 4 | 5 | require ( 6 | github.com/eclipse/paho.mqtt.golang v1.5.0 7 | github.com/stretchr/testify v1.8.2 8 | ) 9 | 10 | require ( 11 | github.com/davecgh/go-spew v1.1.1 // indirect 12 | github.com/gorilla/websocket v1.5.3 // indirect 13 | github.com/pmezard/go-difflib v1.0.0 // indirect 14 | golang.org/x/net v0.29.0 // indirect 15 | golang.org/x/sync v0.8.0 // indirect 16 | gopkg.in/yaml.v3 v3.0.1 // indirect 17 | ) 18 | -------------------------------------------------------------------------------- /v2/go.sum: -------------------------------------------------------------------------------- 1 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 2 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 3 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 4 | github.com/eclipse/paho.mqtt.golang v1.5.0 h1:EH+bUVJNgttidWFkLLVKaQPGmkTUfQQqjOsyvMGvD6o= 5 | github.com/eclipse/paho.mqtt.golang v1.5.0/go.mod h1:du/2qNQVqJf/Sqs4MEL77kR8QTqANF7XU7Fk0aOTAgk= 6 | github.com/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aNNg= 7 | github.com/gorilla/websocket v1.5.3/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= 8 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 9 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 10 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 11 | github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= 12 | github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= 13 | github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 14 | github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= 15 | github.com/stretchr/testify v1.8.2 h1:+h33VjcLVPDHtOdpUCuF+7gSuG3yGIftsP1YvFihtJ8= 16 | github.com/stretchr/testify v1.8.2/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= 17 | golang.org/x/net v0.29.0 h1:5ORfpBpCs4HzDYoodCDBbwHzdR5UrLBZ3sOnUJmFoHo= 18 | golang.org/x/net v0.29.0/go.mod h1:gLkgy8jTGERgjzMic6DS9+SP0ajcu6Xu3Orq/SpETg0= 19 | golang.org/x/sync v0.8.0 h1:3NFvSEYkUoMifnESzZl15y791HH1qU2xm6eCJU5ZPXQ= 20 | golang.org/x/sync v0.8.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= 21 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= 22 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 23 | gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 24 | gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= 25 | gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 26 | -------------------------------------------------------------------------------- /v2/options.go: -------------------------------------------------------------------------------- 1 | package emitter 2 | 3 | import ( 4 | "crypto/tls" 5 | "net/url" 6 | "strconv" 7 | "time" 8 | ) 9 | 10 | // WithMatcher If "mqtt", then topic matching would follow MQTT specification. 11 | func WithMatcher(matcher string) func(*Client) { 12 | return func(c *Client) { 13 | if "mqtt" == matcher { 14 | c.handlers = NewTrieMQTT() 15 | } 16 | } 17 | } 18 | 19 | // WithBrokers configures broker URIs to connect to. The format should be scheme://host:port 20 | // Where "scheme" is one of "tcp", "ssl", or "ws", "host" is the ip-address (or hostname) 21 | // and "port" is the port on which the broker is accepting connections. 22 | func WithBrokers(brokers ...string) func(*Client) { 23 | return func(c *Client) { 24 | c.opts.Servers = []*url.URL{} 25 | for _, broker := range brokers { 26 | brokerURI, err := url.Parse(broker) 27 | if err != nil { 28 | panic(err) 29 | } 30 | 31 | c.opts.Servers = append(c.opts.Servers, brokerURI) 32 | } 33 | } 34 | } 35 | 36 | // WithClientID will set the client id to be used by this client when 37 | // connecting to the MQTT broker. According to the MQTT v3.1 specification, 38 | // a client id mus be no longer than 23 characters. 39 | func WithClientID(id string) func(*Client) { 40 | return func(c *Client) { 41 | c.opts.SetClientID(id) 42 | } 43 | } 44 | 45 | // WithUsername will set the username to be used by this client when connecting 46 | // to the MQTT broker. Note: without the use of SSL/TLS, this information will 47 | // be sent in plaintext accross the wire. 48 | func WithUsername(username string) func(*Client) { 49 | return func(c *Client) { 50 | c.opts.SetUsername(username) 51 | } 52 | } 53 | 54 | // WithPassword will set the password to be used by this client when connecting 55 | // to the MQTT broker. Note: without the use of SSL/TLS, this information will 56 | // be sent in plaintext accross the wire. 57 | func WithPassword(password string) func(*Client) { 58 | return func(c *Client) { 59 | c.opts.SetPassword(password) 60 | } 61 | } 62 | 63 | // WithTLSConfig will set an SSL/TLS configuration to be used when connecting 64 | // to an MQTT broker. Please read the official Go documentation for more 65 | // information. 66 | func WithTLSConfig(t *tls.Config) func(*Client) { 67 | return func(c *Client) { 68 | c.opts.SetTLSConfig(t) 69 | } 70 | } 71 | 72 | // WithKeepAlive will set the amount of time (in seconds) that the client 73 | // should wait before sending a PING request to the broker. This will 74 | // allow the client to know that a connection has not been lost with the 75 | // server. 76 | func WithKeepAlive(k time.Duration) func(*Client) { 77 | return func(c *Client) { 78 | c.opts.SetKeepAlive(k) 79 | } 80 | } 81 | 82 | // WithPingTimeout will set the amount of time (in seconds) that the client 83 | // will wait after sending a PING request to the broker, before deciding 84 | // that the connection has been lost. Default is 10 seconds. 85 | func WithPingTimeout(k time.Duration) func(*Client) { 86 | return func(c *Client) { 87 | c.opts.SetPingTimeout(k) 88 | } 89 | } 90 | 91 | // WithConnectTimeout limits how long the client will wait when trying to open a connection 92 | // to an MQTT server before timeing out and erroring the attempt. A duration of 0 never times out. 93 | // Default 30 seconds. Currently only operational on TCP/TLS connections. 94 | func WithConnectTimeout(t time.Duration) func(*Client) { 95 | return func(c *Client) { 96 | c.opts.SetConnectTimeout(t) 97 | } 98 | } 99 | 100 | // WithMaxReconnectInterval sets the maximum time that will be waited between reconnection attempts 101 | // when connection is lost 102 | func WithMaxReconnectInterval(t time.Duration) func(*Client) { 103 | return func(c *Client) { 104 | c.opts.SetMaxReconnectInterval(t) 105 | } 106 | } 107 | 108 | // WithAutoReconnect sets whether the automatic reconnection logic should be used 109 | // when the connection is lost, even if disabled the ConnectionLostHandler is still 110 | // called 111 | func WithAutoReconnect(a bool) func(*Client) { 112 | return func(c *Client) { 113 | c.opts.SetAutoReconnect(a) 114 | } 115 | } 116 | 117 | // option represents a key/value pair that can be supplied to the publish/subscribe or unsubscribe 118 | // methods and provide ways to configure the operation. 119 | type option string 120 | 121 | const ( 122 | withRetain = option("+r") 123 | withQos0 = option("+0") 124 | withQos1 = option("+1") 125 | ) 126 | 127 | // String converts the option to a string. 128 | func (o option) String() string { 129 | return string(o) 130 | } 131 | 132 | // WithoutEcho constructs an option which disables self-receiving messages if subscribed to a channel. 133 | func WithoutEcho() Option { 134 | return option("me=0") 135 | } 136 | 137 | // WithTTL constructs an option which can be used during publish requests to set a Time-To-Live. 138 | func WithTTL(seconds int) Option { 139 | return option("ttl=" + strconv.Itoa(seconds)) 140 | } 141 | 142 | // WithLast constructs an option which can be used during subscribe requests to retrieve a message history. 143 | func WithLast(messages int) Option { 144 | return option("last=" + strconv.Itoa(messages)) 145 | } 146 | 147 | // WithRetain constructs an option which sets the message 'retain' flag to true. 148 | func WithRetain() Option { 149 | return withRetain 150 | } 151 | 152 | // WithAtMostOnce instructs to publish at most once (MQTT QoS 0). 153 | func WithAtMostOnce() Option { 154 | return withQos0 155 | } 156 | 157 | // WithAtLeastOnce instructs to publish at least once (MQTT QoS 1). 158 | func WithAtLeastOnce() Option { 159 | return withQos1 160 | } 161 | 162 | func getUTCTimestamp(input time.Time) int64 { 163 | t := input 164 | if zone, _ := t.Zone(); zone != "UTC" { 165 | loc, _ := time.LoadLocation("UTC") 166 | t = t.In(loc) 167 | } 168 | return t.Unix() 169 | } 170 | 171 | // WithFrom request messages from a point in time. 172 | func WithFrom(from time.Time) Option { 173 | return option("from=" + strconv.FormatInt(getUTCTimestamp(from), 10)) 174 | } 175 | 176 | // WithUntil request messages until a point in time. 177 | func WithUntil(until time.Time) Option { 178 | return option("until=" + strconv.FormatInt(getUTCTimestamp(until), 10)) 179 | } 180 | -------------------------------------------------------------------------------- /v2/options_test.go: -------------------------------------------------------------------------------- 1 | package emitter 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/stretchr/testify/assert" 7 | ) 8 | 9 | func TestOptions(t *testing.T) { 10 | assert.Equal(t, "me=0", WithoutEcho().String()) 11 | assert.Equal(t, "ttl=5", WithTTL(5).String()) 12 | assert.Equal(t, "last=5", WithLast(5).String()) 13 | } 14 | -------------------------------------------------------------------------------- /v2/store.go: -------------------------------------------------------------------------------- 1 | package emitter 2 | 3 | import ( 4 | "fmt" 5 | "sync" 6 | 7 | "github.com/eclipse/paho.mqtt.golang/packets" 8 | ) 9 | 10 | // In-memory storage implementation 11 | type store struct { 12 | sync.RWMutex 13 | messages map[string]*packet 14 | } 15 | 16 | // Response represents a generic response sent by the broker. 17 | type Response interface { 18 | RequestID() uint16 19 | } 20 | 21 | // The control packet with an optional error and a response. 22 | type packet struct { 23 | packets.ControlPacket 24 | callback chan Response 25 | } 26 | 27 | // newStore creates a new message storage layer. 28 | func newErrorStore() *store { 29 | store := &store{ 30 | messages: make(map[string]*packet), 31 | } 32 | return store 33 | } 34 | 35 | // Open initializes a MemoryStore instance. 36 | func (store *store) Open() {} 37 | 38 | // Put takes a key and a pointer to a Message and stores the message. 39 | func (store *store) Put(key string, message packets.ControlPacket) { 40 | store.Lock() 41 | defer store.Unlock() 42 | 43 | store.messages[key] = &packet{ 44 | ControlPacket: message, 45 | } 46 | } 47 | 48 | // Get takes a key and looks in the store for a matching Message 49 | // returning either the Message pointer or nil. 50 | func (store *store) Get(key string) packets.ControlPacket { 51 | store.RLock() 52 | defer store.RUnlock() 53 | return store.messages[key] 54 | } 55 | 56 | // All returns a slice of strings containing all the keys currently 57 | // in the MemoryStore. 58 | func (store *store) All() []string { 59 | store.RLock() 60 | defer store.RUnlock() 61 | 62 | keys := []string{} 63 | for k := range store.messages { 64 | keys = append(keys, k) 65 | } 66 | return keys 67 | } 68 | 69 | // Del takes a key, searches the MemoryStore and if the key is found 70 | // deletes the Message pointer associated with it. 71 | func (store *store) Del(key string) { 72 | store.Lock() 73 | defer store.Unlock() 74 | 75 | m := store.messages[key] 76 | if m != nil && m.callback == nil { 77 | delete(store.messages, key) 78 | } 79 | } 80 | 81 | // Close will disallow modifications to the state of the store. 82 | func (store *store) Close() {} 83 | 84 | // Reset eliminates all persisted message data in the store. 85 | func (store *store) Reset() { 86 | store.Lock() 87 | defer store.Unlock() 88 | store.messages = make(map[string]*packet) 89 | } 90 | 91 | // PutCallback adds a callback channel to a message. 92 | func (store *store) PutCallback(id uint16) <-chan Response { 93 | store.Lock() 94 | defer store.Unlock() 95 | 96 | key := outboundKeyFromMID(id) 97 | if m, ok := store.messages[key]; ok && m != nil { 98 | m.callback = make(chan Response, 1) 99 | return m.callback 100 | } 101 | return nil 102 | } 103 | 104 | // NotifyResponse notifies a response on a callback (if exists) 105 | func (store *store) NotifyResponse(id uint16, response Response) bool { 106 | store.RLock() 107 | defer store.RUnlock() 108 | 109 | key := outboundKeyFromMID(id) 110 | if m, ok := store.messages[key]; ok && m != nil { 111 | m.callback <- response 112 | close(m.callback) 113 | delete(store.messages, key) 114 | return true 115 | } 116 | return false 117 | } 118 | 119 | // Return a string of the form "o.[id]" 120 | func outboundKeyFromMID(id uint16) string { 121 | return fmt.Sprintf("o.%d", id) 122 | } 123 | -------------------------------------------------------------------------------- /v2/subtrie.go: -------------------------------------------------------------------------------- 1 | package emitter 2 | 3 | import ( 4 | "strings" 5 | "sync" 6 | ) 7 | 8 | // route is a value associated with a subscription. 9 | type route struct { 10 | Topic string 11 | Action MessageHandler 12 | } 13 | 14 | func newRoute(topic string, handler MessageHandler) ([]string, route) { 15 | query := strings.FieldsFunc(topic, func(c rune) bool { 16 | return c == '/' 17 | }) 18 | 19 | return query, route{ 20 | Topic: topic, 21 | Action: handler, 22 | } 23 | } 24 | 25 | // ------------------------------------------------------------------------------------ 26 | 27 | type node struct { 28 | word string 29 | routes map[string]route 30 | parent *node 31 | children map[string]*node 32 | } 33 | 34 | func (n *node) orphan() { 35 | if n.parent == nil { 36 | return 37 | } 38 | 39 | delete(n.parent.children, n.word) 40 | if len(n.parent.routes) == 0 && len(n.parent.children) == 0 { 41 | n.parent.orphan() 42 | } 43 | } 44 | 45 | // trie represents an efficient collection of subscriptions with lookup capability. 46 | type trie struct { 47 | sync.RWMutex 48 | root *node // The root node of the tree. 49 | lookup func(query []string, result *[]MessageHandler, node *node) 50 | } 51 | 52 | // newTrie creates a new trie without a lookup function. 53 | func newTrie() *trie { 54 | return &trie{ 55 | root: &node{ 56 | children: make(map[string]*node), 57 | }, 58 | } 59 | } 60 | 61 | // NewTrie creates a new subscriptions matcher using standard emitter strategy. 62 | func NewTrie() *trie { 63 | t := newTrie() 64 | t.lookup = t.lookupEmitter 65 | return t 66 | } 67 | 68 | // NewTrieMQTT creates a new subscriptions matcher using standard MQTT strategy. 69 | func NewTrieMQTT() *trie { 70 | t := newTrie() 71 | t.lookup = t.lookupMqtt 72 | return t 73 | } 74 | 75 | // AddHandler adds a message handler to a topic. 76 | func (t *trie) AddHandler(topic string, handler MessageHandler) error { 77 | query, rt := newRoute(topic, handler) 78 | 79 | t.Lock() 80 | curr := t.root 81 | for _, word := range query { 82 | child, ok := curr.children[word] 83 | if !ok { 84 | child = &node{ 85 | word: word, 86 | parent: curr, 87 | routes: make(map[string]route), 88 | children: make(map[string]*node), 89 | } 90 | curr.children[word] = child 91 | } 92 | curr = child 93 | } 94 | 95 | // Add the handler 96 | curr.routes[rt.Topic] = rt 97 | t.Unlock() 98 | return nil 99 | } 100 | 101 | // RemoveHandler removes a message handler from a topic. 102 | func (t *trie) RemoveHandler(topic string) { 103 | query, _ := newRoute(topic, nil) 104 | 105 | t.Lock() 106 | curr := t.root 107 | for _, word := range query { 108 | child, ok := curr.children[word] 109 | if !ok { 110 | // Subscription doesn't exist. 111 | t.Unlock() 112 | return 113 | } 114 | curr = child 115 | } 116 | 117 | // Remove the route 118 | delete(curr.routes, topic) 119 | 120 | // Remove orphans 121 | if len(curr.routes) == 0 && len(curr.children) == 0 { 122 | curr.orphan() 123 | } 124 | t.Unlock() 125 | } 126 | 127 | // Lookup returns the handlers for the given topic. 128 | func (t *trie) Lookup(topic string) []MessageHandler { 129 | query, _ := newRoute(topic, nil) 130 | var result []MessageHandler 131 | 132 | t.RLock() 133 | t.lookup(query, &result, t.root) 134 | t.RUnlock() 135 | return result 136 | } 137 | 138 | func (t *trie) lookupEmitter(query []string, result *[]MessageHandler, node *node) { 139 | 140 | // Add routes from the current branch 141 | for _, route := range node.routes { 142 | *result = append(*result, route.Action) 143 | } 144 | 145 | // If we're not yet done, continue 146 | if len(query) > 0 { 147 | 148 | // Go through the exact match branch 149 | if n, ok := node.children[query[0]]; ok { 150 | t.lookupEmitter(query[1:], result, n) 151 | } 152 | 153 | // Go through wildcard match branch 154 | if n, ok := node.children["+"]; ok { 155 | t.lookupEmitter(query[1:], result, n) 156 | } 157 | } 158 | } 159 | 160 | func (t *trie) lookupMqtt(query []string, result *[]MessageHandler, node *node) { 161 | if len(query) == 0 { 162 | // Add routes from the current branch 163 | for _, route := range node.routes { 164 | *result = append(*result, route.Action) 165 | } 166 | } 167 | 168 | // If we're not yet done, continue 169 | if len(query) > 0 { 170 | // Go through the exact match branch 171 | if n, ok := node.children[query[0]]; ok { 172 | t.lookupMqtt(query[1:], result, n) 173 | } 174 | 175 | // Go through wildcard match branch 176 | if n, ok := node.children["+"]; ok { 177 | t.lookupMqtt(query[1:], result, n) 178 | } 179 | 180 | // Go through wildcard match branch 181 | if n, ok := node.children["#"]; ok { 182 | for _, route := range n.routes { 183 | *result = append(*result, route.Action) 184 | } 185 | } 186 | } 187 | } 188 | -------------------------------------------------------------------------------- /v2/subtrie_test.go: -------------------------------------------------------------------------------- 1 | package emitter 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/stretchr/testify/assert" 7 | ) 8 | 9 | func TestRoute(t *testing.T) { 10 | query, _ := newRoute("a/", nil) 11 | assert.Len(t, query, 1) 12 | } 13 | 14 | func TestTrieMatch(t *testing.T) { 15 | m := NewTrie() 16 | testPopulateWithStrings(m, []string{ 17 | "a/", 18 | "a/b/c/", 19 | "a/+/c/", 20 | "a/b/c/d/", 21 | "a/+/c/+/", 22 | "x/", 23 | "x/y/", 24 | "x/+/z", 25 | }) 26 | 27 | // Tests to run 28 | tests := []struct { 29 | topic string 30 | n int 31 | }{ 32 | {topic: "a/", n: 1}, 33 | {topic: "a/1/", n: 1}, 34 | {topic: "a/2/", n: 1}, 35 | {topic: "a/1/2/", n: 1}, 36 | {topic: "a/1/2/3/", n: 1}, 37 | {topic: "a/x/y/c/", n: 1}, 38 | {topic: "a/x/c/", n: 2}, 39 | {topic: "a/b/c/", n: 3}, 40 | {topic: "a/b/c/d/", n: 5}, 41 | {topic: "a/b/c/e/", n: 4}, 42 | {topic: "x/y/c/e/", n: 2}, 43 | } 44 | 45 | for _, tc := range tests { 46 | result := m.Lookup(tc.topic) 47 | assert.Equal(t, tc.n, len(result)) 48 | } 49 | } 50 | 51 | // Populates the trie with a set of strings 52 | func testPopulateWithStrings(m *trie, values []string) { 53 | for _, s := range values { 54 | m.AddHandler(s, func(_ *Client, _ Message) { 55 | println("dummy handler") 56 | }) 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /v2/types.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) Roman Atachiants and contributors. All rights reserved. 2 | // Licensed under the MIT license. See LICENSE file in the project root for details. 3 | 4 | package emitter 5 | 6 | import ( 7 | "crypto/rand" 8 | "encoding/json" 9 | "fmt" 10 | ) 11 | 12 | // MessageHandler is a callback type which can be set to be 13 | // executed upon the arrival of messages published to topics 14 | // to which the client is subscribed. 15 | type MessageHandler func(*Client, Message) 16 | 17 | // PresenceHandler is a callback type which can be set to be executed upon 18 | // the arrival of presence events. 19 | type PresenceHandler func(*Client, PresenceEvent) 20 | 21 | // ErrorHandler is a callback type which can be set to be executed upon 22 | // the arrival of an emitter error. 23 | type ErrorHandler func(*Client, Error) 24 | 25 | // DisconnectHandler is a callback type which can be set to be 26 | // executed upon an unintended disconnection from the MQTT broker. 27 | // Disconnects caused by calling Disconnect or ForceDisconnect will 28 | // not cause an OnConnectionLost callback to execute. 29 | type DisconnectHandler func(*Client, error) 30 | 31 | // ConnectHandler is a callback that is called when the client 32 | // state changes from unconnected/disconnected to connected. Both 33 | // at initial connection and on reconnection 34 | type ConnectHandler func(*Client) 35 | 36 | // Option represents a key/value pair that can be supplied to the publish/subscribe or unsubscribe 37 | // methods and provide ways to configure the operation. 38 | type Option interface { 39 | String() string 40 | } 41 | 42 | // Error represents an event code which provides a more details. 43 | type Error struct { 44 | Request uint16 `json:"req,omitempty"` 45 | Status int `json:"status"` 46 | Message string `json:"message"` 47 | } 48 | 49 | // Error returns the error message. 50 | func (e *Error) Error() string { 51 | return e.Message 52 | } 53 | 54 | // RequestID returns the request ID for the response. 55 | func (e *Error) RequestID() uint16 { 56 | return e.Request 57 | } 58 | 59 | // ------------------------------------------------------------------------------------ 60 | 61 | // KeyGenRequest represents a request that can be sent to emitter broker 62 | // in order to generate a new channel key. 63 | type keygenRequest struct { 64 | Key string `json:"key"` 65 | Channel string `json:"channel"` 66 | Type string `json:"type"` 67 | TTL int `json:"ttl"` 68 | } 69 | 70 | // KeyGenResponse represents a response from emitter broker which contains 71 | // the response to the key generation request. 72 | type keyGenResponse struct { 73 | Request uint16 `json:"req,omitempty"` 74 | Status int `json:"status"` 75 | Key string `json:"key"` 76 | Channel string `json:"channel"` 77 | ErrorMessage string `json:"message"` 78 | } 79 | 80 | // RequestID returns the request ID for the response. 81 | func (r *keyGenResponse) RequestID() uint16 { 82 | return r.Request 83 | } 84 | 85 | // ------------------------------------------------------------------------------------ 86 | 87 | // PresenceRequest represents a request that can be sent to emitter broker 88 | // in order to request presence information. 89 | type presenceRequest struct { 90 | Key string `json:"key"` 91 | Channel string `json:"channel"` 92 | Status bool `json:"status"` 93 | Changes bool `json:"changes"` 94 | } 95 | 96 | // presenceResponse represents a presence message, for partial unmarshal 97 | type presenceResponse struct { 98 | Request uint16 `json:"req,omitempty"` 99 | Event string `json:"event"` 100 | Channel string `json:"channel"` 101 | Time int `json:"time"` 102 | Who json.RawMessage `json:"who"` 103 | } 104 | 105 | // RequestID returns the request ID for the response. 106 | func (r *presenceResponse) RequestID() uint16 { 107 | return r.Request 108 | } 109 | 110 | // PresenceEvent represents a response from emitter broker which contains 111 | // presence state or a join/leave notification. 112 | type PresenceEvent struct { 113 | presenceResponse 114 | Who []PresenceInfo 115 | } 116 | 117 | // RequestID returns the request ID for the response. 118 | func (r *PresenceEvent) RequestID() uint16 { 119 | return r.Request 120 | } 121 | 122 | // PresenceInfo represents a response from emitter broker which contains 123 | // presence information. 124 | type PresenceInfo struct { 125 | ID string `json:"id"` 126 | Username string `json:"username"` 127 | } 128 | 129 | // ------------------------------------------------------------------------------------ 130 | 131 | // meResponse represents information about the client. 132 | type meResponse struct { 133 | Request uint16 `json:"req,omitempty"` // The corresponding request ID. 134 | ID string `json:"id"` // The private ID of the connection. 135 | Links map[string]string `json:"links,omitempty"` // The set of pre-defined channels. 136 | } 137 | 138 | // RequestID returns the request ID for the response. 139 | func (r *meResponse) RequestID() uint16 { 140 | return r.Request 141 | } 142 | 143 | // ------------------------------------------------------------------------------------ 144 | 145 | // linkRequest represents a request to create a link. 146 | type linkRequest struct { 147 | Name string `json:"name"` // The name of the shortcut, max 2 characters. 148 | Key string `json:"key"` // The key for the channel. 149 | Channel string `json:"channel"` // The channel name for the shortcut. 150 | Subscribe bool `json:"subscribe"` // Specifies whether the broker should auto-subscribe. 151 | } 152 | 153 | // Link represents a response for the link creation. 154 | type Link struct { 155 | Request uint16 `json:"req,omitempty"` 156 | Name string `json:"name,omitempty"` // The name of the shortcut, max 2 characters. 157 | Channel string `json:"channel,omitempty"` // The channel which was registered. 158 | } 159 | 160 | // RequestID returns the request ID for the response. 161 | func (r *Link) RequestID() uint16 { 162 | return r.Request 163 | } 164 | 165 | // ------------------------------------------------------------------------------------ 166 | 167 | // KeyBanRequest represents a request that can be sent to emitter broker 168 | // in order to ban/blacklist a channel key. 169 | type keybanRequest struct { 170 | Secret string `json:"secret"` // The master key to use. 171 | Target string `json:"target"` // The target key to ban. 172 | Banned bool `json:"banned"` // Whether the target should be banned or not. 173 | } 174 | 175 | // keyBanResponse represents a response from emitter broker which contains 176 | // the response to the key ban request. 177 | type keyBanResponse struct { 178 | Request uint16 `json:"req,omitempty"` 179 | Status int `json:"status"` // The status of the response 180 | Banned bool `json:"banned"` // Whether the target should be banned or not. 181 | ErrorMessage string `json:"message"` 182 | } 183 | 184 | // RequestID returns the request ID for the response. 185 | func (r *keyBanResponse) RequestID() uint16 { 186 | return r.Request 187 | } 188 | 189 | // ------------------------------------------------------------------------------------ 190 | 191 | type MessageID []byte 192 | 193 | type historyRequest struct { 194 | Channel string `json:"channel"` 195 | StartFromID MessageID `json:"startFromID"` 196 | } 197 | 198 | type HistoryMessage struct { 199 | ID MessageID `json:"id"` 200 | Channel string `json:"channel"` 201 | Payload []byte `json:"payload"` 202 | } 203 | 204 | type historyResponse struct { 205 | Request uint16 `json:"req,omitempty"` // The corresponding request ID. 206 | Messages []HistoryMessage `json:"messages"` 207 | } 208 | 209 | // RequestID returns the request ID for the response. 210 | func (r *historyResponse) RequestID() uint16 { 211 | return r.Request 212 | } 213 | 214 | // ------------------------------------------------------------------------------------ 215 | 216 | // uuid generates a simple UUID 217 | func uuid() string { 218 | b := make([]byte, 16) 219 | _, err := rand.Read(b) 220 | if err != nil { 221 | panic(err) 222 | } 223 | 224 | return fmt.Sprintf("%x-%x-%x-%x-%x", b[0:4], b[4:6], b[6:8], b[8:10], b[10:]) 225 | } 226 | -------------------------------------------------------------------------------- /v2/types_test.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) Roman Atachiants and contributors. All rights reserved. 2 | // Licensed under the MIT license. See LICENSE file in the project root for details. 3 | 4 | package emitter 5 | 6 | import ( 7 | "testing" 8 | 9 | "github.com/stretchr/testify/assert" 10 | ) 11 | 12 | func TestUUID(t *testing.T) { 13 | id := uuid() 14 | 15 | assert.NotNil(t, id) 16 | assert.Len(t, id, 36) 17 | } 18 | 19 | type message struct { 20 | duplicate bool 21 | qos byte 22 | retained bool 23 | topic string 24 | messageID uint16 25 | payload string 26 | } 27 | 28 | func (m *message) Duplicate() bool { 29 | return m.duplicate 30 | } 31 | 32 | func (m *message) Qos() byte { 33 | return m.qos 34 | } 35 | 36 | func (m *message) Retained() bool { 37 | return m.retained 38 | } 39 | 40 | func (m *message) Topic() string { 41 | return m.topic 42 | } 43 | 44 | func (m *message) MessageID() uint16 { 45 | return m.messageID 46 | } 47 | 48 | func (m *message) Payload() []byte { 49 | return []byte(m.payload) 50 | } 51 | 52 | func (m *message) Ack() { 53 | } 54 | --------------------------------------------------------------------------------