mirror of
https://github.com/Luzifer/lounge-control.git
synced 2024-12-22 22:41:16 +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