Compare commits

..

No commits in common. "b547819234b214f1b67c7d65fff54547306418b9" and "3950f8b672615a2fa78adf2585595f69819e7f03" have entirely different histories.

12 changed files with 273 additions and 1114 deletions

View File

@ -1,4 +1,4 @@
{ {
"channels": ["karaokards"], "channels": ["iMartynOnTwitch"],
"externalUrl": "karaokards.ing.martyn.berlin" "externalUrl": "karaokards.ing.martyn.berlin"
} }

View File

@ -4,6 +4,7 @@ metadata:
labels: labels:
run: kardbot run: kardbot
name: kardbot name: kardbot
namespace: karaokards
spec: spec:
progressDeadlineSeconds: 600 progressDeadlineSeconds: 600
replicas: 1 replicas: 1

View File

@ -6,16 +6,20 @@ metadata:
name: karaokards name: karaokards
spec: spec:
rules: rules:
- host: karaokards-dev.ing.martyn.berlin - host: karaokards.ing.martyn.berlin
http: http:
paths: paths:
- backend:
serviceName: karaokards
servicePort: 80
path: /nope
- backend: - backend:
serviceName: karaokards serviceName: karaokards
servicePort: 80 servicePort: 80
path: / path: /
tls: tls:
- hosts: - hosts:
- karaokards-dev.ing.martyn.berlin - karaokards.ing.martyn.berlin
secretName: karaokards-dev-cert secretName: karaokards-cert
status: status:
loadBalancer: {} loadBalancer: {}

View File

@ -2,8 +2,8 @@ package irc
import ( import (
"bufio" "bufio"
"encoding/base64"
"encoding/json" "encoding/json"
"encoding/base64"
"errors" "errors"
"fmt" "fmt"
"io" "io"
@ -16,23 +16,17 @@ import (
"time" "time"
rgb "github.com/foresthoffman/rgblog" rgb "github.com/foresthoffman/rgblog"
uuid "github.com/google/uuid"
scribble "github.com/nanobox-io/golang-scribble" scribble "github.com/nanobox-io/golang-scribble"
uuid "github.com/google/uuid"
) )
const UTCFormat = "Jan 2 15:04:05 UTC" const PSTFormat = "Jan 2 15:04:05 PST"
// Regex for parsing connection messages
//
// First matched group is our real username - twitch doesn't complain at using NICK command but doesn't honor it.
var ConnectRegex *regexp.Regexp = regexp.MustCompile(`^:tmi.twitch.tv 001 ([^ ]+) .*`)
// Regex for parsing PRIVMSG strings. // Regex for parsing PRIVMSG strings.
// //
// First matched group is the user's name, second is the channel? and the third matched group is the content of the // First matched group is the user's name, second is the channel? and the third matched group is the content of the
// user's message. // user's message.
var MsgRegex *regexp.Regexp = regexp.MustCompile(`^:(\w+)!\w+@\w+\.tmi\.twitch\.tv (PRIVMSG) #(\w+)(?: :(.*))?$`) var MsgRegex *regexp.Regexp = regexp.MustCompile(`^:(\w+)!\w+@\w+\.tmi\.twitch\.tv (PRIVMSG) #(\w+)(?: :(.*))?$`)
var DirectMsgRegex *regexp.Regexp = regexp.MustCompile(`^:(\w+)!\w+@\w+\.tmi\.twitch\.tv (PRIVMSG) (\w+)(?: :(.*))?$`)
// Regex for parsing user commands, from already parsed PRIVMSG strings. // Regex for parsing user commands, from already parsed PRIVMSG strings.
// //
@ -49,41 +43,27 @@ type OAuthCred struct {
ClientID string `json:"client_id,omitempty"` ClientID string `json:"client_id,omitempty"`
} }
type ConfigStruct struct {
InitialChannels []string `json:"channels"`
IrcOAuthPath string `json:"ircoauthpath,omitempty"`
StringPath string `json:"authpath,omitempty"`
DataPath string `json:"datapath,omitempty"`
ExternalUrl string `json:"externalurl,omitempty"`
AppOAuthPath string `json:"appoauthpath,omitempty"`
}
type KardBot struct { type KardBot struct {
Channel string Channel string
conn net.Conn conn net.Conn
IrcCredentials *OAuthCred Credentials *OAuthCred
AppCredentials *OAuthCred
MsgRate time.Duration MsgRate time.Duration
Name string Name string
Port string Port string
IrcPrivatePath string PrivatePath string
AppPrivatePath string
Server string Server string
startTime time.Time startTime time.Time
Prompts []string Prompts []string
Database scribble.Driver Database scribble.Driver
ChannelData map[string]ChannelData channelData map[string]ChannelData
Config ConfigStruct
} }
type ChannelData struct { type ChannelData struct {
Name string `json:"name"` Name string `json:"name"`
AdminKey string `json:"value,omitempty"` AdminKey string `json:"value,omitempty"`
Command string `json:"customcommand,omitempty"` CustomCommand string `json:"customcommand,omitempty"`
ExtraStrings string `json:"extrastrings,omitempty"` ExtraStrings string `json:"extrastrings,omitempty"`
JoinTime time.Time `json:"jointime"` JoinTime time.Time `json:"jointime"`
ControlChannel bool
HasLeft bool `json:"hasleft"`
} }
// Connects the bot to the Twitch IRC server. The bot will continue to try to connect until it // Connects the bot to the Twitch IRC server. The bot will continue to try to connect until it
@ -110,17 +90,6 @@ func (bb *KardBot) Disconnect() {
rgb.YPrintf("[%s] Closed connection from %s! | Live for: %fs\n", TimeStamp(), bb.Server, upTime) rgb.YPrintf("[%s] Closed connection from %s! | Live for: %fs\n", TimeStamp(), bb.Server, upTime)
} }
// Look at the channels I'm actually in
func (bb *KardBot) ActiveChannels() int {
count := 0
for _, channel := range bb.ChannelData {
if !channel.HasLeft {
count = count + 1
}
}
return count
}
// Listens for and logs messages from chat. Responds to commands from the channel owner. The bot // Listens for and logs messages from chat. Responds to commands from the channel owner. The bot
// continues until it gets disconnected, told to shutdown, or forcefully shutdown. // continues until it gets disconnected, told to shutdown, or forcefully shutdown.
func (bb *KardBot) HandleChat() error { func (bb *KardBot) HandleChat() error {
@ -149,29 +118,9 @@ func (bb *KardBot) HandleChat() error {
bb.conn.Write([]byte("PONG :tmi.twitch.tv\r\n")) bb.conn.Write([]byte("PONG :tmi.twitch.tv\r\n"))
continue continue
} else { } else {
matches := ConnectRegex.FindStringSubmatch(line)
if nil != matches {
realUserName := matches[1]
if bb.ChannelData[realUserName].Name == "" {
record := ChannelData{Name: realUserName, JoinTime: time.Now(), Command: "card", ControlChannel: true}
bb.Database.Write("channelData", realUserName, record)
bb.ChannelData[realUserName] = record
}
bb.JoinChannel(realUserName)
}
matches = DirectMsgRegex.FindStringSubmatch(line)
if nil != matches {
userName := matches[1]
// msgType := matches[2]
// channel := matches[3]
msg := matches[4]
rgb.GPrintf("[%s] Direct message %s: %s\n", TimeStamp(), userName, msg)
}
// handle a PRIVMSG message // handle a PRIVMSG message
matches = MsgRegex.FindStringSubmatch(line) matches := MsgRegex.FindStringSubmatch(line)
if nil != matches { if nil != matches {
userName := matches[1] userName := matches[1]
msgType := matches[2] msgType := matches[2]
@ -187,22 +136,11 @@ func (bb *KardBot) HandleChat() error {
if nil != cmdMatches { if nil != cmdMatches {
cmd := cmdMatches[1] cmd := cmdMatches[1]
rgb.YPrintf("[%s] Checking cmd %s against %s\n", TimeStamp(), cmd, bb.ChannelData[channel].Command)
switch cmd { switch cmd {
case bb.ChannelData[channel].Command: case "card":
rgb.CPrintf("[%s] Card asked for by %s on %s' channel!\n", TimeStamp(), userName, channel) rgb.CPrintf("[%s] Card asked for by %s on %s' channel!\n", TimeStamp(), userName, channel)
bb.Say("Your prompt is : "+bb.Prompts[rand.Intn(len(bb.Prompts))], channel) bb.Say("Your prompt is : "+bb.Prompts[rand.Intn(len(bb.Prompts))], channel)
case "join":
if bb.ChannelData[channel].ControlChannel {
rgb.CPrintf("[%s] Join asked for by %s on %s' channel!\n", TimeStamp(), userName, channel)
if bb.ChannelData[userName].Name == "" {
record := ChannelData{Name: userName, JoinTime: time.Now(), Command: "card", ControlChannel: true}
bb.Database.Write("channelData", userName, record)
bb.ChannelData[userName] = record
}
bb.JoinChannel(userName)
}
} }
// channel-owner specific commands // channel-owner specific commands
@ -216,17 +154,13 @@ func (bb *KardBot) HandleChat() error {
bb.Disconnect() bb.Disconnect()
return nil return nil
case "kcardadmin": case "wat":
magicCode := bb.ReadOrCreateChannelKey(channel) magicCode := bb.readOrCreateChannelKey(channel)
rgb.CPrintf( rgb.CPrintf(
"[%s] Magic code is %s - https://karaokards.ing.martyn.berlin/admin/%s/%s\n", "[%s] Magic code is %s - https://karaokards.ing.martyn.berlin/admin/%s/%s\n",
TimeStamp(), TimeStamp(),
magicCode, userName, magicCode, magicCode, userName, magicCode,
) )
err := bb.Msg("Welcome to Karaokards, your admin panel is https://karaokards.ing.martyn.berlin/admin/"+userName+"/"+magicCode, userName)
if err != nil {
rgb.RPrintf("[%s] ERROR %s\n",err)
}
bb.Say("Ack.") bb.Say("Ack.")
default: default:
// do nothing // do nothing
@ -246,18 +180,10 @@ func (bb *KardBot) HandleChat() error {
// Login to the IRC server // Login to the IRC server
func (bb *KardBot) Login() { func (bb *KardBot) Login() {
rgb.YPrintf("[%s] Logging into #%s...\n", TimeStamp(), bb.Channel) rgb.YPrintf("[%s] Logging into #%s...\n", TimeStamp(), bb.Channel)
bb.conn.Write([]byte("PASS " + bb.IrcCredentials.Password + "\r\n")) bb.conn.Write([]byte("PASS " + bb.Credentials.Password + "\r\n"))
bb.conn.Write([]byte("NICK " + bb.Name + "\r\n")) bb.conn.Write([]byte("NICK " + bb.Name + "\r\n"))
} }
func (bb *KardBot) LeaveChannel(channels ...string) {
for _, channel := range channels {
rgb.YPrintf("[%s] Leaving #%s...\n", TimeStamp(), channel)
bb.conn.Write([]byte("PART #" + channel + "\r\n"))
rgb.YPrintf("[%s] Left #%s as @%s!\n", TimeStamp(), channel, bb.Name)
}
}
// Makes the bot join its pre-specified channel. // Makes the bot join its pre-specified channel.
func (bb *KardBot) JoinChannel(channels ...string) { func (bb *KardBot) JoinChannel(channels ...string) {
if len(channels) == 0 { if len(channels) == 0 {
@ -271,59 +197,23 @@ func (bb *KardBot) JoinChannel(channels ...string) {
} }
} }
// Reads from the private credentials file and stores the data in the bot's appropriate Credentials field. // Reads from the private credentials file and stores the data in the bot's Credentials field.
func (bb *KardBot) ReadCredentials(credType string) error { func (bb *KardBot) ReadCredentials() error {
var err error
var credFile []byte
// reads from the file // reads from the file
if credType == "IRC" { credFile, err := ioutil.ReadFile(bb.PrivatePath)
credFile, err = ioutil.ReadFile(bb.IrcPrivatePath)
} else {
credFile, err = ioutil.ReadFile(bb.AppPrivatePath)
}
if nil != err { if nil != err {
return err return err
} }
bb.Credentials = &OAuthCred{}
// parses the file contents // parses the file contents
var creds OAuthCred
dec := json.NewDecoder(strings.NewReader(string(credFile))) dec := json.NewDecoder(strings.NewReader(string(credFile)))
if err = dec.Decode(&creds); nil != err && io.EOF != err { if err = dec.Decode(bb.Credentials); nil != err && io.EOF != err {
return err return err
} }
if credType == "IRC" {
bb.IrcCredentials = &creds
} else {
bb.AppCredentials = &creds
}
return nil
}
func (bb *KardBot) Msg(msg string, users ...string) error {
if "" == msg {
return errors.New("BasicBot.Say: msg was empty.")
}
// check if message is too large for IRC
if len(msg) > 512 {
return errors.New("BasicBot.Say: msg exceeded 512 bytes")
}
if len(users) == 0 {
return errors.New("BasicBot.Say: users was empty.")
}
rgb.YPrintf("[%s] sending %s to users %v as @%s!\n", TimeStamp(), msg, users, bb.Name)
for _, channel := range users {
_, err := bb.conn.Write([]byte(fmt.Sprintf("PRIVMSG %s :%s\r\n", channel, msg)))
rgb.YPrintf("[%s] PRIVMSG %s :%s\r\n", TimeStamp(), channel, msg)
if nil != err {
return err
}
}
return nil return nil
} }
@ -357,14 +247,7 @@ func (bb *KardBot) Say(msg string, channels ...string) error {
// pre-specified channel, and then handle the chat. It will attempt to reconnect until it is told to // pre-specified channel, and then handle the chat. It will attempt to reconnect until it is told to
// shut down, or is forcefully shutdown. // shut down, or is forcefully shutdown.
func (bb *KardBot) Start() { func (bb *KardBot) Start() {
err := bb.ReadCredentials("IRC") err := bb.ReadCredentials()
if nil != err {
fmt.Println(err)
fmt.Println("Aborting!")
return
}
err = bb.ReadCredentials("App")
if nil != err { if nil != err {
fmt.Println(err) fmt.Println(err)
fmt.Println("Aborting!") fmt.Println("Aborting!")
@ -381,12 +264,10 @@ func (bb *KardBot) Start() {
for { for {
bb.Connect() bb.Connect()
bb.Login() bb.Login()
if len(bb.ChannelData) > 0 { if len(bb.channelData) > 0 {
for channelName,channelData := range bb.ChannelData { for channelName := range(bb.channelData) {
if !channelData.HasLeft {
bb.JoinChannel(channelName) bb.JoinChannel(channelName)
} }
}
} else { } else {
bb.JoinChannel() bb.JoinChannel()
} }
@ -407,65 +288,43 @@ func (bb *KardBot) readChannelData() error {
records, err := bb.Database.ReadAll("channelData") records, err := bb.Database.ReadAll("channelData")
if err != nil { if err != nil {
// no db? initialise one? // no db? initialise one?
record := ChannelData{Name: bb.Channel, JoinTime: time.Now(), Command: "card"} record := ChannelData{Name: bb.Channel, JoinTime: time.Now()}
rgb.YPrintf("[%s] No channel table for #%s exists, creating...\n", TimeStamp(), bb.Channel) rgb.YPrintf("[%s] No channel data for #%s exists, creating...\n", TimeStamp(), bb.Channel)
if err := bb.Database.Write("channelData", bb.Channel, record); err != nil { if err := bb.Database.Write("channelData", bb.Channel, record); err != nil {
return err return err
} }
bb.ChannelData = make(map[string]ChannelData) bb.channelData = make(map[string]ChannelData)
bb.ChannelData[bb.Channel] = record bb.channelData[bb.Channel] = record;
} else { } else {
bb.ChannelData = make(map[string]ChannelData) bb.channelData = make(map[string]ChannelData)
} }
for _, data := range records { for _, data := range records {
record := ChannelData{} record := ChannelData{}
err := json.Unmarshal([]byte(data), &record) err := json.Unmarshal([]byte(data), &record);
if err != nil { if err != nil {
return err return err
} }
if record.Name != "" { bb.channelData[record.Name] = record
if record.Command == "" {
record.Command = "card"
rgb.YPrintf("[%s] Rewriting data for #%s...\n", TimeStamp(), bb.Channel)
if err := bb.Database.Write("channelData", record.Name, record); err != nil {
return err
} }
}
bb.ChannelData[record.Name] = record
}
}
// Managed to leave the main channel!?
if bb.ChannelData[bb.Channel].Name == "" {
rgb.YPrintf("[%s] No channel data for #%s exists, creating...\n", TimeStamp(), bb.Channel)
record := ChannelData{Name: bb.Channel, JoinTime: time.Now(), Command: "card"}
bb.ChannelData[bb.Channel] = record
if err := bb.Database.Write("channelData", bb.Channel, record); err != nil {
return err
}
records, err = bb.Database.ReadAll("channelData")
}
rgb.YPrintf("[%s] Read channel data for %d channels\n", TimeStamp(), len(bb.ChannelData))
return nil return nil
} }
func (bb *KardBot) ReadOrCreateChannelKey(channel string) string { func (bb *KardBot) readOrCreateChannelKey(channel string) string {
magicCode := "" magicCode := ""
var err error var err error
var record ChannelData var record ChannelData
if record, ok := bb.ChannelData[channel]; !ok { if record, ok := bb.channelData[channel]; !ok {
rgb.YPrintf("[%s] No channel data for #%s exists, creating\n", TimeStamp(), channel) rgb.YPrintf("[%s] No channel data for #%s exists, creating\n", TimeStamp(), channel)
err = bb.Database.Read("channelData", channel, &record) err = bb.Database.Read("channelData", channel, &record);
if err == nil { if err == nil {
bb.ChannelData[channel] = record bb.channelData[channel] = record
} }
} }
record = bb.ChannelData[channel] record = bb.channelData[channel]
if err != nil || record.AdminKey == "" { if err != nil || record.AdminKey == "" {
rgb.YPrintf("[%s] No channel key for #%s exists, creating one\n", TimeStamp(), channel) rgb.YPrintf("[%s] No channel key for #%s exists, creating one\n", TimeStamp(), channel)
newuu, _ := uuid.NewRandom() newuu, _ := uuid.NewRandom()
magicCode = base64.StdEncoding.EncodeToString([]byte(newuu.String())) magicCode = base64.StdEncoding.EncodeToString([]byte(newuu.String()))
record.HasLeft = true
record.AdminKey = magicCode record.AdminKey = magicCode
if record.Name == "" { if record.Name == "" {
record.Name = channel record.Name = channel
@ -473,7 +332,7 @@ func (bb *KardBot) ReadOrCreateChannelKey(channel string) string {
if err := bb.Database.Write("channelData", channel, record); err != nil { if err := bb.Database.Write("channelData", channel, record); err != nil {
rgb.RPrintf("[%s] Error writing channel data for #%s\n", TimeStamp(), channel) rgb.RPrintf("[%s] Error writing channel data for #%s\n", TimeStamp(), channel)
} }
bb.ChannelData[record.Name] = record bb.channelData[record.Name] = record
rgb.YPrintf("[%s] Cached channel key for #%s\n", TimeStamp(), record.Name) rgb.YPrintf("[%s] Cached channel key for #%s\n", TimeStamp(), record.Name)
} else { } else {
magicCode = record.AdminKey magicCode = record.AdminKey
@ -483,7 +342,7 @@ func (bb *KardBot) ReadOrCreateChannelKey(channel string) string {
} }
func TimeStamp() string { func TimeStamp() string {
return TimeStampFmt(UTCFormat) return TimeStampFmt(PSTFormat)
} }
func TimeStampFmt(format string) string { func TimeStampFmt(format string) string {

View File

@ -10,41 +10,14 @@ import (
"fmt" "fmt"
"html/template" "html/template"
"net/http" "net/http"
"net/url"
"os" "os"
"strings" "strings"
"time" "time"
"encoding/json"
"io/ioutil"
) )
//var store = sessions.NewCookieStore(os.Getenv("SESSION_KEY")) //var store = sessions.NewCookieStore(os.Getenv("SESSION_KEY"))
type twitchauthresponse struct { var ircBot irc.KardBot
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) { func HealthHandler(response http.ResponseWriter, request *http.Request) {
response.Header().Add("Content-type", "text/plain") response.Header().Add("Content-type", "text/plain")
@ -76,8 +49,6 @@ func TemplateHandler(response http.ResponseWriter, request *http.Request) {
AvailCount int AvailCount int
ChannelCount int ChannelCount int
MessageCount int MessageCount int
ClientID string
BaseURI string
} }
// tmpl, err := template.New("html"+request.URL.Path).Funcs(template.FuncMap{ // tmpl, err := template.New("html"+request.URL.Path).Funcs(template.FuncMap{
// "ToUpper": strings.ToUpper, // "ToUpper": strings.ToUpper,
@ -102,7 +73,7 @@ func TemplateHandler(response http.ResponseWriter, request *http.Request) {
// NotFoundHandler(response, request) // NotFoundHandler(response, request)
// return // return
} }
var td = TemplateData{ircBot.Prompts[rand.Intn(len(ircBot.Prompts))], len(ircBot.Prompts), ircBot.ActiveChannels(), 0, ircBot.AppCredentials.ClientID, "https://"+ircBot.Config.ExternalUrl} var td = TemplateData{ircBot.Prompts[rand.Intn(len(ircBot.Prompts))], len(ircBot.Prompts), 0, 0}
err = tmpl.Execute(response, td) err = tmpl.Execute(response, td)
if err != nil { if err != nil {
http.Error(response, err.Error(), http.StatusInternalServerError) http.Error(response, err.Error(), http.StatusInternalServerError)
@ -110,238 +81,21 @@ func TemplateHandler(response http.ResponseWriter, request *http.Request) {
} }
} }
func LeaveHandler(response http.ResponseWriter, request *http.Request) { func AdminHandler(response http.ResponseWriter, request *http.Request) {
request.URL.Path = "/bye.html" request.URL.Path = "/index.html"
TemplateHandler(response, request) TemplateHandler(response, request)
} }
func AdminHandler(response http.ResponseWriter, request *http.Request) { func HandleHTTP(passedIrcBot irc.KardBot) {
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")
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.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!")
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 ircBot = passedIrcBot
r := mux.NewRouter() r := mux.NewRouter()
loggedRouter := handlers.LoggingHandler(os.Stdout, r) loggedRouter := handlers.LoggingHandler(os.Stdout, r)
r.NotFoundHandler = http.HandlerFunc(NotFoundHandler) r.NotFoundHandler = http.HandlerFunc(NotFoundHandler)
r.HandleFunc("/", RootHandler) r.HandleFunc("/", RootHandler)
r.HandleFunc("/healthz", HealthHandler) r.HandleFunc("/healthz", HealthHandler)
r.HandleFunc("/web/{.*}", TemplateHandler) r.HandleFunc("/example/{.*}", TemplateHandler)
r.PathPrefix("/static/").Handler(http.FileServer(http.Dir("./web/")))
r.HandleFunc("/cover.css", CSSHandler) r.HandleFunc("/cover.css", CSSHandler)
r.HandleFunc("/admin/{channel}/{key}", AdminHandler) 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) http.Handle("/", r)
srv := &http.Server{ srv := &http.Server{
Handler: loggedRouter, Handler: loggedRouter,

82
main.go
View File

@ -15,6 +15,13 @@ import (
scribble "github.com/nanobox-io/golang-scribble" scribble "github.com/nanobox-io/golang-scribble"
) )
type configStruct struct {
InitialChannels []string `json:"channels"`
OAuthPath string `json:"oauthpath,omitempty"`
StringPath string `json:"authpath,omitempty"`
DataPath string `json:"datapath,omitempty"`
}
type customStringsStruct struct { type customStringsStruct struct {
Strings []string `json:"strings,omitempty"` Strings []string `json:"strings,omitempty"`
} }
@ -23,7 +30,7 @@ var selectablePrompts []string
var customStrings customStringsStruct var customStrings customStringsStruct
var config irc.ConfigStruct var config configStruct
func readConfig() { func readConfig() {
var data []byte var data []byte
@ -44,7 +51,7 @@ func readConfig() {
if _, err := os.Stat(exPath + "/config.json"); os.IsNotExist(err) { if _, err := os.Stat(exPath + "/config.json"); os.IsNotExist(err) {
rgb.YPrintf("[%s] Warning, KARAOKARDS_CONFIGFILE env var unset and `config.json` not alongside executable!\n", irc.TimeStamp()) rgb.YPrintf("[%s] Warning, KARAOKARDS_CONFIGFILE env var unset and `config.json` not alongside executable!\n", irc.TimeStamp())
if _, err := os.Stat("/etc/karaokards/config.json"); os.IsNotExist(err) { if _, err := os.Stat("/etc/karaokards/config.json"); os.IsNotExist(err) {
rgb.RPrintf("[%s] Error, KARAOKARDS_CONFIGFILE env var unset and neither '%s' nor '%s' exist!\n", irc.TimeStamp(), exPath+"/config.json", "/etc/karaokards/config.json") rgb.RPrintf("[%s] Error, KARAOKARDS_CONFIGFILE env var unset and neither '%s' nor '%s' exist!\n", irc.TimeStamp(), exPath + "/config.json", "/etc/karaokards/config.json")
os.Exit(1) os.Exit(1)
} else { } else {
configFile = "/etc/karaokards/config.json" configFile = "/etc/karaokards/config.json"
@ -58,13 +65,12 @@ func readConfig() {
rgb.RPrintf("[%s] Could not read `%s`. File reading error: %s\n", irc.TimeStamp(), configFile, err) rgb.RPrintf("[%s] Could not read `%s`. File reading error: %s\n", irc.TimeStamp(), configFile, err)
os.Exit(1) os.Exit(1)
} }
err = json.Unmarshal(data, &config) err = json.Unmarshal(data, &customStrings)
if err != nil { if err != nil {
rgb.RPrintf("[%s] Could not unmarshal `%s`. Unmarshal error: %s\n", irc.TimeStamp(), configFile, err) rgb.RPrintf("[%s] Could not unmarshal `%s`. Unmarshal error: %s\n", irc.TimeStamp(), configFile, err)
os.Exit(1) os.Exit(1)
} }
rgb.YPrintf("[%s] Read config file from `%s`\n", irc.TimeStamp(), configFile) rgb.YPrintf("[%s] Read config file from `%s`\n", irc.TimeStamp(), configFile)
rgb.YPrintf("[%s] config %v\n", irc.TimeStamp(), config)
return return
} }
@ -86,18 +92,18 @@ func openDatabase() *scribble.Driver {
} }
exPath := filepath.Dir(ex) exPath := filepath.Dir(ex)
if _, err := os.Stat(exPath + "/data"); os.IsNotExist(err) { if _, err := os.Stat(exPath + "/data"); os.IsNotExist(err) {
rgb.YPrintf("[%s] Warning %s doesn't exist, trying to create it.\n", irc.TimeStamp(), exPath+"/data") rgb.YPrintf("[%s] Warning %s doesn't exist, trying to create it.\n", irc.TimeStamp(), exPath + "/data")
err = os.Mkdir(exPath+"/data", 0770) err = os.Mkdir(exPath + "/data", 0770)
if err != nil { if err != nil {
rgb.RPrintf("[%s] Error cannot create %s: %s!\n", irc.TimeStamp(), exPath+"/data", err) rgb.RPrintf("[%s] Error cannot create %s: %s!\n", irc.TimeStamp(), exPath + "/data", err)
os.Exit(1) os.Exit(1)
} }
} }
dataPath = exPath + "/data" dataPath = exPath + "/data"
} }
} else { } else {
if _, err := os.Stat(config.DataPath); os.IsNotExist(err) { if _, err := os.Stat(config.OAuthPath); os.IsNotExist(err) {
rgb.RPrintf("[%s] Error, config-specified path '%s' doesn't exist!\n", irc.TimeStamp(), config.DataPath) rgb.RPrintf("[%s] Error, config-specified path '%s' doesn't exist!\n", irc.TimeStamp(), os.Getenv("KARAOKARDS_DATA_FOLDER"))
os.Exit(1) os.Exit(1)
} }
dataPath = config.DataPath dataPath = config.DataPath
@ -161,76 +167,48 @@ func main() {
selectablePrompts := append(selectablePrompts, dbGlobalPrompts...) selectablePrompts := append(selectablePrompts, dbGlobalPrompts...)
rgb.YPrintf("[%s] %d prompts available.\n", irc.TimeStamp(), len(selectablePrompts)) rgb.YPrintf("[%s] %d prompts available.\n", irc.TimeStamp(), len(selectablePrompts))
ircOauthPath := "" oauthPath := ""
if config.IrcOAuthPath == "" { if config.OAuthPath == "" {
if os.Getenv("TWITCH_OAUTH_JSON") != "" { if os.Getenv("TWITCH_OAUTH_JSON") != "" {
if _, err := os.Stat(os.Getenv("TWITCH_OAUTH_JSON")); os.IsNotExist(err) { if _, err := os.Stat(os.Getenv("TWITCH_OAUTH_JSON")); os.IsNotExist(err) {
os.Exit(1) os.Exit(1)
} }
ircOauthPath = os.Getenv("TWITCH_OAUTH_JSON") oauthPath = os.Getenv("TWITCH_OAUTH_JSON")
} else { } else {
if _, err := os.Stat(os.Getenv("HOME") + "/.twitch/ircoauth.json"); os.IsNotExist(err) { if _, err := os.Stat(os.Getenv("HOME") + "/.twitch/oauth.json"); os.IsNotExist(err) {
rgb.YPrintf("[%s] Warning %s doesn't exist, trying %s next!\n", irc.TimeStamp(), os.Getenv("HOME")+"/.twitch/ircoauth.json", "/etc/twitch/ircoauth.json") rgb.YPrintf("[%s] Warning %s doesn't exist, trying %s next!\n", irc.TimeStamp(), os.Getenv("HOME")+"/.twitch/oauth.json", "/etc/twitch/oauth.json")
if _, err := os.Stat("/etc/twitch/ircoauth.json"); os.IsNotExist(err) { if _, err := os.Stat("/etc/twitch/oauth.json"); os.IsNotExist(err) {
rgb.YPrintf("[%s] Error %s doesn't exist either, bailing!\n", irc.TimeStamp(), "/etc/twitch/ircoauth.json") rgb.YPrintf("[%s] Error %s doesn't exist either, bailing!\n", irc.TimeStamp(), "/etc/twitch/oauth.json")
os.Exit(1) os.Exit(1)
} }
ircOauthPath = "/etc/twitch/ircoauth.json" oauthPath = "/etc/twitch/oauth.json"
} else { } else {
ircOauthPath = os.Getenv("HOME") + "/.twitch/ircoauth.json" oauthPath = os.Getenv("HOME") + "/.twitch/oauth.json"
} }
} }
} else { } else {
if _, err := os.Stat(config.IrcOAuthPath); os.IsNotExist(err) { if _, err := os.Stat(config.OAuthPath); os.IsNotExist(err) {
rgb.YPrintf("[%s] Error config-specified oauth file %s doesn't exist, bailing!\n", irc.TimeStamp(), config.IrcOAuthPath) rgb.YPrintf("[%s] Error config-specified oauth file %s doesn't exist, bailing!\n", irc.TimeStamp(), config.OAuthPath)
os.Exit(1) os.Exit(1)
} }
ircOauthPath = config.IrcOAuthPath oauthPath = config.OAuthPath
}
appOauthPath := ""
if config.AppOAuthPath == "" {
if os.Getenv("TWITCH_OAUTH_JSON") != "" {
if _, err := os.Stat(os.Getenv("TWITCH_OAUTH_JSON")); os.IsNotExist(err) {
os.Exit(1)
}
appOauthPath = os.Getenv("TWITCH_OAUTH_JSON")
} else {
if _, err := os.Stat(os.Getenv("HOME") + "/.twitch/appoauth.json"); os.IsNotExist(err) {
rgb.YPrintf("[%s] Warning %s doesn't exist, trying %s next!\n", irc.TimeStamp(), os.Getenv("HOME")+"/.twitch/appoauth.json", "/etc/twitch/appoauth.json")
if _, err := os.Stat("/etc/twitch/appoauth.json"); os.IsNotExist(err) {
rgb.YPrintf("[%s] Error %s doesn't exist either, bailing!\n", irc.TimeStamp(), "/etc/twitch/appoauth.json")
os.Exit(1)
}
appOauthPath = "/etc/twitch/appoauth.json"
} else {
appOauthPath = os.Getenv("HOME") + "/.twitch/appoauth.json"
}
}
} else {
if _, err := os.Stat(config.AppOAuthPath); os.IsNotExist(err) {
rgb.YPrintf("[%s] Error config-specified oauth file %s doesn't exist, bailing!\n", irc.TimeStamp(), config.AppOAuthPath)
os.Exit(1)
}
appOauthPath = config.AppOAuthPath
} }
// Replace the channel name, bot name, and the path to the private directory with your respective // Replace the channel name, bot name, and the path to the private directory with your respective
// values. // values.
myBot := irc.KardBot{ myBot := irc.KardBot{
Channel: "karaokards", Channel: "imartynontwitch",
MsgRate: time.Duration(20/30) * time.Millisecond, MsgRate: time.Duration(20/30) * time.Millisecond,
Name: "Karaokards", Name: "Karaokards",
Port: "6667", Port: "6667",
IrcPrivatePath: ircOauthPath, PrivatePath: oauthPath,
AppPrivatePath: appOauthPath,
Server: "irc.chat.twitch.tv", Server: "irc.chat.twitch.tv",
Prompts: selectablePrompts, Prompts: selectablePrompts,
Database: *persistentData, Database: *persistentData,
Config: config,
} }
go func() { go func() {
rgb.YPrintf("[%s] Starting webserver on port %s\n", irc.TimeStamp(), "5353") rgb.YPrintf("[%s] Starting webserver on port %s\n", irc.TimeStamp(), "5353")
webserver.HandleHTTP(&myBot) webserver.HandleHTTP(myBot)
}() }()
myBot.Start() myBot.Start()
} }

View File

@ -1,107 +0,0 @@
<html>
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
<meta name="description" content="Karaokards">
<meta name="author" content="Martyn Ranyard">
<title>The great unknown!</title>
<!-- Bootstrap core CSS -->
<link href="https://maxcdn.bootstrapcdn.com/bootstrap/4.3.1/css/bootstrap.min.css" rel="stylesheet" crossorigin="anonymous">
<!-- Custom styles for this template -->
<link href="/cover.css" rel="stylesheet">
<style>
.bd-placeholder-img {
font-size: 1.125rem;
text-anchor: middle;
-webkit-user-select: none;
-moz-user-select: none;
-ms-user-select: none;
user-select: none;
}
@media (min-width: 768px) {
.bd-placeholder-img-lg {
font-size: 3.5rem;
}
}
li.match-nomatch{
background-color: #1e2122;
}
li.match-matchtrack{
background-color: #E9B000;
}
li.match-fullmatch{
background-color: #008F95;
}
li.match-matchtrackfuzzt{
background-color: darkgray;
}
li.match-fullmatchfuzzy{
background-color: darkgray;
}
a{
text-decoration-line: underline;
}
</style>
</head>
<body class="text-center">
<div class="cover-container d-flex w-100 h-100 p-3 mx-auto flex-column">
<header class="masthead mb-auto">
<div class="inner">
<h3 class="masthead-brand">k8s-zoo</h3>
<nav class="nav nav-masthead justify-content-center">
<a class="nav-link active" href="/">Home</a>
</nav>
</div>
</header>
<main role="main" class="inner cover">
<h1 class="cover-heading">Scary user alert!</h1>
<img src="https://http.cat/401" alt="Cat sat outside a door that has a sign depictinging no cats allowed" />
<p>It seems you've gone somewhere you shouldn't! 401 NOT AUTHORIZED!</p>
<p/>
<p>I'm not quite sure how you got here to be honest, if it was via a link on the site, let me know via twitch DM, if it was from someone else, let them know.</p>
<p>Shameless self-promotion : Follow me on twitch - <a href="https://www.twitch.tv/iMartynOnTwitch">iMartynOnTwitch</a>, oddly enough, I do a lot of twitchsings!</p>
</main>
<footer class="mastfoot mt-auto">
<div class="inner">
<p>Cover template for <a href="https://getbootstrap.com/">Bootstrap</a>, by <a href="https://twitter.com/mdo">@mdo</a>.</p>
</div>
</footer>
</div>
<script>
function directToResults() {
var url = document.createElement('a');
url.setAttribute("href", window.location.href);
if ((url.port != 80) && (url.port != 443)) {
customPort = ":"+url.port
} else {
customPort = ""
}
var destination = url.protocol + "//" + url.hostname + customPort + "/" + document.getElementById("mode").value + "/" + document.getElementById("spotifyid").value
window.location.href = destination
}
function toggleUnfound() {
var unmatched = document.getElementsByClassName('match-nomatch'), i;
if (document.getElementById("showhidebutton").getAttribute("tracksHidden") != "true") {
document.getElementById("showhidebutton").setAttribute("tracksHidden","true")
for (i = 0; i < unmatched.length; i += 1) {
unmatched[i].style.display = 'none';
}
} else {
document.getElementById("showhidebutton").setAttribute("tracksHidden","false")
for (i = 0; i < unmatched.length; i += 1) {
unmatched[i].style.display = 'list-item';
}
}
}
</script>
</body>
</html>
</body>
</html>

View File

@ -1,143 +0,0 @@
<html>
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
<meta name="description" content="Karaokards bot for twitch chat">
<meta name="author" content="Martyn Ranyard">
<title>Karaokards</title>
<!-- Bootstrap core CSS -->
<link href="https://maxcdn.bootstrapcdn.com/bootstrap/4.3.1/css/bootstrap.min.css" rel="stylesheet" crossorigin="anonymous">
<!-- Custom styles for this template -->
<link href="/cover.css" rel="stylesheet">
<style>
.bd-placeholder-img {
font-size: 1.125rem;
text-anchor: middle;
-webkit-user-select: none;
-moz-user-select: none;
-ms-user-select: none;
user-select: none;
}
@media (min-width: 768px) {
.bd-placeholder-img-lg {
font-size: 3.5rem;
}
}
li.match-nomatch{
background-color: #1e2122;
}
li.match-matchtrack{
background-color: #E9B000;
}
li.match-fullmatch{
background-color: #008F95;
}
li.match-matchtrackfuzzt{
background-color: darkgray;
}
li.match-fullmatchfuzzy{
background-color: darkgray;
}
a{
text-decoration-line: underline;
}
.displaySetting{
display: inline
}
.hiddenDisplaySetting{
display: none;
}
.hiddenSave {
display: none;
}
.editSetting{
display: none;
}
.visibleEditSetting{
display: inline;
}
.visibleSave {
display: inline;
}
</style>
</head>
<body class="text-center">
<div class="cover-container d-flex w-100 h-100 p-3 mx-auto flex-column">
<header class="masthead mb-auto">
<div class="inner">
<h3 class="masthead-brand">Karaokards</h3>
<nav class="nav nav-masthead justify-content-center">
<a class="nav-link active" href="/">Home</a>
</nav>
</div>
</header>
<main role="main" class="inner cover">
<script>
function editMode() {
var alldisps = document.getElementsByClassName("displaySetting");
for (item of alldisps) {
item.classList.add("hiddenDisplaySetting")
}
for (item of alldisps) {
item.classList.remove("displaySetting")
}
var alldisps = document.getElementsByClassName("editSetting");
for (item of alldisps) {
item.classList.add("visibleEditSetting")
}
for (item of alldisps) {
item.classList.remove("editSetting")
}
document.getElementById("saveButton").classList.remove("hiddenSave")
document.getElementById("saveButton").classList.add("visibleSave")
document.getElementById("leaveButton").classList.remove("hiddenSave")
document.getElementById("leaveButton").classList.add("visibleSave")
document.getElementById("yuhateme").classList.remove("hiddenSave")
document.getElementById("yuhateme").classList.add("visibleSave")
}
</script>
<h1 class="cover-heading">Karaokards admin panel for {{.Channel}}!!!</h1>
<form method="POST">
{{ if .HasLeft }}
<h2>Not in your channel at the moment!</h2>
<p>The bot is not currently in your channel, chances are you've not ever asked it to join, you asked it to leave, or something went horribly wrong.</p>
<p>You can invite the bot to your channel by clicking here : <input id="joinButton" type="submit" name="join" value="Come on in"></p>
{{ else }}
{{ if .Leaving }}
<h2>Do you really want this bot to leave your channel?</h2>
<p><input id="leaveButton" type="submit" name="reallyleave" value="Really leave twitch channel"></p>
{{ else }}
<h2>Note you can give your moderators the url you are on right now to control this bot. They don't have to be logged into twitch to do so.</h2>
<table>
<thead><tr><td>Channel Data :</td><td><input type="button" value="Edit" onclick="javascript:editMode();"></td></tr></thead>
<tbody>
<tr><td>Member of channel since {{.SinceTimeUTC}}</td></tr>
<tr><td>Command for prompt:</td><td class="displaySetting"></tdclass>{{.Command}}</td><td class="editSetting"><input type="text" name="Command" value="{{.Command}}"></td></tr>
<tr><td>Extra prompts (one per line):</td><td class="displaySetting">{{.ExtraStrings}}</td><td class="editSetting"><textarea name="ExtraStrings" >{{.ExtraStrings}}</textarea></td></tr>
<tr><td>&nbsp;</td><td><input id="saveButton" type="submit" class="hiddenSave" name="save" value="Save changes"></td></tr>
<tr id="yuhateme" class="hiddenSave"><td>Or... please don't go but...</td></tr>
<tr><td><input id="leaveButton" type="submit" class="hiddenSave" name="leave" value="Leave twitch channel"></td></tr>
</tbody>
</table>
{{ end }}
{{ end }}
</form>
</main>
<footer class="mastfoot mt-auto">
<div class="inner">
<p>Cover template for <a href="https://getbootstrap.com/">Bootstrap</a>, by <a href="https://twitter.com/mdo">@mdo</a>.</p>
</div>
</footer>
</div>
</body>
</html>
</body>
</html>

View File

@ -1,75 +0,0 @@
<html>
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
<meta name="description" content="Karaokards bot for twitch chat">
<meta name="author" content="Martyn Ranyard">
<title>Karaokards</title>
<!-- Bootstrap core CSS -->
<link href="https://maxcdn.bootstrapcdn.com/bootstrap/4.3.1/css/bootstrap.min.css" rel="stylesheet" crossorigin="anonymous">
<!-- Custom styles for this template -->
<link href="/cover.css" rel="stylesheet">
<style>
.bd-placeholder-img {
font-size: 1.125rem;
text-anchor: middle;
-webkit-user-select: none;
-moz-user-select: none;
-ms-user-select: none;
user-select: none;
}
@media (min-width: 768px) {
.bd-placeholder-img-lg {
font-size: 3.5rem;
}
}
li.match-nomatch{
background-color: #1e2122;
}
li.match-matchtrack{
background-color: #E9B000;
}
li.match-fullmatch{
background-color: #008F95;
}
li.match-matchtrackfuzzt{
background-color: darkgray;
}
li.match-fullmatchfuzzy{
background-color: darkgray;
}
a{
text-decoration-line: underline;
}
</style>
</head>
<body class="text-center">
<div class="cover-container d-flex w-100 h-100 p-3 mx-auto flex-column">
<header class="masthead mb-auto">
<div class="inner">
<h3 class="masthead-brand">Karaokards</h3>
<nav class="nav nav-masthead justify-content-center">
<a class="nav-link active" href="/">Home</a>
</nav>
</div>
</header>
<main role="main" class="inner cover">
<h1 class="cover-heading">Left channel!!!</h1>
<p><img src="/static/okaybye.gif" alt="animation of dissappointed sister from Frozen saying Okay, bye..."/></p>
</main>
<footer class="mastfoot mt-auto">
<div class="inner">
<p>Cover template for <a href="https://getbootstrap.com/">Bootstrap</a>, by <a href="https://twitter.com/mdo">@mdo</a>.</p>
</div>
</footer>
</div>
</body>
</html>
</body>
</html>

View File

@ -57,7 +57,6 @@
<h3 class="masthead-brand">Karaokards</h3> <h3 class="masthead-brand">Karaokards</h3>
<nav class="nav nav-masthead justify-content-center"> <nav class="nav nav-masthead justify-content-center">
<a class="nav-link active" href="/">Home</a> <a class="nav-link active" href="/">Home</a>
<a class="nav-link active" href="https://id.twitch.tv/oauth2/authorize?client_id={{.ClientID}}&redirect_uri={{.BaseURI}}/twitchadmin&response_type=code&scope=user:read:broadcast">Admin - log in with twitch</a>
</nav> </nav>
</div> </div>
</header> </header>

View File

@ -1,111 +0,0 @@
<html>
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
<meta name="description" content="Karaokards">
<meta name="author" content="Martyn Ranyard">
<title>Please Stand By!</title>
<!-- Bootstrap core CSS -->
<link href="https://maxcdn.bootstrapcdn.com/bootstrap/4.3.1/css/bootstrap.min.css" rel="stylesheet" crossorigin="anonymous">
<!-- Custom styles for this template -->
<link href="/cover.css" rel="stylesheet">
<style>
.bd-placeholder-img {
font-size: 1.125rem;
text-anchor: middle;
-webkit-user-select: none;
-moz-user-select: none;
-ms-user-select: none;
user-select: none;
}
@media (min-width: 768px) {
.bd-placeholder-img-lg {
font-size: 3.5rem;
}
}
li.match-nomatch{
background-color: #1e2122;
}
li.match-matchtrack{
background-color: #E9B000;
}
li.match-fullmatch{
background-color: #008F95;
}
li.match-matchtrackfuzzt{
background-color: darkgray;
}
li.match-fullmatchfuzzy{
background-color: darkgray;
}
a{
text-decoration-line: underline;
}
</style>
</head>
<body class="text-center" onload=directToResults()>
<div class="cover-container d-flex w-100 h-100 p-3 mx-auto flex-column">
<header class="masthead mb-auto">
<div class="inner">
<h3 class="masthead-brand">Karaokards</h3>
<nav class="nav nav-masthead justify-content-center">
<a class="nav-link active" href="/">Home</a>
</nav>
</div>
</header>
<main role="main" class="inner cover">
<h1 class="cover-heading">Please stand by, twitch gives us stuff that needs to be sent to the server!</h1>
<img src="https://media.giphy.com/media/ule4vhcY1xEKQ/source.gif" alt="Cats typing furiously" />
<p/>
<p>Just hold on, the javascript is doing it's stuff. Don't blame me, twitch really forces us to use javascript here.</p>
<p>Shameless self-promotion : Follow me on twitch - <a href="https://www.twitch.tv/iMartynOnTwitch">iMartynOnTwitch</a>, oddly enough, I do a lot of twitchsings!</p>
</main>
<footer class="mastfoot mt-auto">
<div class="inner">
<p>Cover template for <a href="https://getbootstrap.com/">Bootstrap</a>, by <a href="https://twitter.com/mdo">@mdo</a>.</p>
</div>
</footer>
</div>
<script>
// #access_token=hkq5diaiopu23tzyo5oik7jl7g2w0n&scope=user%3Aread%3Abroadcast&token_type=bearer
function directToResults() {
var url = document.createElement('a');
url.setAttribute("href", window.location.href);
if ((url.port != 80) && (url.port != 443)) {
customPort = ":"+url.port
} else {
customPort = ""
}
u = new URLSearchParams(document.location.hash.substr(1))
var destination = new URL(url.protocol + "//" + url.hostname + customPort + "/twitchtobackend?" + u.toString())
console.log(destination)
window.location.href = destination
}
function toggleUnfound() {
var unmatched = document.getElementsByClassName('match-nomatch'), i;
if (document.getElementById("showhidebutton").getAttribute("tracksHidden") != "true") {
document.getElementById("showhidebutton").setAttribute("tracksHidden","true")
for (i = 0; i < unmatched.length; i += 1) {
unmatched[i].style.display = 'none';
}
} else {
document.getElementById("showhidebutton").setAttribute("tracksHidden","false")
for (i = 0; i < unmatched.length; i += 1) {
unmatched[i].style.display = 'list-item';
}
}
}
</script>
</body>
</html>
</body>
</html>

Binary file not shown.

Before

Width:  |  Height:  |  Size: 263 KiB