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