Compare commits
No commits in common. "cf5f2e65986fdb7216b0f522cbbe9fc5e3cb349a" and "900fd69a59d8cc50b556e9c28fb7b2c9492db2e2" have entirely different histories.
cf5f2e6598
...
900fd69a59
|
@ -0,0 +1,24 @@
|
||||||
|
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
|
|
@ -12,4 +12,3 @@
|
||||||
# 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
|
|
|
@ -0,0 +1,12 @@
|
||||||
|
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"]
|
|
@ -1,63 +0,0 @@
|
||||||
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
|
|
|
@ -1,13 +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/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"]
|
|
|
@ -1,181 +0,0 @@
|
||||||
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, I’m or I’ve",
|
|
||||||
"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",
|
|
||||||
"I’m 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",
|
|
||||||
"I’ve seen them in real life",
|
|
||||||
"A beautiful love story",
|
|
||||||
"OK, this is just ridiculous",
|
|
||||||
"Artist begins with B",
|
|
||||||
"Artist begins with D",
|
|
||||||
"They’re dead :(",
|
|
||||||
"Artist begins with L",
|
|
||||||
"Amazing hair",
|
|
||||||
"Artist begins with J"}
|
|
|
@ -1,238 +0,0 @@
|
||||||
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.
444
main.go
444
main.go
|
@ -1,26 +1,431 @@
|
||||||
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, I’m or I’ve",
|
||||||
|
"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",
|
||||||
|
"I’m 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",
|
||||||
|
"I’ve seen them in real life",
|
||||||
|
"A beautiful love story",
|
||||||
|
"OK, this is just ridiculous",
|
||||||
|
"Artist begins with B",
|
||||||
|
"Artist begins with D",
|
||||||
|
"They’re 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 {
|
||||||
|
|
||||||
|
@ -35,53 +440,52 @@ func readBonusStrings() []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)
|
||||||
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 builtins.Karaokards {
|
for _, val := range 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", irc.TimeStamp(), os.Getenv("HOME")+"/.twitch/oauth.json", "/etc/twitch/oauth.json")
|
rgb.YPrintf("[%s] Warning %s doesn't exist, trying %s next!\n", 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", irc.TimeStamp(), "/etc/twitch/oauth.json")
|
rgb.YPrintf("[%s] Error %s doesn't exist either, bailing!\n", 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 := irc.KardBot{
|
myBot := 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()
|
||||||
}
|
}
|
Loading…
Reference in New Issue