Created
May 15, 2013 07:29
-
-
Save robert-nix/5582199 to your computer and use it in GitHub Desktop.
My first Golang program; an absolute mess.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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