karaokards/internal/webserver/webserver.go

370 lines
12 KiB
Go
Executable File

package webserver
import (
"math/rand"
irc "git.martyn.berlin/martyn/karaokards/internal/irc"
"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"`
}
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
}
// tmpl, err := template.New("html"+request.URL.Path).Funcs(template.FuncMap{
// "ToUpper": strings.ToUpper,
// "ToLower": strings.ToLower,
// }).ParseFiles("html"+request.URL.Path)
_ = 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 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
}
channelData := ircBot.ChannelData[vars["channel"]]
var td = TemplateData{channelData.Name, channelData.Command, channelData.ExtraStrings, channelData.JoinTime, channelData.JoinTime.Format(irc.UTCFormat), false, channelData.HasLeft}
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}
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 := template.Must(template.ParseFiles("web/admin.html"))
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("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 TwitchAdminHandler(response http.ResponseWriter, request *http.Request) {
vars := mux.Vars(request)
if vars["code"] != "" {
response.Header().Add("Content-type", "text/plain")
form := url.Values{
"client_id": {ircBot.AppCredentials.ClientID},
"client_secret": {ircBot.AppCredentials.Password},
"code": {vars["code"]},
"grant_type": {"authorization_code"},
"redirect_uri": {"https://" + ircBot.Config.ExternalUrl + "/twitchadmin"}}
req, err := http.NewRequest("POST", "https://id.twitch.tv/oauth2/token", strings.NewReader(form.Encode()))
if err != nil {
response.WriteHeader(500)
response.Header().Add("Content-type", "text/plain")
fmt.Fprint(response, "ERROR: "+err.Error())
return
}
req.Header.Add("Client-ID", ircBot.AppCredentials.ClientID)
req.Header.Add("Authorization", "Bearer "+ircBot.AppCredentials.Password)
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
resp, err := http.DefaultClient.Do(req)
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")
fmt.Fprint(response, usersObject.Data)
fmt.Fprint(response, "\n---\n")
fmt.Fprint(response, body)
fmt.Fprint(response, "\n---\n")
fmt.Fprint(response, usersResponse)
fmt.Fprint(response, "\n---\n")
fmt.Fprint(response, usersObject)
return
}
user := usersObject.Data[0]
magicCode := ircBot.ReadOrCreateChannelKey(user.Login)
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.Fprint(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.Password},
"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 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("/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()
}