package webserver import ( "container/heap" "errors" "math/rand" "regexp" "sort" "unicode" irc "git.martyn.berlin/martyn/twitchsingstools/internal/irc" "github.com/dustin/go-humanize" "github.com/gorilla/handlers" "github.com/gorilla/mux" "encoding/json" "fmt" "html/template" "io/ioutil" "net/http" "net/url" "os" "strings" "time" ) //var store = sessions.NewCookieStore(os.Getenv("SESSION_KEY")) type twitchauthresponse struct { Access_token string `json: "access_token"` Expires_in int `json: "expires_in"` Refresh_token string `json: "refresh_token"` Scope []string `json: "scope"` Token_type string `json: "token_type"` } type twitchUser struct { Id string `json: "id"` Login string `json: "login"` Display_name string `json: "display_name"` Type string `json: "type"` Broadcaster_type string `json: "affiliate"` Description string `json: "description"` Profile_image_url string `json: "profile_image_url"` Offline_image_url string `json: "offline_image_url"` View_count int `json: "view_count"` } type twitchUsersBigResponse struct { Data []twitchUser `json:"data"` } type twitchCursor struct { Cursor string `json:"cursor"` } type videoStruct struct { CreatedAt string `json:"created_at"` // Date when the video was created. Description string `json:"description"` // Description of the video. Duration string `json:"duration"` // Length of the video. ID string `json:"id"` // ID of the video. Language string `json:"language"` // Language of the video. Pagination string `json:"pagination"` // A cursor value, to be used in a subsequent request to specify the starting point of the next set of results. PublishedAt string `json:"published_at"` // Date when the video was published. ThumbnailURL string `json:"thumbnail_url"` // Template URL for the thumbnail of the video. Title string `json:"title"` // Title of the video. Type string `json:"type"` // Type of video. Valid values: "upload", "archive", "highlight". URL string `json:"url"` // URL of the video. UserID string `json:"user_id"` // ID of the user who owns the video. UserName string `json:"user_name"` // Display name corresponding to user_id. ViewCount int `json:"view_count"` // Number of times the video has been viewed. Viewable string `json:"viewable"` // Indicates whether the video is publicly viewable. Valid values: "public", "private". } type videosResponse struct { Data []videoStruct `json:"data"` Pagination twitchCursor `json:"pagination"` } var ircBot *irc.KardBot func HealthHandler(response http.ResponseWriter, request *http.Request) { response.Header().Add("Content-type", "text/plain") fmt.Fprint(response, "I'm okay jack!") } func NotFoundHandler(response http.ResponseWriter, request *http.Request) { response.Header().Add("X-Template-File", "html"+request.URL.Path) response.WriteHeader(404) tmpl := template.Must(template.ParseFiles("web/404.html")) tmpl.Execute(response, nil) } func CSSHandler(response http.ResponseWriter, request *http.Request) { response.Header().Add("Content-type", "text/css") tmpl := template.Must(template.ParseFiles("web/cover.css")) tmpl.Execute(response, nil) } func RootHandler(response http.ResponseWriter, request *http.Request) { request.URL.Path = "/index.html" TemplateHandler(response, request) } func TemplateHandler(response http.ResponseWriter, request *http.Request) { response.Header().Add("X-Template-File", "web"+request.URL.Path) type TemplateData struct { Prompt string AvailCount int ChannelCount int MessageCount int ClientID string BaseURI string } _ = strings.ToLower("Hello") if strings.Index(request.URL.Path, "/") < 0 { http.Error(response, "No slashes wat - "+request.URL.Path, http.StatusInternalServerError) return } basenameSlice := strings.Split(request.URL.Path, "/") basename := basenameSlice[len(basenameSlice)-1] //fmt.Fprintf(response, "%q", basenameSlice) tmpl, err := template.New(basename).Funcs(template.FuncMap{ "ToUpper": strings.ToUpper, "ToLower": strings.ToLower, }).ParseFiles("web" + request.URL.Path) if err != nil { http.Error(response, err.Error(), http.StatusInternalServerError) return // NotFoundHandler(response, request) // return } var td = TemplateData{ircBot.Prompts[rand.Intn(len(ircBot.Prompts))], len(ircBot.Prompts), ircBot.ActiveChannels(), 0, ircBot.AppCredentials.ClientID, "https://" + ircBot.Config.ExternalUrl} err = tmpl.Execute(response, td) if err != nil { http.Error(response, err.Error(), http.StatusInternalServerError) return } } func LeaveHandler(response http.ResponseWriter, request *http.Request) { request.URL.Path = "/bye.html" TemplateHandler(response, request) } func humanTimeFromTimeString(s string) string { return s // format := "2006-01-02 15:04:05 +0000 UTC" // d, _ := time.Parse(format, s) // return humanize.Time(d) } type AugmentedSingsVideoStruct struct { Date time.Time NiceDate string FullTitle string Duet bool OtherSinger string SongTitle string LastSungSong time.Time NiceLastSungSong string LastSungSinger time.Time NiceLastSungSinger string } func AugmentSingsVideoStructForCSV(input irc.SingsVideoStruct) AugmentedSingsVideoStruct { var ret AugmentedSingsVideoStruct ret.Date = input.Date ret.NiceDate = input.Date.Format("2006-01-02 15:04:05") ret.FullTitle = input.FullTitle ret.Duet = input.Duet ret.OtherSinger = input.OtherSinger ret.SongTitle = input.SongTitle ret.LastSungSong = input.LastSungSong ret.NiceLastSungSong = input.LastSungSong.Format("2006-01-02 15:04:05") ret.LastSungSinger = input.LastSungSinger if !ret.Duet { ret.NiceLastSungSinger = "Solo performance" } else { ret.NiceLastSungSinger = input.LastSungSinger.Format("2006-01-02 15:04:05") } return ret } func AugmentSingsVideoStructSliceForCSV(input []irc.SingsVideoStruct) []AugmentedSingsVideoStruct { ret := make([]AugmentedSingsVideoStruct, 0) for _, record := range input { ret = append(ret, AugmentSingsVideoStructForCSV(record)) } return ret } func AugmentSingsVideoStruct(input irc.SingsVideoStruct) AugmentedSingsVideoStruct { var ret AugmentedSingsVideoStruct ret.Date = input.Date ret.NiceDate = humanize.Time(input.Date) ret.FullTitle = input.FullTitle ret.Duet = input.Duet ret.OtherSinger = input.OtherSinger ret.SongTitle = input.SongTitle ret.LastSungSong = input.LastSungSong ret.NiceLastSungSong = humanize.Time(input.LastSungSong) ret.LastSungSinger = input.LastSungSinger if !ret.Duet { ret.NiceLastSungSinger = "Solo performance" } else { ret.NiceLastSungSinger = humanize.Time(input.LastSungSinger) } return ret } func AugmentSingsVideoStructSlice(input []irc.SingsVideoStruct) []AugmentedSingsVideoStruct { ret := make([]AugmentedSingsVideoStruct, 0) for _, record := range input { ret = append(ret, AugmentSingsVideoStruct(record)) } return ret } func AdminHandler(response http.ResponseWriter, request *http.Request) { vars := mux.Vars(request) if vars["key"] != ircBot.ChannelData[vars["channel"]].AdminKey { UnauthorizedHandler(response, request) return } type TemplateData struct { Channel string Command string ExtraStrings string SinceTime time.Time SinceTimeUTC string Leaving bool HasLeft bool SongData []AugmentedSingsVideoStruct TopNSongs []SongSings TopNSingers []SingerSings ChannelKey string } channelData := ircBot.ChannelData[vars["channel"]] if time.Now().Sub(ircBot.ChannelData[vars["channel"]].VideoCacheUpdated).Hours() > 1 { fmt.Printf("Cache of %d performances is older than an hour - %.1f hours old to be precise... fetching.\n", len(ircBot.ChannelData[vars["channel"]].VideoCache), time.Now().Sub(ircBot.ChannelData[vars["channel"]].VideoCacheUpdated).Hours()) vids, err := fetchAllVoDs(channelData.TwitchUserID, channelData.Bearer) if err != nil { errCache := make([]irc.SingsVideoStruct, 0) var ret irc.SingsVideoStruct ret.FullTitle = "Error fetching videos: " + err.Error() errCache = append(errCache, ret) vids = errCache } updateCalculatedFields(vids) ircBot.UpdateVideoCache(vars["channel"], vids) } else { fmt.Printf("Cache of %d performances is younger than an hour - %.1f hours old to be precise... not fetching.\n", len(ircBot.ChannelData[vars["channel"]].VideoCache), time.Now().Sub(ircBot.ChannelData[vars["channel"]].VideoCacheUpdated).Hours()) } updateCalculatedFields(channelData.VideoCache) for _, song := range channelData.VideoCache { if song.Duet && song.OtherSinger == "" { fmt.Printf("WARNING: found duet with no other singer! %s", song.SongTitle) // should never happen but debug in case it does! } } topNSongs := calculateTopNSongs(channelData.VideoCache, 10) topNSingers := calculateTopNSingers(channelData.VideoCache, 10) var td = TemplateData{channelData.Name, channelData.Command, channelData.ExtraStrings, channelData.JoinTime, channelData.JoinTime.Format(irc.UTCFormat), false, channelData.HasLeft, AugmentSingsVideoStructSlice(channelData.VideoCache), topNSongs, topNSingers, vars["key"]} if request.Method == "POST" { request.ParseForm() if strings.Join(request.PostForm["leave"], ",") == "Leave twitch channel" { td.Leaving = true } else if strings.Join(request.PostForm["reallyleave"], ",") == "Really leave twitch channel" { record := ircBot.ChannelData[vars["channel"]] record.HasLeft = true ircBot.ChannelData[vars["channel"]] = record ircBot.LeaveChannel(vars["channel"]) ircBot.Database.Write("channelData", vars["channel"], record) LeaveHandler(response, request) return } if strings.Join(request.PostForm["join"], ",") == "Come on in" { record := ircBot.ChannelData[vars["channel"]] td.HasLeft = false record.Name = vars["channel"] record.JoinTime = time.Now() record.HasLeft = false if record.Command == "" { record.Command = "card" } ircBot.Database.Write("channelData", vars["channel"], record) ircBot.ChannelData[vars["channel"]] = record td = TemplateData{record.Name, record.Command, record.ExtraStrings, record.JoinTime, record.JoinTime.Format(irc.UTCFormat), false, record.HasLeft, AugmentSingsVideoStructSlice(channelData.VideoCache), topNSongs, topNSingers, vars["key"]} ircBot.JoinChannel(record.Name) } sourceData := ircBot.ChannelData[vars["channel"]] if strings.Join(request.PostForm["Command"], ",") != "" { sourceData.Command = strings.Join(request.PostForm["Command"], ",") td.Command = sourceData.Command ircBot.ChannelData[vars["channel"]] = sourceData } if strings.Join(request.PostForm["ExtraStrings"], ",") != sourceData.ExtraStrings { sourceData.ExtraStrings = strings.Join(request.PostForm["ExtraStrings"], ",") td.ExtraStrings = sourceData.ExtraStrings ircBot.ChannelData[vars["channel"]] = sourceData } ircBot.Database.Write("channelData", vars["channel"], sourceData) } tmpl, err := template.New("admin.html").ParseFiles("web/admin.html") if err != nil { panic(err.Error()) } tmpl.Execute(response, td) } func UnauthorizedHandler(response http.ResponseWriter, request *http.Request) { response.Header().Add("X-Template-File", "html"+request.URL.Path) response.WriteHeader(401) tmpl := template.Must(template.ParseFiles("web/401.html")) tmpl.Execute(response, nil) } func twitchHTTPClient(call string, bearer string) (string, error) { url := "https://api.twitch.tv/helix/" + call var bearerHeader = "Bearer " + bearer req, err := http.NewRequest("GET", url, nil) req.Header.Add("Client-ID", ircBot.AppCredentials.ClientID) req.Header.Add("Authorization", bearerHeader) client := &http.Client{} resp, err := client.Do(req) if err != nil { return "", err } defer resp.Body.Close() body, _ := ioutil.ReadAll(resp.Body) return string([]byte(body)), nil } func ValidateTwitchBearerToken(bearer string) (bool, error) { url := "https://id.twitch.tv/oauth2/validate" var bearerHeader = "Bearer " + bearer req, err := http.NewRequest("GET", url, nil) req.Header.Add("Client-ID", ircBot.AppCredentials.ClientID) req.Header.Add("Authorization", bearerHeader) client := &http.Client{} resp, err := client.Do(req) if err != nil { return false, err } defer resp.Body.Close() _, _ = ioutil.ReadAll(resp.Body) return resp.StatusCode == 200, nil } func twitchVidToSingsVid(twitchFormat videoStruct) (irc.SingsVideoStruct, error) { var ret irc.SingsVideoStruct layout := "2006-01-02T15:04:05Z" var d time.Time d, err := time.Parse(layout, twitchFormat.CreatedAt) if err != nil { return ret, err } ret.Date = d var DuetRegex = regexp.MustCompile(`^Duet with ([^ ]*): (.*)$`) matches := DuetRegex.FindAllStringSubmatch(twitchFormat.Title, -1) if matches == nil { var SoloRegex = regexp.MustCompile(`^Solo performance: (.*)$`) matches := SoloRegex.FindAllStringSubmatch(twitchFormat.Title, -1) if matches == nil { ret.SongTitle = "Does not match either solo or duet performance regex " ret.FullTitle = twitchFormat.Title return ret, errors.New("Does not match either solo or duet performance regex : " + twitchFormat.Title) } ret.Duet = false ret.SongTitle = matches[0][1] ret.FullTitle = twitchFormat.Title return ret, nil } ret.Duet = true ret.OtherSinger = matches[0][1] ret.SongTitle = matches[0][2] ret.FullTitle = twitchFormat.Title return ret, nil } type SongSings struct { SongTitle string Sings int } type SingerSings struct { SingerName string Sings int } func calculateTopNSongs(songCache []irc.SingsVideoStruct, howMany int) []SongSings { songMap := map[string]int{} for _, record := range songCache { sings := songMap[record.SongTitle] sings += 1 songMap[record.SongTitle] = sings } slice := make([]SongSings, 0) for key, value := range songMap { ss := SongSings{key, value} slice = append(slice, ss) } sort.SliceStable(slice, func(i, j int) bool { return slice[i].Sings > slice[j].Sings }) var ret []SongSings for i := 1; i <= howMany; i++ { ret = append(ret, slice[i]) } return ret } func IsLower(s string) bool { for _, r := range s { if !unicode.IsLower(r) && unicode.IsLetter(r) { return false } } return true } func unmangleSingerName(MangledCaseName string, songCache []irc.SingsVideoStruct) string { options := make(map[string]string, 0) for _, record := range songCache { if strings.ToUpper(MangledCaseName) == strings.ToUpper(record.OtherSinger) { options[record.OtherSinger] = "WHATEVER" } } // One answer means we don't care upper, lower, mixed. if len(options) == 1 { for key := range options { return key } } // More than one is probably closed-beta where the name was lowercased. for key := range options { if !IsLower(key) { return key } } // Eep, we shouldn't get here, let's just return something. for key := range options { return key } return "" } func prependSong(x []SingerSings, y SingerSings) []SingerSings { x = append(x, SingerSings{}) copy(x[1:], x) x[0] = y return x } type kv struct { Key string Value int } func getHeap(m map[string]int) *KVHeap { h := &KVHeap{} heap.Init(h) for k, v := range m { heap.Push(h, kv{k, v}) } return h } type KVHeap []kv func (h KVHeap) Len() int { return len(h) } func (h KVHeap) Less(i, j int) bool { return h[i].Value > h[j].Value } func (h KVHeap) Swap(i, j int) { h[i], h[j] = h[j], h[i] } func (h *KVHeap) Push(x interface{}) { *h = append(*h, x.(kv)) } func (h *KVHeap) Pop() interface{} { old := *h n := len(old) x := old[n-1] *h = old[0 : n-1] return x } func calculateTopNSingers(songCache []irc.SingsVideoStruct, howMany int) []SingerSings { songMap := map[string]int{} songCount := 0 for _, record := range songCache { if record.Duet { sings := songMap[strings.ToUpper(record.OtherSinger)] sings++ songCount++ songMap[strings.ToUpper(record.OtherSinger)] = sings } } slice := make([]SingerSings, 0) h := getHeap(songMap) for i := 0; i < howMany; i++ { deets := heap.Pop(h) position := i + 1 fmt.Printf("%d) %#v\n", position, deets) ss := SingerSings{unmangleSingerName(deets.(kv).Key, songCache), deets.(kv).Value} slice = append(slice, ss) } fmt.Printf("Considered %d songs, Shan has %d\n", songCount, songMap["SHANXOX_"]) return slice } func calculateLastSungSongDate(songCache []irc.SingsVideoStruct, SongTitle string) time.Time { var t time.Time for _, record := range songCache { if record.SongTitle == SongTitle { if record.Date.After(t) { t = record.Date } } } return t } func calculateLastSungSingerDate(songCache []irc.SingsVideoStruct, Singer string) time.Time { var t time.Time for _, record := range songCache { if strings.ToUpper(record.OtherSinger) == strings.ToUpper(Singer) { if record.Date.After(t) { t = record.Date if strings.ToUpper(Singer) == "SHANXOX_" { fmt.Printf("Last sang with %s (%s) %s", Singer, record.OtherSinger, t) } } } } return t } func updateCalculatedFields(songCache []irc.SingsVideoStruct) { for i, record := range songCache { if record.Duet { songCache[i].LastSungSinger = calculateLastSungSingerDate(songCache, record.OtherSinger) } songCache[i].LastSungSong = calculateLastSungSongDate(songCache, record.SongTitle) } } func fetchVoDsPagesRecursive(userID string, bearer string, from string) ([]irc.SingsVideoStruct, error) { url := "" if from == "" { url = "videos?user_id=" + userID + "&first=100&type=upload" } else { url = "videos?user_id=" + userID + "&first=100&type=upload&after=" + from } vidResponse, err := twitchHTTPClient(url, bearer) if err != nil { return nil, err } var fullResponse videosResponse err = json.Unmarshal([]byte(vidResponse), &fullResponse) if err != nil { return nil, err } titles := make([]irc.SingsVideoStruct, 0) for _, videoData := range fullResponse.Data { ret, err := twitchVidToSingsVid(videoData) if err != nil { titles = append(titles, ret) } else { titles = append(titles, ret) } } if fullResponse.Pagination.Cursor != "" { fmt.Println("NOTICE: Recursion needed, cursor is " + fullResponse.Pagination.Cursor) recurse, err := fetchVoDsPagesRecursive(userID, bearer, fullResponse.Pagination.Cursor) if err != nil { fmt.Println("ERROR: Bailing out during recursion because of error : " + err.Error()) } titles = append(titles, recurse...) } return titles, nil } func fetchAllVoDs(userID string, bearer string) ([]irc.SingsVideoStruct, error) { tokenValid, err := ValidateTwitchBearerToken(bearer) if err != nil { return nil, err } if !tokenValid { return nil, errors.New("Failed to validate token with twitch (authorization revoked?!)") } titles, err := fetchVoDsPagesRecursive(userID, bearer, "") if err != nil { return make([]irc.SingsVideoStruct, 0), err } return titles, nil } func TwitchAdminHandler(response http.ResponseWriter, request *http.Request) { vars := mux.Vars(request) if vars["code"] != "" { response.Header().Add("Content-type", "text/plain") resp, err := http.PostForm( "https://id.twitch.tv/oauth2/token", url.Values{ "client_id": {ircBot.AppCredentials.ClientID}, "client_secret": {ircBot.AppCredentials.ClientSecret}, "code": {vars["code"]}, "grant_type": {"authorization_code"}, "redirect_uri": {"https://" + ircBot.Config.ExternalUrl + "/twitchadmin"}}) if err != nil { response.WriteHeader(500) response.Header().Add("Content-type", "text/plain") fmt.Fprint(response, "ERROR: "+err.Error()) return } defer resp.Body.Close() body, err := ioutil.ReadAll(resp.Body) if err != nil { response.WriteHeader(500) response.Header().Add("Content-type", "text/plain") fmt.Fprint(response, "ERROR: "+err.Error()) return } var oauthResponse twitchauthresponse err = json.Unmarshal(body, &oauthResponse) if err != nil { response.WriteHeader(500) response.Header().Add("Content-type", "text/plain") fmt.Fprint(response, "ERROR: "+err.Error()) return } usersResponse, err := twitchHTTPClient("users", oauthResponse.Access_token) if err != nil { response.WriteHeader(500) response.Header().Add("Content-type", "text/plain") fmt.Fprint(response, "ERROR: "+err.Error()) return } var usersObject twitchUsersBigResponse err = json.Unmarshal([]byte(usersResponse), &usersObject) if err != nil { response.WriteHeader(500) response.Header().Add("Content-type", "text/plain") fmt.Fprint(response, "ERROR: "+err.Error()) return } if len(usersObject.Data) != 1 { response.WriteHeader(500) response.Header().Add("Content-type", "text/plain") fmt.Fprint(response, "ERROR: Twitch returned not 1 user for the request!\n---\n") return } user := usersObject.Data[0] magicCode := ircBot.ReadOrCreateChannelKey(user.Login) ircBot.UpdateBearerToken(user.Login, oauthResponse.Access_token) ircBot.UpdateTwitchUserID(user.Login, user.Id) url := "https://" + ircBot.Config.ExternalUrl + "/admin/" + user.Login + "/" + magicCode http.Redirect(response, request, url, http.StatusFound) } else { fmt.Fprintf(response, "I'm not okay jack! %v \n", vars) for key, val := range vars { fmt.Fprintf(response, "%s = %s\n", key, val) } } } func TwitchBackendHandler(response http.ResponseWriter, request *http.Request) { response.Header().Add("Content-type", "text/plain") vars := mux.Vars(request) // fmt.Fprintf(response, "I'm okay jack! %v \n", vars) // for key, val := range(vars) { // fmt.Fprint(response, "%s = %s\n", key, val) // } if vars["code"] != "" { // https://id.twitch.tv/oauth2/token // ?client_id= // &client_secret= // &code= // &grant_type=authorization_code // &redirect_uri= // ircBot.AppCredentials.ClientID // ircBot.AppCredentials.Password // vars["oauthtoken"] // authorization_code // "https://"+ircBot.Config.ExternalUrl+/twitchadmin fmt.Println("Asking twitch for more...") resp, err := http.PostForm( "https://id.twitch.tv/oauth2/token", url.Values{ "client_id": {ircBot.AppCredentials.ClientID}, "client_secret": {ircBot.AppCredentials.ClientSecret}, "code": {vars["code"]}, "grant_type": {"authorization_code"}, "redirect_uri": {"https://" + ircBot.Config.ExternalUrl + "/twitchadmin"}}) if err != nil { response.Header().Add("Content-type", "text/plain") fmt.Fprint(response, "ERROR: "+err.Error()) } defer resp.Body.Close() body, err := ioutil.ReadAll(resp.Body) if err != nil { response.Header().Add("Content-type", "text/plain") fmt.Fprint(response, "ERROR: "+err.Error()) } response.Header().Add("Content-type", "text/plain") fmt.Fprint(response, string(body)) } else { UnauthorizedHandler(response, request) } } func CSVHandler(response http.ResponseWriter, request *http.Request) { vars := mux.Vars(request) if vars["key"] != ircBot.ChannelData[vars["channel"]].AdminKey { UnauthorizedHandler(response, request) return } type TemplateData struct { Channel string Command string ExtraStrings string SinceTime time.Time SinceTimeUTC string Leaving bool HasLeft bool SongData []AugmentedSingsVideoStruct TopNSongs []SongSings TopNSingers []SingerSings } channelData := ircBot.ChannelData[vars["channel"]] if time.Now().Sub(ircBot.ChannelData[vars["channel"]].VideoCacheUpdated).Hours() > 1 { fmt.Printf("Cache of %d performances is older than an hour - %.1f hours old to be precise... fetching.\n", len(ircBot.ChannelData[vars["channel"]].VideoCache), time.Now().Sub(ircBot.ChannelData[vars["channel"]].VideoCacheUpdated).Hours()) vids, err := fetchAllVoDs(channelData.TwitchUserID, channelData.Bearer) if err != nil { errCache := make([]irc.SingsVideoStruct, 0) var ret irc.SingsVideoStruct ret.FullTitle = "Error fetching videos: " + err.Error() errCache = append(errCache, ret) vids = errCache } updateCalculatedFields(vids) ircBot.UpdateVideoCache(vars["channel"], vids) } else { fmt.Printf("Cache of %d performances is younger than an hour - %.1f hours old to be precise... not fetching.\n", len(ircBot.ChannelData[vars["channel"]].VideoCache), time.Now().Sub(ircBot.ChannelData[vars["channel"]].VideoCacheUpdated).Hours()) } topNSongs := calculateTopNSongs(channelData.VideoCache, 10) topNSingers := calculateTopNSingers(channelData.VideoCache, 10) var td = TemplateData{channelData.Name, channelData.Command, channelData.ExtraStrings, channelData.JoinTime, channelData.JoinTime.Format(irc.UTCFormat), false, channelData.HasLeft, AugmentSingsVideoStructSliceForCSV(channelData.VideoCache), topNSongs, topNSingers} if request.URL.Path[0:4] == "/csv" { response.Header().Add("Content-Disposition", "attachment; filename=\"duets.csv\"") response.Header().Add("Content-type", "text/csv") tmpl := template.Must(template.ParseFiles("web/data.csv")) tmpl.Execute(response, td) } else { response.Header().Add("Content-Disposition", "attachment; filename=\"duets.tsv\"") response.Header().Add("Content-type", "text/tab-separated-values") tmpl := template.Must(template.ParseFiles("web/data.tsv")) tmpl.Execute(response, td) } } func HandleHTTP(passedIrcBot *irc.KardBot) { ircBot = passedIrcBot r := mux.NewRouter() loggedRouter := handlers.LoggingHandler(os.Stdout, r) r.NotFoundHandler = http.HandlerFunc(NotFoundHandler) r.HandleFunc("/", RootHandler) r.HandleFunc("/healthz", HealthHandler) r.HandleFunc("/web/{.*}", TemplateHandler) r.PathPrefix("/static/").Handler(http.FileServer(http.Dir("./web/"))) r.HandleFunc("/cover.css", CSSHandler) r.HandleFunc("/admin/{channel}/{key}", AdminHandler) r.HandleFunc("/csv/{channel}/{key}", CSVHandler) r.HandleFunc("/tsv/{channel}/{key}", CSVHandler) //r.HandleFunc("/twitchadmin", TwitchAdminHandler) //r.HandleFunc("/twitchtobackend", TwitchBackendHandler) r.Path("/twitchtobackend").Queries("access_token", "{access_token}", "scope", "{scope}", "token_type", "{token_type}").HandlerFunc(TwitchBackendHandler) r.Path("/twitchadmin").Queries("code", "{code}", "scope", "{scope}").HandlerFunc(TwitchAdminHandler) http.Handle("/", r) srv := &http.Server{ Handler: loggedRouter, Addr: "0.0.0.0:5353", WriteTimeout: 15 * time.Second, ReadTimeout: 15 * time.Second, } fmt.Println("Listening on 0.0.0.0:5353") srv.ListenAndServe() }