now using go-twitch-irc and simplified the bot a lot

This commit is contained in:
gempir 2017-04-14 17:43:12 +02:00
parent dc3a4f721a
commit bf56fc2c47
14 changed files with 205 additions and 595 deletions

1
.gitignore vendored
View file

@ -1,6 +1,5 @@
gempbotgo
vendor/
glide.lock
.idea/
config.json
logs/

View file

@ -2,7 +2,6 @@ PACKAGES = $(shell go list ./... | grep -v /vendor/)
build:
glide install
glide update
go build
install:

View file

@ -1,153 +0,0 @@
package command
import (
"fmt"
"github.com/gempir/gempbotgo/modules"
"github.com/gempir/gempbotgo/twitch"
"github.com/op/go-logging"
"strings"
"time"
)
type Handler struct {
admin string
bot *twitch.Bot
startTime time.Time
log logging.Logger
}
func NewHandler(admin string, bot *twitch.Bot, startTime time.Time, apiUser string, apiKey string, logger logging.Logger) Handler {
logger = logger
return Handler{
admin: strings.ToLower(admin),
bot: bot,
startTime: startTime,
log: logger,
}
}
func (h *Handler) HandleCommand(msg twitch.Message) error {
switch msg.Command.Name {
case "!status":
h.handleStatusCommand(msg)
break
}
return nil
}
func (h *Handler) handleStatusCommand(msg twitch.Message) {
if msg.Username != h.admin {
return
}
uptime := formatDiff(diff(h.startTime, time.Now()))
h.bot.Say(msg.Channel, h.admin+", uptime: "+uptime, modules.STATUS)
}
func formatDiff(years, months, days, hours, mins, secs int) string {
since := ""
if years > 0 {
switch years {
case 1:
since += fmt.Sprintf("%d year ", years)
break
default:
since += fmt.Sprintf("%d years ", years)
break
}
}
if months > 0 {
switch months {
case 1:
since += fmt.Sprintf("%d month ", months)
break
default:
since += fmt.Sprintf("%d months ", months)
break
}
}
if days > 0 {
switch days {
case 1:
since += fmt.Sprintf("%d day ", days)
break
default:
since += fmt.Sprintf("%d days ", days)
break
}
}
if hours > 0 {
switch hours {
case 1:
since += fmt.Sprintf("%d hour ", hours)
break
default:
since += fmt.Sprintf("%d hours ", hours)
break
}
}
if mins > 0 && days == 0 && months == 0 && years == 0 {
switch mins {
case 1:
since += fmt.Sprintf("%d min ", mins)
break
default:
since += fmt.Sprintf("%d mins ", mins)
break
}
}
if secs > 0 && days == 0 && months == 0 && years == 0 && hours == 0 {
switch secs {
case 1:
since += fmt.Sprintf("%d sec ", secs)
break
default:
since += fmt.Sprintf("%d secs ", secs)
break
}
}
return strings.TrimSpace(since)
}
func diff(a, b time.Time) (year, month, day, hour, min, sec int) {
if a.After(b) {
a, b = b, a
}
y1, M1, d1 := a.Date()
y2, M2, d2 := b.Date()
h1, m1, s1 := a.Clock()
h2, m2, s2 := b.Clock()
year = int(y2 - y1)
month = int(M2 - M1)
day = int(d2 - d1)
hour = int(h2 - h1)
min = int(m2 - m1)
sec = int(s2 - s1)
// Normalize negative values
if sec < 0 {
sec += 60
min--
}
if min < 0 {
min += 60
hour--
}
if hour < 0 {
hour += 24
day--
}
if day < 0 {
// days in month:
t := time.Date(y1, M1, 32, 0, 0, 0, 0, time.UTC)
day += 32 - t.Day()
month--
}
if month < 0 {
month += 12
year--
}
return
}

View file

@ -1,32 +0,0 @@
package config
import (
"github.com/gempir/gempbotgo/modules"
"gopkg.in/redis.v5"
)
type UserConfig struct {
rClient redis.Client
}
func NewUserConfig(rClient redis.Client) UserConfig {
return UserConfig{
rClient: rClient,
}
}
func (uCfg *UserConfig) IsEnabled(channel, key string) bool {
if key == modules.STATUS {
return true
}
res, err := uCfg.rClient.HGet(channel+":config", key).Result()
if err != nil {
return false
}
if res == "true" || res == "1" {
return true
}
return false
}

View file

@ -2,16 +2,14 @@ package filelog
import (
"fmt"
"github.com/gempir/gempbotgo/twitch"
"os"
"strings"
"github.com/gempir/go-twitch-irc"
)
func (l *Logger) LogMessageForChannel(msg twitch.Message) error {
year := msg.Time.Year()
month := msg.Time.Month()
channel := strings.Replace(msg.Channel.Name, "#", "", 1)
day := msg.Time.Day()
func (l *Logger) LogMessageForChannel(channel string, user twitch.User, message twitch.Message) error {
year := message.Time.Year()
month := message.Time.Month()
day := message.Time.Day()
err := os.MkdirAll(fmt.Sprintf(l.logPath+"%s/%d/%s/%d", channel, year, month, day), 0755)
if err != nil {
return err
@ -24,7 +22,7 @@ func (l *Logger) LogMessageForChannel(msg twitch.Message) error {
}
defer file.Close()
contents := fmt.Sprintf("[%s] %s: %s\r\n", msg.Time.Format("2006-01-2 15:04:05"), msg.Username, msg.Text)
contents := fmt.Sprintf("[%s] %s: %s\r\n", message.Time.Format("2006-01-2 15:04:05"), user.Username, message.Text)
if _, err = file.WriteString(contents); err != nil {
return err
}

View file

@ -2,9 +2,8 @@ package filelog
import (
"fmt"
"github.com/gempir/gempbotgo/twitch"
"os"
"strings"
"github.com/gempir/go-twitch-irc"
)
type Logger struct {
@ -17,15 +16,14 @@ func NewFileLogger(logPath string) Logger {
}
}
func (l *Logger) LogMessageForUser(msg twitch.Message) error {
year := msg.Time.Year()
month := msg.Time.Month()
channel := strings.Replace(msg.Channel.Name, "#", "", 1)
func (l *Logger) LogMessageForUser(channel string, user twitch.User, message twitch.Message) error {
year := message.Time.Year()
month := message.Time.Month()
err := os.MkdirAll(fmt.Sprintf(l.logPath+"%s/%d/%s/", channel, year, month), 0755)
if err != nil {
return err
}
filename := fmt.Sprintf(l.logPath+"%s/%d/%s/%s.txt", channel, year, month, msg.Username)
filename := fmt.Sprintf(l.logPath+"%s/%d/%s/%s.txt", channel, year, month, user.Username)
file, err := os.OpenFile(filename, os.O_APPEND|os.O_WRONLY|os.O_CREATE, 0755)
if err != nil {
@ -33,7 +31,7 @@ func (l *Logger) LogMessageForUser(msg twitch.Message) error {
}
defer file.Close()
contents := fmt.Sprintf("[%s] %s: %s\r\n", msg.Time.Format("2006-01-2 15:04:05"), msg.Username, msg.Text)
contents := fmt.Sprintf("[%s] %s: %s\r\n", message.Time.Format("2006-01-2 15:04:05"), user.Username, message.Text)
if _, err = file.WriteString(contents); err != nil {
return err
}

59
glide.lock generated Normal file
View file

@ -0,0 +1,59 @@
hash: 3d70255a398ee1bb9a350b617330de4610870fd94e2aabff1b6af6d7e979f86f
updated: 2017-04-14T17:25:52.75741782+02:00
imports:
- name: github.com/CleverbotIO/go-cleverbot.io
version: 2b334e3eb17cf9556f3d9dc1d42eddc7b2e37007
- name: github.com/gempir/go-twitch-irc
version: e2aaa1468dee36fd689f30f56ac0ce2654f45622
- name: github.com/labstack/echo
version: 948d607539e92f3974d8f24951c8975d7342e24a
- name: github.com/labstack/gommon
version: e8995fb26e646187d33cff439b18609cfba23088
subpackages:
- color
- log
- name: github.com/mattn/go-colorable
version: acb9493f2794fd0f820de7a27a217dafbb1b65ea
- name: github.com/mattn/go-isatty
version: 57fdcb988a5c543893cc61bce354a6e24ab70022
- name: github.com/op/go-logging
version: b2cb9fa56473e98db8caba80237377e83fe44db5
- name: github.com/stretchr/testify
version: 69483b4bd14f5845b5a1e55bca19e954e827f1d0
subpackages:
- assert
- name: github.com/valyala/bytebufferpool
version: e746df99fe4a3986f4d4f79e13c1e0117ce9c2f7
- name: github.com/valyala/fasttemplate
version: dcecefd839c4193db0d35b88ec65b4c12d360ab0
- name: golang.org/x/crypto
version: 728b753d0135da6801d45a38e6f43ff55779c5c2
subpackages:
- acme
- acme/autocert
- name: golang.org/x/net
version: a6577fac2d73be281a500b310739095313165611
subpackages:
- context
- context/ctxhttp
- name: golang.org/x/sys
version: 99f16d856c9836c42d24e7ab64ea72916925fa97
subpackages:
- unix
- name: gopkg.in/redis.v5
version: a16aeec10ff407b1e7be6dd35797ccf5426ef0f0
subpackages:
- internal
- internal/consistenthash
- internal/hashtag
- internal/pool
- internal/proto
testImports:
- name: github.com/davecgh/go-spew
version: 6d212800a42e8ab5c146b8ace3490ee17e5225f9
subpackages:
- spew
- name: github.com/pmezard/go-difflib
version: d8ed2627bdf02c080bf22230dbb337003b7aba2d
subpackages:
- difflib

View file

@ -9,3 +9,5 @@ import:
- package: github.com/labstack/echo
version: ^3.1.0-rc.1
- package: github.com/CleverbotIO/go-cleverbot.io
- package: github.com/gempir/go-twitch-irc
version: ~0.2.0

161
main.go
View file

@ -10,10 +10,10 @@ import (
"gopkg.in/redis.v5"
"github.com/gempir/gempbotgo/api"
"github.com/gempir/gempbotgo/command"
"github.com/gempir/gempbotgo/config"
"github.com/gempir/gempbotgo/filelog"
"github.com/gempir/gempbotgo/twitch"
"github.com/gempir/go-twitch-irc"
"fmt"
"strings"
)
var (
@ -37,7 +37,6 @@ type sysConfig struct {
var (
fileLogger filelog.Logger
cmdHandler command.Handler
)
func main() {
@ -49,55 +48,51 @@ func main() {
logger.Fatal(err)
}
apiServer := api.NewServer(cfg.APIPort, cfg.LogPath)
go apiServer.Init()
rClient := redis.NewClient(&redis.Options{
Addr: cfg.RedisAddress,
Password: cfg.RedisPassword,
DB: cfg.RedisDatabase,
})
apiServer := api.NewServer(cfg.APIPort, cfg.LogPath)
go apiServer.Init()
userConfig := config.NewUserConfig(*rClient)
bot := twitch.NewBot(cfg.IrcAddress, cfg.IrcUser, cfg.IrcToken, userConfig, *rClient, logger)
go func() {
err := bot.CreatePersistentConnection()
if err != nil {
logger.Error(err.Error())
}
}()
twitchClient := twitch.NewClient(cfg.IrcUser, cfg.IrcToken)
twitchClient.SetIrcAddress(cfg.IrcAddress)
fileLogger = filelog.NewFileLogger(cfg.LogPath)
cmdHandler = command.NewHandler(cfg.Admin, &bot, startTime, cfg.CleverBotUser, cfg.CleverBotKey, logger)
for msg := range bot.Messages {
val, _ := rClient.HGetAll("channels").Result()
for channelStr := range val {
fmt.Println("Joining " + channelStr)
go twitchClient.Join(strings.TrimPrefix(channelStr, "#"))
}
if msg.Type == twitch.PRIVMSG || msg.Type == twitch.CLEARCHAT {
twitchClient.OnNewMessage(func(channel string, user twitch.User, message twitch.Message) {
if message.Type == twitch.PRIVMSG || message.Type == twitch.CLEARCHAT {
go func() {
err := fileLogger.LogMessageForUser(msg)
err := fileLogger.LogMessageForUser(channel, user, message)
if err != nil {
logger.Error(err.Error())
}
}()
go func() {
err := fileLogger.LogMessageForChannel(msg)
err := fileLogger.LogMessageForChannel(channel, user, message)
if err != nil {
logger.Error(err.Error())
}
}()
if msg.Command.IsCommand {
go func() {
err := cmdHandler.HandleCommand(msg)
if err != nil {
logger.Error(err.Error())
}
}()
if user.Username == cfg.Admin && strings.HasPrefix(message.Text, "!status") {
uptime := formatDiff(diff(startTime, time.Now()))
twitchClient.Say(channel, cfg.Admin+", uptime: "+uptime)
}
}
}
})
fmt.Println(twitchClient.Connect())
}
func initLogger() logging.Logger {
@ -129,3 +124,111 @@ func unmarshalConfig(file []byte) (sysConfig, error) {
}
return cfg, nil
}
func formatDiff(years, months, days, hours, mins, secs int) string {
since := ""
if years > 0 {
switch years {
case 1:
since += fmt.Sprintf("%d year ", years)
break
default:
since += fmt.Sprintf("%d years ", years)
break
}
}
if months > 0 {
switch months {
case 1:
since += fmt.Sprintf("%d month ", months)
break
default:
since += fmt.Sprintf("%d months ", months)
break
}
}
if days > 0 {
switch days {
case 1:
since += fmt.Sprintf("%d day ", days)
break
default:
since += fmt.Sprintf("%d days ", days)
break
}
}
if hours > 0 {
switch hours {
case 1:
since += fmt.Sprintf("%d hour ", hours)
break
default:
since += fmt.Sprintf("%d hours ", hours)
break
}
}
if mins > 0 && days == 0 && months == 0 && years == 0 {
switch mins {
case 1:
since += fmt.Sprintf("%d min ", mins)
break
default:
since += fmt.Sprintf("%d mins ", mins)
break
}
}
if secs > 0 && days == 0 && months == 0 && years == 0 && hours == 0 {
switch secs {
case 1:
since += fmt.Sprintf("%d sec ", secs)
break
default:
since += fmt.Sprintf("%d secs ", secs)
break
}
}
return strings.TrimSpace(since)
}
func diff(a, b time.Time) (year, month, day, hour, min, sec int) {
if a.After(b) {
a, b = b, a
}
y1, M1, d1 := a.Date()
y2, M2, d2 := b.Date()
h1, m1, s1 := a.Clock()
h2, m2, s2 := b.Clock()
year = int(y2 - y1)
month = int(M2 - M1)
day = int(d2 - d1)
hour = int(h2 - h1)
min = int(m2 - m1)
sec = int(s2 - s1)
// Normalize negative values
if sec < 0 {
sec += 60
min--
}
if min < 0 {
min += 60
hour--
}
if hour < 0 {
hour += 24
day--
}
if day < 0 {
// days in month:
t := time.Date(y1, M1, 32, 0, 0, 0, 0, time.UTC)
day += 32 - t.Day()
month--
}
if month < 0 {
month += 12
year--
}
return
}

View file

@ -1,5 +0,0 @@
package modules
const (
STATUS = "STATUS"
)

View file

@ -1,139 +0,0 @@
package twitch
import (
"bufio"
"fmt"
"github.com/gempir/gempbotgo/config"
"github.com/op/go-logging"
"gopkg.in/redis.v5"
"io/ioutil"
"net"
"net/http"
"net/textproto"
"strings"
)
var (
logger logging.Logger
)
type Bot struct {
Messages chan Message
ircAddress string
ircUser string
ircToken string
userConfig config.UserConfig
rClient redis.Client
logger logging.Logger
connection net.Conn
}
func NewBot(ircAddress string, ircUser string, ircToken string, uCfg config.UserConfig, rClient redis.Client, loggerMain logging.Logger) Bot {
channels := make(map[Channel]bool)
channels[NewChannel(ircUser)] = true
logger = logger
return Bot{
Messages: make(chan Message),
ircAddress: ircAddress,
ircUser: strings.ToLower(ircUser),
ircToken: ircToken,
userConfig: uCfg,
rClient: rClient,
logger: loggerMain,
}
}
func (bot *Bot) Say(channel Channel, text string, responseType string) {
if bot.userConfig.IsEnabled(channel.Name, responseType) {
bot.send(fmt.Sprintf("PRIVMSG %s :%s", channel.Name, text))
}
}
func (bot *Bot) CreatePersistentConnection() error {
for {
conn, err := net.Dial("tcp", bot.ircAddress)
bot.connection = conn
if err != nil {
bot.logger.Error(err.Error())
return err
}
bot.setupConnection()
bot.joinDefault()
err = bot.readConnection(conn)
if err != nil {
bot.logger.Error("connection read error, redialing")
continue
}
}
return nil
}
func (bot *Bot) readConnection(conn net.Conn) error {
reader := bufio.NewReader(conn)
tp := textproto.NewReader(reader)
for {
line, err := tp.ReadLine()
if err != nil {
bot.logger.Error(err.Error())
return err
}
messages := strings.Split(line, "\r\n")
if len(messages) == 0 {
continue
}
for _, msg := range messages {
bot.handleLine(msg)
}
}
}
func (bot *Bot) setupConnection() {
bot.send(fmt.Sprintf("PASS %s", bot.ircToken))
bot.send(fmt.Sprintf("NICK %s", bot.ircUser))
bot.send("CAP REQ :twitch.tv/tags")
bot.send("CAP REQ :twitch.tv/commands")
bot.send(fmt.Sprintf("JOIN %s", "#"+bot.ircUser))
}
func (bot *Bot) send(line string) {
fmt.Fprint(bot.connection, line+"\r\n")
}
func (bot *Bot) handleLine(line string) {
if strings.HasPrefix(line, "PING") {
bot.send(fmt.Sprintf(strings.Replace(line, "PING", "PONG", 1)))
}
if strings.HasPrefix(line, "@") {
bot.Messages <- *bot.parseMessage(line)
}
}
func (bot *Bot) joinDefault() {
val, _ := bot.rClient.HGetAll("channels").Result()
for channelStr := range val {
channel := NewChannel(channelStr)
go bot.join(channel)
}
}
func (bot *Bot) join(channel Channel) {
bot.send(fmt.Sprintf("JOIN %s", channel.Name))
}
func (bot *Bot) httpRequest(url string) ([]byte, error) {
response, err := http.Get(url)
if err != nil {
return nil, err
}
defer response.Body.Close()
contents, err := ioutil.ReadAll(response.Body)
if err != nil {
bot.logger.Error(err.Error())
return nil, err
}
return contents, nil
}

View file

@ -1,16 +0,0 @@
package twitch
import "strings"
type Channel struct {
Name string
}
func NewChannel(channel string) Channel {
if !strings.HasPrefix(channel, "#") {
channel = "#" + channel
}
return Channel{
Name: strings.ToLower(channel),
}
}

View file

@ -1,7 +0,0 @@
package twitch
type Command struct {
IsCommand bool
Name string
Args []string
}

View file

@ -1,196 +0,0 @@
package twitch
import (
"fmt"
"strconv"
"strings"
"time"
)
type msgType int
const (
PRIVMSG msgType = iota + 1
CLEARCHAT
RANDOM
EMOTE = "EMOTE"
)
type Message struct {
Type msgType
Time time.Time `json:"time"`
Channel Channel `json:"channel"`
Username string `json:"username"`
DisplayName string `json:"displayName"`
UserType string `json:"userType"`
Color string `json:"color"`
Badges map[string]int `json:"badges"`
Emotes []*Emote `json:"emotes"`
Tags map[string]string `json:"tags"`
Text string `json:"text"`
Command Command
}
type Emote struct {
Name string `json:"name"`
ID string `json:"id"`
Type string `json:"type"`
Count int `json:"count"`
}
func (bot *Bot) parseMessage(line string) *Message {
if !strings.HasPrefix(line, "@") {
return &Message{
Text: line,
}
}
spl := strings.SplitN(line, " :", 3)
if len(spl) < 3 {
return &Message{
Text: line,
}
}
tags, middle, text := spl[0], spl[1], spl[2]
if strings.HasPrefix(text, "\u0001ACTION") {
text = text[8 : len(text)-1]
}
msg := &Message{
Time: time.Now(),
Text: text,
Tags: map[string]string{},
}
parseMiddle(msg, middle)
parseTags(msg, tags[1:])
if msg.Type == CLEARCHAT {
msg.Username = "twitch"
targetUser := msg.Text
seconds, _ := strconv.Atoi(msg.Tags["ban-duration"])
msg.Text = fmt.Sprintf("%s was timed out for %s: %s",
targetUser,
time.Duration(time.Duration(seconds)*time.Second),
msg.Tags["ban-reason"])
}
msg.Command = parseCommand(msg.Text)
return msg
}
func parseMiddle(msg *Message, middle string) {
for i, c := range middle {
if c == '!' {
msg.Username = middle[:i]
middle = middle[i:]
}
}
start := -1
for i, c := range middle {
if c == ' ' {
if start == -1 {
start = i + 1
} else {
typ := middle[start:i]
switch typ {
case "PRIVMSG":
msg.Type = PRIVMSG
case "CLEARCHAT":
msg.Type = CLEARCHAT
default:
msg.Type = RANDOM
}
middle = middle[i:]
}
}
}
for i, c := range middle {
if c == '#' {
msg.Channel = NewChannel(middle[i+1:])
}
}
}
func parseTags(msg *Message, tagsRaw string) {
tags := strings.Split(tagsRaw, ";")
for _, tag := range tags {
spl := strings.SplitN(tag, "=", 2)
if len(spl) < 2 {
return
}
value := strings.Replace(spl[1], "\\:", ";", -1)
value = strings.Replace(value, "\\s", " ", -1)
value = strings.Replace(value, "\\\\", "\\", -1)
switch spl[0] {
case "badges":
msg.Badges = parseBadges(value)
case "color":
msg.Color = value
case "display-name":
msg.DisplayName = value
case "emotes":
msg.Emotes = parseTwitchEmotes(value, msg.Text)
case "user-type":
msg.UserType = value
default:
msg.Tags[spl[0]] = value
}
}
}
func parseBadges(badges string) map[string]int {
m := map[string]int{}
spl := strings.Split(badges, ",")
for _, badge := range spl {
s := strings.SplitN(badge, "/", 2)
if len(s) < 2 {
continue
}
n, _ := strconv.Atoi(s[1])
m[s[0]] = n
}
return m
}
func parseTwitchEmotes(emoteTag, text string) []*Emote {
emotes := []*Emote{}
if emoteTag == "" {
return emotes
}
runes := []rune(text)
emoteSlice := strings.Split(emoteTag, "/")
for i := range emoteSlice {
spl := strings.Split(emoteSlice[i], ":")
pos := strings.Split(spl[1], ",")
sp := strings.Split(pos[0], "-")
start, _ := strconv.Atoi(sp[0])
end, _ := strconv.Atoi(sp[1])
id := spl[0]
e := &Emote{
Type: EMOTE,
ID: id,
Count: strings.Count(emoteSlice[i], "-"),
Name: string(runes[start : end+1]),
}
emotes = append(emotes, e)
}
return emotes
}
func parseCommand(text string) Command {
cmd := new(Command)
if !strings.HasPrefix(text, "!") {
cmd.IsCommand = false
return *cmd
}
cmd.IsCommand = true
argsFull := strings.Split(text, " ")
cmd.Name = argsFull[0]
args := append(argsFull[:0], argsFull[0+1:]...)
cmd.Args = args
return *cmd
}