├── .gitignore ├── .travis.yml ├── Dockerfile ├── LICENSE ├── README.md ├── bot.go ├── client.go ├── conn.go ├── go.mod ├── go.sum ├── main.go ├── main_test.go ├── server.go └── twitch.go /.gitignore: -------------------------------------------------------------------------------- 1 | # Ignore binary files 2 | relaybroker 3 | *.exe 4 | *.exe~ 5 | config.json 6 | config.go 7 | vendor/ 8 | glide.lock 9 | .idea/ 10 | coverage-all.out 11 | coverage.out -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: go 2 | go: 3 | - 1.6.3 4 | - 1.7.1 5 | sudo: required 6 | services: 7 | - docker 8 | 9 | script: 10 | - go get -t -v ./... 11 | - go test -v -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM golang:latest 2 | WORKDIR /go/src/github.com/gempir/relaybroker 3 | RUN go get github.com/op/go-logging \ 4 | && go get github.com/stretchr/testify/assert 5 | COPY . . 6 | RUN CGO_ENABLED=0 GOOS=linux go build -a -installsuffix cgo -o app . 7 | 8 | FROM alpine:latest 9 | RUN apk --no-cache add ca-certificates 10 | WORKDIR /root/ 11 | COPY --from=0 /go/src/github.com/gempir/relaybroker/app . 12 | CMD ["./app"] 13 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | Copyright (c) 2016 gempir, pajlada, nuuls 3 | 4 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 5 | 6 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 7 | 8 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 9 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # relaybroker [![Build Status](https://travis-ci.org/gempir/relaybroker.svg?branch=master)](https://travis-ci.org/gempir/relaybroker) 2 | 3 | #### What is this? 4 | relaybroker is a piece of software that is supposed to act as a proxy between your bot and twitch.tv irc servers. 5 | It will handle ratelimiting so you don't have to worry about getting global banned or having connection issues. 6 | 7 | #### How to use 8 | Loglevel can be changed via env var "LOGLEVEL". The options are debug, info and error. Info is default. 9 | 10 | The ip/port relaybroker listens to can be changed via the env var "BROKERHOST". The default is "127.0.0.1:3333" which means it listens to port 3333 on local 11 | 12 | ### Docker 13 | 14 | #### Warning this is old and not updated currently 15 | 16 | Run relaybroker as a docker container like this: 17 | 18 | docker run -p 8080:3333 -e BROKERPASS="mypassword" gempir/relaybroker -------------------------------------------------------------------------------- /bot.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "strings" 5 | "sync" 6 | "time" 7 | ) 8 | 9 | type bot struct { 10 | sync.Mutex 11 | ID string 12 | pass string 13 | nick string 14 | read chan string 15 | toClient chan string 16 | join chan string 17 | channels map[string][]*connection 18 | readconns []*connection 19 | sendconns []*connection 20 | whisperconn *connection 21 | ticker *time.Ticker 22 | clientConnected bool 23 | client *Client 24 | } 25 | 26 | func newBot(client *Client) *bot { 27 | return &bot{ 28 | read: make(chan string, 10), 29 | join: make(chan string, 50000), 30 | channels: make(map[string][]*connection), 31 | readconns: make([]*connection, 0), 32 | sendconns: make([]*connection, 0), 33 | ticker: time.NewTicker(1 * time.Minute), 34 | client: client, 35 | } 36 | } 37 | 38 | func (bot *bot) Init() { 39 | go bot.joinChannels() 40 | go bot.checkConnections() 41 | bot.newConn(connReadConn) 42 | // twitch changed something about whispers or there is some black magic going on, 43 | // but its only reading whispers once even with more connections 44 | bot.newConn(connWhisperConn) 45 | } 46 | 47 | // close all connections and delete bot 48 | func (bot *bot) close() { 49 | bot.Lock() 50 | bot.ticker.Stop() 51 | close(bot.read) 52 | close(bot.join) 53 | for _, conn := range bot.readconns { 54 | conn.conntype = connDelete 55 | conn.close() 56 | } 57 | for _, conn := range bot.sendconns { 58 | conn.conntype = connDelete 59 | conn.close() 60 | } 61 | bot.whisperconn.conntype = connDelete 62 | bot.whisperconn.close() 63 | for k := range bot.channels { 64 | delete(bot.channels, k) 65 | } 66 | Log.Info("CLOSED BOT", bot.nick) 67 | bot.Unlock() 68 | } 69 | 70 | func (bot *bot) checkConnections() { 71 | for _ = range bot.ticker.C { 72 | for _, co := range bot.readconns { 73 | conn := co 74 | conn.active = false 75 | err := conn.send("PING") 76 | if err != nil { 77 | Log.Error(err.Error()) 78 | conn.restore() 79 | conn.close() 80 | } 81 | go func() { 82 | time.Sleep(10 * time.Second) 83 | if !conn.active { 84 | Log.Info("read connection died, reconnecting...") 85 | conn.restore() 86 | conn.close() 87 | } 88 | }() 89 | } 90 | for _, co := range bot.sendconns { 91 | conn := co 92 | if time.Since(conn.lastUse) < time.Minute*10 { 93 | // close unused connections 94 | conn.active = false 95 | err := conn.send("PING") 96 | if err != nil { 97 | Log.Error(err.Error()) 98 | conn.restore() 99 | conn.close() 100 | } else { 101 | go func() { 102 | time.Sleep(10 * time.Second) 103 | if !conn.active { 104 | Log.Info("send connection died, closing...") 105 | conn.restore() 106 | conn.close() 107 | } 108 | }() 109 | } 110 | } else { 111 | if len(bot.sendconns) > 2 { 112 | Log.Info("closing unused connection") 113 | conn.restore() 114 | conn.close() 115 | } else { 116 | conn.lastUse = time.Now() 117 | } 118 | } 119 | } 120 | go func() { 121 | err := bot.whisperconn.send("PING") 122 | if err != nil { 123 | Log.Error(err.Error()) 124 | bot.whisperconn.restore() 125 | } 126 | time.Sleep(10 * time.Second) 127 | if !bot.whisperconn.active { 128 | bot.newConn(connWhisperConn) 129 | } 130 | }() 131 | for channel := range bot.channels { 132 | if conns, ok := bot.channels[channel]; ok && len(conns) < 1 { 133 | bot.join <- channel 134 | } 135 | } 136 | } 137 | } 138 | 139 | func (bot *bot) partChannel(channel string) { 140 | channel = strings.ToLower(channel) 141 | if conns, ok := bot.channels[channel]; ok { 142 | for _, conn := range conns { 143 | err := conn.send("PART " + channel) 144 | if err != nil { 145 | Log.Error(err.Error()) 146 | conn.restore() 147 | conn.close() 148 | bot.partChannel(channel) 149 | return 150 | } 151 | conn.part(channel) 152 | } 153 | Log.Infof("left channel on %d connections\n", len(conns)) 154 | delete(bot.channels, channel) 155 | return 156 | } 157 | Log.Error("never joined ", channel) 158 | } 159 | 160 | func (bot *bot) joinChannels() { 161 | for channel := range bot.join { 162 | Log.Debug(channel) 163 | bot.joinChannel(channel) 164 | <-joinTicker.C 165 | } 166 | } 167 | 168 | func (bot *bot) joinChannel(channel string) { 169 | channel = strings.ToLower(channel) 170 | if conns, ok := bot.channels[channel]; ok && len(conns) > 0 { 171 | // TODO: check msg ids and join channels more than one time 172 | Log.Info("already joined channel", channel) 173 | return 174 | } 175 | var conn *connection 176 | for _, c := range bot.readconns { 177 | if len(c.joins) < 50 { 178 | conn = c 179 | break 180 | } 181 | } 182 | if conn == nil { 183 | bot.newConn(connReadConn) 184 | bot.joinChannel(channel) 185 | return 186 | } 187 | for !conn.active { 188 | time.Sleep(100 * time.Millisecond) 189 | } 190 | err := conn.send("JOIN " + channel) 191 | if err != nil { 192 | Log.Error(err.Error()) 193 | conn.restore() 194 | conn.close() 195 | bot.join <- channel 196 | return 197 | } 198 | if _, ok := bot.channels[channel]; !ok { 199 | bot.channels[channel] = make([]*connection, 0) 200 | } 201 | conn.joins = append(conn.joins, channel) 202 | bot.channels[channel] = append(bot.channels[channel], conn) 203 | Log.Info("joined channel", channel) 204 | } 205 | 206 | func (bot *bot) newConn(t connType) { 207 | whisperConns := 0 208 | var conn *connection 209 | switch t { 210 | case connReadConn: 211 | conn = newConnection(t) 212 | go conn.connect(bot.client, bot.pass, bot.nick) 213 | bot.readconns = append(bot.readconns, conn) 214 | case connSendConn: 215 | conn = newConnection(t) 216 | go conn.connect(bot.client, bot.pass, bot.nick) 217 | bot.sendconns = append(bot.sendconns, conn) 218 | case connWhisperConn: 219 | if bot.whisperconn != nil { 220 | bot.whisperconn.close() 221 | } 222 | conn = newConnection(t) 223 | go conn.connect(bot.client, bot.pass, bot.nick) 224 | bot.whisperconn = conn 225 | } 226 | if bot.whisperconn != nil { 227 | whisperConns = 1 228 | } 229 | Log.Infof("NEW %s, TOTAL: %d", conn.name, len(bot.readconns)+len(bot.sendconns)+whisperConns) 230 | } 231 | 232 | func (bot *bot) readChat() { 233 | for msg := range bot.toClient { 234 | bot.read <- msg 235 | } 236 | } 237 | 238 | func (bot *bot) say(msg string) { 239 | var conn *connection 240 | var min = 15 241 | // find connection with the least sent messages 242 | for _, c := range bot.sendconns { 243 | if c.msgCount < min { 244 | conn = c 245 | min = conn.msgCount 246 | } 247 | } 248 | if conn == nil || min > 10 { 249 | bot.newConn(connSendConn) 250 | Log.Infof("created new conn, total: %d\n", len(bot.sendconns)) 251 | bot.say(msg) 252 | return 253 | } 254 | // add to msg counter before waiting to stop other go routines from sending on this connection 255 | conn.countMsg() 256 | for !conn.active { 257 | time.Sleep(100 * time.Millisecond) 258 | } 259 | conn.lastUse = time.Now() 260 | err := conn.send("PRIVMSG " + msg) 261 | if err != nil { 262 | Log.Error(err.Error()) 263 | conn.restore() 264 | conn.close() 265 | bot.say(msg) 266 | return 267 | } 268 | Log.Debugf("%p %d\n", conn, conn.msgCount) 269 | Log.Info("sent:", msg) 270 | } 271 | 272 | func (bot *bot) handleMessage(spl []string) { 273 | msg := spl[1] 274 | switch spl[0] { 275 | case "JOIN": 276 | bot.join <- strings.ToLower(msg) 277 | case "PART": 278 | bot.partChannel(strings.ToLower(msg)) 279 | case "PRIVMSG": 280 | bot.say(msg) 281 | default: 282 | Log.Warning("unhandled message", spl[0], msg) 283 | } 284 | } 285 | -------------------------------------------------------------------------------- /client.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "math/rand" 6 | "net" 7 | "strings" 8 | "time" 9 | ) 10 | 11 | // Client for connection to relaybroker 12 | type Client struct { 13 | ID string 14 | bot *bot 15 | incomingConn net.Conn 16 | fromClient chan string 17 | toClient chan string 18 | joinedChannels map[string]bool 19 | join chan string // TODO: this should be some kind of priority queue 20 | test []string 21 | } 22 | 23 | func newClient(conn net.Conn) Client { 24 | return Client{ 25 | incomingConn: conn, 26 | fromClient: make(chan string, 10), 27 | toClient: make(chan string, 10), 28 | join: make(chan string, 50000), 29 | test: make([]string, 0), 30 | joinedChannels: make(map[string]bool), 31 | } 32 | } 33 | 34 | func (c *Client) init() { 35 | go c.joinChannels() 36 | go c.read() 37 | go c.relaybrokerMeta() 38 | } 39 | 40 | func (c *Client) joinChannels() { 41 | for channelsJoinMessage := range c.join { 42 | c.bot.join <- channelsJoinMessage 43 | 44 | for _, channel := range strings.Split(channelsJoinMessage, ",") { 45 | c.joinedChannels[strings.TrimPrefix(channel, "#")] = true 46 | } 47 | } 48 | } 49 | 50 | func (c *Client) read() { 51 | // cha := make(chan string, 5) 52 | // go c.relaybrokerCommand(cha) 53 | for msg := range c.toClient { 54 | c.incomingConn.Write([]byte(msg + "\r\n")) 55 | //cha <- msg 56 | } 57 | //closeChannel(cha) 58 | } 59 | 60 | func (c *Client) relaybrokerMeta() { 61 | ticker := time.NewTicker(1 * time.Second) 62 | msg := "@badge-info=founder/47;badges=moderator/1,founder/0,premium/1;color=#00FF80;display-name=gempir;emotes=;flags=;id=28b511cc-43b3-44b7-a605-230aadbb2f9b;mod=1;room-id=11148817;subscriber=0;tmi-sent-ts=1576066088367;turbo=0;user-id=77829817;user-type=mod :gempir!gempir@gempir.tmi.twitch.tv PRIVMSG #pajlada :" 63 | 64 | for range ticker.C { 65 | c.incomingConn.Write([]byte(fmt.Sprintf("%s%d\r\n", msg, len(c.joinedChannels)))) 66 | } 67 | } 68 | 69 | func closeChannel(c chan string) { 70 | defer func() { 71 | if r := recover(); r != nil { 72 | Log.Error("error closing channel ") 73 | } 74 | }() 75 | close(c) 76 | } 77 | 78 | func (c *Client) close() { 79 | closeChannel(c.join) 80 | // keep bot running if he wants to reconnect 81 | if c.bot.ID != "" { 82 | // dont let the channel fill up and block 83 | for m := range c.toClient { 84 | if c.bot.clientConnected { 85 | bots[c.ID].toClient <- m 86 | return 87 | } 88 | Log.Debug("msg on dc bot") 89 | } 90 | } 91 | if c.bot.clientConnected { 92 | return 93 | } 94 | closeChannel(c.fromClient) 95 | closeChannel(c.toClient) 96 | c.bot.close() 97 | delete(bots, c.ID) 98 | Log.Debug("CLOSED CLIENT", c.bot.nick) 99 | 100 | } 101 | 102 | func (c *Client) handleMessage(line string) { 103 | Log.Info("[CLIENT] " + line) 104 | c.test = append(c.test, line) 105 | defer func() { 106 | if r := recover(); r != nil { 107 | Log.Error("message handling error") 108 | c.close() 109 | } 110 | }() 111 | spl := strings.SplitN(line, " ", 2) 112 | msg := spl[1] 113 | if c.bot == nil { 114 | if c.registerBot(spl[0], msg) { 115 | return 116 | } 117 | } 118 | // irc command 119 | switch spl[0] { 120 | case "PASS": 121 | c.bot.pass = msg 122 | case "NICK": 123 | c.bot.nick = strings.ToLower(msg) // make sure the nick is lowercase 124 | // start bot when we got all login info 125 | c.bot.Init() 126 | case "JOIN": 127 | c.join <- msg 128 | case "USER": 129 | default: 130 | go c.bot.handleMessage(spl) 131 | } 132 | } 133 | 134 | /* 135 | if first line from client == LOGIN, reconnect to old bot 136 | if its something else, create new bot 137 | return true on LOGIN, false on any other command so it can be processed further 138 | */ 139 | func (c *Client) registerBot(cmd string, msg string) bool { 140 | if cmd == "LOGIN" { 141 | if bot, ok := bots[msg]; ok { 142 | c.ID = msg 143 | c.bot = bot 144 | c.bot.client.toClient = c.toClient 145 | close(c.join) 146 | c.join = make(chan string, 50000) 147 | go c.joinChannels() 148 | c.bot.clientConnected = true 149 | Log.Debug("old bot reconnected", msg) 150 | return true 151 | } 152 | c.bot = newBot(c) 153 | c.ID = msg 154 | c.bot.ID = msg 155 | c.bot.clientConnected = true 156 | bots[msg] = c.bot 157 | return true 158 | } 159 | c.bot = newBot(c) 160 | // generate random ID 161 | if c.bot.ID == "" { 162 | rand.Seed(int64(time.Now().Nanosecond())) 163 | r := rand.Int31n(123456) 164 | ID := fmt.Sprintf("%s%d", c.bot.nick, r) 165 | bots[ID] = c.bot 166 | c.ID = ID 167 | } 168 | return false 169 | } 170 | -------------------------------------------------------------------------------- /conn.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "bufio" 5 | "crypto/tls" 6 | "errors" 7 | "fmt" 8 | "math/rand" 9 | "net" 10 | "net/textproto" 11 | "strings" 12 | "sync" 13 | "time" 14 | ) 15 | 16 | type connType uint32 17 | 18 | const ( 19 | connWhisperConn = iota 20 | connReadConn 21 | connSendConn 22 | connDelete 23 | ) 24 | 25 | type connection struct { 26 | sync.Mutex 27 | conn net.Conn 28 | active bool 29 | anon bool 30 | joins []string 31 | msgCount int 32 | lastUse time.Time 33 | alive bool 34 | conntype connType 35 | bot *bot 36 | name string 37 | } 38 | 39 | func newConnection(t connType) *connection { 40 | return &connection{ 41 | joins: make([]string, 0), 42 | conntype: t, 43 | lastUse: time.Now(), 44 | name: randomHash(), 45 | } 46 | } 47 | 48 | func (conn *connection) login(pass string, nick string) { 49 | conn.anon = pass == "" 50 | if !conn.anon { 51 | conn.send("PASS " + pass) 52 | conn.send("NICK " + nick) 53 | return 54 | } 55 | conn.send("NICK justinfan123") 56 | } 57 | 58 | func (conn *connection) close() { 59 | if conn.conn != nil { 60 | conn.conn.Close() 61 | } 62 | for _, channel := range conn.joins { 63 | conn.part(channel) 64 | } 65 | conn.alive = false 66 | } 67 | 68 | func (conn *connection) part(channel string) { 69 | channel = strings.ToLower(channel) 70 | for i, ch := range conn.joins { 71 | if ch == channel { 72 | conn.joins = append(conn.joins[:i], conn.joins[i+1:]...) 73 | } 74 | } 75 | } 76 | 77 | func (conn *connection) restore() { 78 | defer func() { 79 | if r := recover(); r != nil { 80 | Log.Error("cannot restore connection") 81 | } 82 | }() 83 | if conn.conntype == connReadConn { 84 | var i int 85 | var channels []string 86 | for index, co := range conn.bot.readconns { 87 | if conn == co { 88 | i = index 89 | channels = co.joins 90 | break 91 | } 92 | } 93 | Log.Error("readconn died, lost joins:", channels) 94 | conn.bot.Lock() 95 | conn.bot.readconns = append(conn.bot.readconns[:i], conn.bot.readconns[i+1:]...) 96 | conn.bot.Unlock() 97 | for _, channel := range channels { 98 | conns := conn.bot.channels[channel] 99 | for i, co := range conns { 100 | if conn == co { 101 | conn.bot.Lock() 102 | conn.bot.channels[channel] = append(conns[:i], conns[i+1:]...) 103 | conn.bot.Unlock() 104 | conn.part(channel) 105 | } 106 | } 107 | conn.bot.join <- channel 108 | 109 | } 110 | 111 | } else if conn.conntype == connSendConn { 112 | Log.Error("sendconn died") 113 | var i int 114 | for index, co := range conn.bot.sendconns { 115 | if conn == co { 116 | i = index 117 | break 118 | } 119 | } 120 | conn.bot.Lock() 121 | conn.bot.sendconns = append(conn.bot.sendconns[:i], conn.bot.sendconns[i+1:]...) 122 | conn.bot.Unlock() 123 | } else if conn.conntype == connWhisperConn { 124 | Log.Error("whisperconn died, reconnecting") 125 | conn.close() 126 | conn.bot.newConn(connWhisperConn) 127 | } 128 | conn.conntype = connDelete 129 | } 130 | 131 | func (conn *connection) connect(client *Client, pass string, nick string) { 132 | dialer := &net.Dialer{ 133 | KeepAlive: time.Second * 10, 134 | } 135 | 136 | conn.bot = client.bot 137 | c, err := tls.DialWithDialer(dialer, "tcp", *addr, &tls.Config{}) 138 | if err != nil { 139 | Log.Error("unable to connect to irc server", err) 140 | time.Sleep(2 * time.Second) 141 | conn.restore() 142 | return 143 | } 144 | conn.conn = c 145 | 146 | conn.send("CAP REQ :twitch.tv/tags twitch.tv/commands") 147 | conn.login(pass, nick) 148 | 149 | defer func() { 150 | if r := recover(); r != nil { 151 | Log.Error("error connecting") 152 | } 153 | conn.restore() 154 | }() 155 | tp := textproto.NewReader(bufio.NewReader(conn.conn)) 156 | 157 | for { 158 | line, err := tp.ReadLine() 159 | if err != nil { 160 | Log.Errorf("[READERROR:%s] %s", conn.name, err.Error()) 161 | conn.restore() 162 | return 163 | } 164 | Log.Debugf("[TWITCH:%s] %s", conn.name, line) 165 | if conn.conntype == connDelete { 166 | conn.restore() 167 | } 168 | if strings.HasPrefix(line, "PING") { 169 | conn.send(strings.Replace(line, "PING", "PONG", 1)) 170 | } else if strings.HasPrefix(line, "PONG") { 171 | Log.Debug("PONG") 172 | } else { 173 | if isWhisper(line) && conn.conntype != connWhisperConn { 174 | // throw away message 175 | } else { 176 | client.toClient <- line 177 | } 178 | } 179 | conn.active = true 180 | } 181 | } 182 | 183 | func isWhisper(line string) bool { 184 | if !strings.Contains(line, ".tmi.twitch.tv WHISPER ") { 185 | return false 186 | } 187 | spl := strings.SplitN(line, " :", 3) 188 | if strings.Contains(spl[1], ".tmi.twitch.tv WHISPER ") { 189 | return true 190 | } 191 | return false 192 | } 193 | 194 | func (conn *connection) send(msg string) error { 195 | if conn.conn == nil { 196 | Log.Error("conn is nil", conn, conn.conn) 197 | return errors.New("connection is nil") 198 | } 199 | _, err := fmt.Fprint(conn.conn, msg+"\r\n") 200 | if err != nil { 201 | Log.Error("error sending message") 202 | return err 203 | } 204 | Log.Debugf("[OUTGOING:%s] %s", conn.name, msg) 205 | return nil 206 | } 207 | 208 | func (conn *connection) reduceMsgCount() { 209 | conn.msgCount-- 210 | } 211 | 212 | func (conn *connection) countMsg() { 213 | conn.msgCount++ 214 | time.AfterFunc(30*time.Second, conn.reduceMsgCount) 215 | } 216 | 217 | func randomHash() string { 218 | n := 5 219 | b := make([]byte, n) 220 | if _, err := rand.Read(b); err != nil { 221 | panic(err) 222 | } 223 | return fmt.Sprintf("%X", b) 224 | } 225 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/gempir/relaybroker 2 | 3 | go 1.13 4 | 5 | require ( 6 | github.com/labstack/gommon v0.3.0 7 | github.com/op/go-logging v0.0.0-20160315200505-970db520ece7 8 | github.com/stretchr/testify v1.5.1 9 | ) 10 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8= 2 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 3 | github.com/labstack/gommon v0.3.0 h1:JEeO0bvc78PKdyHxloTKiF8BD5iGrH8T6MSeGvSgob0= 4 | github.com/labstack/gommon v0.3.0/go.mod h1:MULnywXg0yavhxWKc+lOruYdAhDwPK9wf0OL7NoOu+k= 5 | github.com/mattn/go-colorable v0.1.2 h1:/bC9yWikZXAL9uJdulbSfyVNIR3n3trXl+v8+1sx8mU= 6 | github.com/mattn/go-colorable v0.1.2/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE= 7 | github.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s= 8 | github.com/mattn/go-isatty v0.0.9 h1:d5US/mDsogSGW37IV293h//ZFaeajb69h+EHFsv2xGg= 9 | github.com/mattn/go-isatty v0.0.9/go.mod h1:YNRxwqDuOph6SZLI9vUUz6OYw3QyUt7WiY2yME+cCiQ= 10 | github.com/op/go-logging v0.0.0-20160315200505-970db520ece7 h1:lDH9UUVJtmYCjyT0CI4q8xvlXPxeZ0gYCVvWbmPlp88= 11 | github.com/op/go-logging v0.0.0-20160315200505-970db520ece7/go.mod h1:HzydrMdWErDVzsI23lYNej1Htcns9BCg93Dk0bBINWk= 12 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 13 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 14 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 15 | github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= 16 | github.com/stretchr/testify v1.5.1 h1:nOGnQDM7FYENwehXlg/kFVnos3rEvtKTjRvOWSzb6H4= 17 | github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA= 18 | github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw= 19 | github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc= 20 | github.com/valyala/fasttemplate v1.0.1 h1:tY9CJiPnMXf1ERmG2EyK7gNUd+c6RKGD0IfU8WdUSz8= 21 | github.com/valyala/fasttemplate v1.0.1/go.mod h1:UQGH1tvbgY+Nz5t2n7tXsz52dQxojPUpymEIMZ47gx8= 22 | golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 23 | golang.org/x/sys v0.0.0-20190813064441-fde4db37ae7a h1:aYOabOQFp6Vj6W1F80affTUvO9UxmJRx8K0gsfABByQ= 24 | golang.org/x/sys v0.0.0-20190813064441-fde4db37ae7a/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 25 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 26 | gopkg.in/yaml.v2 v2.2.2 h1:ZCJp+EgiOT7lHqUV2J862kp8Qj64Jo6az82+3Td9dZw= 27 | gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 28 | -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "os" 5 | "time" 6 | 7 | "github.com/op/go-logging" 8 | ) 9 | 10 | var ( 11 | // Log logger from go-logging 12 | Log logging.Logger 13 | brokerHost string 14 | logLevel logging.Level 15 | 16 | bots = make(map[string]*bot) 17 | 18 | // sync all bots joins since its ip based and not account based 19 | joinTicker = time.NewTicker(time.Millisecond) 20 | ) 21 | 22 | func main() { 23 | brokerHost = getEnv("BROKERHOST", "127.0.0.1:3333") 24 | logLevel = getLogLevel(getEnv("LOGLEVEL", "info")) 25 | 26 | Log = initLogger(logLevel) 27 | server := new(Server) 28 | server.startServer() 29 | } 30 | 31 | func getEnv(key, fallback string) string { 32 | if value, ok := os.LookupEnv(key); ok { 33 | return value 34 | } 35 | return fallback 36 | } 37 | 38 | func getLogLevel(level string) logging.Level { 39 | switch level { 40 | case "debug": 41 | return logging.DEBUG 42 | case "error": 43 | return logging.ERROR 44 | default: 45 | return logging.INFO 46 | } 47 | } 48 | 49 | func initLogger(level logging.Level) logging.Logger { 50 | var logger *logging.Logger 51 | logger = logging.MustGetLogger("relaybroker") 52 | logging.SetLevel(level, "relaybroker") 53 | backend := logging.NewLogBackend(os.Stdout, "", 0) 54 | 55 | format := logging.MustStringFormatter( 56 | `%{color}%{time:2006-01-02 15:04:05.000} %{level:.4s} %{shortfile}%{color:reset} %{message}`, 57 | ) 58 | logging.SetFormatter(format) 59 | backendLeveled := logging.AddModuleLevel(backend) 60 | backendLeveled.SetLevel(level, "relaybroker") 61 | logging.SetBackend(backendLeveled) 62 | return *logger 63 | } 64 | -------------------------------------------------------------------------------- /main_test.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/op/go-logging" 7 | "github.com/stretchr/testify/assert" 8 | ) 9 | 10 | func TestCanInitLogger(t *testing.T) { 11 | log := initLogger(0) 12 | assert.IsType(t, logging.Logger{}, log) 13 | } 14 | -------------------------------------------------------------------------------- /server.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "bufio" 5 | "net" 6 | "net/textproto" 7 | "os" 8 | ) 9 | 10 | // Server who handles incoming messages to relaybroker from a client 11 | type Server struct { 12 | ln net.Listener 13 | conn net.Conn 14 | } 15 | 16 | func (s *Server) startServer() { 17 | ln, err := net.Listen("tcp", brokerHost) 18 | if err != nil { 19 | Log.Fatal("tcp server not starting", err) 20 | } 21 | defer ln.Close() 22 | Log.Info("started listening on", brokerHost) 23 | for { 24 | conn, err := ln.Accept() 25 | if err != nil { 26 | Log.Error(err.Error()) 27 | os.Exit(1) 28 | } 29 | go s.handleClient(newClient(conn)) 30 | } 31 | } 32 | 33 | func (s *Server) stopServer() { 34 | s.ln.Close() 35 | } 36 | 37 | func (s *Server) handleClient(c Client) { 38 | Log.Info("new client: " + c.incomingConn.RemoteAddr().String()) 39 | r := bufio.NewReader(c.incomingConn) 40 | tp := textproto.NewReader(r) 41 | c.init() 42 | 43 | for { 44 | line, err := tp.ReadLine() 45 | if err != nil { 46 | Log.Info("closing client", c.incomingConn.RemoteAddr().String(), err) 47 | c.bot.clientConnected = false 48 | c.close() 49 | return 50 | } 51 | c.handleMessage(line) 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /twitch.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import "flag" 4 | 5 | var addr = flag.String("addr", "irc.chat.twitch.tv:6697", "secure twitch irc address") 6 | --------------------------------------------------------------------------------