twitchsingstools/internal/webserver/webserver.go

865 lines
29 KiB
Go
Executable File

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=<your client ID>
// &client_secret=<your client secret>
// &code=<authorization code received above>
// &grant_type=authorization_code
// &redirect_uri=<your registered 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 JSONHandler(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, AugmentSingsVideoStructSlice(channelData.VideoCache), topNSongs, topNSingers}
response.Header().Add("Content-type", "application/json")
if request.URL.Path[0:5] == "/json" {
tmpl := template.Must(template.ParseFiles("web/data.json"))
tmpl.Execute(response, td)
} else if request.URL.Path[0:9] == "/topsongs" {
tmpl := template.Must(template.ParseFiles("web/topsongs.json"))
tmpl.Execute(response, td)
} else { // top 10 singers!
tmpl := template.Must(template.ParseFiles("web/topsingers.json"))
tmpl.Execute(response, td)
}
}
func ReactIndexHandler(entrypoint string) func(w http.ResponseWriter, r *http.Request) {
fn := func(w http.ResponseWriter, r *http.Request) {
http.ServeFile(w, r, entrypoint)
}
return http.HandlerFunc(fn)
}
func HandleHTTP(passedIrcBot *irc.KardBot) {
ircBot = passedIrcBot
r := mux.NewRouter()
loggedRouter := handlers.LoggingHandler(os.Stdout, r)
r.NotFoundHandler = http.HandlerFunc(NotFoundHandler)
r.HandleFunc("/healthz", HealthHandler)
r.HandleFunc("/web/{.*}", TemplateHandler)
r.HandleFunc("/csv/{channel}/{key}", CSVHandler)
r.HandleFunc("/tsv/{channel}/{key}", CSVHandler)
r.HandleFunc("/json/{channel}/{key}", JSONHandler)
r.HandleFunc("/topsongs/{channel}/{key}", JSONHandler)
r.HandleFunc("/topsingers/{channel}/{key}", JSONHandler)
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)
r.PathPrefix("/static").Handler(http.StripPrefix("/static", http.FileServer(http.Dir("./web/react-frontend/static"))))
r.PathPrefix("/").HandlerFunc(ReactIndexHandler("./web/react-frontend/index.html"))
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()
}