695 lines
21 KiB
Go
695 lines
21 KiB
Go
package main
|
|
|
|
import (
|
|
"bytes"
|
|
"fmt"
|
|
"log"
|
|
"math"
|
|
"regexp"
|
|
"sort"
|
|
"strings"
|
|
"time"
|
|
|
|
"net/http"
|
|
|
|
"github.com/alecthomas/kingpin"
|
|
"github.com/dustin/go-humanize"
|
|
"github.com/icedream/go-footballdata"
|
|
"github.com/olekukonko/tablewriter"
|
|
"github.com/thoj/go-ircevent"
|
|
|
|
"git.icedream.tech/icedream/soccer-bot/antispam"
|
|
"git.icedream.tech/icedream/soccer-bot/manager"
|
|
)
|
|
|
|
const (
|
|
Competition_Testing_GermanLeague2 = 453
|
|
Competition_Testing = 464
|
|
Competition_WorldChampionShip2018 = 467
|
|
|
|
Competition = Competition_WorldChampionShip2018
|
|
|
|
day = 24 * time.Hour
|
|
week = 7 * day
|
|
)
|
|
|
|
var (
|
|
rDiff = regexp.MustCompile("[\\+\\-]\\d+")
|
|
)
|
|
|
|
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
|
|
topicRefreshDelay := 30 * time.Second
|
|
|
|
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)
|
|
kingpin.Flag("topic-refresh-delay", "The interval between topic updates. (This includes asking football-data for new information!)").DurationVar(&topicRefreshDelay)
|
|
|
|
// 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 := 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
|
|
}
|
|
|
|
// Antispam
|
|
antispamInstance := antispam.New()
|
|
|
|
updateTopics := func() {
|
|
// Get football data
|
|
if r, err := footballData.FixturesOfCompetition(Competition).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 m.GetChannels() {
|
|
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("PART", func(e *irc.Event) {
|
|
// Is this PART not about us?
|
|
if !strings.EqualFold(e.Nick, conn.GetNick()) {
|
|
return
|
|
}
|
|
|
|
m.DeleteTopic(e.Arguments[0])
|
|
})
|
|
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)
|
|
|
|
cmd := parseCommand(msg)
|
|
isCommand := !isChannel || strings.HasPrefix(cmd.Name, "!")
|
|
if !isCommand {
|
|
return
|
|
}
|
|
|
|
log.Printf("Antispam check: %s, %s", event.Source, cmd.Name)
|
|
if ok, duration := antispamInstance.Check(event.Source, cmd.Name); !ok {
|
|
conn.Noticef(sender, "Sorry, please try again %s.", humanize.Time(time.Now().Add(duration)))
|
|
return
|
|
}
|
|
|
|
switch {
|
|
|
|
case !isChannel && strings.EqualFold(cmd.Name, "updatetopics"):
|
|
updateTopicsChan <- nil
|
|
|
|
case strings.EqualFold(cmd.Name, "!help"):
|
|
conn.Noticef(sender, "\x02!group <group name>\x02 - Displays group table, for example \x02!group a\x02 would show the group A table.")
|
|
conn.Noticef(sender, "\x02!country <country name>\x02 or \x02!team <country name>\x02 - Displays upcoming matches and results of past matches of a playing country, for example \x02!country germany\x02.")
|
|
conn.Noticef(sender, "\x02!next\x02 - Originally meant to tell you what's the next match, this command really just tells you to look at \x02/topic\x02.")
|
|
|
|
case strings.EqualFold(cmd.Name, "!country"),
|
|
strings.EqualFold(cmd.Name, "!team"):
|
|
teamList, err := footballData.TeamsOfCompetition(Competition).Do()
|
|
if err != nil {
|
|
conn.Noticef(sender, "Sorry, can't display team information at this moment.")
|
|
log.Print(err)
|
|
}
|
|
|
|
// find correct team
|
|
name := strings.Join(cmd.Arguments, " ")
|
|
for _, competitionTeam := range teamList.Teams {
|
|
if strings.EqualFold(competitionTeam.Name, name) ||
|
|
// strings.EqualFold(team.Code, name) ||
|
|
strings.EqualFold(competitionTeam.ShortName, name) {
|
|
// found matching team
|
|
if competitionTeam.Id == 0 {
|
|
conn.Noticef(sender, "Sorry, something went horribly wrong here.")
|
|
log.Print("Returned team ID of a team in competition was 0...")
|
|
return
|
|
}
|
|
|
|
fixtures, err := footballData.FixturesOfTeam(competitionTeam.Id).
|
|
// Season(Competition).
|
|
Do()
|
|
if err != nil {
|
|
conn.Noticef(sender, "Sorry, could not fetch fixtures information at this moment.")
|
|
log.Print(err)
|
|
return
|
|
}
|
|
outputs := []string{}
|
|
for _, fixture := range fixtures.Fixtures {
|
|
homeTeam := fixture.HomeTeamName
|
|
awayTeam := fixture.AwayTeamName
|
|
var vs string
|
|
switch fixture.Status {
|
|
case footballdata.FixtureStatus_InPlay,
|
|
footballdata.FixtureStatus_Canceled,
|
|
footballdata.FixtureStatus_Finished:
|
|
// finished, display score
|
|
vs = fmt.Sprintf("%d:%d",
|
|
fixture.Result.GoalsHomeTeam,
|
|
fixture.Result.GoalsAwayTeam)
|
|
default:
|
|
// upcoming, display time
|
|
vs = fmt.Sprintf("[%s]",
|
|
fixture.Date.Format("Jan 02 @ 15:04 MST"))
|
|
}
|
|
outputs = append(outputs,
|
|
fmt.Sprintf("%s %s %s",
|
|
homeTeam, vs, awayTeam))
|
|
}
|
|
|
|
conn.Privmsg(target, strings.Join(outputs, " | "))
|
|
|
|
return
|
|
}
|
|
}
|
|
|
|
conn.Noticef(sender, "Sorry, I don't know this team.")
|
|
|
|
// Not implemented - football-data api data not reliable enough?
|
|
case strings.EqualFold(cmd.Name, "!player"):
|
|
// TODO
|
|
conn.Noticef(sender, "Sorry, this isn't implemented yet.")
|
|
|
|
case strings.EqualFold(cmd.Name, "!next"):
|
|
conn.Privmsg(target, "The upcoming match is always listed in the channel topic! If you can't see it (usually above the chat), try to type \x02/topic\x02.")
|
|
|
|
// Currently implemented:
|
|
// - !group / !group <Group name> (for example !group a)
|
|
// Not implemented yet:
|
|
// - !group <Country name>
|
|
case strings.EqualFold(cmd.Name, "!group"),
|
|
strings.EqualFold(cmd.Name, "!table"):
|
|
if leagueTable, err := footballData.LeagueTableOfCompetition(Competition).Do(); err != nil {
|
|
var s string
|
|
if s, err = tplString("error", err); err != nil {
|
|
log.Print(err)
|
|
} else {
|
|
conn.Privmsg(target, s)
|
|
}
|
|
} else {
|
|
log.Printf("%+v", leagueTable)
|
|
|
|
header := []string{"Rank", "Team", "Games", "Goals", "Diff", "Points"}
|
|
data := [][]string{}
|
|
|
|
// cast standing items to a new array with a more generic type
|
|
var standing []interface{}
|
|
if leagueTable.Standing != nil {
|
|
standing = make([]interface{}, len(leagueTable.Standing))
|
|
for i, v := range leagueTable.Standing {
|
|
standing[i] = v
|
|
}
|
|
}
|
|
|
|
if standing == nil {
|
|
if leagueTable.Standings == nil {
|
|
conn.Noticef(sender, "Sorry, can't display league table at this moment.")
|
|
return
|
|
}
|
|
|
|
// expect a standing name to be input
|
|
if len(cmd.Arguments) < 1 {
|
|
conn.Noticef(sender, "You need to type in which standing you want to view the table of.")
|
|
return
|
|
}
|
|
|
|
// find matching standing (case-insensitive)
|
|
ok := false
|
|
for key, val := range leagueTable.Standings {
|
|
if strings.EqualFold(key, cmd.Arguments[0]) {
|
|
ok = true
|
|
standing = make([]interface{}, len(val))
|
|
for i, v := range val {
|
|
standing[i] = v
|
|
}
|
|
break
|
|
}
|
|
}
|
|
if !ok {
|
|
conn.Noticef(sender, "Can not find requested standing for league table.")
|
|
return
|
|
}
|
|
}
|
|
|
|
for _, standing := range standing {
|
|
// HACK
|
|
var actualStanding footballdata.TeamLeagueStatistics
|
|
switch v := standing.(type) {
|
|
case footballdata.TeamLeagueStatisticsInStanding:
|
|
actualStanding = v.TeamLeagueStatistics
|
|
case footballdata.TeamLeagueStatisticsInStandings:
|
|
actualStanding = v.TeamLeagueStatistics
|
|
}
|
|
|
|
var goalDiffPrefix string
|
|
if actualStanding.GoalDifference > 0 {
|
|
goalDiffPrefix = "+"
|
|
} else {
|
|
goalDiffPrefix = ""
|
|
}
|
|
|
|
// HACK - get team name and rank from two diff fields
|
|
var teamName string
|
|
var rank uint8
|
|
switch v := standing.(type) {
|
|
case footballdata.TeamLeagueStatisticsInStanding:
|
|
teamName = v.TeamName
|
|
rank = v.Position
|
|
case footballdata.TeamLeagueStatisticsInStandings:
|
|
teamName = v.Team
|
|
rank = v.Rank
|
|
}
|
|
|
|
data = append(data, []string{
|
|
fmt.Sprintf("%d", rank),
|
|
teamName,
|
|
fmt.Sprintf("%d", actualStanding.PlayedGames),
|
|
fmt.Sprintf("%d:%d", actualStanding.Goals, actualStanding.GoalsAgainst),
|
|
fmt.Sprintf("%s%d", goalDiffPrefix, actualStanding.GoalDifference),
|
|
fmt.Sprintf("%d", actualStanding.Points),
|
|
})
|
|
}
|
|
|
|
buf := new(bytes.Buffer)
|
|
|
|
table := tablewriter.NewWriter(buf)
|
|
table.SetHeader(header)
|
|
table.SetAlignment(tablewriter.ALIGN_CENTER)
|
|
table.SetNewLine("\n")
|
|
table.SetBorder(true)
|
|
table.AppendBulk(data)
|
|
table.Render()
|
|
|
|
tableStr := rDiff.ReplaceAllStringFunc(buf.String(), func(s string) string {
|
|
switch s[0] {
|
|
case '+':
|
|
return fmt.Sprintf("\x0303%s\x03", s)
|
|
}
|
|
return fmt.Sprintf("\x0304%s\x03", s)
|
|
})
|
|
|
|
lines := strings.Split(tableStr, "\n")
|
|
for _, line := range lines {
|
|
conn.Privmsg(target, line)
|
|
}
|
|
}
|
|
case strings.EqualFold(cmd.Name, "!match"):
|
|
/*if r, err := footballData.FixturesOfCompetition(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(topicRefreshDelay):
|
|
case <-updateTopicsChan:
|
|
}
|
|
|
|
updateTopics()
|
|
}
|
|
}()
|
|
|
|
// listen for errors
|
|
log.Print("Now looping.")
|
|
conn.Loop()
|
|
}
|