mirror of
https://github.com/Luzifer/lounge-control.git
synced 2024-12-22 14:31:17 +00:00
Initial bunch of code
This commit is contained in:
commit
1be09c7ccd
14 changed files with 930 additions and 0 deletions
1
.gitignore
vendored
Normal file
1
.gitignore
vendored
Normal file
|
@ -0,0 +1 @@
|
|||
.env
|
66
cmd_join.go
Normal file
66
cmd_join.go
Normal file
|
@ -0,0 +1,66 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"strings"
|
||||
|
||||
"github.com/Luzifer/lounge-control/sioclient"
|
||||
"github.com/pkg/errors"
|
||||
log "github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
func init() {
|
||||
registerCommand("join", commandJoin)
|
||||
}
|
||||
|
||||
func commandJoin(args []string) handlerFunc {
|
||||
if len(args) == 0 {
|
||||
log.Fatal("No channels given to join")
|
||||
}
|
||||
|
||||
return addGenericHandler(func(pType string, msg *sioclient.Message) error {
|
||||
if pType != "init" {
|
||||
return nil
|
||||
}
|
||||
|
||||
// After join command is finished we can execute the joins
|
||||
network := initData.NetworkByNameOrUUID(cfg.Network)
|
||||
if network == nil {
|
||||
return errors.New("Network not found")
|
||||
}
|
||||
|
||||
var lobby *channel
|
||||
for _, c := range network.Channels {
|
||||
if c.Type == "lobby" {
|
||||
lobby = &c
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if lobby == nil {
|
||||
return errors.New("Unable to find lobby for network")
|
||||
}
|
||||
|
||||
for _, ch := range args {
|
||||
if !strings.HasPrefix(ch, "#") {
|
||||
ch = "#" + ch
|
||||
}
|
||||
|
||||
msg, err := sioclient.NewMessage(sioclient.MessageTypeEvent, 0, "input", map[string]interface{}{
|
||||
"text": fmt.Sprintf("/join %s", ch),
|
||||
"target": lobby.ID,
|
||||
})
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "Unable to compose join message")
|
||||
}
|
||||
|
||||
if err = msg.Send(client); err != nil {
|
||||
return errors.Wrap(err, "Unable to send join message")
|
||||
}
|
||||
}
|
||||
|
||||
interrupt <- os.Interrupt
|
||||
return nil
|
||||
})
|
||||
}
|
44
cmd_listChannels.go
Normal file
44
cmd_listChannels.go
Normal file
|
@ -0,0 +1,44 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"os"
|
||||
"sort"
|
||||
"strings"
|
||||
|
||||
"github.com/Luzifer/lounge-control/sioclient"
|
||||
)
|
||||
|
||||
func init() {
|
||||
registerCommand("list-channels", commandListChannels)
|
||||
}
|
||||
|
||||
func commandListChannels(args []string) handlerFunc {
|
||||
return addGenericHandler(func(pType string, msg *sioclient.Message) error {
|
||||
if pType != "init" {
|
||||
return nil
|
||||
}
|
||||
|
||||
network := initData.NetworkByNameOrUUID(cfg.Network)
|
||||
if network == nil {
|
||||
return errors.New("Network not found")
|
||||
}
|
||||
|
||||
var channels []string
|
||||
|
||||
for _, c := range network.Channels {
|
||||
if c.Type == "lobby" {
|
||||
continue
|
||||
}
|
||||
|
||||
channels = append(channels, c.Name)
|
||||
}
|
||||
|
||||
sort.Strings(channels)
|
||||
|
||||
fmt.Println(strings.Join(channels, "\n"))
|
||||
interrupt <- os.Interrupt
|
||||
return nil
|
||||
})
|
||||
}
|
66
cmd_part.go
Normal file
66
cmd_part.go
Normal file
|
@ -0,0 +1,66 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"strings"
|
||||
|
||||
"github.com/Luzifer/lounge-control/sioclient"
|
||||
"github.com/pkg/errors"
|
||||
log "github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
func init() {
|
||||
registerCommand("part", commandPart)
|
||||
}
|
||||
|
||||
func commandPart(args []string) handlerFunc {
|
||||
if len(args) == 0 {
|
||||
log.Fatal("No channels given to part")
|
||||
}
|
||||
|
||||
return addGenericHandler(func(pType string, msg *sioclient.Message) error {
|
||||
if pType != "init" {
|
||||
return nil
|
||||
}
|
||||
|
||||
// After join command is finished we can execute the joins
|
||||
network := initData.NetworkByNameOrUUID(cfg.Network)
|
||||
if network == nil {
|
||||
return errors.New("Network not found")
|
||||
}
|
||||
|
||||
var lobby *channel
|
||||
for _, c := range network.Channels {
|
||||
if c.Type == "lobby" {
|
||||
lobby = &c
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if lobby == nil {
|
||||
return errors.New("Unable to find lobby for network")
|
||||
}
|
||||
|
||||
for _, ch := range args {
|
||||
if !strings.HasPrefix(ch, "#") {
|
||||
ch = "#" + ch
|
||||
}
|
||||
|
||||
msg, err := sioclient.NewMessage(sioclient.MessageTypeEvent, 0, "input", map[string]interface{}{
|
||||
"text": fmt.Sprintf("/part %s", ch),
|
||||
"target": lobby.ID,
|
||||
})
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "Unable to compose part message")
|
||||
}
|
||||
|
||||
if err = msg.Send(client); err != nil {
|
||||
return errors.Wrap(err, "Unable to send part message")
|
||||
}
|
||||
}
|
||||
|
||||
interrupt <- os.Interrupt
|
||||
return nil
|
||||
})
|
||||
}
|
42
commands.go
Normal file
42
commands.go
Normal file
|
@ -0,0 +1,42 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"sort"
|
||||
"sync"
|
||||
|
||||
log "github.com/sirupsen/logrus"
|
||||
|
||||
"github.com/Luzifer/lounge-control/sioclient"
|
||||
)
|
||||
|
||||
type commandFunc func(args []string) handlerFunc
|
||||
type handlerFunc func(msg *sioclient.Message) error
|
||||
type typedHandlerFunc func(mType string, msg *sioclient.Message) error
|
||||
|
||||
var (
|
||||
commands = map[string]commandFunc{}
|
||||
commandsMutex = new(sync.RWMutex)
|
||||
)
|
||||
|
||||
func availableCommands() (cmds []string) {
|
||||
commandsMutex.RLock()
|
||||
defer commandsMutex.RUnlock()
|
||||
|
||||
for k := range commands {
|
||||
cmds = append(cmds, k)
|
||||
}
|
||||
|
||||
sort.Strings(cmds)
|
||||
return cmds
|
||||
}
|
||||
|
||||
func registerCommand(cmd string, cf commandFunc) {
|
||||
commandsMutex.Lock()
|
||||
defer commandsMutex.Unlock()
|
||||
|
||||
if _, ok := commands[cmd]; ok {
|
||||
log.Fatalf("Duplicate registration of command %q", cmd)
|
||||
}
|
||||
|
||||
commands[cmd] = cf
|
||||
}
|
15
go.mod
Normal file
15
go.mod
Normal file
|
@ -0,0 +1,15 @@
|
|||
module github.com/Luzifer/lounge-control
|
||||
|
||||
go 1.14
|
||||
|
||||
replace github.com/Luzifer/lounge-control/sioclient => ./sioclient
|
||||
|
||||
require (
|
||||
github.com/Luzifer/lounge-control/sioclient v0.0.0-00010101000000-000000000000
|
||||
github.com/Luzifer/rconfig/v2 v2.2.1
|
||||
github.com/gorilla/websocket v1.4.2 // indirect
|
||||
github.com/pkg/errors v0.9.1
|
||||
github.com/sacOO7/go-logger v0.0.0-20180719173527-9ac9add5a50d // indirect
|
||||
github.com/sacOO7/gowebsocket v0.0.0-20180719182212-1436bb906a4e
|
||||
github.com/sirupsen/logrus v1.6.0
|
||||
)
|
27
go.sum
Normal file
27
go.sum
Normal file
|
@ -0,0 +1,27 @@
|
|||
github.com/Luzifer/rconfig v1.2.0 h1:waD1sqasGVSQSrExpLrQ9Q1JmMaltrS391VdOjWXP/I=
|
||||
github.com/Luzifer/rconfig/v2 v2.2.1 h1:zcDdLQlnlzwcBJ8E0WFzOkQE1pCMn3EbX0dFYkeTczg=
|
||||
github.com/Luzifer/rconfig/v2 v2.2.1/go.mod h1:OKIX0/JRZrPJ/ZXXWklQEFXA6tBfWaljZbW37w+sqBw=
|
||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/gorilla/websocket v1.4.2 h1:+/TMaTYc4QFitKJxsQ7Yye35DkWvkdLcvGKqM+x0Ufc=
|
||||
github.com/gorilla/websocket v1.4.2/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
|
||||
github.com/konsorten/go-windows-terminal-sequences v1.0.3 h1:CE8S1cTafDpPvMhIxNJKvHsGVBgn1xWYf1NbHQhywc8=
|
||||
github.com/konsorten/go-windows-terminal-sequences v1.0.3/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
|
||||
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
|
||||
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/sacOO7/go-logger v0.0.0-20180719173527-9ac9add5a50d h1:5T+fbRuQbpi+WZtB2yfuu59r00F6T2HV/zGYrwX8nvE=
|
||||
github.com/sacOO7/go-logger v0.0.0-20180719173527-9ac9add5a50d/go.mod h1:L5EJe2k8GwpBoGXDRLAEs58R239jpZuE7NNEtW+T7oo=
|
||||
github.com/sacOO7/gowebsocket v0.0.0-20180719182212-1436bb906a4e h1:yG1sLAkFltiFiwIpKdiA2CVIPxRL4P9OywNnfq45ivg=
|
||||
github.com/sacOO7/gowebsocket v0.0.0-20180719182212-1436bb906a4e/go.mod h1:4a2a9BlxB807BaME8FJzQRLrZwYKj0cWjon25PlIssM=
|
||||
github.com/sirupsen/logrus v1.6.0 h1:UBcNElsrwanuuMsnGSlYmtmgbb23qDR5dG+6X6Oo89I=
|
||||
github.com/sirupsen/logrus v1.6.0/go.mod h1:7uNnSEd1DgxDLC74fIahvMZmmYsHGZGEOFrfsX/uA88=
|
||||
github.com/spf13/pflag v1.0.3 h1:zPAT6CGy6wXeQ7NtTnaTerfKOsV6V6F8agHXFiazDkg=
|
||||
github.com/spf13/pflag v1.0.3/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4=
|
||||
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
|
||||
golang.org/x/sys v0.0.0-20190422165155-953cdadca894 h1:Cz4ceDQGXuKRnVBDTS23GTn/pU5OE2C0WrNTOYK1Uuc=
|
||||
golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/validator.v2 v2.0.0-20180514200540-135c24b11c19 h1:WB265cn5OpO+hK3pikC9hpP1zI/KTwmyMFKloW9eOVc=
|
||||
gopkg.in/validator.v2 v2.0.0-20180514200540-135c24b11c19/go.mod h1:o4V0GXN9/CAmCsvJ0oXYZvrZOe7syiDZSN1GWGZTGzc=
|
||||
gopkg.in/yaml.v2 v2.2.2 h1:ZCJp+EgiOT7lHqUV2J862kp8Qj64Jo6az82+3Td9dZw=
|
||||
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
122
handler.go
Normal file
122
handler.go
Normal file
|
@ -0,0 +1,122 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
log "github.com/sirupsen/logrus"
|
||||
|
||||
"github.com/Luzifer/lounge-control/sioclient"
|
||||
)
|
||||
|
||||
func addGenericHandler(f typedHandlerFunc) handlerFunc {
|
||||
return func(msg *sioclient.Message) error {
|
||||
if msg.Type != sioclient.MessageTypeEvent {
|
||||
// We don't care about anything but events
|
||||
return nil
|
||||
}
|
||||
|
||||
pType, err := msg.PayloadType()
|
||||
if err != nil {
|
||||
log.Printf("Event message had no payload type: %#v - %s", msg, err)
|
||||
return nil
|
||||
}
|
||||
|
||||
switch pType {
|
||||
|
||||
case "auth:failed":
|
||||
log.Fatal("Login failed")
|
||||
|
||||
case "auth:start":
|
||||
msg, err := sioclient.NewMessage(sioclient.MessageTypeEvent, 0, "auth:perform", map[string]string{"user": cfg.Username, "password": cfg.Password})
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "Unable to create auth:peform")
|
||||
}
|
||||
|
||||
if err := msg.Send(client); err != nil {
|
||||
return errors.Wrap(err, "Unable to create payload")
|
||||
}
|
||||
|
||||
case "init":
|
||||
if err := json.Unmarshal(msg.Payload[1], &initData); err != nil {
|
||||
return errors.Wrap(err, "Unable to parse init payload")
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
return f(pType, msg)
|
||||
}
|
||||
}
|
||||
|
||||
// DEPRECATED: Only storing code for now
|
||||
func handleMessage(msg *sioclient.Message) error {
|
||||
if msg.Type != sioclient.MessageTypeEvent {
|
||||
// We don't care about anything but events
|
||||
return nil
|
||||
}
|
||||
|
||||
pType, err := msg.PayloadType()
|
||||
if err != nil {
|
||||
log.Printf("Event message had no payload type: %#v - %s", msg, err)
|
||||
return nil
|
||||
}
|
||||
|
||||
switch pType {
|
||||
|
||||
case "auth:failed":
|
||||
log.Fatal("Login failed")
|
||||
|
||||
case "auth:start":
|
||||
msg, err := sioclient.NewMessage(sioclient.MessageTypeEvent, 0, "auth:perform", map[string]string{"user": cfg.Username, "password": cfg.Password})
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "Unable to create auth:peform")
|
||||
}
|
||||
|
||||
if err := msg.Send(client); err != nil {
|
||||
return errors.Wrap(err, "Unable to create payload")
|
||||
}
|
||||
|
||||
case "auth:success":
|
||||
log.Info("Logged in successfully")
|
||||
|
||||
case "init":
|
||||
if err := json.Unmarshal(msg.Payload[1], &initData); err != nil {
|
||||
return errors.Wrap(err, "Unable to parse init payload")
|
||||
}
|
||||
|
||||
case "msg":
|
||||
// Message in channel
|
||||
var payload chatMessage
|
||||
if err := json.Unmarshal(msg.Payload[1], &payload); err != nil {
|
||||
return errors.Wrap(err, "Unable to parse init payload")
|
||||
}
|
||||
|
||||
switch payload.Msg.Type {
|
||||
|
||||
case "join", "part":
|
||||
// Don't care
|
||||
|
||||
case "message", "notice":
|
||||
//log.Infof("Msg: %#v", payload)
|
||||
|
||||
case "unhandled":
|
||||
log.Infof("CMD: %s", msg.Payload[1])
|
||||
|
||||
default:
|
||||
log.Infof("Unhandled message %q: %#v", payload.Msg.Type, payload)
|
||||
|
||||
}
|
||||
|
||||
case "commands", "configuration", "names", "open", "push:issubscribed", "users":
|
||||
// Drop irrelevantt messages
|
||||
|
||||
default:
|
||||
if len(msg.Payload) == 2 {
|
||||
log.Warnf("Recieved unhandled message %q: %s", pType, msg.Payload[1])
|
||||
return nil
|
||||
}
|
||||
log.Warnf("Recieved unhandled message %q: %#v", pType, msg)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
87
main.go
Normal file
87
main.go
Normal file
|
@ -0,0 +1,87 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"os/signal"
|
||||
"strings"
|
||||
|
||||
log "github.com/sirupsen/logrus"
|
||||
|
||||
"github.com/Luzifer/lounge-control/sioclient"
|
||||
"github.com/Luzifer/rconfig/v2"
|
||||
)
|
||||
|
||||
var (
|
||||
cfg = struct {
|
||||
LogLevel string `flag:"log-level" default:"info" description:"Log level (debug, info, warn, error, fatal)"`
|
||||
Network string `flag:"network,n" description:"Name or UUID of the network to act on"`
|
||||
Password string `flag:"password,p" description:"Password for the given username" validate:"nonzero"`
|
||||
SocketURL string `flag:"socket-url" description:"URL to TheLounge websocket (i.e. 'wss://example.com/socket.io/')" validate:"nonzero"`
|
||||
Username string `flag:"username,u" description:"Username to log into the socket" validate:"nonzero"`
|
||||
VersionAndExit bool `flag:"version" default:"false" description:"Prints current version and exits"`
|
||||
}{}
|
||||
|
||||
client *sioclient.Client
|
||||
initData initMessage
|
||||
interrupt = make(chan os.Signal, 1)
|
||||
|
||||
version = "dev"
|
||||
)
|
||||
|
||||
func init() {
|
||||
rconfig.AutoEnv(true)
|
||||
if err := rconfig.ParseAndValidate(&cfg); err != nil {
|
||||
log.Fatalf("Unable to parse commandline options: %s", err)
|
||||
}
|
||||
|
||||
if cfg.VersionAndExit {
|
||||
fmt.Printf("lounge-control %s\n", version)
|
||||
os.Exit(0)
|
||||
}
|
||||
|
||||
if l, err := log.ParseLevel(cfg.LogLevel); err != nil {
|
||||
log.WithError(err).Fatal("Unable to parse log level")
|
||||
} else {
|
||||
log.SetLevel(l)
|
||||
}
|
||||
}
|
||||
|
||||
func main() {
|
||||
signal.Notify(interrupt, os.Interrupt)
|
||||
|
||||
args := rconfig.Args()[1:]
|
||||
if len(args) == 0 {
|
||||
log.Fatalf("No command given. Available commands: %s", strings.Join(availableCommands(), ", "))
|
||||
}
|
||||
|
||||
commandsMutex.RLock()
|
||||
cf, ok := commands[args[0]]
|
||||
commandsMutex.RUnlock()
|
||||
if !ok {
|
||||
log.Fatalf("Unknown command %q. Available commands: %s", args[0], strings.Join(availableCommands(), ", "))
|
||||
}
|
||||
|
||||
var err error
|
||||
client, err = sioclient.New(sioclient.Config{
|
||||
MessageHandler: cf(args[1:]),
|
||||
URL: cfg.SocketURL,
|
||||
})
|
||||
if err != nil {
|
||||
log.WithError(err).Fatal("Unable to connect to server")
|
||||
}
|
||||
defer client.Close()
|
||||
|
||||
for {
|
||||
select {
|
||||
|
||||
case <-interrupt:
|
||||
return
|
||||
|
||||
case err := <-client.EIO.Errors():
|
||||
log.WithError(err).Error("Error in in command / socket")
|
||||
return
|
||||
|
||||
}
|
||||
}
|
||||
}
|
73
messages.go
Normal file
73
messages.go
Normal file
|
@ -0,0 +1,73 @@
|
|||
package main
|
||||
|
||||
import "time"
|
||||
|
||||
type chatMessageContent struct {
|
||||
Command string `json:"command"`
|
||||
From struct {
|
||||
Mode string `json:"mode"`
|
||||
Nick string `json:"nick"`
|
||||
} `json:"from"`
|
||||
Highlight bool `json:"highlight"`
|
||||
ID int `json:"id"`
|
||||
Params []string `json:"params"`
|
||||
Previews []interface{} `json:"previews"`
|
||||
Self bool `json:"self"`
|
||||
Text string `json:"text"`
|
||||
Time time.Time `json:"time"`
|
||||
Type string `json:"type"`
|
||||
Users []string `json:"users"`
|
||||
}
|
||||
|
||||
type chatMessage struct {
|
||||
Chan int `json:"chan"`
|
||||
Msg chatMessageContent `json:"msg"`
|
||||
Unread int `json:"unread"`
|
||||
}
|
||||
|
||||
type channel struct {
|
||||
Name string `json:"name"`
|
||||
Type string `json:"type"`
|
||||
ID int `json:"id"`
|
||||
Messages []chatMessageContent `json:"messages"`
|
||||
TotalMessages int `json:"totalMessages"`
|
||||
Key string `json:"key"`
|
||||
Topic string `json:"topic"`
|
||||
State int `json:"state"`
|
||||
FirstUnread int `json:"firstUnread"`
|
||||
Unread int `json:"unread"`
|
||||
Highlight int `json:"highlight"`
|
||||
Users []string `json:"users"`
|
||||
}
|
||||
|
||||
type network struct {
|
||||
UUID string `json:"uuid"`
|
||||
Name string `json:"name"`
|
||||
Nick string `json:"nick"`
|
||||
Channels []channel `json:"channels"`
|
||||
ServerOptions struct {
|
||||
CHANTYPES []string `json:"CHANTYPES"`
|
||||
PREFIX []string `json:"PREFIX"`
|
||||
NETWORK string `json:"NETWORK"`
|
||||
} `json:"serverOptions"`
|
||||
Status struct {
|
||||
Connected bool `json:"connected"`
|
||||
Secure bool `json:"secure"`
|
||||
} `json:"status"`
|
||||
}
|
||||
|
||||
type initMessage struct {
|
||||
Active int `json:"active"`
|
||||
Networks []network `json:"networks"`
|
||||
Token string `json:"token"`
|
||||
}
|
||||
|
||||
func (i initMessage) NetworkByNameOrUUID(id string) *network {
|
||||
for _, n := range i.Networks {
|
||||
if n.Name == id || n.UUID == id {
|
||||
return &n
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
194
sioclient/eio.go
Normal file
194
sioclient/eio.go
Normal file
|
@ -0,0 +1,194 @@
|
|||
package sioclient
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"strconv"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/gorilla/websocket"
|
||||
"github.com/pkg/errors"
|
||||
)
|
||||
|
||||
type EIOMessageType int
|
||||
|
||||
const (
|
||||
EIOMessageTypeOpen EIOMessageType = iota
|
||||
EIOMessageTypeClose
|
||||
EIOMessageTypePing
|
||||
EIOMessageTypePong
|
||||
EIOMessageTypeMessage
|
||||
EIOMessageTypeUpgrade
|
||||
EIOMessageTypeNoop
|
||||
)
|
||||
|
||||
var ErrNotConnected = errors.New("Websocket is not connected")
|
||||
|
||||
type EIOClientConfig struct {
|
||||
MessageHandlerBinary func([]byte) error
|
||||
MessageHandlerText func([]byte) error
|
||||
URL string
|
||||
}
|
||||
|
||||
type eioSessionStart struct {
|
||||
SID string `json:"sid"`
|
||||
Upgrades []string `json:"upgrades"`
|
||||
PingTimeout int64 `json:"pingTimeout"`
|
||||
PingInterval int64 `json:"pingInterval"`
|
||||
}
|
||||
|
||||
type EIOClient struct {
|
||||
cfg EIOClientConfig
|
||||
dialer *websocket.Dialer
|
||||
errC chan error
|
||||
isConnected bool
|
||||
writeMutex *sync.Mutex
|
||||
ws *websocket.Conn
|
||||
}
|
||||
|
||||
func NewEIOClient(config EIOClientConfig) (*EIOClient, error) {
|
||||
var (
|
||||
client = new(EIOClient)
|
||||
err error
|
||||
)
|
||||
|
||||
socketURL, err := url.Parse(config.URL)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "Unable to parse URL")
|
||||
}
|
||||
|
||||
qVars := socketURL.Query()
|
||||
qVars.Set("EIO", "3")
|
||||
qVars.Set("transport", "websocket")
|
||||
|
||||
socketURL.RawQuery = qVars.Encode()
|
||||
|
||||
client.cfg = config
|
||||
client.errC = make(chan error, 10)
|
||||
client.writeMutex = new(sync.Mutex)
|
||||
client.ws, _, err = client.dialer.Dial(socketURL.String(), http.Header{})
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "Unable to dial to given URL")
|
||||
}
|
||||
|
||||
// Mark connection as established
|
||||
client.isConnected = true
|
||||
|
||||
defaultCloseHandler := client.ws.CloseHandler()
|
||||
client.ws.SetCloseHandler(func(code int, text string) error {
|
||||
client.isConnected = false
|
||||
return defaultCloseHandler(code, text)
|
||||
})
|
||||
|
||||
go func() {
|
||||
for client.isConnected {
|
||||
messageType, message, err := client.ws.ReadMessage()
|
||||
if err != nil {
|
||||
client.errC <- err
|
||||
continue
|
||||
}
|
||||
|
||||
if err = client.handleMessage(messageType, message); err != nil {
|
||||
client.errC <- err
|
||||
continue
|
||||
}
|
||||
}
|
||||
}()
|
||||
|
||||
return client, nil
|
||||
}
|
||||
|
||||
func (e EIOClient) Close() error { return e.ws.Close() }
|
||||
|
||||
func (e EIOClient) Errors() <-chan error { return e.errC }
|
||||
|
||||
func (e EIOClient) SendTextMessage(t EIOMessageType, data string) error {
|
||||
if !e.isConnected {
|
||||
return ErrNotConnected
|
||||
}
|
||||
|
||||
e.writeMutex.Lock()
|
||||
defer e.writeMutex.Unlock()
|
||||
|
||||
return errors.Wrap(
|
||||
e.ws.WriteMessage(websocket.TextMessage, []byte(fmt.Sprintf("%d%s", t, data))),
|
||||
"Unable to transmit message",
|
||||
)
|
||||
}
|
||||
|
||||
func (e EIOClient) handleMessage(messageType int, message []byte) error {
|
||||
if len(message) < 1 {
|
||||
return errors.New("Empty message received")
|
||||
}
|
||||
|
||||
var mType EIOMessageType
|
||||
|
||||
switch messageType {
|
||||
|
||||
case websocket.TextMessage:
|
||||
v, err := strconv.Atoi(string(message[0]))
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "Unable to parse message type")
|
||||
}
|
||||
mType = EIOMessageType(v)
|
||||
|
||||
case websocket.BinaryMessage:
|
||||
mType = EIOMessageType(message[0])
|
||||
|
||||
}
|
||||
|
||||
switch mType {
|
||||
|
||||
case EIOMessageTypeOpen:
|
||||
var handshake eioSessionStart
|
||||
|
||||
if err := json.Unmarshal(message[1:], &handshake); err != nil {
|
||||
return errors.Wrap(err, "Unable to unmarshal handshake")
|
||||
}
|
||||
|
||||
// Start pinger
|
||||
go func() {
|
||||
for t := time.NewTicker(time.Duration(handshake.PingInterval) * time.Millisecond); e.isConnected; <-t.C {
|
||||
e.SendTextMessage(EIOMessageTypePing, "")
|
||||
}
|
||||
}()
|
||||
|
||||
case EIOMessageTypeClose:
|
||||
e.ws.Close()
|
||||
|
||||
case EIOMessageTypePing:
|
||||
e.SendTextMessage(EIOMessageTypePong, "")
|
||||
|
||||
case EIOMessageTypePong:
|
||||
// Ignore
|
||||
|
||||
case EIOMessageTypeMessage:
|
||||
var hdl func([]byte) error
|
||||
|
||||
switch messageType {
|
||||
case websocket.TextMessage:
|
||||
hdl = e.cfg.MessageHandlerText
|
||||
case websocket.BinaryMessage:
|
||||
hdl = e.cfg.MessageHandlerBinary
|
||||
}
|
||||
|
||||
if err := hdl(message[1:]); err != nil {
|
||||
return errors.Wrap(err, "Failed to handle message")
|
||||
}
|
||||
|
||||
case EIOMessageTypeUpgrade:
|
||||
// Ignore?
|
||||
|
||||
case EIOMessageTypeNoop:
|
||||
// Noop!
|
||||
|
||||
default:
|
||||
return errors.Errorf("Received unknown EIO message type %d", mType)
|
||||
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
8
sioclient/go.mod
Normal file
8
sioclient/go.mod
Normal file
|
@ -0,0 +1,8 @@
|
|||
module github.com/Luzifer/lounge-control/sioclient
|
||||
|
||||
go 1.14
|
||||
|
||||
require (
|
||||
github.com/gorilla/websocket v1.4.2
|
||||
github.com/pkg/errors v0.9.1
|
||||
)
|
4
sioclient/go.sum
Normal file
4
sioclient/go.sum
Normal file
|
@ -0,0 +1,4 @@
|
|||
github.com/gorilla/websocket v1.4.2 h1:+/TMaTYc4QFitKJxsQ7Yye35DkWvkdLcvGKqM+x0Ufc=
|
||||
github.com/gorilla/websocket v1.4.2/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
|
||||
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
|
||||
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
181
sioclient/sio.go
Normal file
181
sioclient/sio.go
Normal file
|
@ -0,0 +1,181 @@
|
|||
package sioclient
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"strconv"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
)
|
||||
|
||||
type MessageType int
|
||||
|
||||
const (
|
||||
MessageTypeConnect MessageType = iota
|
||||
MessageTypeDisconnect
|
||||
MessageTypeEvent
|
||||
MessageTypeAck
|
||||
MessageTypeError
|
||||
MessageTypeBinaryEvent
|
||||
MessageTypeBinaryAck
|
||||
)
|
||||
|
||||
type Config struct {
|
||||
MessageHandler func(*Message) error
|
||||
URL string
|
||||
}
|
||||
|
||||
type Client struct {
|
||||
EIO *EIOClient
|
||||
|
||||
cfg Config
|
||||
}
|
||||
|
||||
func New(c Config) (*Client, error) {
|
||||
var (
|
||||
client = new(Client)
|
||||
err error
|
||||
)
|
||||
|
||||
client.cfg = c
|
||||
|
||||
if client.EIO, err = NewEIOClient(EIOClientConfig{
|
||||
MessageHandlerText: client.handleTextMessage,
|
||||
URL: c.URL,
|
||||
}); err != nil {
|
||||
return nil, errors.Wrap(err, "Unable to create EIO client")
|
||||
}
|
||||
|
||||
return client, nil
|
||||
}
|
||||
|
||||
func (c Client) Close() error {
|
||||
return c.EIO.Close()
|
||||
}
|
||||
|
||||
func (c Client) handleTextMessage(msg []byte) error {
|
||||
m, err := c.parseProto(msg)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "Unable to parse message")
|
||||
}
|
||||
|
||||
return c.cfg.MessageHandler(m)
|
||||
}
|
||||
|
||||
type Message struct {
|
||||
Type MessageType
|
||||
Namespace string
|
||||
ID int
|
||||
Payload []json.RawMessage
|
||||
}
|
||||
|
||||
func NewMessage(sType MessageType, id int, payloadType string, data interface{}) (*Message, error) {
|
||||
out := &Message{
|
||||
Type: sType,
|
||||
Namespace: "/",
|
||||
ID: id,
|
||||
Payload: make([]json.RawMessage, 2),
|
||||
}
|
||||
|
||||
var err error
|
||||
|
||||
if out.Payload[0], err = json.Marshal(payloadType); err != nil {
|
||||
return nil, errors.Wrap(err, "Unable to marshal payloadType")
|
||||
}
|
||||
|
||||
if out.Payload[1], err = json.Marshal(data); err != nil {
|
||||
return nil, errors.Wrap(err, "Unable to marshal data")
|
||||
}
|
||||
|
||||
return out, nil
|
||||
}
|
||||
|
||||
func (m Message) Encode() (string, error) {
|
||||
data, err := json.Marshal(m.Payload)
|
||||
if err != nil {
|
||||
return "", errors.Wrap(err, "Unable to marshal payload")
|
||||
}
|
||||
|
||||
var msg = new(bytes.Buffer)
|
||||
msg.WriteString(strconv.Itoa(int(m.Type)))
|
||||
|
||||
if m.Namespace != "" && m.Namespace != "/" {
|
||||
msg.WriteString(m.Namespace + ",")
|
||||
}
|
||||
|
||||
if m.ID > 0 {
|
||||
msg.WriteString(strconv.Itoa(m.ID))
|
||||
}
|
||||
|
||||
msg.Write(data)
|
||||
|
||||
return msg.String(), nil
|
||||
}
|
||||
|
||||
func (m Message) PayloadType() (string, error) {
|
||||
if len(m.Payload) == 0 {
|
||||
return "", errors.New("No payload type available")
|
||||
}
|
||||
|
||||
var t string
|
||||
err := json.Unmarshal(m.Payload[0], &t)
|
||||
return t, errors.Wrap(err, "Unable to unmarshal payload type")
|
||||
}
|
||||
|
||||
func (m Message) Send(c *Client) error {
|
||||
raw, err := m.Encode()
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "Unable to encode message")
|
||||
}
|
||||
|
||||
return c.EIO.SendTextMessage(EIOMessageTypeMessage, raw)
|
||||
}
|
||||
|
||||
func (m Message) UnmarshalPayload(out interface{}) error { return json.Unmarshal(m.Payload[1], out) }
|
||||
|
||||
func (c Client) parseProto(msg []byte) (*Message, error) {
|
||||
var (
|
||||
err error
|
||||
outMsg = new(Message)
|
||||
ptr int
|
||||
)
|
||||
|
||||
if len(msg) < 1 {
|
||||
return nil, errors.New("Message was empty")
|
||||
}
|
||||
|
||||
// Get message type
|
||||
mType, err := strconv.Atoi(string(msg[ptr]))
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "Unable to parse message type")
|
||||
}
|
||||
outMsg.Type = MessageType(mType)
|
||||
ptr++
|
||||
|
||||
// Message contains only message type
|
||||
if len(msg[ptr:]) == 0 {
|
||||
return outMsg, nil
|
||||
}
|
||||
|
||||
// Binary
|
||||
if outMsg.Type == MessageTypeBinaryEvent || outMsg.Type == MessageTypeBinaryAck {
|
||||
return nil, errors.New("Binary is not supported")
|
||||
}
|
||||
|
||||
// Check for namespace
|
||||
if msg[ptr] == '/' {
|
||||
return nil, errors.New("Namespaces is not supported")
|
||||
}
|
||||
|
||||
// Read message ID if any
|
||||
if outMsg.ID, err = strconv.Atoi(string(msg[ptr])); err == nil {
|
||||
ptr++
|
||||
}
|
||||
|
||||
// If there is no more data we have an empty message
|
||||
if len(msg[ptr:]) == 0 || outMsg.Type == MessageTypeConnect {
|
||||
return outMsg, nil
|
||||
}
|
||||
|
||||
return outMsg, errors.Wrap(json.Unmarshal(msg[ptr:], &outMsg.Payload), "Unable to unmarshal message")
|
||||
}
|
Loading…
Reference in a new issue