Initial commit.
commit
bbd531ef5c
|
@ -0,0 +1,49 @@
|
||||||
|
# Binaries
|
||||||
|
**/*.o
|
||||||
|
**/*.a
|
||||||
|
**/*.so
|
||||||
|
**/*.dll
|
||||||
|
**/*.exe
|
||||||
|
**/*.test
|
||||||
|
**/*.prof
|
||||||
|
embot
|
||||||
|
|
||||||
|
# Folders
|
||||||
|
_obj
|
||||||
|
_test
|
||||||
|
|
||||||
|
# Architecture specific extensions/prefixes
|
||||||
|
**/*.[568vq]
|
||||||
|
**/[568vq].out
|
||||||
|
|
||||||
|
# cgo
|
||||||
|
**/*.cgo1.go
|
||||||
|
**/*.cgo2.c
|
||||||
|
**/_cgo_defun.c
|
||||||
|
**/_cgo_gotypes.go
|
||||||
|
**/_cgo_export.*
|
||||||
|
|
||||||
|
# Unit tests
|
||||||
|
**/*_test.go
|
||||||
|
|
||||||
|
# Configurations
|
||||||
|
config.yml
|
||||||
|
secrets.yml
|
||||||
|
|
||||||
|
# Scripts
|
||||||
|
**/*.bat
|
||||||
|
|
||||||
|
**/_testmain.go
|
||||||
|
|
||||||
|
# Docker
|
||||||
|
**/Dockerfile
|
||||||
|
|
||||||
|
# Ignores
|
||||||
|
**/*.*ignore
|
||||||
|
|
||||||
|
# Documentation
|
||||||
|
LICENSE
|
||||||
|
**/*.md
|
||||||
|
|
||||||
|
# Source code metadata
|
||||||
|
.git
|
|
@ -0,0 +1,31 @@
|
||||||
|
# Binaries
|
||||||
|
*.o
|
||||||
|
*.a
|
||||||
|
*.so
|
||||||
|
*.dll
|
||||||
|
*.exe
|
||||||
|
*.test
|
||||||
|
*.prof
|
||||||
|
embot
|
||||||
|
|
||||||
|
# Folders
|
||||||
|
_obj
|
||||||
|
_test
|
||||||
|
|
||||||
|
# Architecture specific extensions/prefixes
|
||||||
|
*.[568vq]
|
||||||
|
[568vq].out
|
||||||
|
|
||||||
|
# cgo
|
||||||
|
*.cgo1.go
|
||||||
|
*.cgo2.c
|
||||||
|
_cgo_defun.c
|
||||||
|
_cgo_gotypes.go
|
||||||
|
_cgo_export.*
|
||||||
|
|
||||||
|
# Configurations
|
||||||
|
config.yml
|
||||||
|
secrets.yml
|
||||||
|
|
||||||
|
_testmain.go
|
||||||
|
|
|
@ -0,0 +1,16 @@
|
||||||
|
[submodule "vendor/github.com/icedream/go-footballdata"]
|
||||||
|
path = vendor/github.com/icedream/go-footballdata
|
||||||
|
url = https://github.com/icedream/go-footballdata.git
|
||||||
|
branch = develop
|
||||||
|
[submodule "vendor/github.com/stretchr/testify"]
|
||||||
|
path = vendor/github.com/stretchr/testify
|
||||||
|
url = https://github.com/stretchr/testify.git
|
||||||
|
[submodule "vendor/github.com/helmbold/richgo"]
|
||||||
|
path = vendor/github.com/helmbold/richgo
|
||||||
|
url = https://github.com/helmbold/richgo.git
|
||||||
|
[submodule "vendor/github.com/thoj/go-ircevent"]
|
||||||
|
path = vendor/github.com/thoj/go-ircevent
|
||||||
|
url = https://github.com/thoj/go-ircevent.git
|
||||||
|
[submodule "vendor/github.com/patrickmn/go-cache"]
|
||||||
|
path = vendor/github.com/patrickmn/go-cache
|
||||||
|
url = https://github.com/patrickmn/go-cache.git
|
|
@ -0,0 +1,13 @@
|
||||||
|
FROM golang:1.8
|
||||||
|
|
||||||
|
RUN mkdir -p /go/src/app
|
||||||
|
WORKDIR /go/src/app
|
||||||
|
|
||||||
|
COPY . /go/src/app
|
||||||
|
RUN \
|
||||||
|
mkdir -p "$GOPATH/src/github.com/icedream" &&\
|
||||||
|
ln -sf /go/src/app "$GOPATH/src/github.com/icedream/embot" &&\
|
||||||
|
go-wrapper download &&\
|
||||||
|
go-wrapper install
|
||||||
|
|
||||||
|
CMD ["go-wrapper", "run"]
|
|
@ -0,0 +1,461 @@
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"log"
|
||||||
|
"math"
|
||||||
|
"sort"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"net/http"
|
||||||
|
|
||||||
|
"github.com/thoj/go-ircevent"
|
||||||
|
"gopkg.in/alecthomas/kingpin.v2"
|
||||||
|
|
||||||
|
"github.com/icedream/embot/manager"
|
||||||
|
"github.com/icedream/go-footballdata"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
SoccerSeason_EuropeanChampionShipsFrance2016 = 444
|
||||||
|
|
||||||
|
day = 24 * time.Hour
|
||||||
|
week = 7 * day
|
||||||
|
)
|
||||||
|
|
||||||
|
type versus struct {
|
||||||
|
HomeTeam, AwayTeam string
|
||||||
|
}
|
||||||
|
|
||||||
|
func must(err error) {
|
||||||
|
if err == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
var debug bool
|
||||||
|
var useTLS bool
|
||||||
|
var server string
|
||||||
|
var password string
|
||||||
|
var timeout time.Duration
|
||||||
|
var pingFreq time.Duration
|
||||||
|
|
||||||
|
var allowInvite bool
|
||||||
|
|
||||||
|
var authToken string
|
||||||
|
|
||||||
|
nickname := "EMBot"
|
||||||
|
ident := "embot"
|
||||||
|
var nickservPw string
|
||||||
|
channels := []string{}
|
||||||
|
|
||||||
|
// IRC config
|
||||||
|
kingpin.Flag("nick", "The nickname.").Short('n').StringVar(&nickname)
|
||||||
|
kingpin.Flag("ident", "The ident.").Short('i').StringVar(&ident)
|
||||||
|
kingpin.Flag("debug", "Enables debug mode.").Short('d').BoolVar(&debug)
|
||||||
|
kingpin.Flag("tls", "Use TLS.").BoolVar(&useTLS)
|
||||||
|
kingpin.Flag("server", "The server to connect to.").Short('s').StringVar(&server)
|
||||||
|
kingpin.Flag("password", "The password to use for logging into the IRC server.").Short('p').StringVar(&password)
|
||||||
|
kingpin.Flag("timeout", "The timeout on the connection.").Short('t').DurationVar(&timeout)
|
||||||
|
kingpin.Flag("pingfreq", "The ping frequency.").DurationVar(&pingFreq)
|
||||||
|
kingpin.Flag("nickserv-pw", "NickServ password.").StringVar(&nickservPw)
|
||||||
|
kingpin.Flag("channel", "Channel to join. Can be used multiple times.").Short('c').StringsVar(&channels)
|
||||||
|
|
||||||
|
// football-data config
|
||||||
|
kingpin.Flag("footballdata-key", "The API key to use to access the YouTube API.").StringVar(&authToken)
|
||||||
|
|
||||||
|
// behavior config
|
||||||
|
kingpin.Flag("allow-invite", "Determines whether the bot can be invited to other IRC channels.").BoolVar(&allowInvite)
|
||||||
|
|
||||||
|
kingpin.Parse()
|
||||||
|
|
||||||
|
if len(nickname) == 0 {
|
||||||
|
log.Fatal("Nickname must be longer than 0 chars.")
|
||||||
|
}
|
||||||
|
if len(ident) == 0 {
|
||||||
|
log.Fatal("Ident must be longer than 0 chars.")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Load football data API client
|
||||||
|
footballData := footballdata.NewClient(http.DefaultClient)
|
||||||
|
footballData.SetToken(authToken)
|
||||||
|
|
||||||
|
// Manager
|
||||||
|
m := manager.NewManager()
|
||||||
|
|
||||||
|
// IRC
|
||||||
|
//conn := m.AntifloodIrcConn(irc.IRC(nickname, ident))
|
||||||
|
conn := irc.IRC(nickname, ident)
|
||||||
|
conn.Debug = debug
|
||||||
|
conn.VerboseCallbackHandler = conn.Debug
|
||||||
|
conn.UseTLS = useTLS
|
||||||
|
conn.Password = password
|
||||||
|
if timeout > time.Duration(0) {
|
||||||
|
conn.Timeout = timeout
|
||||||
|
}
|
||||||
|
if pingFreq > time.Duration(0) {
|
||||||
|
conn.PingFreq = pingFreq
|
||||||
|
}
|
||||||
|
|
||||||
|
updateTopics := func() {
|
||||||
|
// Get football data
|
||||||
|
if r, err := footballData.FixturesOfSoccerSeason(SoccerSeason_EuropeanChampionShipsFrance2016).Do(); err != nil {
|
||||||
|
log.Print(err)
|
||||||
|
} else {
|
||||||
|
currentMatches := []*footballdata.Fixture{}
|
||||||
|
lastMatches := []*footballdata.Fixture{}
|
||||||
|
nextMatches := []*footballdata.Fixture{}
|
||||||
|
for i, fixture := range r.Fixtures {
|
||||||
|
switch {
|
||||||
|
case fixture.Status == footballdata.FixtureStatus_InPlay ||
|
||||||
|
(fixture.Status == footballdata.FixtureStatus_Timed && fixture.Date.Sub(time.Now()).Minutes() < 10):
|
||||||
|
// This is a current match!
|
||||||
|
currentMatches = append(currentMatches, &r.Fixtures[i])
|
||||||
|
case fixture.Status == footballdata.FixtureStatus_Timed:
|
||||||
|
// This is an upcoming match!
|
||||||
|
add := true
|
||||||
|
for _, existingMatch := range nextMatches {
|
||||||
|
difference := math.Abs(fixture.Date.Sub(existingMatch.Date).Minutes())
|
||||||
|
if difference > 45 {
|
||||||
|
if fixture.Date.Before(existingMatch.Date) {
|
||||||
|
nextMatches = []*footballdata.Fixture{&r.Fixtures[i]}
|
||||||
|
}
|
||||||
|
add = false
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if !add {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
nextMatches = append(nextMatches, &r.Fixtures[i])
|
||||||
|
case fixture.Status == footballdata.FixtureStatus_Finished:
|
||||||
|
// This is a finished match!
|
||||||
|
add := true
|
||||||
|
for _, existingMatch := range lastMatches {
|
||||||
|
difference := math.Abs(fixture.Date.Sub(existingMatch.Date).Minutes())
|
||||||
|
if difference > 45 {
|
||||||
|
if fixture.Date.After(existingMatch.Date) {
|
||||||
|
lastMatches = []*footballdata.Fixture{&r.Fixtures[i]}
|
||||||
|
}
|
||||||
|
add = false
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if !add {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
lastMatches = append(lastMatches, &r.Fixtures[i])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, target := range channels {
|
||||||
|
oldTopic := m.GetTopic(target)
|
||||||
|
oldTopicParts := strings.Split(oldTopic, "|")
|
||||||
|
|
||||||
|
newtopicParts := []string{}
|
||||||
|
|
||||||
|
additionals := map[versus]string{}
|
||||||
|
|
||||||
|
for _, oldTopicPart := range oldTopicParts {
|
||||||
|
if r, err := parseTopicPart(oldTopicPart); err == nil {
|
||||||
|
if !r.HasMatches() {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
for _, match := range r.Matches {
|
||||||
|
vs := versus{match.HomeTeam, match.AwayTeam}
|
||||||
|
additionals[vs] = match.Additional
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, oldTopicPart := range oldTopicParts {
|
||||||
|
newTopicPart := ""
|
||||||
|
newTopicPartAdd := false
|
||||||
|
|
||||||
|
if topicPartInfo, err := parseTopicPart(oldTopicPart); err == nil {
|
||||||
|
switch strings.ToUpper(topicPartInfo.Prefix) {
|
||||||
|
case "LAST", "NOW":
|
||||||
|
newTopicPartInfo := topicPartInfo
|
||||||
|
newTopicPartInfo.Prefix = "Last"
|
||||||
|
newTopicPartInfo.Matches = []topicMatchInfo{}
|
||||||
|
|
||||||
|
fixtures := timeSortedMatches{}
|
||||||
|
|
||||||
|
if len(currentMatches) > 0 {
|
||||||
|
newTopicPartInfo.Prefix = "Now"
|
||||||
|
|
||||||
|
// Add current matches
|
||||||
|
fixtures = append(fixtures, currentMatches...)
|
||||||
|
} else {
|
||||||
|
// Add last matches only
|
||||||
|
fixtures = append(fixtures, lastMatches...)
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(fixtures) <= 0 {
|
||||||
|
// Use old data to keep this part of the topic
|
||||||
|
newTopicPartInfo = topicPartInfo
|
||||||
|
} else {
|
||||||
|
sort.Sort(fixtures)
|
||||||
|
for _, fixture := range fixtures {
|
||||||
|
newTopicMatchInfo := topicMatchInfo{
|
||||||
|
HomeTeam: fixture.HomeTeamName,
|
||||||
|
AwayTeam: fixture.AwayTeamName,
|
||||||
|
}
|
||||||
|
|
||||||
|
// Re-apply old additional info
|
||||||
|
if a, ok := additionals[versus{fixture.HomeTeamName, fixture.AwayTeamName}]; ok {
|
||||||
|
newTopicMatchInfo.Additional = a
|
||||||
|
}
|
||||||
|
|
||||||
|
if fixture.Status != footballdata.FixtureStatus_Timed {
|
||||||
|
// Real-time match scores
|
||||||
|
newTopicMatchInfo.Current = &topicScoreInfo{
|
||||||
|
HomeScore: int(fixture.Result.GoalsHomeTeam),
|
||||||
|
AwayScore: int(fixture.Result.GoalsAwayTeam),
|
||||||
|
}
|
||||||
|
|
||||||
|
// Half-time additional info
|
||||||
|
additionalScoreInfo := newTopicMatchInfo.AdditionalScoreInfo()
|
||||||
|
if additionalScoreInfo == nil {
|
||||||
|
additionalScoreInfo = new(topicAdditionalScoreInfo)
|
||||||
|
additionalScoreInfo.Pre = newTopicMatchInfo.Additional
|
||||||
|
}
|
||||||
|
if fixture.Result.HalfTime != nil {
|
||||||
|
additionalScoreInfo.HalfScore = fmt.Sprintf("%d:%d", fixture.Result.HalfTime.GoalsHomeTeam, fixture.Result.HalfTime.GoalsAwayTeam)
|
||||||
|
} else if fixture.Status == footballdata.FixtureStatus_InPlay &&
|
||||||
|
len(additionalScoreInfo.HalfScore) <= 0 &&
|
||||||
|
time.Now().Sub(fixture.Date).Minutes() >= 50 {
|
||||||
|
// API doesn't provide us with half-time data yet, just generate that data ourselves
|
||||||
|
additionalScoreInfo.HalfScore = fmt.Sprintf("%d:%d", fixture.Result.GoalsHomeTeam, fixture.Result.GoalsAwayTeam)
|
||||||
|
}
|
||||||
|
if fixture.Result.ExtraTime != nil {
|
||||||
|
additionalScoreInfo.IsAET = true
|
||||||
|
newTopicMatchInfo.Current = &topicScoreInfo{
|
||||||
|
HomeScore: int(fixture.Result.ExtraTime.GoalsHomeTeam),
|
||||||
|
AwayScore: int(fixture.Result.ExtraTime.GoalsAwayTeam),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if fixture.Result.PenaltyShootout != nil {
|
||||||
|
additionalScoreInfo.PenScore = fmt.Sprintf("%d:%d", fixture.Result.PenaltyShootout.GoalsHomeTeam, fixture.Result.PenaltyShootout.GoalsAwayTeam)
|
||||||
|
}
|
||||||
|
newTopicMatchInfo.Additional = additionalScoreInfo.String()
|
||||||
|
}
|
||||||
|
|
||||||
|
newTopicPartInfo.Matches = append(newTopicPartInfo.Matches, newTopicMatchInfo)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
newTopicPart = newTopicPartInfo.String()
|
||||||
|
newTopicPartAdd = true
|
||||||
|
case "NEXT":
|
||||||
|
newTopicPartInfo := topicPartInfo
|
||||||
|
newTopicPartInfo.Matches = []topicMatchInfo{}
|
||||||
|
|
||||||
|
fixtures := append(timeSortedMatches{}, nextMatches...)
|
||||||
|
|
||||||
|
if len(fixtures) <= 0 {
|
||||||
|
// Use old data to keep this part of the topic
|
||||||
|
if len(newTopicPartInfo.Text) <= 0 {
|
||||||
|
newTopicPartInfo.Text = "No match"
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
sort.Sort(fixtures)
|
||||||
|
for _, fixture := range fixtures {
|
||||||
|
newTopicMatchInfo := topicMatchInfo{
|
||||||
|
HomeTeam: fixture.HomeTeamName,
|
||||||
|
AwayTeam: fixture.AwayTeamName,
|
||||||
|
}
|
||||||
|
if a, ok := additionals[versus{fixture.HomeTeamName, fixture.AwayTeamName}]; ok {
|
||||||
|
newTopicMatchInfo.Additional = a
|
||||||
|
}
|
||||||
|
newTopicPartInfo.Matches = append(newTopicPartInfo.Matches, newTopicMatchInfo)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
newTopicPart = newTopicPartInfo.String()
|
||||||
|
newTopicPartAdd = true
|
||||||
|
default:
|
||||||
|
newTopicPart = topicPartInfo.String()
|
||||||
|
newTopicPartAdd = true
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
newTopicPart = strings.TrimSpace(oldTopicPart)
|
||||||
|
newTopicPartAdd = true
|
||||||
|
}
|
||||||
|
|
||||||
|
if newTopicPartAdd {
|
||||||
|
newtopicParts = append(newtopicParts, newTopicPart)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
newTopic := strings.Join(newtopicParts, " | ")
|
||||||
|
|
||||||
|
if oldTopic != newTopic {
|
||||||
|
conn.SendRawf("TOPIC %s :%s", target, newTopic)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
joinChan := make(chan string)
|
||||||
|
inviteChan := make(chan string)
|
||||||
|
updateTopicsChan := make(chan interface{})
|
||||||
|
|
||||||
|
// register callbacks
|
||||||
|
conn.AddCallback("001", func(e *irc.Event) { // handle RPL_WELCOME
|
||||||
|
// nickserv login
|
||||||
|
if len(nickservPw) > 0 {
|
||||||
|
conn.Privmsg("NickServ", "IDENTIFY "+nickservPw)
|
||||||
|
log.Print("Sent NickServ login request.")
|
||||||
|
}
|
||||||
|
|
||||||
|
// I am a bot! (+B user mode)
|
||||||
|
conn.Mode(conn.GetNick(), "+B-iw")
|
||||||
|
|
||||||
|
// Join configured channels
|
||||||
|
if len(channels) > 0 {
|
||||||
|
conn.Join(strings.Join(channels, ","))
|
||||||
|
}
|
||||||
|
})
|
||||||
|
conn.AddCallback("JOIN", func(e *irc.Event) {
|
||||||
|
// Is this JOIN not about us?
|
||||||
|
if !strings.EqualFold(e.Nick, conn.GetNick()) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
m.SaveTopic(e.Arguments[0], "")
|
||||||
|
|
||||||
|
// Asynchronous notification
|
||||||
|
select {
|
||||||
|
case joinChan <- e.Arguments[0]:
|
||||||
|
default:
|
||||||
|
}
|
||||||
|
})
|
||||||
|
conn.AddCallback("INVITE", func(e *irc.Event) {
|
||||||
|
// Allow invites?
|
||||||
|
if !allowInvite {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Is this INVITE not for us?
|
||||||
|
if !strings.EqualFold(e.Arguments[0], conn.GetNick()) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Asynchronous notification
|
||||||
|
select {
|
||||||
|
case inviteChan <- e.Arguments[1]:
|
||||||
|
default:
|
||||||
|
}
|
||||||
|
|
||||||
|
// We have been invited, autojoin!
|
||||||
|
go func(sourceNick string, targetChannel string) {
|
||||||
|
joinWaitLoop:
|
||||||
|
for {
|
||||||
|
select {
|
||||||
|
case channel := <-joinChan:
|
||||||
|
if strings.EqualFold(channel, targetChannel) {
|
||||||
|
// TODO - Thanks message
|
||||||
|
time.Sleep(1 * time.Second)
|
||||||
|
conn.Privmsgf(targetChannel, "Thanks for inviting me, %s! I am %s. I hope I can be of great help for everyone here in %s! :)", sourceNick, conn.GetNick(), targetChannel)
|
||||||
|
//time.Sleep(2 * time.Second)
|
||||||
|
//conn.Privmsg(targetChannel, "If you ever run into trouble with me (or find any bugs), please use the channel #MediaLink for contact on this IRC.")
|
||||||
|
break joinWaitLoop
|
||||||
|
}
|
||||||
|
case channel := <-inviteChan:
|
||||||
|
if strings.EqualFold(channel, targetChannel) {
|
||||||
|
break joinWaitLoop
|
||||||
|
}
|
||||||
|
case <-time.After(time.Minute):
|
||||||
|
log.Printf("WARNING: Timed out waiting for us to join %s as we got invited", targetChannel)
|
||||||
|
break joinWaitLoop
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}(e.Nick, e.Arguments[1])
|
||||||
|
conn.Join(e.Arguments[1])
|
||||||
|
})
|
||||||
|
conn.AddCallback("PRIVMSG", func(e *irc.Event) {
|
||||||
|
go func(event *irc.Event) {
|
||||||
|
//sender := event.Nick
|
||||||
|
target := event.Arguments[0]
|
||||||
|
isChannel := true
|
||||||
|
if strings.EqualFold(target, conn.GetNick()) {
|
||||||
|
// Private message to us!
|
||||||
|
target = event.Nick
|
||||||
|
isChannel = false
|
||||||
|
}
|
||||||
|
if strings.EqualFold(target, conn.GetNick()) {
|
||||||
|
// Emergency switch to avoid endless loop,
|
||||||
|
// dropping all messages from the bot to the bot!
|
||||||
|
log.Printf("BUG - Emergency switch, caught message from bot to bot: %s", event.Arguments)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
msg := stripIrcFormatting(event.Message())
|
||||||
|
|
||||||
|
log.Printf("<%s @ %s> %s", event.Nick, target, msg)
|
||||||
|
switch {
|
||||||
|
case !isChannel && strings.HasPrefix(msg, "updatetopics"):
|
||||||
|
updateTopicsChan <- nil
|
||||||
|
case strings.HasPrefix(msg, "!match"):
|
||||||
|
/*if r, err := footballData.FixturesOfSoccerSeason(SoccerSeason_EuropeanChampionShipsFrance2016).TimeFrame(1 * day).Do(); err != nil {
|
||||||
|
if s, err := tplString("error", err); err != nil {
|
||||||
|
log.Print(err)
|
||||||
|
} else {
|
||||||
|
conn.Privmsg(target, s)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
log.Printf("%+v", r)
|
||||||
|
for _, fixture := range r.Fixtures {
|
||||||
|
if s, err := tplString("match", fixture); err != nil {
|
||||||
|
log.Print(err)
|
||||||
|
} else {
|
||||||
|
conn.Privmsg(target, s)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}*/
|
||||||
|
}
|
||||||
|
}(e)
|
||||||
|
})
|
||||||
|
|
||||||
|
conn.AddCallback("TOPIC", func(e *irc.Event) {
|
||||||
|
channel := e.Arguments[0]
|
||||||
|
topic := e.Arguments[1]
|
||||||
|
log.Printf("Topic for %s is: %s", channel, topic)
|
||||||
|
m.SaveTopic(channel, topic)
|
||||||
|
})
|
||||||
|
conn.AddCallback("332", func(e *irc.Event) {
|
||||||
|
channel := e.Arguments[1]
|
||||||
|
topic := e.Arguments[2]
|
||||||
|
log.Printf("Topic for %s is: %s", channel, topic)
|
||||||
|
m.SaveTopic(channel, topic)
|
||||||
|
})
|
||||||
|
conn.AddCallback("331", func(e *irc.Event) {
|
||||||
|
channel := e.Arguments[1]
|
||||||
|
log.Printf("No topic set for %s.", channel)
|
||||||
|
m.SaveTopic(channel, "")
|
||||||
|
})
|
||||||
|
|
||||||
|
// connect
|
||||||
|
must(conn.Connect(server))
|
||||||
|
|
||||||
|
// Fetch realtime data regularly for topic
|
||||||
|
go func() {
|
||||||
|
for {
|
||||||
|
// Wait a minute before refreshing
|
||||||
|
select {
|
||||||
|
case <-time.After(30 * time.Second):
|
||||||
|
case <-updateTopicsChan:
|
||||||
|
}
|
||||||
|
|
||||||
|
updateTopics()
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
// listen for errors
|
||||||
|
log.Print("Now looping.")
|
||||||
|
conn.Loop()
|
||||||
|
}
|
|
@ -0,0 +1,45 @@
|
||||||
|
{{ define "error" }}
|
||||||
|
{{ bold -}}
|
||||||
|
{{ color 4 -}}
|
||||||
|
ERROR:
|
||||||
|
{{- reset }}
|
||||||
|
|
||||||
|
{{ . }}
|
||||||
|
{{ end }}
|
||||||
|
|
||||||
|
{{ define "match" }}
|
||||||
|
{{/*
|
||||||
|
Date time.Time
|
||||||
|
Status FixtureStatus = "IN_PLAY" | "FINISHED" | "TIMED"
|
||||||
|
Matchday uint16
|
||||||
|
HomeTeamName string
|
||||||
|
AwayTeamName string
|
||||||
|
Result FixtureResult
|
||||||
|
{
|
||||||
|
GoalsHomeTeam uint16
|
||||||
|
GoalsAwayTeam uint16
|
||||||
|
}
|
||||||
|
*/}}
|
||||||
|
|
||||||
|
{{ if or (eq .Status "IN_PLAY") (eq .Status "FINISHED") }}
|
||||||
|
{{ bold }}{{ .HomeTeamName }}{{ bold }}
|
||||||
|
{{ .Result.GoalsHomeTeam -}}
|
||||||
|
:
|
||||||
|
{{- .Result.GoalsAwayTeam }}
|
||||||
|
{{ bold }}{{ .AwayTeamName }}{{ bold }}
|
||||||
|
{{ else }}
|
||||||
|
{{ bold }}{{ .HomeTeamName }}{{ bold }}
|
||||||
|
vs
|
||||||
|
{{ bold }}{{ .AwayTeamName }}{{ bold }}
|
||||||
|
{{ end }}
|
||||||
|
|
|
||||||
|
{{ if eq .Status "IN_PLAY" }}
|
||||||
|
{{ playtime .Date }}
|
||||||
|
{{ else }}
|
||||||
|
{{ if eq .Status "FINISHED" }}
|
||||||
|
Match finished
|
||||||
|
{{ else }}
|
||||||
|
Match starts {{ ago .Date }}
|
||||||
|
{{ end }}
|
||||||
|
{{ end }}
|
||||||
|
{{ end }}
|
|
@ -0,0 +1,86 @@
|
||||||
|
package manager
|
||||||
|
|
||||||
|
import (
|
||||||
|
"crypto/sha512"
|
||||||
|
"fmt"
|
||||||
|
"log"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/thoj/go-ircevent"
|
||||||
|
|
||||||
|
cache "github.com/patrickmn/go-cache"
|
||||||
|
)
|
||||||
|
|
||||||
|
func (m *Manager) initAntiflood() {
|
||||||
|
m.cache = cache.New(1*time.Minute, 5*time.Second)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *Manager) TrackOutput(target, t string) (shouldNotSend bool) {
|
||||||
|
key := normalizeTextAntiflood(target, t)
|
||||||
|
|
||||||
|
if _, ok := m.cache.Get(key); ok {
|
||||||
|
// The URL has been used recently, should ignore
|
||||||
|
shouldNotSend = true
|
||||||
|
} else {
|
||||||
|
m.cache.Add(key, nil, cache.DefaultExpiration)
|
||||||
|
}
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *Manager) AntifloodIrcConn(c *irc.Connection) *ircConnectionProxy {
|
||||||
|
return &ircConnectionProxy{Connection: c, m: m}
|
||||||
|
}
|
||||||
|
|
||||||
|
func normalizeTextAntiflood(target, text string) string {
|
||||||
|
s := sha512.New()
|
||||||
|
s.Write([]byte(text))
|
||||||
|
return fmt.Sprintf("TEXT/%s/%X", strings.ToUpper(target), s.Sum([]byte{}))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Proxies several methods of the IRC connection in order to drop repeated messages
|
||||||
|
type ircConnectionProxy struct {
|
||||||
|
*irc.Connection
|
||||||
|
|
||||||
|
m *Manager
|
||||||
|
}
|
||||||
|
|
||||||
|
func (proxy *ircConnectionProxy) Action(target, message string) {
|
||||||
|
if proxy.m.TrackOutput(target, message) {
|
||||||
|
log.Printf("WARNING: Output antiflood triggered, dropping message for %s: %s", target, message)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
proxy.Connection.Action(target, message)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (proxy *ircConnectionProxy) Actionf(target, format string, a ...interface{}) {
|
||||||
|
proxy.Action(target, fmt.Sprintf(format, a...))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (proxy *ircConnectionProxy) Privmsg(target, message string) {
|
||||||
|
if proxy.m.TrackOutput(target, message) {
|
||||||
|
log.Printf("WARNING: Output antiflood triggered, dropping message for %s: %s", target, message)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
proxy.Connection.Privmsg(target, message)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (proxy *ircConnectionProxy) Privmsgf(target, format string, a ...interface{}) {
|
||||||
|
proxy.Privmsg(target, fmt.Sprintf(format, a...))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (proxy *ircConnectionProxy) Notice(target, message string) {
|
||||||
|
if proxy.m.TrackOutput(target, message) {
|
||||||
|
log.Printf("WARNING: Output antiflood triggered, dropping message for %s: %s", target, message)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
proxy.Connection.Notice(target, message)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (proxy *ircConnectionProxy) Noticef(target, format string, a ...interface{}) {
|
||||||
|
proxy.Notice(target, fmt.Sprintf(format, a...))
|
||||||
|
}
|
|
@ -0,0 +1,23 @@
|
||||||
|
package manager
|
||||||
|
|
||||||
|
import (
|
||||||
|
"sync"
|
||||||
|
|
||||||
|
"github.com/patrickmn/go-cache"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Manager struct {
|
||||||
|
// antiflood variables
|
||||||
|
cache *cache.Cache
|
||||||
|
|
||||||
|
// topic variables
|
||||||
|
topicStateLock sync.RWMutex
|
||||||
|
topicMap map[string]string
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewManager() *Manager {
|
||||||
|
m := new(Manager)
|
||||||
|
m.initAntiflood()
|
||||||
|
m.initTopic()
|
||||||
|
return m
|
||||||
|
}
|
|
@ -0,0 +1,22 @@
|
||||||
|
package manager
|
||||||
|
|
||||||
|
import "strings"
|
||||||
|
|
||||||
|
func (m *Manager) initTopic() {
|
||||||
|
m.topicMap = map[string]string{}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *Manager) GetTopic(channel string) (retval string) {
|
||||||
|
channel = strings.ToLower(channel)
|
||||||
|
m.topicStateLock.RLock()
|
||||||
|
defer m.topicStateLock.RUnlock()
|
||||||
|
retval, _ = m.topicMap[channel]
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *Manager) SaveTopic(channel string, topic string) {
|
||||||
|
channel = strings.ToLower(channel)
|
||||||
|
m.topicStateLock.Lock()
|
||||||
|
defer m.topicStateLock.Unlock()
|
||||||
|
m.topicMap[channel] = topic
|
||||||
|
}
|
|
@ -0,0 +1,16 @@
|
||||||
|
package main
|
||||||
|
|
||||||
|
import "github.com/icedream/go-footballdata"
|
||||||
|
|
||||||
|
type timeSortedMatches []*footballdata.Fixture
|
||||||
|
|
||||||
|
func (slice timeSortedMatches) Len() int {
|
||||||
|
return len(slice)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (slice timeSortedMatches) Less(i, j int) bool {
|
||||||
|
return slice[i].Date.Before(slice[j].Date)
|
||||||
|
}
|
||||||
|
func (slice timeSortedMatches) Swap(i, j int) {
|
||||||
|
slice[i], slice[j] = slice[j], slice[i]
|
||||||
|
}
|
|
@ -0,0 +1,121 @@
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"fmt"
|
||||||
|
"log"
|
||||||
|
"math"
|
||||||
|
"net/url"
|
||||||
|
"regexp"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
"text/template"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/dustin/go-humanize"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
compactNumUnits = []string{"", "k", "M"}
|
||||||
|
|
||||||
|
tplFuncMap = template.FuncMap{
|
||||||
|
// The name "title" is what the function will be called in the template text.
|
||||||
|
"color": func(num int) string {
|
||||||
|
return string(runeIrcColor) + strconv.Itoa(num)
|
||||||
|
},
|
||||||
|
"bcolor": func(fgNum, bgNum int) string {
|
||||||
|
return string(runeIrcColor) + strconv.Itoa(fgNum) + "," + strconv.Itoa(bgNum)
|
||||||
|
},
|
||||||
|
"bold": func() string {
|
||||||
|
return string(runeIrcBold)
|
||||||
|
},
|
||||||
|
"italic": func() string {
|
||||||
|
return string(runeIrcItalic)
|
||||||
|
},
|
||||||
|
"reset": func() string {
|
||||||
|
return string(runeIrcReset) + string(runeIrcColor)
|
||||||
|
},
|
||||||
|
"reverse": func() string {
|
||||||
|
return string(runeIrcReverse)
|
||||||
|
},
|
||||||
|
"underline": func() string {
|
||||||
|
return string(runeIrcUnderline)
|
||||||
|
},
|
||||||
|
"urlencode": func(s string) string {
|
||||||
|
return url.QueryEscape(s)
|
||||||
|
},
|
||||||
|
"yesno": func(yes string, no string, value bool) string {
|
||||||
|
if value {
|
||||||
|
return yes
|
||||||
|
}
|
||||||
|
|
||||||
|
return no
|
||||||
|
},
|
||||||
|
"excerpt": func(maxLength uint16, text string) string {
|
||||||
|
if len(text) > int(maxLength) {
|
||||||
|
return text[0:maxLength-1] + "\u2026"
|
||||||
|
}
|
||||||
|
return text
|
||||||
|
},
|
||||||
|
"comma": func(num uint64) string {
|
||||||
|
return humanize.Comma(int64(num))
|
||||||
|
},
|
||||||
|
"compactnum": func(num uint64) string {
|
||||||
|
// 1 => 0
|
||||||
|
// 1000 => 1
|
||||||
|
// 1000000 => 2
|
||||||
|
log10 := math.Floor(math.Log10(float64(num)) / 3)
|
||||||
|
|
||||||
|
// Cut to available units
|
||||||
|
cut := int(math.Min(float64(len(compactNumUnits)-1), log10))
|
||||||
|
|
||||||
|
numf := float64(num)
|
||||||
|
numf /= math.Pow10(cut * 3)
|
||||||
|
|
||||||
|
// Rounding
|
||||||
|
numf = math.Floor((numf*10)+.5) / 10
|
||||||
|
if numf >= 1000 {
|
||||||
|
numf /= 1000
|
||||||
|
if cut < len(compactNumUnits)-1 {
|
||||||
|
cut++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
unit := compactNumUnits[cut]
|
||||||
|
f := "%.1f%s"
|
||||||
|
if numf-math.Floor(numf) < 0.05 {
|
||||||
|
f = "%.0f%s"
|
||||||
|
}
|
||||||
|
|
||||||
|
return fmt.Sprintf(f, numf, unit)
|
||||||
|
},
|
||||||
|
"ago": func(t time.Time) string {
|
||||||
|
return humanize.Time(t)
|
||||||
|
},
|
||||||
|
"size": func(s uint64) string {
|
||||||
|
return humanize.Bytes(s)
|
||||||
|
},
|
||||||
|
"playtime": func(s time.Duration) string {
|
||||||
|
return fmt.Sprintf("%d", int(math.Floor(s.Minutes()))) + "'"
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
ircTpl = template.Must(
|
||||||
|
template.New("").
|
||||||
|
Funcs(tplFuncMap).
|
||||||
|
ParseGlob("*.tpl"))
|
||||||
|
|
||||||
|
rxInsignificantWhitespace = regexp.MustCompile(`\s+`)
|
||||||
|
)
|
||||||
|
|
||||||
|
func tplString(name string, data interface{}) (string, error) {
|
||||||
|
w := new(bytes.Buffer)
|
||||||
|
if err := ircTpl.ExecuteTemplate(w, name, data); err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
s := w.String()
|
||||||
|
s = rxInsignificantWhitespace.ReplaceAllString(s, " ")
|
||||||
|
s = strings.Trim(s, " ")
|
||||||
|
log.Printf("tplString(%v): %s", name, s)
|
||||||
|
return s, nil
|
||||||
|
}
|
|
@ -0,0 +1,36 @@
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
)
|
||||||
|
|
||||||
|
func Test_TemplateFuncs_comma(t *testing.T) {
|
||||||
|
f := tplFuncMap["comma"].(func(num uint64) string)
|
||||||
|
|
||||||
|
assert.Equal(t, "1", f(1))
|
||||||
|
assert.Equal(t, "10", f(10))
|
||||||
|
assert.Equal(t, "100", f(100))
|
||||||
|
assert.Equal(t, "1,000", f(1000))
|
||||||
|
assert.Equal(t, "1,000,000", f(1000000))
|
||||||
|
}
|
||||||
|
|
||||||
|
func Test_TemplateFuncs_compactnum(t *testing.T) {
|
||||||
|
f := tplFuncMap["compactnum"].(func(num uint64) string)
|
||||||
|
|
||||||
|
assert.Equal(t, "1", f(1))
|
||||||
|
assert.Equal(t, "10", f(10))
|
||||||
|
assert.Equal(t, "100", f(100))
|
||||||
|
assert.Equal(t, "999", f(999))
|
||||||
|
assert.Equal(t, "1k", f(1000))
|
||||||
|
assert.Equal(t, "9k", f(9000))
|
||||||
|
assert.Equal(t, "9.9k", f(9900))
|
||||||
|
assert.Equal(t, "10k", f(9999))
|
||||||
|
assert.Equal(t, "10k", f(10000))
|
||||||
|
assert.Equal(t, "100k", f(100000))
|
||||||
|
assert.Equal(t, "999k", f(999000))
|
||||||
|
assert.Equal(t, "999.9k", f(999900))
|
||||||
|
assert.Equal(t, "1M", f(999999))
|
||||||
|
assert.Equal(t, "1M", f(1000000))
|
||||||
|
}
|
|
@ -0,0 +1,198 @@
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/helmbold/richgo/regexp"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
errIncompatibleTopicPart = errors.New("Incompatible topic part")
|
||||||
|
|
||||||
|
rxTopicPart = regexp.MustCompile(`\s*\x02` +
|
||||||
|
`\s*\x03\s*(?P<prefixColor>[0-9]+)` +
|
||||||
|
`(?P<prefix>[A-Za-z\s]+):` +
|
||||||
|
`\s*\x03` +
|
||||||
|
`\s*(?P<text>.+)` +
|
||||||
|
`\s*\x02` +
|
||||||
|
`\s*`)
|
||||||
|
|
||||||
|
rxMatch = regexp.MustCompile(`^` +
|
||||||
|
`(?P<homeTeam>` +
|
||||||
|
`\w+(?:\s+\w+)*)` +
|
||||||
|
`\s+(?P<homeScore>\-|\d+):(?P<awayScore>\-|\d+)` +
|
||||||
|
`\s+(?P<awayTeam>\w+(?:\s+\w+)*)` +
|
||||||
|
`(` +
|
||||||
|
`\s+(?P<additional>.+)` +
|
||||||
|
`)?` +
|
||||||
|
`$`)
|
||||||
|
|
||||||
|
rxAdditionalScoreInfo = regexp.MustCompile(`^` +
|
||||||
|
`(?P<pre>.*?)\s*` +
|
||||||
|
`(\[(?:` +
|
||||||
|
`(?P<penTag>P)\s*(?P<penScore>\d+:\d+)` +
|
||||||
|
`|` +
|
||||||
|
`(?P<extraTag>AET)` +
|
||||||
|
`)\]\s*)?` +
|
||||||
|
`\((?P<halfScore>\d+:\d+)\)` +
|
||||||
|
`\s*(?P<post>.*)` +
|
||||||
|
`$`)
|
||||||
|
)
|
||||||
|
|
||||||
|
type topicPart struct {
|
||||||
|
PrefixColor string
|
||||||
|
Prefix string
|
||||||
|
|
||||||
|
Text string
|
||||||
|
Matches []topicMatchInfo
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *topicPart) HasMatches() bool {
|
||||||
|
return t.Matches != nil && len(t.Matches) > 0
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *topicPart) String() string {
|
||||||
|
retval := fmt.Sprintf("\x02\x03%s%s:\x03 ", t.PrefixColor, t.Prefix)
|
||||||
|
if t.Matches == nil || len(t.Matches) == 0 {
|
||||||
|
retval += t.Text
|
||||||
|
} else {
|
||||||
|
first := true
|
||||||
|
for _, match := range t.Matches {
|
||||||
|
if !first {
|
||||||
|
retval += " + "
|
||||||
|
}
|
||||||
|
retval += match.String()
|
||||||
|
first = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
retval += "\x02"
|
||||||
|
return retval
|
||||||
|
}
|
||||||
|
|
||||||
|
type topicMatchInfo struct {
|
||||||
|
HomeTeam string
|
||||||
|
AwayTeam string
|
||||||
|
Current *topicScoreInfo
|
||||||
|
Additional string
|
||||||
|
}
|
||||||
|
|
||||||
|
type topicAdditionalScoreInfo struct {
|
||||||
|
IsAET bool
|
||||||
|
PenScore string
|
||||||
|
HalfScore string
|
||||||
|
|
||||||
|
Pre string
|
||||||
|
Post string
|
||||||
|
}
|
||||||
|
|
||||||
|
func (i *topicAdditionalScoreInfo) String() string {
|
||||||
|
retval := ""
|
||||||
|
switch {
|
||||||
|
case len(i.PenScore) > 0:
|
||||||
|
retval += fmt.Sprintf("[P %s]", i.PenScore)
|
||||||
|
case i.IsAET:
|
||||||
|
retval += "[AET]"
|
||||||
|
}
|
||||||
|
if len(retval) > 0 {
|
||||||
|
retval += " "
|
||||||
|
}
|
||||||
|
if len(i.HalfScore) > 0 {
|
||||||
|
retval += fmt.Sprintf("(%s)", i.HalfScore)
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(i.Pre) > 0 {
|
||||||
|
if len(retval) > 0 {
|
||||||
|
retval = " " + retval
|
||||||
|
}
|
||||||
|
retval = i.Pre + retval
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(i.Post) > 0 {
|
||||||
|
if len(retval) > 0 {
|
||||||
|
retval += " "
|
||||||
|
}
|
||||||
|
retval += i.Post
|
||||||
|
}
|
||||||
|
return retval
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *topicMatchInfo) AdditionalScoreInfo() *topicAdditionalScoreInfo {
|
||||||
|
a := rxAdditionalScoreInfo.Match(m.Additional)
|
||||||
|
if a == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return &topicAdditionalScoreInfo{
|
||||||
|
HalfScore: a.NamedGroups["halfScore"],
|
||||||
|
IsAET: len(a.NamedGroups["penScore"]) > 0 || len(a.NamedGroups["extraTag"]) > 0,
|
||||||
|
PenScore: a.NamedGroups["penScore"],
|
||||||
|
|
||||||
|
Pre: a.NamedGroups["pre"],
|
||||||
|
Post: a.NamedGroups["post"],
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *topicMatchInfo) String() string {
|
||||||
|
retval := fmt.Sprintf("%s %s %s", m.HomeTeam, m.Current, m.AwayTeam)
|
||||||
|
if len(m.Additional) > 0 {
|
||||||
|
retval += " " + m.Additional
|
||||||
|
}
|
||||||
|
return retval
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
type topicScoreInfo struct {
|
||||||
|
HomeScore int
|
||||||
|
AwayScore int
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *topicScoreInfo) String() string {
|
||||||
|
if s == nil {
|
||||||
|
return "-:-"
|
||||||
|
}
|
||||||
|
return fmt.Sprintf("%d:%d", s.HomeScore, s.AwayScore)
|
||||||
|
}
|
||||||
|
|
||||||
|
func parseTopicPart(s string) (r topicPart, err error) {
|
||||||
|
match := rxTopicPart.Match(s)
|
||||||
|
if match == nil {
|
||||||
|
err = errIncompatibleTopicPart
|
||||||
|
return
|
||||||
|
}
|
||||||
|
r.PrefixColor = match.NamedGroups["prefixColor"]
|
||||||
|
r.Prefix = match.NamedGroups["prefix"]
|
||||||
|
r.Text = match.NamedGroups["text"]
|
||||||
|
|
||||||
|
// Try to get match info if compatible
|
||||||
|
matchCompatible := true
|
||||||
|
matchInfos := []topicMatchInfo{}
|
||||||
|
for _, matchPart := range strings.Split(r.Text, " + ") {
|
||||||
|
match = rxMatch.Match(matchPart)
|
||||||
|
if match == nil {
|
||||||
|
matchCompatible = false
|
||||||
|
break
|
||||||
|
}
|
||||||
|
matchInfo := topicMatchInfo{}
|
||||||
|
matchInfo.HomeTeam = match.NamedGroups["homeTeam"]
|
||||||
|
matchInfo.AwayTeam = match.NamedGroups["awayTeam"]
|
||||||
|
homeScore, err := strconv.Atoi(match.NamedGroups["homeScore"])
|
||||||
|
if err == nil {
|
||||||
|
awayScore, err := strconv.Atoi(match.NamedGroups["awayScore"])
|
||||||
|
if err == nil {
|
||||||
|
matchInfo.Current = &topicScoreInfo{
|
||||||
|
HomeScore: homeScore,
|
||||||
|
AwayScore: awayScore,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
matchInfo.Additional = match.NamedGroups["additional"]
|
||||||
|
matchInfos = append(matchInfos, matchInfo)
|
||||||
|
}
|
||||||
|
if matchCompatible {
|
||||||
|
r.Matches = matchInfos
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
|
@ -0,0 +1,245 @@
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
)
|
||||||
|
|
||||||
|
func Test_topicPart_String(t *testing.T) {
|
||||||
|
assert.Equal(t, "\x02\x031Now:\x03 testing\x02", (&topicPart{
|
||||||
|
PrefixColor: "1",
|
||||||
|
Prefix: "Now",
|
||||||
|
Text: "testing",
|
||||||
|
}).String())
|
||||||
|
assert.Equal(t, "\x02\x031Now:\x03 testing\x02", (&topicPart{
|
||||||
|
PrefixColor: "1",
|
||||||
|
Prefix: "Now",
|
||||||
|
Text: "testing",
|
||||||
|
Matches: []topicMatchInfo{},
|
||||||
|
}).String())
|
||||||
|
assert.Equal(t, "\x02\x031Now:\x03 a -:- b\x02", (&topicPart{
|
||||||
|
PrefixColor: "1",
|
||||||
|
Prefix: "Now",
|
||||||
|
Text: "testing",
|
||||||
|
Matches: []topicMatchInfo{
|
||||||
|
topicMatchInfo{
|
||||||
|
HomeTeam: "a",
|
||||||
|
AwayTeam: "b",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}).String())
|
||||||
|
assert.Equal(t, "\x02\x031Now:\x03 a 0:1 b\x02", (&topicPart{
|
||||||
|
PrefixColor: "1",
|
||||||
|
Prefix: "Now",
|
||||||
|
Text: "testing",
|
||||||
|
Matches: []topicMatchInfo{
|
||||||
|
topicMatchInfo{
|
||||||
|
HomeTeam: "a",
|
||||||
|
AwayTeam: "b",
|
||||||
|
Current: &topicScoreInfo{
|
||||||
|
HomeScore: 0,
|
||||||
|
AwayScore: 1,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}).String())
|
||||||
|
assert.EqualValues(t, "\x02\x031Now:\x03 a 0:1 b (0:0)\x02", (&topicPart{
|
||||||
|
PrefixColor: "1",
|
||||||
|
Prefix: "Now",
|
||||||
|
Text: "testing",
|
||||||
|
Matches: []topicMatchInfo{
|
||||||
|
topicMatchInfo{
|
||||||
|
HomeTeam: "a",
|
||||||
|
AwayTeam: "b",
|
||||||
|
Current: &topicScoreInfo{
|
||||||
|
HomeScore: 0,
|
||||||
|
AwayScore: 1,
|
||||||
|
},
|
||||||
|
Additional: "(0:0)",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}).String())
|
||||||
|
assert.EqualValues(t, "\x02\x031Now:\x03 a 0:1 b (0:0) + c 0:0 d\x02", (&topicPart{
|
||||||
|
PrefixColor: "1",
|
||||||
|
Prefix: "Now",
|
||||||
|
Text: "testing",
|
||||||
|
Matches: []topicMatchInfo{
|
||||||
|
topicMatchInfo{
|
||||||
|
HomeTeam: "a",
|
||||||
|
AwayTeam: "b",
|
||||||
|
Current: &topicScoreInfo{
|
||||||
|
HomeScore: 0,
|
||||||
|
AwayScore: 1,
|
||||||
|
},
|
||||||
|
Additional: "(0:0)",
|
||||||
|
},
|
||||||
|
topicMatchInfo{
|
||||||
|
HomeTeam: "c",
|
||||||
|
AwayTeam: "d",
|
||||||
|
Current: &topicScoreInfo{
|
||||||
|
HomeScore: 0,
|
||||||
|
AwayScore: 0,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}).String())
|
||||||
|
assert.EqualValues(t, "\x02\x031Now:\x03 a 0:1 b (0:0) + c 2:2 d (1:1)\x02", (&topicPart{
|
||||||
|
PrefixColor: "1",
|
||||||
|
Prefix: "Now",
|
||||||
|
Text: "testing",
|
||||||
|
Matches: []topicMatchInfo{
|
||||||
|
topicMatchInfo{
|
||||||
|
HomeTeam: "a",
|
||||||
|
AwayTeam: "b",
|
||||||
|
Current: &topicScoreInfo{
|
||||||
|
HomeScore: 0,
|
||||||
|
AwayScore: 1,
|
||||||
|
},
|
||||||
|
Additional: "(0:0)",
|
||||||
|
},
|
||||||
|
topicMatchInfo{
|
||||||
|
HomeTeam: "c",
|
||||||
|
AwayTeam: "d",
|
||||||
|
Current: &topicScoreInfo{
|
||||||
|
HomeScore: 2,
|
||||||
|
AwayScore: 2,
|
||||||
|
},
|
||||||
|
Additional: "(1:1)",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}).String())
|
||||||
|
}
|
||||||
|
|
||||||
|
func Test_parseTopic(t *testing.T) {
|
||||||
|
var topic topicPart
|
||||||
|
var err error
|
||||||
|
|
||||||
|
topic, err = parseTopicPart("Incompatible text")
|
||||||
|
assert.Equal(t, errIncompatibleTopicPart, err)
|
||||||
|
|
||||||
|
topic, err = parseTopicPart("\x02\x033Last:\x03 No match\x02")
|
||||||
|
if assert.Nil(t, err) {
|
||||||
|
assert.EqualValues(t, topicPart{
|
||||||
|
PrefixColor: "3",
|
||||||
|
Prefix: "Last",
|
||||||
|
Text: "No match",
|
||||||
|
}, topic)
|
||||||
|
}
|
||||||
|
|
||||||
|
topic, err = parseTopicPart("\x02\x033Last:\x03 a -:- b\x02")
|
||||||
|
if assert.Nil(t, err) {
|
||||||
|
assert.EqualValues(t, topicPart{
|
||||||
|
PrefixColor: "3",
|
||||||
|
Prefix: "Last",
|
||||||
|
Text: "a -:- b",
|
||||||
|
Matches: []topicMatchInfo{
|
||||||
|
topicMatchInfo{
|
||||||
|
HomeTeam: "a",
|
||||||
|
AwayTeam: "b",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}, topic)
|
||||||
|
}
|
||||||
|
|
||||||
|
topic, err = parseTopicPart(" \x02\x0313Last:\x03 needs to be replaced by the bot\x02 ")
|
||||||
|
if assert.Nil(t, err) {
|
||||||
|
assert.EqualValues(t, topicPart{
|
||||||
|
PrefixColor: "13",
|
||||||
|
Prefix: "Last",
|
||||||
|
Text: "needs to be replaced by the bot",
|
||||||
|
}, topic)
|
||||||
|
}
|
||||||
|
|
||||||
|
topic, err = parseTopicPart("\x02\x033Last:\x03 a 0:0 b (0:0)\x02")
|
||||||
|
if assert.Nil(t, err) {
|
||||||
|
assert.EqualValues(t, topicPart{
|
||||||
|
PrefixColor: "3",
|
||||||
|
Prefix: "Last",
|
||||||
|
Text: "a 0:0 b (0:0)",
|
||||||
|
Matches: []topicMatchInfo{
|
||||||
|
topicMatchInfo{
|
||||||
|
HomeTeam: "a",
|
||||||
|
AwayTeam: "b",
|
||||||
|
Current: &topicScoreInfo{
|
||||||
|
HomeScore: 0,
|
||||||
|
AwayScore: 0,
|
||||||
|
},
|
||||||
|
Additional: "(0:0)",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}, topic)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func Test_topicPart_Matches_AdditionalScoreInfo(t *testing.T) {
|
||||||
|
topic, err := parseTopicPart("\x02\x033Last:\x03 a 0:0 b [aBx] (0:0) (abcd)\x02")
|
||||||
|
if assert.Nil(t, err) && assert.Len(t, topic.Matches, 1) {
|
||||||
|
info := topic.Matches[0].AdditionalScoreInfo()
|
||||||
|
if assert.NotNil(t, info) {
|
||||||
|
assert.EqualValues(t, &topicAdditionalScoreInfo{
|
||||||
|
Pre: "[aBx]",
|
||||||
|
Post: "(abcd)",
|
||||||
|
PenScore: "",
|
||||||
|
HalfScore: "0:0",
|
||||||
|
IsAET: false,
|
||||||
|
}, info)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
topic, err = parseTopicPart("\x02\x033Last:\x03 a 0:0 b (0:0)\x02")
|
||||||
|
if assert.Nil(t, err) && assert.Len(t, topic.Matches, 1) {
|
||||||
|
info := topic.Matches[0].AdditionalScoreInfo()
|
||||||
|
if assert.NotNil(t, info) {
|
||||||
|
assert.EqualValues(t, &topicAdditionalScoreInfo{
|
||||||
|
Pre: "",
|
||||||
|
Post: "",
|
||||||
|
PenScore: "",
|
||||||
|
HalfScore: "0:0",
|
||||||
|
IsAET: false,
|
||||||
|
}, info)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
topic, err = parseTopicPart("\x02\x033Last:\x03 a 0:0 b [P5:4] (0:0)\x02")
|
||||||
|
if assert.Nil(t, err) && assert.Len(t, topic.Matches, 1) {
|
||||||
|
info := topic.Matches[0].AdditionalScoreInfo()
|
||||||
|
if assert.NotNil(t, info) {
|
||||||
|
assert.EqualValues(t, &topicAdditionalScoreInfo{
|
||||||
|
Pre: "",
|
||||||
|
Post: "",
|
||||||
|
PenScore: "5:4",
|
||||||
|
HalfScore: "0:0",
|
||||||
|
IsAET: true,
|
||||||
|
}, info)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
topic, err = parseTopicPart("\x02\x033Last:\x03 a 0:0 b [P 5:4] (0:0)\x02")
|
||||||
|
if assert.Nil(t, err) && assert.Len(t, topic.Matches, 1) {
|
||||||
|
info := topic.Matches[0].AdditionalScoreInfo()
|
||||||
|
if assert.NotNil(t, info) {
|
||||||
|
assert.EqualValues(t, &topicAdditionalScoreInfo{
|
||||||
|
Pre: "",
|
||||||
|
Post: "",
|
||||||
|
PenScore: "5:4",
|
||||||
|
HalfScore: "0:0",
|
||||||
|
IsAET: true,
|
||||||
|
}, info)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
topic, err = parseTopicPart("\x02\x033Last:\x03 a 0:0 b [AET] (0:0)\x02")
|
||||||
|
if assert.Nil(t, err) && assert.Len(t, topic.Matches, 1) {
|
||||||
|
info := topic.Matches[0].AdditionalScoreInfo()
|
||||||
|
if assert.NotNil(t, info) {
|
||||||
|
assert.EqualValues(t, &topicAdditionalScoreInfo{
|
||||||
|
Pre: "",
|
||||||
|
Post: "",
|
||||||
|
PenScore: "",
|
||||||
|
HalfScore: "0:0",
|
||||||
|
IsAET: true,
|
||||||
|
}, info)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,39 @@
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"regexp"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/icedream/go-footballdata"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
runeIrcBold = '\x02'
|
||||||
|
runeIrcColor = '\x03'
|
||||||
|
runeIrcReset = '\x0f'
|
||||||
|
runeIrcReverse = '\x16'
|
||||||
|
runeIrcItalic = '\x1d'
|
||||||
|
runeIrcUnderline = '\x1f'
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
rxIrcColor = regexp.MustCompile(string(runeIrcColor) + "([0-9]*(,[0-9]*)?)")
|
||||||
|
)
|
||||||
|
|
||||||
|
func stripIrcFormatting(text string) string {
|
||||||
|
text = strings.Replace(text, string(runeIrcBold), "", -1)
|
||||||
|
text = strings.Replace(text, string(runeIrcReset), "", -1)
|
||||||
|
text = strings.Replace(text, string(runeIrcReverse), "", -1)
|
||||||
|
text = strings.Replace(text, string(runeIrcItalic), "", -1)
|
||||||
|
text = strings.Replace(text, string(runeIrcUnderline), "", -1)
|
||||||
|
text = rxIrcColor.ReplaceAllLiteralString(text, "")
|
||||||
|
return text
|
||||||
|
}
|
||||||
|
|
||||||
|
func fixturesMap(fixtures []footballdata.Fixture) map[versus]footballdata.Fixture {
|
||||||
|
retval := make(map[versus]footballdata.Fixture)
|
||||||
|
for _, fixture := range fixtures {
|
||||||
|
retval[versus{fixture.HomeTeamName, fixture.AwayTeamName}] = fixture
|
||||||
|
}
|
||||||
|
return retval
|
||||||
|
}
|
|
@ -0,0 +1 @@
|
||||||
|
package main
|
|
@ -0,0 +1 @@
|
||||||
|
Subproject commit f0f3222ce8996a866feeaa5aabfbde70e8f31109
|
|
@ -0,0 +1 @@
|
||||||
|
Subproject commit 17cd8cd68efe4599201b997bf9bf945a72077755
|
|
@ -0,0 +1 @@
|
||||||
|
Subproject commit a3647f8e31d79543b2d0f0ae2fe5c379d72cedc0
|
|
@ -0,0 +1 @@
|
||||||
|
Subproject commit 05e8a0eda380579888eb53c394909df027f06991
|
|
@ -0,0 +1 @@
|
||||||
|
Subproject commit 1b0acb5f2f1b615cfbd4b9f91abb14cb39a18769
|
Loading…
Reference in New Issue