Skip to content

Instantly share code, notes, and snippets.

@robert-nix
Created May 15, 2013 07:29
Show Gist options
  • Select an option

  • Save robert-nix/5582199 to your computer and use it in GitHub Desktop.

Select an option

Save robert-nix/5582199 to your computer and use it in GitHub Desktop.
My first Golang program; an absolute mess.
package main
import (
"crypto/tls"
"encoding/json"
"flag"
"fmt"
irc "github.com/fluffle/goirc/client"
"labix.org/v2/mgo"
"net/http"
"net/url"
"strings"
"time"
)
var user *string = flag.String("user", "LoggerBot", "username")
var pass *string = flag.String("pass", "password", "password")
var twitchClientId *string = flag.String("cid", "", "Twitch API Client ID")
var channelBlacklist = map[string]bool{
"sayurit": true,
}
var viewerCounts = map[string]int{}
func text(line *irc.Line) string {
if len(line.Args) > 0 {
return line.Args[len(line.Args)-1]
}
return ""
}
func log(msg string) {
fmt.Printf("[%s]%s\n", time.Now().Format("15:04:05"), msg)
}
type ChatMessage struct {
Timestamp time.Time
Channel string
Nick string
Msg string
}
type StatSnapshot struct {
Timestamp time.Time
Channel string
Userlist []string
Viewers int
}
func DbWriter(messageList chan ChatMessage, statsList chan *StatSnapshot, stop chan bool) {
session, err := mgo.Dial("localhost")
if err != nil {
panic(err)
}
defer session.Close()
coll := session.DB("test").C("messages-4")
statsColl := session.DB("test").C("stats-4")
messageBuf := make([]interface{}, 0, 1000)
var message ChatMessage
statsBuf := make([]interface{}, 0, 500)
var stats *StatSnapshot
interval := time.Tick(10 * time.Second)
// Write chats
writeBuf := func() {
if len(messageBuf) > 0 {
err := coll.Insert(messageBuf...)
if err != nil {
panic(err)
}
log(fmt.Sprintf("[d]Wrote %d chats", len(messageBuf)))
messageBuf = make([]interface{}, 0, 1000)
}
if len(statsBuf) > 0 {
err := statsColl.Insert(statsBuf...)
if err != nil {
panic(err)
}
log(fmt.Sprintf("[d]Wrote %d stats", len(statsBuf)))
statsBuf = make([]interface{}, 0, 500)
}
}
for {
select {
case <-stop:
fmt.Println("Stopping DB writer...")
writeBuf()
return
case message = <-messageList:
if message.Nick != "jtv" && message.Nick != "" {
m := message
messageBuf = append(messageBuf, &m)
}
case stats = <-statsList:
statsBuf = append(statsBuf, stats)
case <-interval:
writeBuf()
}
}
}
func ChannelList() []string {
tr := &http.Transport{
TLSClientConfig: &tls.Config{},
}
client := &http.Client{Transport: tr}
result := make([]string, 0, 100)
offset := 0
quit := 0
for {
filterVal := url.Values{}
filterVal.Set("limit", "50")
filterVal.Set("offset", fmt.Sprintf("%d", offset))
req, err := http.NewRequest(
"GET",
strings.Join(
[]string{"https://api.twitch.tv/kraken/streams?", filterVal.Encode()},
""),
nil)
req.Header.Add("Client-ID", *twitchClientId)
req.Header.Add("Accept", "application/vnd.twitchtv.v3+json")
resp, err := client.Do(req)
if resp.StatusCode == 503 {
log("[@]API 503")
}
if err != nil {
panic(err)
}
defer resp.Body.Close()
respAsJson := json.NewDecoder(resp.Body)
type ChannelItem struct {
Name string
}
type StreamItem struct {
Viewers int
Channel ChannelItem
}
type StreamsResponse struct {
Streams []StreamItem
}
var body StreamsResponse
err = respAsJson.Decode(&body)
viewerCounts = map[string]int{}
for _, stream := range body.Streams {
streamName := stream.Channel.Name
viewers := stream.Viewers
viewerCounts[streamName] = viewers
if viewers > 100 {
result = append(result, streamName)
} else {
quit++
}
}
if quit > 10 {
break
}
offset += 50
}
return result
}
func ChannelManager(stop chan bool) {
// Global message channel
list := make(chan ChatMessage, 100)
// Stats channel
statsList := make(chan *StatSnapshot, 100)
// IRC join
conn := irc.SimpleClient(*user, "loggerbot", "TwitchTV LoggerBot")
conn.Flood = true
hostname := "loggerbot.jtvirc.com"
connected := make(chan bool)
dead := make(chan bool)
conn.AddHandler(irc.DISCONNECTED, func(c *irc.Conn, line *irc.Line) {
dead <- true
})
conn.AddHandler("376", func(c *irc.Conn, line *irc.Line) {
connected <- true
})
conn.AddHandler("PRIVMSG", func(c *irc.Conn, line *irc.Line) {
if line.Args[0] != "loggerbot" {
m := ChatMessage{line.Time, line.Args[0][1:], line.Nick, text(line)}
list <- m
}
})
conn.AddHandler("ACTION", func(c *irc.Conn, line *irc.Line) {
if line.Args[0] != "loggerbot" {
actionText := fmt.Sprintf("/me %s", text(line))
m := ChatMessage{line.Time, line.Args[0][1:], line.Nick, actionText}
list <- m
}
})
// -- Names list
// Per-channel userlist
usersMap := make(map[string](map[string]bool))
// Count users in channel
count := func(stream string) int {
channelHash := fmt.Sprintf("#%s", stream)
n := 0
userMap, ok := usersMap[channelHash]
if !ok {
return n
}
for _, active := range userMap {
if active {
n++
}
}
return n
}
// NAMES, JOIN, and PART handlers
conn.AddHandler("353", func(c *irc.Conn, line *irc.Line) {
userMap, ok := usersMap[line.Args[2]]
if !ok {
usersMap[line.Args[2]] = map[string]bool{}
userMap = usersMap[line.Args[2]]
}
for _, user := range strings.Split(text(line), " ") {
userMap[user] = true
}
})
conn.AddHandler("JOIN", func(c *irc.Conn, line *irc.Line) {
userMap, ok := usersMap[line.Args[0]]
if !ok {
usersMap[line.Args[0]] = map[string]bool{}
userMap = usersMap[line.Args[0]]
}
userMap[line.Nick] = true
})
conn.AddHandler("PART", func(c *irc.Conn, line *irc.Line) {
userMap, ok := usersMap[line.Args[0]]
if !ok {
usersMap[line.Args[0]] = map[string]bool{}
userMap = usersMap[line.Args[0]]
}
userMap[line.Nick] = false
})
// Connect through a global connection
err := conn.Connect(hostname, *pass)
if err != nil {
fmt.Printf("Connection error: %s\n", err)
return
}
go func() {
<-dead
stop <- true
}()
<-connected
// Db routine
dbstop := make(chan bool)
go DbWriter(list, statsList, dbstop)
// Whether a client should be bound for a stream
streamMap := map[string]bool{}
// Ticker for updating channel list
interval := time.Tick(5 * time.Minute)
// Manager tick channel
ticks := make(chan bool)
go func() {
// Tick the manager on program start as well
ticks <- true
for {
// This kills the ticker's automatic throttling, but we don't need it
<-interval
ticks <- true
}
}()
// Try to mitigate bouncing in and out
blackoutTimers := map[string]time.Time{}
// Part helper
part := func(stream string) {
channelHash := fmt.Sprintf("#%s", stream)
streamMap[stream] = false
log(fmt.Sprintf("[-]Part: %s", channelHash))
conn.Part(channelHash)
blackoutTimers[stream] = time.Now()
}
// Join helper
join := func(stream string) {
if nojoin, ok := channelBlacklist[stream]; ok && nojoin {
return
}
if black, ok := blackoutTimers[stream]; ok {
if time.Since(black) < time.Minute*30 {
return
}
}
channelHash := fmt.Sprintf("#%s", stream)
streamMap[stream] = true
log(fmt.Sprintf("[+]Join: %s", channelHash))
conn.Join(channelHash)
}
// Stats helper
postStats := func(stream string) {
channelHash := fmt.Sprintf("#%s", stream)
userMap, ok := usersMap[channelHash]
users := make([]string, 0, 100)
if ok {
for user, active := range userMap {
if active {
users = append(users, user)
}
}
}
viewerCount := 0
if v, ok := viewerCounts[stream]; ok {
viewerCount = v
}
statsList <- &StatSnapshot{time.Now(), stream, users, viewerCount}
}
for {
select {
case <-stop:
fmt.Println("Stopping IRC clients...")
conn.Quit("leaving")
dbstop <- true
return
case <-ticks:
// update channels...
newStreamMap := map[string]bool{}
// Union set
modified := map[string]bool{}
for stream, _ := range streamMap {
modified[stream] = true
}
for _, stream := range ChannelList() {
newStreamMap[stream] = true
modified[stream] = true
}
for stream := range modified {
active, ok := streamMap[stream]
if !ok {
active = false
}
newActive, ok := newStreamMap[stream]
// off -> on
if !active && ok && newActive {
join(stream)
}
// leave if low on chatters
if active && count(stream) < 30 {
part(stream)
log("[-] (Too few chatters)")
}
// Log stats with a 20 second delay to let the userlist fill in
go func(s string) {
<-time.After(20 * time.Second)
postStats(s)
}(stream)
}
}
}
}
func main() {
flag.Parse()
stop := make(chan bool)
quit := make(chan bool)
go ChannelManager(stop)
// Stop on line in
go func() {
var in string
fmt.Scan(&in)
stop <- true
// Say our good-byes
<-time.After(1 * time.Second)
quit <- true
}()
<-quit
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment