From 21eba2d77d3172a4d06a85674adaa211c44c4b70 Mon Sep 17 00:00:00 2001 From: Daniel Aberger Date: Sun, 24 Apr 2016 03:42:50 +0200 Subject: [PATCH] very first iteration --- main.go | 81 +++++++++++++++ modules/modules.go | 48 +++++++++ modules/ts3.go | 162 ++++++++++++++++++++++++++++++ modules/twitch.go | 243 +++++++++++++++++++++++++++++++++++++++++++++ modules/utility.go | 12 +++ util/util.go | 54 ++++++++++ 6 files changed, 600 insertions(+) create mode 100644 main.go create mode 100644 modules/modules.go create mode 100644 modules/ts3.go create mode 100644 modules/twitch.go create mode 100644 modules/utility.go create mode 100644 util/util.go diff --git a/main.go b/main.go new file mode 100644 index 0000000..46030b4 --- /dev/null +++ b/main.go @@ -0,0 +1,81 @@ +// WGoSlackBot +// +// Copyright (c) 2016 by Daniel Aberger +package main + +import ( + "bytes" + "encoding/json" + "flag" + "fmt" + "log" + "net/http" + + "dev.ixab.de/WGSWATBP/WGoSlackBot/modules" +) + +var ( + sayCh chan modules.Payload + webhookPort = flag.String("webhookport", "61321", "Webhook port") + slackHook = flag.String("slackhook", "https://hooks.slack.com/services/", "Slack webhook URL") + params = flag.String("params", "", "Module params") +) + +func init() { + flag.Parse() +} + +func main() { + sayCh = make(chan modules.Payload, 1024) + modules.Init(sayCh, *params) + + go webhook() + + for { + sendPayload(<-sayCh) + } +} + +func webhook() { + http.HandleFunc("/hook", webhookHandler) + log.Fatal(http.ListenAndServe(":"+*webhookPort, nil)) +} +func webhookHandler(w http.ResponseWriter, req *http.Request) { + req.ParseForm() + whp := modules.WebhookPayload{ + Token: req.Form.Get("token"), + TeamID: req.Form.Get("team_id"), + TeamDomain: req.Form.Get("team_domain"), + ServiceID: req.Form.Get("service_id"), + ChannelID: req.Form.Get("channel_id"), + ChannelName: req.Form.Get("channel_name"), + Timestamp: req.Form.Get("timestamp"), + UserID: req.Form.Get("user_id"), + UserName: req.Form.Get("user_name"), + Text: req.Form.Get("text"), + TriggerWord: req.Form.Get("trigger_word"), + } + modules.HandleMessage(&whp) +} + +func sendPayload(payload modules.Payload) { + p, err := json.Marshal(payload) + if err != nil { + fmt.Println("error:", err) + } + url := *slackHook + + var jsonStr = p + req, err := http.NewRequest("POST", url, bytes.NewBuffer(jsonStr)) + req.Header.Set("X-Custom-Header", "myvalue") + req.Header.Set("Content-Type", "application/json") + + client := &http.Client{} + resp, err := client.Do(req) + if err != nil { + panic(err) + } + defer resp.Body.Close() + + log.Println("Sent payload. Response Status:", resp.Status) +} diff --git a/modules/modules.go b/modules/modules.go new file mode 100644 index 0000000..4784fda --- /dev/null +++ b/modules/modules.go @@ -0,0 +1,48 @@ +package modules + +import ( + "log" + "strings" +) + +type Payload struct { + Channel string `json:"channel"` + IconEmoji string `json:"icon_emoji"` + Text string `json:"text"` + Username string `json:"username"` +} + +type WebhookPayload struct { + ChannelID string `json:"channel_id"` + ChannelName string `json:"channel_name"` + ServiceID string `json:"service_id"` + TeamDomain string `json:"team_domain"` + TeamID string `json:"team_id"` + Text string `json:"text"` + Timestamp string `json:"timestamp"` + Token string `json:"token"` + TriggerWord string `json:"trigger_word"` + UserID string `json:"user_id"` + UserName string `json:"user_name"` +} + +var ( + SayCh chan Payload + MsgHandlers = make(map[string]func(*WebhookPayload)) + ModParams = make(map[string]string) +) + +func Init(ch chan Payload, params string) { + SayCh = ch + for _, param := range strings.Split(params, "!") { + kv := strings.Split(param, ":") + ModParams[kv[0]] = kv[1] + log.Println(kv[0], kv[1]) + } +} + +func HandleMessage(payload *WebhookPayload) { + for _, fn := range MsgHandlers { + fn(payload) + } +} diff --git a/modules/ts3.go b/modules/ts3.go new file mode 100644 index 0000000..374f7db --- /dev/null +++ b/modules/ts3.go @@ -0,0 +1,162 @@ +// ts3.go +package modules + +import ( + "bufio" + "log" + "net" + "regexp" + "strings" + "time" +) + +var ( + users = map[string]bool{} + host string + port string + user string + pass string + botname string + channel string +) + +func init() { + MsgHandlers["ts3"] = ts3HandleMessage + log.Println("Initializing ts3 module") + go setConfig() +} +func setConfig() { + time.Sleep(5 * time.Second) + host = ModParams["tshost"] + port = ModParams["tsport"] + user = ModParams["tsuser"] + pass = ModParams["tspass"] + botname = ModParams["tsbotname"] + channel = ModParams["tschan"] + go loop() +} +func loop() { + for { + var err error + users, err = getUsers() + if err != nil { + log.Println(err) + log.Println("Trying again in 60 seconds.") + time.Sleep(60 * time.Second) + } else { + break + } + } + + for { + if channel == "" { // dont spam if no channel is set + break + } + neu, err := getUsers() + if err != nil { + log.Println(err) + time.Sleep(50 * time.Second) + } else { + for i, _ := range neu { + if users[i] != neu[i] { + SayCh <- GeneratePayload(channel, ":poop:", i+" joined TS3.", botname) + } + } + for i, _ := range users { + if users[i] != neu[i] { + SayCh <- GeneratePayload(channel, ":poop:", i+" left TS3.", botname) + } + } + users = neu + } + + time.Sleep(10 * time.Second) + } +} +func ts3HandleMessage(payload *WebhookPayload) { + log.Println("TS3 users requested by " + payload.UserName) + users, _ := getUsers() + var s string + i := 0 + s += "Current users on TS3 Server:\n" + for u, _ := range users { + i++ + s += "- " + u + if i != len(users) { + s += "\n" + } + } + SayCh <- GeneratePayload("@"+payload.UserName, ":poop:", s, botname) +} + +func getUsers() (u map[string]bool, e error) { + conn, err := net.Dial("tcp", host+":"+port) + + if err != nil { + return nil, err + } + + reader := bufio.NewReader(conn) + writer := bufio.NewWriter(conn) + + for i := 0; i < 2; i++ { // get rid of banner + _, _, err := reader.ReadLine() + if err != nil { + if err.Error() == "EOF" { + log.Println("Connection closed by foreign host.") + } + return nil, err + } + } + writer.WriteString("login " + user + " " + pass + "\n") + writer.Flush() + _, _, err = reader.ReadLine() // get rid of "OK" message + if err != nil { + if err.Error() == "EOF" { + log.Println("Connection closed by foreign host.") + } + return nil, err + } + + writer.WriteString("use sid=1\n") + writer.Flush() + _, _, err = reader.ReadLine() // get rid of "OK" message + if err != nil { + if err.Error() == "EOF" { + log.Println("Connection closed by foreign host.") + } + return nil, err + } + + writer.WriteString("clientlist\n") + writer.Flush() + line, _, err := reader.ReadLine() // get rid of "OK" message + if err != nil { + if err.Error() == "EOF" { + log.Println("Connection closed by foreign host.") + } + return nil, err + } + users_raw := string(line) + + re := regexp.MustCompile("client_nickname=(.*?) ") + users_still_raw := re.FindAllString(users_raw, -1) + + for i := range users_still_raw { + users_still_raw[i] = strings.Trim(users_still_raw[i], " ") + users_still_raw[i] = strings.Replace(users_still_raw[i], "client_nickname=", "", 1) + users_still_raw[i] = strings.Replace(users_still_raw[i], "\\s", " ", -1) + } + users := make([]string, 0) + for i := range users_still_raw { + if !strings.HasPrefix(users_still_raw[i], "serveradmin from") { + users = append(users, users_still_raw[i]) + } + } + result := make(map[string]bool) + for i := range users { + result[users[i]] = true + } + conn.Close() + return result, nil +} diff --git a/modules/twitch.go b/modules/twitch.go new file mode 100644 index 0000000..c4b0a60 --- /dev/null +++ b/modules/twitch.go @@ -0,0 +1,243 @@ +// flokatirc twitch module rewritten for WGoSlackBot +// +// Copyright (c) 2016 Daniel Aberger + +package modules + +import ( + "encoding/json" + "io/ioutil" + "log" + "net/http" + "strings" + "time" + + "dev.ixab.de/WGSWATBP/WGoSlackBot/util" +) + +type TwitchChannelObject struct { + _id int `json:"_id"` + _links struct { + Chat string `json:"chat"` + Commercial string `json:"commercial"` + Editors string `json:"editors"` + Features string `json:"features"` + Follows string `json:"follows"` + Self string `json:"self"` + StreamKey string `json:"stream_key"` + Subscriptions string `json:"subscriptions"` + Teams string `json:"teams"` + Videos string `json:"videos"` + } `json:"_links"` + Background interface{} `json:"background"` + Banner string `json:"banner"` + BroadcasterLanguage string `json:"broadcaster_language"` + CreatedAt string `json:"created_at"` + Delay interface{} `json:"delay"` + DisplayName string `json:"display_name"` + Followers int `json:"followers"` + Game string `json:"game"` + Language string `json:"language"` + Logo string `json:"logo"` + Mature bool `json:"mature"` + Name string `json:"name"` + Partner bool `json:"partner"` + ProfileBanner string `json:"profile_banner"` + ProfileBannerBackgroundColor string `json:"profile_banner_background_color"` + Status string `json:"status"` + UpdatedAt string `json:"updated_at"` + URL string `json:"url"` + VideoBanner string `json:"video_banner"` + Views int `json:"views"` +} + +type TwitchStreamObject struct { + _links struct { + Channel string `json:"channel"` + Self string `json:"self"` + } `json:"_links"` + Stream struct { + _id int `json:"_id"` + _links struct { + Self string `json:"self"` + } `json:"_links"` + AverageFps float64 `json:"average_fps"` + Channel struct { + _id int `json:"_id"` + _links struct { + Chat string `json:"chat"` + Commercial string `json:"commercial"` + Editors string `json:"editors"` + Features string `json:"features"` + Follows string `json:"follows"` + Self string `json:"self"` + StreamKey string `json:"stream_key"` + Subscriptions string `json:"subscriptions"` + Teams string `json:"teams"` + Videos string `json:"videos"` + } `json:"_links"` + Background interface{} `json:"background"` + Banner string `json:"banner"` + BroadcasterLanguage string `json:"broadcaster_language"` + CreatedAt string `json:"created_at"` + Delay interface{} `json:"delay"` + DisplayName string `json:"display_name"` + Followers int `json:"followers"` + Game string `json:"game"` + Language string `json:"language"` + Logo string `json:"logo"` + Mature bool `json:"mature"` + Name string `json:"name"` + Partner bool `json:"partner"` + ProfileBanner string `json:"profile_banner"` + ProfileBannerBackgroundColor string `json:"profile_banner_background_color"` + Status string `json:"status"` + UpdatedAt string `json:"updated_at"` + URL string `json:"url"` + VideoBanner string `json:"video_banner"` + Views int `json:"views"` + } `json:"channel"` + CreatedAt string `json:"created_at"` + Delay int `json:"delay"` + Game string `json:"game"` + IsPlaylist bool `json:"is_playlist"` + Preview struct { + Large string `json:"large"` + Medium string `json:"medium"` + Small string `json:"small"` + Template string `json:"template"` + } `json:"preview"` + VideoHeight int `json:"video_height"` + Viewers int `json:"viewers"` + } `json:"stream"` +} + +var ( + twitch = map[string]bool{ + "rocketbeanstv": false, + "blizzard": false, + "ea": false, + "warcraft": false, + "starcraft": false, + "bobross": false, + } + twitchapiurlstreams = "https://api.twitch.tv/kraken/streams/" + twitchapiurlchannels = "https://api.twitch.tv/kraken/channels/" +) + +func init() { + MsgHandlers["twitch"] = twitchHandleMessage + log.Println("Initializing twitch module") + go pollStreamData() +} + +func twitchHandleMessage(payload *WebhookPayload) { + tok := strings.Split(payload.Text, " ") + if len(tok) < 1 { + return + } + switch tok[0] { + case "!twitch": + switch len(tok) { + case 1: + onlinestreams := 0 + for streamname, _ := range twitch { + var so TwitchStreamObject + var co TwitchChannelObject + so = getTwitchStreamObject(streamname) + if so.Stream.Game != "" { + onlinestreams++ + co = getTwitchChannelObject(streamname) + twitchSendMsg(co, so) + twitch[streamname] = true + } else { + twitch[streamname] = false + } + } + if onlinestreams == 0 { + SayCh <- GeneratePayload("@"+payload.UserName, "", "All streams offline", "Twitch_Bot") + } + case 2: + streamname := tok[1] + var so TwitchStreamObject + var co TwitchChannelObject + so = getTwitchStreamObject(streamname) + if so.Stream.Game != "" { + co = getTwitchChannelObject(streamname) + twitchSendMsg(co, so) + } else { + SayCh <- GeneratePayload("@"+payload.UserName, "", streamname+" not found or offline", "Twitch_Bot") + } + default: + } + default: + } +} + +func pollStreamData() { + time.Sleep(10 * time.Second) + for { + for streamname, _ := range twitch { + var so TwitchStreamObject + var co TwitchChannelObject + so = getTwitchStreamObject(streamname) + if so.Stream.Game != "" && !twitch[streamname] { + co = getTwitchChannelObject(streamname) + twitchSendMsg(co, so) + twitch[streamname] = true + } else if so.Stream.Game == "" && twitch[streamname] { + twitch[streamname] = false + } + } + time.Sleep(180 * time.Second) + } +} + +func getTwitchStreamObject(streamname string) TwitchStreamObject { + twsurl := twitchapiurlstreams + streamname + var tobj TwitchStreamObject + resp, err := http.Get(twsurl) + if err != nil { + log.Println(err) + } else { + reader, err := ioutil.ReadAll(resp.Body) + if err != nil { + log.Println(err) + } else { + defer resp.Body.Close() + json.Unmarshal(reader, &tobj) + return tobj + } + } + return tobj +} + +func getTwitchChannelObject(streamname string) TwitchChannelObject { + twcurl := twitchapiurlchannels + streamname + var tcobj TwitchChannelObject + resp2, err := http.Get(twcurl) + if err != nil { + log.Println(err) + } else { + reader2, err := ioutil.ReadAll(resp2.Body) + if err != nil { + log.Println(err) + } else { + defer resp2.Body.Close() + json.Unmarshal(reader2, &tcobj) + return tcobj + } + } + return tcobj +} + +func twitchSendMsg(tcobj TwitchChannelObject, tso TwitchStreamObject) { + SayCh <- GeneratePayload("#wgs-spam", + "", + "*"+tso.Stream.Channel.DisplayName+ + "*\n*Title:* "+tcobj.Status+ + "\n*Viewers:* "+util.NumberToString(tso.Stream.Viewers, '.')+ + "\n*Playing:* "+tso.Stream.Game+ + "\n"+tcobj.URL+"\n", + "Twitch_Bot") +} diff --git a/modules/utility.go b/modules/utility.go new file mode 100644 index 0000000..d101c64 --- /dev/null +++ b/modules/utility.go @@ -0,0 +1,12 @@ +// utility.go +package modules + +func GeneratePayload(target string, icon string, text string, botname string) Payload { + payload := Payload{ + Channel: target, + IconEmoji: icon, + Text: text, + Username: botname, + } + return payload +} diff --git a/util/util.go b/util/util.go new file mode 100644 index 0000000..a3d1d0b --- /dev/null +++ b/util/util.go @@ -0,0 +1,54 @@ +// vi:ts=4:sts=4:sw=4:noet:tw=72 + +package util + +import ( + "bytes" + "math/rand" + "strconv" + "time" +) + +func ToInt(v interface{}) int { + switch v.(type) { + case int: + return v.(int) + case float64: + return int(v.(float64)) + case string: + ret, _ := strconv.Atoi(v.(string)) + return ret + default: + return 0 + } +} + +func NumberToString(n int, sep rune) string { + start := 0 + var buf bytes.Buffer + + s := strconv.Itoa(n) + if n < 0 { + start = 1 + buf.WriteByte('-') + } + l := len(s) + ci := 3 - ((l - start) % 3) + if ci == 3 { + ci = 0 + } + for i := start; i < l; i++ { + if ci == 3 { + buf.WriteRune(sep) + ci = 0 + } + ci++ + buf.WriteByte(s[i]) + } + return buf.String() +} + +func Random(min, max int) int { + rand.Seed(time.Now().Unix()) + return rand.Intn(max-min) + min +}