Compare commits

..

10 Commits

Author SHA1 Message Date
Martyn Ranyard cf5f2e6598 Dockerfile to go standard package location
continuous-integration/drone/tag Build was killed Details
2020-02-14 11:03:25 +01:00
Martyn Ranyard 13b8ce33b5 match https://github.com/golang-standards/project-layout 2020-02-14 10:58:30 +01:00
Martyn Ranyard edc6f08b8b Only attempt to docker build when version tag is rolled. 2020-02-14 09:57:02 +01:00
Martyn 0af0f25be4 Sorta works
Signed-off-by: Martyn Ranyard <m@rtyn.berlin>
2020-02-13 23:52:48 +01:00
Martyn 96b4b8f8e3 maybe drone likes this?
Signed-off-by: Martyn Ranyard <m@rtyn.berlin>
2020-02-13 23:51:38 +01:00
Martyn 42f846a6bc Some debug
Signed-off-by: Martyn Ranyard <m@rtyn.berlin>
2020-02-13 23:49:48 +01:00
Martyn 3aa8d59ead How likely is this to work?
Signed-off-by: Martyn Ranyard <m@rtyn.berlin>
2020-02-13 23:47:33 +01:00
Martyn e4b1cdd869 Fix Dockerfile and move deployment stuff
Signed-off-by: Martyn Ranyard <m@rtyn.berlin>
2020-02-13 16:48:32 +01:00
Martyn 3f358aa1fa Refactored to a package
Signed-off-by: Martyn Ranyard <m@rtyn.berlin>
2020-02-13 16:39:57 +01:00
Martyn Ranyard 23a5cdc98c move irc to a package 2020-02-13 16:18:44 +01:00
11 changed files with 527 additions and 471 deletions

View File

@ -1,24 +0,0 @@
kind: pipeline
type: docker
name: linux-amd64
platform:
arch: amd64
os: linux
steps:
- name: publish
image: plugins/docker:18
settings:
auto_tag: true
auto_tag_suffix: linux-amd64
dockerfile: Dockerfile
repo: imartyn/karaokardbot
username:
from_secret: docker_username
password:
from_secret: docker_password
when:
event:
- push
- tag

1
.gitignore vendored
View File

@ -12,3 +12,4 @@
# Output of the go coverage tool, specifically when used with LiteIDE # Output of the go coverage tool, specifically when used with LiteIDE
*.out *.out
karaokards

View File

@ -1,12 +0,0 @@
FROM golang@sha256:cee6f4b901543e8e3f20da3a4f7caac6ea643fd5a46201c3c2387183a332d989 AS builder
RUN apk update && apk add --no-cache git make ca-certificates && update-ca-certificates
COPY main.go /go/src/github.com/iMartyn/karaokards/
RUN cd /go/src/github.com/iMartyn/karaokards/; go get; CGO_ENABLED=0 GOOS=linux go build -a -installsuffix cgo -o karaokards .
#RUN ls /go/src/github.com/karaokards/ -l
FROM scratch
COPY --from=builder /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/
COPY --from=builder /go/src/github.com/iMartyn/karaokards/karaokards /app/
COPY strings.json /app/strings.json
CMD ["/app/karaokards"]

63
build/ci/drone.yml Normal file
View File

@ -0,0 +1,63 @@
kind: pipeline
type: docker
name: linux-amd64-taggedver
platform:
arch: amd64
os: linux
steps:
- name: build
image: golang
commands:
- pwd
- mkdir -p /go/src/git.martyn.berlin/martyn
- ln -s /drone/src /go/src/git.martyn.berlin/martyn/karaokards
- cd /go/src/git.martyn.berlin/martyn/karaokards
- go get
- go build
- name: publish
image: plugins/docker:18
settings:
auto_tag: true
auto_tag_suffix: linux-amd64
dockerfile: build/package/Dockerfile
repo: imartyn/karaokardbot
username:
from_secret: docker_username
password:
from_secret: docker_password
when:
event:
- push
- tag
trigger:
ref:
- refs/tags/v*
---
kind: pipeline
type: docker
name: linux-amd64-devel-master
platform:
arch: amd64
os: linux
steps:
- name: build
image: golang
commands:
- pwd
- mkdir -p /go/src/git.martyn.berlin/martyn
- ln -s /drone/src /go/src/git.martyn.berlin/martyn/karaokards
- cd /go/src/git.martyn.berlin/martyn/karaokards
- go get
- go build
trigger:
ref:
- refs/heads/devel
- refs/heads/master

13
build/package/Dockerfile Executable file
View File

@ -0,0 +1,13 @@
FROM golang@sha256:cee6f4b901543e8e3f20da3a4f7caac6ea643fd5a46201c3c2387183a332d989 AS builder
RUN apk update && apk add --no-cache git make ca-certificates && update-ca-certificates
COPY main.go /go/src/git.martyn.berlin/martyn/karaokards/
COPY internal/ /go/src/git.martyn.berlin/martyn/karaokards/internal/
RUN cd /go/src/git.martyn.berlin/martyn/karaokards/; go get; CGO_ENABLED=0 GOOS=linux go build -a -installsuffix cgo -o karaokards .
#RUN ls /go/src/github.com/karaokards/ -l
FROM scratch
COPY --from=builder /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/
COPY --from=builder /go/src/git.martyn.berlin/martyn/karaokards /app/
COPY strings.json /app/strings.json
CMD ["/app/karaokards"]

181
internal/builtins/kards.go Normal file
View File

@ -0,0 +1,181 @@
package builtins
var Karaokards = [...]string{
"Chorus contains a name",
"Refers to an animal",
"Aggressive",
"Contains instructions",
"Refers to relationships",
"Folk",
"Novelty",
"Pre-sixties",
"1960s",
"Ballad",
"Musical",
"Heartbroken",
"Contains made-up words",
"Refers to a colour",
"Chorus contains me, mine or my",
"Chorus contains want, need or have",
"Chorus contains how, when or why",
"Refers to money",
"Chorus contains love, like or hate",
"Chorus contains man, woman or everybody",
"Anthemic",
"Refers to explosives",
"Chorus contains a number",
"1970s",
"2000s",
"Love Song",
"Country-pop",
"Grunge",
"Indie",
"This is a morality tale",
"I've never sung this before",
"They'd beat me in a fight",
"My mortal enemy",
"Their name starts with the same letter as mine",
"This is NOT my era",
"Chorus contains oh, ooh or baby",
"Samples another song",
"Chorus contains don't, won't or can't",
"Euphoric",
"Title contains day, night or tomorrow",
"Chorus contains boy, girl or child",
"Refers to space",
"Soul",
"R&B",
"Dance",
"Electronie",
"Pop",
"1990s",
"Rock",
"Title contains brackets",
"Refers to religion",
"Refers to music",
"Chorus contains this, that or there",
"Chorus contains up, down or over",
"Beautiful",
"Mean about someone",
"Refers to weather",
"Chorus contains you, your or you're",
"Gloomy",
"Contains questions",
"Refers to death",
"Refers to sleep",
"Chorus contains heart, head or soul",
"Rock",
"Pop",
"Country",
"Punk",
"Rap",
"Christmas",
"Motown",
"This is their Second-best song",
"I shouldn't know this song. But I do!",
"I don't need the screen!",
"I'm too old for this song",
"The story of my life",
"I love this song SO much",
"They're twice my age!",
"Chorus contains I, Im or Ive",
"Title is one word long",
"Soundtrack",
"Pop",
"Is a metaphor",
"Title is at least five words long",
"Chorus contains move, stay or go",
"Refers to a place",
"R&B",
"Metal",
"Good workout music",
"Requires audience participation",
"Power Ballad",
"1980s",
"Rock",
"Rap",
"Pop-punk",
"Alternative",
"Soul",
"My secret shame",
"This gets me a bit emotional",
"Out of my range",
"This person is bad at their job",
"They look like someone here",
"I don't know the verse",
"They're half my age!",
"Play this at my funeral",
"This is NOT my genre",
"I hate this song so much",
"Girl band",
"Male solo",
"Artist begins with A",
"Just one word",
"Mixed gender band",
"Artist begins with C",
"Artist begins with G",
"Two people",
"TV contestant",
"European",
"Artist begins with P",
"Artist begins with M",
"In a famous family",
"Male-fronted band",
"Australian",
"One-hit wonder",
"Female-fronted band",
"Artist begins with T",
"Rock",
"Indie-rock",
"R&B",
"Latin",
"Disco",
"Britpop",
"2010s",
"I was a teenager!",
"The music video is so good",
"Im younger than this song",
"First dance at my imaginary wedding",
"I'm worried about them",
"This person is the best dancer",
"I know the dance",
"Mostly shouting",
"They're not my gender",
"I wish we were married",
"I played this song too often",
"They are so influential",
"My favourite song of theirs",
"Perfect montage music",
"Artist uses their surname",
"Famous partner",
"Artist begins with E",
"Troubled artist",
"Actor",
"Artist begins with F",
"Solo artist",
"Artist begins with R",
"Hellraiser",
"Artist begins with The",
"10+ year career",
"Boy band",
"Female solo",
"British",
"Inappropriately clothed",
"Award winners",
"Artist begins with S",
"Artist begins with W",
"Asian",
"They split up :(",
"North American",
"Pop",
"Goth or Emo",
"This is a bit creepy, frankly",
"Ive seen them in real life",
"A beautiful love story",
"OK, this is just ridiculous",
"Artist begins with B",
"Artist begins with D",
"Theyre dead :(",
"Artist begins with L",
"Amazing hair",
"Artist begins with J"}

238
internal/irc/irc.go Normal file
View File

@ -0,0 +1,238 @@
package irc
import (
"bufio"
"encoding/json"
"errors"
"fmt"
"io"
"io/ioutil"
"math/rand"
"net"
"net/textproto"
"regexp"
"strings"
"time"
rgb "github.com/foresthoffman/rgblog"
)
const PSTFormat = "Jan 2 15:04:05 PST"
// Regex for parsing PRIVMSG strings.
//
// First matched group is the user's name and the second matched group is the content of the
// user's message.
var MsgRegex *regexp.Regexp = regexp.MustCompile(`^:(\w+)!\w+@\w+\.tmi\.twitch\.tv (PRIVMSG) #\w+(?: :(.*))?$`)
// Regex for parsing user commands, from already parsed PRIVMSG strings.
//
// First matched group is the command name and the second matched group is the argument for the
// command.
var CmdRegex *regexp.Regexp = regexp.MustCompile(`^!(\w+)\s?(\w+)?`)
type OAuthCred struct {
// The bot account's OAuth password.
Password string `json:"password,omitempty"`
// The developer application client ID. Used for API calls to Twitch.
ClientID string `json:"client_id,omitempty"`
}
type KardBot struct {
Channel string
conn net.Conn
Credentials *OAuthCred
MsgRate time.Duration
Name string
Port string
PrivatePath string
Server string
startTime time.Time
Prompts []string
}
// Connects the bot to the Twitch IRC server. The bot will continue to try to connect until it
// succeeds or is forcefully shutdown.
func (bb *KardBot) Connect() {
var err error
rgb.YPrintf("[%s] Connecting to %s...\n", TimeStamp(), bb.Server)
// makes connection to Twitch IRC server
bb.conn, err = net.Dial("tcp", bb.Server+":"+bb.Port)
if nil != err {
rgb.YPrintf("[%s] Cannot connect to %s, retrying.\n", TimeStamp(), bb.Server)
bb.Connect()
return
}
rgb.YPrintf("[%s] Connected to %s!\n", TimeStamp(), bb.Server)
bb.startTime = time.Now()
}
// Officially disconnects the bot from the Twitch IRC server.
func (bb *KardBot) Disconnect() {
bb.conn.Close()
upTime := time.Now().Sub(bb.startTime).Seconds()
rgb.YPrintf("[%s] Closed connection from %s! | Live for: %fs\n", TimeStamp(), bb.Server, upTime)
}
// Listens for and logs messages from chat. Responds to commands from the channel owner. The bot
// continues until it gets disconnected, told to shutdown, or forcefully shutdown.
func (bb *KardBot) HandleChat() error {
rgb.YPrintf("[%s] Watching #%s...\n", TimeStamp(), bb.Channel)
// reads from connection
tp := textproto.NewReader(bufio.NewReader(bb.conn))
// listens for chat messages
for {
line, err := tp.ReadLine()
if nil != err {
// officially disconnects the bot from the server
bb.Disconnect()
return errors.New("bb.Bot.HandleChat: Failed to read line from channel. Disconnected.")
}
// logs the response from the IRC server
rgb.YPrintf("[%s] %s\n", TimeStamp(), line)
if "PING :tmi.twitch.tv" == line {
// respond to PING message with a PONG message, to maintain the connection
bb.conn.Write([]byte("PONG :tmi.twitch.tv\r\n"))
continue
} else {
// handle a PRIVMSG message
matches := MsgRegex.FindStringSubmatch(line)
if nil != matches {
userName := matches[1]
msgType := matches[2]
switch msgType {
case "PRIVMSG":
msg := matches[3]
rgb.GPrintf("[%s] %s: %s\n", TimeStamp(), userName, msg)
// parse commands from user message
cmdMatches := CmdRegex.FindStringSubmatch(msg)
if nil != cmdMatches {
cmd := cmdMatches[1]
switch cmd {
case "card":
rgb.CPrintf("[%s] Card asked for!\n", TimeStamp())
bb.Say("Your prompt is : " + bb.Prompts[rand.Intn(len(bb.Prompts))])
}
// channel-owner specific commands
if userName == bb.Channel {
switch cmd {
case "tbdown":
rgb.CPrintf(
"[%s] Shutdown command received. Shutting down now...\n",
TimeStamp(),
)
bb.Disconnect()
return nil
default:
// do nothing
}
}
}
default:
// do nothing
}
}
}
time.Sleep(bb.MsgRate)
}
}
// Makes the bot join its pre-specified channel.
func (bb *KardBot) JoinChannel() {
rgb.YPrintf("[%s] Joining #%s...\n", TimeStamp(), bb.Channel)
bb.conn.Write([]byte("PASS " + bb.Credentials.Password + "\r\n"))
bb.conn.Write([]byte("NICK " + bb.Name + "\r\n"))
bb.conn.Write([]byte("JOIN #" + bb.Channel + "\r\n"))
rgb.YPrintf("[%s] Joined #%s as @%s!\n", TimeStamp(), bb.Channel, bb.Name)
}
// Reads from the private credentials file and stores the data in the bot's Credentials field.
func (bb *KardBot) ReadCredentials() error {
// reads from the file
credFile, err := ioutil.ReadFile(bb.PrivatePath)
if nil != err {
return err
}
bb.Credentials = &OAuthCred{}
// parses the file contents
dec := json.NewDecoder(strings.NewReader(string(credFile)))
if err = dec.Decode(bb.Credentials); nil != err && io.EOF != err {
return err
}
return nil
}
// Makes the bot send a message to the chat channel.
func (bb *KardBot) Say(msg string) error {
if "" == msg {
return errors.New("BasicBot.Say: msg was empty.")
}
// check if message is too large for IRC
if len(msg) > 512 {
return errors.New("BasicBot.Say: msg exceeded 512 bytes")
}
_, err := bb.conn.Write([]byte(fmt.Sprintf("PRIVMSG #%s :%s\r\n", bb.Channel, msg)))
if nil != err {
return err
}
return nil
}
// Starts a loop where the bot will attempt to connect to the Twitch IRC server, then connect to the
// pre-specified channel, and then handle the chat. It will attempt to reconnect until it is told to
// shut down, or is forcefully shutdown.
func (bb *KardBot) Start() {
err := bb.ReadCredentials()
if nil != err {
fmt.Println(err)
fmt.Println("Aborting...")
return
}
for {
bb.Connect()
bb.JoinChannel()
err = bb.HandleChat()
if nil != err {
// attempts to reconnect upon unexpected chat error
time.Sleep(1000 * time.Millisecond)
fmt.Println(err)
fmt.Println("Starting bot again...")
} else {
return
}
}
}
func TimeStamp() string {
return TimeStampFmt(PSTFormat)
}
func TimeStampFmt(format string) string {
return time.Now().Format(format)
}

Binary file not shown.

466
main.go
View File

@ -1,491 +1,87 @@
package main package main
import ( import (
"bufio"
"encoding/json" "encoding/json"
"errors"
"fmt" "fmt"
"io"
"io/ioutil" "io/ioutil"
"net"
"net/textproto"
"regexp"
"strings"
"time"
"math/rand" "math/rand"
"os" "os"
"path/filepath" "path/filepath"
"time"
builtins "git.martyn.berlin/martyn/karaokards/internal/builtins"
irc "git.martyn.berlin/martyn/karaokards/internal/irc"
rgb "github.com/foresthoffman/rgblog" rgb "github.com/foresthoffman/rgblog"
) )
var karaokards = [...]string {
"Chorus contains a name",
"Refers to an animal",
"Aggressive",
"Contains instructions",
"Refers to relationships",
"Folk",
"Novelty",
"Pre-sixties",
"1960s",
"Ballad",
"Musical",
"Heartbroken",
"Contains made-up words",
"Refers to a colour",
"Chorus contains me, mine or my",
"Chorus contains want, need or have",
"Chorus contains how, when or why",
"Refers to money",
"Chorus contains love, like or hate",
"Chorus contains man, woman or everybody",
"Anthemic",
"Refers to explosives",
"Chorus contains a number",
"1970s",
"2000s",
"Love Song",
"Country-pop",
"Grunge",
"Indie",
"This is a morality tale",
"I've never sung this before",
"They'd beat me in a fight",
"My mortal enemy",
"Their name starts with the same letter as mine",
"This is NOT my era",
"Chorus contains oh, ooh or baby",
"Samples another song",
"Chorus contains don't, won't or can't",
"Euphoric",
"Title contains day, night or tomorrow",
"Chorus contains boy, girl or child",
"Refers to space",
"Soul",
"R&B",
"Dance",
"Electronie",
"Pop",
"1990s",
"Rock",
"Title contains brackets",
"Refers to religion",
"Refers to music",
"Chorus contains this, that or there",
"Chorus contains up, down or over",
"Beautiful",
"Mean about someone",
"Refers to weather",
"Chorus contains you, your or you're",
"Gloomy",
"Contains questions",
"Refers to death",
"Refers to sleep",
"Chorus contains heart, head or soul",
"Rock",
"Pop",
"Country",
"Punk",
"Rap",
"Christmas",
"Motown",
"This is their Second-best song",
"I shouldn't know this song. But I do!",
"I don't need the screen!",
"I'm too old for this song",
"The story of my life",
"I love this song SO much",
"They're twice my age!",
"Chorus contains I, Im or Ive",
"Title is one word long",
"Soundtrack",
"Pop",
"Is a metaphor",
"Title is at least five words long",
"Chorus contains move, stay or go",
"Refers to a place",
"R&B",
"Metal",
"Good workout music",
"Requires audience participation",
"Power Ballad",
"1980s",
"Rock",
"Rap",
"Pop-punk",
"Alternative",
"Soul",
"My secret shame",
"This gets me a bit emotional",
"Out of my range",
"This person is bad at their job",
"They look like someone here",
"I don't know the verse",
"They're half my age!",
"Play this at my funeral",
"This is NOT my genre",
"I hate this song so much",
"Girl band",
"Male solo",
"Artist begins with A",
"Just one word",
"Mixed gender band",
"Artist begins with C",
"Artist begins with G",
"Two people",
"TV contestant",
"European",
"Artist begins with P",
"Artist begins with M",
"In a famous family",
"Male-fronted band",
"Australian",
"One-hit wonder",
"Female-fronted band",
"Artist begins with T",
"Rock",
"Indie-rock",
"R&B",
"Latin",
"Disco",
"Britpop",
"2010s",
"I was a teenager!",
"The music video is so good",
"Im younger than this song",
"First dance at my imaginary wedding",
"I'm worried about them",
"This person is the best dancer",
"I know the dance",
"Mostly shouting",
"They're not my gender",
"I wish we were married",
"I played this song too often",
"They are so influential",
"My favourite song of theirs",
"Perfect montage music",
"Artist uses their surname",
"Famous partner",
"Artist begins with E",
"Troubled artist",
"Actor",
"Artist begins with F",
"Solo artist",
"Artist begins with R",
"Hellraiser",
"Artist begins with The",
"10+ year career",
"Boy band",
"Female solo",
"British",
"Inappropriately clothed",
"Award winners",
"Artist begins with S",
"Artist begins with W",
"Asian",
"They split up :(",
"North American",
"Pop",
"Goth or Emo",
"This is a bit creepy, frankly",
"Ive seen them in real life",
"A beautiful love story",
"OK, this is just ridiculous",
"Artist begins with B",
"Artist begins with D",
"Theyre dead :(",
"Artist begins with L",
"Amazing hair",
"Artist begins with J"}
var selectablePrompts []string
const PSTFormat = "Jan 2 15:04:05 PST"
// Regex for parsing PRIVMSG strings.
//
// First matched group is the user's name and the second matched group is the content of the
// user's message.
var MsgRegex *regexp.Regexp = regexp.MustCompile(`^:(\w+)!\w+@\w+\.tmi\.twitch\.tv (PRIVMSG) #\w+(?: :(.*))?$`)
// Regex for parsing user commands, from already parsed PRIVMSG strings.
//
// First matched group is the command name and the second matched group is the argument for the
// command.
var CmdRegex *regexp.Regexp = regexp.MustCompile(`^!(\w+)\s?(\w+)?`)
type OAuthCred struct {
// The bot account's OAuth password.
Password string `json:"password,omitempty"`
// The developer application client ID. Used for API calls to Twitch.
ClientID string `json:"client_id,omitempty"`
}
type kardBot struct {
Channel string
conn net.Conn
Credentials *OAuthCred
MsgRate time.Duration
Name string
Port string
PrivatePath string
Server string
startTime time.Time
}
// Connects the bot to the Twitch IRC server. The bot will continue to try to connect until it
// succeeds or is forcefully shutdown.
func (bb *kardBot) Connect() {
var err error
rgb.YPrintf("[%s] Connecting to %s...\n", timeStamp(), bb.Server)
// makes connection to Twitch IRC server
bb.conn, err = net.Dial("tcp", bb.Server+":"+bb.Port)
if nil != err {
rgb.YPrintf("[%s] Cannot connect to %s, retrying.\n", timeStamp(), bb.Server)
bb.Connect()
return
}
rgb.YPrintf("[%s] Connected to %s!\n", timeStamp(), bb.Server)
bb.startTime = time.Now()
}
// Officially disconnects the bot from the Twitch IRC server.
func (bb *kardBot) Disconnect() {
bb.conn.Close()
upTime := time.Now().Sub(bb.startTime).Seconds()
rgb.YPrintf("[%s] Closed connection from %s! | Live for: %fs\n", timeStamp(), bb.Server, upTime)
}
// Listens for and logs messages from chat. Responds to commands from the channel owner. The bot
// continues until it gets disconnected, told to shutdown, or forcefully shutdown.
func (bb *kardBot) HandleChat() error {
rgb.YPrintf("[%s] Watching #%s...\n", timeStamp(), bb.Channel)
// reads from connection
tp := textproto.NewReader(bufio.NewReader(bb.conn))
// listens for chat messages
for {
line, err := tp.ReadLine()
if nil != err {
// officially disconnects the bot from the server
bb.Disconnect()
return errors.New("bb.Bot.HandleChat: Failed to read line from channel. Disconnected.")
}
// logs the response from the IRC server
rgb.YPrintf("[%s] %s\n", timeStamp(), line)
if "PING :tmi.twitch.tv" == line {
// respond to PING message with a PONG message, to maintain the connection
bb.conn.Write([]byte("PONG :tmi.twitch.tv\r\n"))
continue
} else {
// handle a PRIVMSG message
matches := MsgRegex.FindStringSubmatch(line)
if nil != matches {
userName := matches[1]
msgType := matches[2]
switch msgType {
case "PRIVMSG":
msg := matches[3]
rgb.GPrintf("[%s] %s: %s\n", timeStamp(), userName, msg)
// parse commands from user message
cmdMatches := CmdRegex.FindStringSubmatch(msg)
if nil != cmdMatches {
cmd := cmdMatches[1]
switch cmd {
case "card":
rgb.CPrintf("[%s] Card asked for!\n", timeStamp(),)
bb.Say("Your prompt is : "+selectablePrompts[rand.Intn(len(selectablePrompts))])
}
// channel-owner specific commands
if userName == bb.Channel {
switch cmd {
case "tbdown":
rgb.CPrintf(
"[%s] Shutdown command received. Shutting down now...\n",
timeStamp(),
)
bb.Disconnect()
return nil
default:
// do nothing
}
}
}
default:
// do nothing
}
}
}
time.Sleep(bb.MsgRate)
}
}
// Makes the bot join its pre-specified channel.
func (bb *kardBot) JoinChannel() {
rgb.YPrintf("[%s] Joining #%s...\n", timeStamp(), bb.Channel)
bb.conn.Write([]byte("PASS " + bb.Credentials.Password + "\r\n"))
bb.conn.Write([]byte("NICK " + bb.Name + "\r\n"))
bb.conn.Write([]byte("JOIN #" + bb.Channel + "\r\n"))
rgb.YPrintf("[%s] Joined #%s as @%s!\n", timeStamp(), bb.Channel, bb.Name)
}
// Reads from the private credentials file and stores the data in the bot's Credentials field.
func (bb *kardBot) ReadCredentials() error {
// reads from the file
credFile, err := ioutil.ReadFile(bb.PrivatePath)
if nil != err {
return err
}
bb.Credentials = &OAuthCred{}
// parses the file contents
dec := json.NewDecoder(strings.NewReader(string(credFile)))
if err = dec.Decode(bb.Credentials); nil != err && io.EOF != err {
return err
}
return nil
}
// Makes the bot send a message to the chat channel.
func (bb *kardBot) Say(msg string) error {
if "" == msg {
return errors.New("BasicBot.Say: msg was empty.")
}
// check if message is too large for IRC
if len(msg) > 512 {
return errors.New("BasicBot.Say: msg exceeded 512 bytes")
}
_, err := bb.conn.Write([]byte(fmt.Sprintf("PRIVMSG #%s :%s\r\n", bb.Channel, msg)))
if nil != err {
return err
}
return nil
}
// Starts a loop where the bot will attempt to connect to the Twitch IRC server, then connect to the
// pre-specified channel, and then handle the chat. It will attempt to reconnect until it is told to
// shut down, or is forcefully shutdown.
func (bb *kardBot) Start() {
err := bb.ReadCredentials()
if nil != err {
fmt.Println(err)
fmt.Println("Aborting...")
return
}
for {
bb.Connect()
bb.JoinChannel()
err = bb.HandleChat()
if nil != err {
// attempts to reconnect upon unexpected chat error
time.Sleep(1000 * time.Millisecond)
fmt.Println(err)
fmt.Println("Starting bot again...")
} else {
return
}
}
}
func timeStamp() string {
return TimeStamp(PSTFormat)
}
func TimeStamp(format string) string {
return time.Now().Format(format)
}
type customStringsStruct struct { type customStringsStruct struct {
Strings []string `json:"strings,omitempty"` Strings []string `json:"strings,omitempty"`
} }
var selectablePrompts []string
var customStrings customStringsStruct; var customStrings customStringsStruct
func readBonusStrings() []string { func readBonusStrings() []string {
ex, err := os.Executable() ex, err := os.Executable()
if err != nil { if err != nil {
return []string{} return []string{}
fmt.Println("Could not read `strings.json`, will only have builtin prompts. File reading error", err) fmt.Println("Could not read `strings.json`, will only have builtin prompts. File reading error", err)
} }
exPath := filepath.Dir(ex) exPath := filepath.Dir(ex)
data, err := ioutil.ReadFile(exPath+"/strings.json") data, err := ioutil.ReadFile(exPath + "/strings.json")
if err != nil { if err != nil {
fmt.Println("Could not read `strings.json`, will only have builtin prompts. File reading error", err) fmt.Println("Could not read `strings.json`, will only have builtin prompts. File reading error", err)
return []string{} return []string{}
} }
err = json.Unmarshal(data, &customStrings); err = json.Unmarshal(data, &customStrings)
if err != nil { if err != nil {
fmt.Println("Could not unmarshal `strings.json`, will only have builtin prompts. Unmarshal error", err) fmt.Println("Could not unmarshal `strings.json`, will only have builtin prompts. Unmarshal error", err)
return []string{} return []string{}
} }
fmt.Println("Read ",len(customStrings.Strings)," prompts from `strings.json`"); fmt.Println("Read ", len(customStrings.Strings), " prompts from `strings.json`")
return customStrings.Strings return customStrings.Strings
} }
func main() { func main() {
rand.Seed(time.Now().UnixNano()) rand.Seed(time.Now().UnixNano())
for _, val := range karaokards { for _, val := range builtins.Karaokards {
selectablePrompts = append(selectablePrompts,val); selectablePrompts = append(selectablePrompts, val)
} }
for _, val := range readBonusStrings() { for _, val := range readBonusStrings() {
selectablePrompts = append(selectablePrompts,val); selectablePrompts = append(selectablePrompts, val)
} }
fmt.Println(len(selectablePrompts)," prompts available."); fmt.Println(len(selectablePrompts), " prompts available.")
oauthPath := "" oauthPath := ""
if (os.Getenv("TWITCH_OAUTH_JSON") != "") { if os.Getenv("TWITCH_OAUTH_JSON") != "" {
if _, err := os.Stat(os.Getenv("TWITCH_OAUTH_JSON")); os.IsNotExist(err) { if _, err := os.Stat(os.Getenv("TWITCH_OAUTH_JSON")); os.IsNotExist(err) {
os.Exit(1); os.Exit(1)
} }
oauthPath = os.Getenv("TWITCH_OAUTH_JSON") oauthPath = os.Getenv("TWITCH_OAUTH_JSON")
} else { } else {
if _, err := os.Stat(os.Getenv("HOME")+"/.twitch/oauth.json"); os.IsNotExist(err) { if _, err := os.Stat(os.Getenv("HOME") + "/.twitch/oauth.json"); os.IsNotExist(err) {
rgb.YPrintf("[%s] Warning %s doesn't exist, trying %s next!\n", timeStamp(), os.Getenv("HOME")+"/.twitch/oauth.json", "/etc/twitch/oauth.json"); rgb.YPrintf("[%s] Warning %s doesn't exist, trying %s next!\n", irc.TimeStamp(), os.Getenv("HOME")+"/.twitch/oauth.json", "/etc/twitch/oauth.json")
if _, err := os.Stat("/etc/twitch/oauth.json"); os.IsNotExist(err) { if _, err := os.Stat("/etc/twitch/oauth.json"); os.IsNotExist(err) {
rgb.YPrintf("[%s] Error %s doesn't exist either, bailing!\n", timeStamp(), "/etc/twitch/oauth.json"); rgb.YPrintf("[%s] Error %s doesn't exist either, bailing!\n", irc.TimeStamp(), "/etc/twitch/oauth.json")
os.Exit(1); os.Exit(1)
} }
oauthPath = "/etc/twitch/oauth.json"; oauthPath = "/etc/twitch/oauth.json"
} else { } else {
oauthPath = os.Getenv("HOME")+"/.twitch/oauth.json"; oauthPath = os.Getenv("HOME") + "/.twitch/oauth.json"
} }
} }
// Replace the channel name, bot name, and the path to the private directory with your respective // Replace the channel name, bot name, and the path to the private directory with your respective
// values. // values.
myBot := kardBot{ myBot := irc.KardBot{
Channel: "imartynontwitch", Channel: "imartynontwitch",
MsgRate: time.Duration(20/30) * time.Millisecond, MsgRate: time.Duration(20/30) * time.Millisecond,
Name: "Karaokards", Name: "Karaokards",
Port: "6667", Port: "6667",
PrivatePath: oauthPath, PrivatePath: oauthPath,
Server: "irc.chat.twitch.tv", Server: "irc.chat.twitch.tv",
Prompts: selectablePrompts,
} }
myBot.Start() myBot.Start()
} }