Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion README.MD
Original file line number Diff line number Diff line change
Expand Up @@ -54,10 +54,11 @@ Channel is just a string like "lirik", note the absent #.
### Message Types

If you ever need more than basic PRIVMSG this might be for you.
These are the 5 major message types currently supported
These are the 6 major message types currently supported

PRIVMSG
WHISPER
ROOMSTATE
CLEARCHAT
USERNOTICE
RECONNECT
28 changes: 25 additions & 3 deletions client.go
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,8 @@ type Client struct {
onNewRoomstateMessage func(channel string, user User, message Message)
onNewClearchatMessage func(channel string, user User, message Message)
onNewUsernoticeMessage func(channel string, user User, message Message)
onNewReconnectMessage func()
quitKeepAlive chan struct{}
}

// NewClient to create a new client
Expand Down Expand Up @@ -86,6 +88,12 @@ func (c *Client) OnNewUsernoticeMessage(callback func(channel string, user User,
c.onNewUsernoticeMessage = callback
}

// OnNewReconnectMessage attach callback to new reconnect message
// or when a PING timeout causes keepConnectionAlive() to reconnect
func (c *Client) OnNewReconnectMessage(callback func()) {
c.onNewReconnectMessage = callback
}

// Say write something in a chat
func (c *Client) Say(channel, text string) {
c.send(fmt.Sprintf("PRIVMSG #%s :%s", channel, text))
Expand All @@ -107,6 +115,9 @@ func (c *Client) Join(channel string) {
// Disconnect close current connection
func (c *Client) Disconnect() error {
c.connActive.set(false)
if c.quitKeepAlive != nil {
close(c.quitKeepAlive)
}
if c.connection != nil {
return c.connection.Close()
}
Expand Down Expand Up @@ -172,9 +183,12 @@ func (c *Client) setupConnection() {
go c.keepConnectionAlive()
}

// keepAliveInterval is for decreasing the duration tests
var keepAliveInterval = 500 * time.Second

func (c *Client) keepConnectionAlive() {
ticker := time.NewTicker(500 * time.Second)
quit := make(chan struct{})
ticker := time.NewTicker(keepAliveInterval)
c.quitKeepAlive = make(chan struct{})
go func() {
for {
select {
Expand All @@ -184,7 +198,7 @@ func (c *Client) keepConnectionAlive() {
c.Connect()
}
c.wasPinged.set(false)
case <-quit:
case <-c.quitKeepAlive:
ticker.Stop()
return
}
Expand All @@ -204,6 +218,14 @@ func (c *Client) send(line string) {
}

func (c *Client) handleLine(line string) {
if line == ":tmi.twitch.tv RECONNECT" {
if c.onNewReconnectMessage != nil {
c.onNewReconnectMessage()
}
c.Disconnect()
c.Connect()
return
}
if strings.HasPrefix(line, "PING") {
c.send(strings.Replace(line, "PING", "PONG", 1))
c.wasPinged.set(true)
Expand Down
127 changes: 122 additions & 5 deletions client_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import (
"bufio"
"crypto/tls"
"fmt"
"io"
"log"
"net/textproto"
"reflect"
Expand Down Expand Up @@ -50,7 +51,7 @@ func TestCanConnectAndAuthenticate(t *testing.T) {

for {
message, err := tp.ReadLine()
if err != nil {
if err != nil && err != io.EOF {
t.Fatal(err)
}
message = strings.Replace(message, "\r\n", "", 1)
Expand Down Expand Up @@ -82,6 +83,8 @@ func TestCanConnectAndAuthenticate(t *testing.T) {
}

func TestCanDisconnect(t *testing.T) {
keepAliveInterval = 9999 * time.Second

testMessage := "@badges=subscriber/6,premium/1;color=#FF0000;display-name=Redflamingo13;emotes=;id=2a31a9df-d6ff-4840-b211-a2547c7e656e;mod=0;room-id=11148817;subscriber=1;tmi-sent-ts=1490382457309;turbo=0;user-id=78424343;user-type= :redflamingo13!redflamingo13@redflamingo13.tmi.twitch.tv PRIVMSG #pajlada :Thrashh5, FeelsWayTooAmazingMan kinda"
wait := make(chan struct{})

Expand Down Expand Up @@ -466,7 +469,7 @@ func TestCanSayMessage(t *testing.T) {

for {
message, err := tp.ReadLine()
if err != nil {
if err != nil && err != io.EOF {
t.Fatal(err)
}
message = strings.Replace(message, "\r\n", "", 1)
Expand Down Expand Up @@ -536,7 +539,7 @@ func TestCanWhisperMessage(t *testing.T) {

for {
message, err := tp.ReadLine()
if err != nil {
if err != nil && err != io.EOF {
t.Fatal(err)
}
message = strings.Replace(message, "\r\n", "", 1)
Expand Down Expand Up @@ -605,7 +608,7 @@ func TestCanJoinChannel(t *testing.T) {

for {
message, err := tp.ReadLine()
if err != nil {
if err != nil && err != io.EOF {
t.Fatal(err)
}
message = strings.Replace(message, "\r\n", "", 1)
Expand Down Expand Up @@ -674,7 +677,7 @@ func TestCanPong(t *testing.T) {

for {
message, err := tp.ReadLine()
if err != nil {
if err != nil && err != io.EOF {
t.Fatal(err)
}
message = strings.Replace(message, "\r\n", "", 1)
Expand Down Expand Up @@ -719,3 +722,117 @@ func TestCanNotDialInvalidAddress(t *testing.T) {
t.Fatal("invalid Connect() error")
}
}

func TestCanCreateReconnectMessage(t *testing.T) {
var reconnections int
keepAliveInterval = 3 * time.Second
testMessage := "@badges=subscriber/6,premium/1;color=#FF0000;display-name=Redflamingo13;emotes=;id=2a31a9df-d6ff-4840-b211-a2547c7e656e;mod=0;room-id=11148817;subscriber=1;tmi-sent-ts=1490382457309;turbo=0;user-id=78424343;user-type= :redflamingo13!redflamingo13@redflamingo13.tmi.twitch.tv PRIVMSG #pajlada :Thrashh5, FeelsWayTooAmazingMan kinda"
wait := make(chan struct{})

go func() {
cer, err := tls.LoadX509KeyPair("test_resources/server.crt", "test_resources/server.key")
if err != nil {
log.Println(err)
return
}
config := &tls.Config{
Certificates: []tls.Certificate{cer},
}
ln, err := tls.Listen("tcp", ":4331", config)
if err != nil {
t.Fatal(err)
}
close(wait)
conn, err := ln.Accept()
if err != nil {
t.Fatal(err)
}
defer ln.Close()
defer conn.Close()

fmt.Fprintf(conn, "%s\r\n", testMessage)
}()

// wait for server to start
select {
case <-wait:
case <-time.After(time.Second * 9):
t.Fatal("server didn't start")
}

client := NewClient("justinfan123123", "oauth:123123132")
client.IrcAddress = ":4331"

client.OnNewReconnectMessage(func() {
reconnections++
})

go client.Connect()

waitMsg := make(chan string)

// wait for server to start
select {
case <-waitMsg:
case <-time.After(time.Second * 6):
if reconnections >= 1 {
t.Fatal("failed to a recieve onReconnectEvent event")
}
}
}

func TestCanReceiveReconnectMessage(t *testing.T) {
var reconnections int
testMessage := ":tmi.twitch.tv RECONNECT"
wait := make(chan struct{})

go func() {
cer, err := tls.LoadX509KeyPair("test_resources/server.crt", "test_resources/server.key")
if err != nil {
log.Println(err)
return
}
config := &tls.Config{
Certificates: []tls.Certificate{cer},
}
ln, err := tls.Listen("tcp", ":4332", config)
if err != nil {
t.Fatal(err)
}
close(wait)
conn, err := ln.Accept()
if err != nil {
t.Fatal(err)
}
defer ln.Close()
defer conn.Close()

fmt.Fprintf(conn, "%s\r\n", testMessage)
}()

// wait for server to start
select {
case <-wait:
case <-time.After(time.Second * 3):
t.Fatal("server didn't start")
}

client := NewClient("justinfan123123", "oauth:123123132")
client.IrcAddress = ":4332"
go client.Connect()

waitMsg := make(chan string)

client.OnNewReconnectMessage(func() {
reconnections++
close(waitMsg)
})

// wait for server to start
select {
case <-waitMsg:
case <-time.After(time.Second * 3):
t.Fatal("no message sent")
}
assertIntsEqual(t, 1, reconnections)
}
7 changes: 6 additions & 1 deletion message.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,8 @@ const (
ROOMSTATE = 3
// USERNOTICE messages like subs, resubs, raids, etc
USERNOTICE = 4
// RECONNECT message
RECONNECT = 5
)

type message struct {
Expand Down Expand Up @@ -86,7 +88,10 @@ func parseMessage(line string) *message {
func parseOtherMessage(line string) *message {
msg := &message{}
split := strings.Split(line, " ")

if split[1] == "RECONNECT" {
msg.Type = RECONNECT
return msg
}
switch split[2] {
case "ROOMSTATE":
msg.Type = ROOMSTATE
Expand Down
10 changes: 10 additions & 0 deletions message_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -113,3 +113,13 @@ func TestCanParseRoomstateMessage(t *testing.T) {
t.Error("parsing ROOMSTATE message failed")
}
}
func TestCanParseReconnectMessage(t *testing.T) {
testMessage := `:tmi.twitch.tv RECONNECT`

message := parseOtherMessage(testMessage)

if message.Type != RECONNECT {
t.Error("parsing RECONNECT message failed")
}
assertIntsEqual(t, 5, int(message.Type))
}