2020-07-14 18:56:20 +00:00
package webserver
import (
2020-07-16 20:00:49 +00:00
"container/heap"
2020-07-15 19:19:06 +00:00
"errors"
2020-07-14 18:56:20 +00:00
"math/rand"
2020-07-15 19:19:06 +00:00
"regexp"
"sort"
2020-08-01 10:34:06 +00:00
"sync"
2020-07-16 20:00:49 +00:00
"unicode"
2020-07-14 18:56:20 +00:00
2020-09-05 16:14:56 +00:00
data "git.martyn.berlin/martyn/twitchsingstools/internal/data"
2020-07-14 18:56:20 +00:00
irc "git.martyn.berlin/martyn/twitchsingstools/internal/irc"
2020-07-15 19:19:06 +00:00
"github.com/dustin/go-humanize"
2020-07-14 18:56:20 +00:00
"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" `
}
2020-07-15 19:19:06 +00:00
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" `
}
2020-07-14 18:56:20 +00:00
var ircBot * irc . KardBot
2020-09-05 16:14:56 +00:00
var globalData * data . GlobalData
2020-07-14 18:56:20 +00:00
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
}
2020-09-05 16:14:56 +00:00
var td = TemplateData { ircBot . Prompts [ rand . Intn ( len ( ircBot . Prompts ) ) ] , len ( ircBot . Prompts ) , ircBot . ActiveChannels ( ) , 0 , ircBot . AppCredentials . ClientID , "https://" + globalData . Config . ExternalURL }
2020-07-14 18:56:20 +00:00
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 )
}
2020-07-15 19:19:06 +00:00
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)
}
2020-07-16 10:34:29 +00:00
type AugmentedSingsVideoStruct struct {
Date time . Time
NiceDate string
2020-09-05 16:14:56 +00:00
ShortDate string
2020-07-16 10:34:29 +00:00
FullTitle string
Duet bool
OtherSinger string
SongTitle string
LastSungSong time . Time
NiceLastSungSong string
LastSungSinger time . Time
NiceLastSungSinger string
2020-09-05 16:14:56 +00:00
VideoURL string
VideoNumber string //yes, I don't care any more.
2020-07-16 10:34:29 +00:00
}
2020-09-05 16:14:56 +00:00
func AugmentSingsVideoStructForCSV ( input data . SingsVideoStruct ) AugmentedSingsVideoStruct {
2020-07-16 10:34:29 +00:00
var ret AugmentedSingsVideoStruct
ret . Date = input . Date
ret . NiceDate = input . Date . Format ( "2006-01-02 15:04:05" )
2020-09-05 16:14:56 +00:00
ret . ShortDate = input . Date . Format ( "2006-01-02" )
2020-07-16 10:34:29 +00:00
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
2020-09-05 16:14:56 +00:00
ret . VideoURL = input . VideoURL
urlParts := strings . Split ( input . VideoURL , "/" )
ret . VideoNumber = urlParts [ len ( urlParts ) - 1 ]
2020-07-16 10:34:29 +00:00
if ! ret . Duet {
ret . NiceLastSungSinger = "Solo performance"
} else {
ret . NiceLastSungSinger = input . LastSungSinger . Format ( "2006-01-02 15:04:05" )
}
return ret
}
2020-09-05 16:14:56 +00:00
func AugmentSingsVideoStructSliceForCSV ( input [ ] data . SingsVideoStruct ) [ ] AugmentedSingsVideoStruct {
2020-07-16 10:34:29 +00:00
ret := make ( [ ] AugmentedSingsVideoStruct , 0 )
for _ , record := range input {
ret = append ( ret , AugmentSingsVideoStructForCSV ( record ) )
}
return ret
}
2020-09-05 16:14:56 +00:00
func AugmentSingsVideoStruct ( input data . SingsVideoStruct ) AugmentedSingsVideoStruct {
2020-07-16 10:34:29 +00:00
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
}
2020-09-05 16:14:56 +00:00
func AugmentSingsVideoStructSlice ( input [ ] data . SingsVideoStruct ) [ ] AugmentedSingsVideoStruct {
2020-07-16 10:34:29 +00:00
ret := make ( [ ] AugmentedSingsVideoStruct , 0 )
for _ , record := range input {
ret = append ( ret , AugmentSingsVideoStruct ( record ) )
}
return ret
}
2020-07-14 18:56:20 +00:00
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 )
2020-08-02 22:34:04 +00:00
if resp . StatusCode != 200 {
return string ( http . StatusText ( resp . StatusCode ) ) , errors . New ( "HTTP ERROR: " + http . StatusText ( resp . StatusCode ) )
} else {
return string ( [ ] byte ( body ) ) , nil
}
2020-07-14 18:56:20 +00:00
}
2020-07-15 19:19:06 +00:00
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
}
2020-09-05 16:14:56 +00:00
func twitchVidToSingsVid ( twitchFormat videoStruct ) ( data . SingsVideoStruct , error ) {
var ret data . SingsVideoStruct
2020-07-15 19:19:06 +00:00
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
2020-09-05 16:14:56 +00:00
ret . VideoURL = twitchFormat . URL
2020-07-15 19:19:06 +00:00
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 {
2020-07-16 10:34:29 +00:00
SingerName string
Sings int
2020-07-15 19:19:06 +00:00
}
2020-09-05 16:14:56 +00:00
func calculateTopNSongs ( songCache [ ] data . SingsVideoStruct , howMany int ) [ ] SongSings {
2020-07-15 19:19:06 +00:00
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
}
2020-07-16 20:00:49 +00:00
func IsLower ( s string ) bool {
for _ , r := range s {
if ! unicode . IsLower ( r ) && unicode . IsLetter ( r ) {
return false
}
}
return true
}
2020-09-05 16:14:56 +00:00
func unmangleSingerName ( MangledCaseName string , songCache [ ] data . SingsVideoStruct ) string {
2020-07-16 20:00:49 +00:00
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
}
2020-08-01 10:34:06 +00:00
var cacheLock sync . Mutex
2020-08-02 22:34:04 +00:00
type CacheDetails struct {
Age time . Duration ` json: "cache_age" `
AgeStr string ` json: "cache_age_nice" `
SongCount int ` json: "expires_in" `
}
func getCacheDetails ( channel string ) CacheDetails {
var ret CacheDetails
2020-09-05 16:14:56 +00:00
channelData := globalData . ChannelData [ channel ]
2020-08-02 22:34:04 +00:00
ret . Age = time . Now ( ) . Sub ( channelData . VideoCacheUpdated )
ret . AgeStr = humanize . Time ( channelData . VideoCacheUpdated )
ret . SongCount = len ( channelData . VideoCache )
return ret
}
func forceUpdateCache ( channel string ) {
fmt . Printf ( "Forcing cache update!" )
2020-09-05 16:14:56 +00:00
channelData := globalData . ChannelData [ channel ]
2020-08-02 22:34:04 +00:00
tenHours := time . Hour * - 10
videoCacheUpdated := time . Now ( ) . Add ( tenHours ) // Subtract 10 hours from now, cache is 10 hours old.
channelData . VideoCacheUpdated = videoCacheUpdated
2020-09-05 16:14:56 +00:00
globalData . ChannelData [ channel ] = channelData
2020-08-02 22:34:04 +00:00
updateCacheIfNecessary ( channel )
}
2020-08-01 10:34:06 +00:00
func updateCacheIfNecessary ( channel string ) {
cacheLock . Lock ( )
2020-09-05 16:14:56 +00:00
channelData := globalData . ChannelData [ channel ]
2020-08-01 10:34:06 +00:00
if time . Now ( ) . Sub ( channelData . VideoCacheUpdated ) . Hours ( ) > 1 {
2020-09-05 16:14:56 +00:00
fmt . Printf ( "Cache of %d performances is older than an hour - %.1f hours old to be precise... fetching.\n" , len ( channelData . VideoCache ) , time . Now ( ) . Sub ( globalData . ChannelData [ channel ] . VideoCacheUpdated ) . Hours ( ) )
2020-08-01 10:34:06 +00:00
vids , err := fetchAllVoDs ( channelData . TwitchUserID , channelData . Bearer )
if err != nil {
2020-09-05 16:14:56 +00:00
errCache := make ( [ ] data . SingsVideoStruct , 0 )
var ret data . SingsVideoStruct
2020-08-01 10:34:06 +00:00
ret . FullTitle = "Error fetching videos: " + err . Error ( )
errCache = append ( errCache , ret )
vids = errCache
}
updateCalculatedFields ( vids )
2020-09-05 16:14:56 +00:00
globalData . UpdateVideoCache ( channel , vids )
2020-08-01 10:34:06 +00:00
} else {
2020-09-05 16:14:56 +00:00
fmt . Printf ( "Cache of %d performances is younger than an hour - %.1f hours old to be precise... not fetching.\n" , len ( channelData . VideoCache ) , time . Now ( ) . Sub ( globalData . ChannelData [ channel ] . VideoCacheUpdated ) . Hours ( ) )
2020-08-01 10:34:06 +00:00
}
cacheLock . Unlock ( )
}
2020-09-05 16:14:56 +00:00
func calculateTopNSingers ( songCache [ ] data . SingsVideoStruct , howMany int ) [ ] SingerSings {
2020-07-16 10:34:29 +00:00
songMap := map [ string ] int { }
2020-07-16 20:00:49 +00:00
songCount := 0
2020-07-16 10:34:29 +00:00
for _ , record := range songCache {
if record . Duet {
2020-07-16 20:00:49 +00:00
sings := songMap [ strings . ToUpper ( record . OtherSinger ) ]
sings ++
songCount ++
songMap [ strings . ToUpper ( record . OtherSinger ) ] = sings
2020-07-16 10:34:29 +00:00
}
}
slice := make ( [ ] SingerSings , 0 )
2020-07-16 20:00:49 +00:00
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 }
2020-07-16 10:34:29 +00:00
slice = append ( slice , ss )
}
2020-07-16 20:00:49 +00:00
fmt . Printf ( "Considered %d songs, Shan has %d\n" , songCount , songMap [ "SHANXOX_" ] )
return slice
2020-07-16 10:34:29 +00:00
}
2020-09-05 16:14:56 +00:00
func calculateLastSungSongDate ( songCache [ ] data . SingsVideoStruct , SongTitle string ) time . Time {
2020-07-15 19:19:06 +00:00
var t time . Time
for _ , record := range songCache {
if record . SongTitle == SongTitle {
if record . Date . After ( t ) {
t = record . Date
}
}
}
return t
}
2020-09-05 16:14:56 +00:00
func calculateLastSungSingerDate ( songCache [ ] data . SingsVideoStruct , Singer string ) time . Time {
2020-07-15 19:19:06 +00:00
var t time . Time
for _ , record := range songCache {
2020-07-16 16:25:25 +00:00
if strings . ToUpper ( record . OtherSinger ) == strings . ToUpper ( Singer ) {
2020-07-15 19:19:06 +00:00
if record . Date . After ( t ) {
t = record . Date
2020-07-16 20:00:49 +00:00
if strings . ToUpper ( Singer ) == "SHANXOX_" {
fmt . Printf ( "Last sang with %s (%s) %s" , Singer , record . OtherSinger , t )
}
2020-07-15 19:19:06 +00:00
}
}
}
return t
}
2020-09-05 16:14:56 +00:00
func updateCalculatedFields ( songCache [ ] data . SingsVideoStruct ) {
2020-07-15 19:19:06 +00:00
for i , record := range songCache {
if record . Duet {
songCache [ i ] . LastSungSinger = calculateLastSungSingerDate ( songCache , record . OtherSinger )
}
songCache [ i ] . LastSungSong = calculateLastSungSongDate ( songCache , record . SongTitle )
}
}
2020-09-05 16:14:56 +00:00
func fetchVoDsPagesRecursive ( userID string , bearer string , from string ) ( [ ] data . SingsVideoStruct , error ) {
2020-07-15 19:19:06 +00:00
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
}
2020-09-05 16:14:56 +00:00
titles := make ( [ ] data . SingsVideoStruct , 0 )
2020-07-15 19:19:06 +00:00
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
}
2020-09-05 16:14:56 +00:00
func fetchAllVoDs ( userID string , bearer string ) ( [ ] data . SingsVideoStruct , error ) {
2020-07-15 19:19:06 +00:00
tokenValid , err := ValidateTwitchBearerToken ( bearer )
if err != nil {
2020-08-02 22:34:04 +00:00
fmt . Println ( "Error validating token : " + err . Error ( ) )
2020-07-15 19:19:06 +00:00
return nil , err
}
if ! tokenValid {
2020-08-02 22:34:04 +00:00
fmt . Println ( "Error validating token (revoked?)" )
2020-07-15 19:19:06 +00:00
return nil , errors . New ( "Failed to validate token with twitch (authorization revoked?!)" )
}
titles , err := fetchVoDsPagesRecursive ( userID , bearer , "" )
if err != nil {
2020-09-05 16:14:56 +00:00
return make ( [ ] data . SingsVideoStruct , 0 ) , err
2020-07-15 19:19:06 +00:00
}
return titles , nil
}
2020-07-14 18:56:20 +00:00
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 } ,
2020-07-14 19:23:08 +00:00
"client_secret" : { ircBot . AppCredentials . ClientSecret } ,
2020-07-14 18:56:20 +00:00
"code" : { vars [ "code" ] } ,
"grant_type" : { "authorization_code" } ,
2020-09-05 16:14:56 +00:00
"redirect_uri" : { "https://" + globalData . Config . ExternalURL + "/twitchadmin" } } )
2020-07-14 18:56:20 +00:00
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 ]
2020-09-05 16:14:56 +00:00
magicCode := globalData . ReadOrCreateChannelKey ( user . Login )
globalData . UpdateBearerToken ( user . Login , oauthResponse . Access_token )
globalData . UpdateTwitchUserID ( user . Login , user . Id )
url := "https://" + globalData . Config . ExternalURL + "/admin/" + user . Login + "/" + magicCode
2020-07-14 18:56:20 +00:00
http . Redirect ( response , request , url , http . StatusFound )
} else {
fmt . Fprintf ( response , "I'm not okay jack! %v \n" , vars )
for key , val := range vars {
2020-07-15 19:19:06 +00:00
fmt . Fprintf ( response , "%s = %s\n" , key , val )
2020-07-14 18:56:20 +00:00
}
}
}
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
2020-09-05 16:14:56 +00:00
// "https://"+globalData.Config.ExternalURL+/twitchadmin
2020-07-14 18:56:20 +00:00
fmt . Println ( "Asking twitch for more..." )
resp , err := http . PostForm (
"https://id.twitch.tv/oauth2/token" ,
url . Values {
"client_id" : { ircBot . AppCredentials . ClientID } ,
2020-07-14 19:23:08 +00:00
"client_secret" : { ircBot . AppCredentials . ClientSecret } ,
2020-07-14 18:56:20 +00:00
"code" : { vars [ "code" ] } ,
"grant_type" : { "authorization_code" } ,
2020-09-05 16:14:56 +00:00
"redirect_uri" : { "https://" + globalData . Config . ExternalURL + "/twitchadmin" } } )
2020-07-14 18:56:20 +00:00
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 )
}
}
2020-07-16 10:34:29 +00:00
func CSVHandler ( response http . ResponseWriter , request * http . Request ) {
vars := mux . Vars ( request )
2020-09-05 16:14:56 +00:00
if vars [ "key" ] != globalData . ChannelData [ vars [ "channel" ] ] . AdminKey {
2020-07-16 10:34:29 +00:00
UnauthorizedHandler ( response , request )
return
}
type TemplateData struct {
Channel string
2020-09-05 16:14:56 +00:00
Commands [ ] data . CommandStruct
2020-07-16 10:34:29 +00:00
ExtraStrings string
SinceTime time . Time
SinceTimeUTC string
Leaving bool
HasLeft bool
SongData [ ] AugmentedSingsVideoStruct
TopNSongs [ ] SongSings
TopNSingers [ ] SingerSings
}
2020-08-01 10:34:06 +00:00
updateCacheIfNecessary ( vars [ "channel" ] )
2020-09-05 16:14:56 +00:00
channelData := globalData . ChannelData [ vars [ "channel" ] ]
2020-07-16 10:34:29 +00:00
topNSongs := calculateTopNSongs ( channelData . VideoCache , 10 )
topNSingers := calculateTopNSingers ( channelData . VideoCache , 10 )
2020-08-02 22:34:04 +00:00
var td = TemplateData { channelData . Name , channelData . Commands , channelData . ExtraStrings , channelData . JoinTime , channelData . JoinTime . Format ( irc . UTCFormat ) , false , channelData . HasLeft , AugmentSingsVideoStructSliceForCSV ( channelData . VideoCache ) , topNSongs , topNSingers }
2020-07-16 16:01:04 +00:00
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 )
}
2020-07-16 10:34:29 +00:00
}
2020-08-01 09:37:40 +00:00
func JSONHandler ( response http . ResponseWriter , request * http . Request ) {
vars := mux . Vars ( request )
2020-09-05 16:14:56 +00:00
if vars [ "key" ] != globalData . ChannelData [ vars [ "channel" ] ] . AdminKey {
fmt . Printf ( "%s != %s\n" , vars [ "key" ] , globalData . ChannelData [ vars [ "channel" ] ] . AdminKey )
2020-08-01 09:37:40 +00:00
UnauthorizedHandler ( response , request )
return
}
type TemplateData struct {
Channel string
2020-09-05 16:14:56 +00:00
Commands [ ] data . CommandStruct
2020-08-01 09:37:40 +00:00
ExtraStrings string
SinceTime time . Time
SinceTimeUTC string
Leaving bool
HasLeft bool
SongData [ ] AugmentedSingsVideoStruct
TopNSongs [ ] SongSings
TopNSingers [ ] SingerSings
}
2020-08-01 10:34:06 +00:00
updateCacheIfNecessary ( vars [ "channel" ] )
2020-09-05 16:14:56 +00:00
channelData := globalData . ChannelData [ vars [ "channel" ] ]
2020-08-02 22:34:04 +00:00
var topNSongs [ ] SongSings
var topNSingers [ ] SingerSings
if request . URL . Path [ 0 : 4 ] != "/deb" {
topNSongs = calculateTopNSongs ( channelData . VideoCache , 10 )
topNSingers = calculateTopNSingers ( channelData . VideoCache , 10 )
}
var td = TemplateData { channelData . Name , channelData . Commands , channelData . ExtraStrings , channelData . JoinTime , channelData . JoinTime . Format ( irc . UTCFormat ) , false , channelData . HasLeft , AugmentSingsVideoStructSlice ( channelData . VideoCache ) , topNSongs , topNSingers }
2020-08-01 09:37:40 +00:00
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 )
}
}
2020-09-05 16:14:56 +00:00
func ScriptHandler ( response http . ResponseWriter , request * http . Request ) {
vars := mux . Vars ( request )
if vars [ "key" ] != globalData . ChannelData [ vars [ "channel" ] ] . AdminKey {
UnauthorizedHandler ( response , request )
return
}
type TemplateData struct {
Channel string
Commands [ ] data . CommandStruct
ExtraStrings string
SinceTime time . Time
SinceTimeUTC string
Leaving bool
HasLeft bool
SongData [ ] AugmentedSingsVideoStruct
TopNSongs [ ] SongSings
TopNSingers [ ] SingerSings
}
updateCacheIfNecessary ( vars [ "channel" ] )
channelData := globalData . ChannelData [ vars [ "channel" ] ]
topNSongs := calculateTopNSongs ( channelData . VideoCache , 10 )
topNSingers := calculateTopNSingers ( channelData . VideoCache , 10 )
var td = TemplateData { channelData . Name , channelData . Commands , channelData . ExtraStrings , channelData . JoinTime , channelData . JoinTime . Format ( irc . UTCFormat ) , false , channelData . HasLeft , AugmentSingsVideoStructSliceForCSV ( channelData . VideoCache ) , topNSongs , topNSingers }
if request . URL . Path [ 0 : 11 ] == "/script.bat" {
response . Header ( ) . Add ( "Content-Disposition" , "attachment; filename=\"script.bat\"" )
response . Header ( ) . Add ( "Content-type" , "application/x-bat" )
tmpl := template . Must ( template . ParseFiles ( "web/script.bat" ) )
tmpl . Execute ( response , td )
} else {
response . Header ( ) . Add ( "Content-Disposition" , "attachment; filename=\"script.sh\"" )
response . Header ( ) . Add ( "Content-type" , "text/x-shellscript" )
tmpl := template . Must ( template . ParseFiles ( "web/script.sh" ) )
tmpl . Execute ( response , td )
}
}
2020-08-02 22:34:04 +00:00
func CacheDetailsHandler ( response http . ResponseWriter , request * http . Request ) {
vars := mux . Vars ( request )
2020-09-05 16:14:56 +00:00
if vars [ "key" ] != globalData . ChannelData [ vars [ "channel" ] ] . AdminKey {
2020-08-02 22:34:04 +00:00
UnauthorizedHandler ( response , request )
return
}
deets := getCacheDetails ( vars [ "channel" ] )
response . Header ( ) . Add ( "Content-type" , "application/json" )
enc := json . NewEncoder ( response )
enc . Encode ( deets )
}
func BotDetailsHandler ( response http . ResponseWriter , request * http . Request ) {
vars := mux . Vars ( request )
2020-09-05 16:14:56 +00:00
if vars [ "key" ] != globalData . ChannelData [ vars [ "channel" ] ] . AdminKey {
2020-08-02 22:34:04 +00:00
UnauthorizedHandler ( response , request )
return
}
type ChannelDataSmaller struct {
2020-09-05 16:14:56 +00:00
Commands [ ] data . CommandStruct ` json:"commands,omitempty" `
ExtraStrings string ` json:"extrastrings,omitempty" `
JoinTime time . Time ` json:"jointime" `
HasLeft bool ` json:"hasleft" `
VideoCacheUpdated time . Time ` json:"videoCacheUpdated" `
2020-08-02 22:34:04 +00:00
}
var deets ChannelDataSmaller
2020-09-05 16:14:56 +00:00
deets . Commands = globalData . ChannelData [ vars [ "channel" ] ] . Commands
deets . ExtraStrings = globalData . ChannelData [ vars [ "channel" ] ] . ExtraStrings
deets . JoinTime = globalData . ChannelData [ vars [ "channel" ] ] . JoinTime
deets . HasLeft = globalData . ChannelData [ vars [ "channel" ] ] . HasLeft
deets . VideoCacheUpdated = globalData . ChannelData [ vars [ "channel" ] ] . VideoCacheUpdated
2020-08-02 22:34:04 +00:00
response . Header ( ) . Add ( "Content-type" , "application/json" )
enc := json . NewEncoder ( response )
enc . Encode ( deets )
}
2020-08-01 09:37:40 +00:00
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 )
}
2020-08-02 22:34:04 +00:00
func ForceRefreshHandler ( response http . ResponseWriter , request * http . Request ) {
vars := mux . Vars ( request )
2020-09-05 16:14:56 +00:00
if vars [ "key" ] != globalData . ChannelData [ vars [ "channel" ] ] . AdminKey {
2020-08-02 22:34:04 +00:00
UnauthorizedHandler ( response , request )
return
}
forceUpdateCache ( vars [ "channel" ] )
response . Header ( ) . Add ( "Content-type" , "application/json" )
enc := json . NewEncoder ( response )
enc . Encode ( true )
}
func JoinHandler ( response http . ResponseWriter , request * http . Request ) {
vars := mux . Vars ( request )
2020-09-05 16:14:56 +00:00
if vars [ "key" ] != globalData . ChannelData [ vars [ "channel" ] ] . AdminKey {
2020-08-02 22:34:04 +00:00
UnauthorizedHandler ( response , request )
return
}
2020-09-05 16:14:56 +00:00
globalData . UpdateJoined ( vars [ "channel" ] , false )
ircBot . JoinChannel ( vars [ "channel" ] )
2020-08-02 22:34:04 +00:00
}
2020-09-05 16:14:56 +00:00
func HandleHTTP ( passedIrcBot * irc . KardBot , passedGlobalData * data . GlobalData ) {
2020-07-14 18:56:20 +00:00
ircBot = passedIrcBot
2020-09-05 16:14:56 +00:00
globalData = passedGlobalData
2020-07-14 18:56:20 +00:00
r := mux . NewRouter ( )
loggedRouter := handlers . LoggingHandler ( os . Stdout , r )
r . NotFoundHandler = http . HandlerFunc ( NotFoundHandler )
r . HandleFunc ( "/healthz" , HealthHandler )
r . HandleFunc ( "/web/{.*}" , TemplateHandler )
2020-08-02 22:34:04 +00:00
r . HandleFunc ( "/cachedeets/{channel}/{key}" , CacheDetailsHandler )
r . HandleFunc ( "/botdeets/{channel}/{key}" , BotDetailsHandler )
r . HandleFunc ( "/join/{channel}/{key}" , JoinHandler )
r . HandleFunc ( "/force/{channel}/{key}" , ForceRefreshHandler )
2020-07-16 10:34:29 +00:00
r . HandleFunc ( "/csv/{channel}/{key}" , CSVHandler )
2020-07-16 16:01:04 +00:00
r . HandleFunc ( "/tsv/{channel}/{key}" , CSVHandler )
2020-08-01 09:37:40 +00:00
r . HandleFunc ( "/json/{channel}/{key}" , JSONHandler )
2020-08-02 22:34:04 +00:00
r . HandleFunc ( "/debug/{channel}/{key}" , JSONHandler )
2020-08-01 09:37:40 +00:00
r . HandleFunc ( "/topsongs/{channel}/{key}" , JSONHandler )
r . HandleFunc ( "/topsingers/{channel}/{key}" , JSONHandler )
2020-09-05 16:14:56 +00:00
r . HandleFunc ( "/script.bat/{channel}/{key}" , ScriptHandler )
2020-07-14 18:56:20 +00:00
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 )
2020-08-01 09:37:40 +00:00
r . PathPrefix ( "/static" ) . Handler ( http . StripPrefix ( "/static" , http . FileServer ( http . Dir ( "./web/react-frontend/static" ) ) ) )
r . PathPrefix ( "/" ) . HandlerFunc ( ReactIndexHandler ( "./web/react-frontend/index.html" ) )
2020-07-14 18:56:20 +00:00
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 ( )
}