Compare commits
17 Commits
Author | SHA1 | Date |
---|---|---|
Martyn | 90a00f93d1 | |
Martyn | f574863601 | |
Martyn | 34403053ac | |
Martyn | b547819234 | |
Martyn | 8dbe61da21 | |
Martyn | 11c96d20d0 | |
Martyn | 4803122aad | |
Martyn | 3950f8b672 | |
Martyn | 6e7c52879c | |
Martyn | fad3079132 | |
Martyn | e989b8454b | |
Martyn | 408516ecea | |
Martyn | 4d3b8a33e5 | |
Martyn | ba1c9438f3 | |
Martyn | 06c3b47f47 | |
Martyn | 8a9b37cb02 | |
Martyn | a85052a41a |
|
@ -0,0 +1,14 @@
|
||||||
|
BUILD=`date +%FT%T%z`
|
||||||
|
|
||||||
|
LDFLAGS=-ldflags "-X main.buildDate=${BUILD}"
|
||||||
|
|
||||||
|
.PHONY: build deps static
|
||||||
|
|
||||||
|
build:
|
||||||
|
go build ${LDFLAGS}
|
||||||
|
|
||||||
|
deps:
|
||||||
|
go get
|
||||||
|
|
||||||
|
static:
|
||||||
|
CGO_ENABLED=0 GOOS=linux go build ${LDFLAGS} -a -installsuffix cgo -o karaokards .
|
|
@ -2,6 +2,8 @@
|
||||||
|
|
||||||
Silly little twitch bot that gives prompts for songs to sing.
|
Silly little twitch bot that gives prompts for songs to sing.
|
||||||
|
|
||||||
|
Almost a hackday style project. Apologies for the mess.
|
||||||
|
|
||||||
Auto-built from my Drone CI and pushed to dockerhub.
|
Auto-built from my Drone CI and pushed to dockerhub.
|
||||||
[![Build Status](https://ci.martyn.berlin/api/badges/martyn/karaokards/status.svg)](https://ci.martyn.berlin/martyn/karaokards)
|
[![Build Status](https://ci.martyn.berlin/api/badges/martyn/karaokards/status.svg)](https://ci.martyn.berlin/martyn/karaokards)
|
||||||
|
|
||||||
|
|
|
@ -2,9 +2,8 @@ FROM golang@sha256:cee6f4b901543e8e3f20da3a4f7caac6ea643fd5a46201c3c2387183a332d
|
||||||
RUN apk update && apk add --no-cache git make ca-certificates && update-ca-certificates
|
RUN apk update && apk add --no-cache git make ca-certificates && update-ca-certificates
|
||||||
COPY main.go /go/src/git.martyn.berlin/martyn/karaokards/
|
COPY main.go /go/src/git.martyn.berlin/martyn/karaokards/
|
||||||
COPY internal/ /go/src/git.martyn.berlin/martyn/karaokards/internal/
|
COPY internal/ /go/src/git.martyn.berlin/martyn/karaokards/internal/
|
||||||
RUN cd /go/src/git.martyn.berlin/martyn/karaokards/; go get; CGO_ENABLED=0 GOOS=linux go build -a -installsuffix cgo -o karaokards .
|
COPY Makefile /go/src/git.martyn.berlin/martyn/karaokards/
|
||||||
#RUN ls /go/src/github.com/karaokards/ -l
|
RUN cd /go/src/git.martyn.berlin/martyn/karaokards/; make deps ; make static
|
||||||
|
|
||||||
|
|
||||||
FROM scratch
|
FROM scratch
|
||||||
COPY --from=builder /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/
|
COPY --from=builder /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/
|
||||||
|
|
|
@ -0,0 +1,4 @@
|
||||||
|
{
|
||||||
|
"channels": ["karaokards"],
|
||||||
|
"externalUrl": "karaokards.ing.martyn.berlin"
|
||||||
|
}
|
|
@ -1,12 +1,12 @@
|
||||||
apiVersion: v1
|
apiVersion: v1
|
||||||
data:
|
data:
|
||||||
strings.json: "{\r\n \"strings\": [\r\n\t \"Let Pineboy choose!\",\r\n\t \"They're
|
config.json: |
|
||||||
from the North\",\r\n \"Refers to food\"\r\n ]\r\n}"
|
{
|
||||||
|
"channels": ["iMartynOnTwitch"],
|
||||||
|
"externalUrl": "karaokards.ing.martyn.berlin"
|
||||||
|
}
|
||||||
|
strings.json: "{\r\n \"strings\": [\r\n\t \"They're from the North\",\r\n
|
||||||
|
\ \"Refers to food\"\r\n ]\r\n}\n"
|
||||||
kind: ConfigMap
|
kind: ConfigMap
|
||||||
metadata:
|
metadata:
|
||||||
creationTimestamp: "2020-01-27T20:04:58Z"
|
name: kardconfig
|
||||||
name: extracards
|
|
||||||
namespace: karaokards
|
|
||||||
resourceVersion: "48537355"
|
|
||||||
selfLink: /api/v1/namespaces/karaokards/configmaps/extracards
|
|
||||||
uid: 4b1d73b0-4140-11ea-94d8-9cb6540931b5
|
|
||||||
|
|
|
@ -1,17 +1,9 @@
|
||||||
apiVersion: extensions/v1beta1
|
apiVersion: apps/v1
|
||||||
kind: Deployment
|
kind: Deployment
|
||||||
metadata:
|
metadata:
|
||||||
annotations:
|
|
||||||
deployment.kubernetes.io/revision: "1"
|
|
||||||
creationTimestamp: "2020-01-27T19:50:47Z"
|
|
||||||
generation: 3
|
|
||||||
labels:
|
labels:
|
||||||
run: kardbot
|
run: kardbot
|
||||||
name: kardbot
|
name: kardbot
|
||||||
namespace: karaokards
|
|
||||||
resourceVersion: "48537399"
|
|
||||||
selfLink: /apis/extensions/v1beta1/namespaces/karaokards/deployments/kardbot
|
|
||||||
uid: 502b4760-413e-11ea-94d8-9cb6540931b5
|
|
||||||
spec:
|
spec:
|
||||||
progressDeadlineSeconds: 600
|
progressDeadlineSeconds: 600
|
||||||
replicas: 1
|
replicas: 1
|
||||||
|
@ -31,7 +23,7 @@ spec:
|
||||||
run: kardbot
|
run: kardbot
|
||||||
spec:
|
spec:
|
||||||
containers:
|
containers:
|
||||||
- image: imartyn/karaokardbot:0.0.1
|
- image: imartyn/karaokardbot:devel
|
||||||
imagePullPolicy: IfNotPresent
|
imagePullPolicy: IfNotPresent
|
||||||
name: kardbot
|
name: kardbot
|
||||||
ports:
|
ports:
|
||||||
|
@ -46,6 +38,11 @@ spec:
|
||||||
- mountPath: /app/strings.json
|
- mountPath: /app/strings.json
|
||||||
name: extracards
|
name: extracards
|
||||||
subPath: strings.json
|
subPath: strings.json
|
||||||
|
- mountPath: /app/config.json
|
||||||
|
name: config
|
||||||
|
subPath: config.json
|
||||||
|
- mountPath: /data
|
||||||
|
name: data
|
||||||
dnsPolicy: ClusterFirst
|
dnsPolicy: ClusterFirst
|
||||||
restartPolicy: Always
|
restartPolicy: Always
|
||||||
schedulerName: default-scheduler
|
schedulerName: default-scheduler
|
||||||
|
@ -61,24 +58,15 @@ spec:
|
||||||
items:
|
items:
|
||||||
- key: strings.json
|
- key: strings.json
|
||||||
path: strings.json
|
path: strings.json
|
||||||
name: extracards
|
name: kardconfig
|
||||||
name: extracards
|
name: extracards
|
||||||
status:
|
- configMap:
|
||||||
availableReplicas: 1
|
defaultMode: 420
|
||||||
conditions:
|
items:
|
||||||
- lastTransitionTime: "2020-01-27T19:50:47Z"
|
- key: config.json
|
||||||
lastUpdateTime: "2020-01-27T20:05:34Z"
|
path: config.json
|
||||||
message: ReplicaSet "kardbot-6dff8d86dd" has successfully progressed.
|
name: kardconfig
|
||||||
reason: NewReplicaSetAvailable
|
name: config
|
||||||
status: "True"
|
- name: data
|
||||||
type: Progressing
|
persistentVolumeClaim:
|
||||||
- lastTransitionTime: "2020-01-27T20:08:26Z"
|
claimName: kkard-data
|
||||||
lastUpdateTime: "2020-01-27T20:08:26Z"
|
|
||||||
message: Deployment has minimum availability.
|
|
||||||
reason: MinimumReplicasAvailable
|
|
||||||
status: "True"
|
|
||||||
type: Available
|
|
||||||
observedGeneration: 3
|
|
||||||
readyReplicas: 1
|
|
||||||
replicas: 1
|
|
||||||
updatedReplicas: 1
|
|
||||||
|
|
|
@ -0,0 +1,21 @@
|
||||||
|
apiVersion: extensions/v1beta1
|
||||||
|
kind: Ingress
|
||||||
|
metadata:
|
||||||
|
annotations:
|
||||||
|
certmanager.k8s.io/cluster-issuer: letsencrypt
|
||||||
|
name: karaokards
|
||||||
|
spec:
|
||||||
|
rules:
|
||||||
|
- host: karaokards-dev.ing.martyn.berlin
|
||||||
|
http:
|
||||||
|
paths:
|
||||||
|
- backend:
|
||||||
|
serviceName: karaokards
|
||||||
|
servicePort: 80
|
||||||
|
path: /
|
||||||
|
tls:
|
||||||
|
- hosts:
|
||||||
|
- karaokards-dev.ing.martyn.berlin
|
||||||
|
secretName: karaokards-dev-cert
|
||||||
|
status:
|
||||||
|
loadBalancer: {}
|
|
@ -0,0 +1,10 @@
|
||||||
|
apiVersion: v1
|
||||||
|
kind: PersistentVolumeClaim
|
||||||
|
metadata:
|
||||||
|
name: kkard-data
|
||||||
|
spec:
|
||||||
|
accessModes:
|
||||||
|
- ReadWriteOnce
|
||||||
|
resources:
|
||||||
|
requests:
|
||||||
|
storage: 1Gi
|
|
@ -0,0 +1,17 @@
|
||||||
|
apiVersion: v1
|
||||||
|
kind: Service
|
||||||
|
metadata:
|
||||||
|
creationTimestamp: null
|
||||||
|
labels:
|
||||||
|
run: kardbot
|
||||||
|
name: karaokards
|
||||||
|
spec:
|
||||||
|
externalTrafficPolicy: Cluster
|
||||||
|
ports:
|
||||||
|
- port: 80
|
||||||
|
protocol: TCP
|
||||||
|
targetPort: 5353
|
||||||
|
selector:
|
||||||
|
run: kardbot
|
||||||
|
sessionAffinity: None
|
||||||
|
type: LoadBalancer
|
|
@ -2,6 +2,7 @@ package irc
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"bufio"
|
"bufio"
|
||||||
|
"encoding/base64"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
@ -15,15 +16,23 @@ 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"
|
||||||
)
|
)
|
||||||
|
|
||||||
const PSTFormat = "Jan 2 15:04:05 PST"
|
const UTCFormat = "Jan 2 15:04:05 UTC"
|
||||||
|
|
||||||
|
// 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.
|
||||||
//
|
//
|
||||||
|
@ -38,22 +47,43 @@ type OAuthCred struct {
|
||||||
|
|
||||||
// The developer application client ID. Used for API calls to Twitch.
|
// The developer application client ID. Used for API calls to Twitch.
|
||||||
ClientID string `json:"client_id,omitempty"`
|
ClientID string `json:"client_id,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
// List of Channels to join
|
type ConfigStruct struct {
|
||||||
Channels []string `json:"channels,omitempty"`
|
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
|
||||||
Credentials *OAuthCred
|
IrcCredentials *OAuthCred
|
||||||
|
AppCredentials *OAuthCred
|
||||||
MsgRate time.Duration
|
MsgRate time.Duration
|
||||||
Name string
|
Name string
|
||||||
Port string
|
Port string
|
||||||
PrivatePath string
|
IrcPrivatePath string
|
||||||
|
AppPrivatePath string
|
||||||
Server string
|
Server string
|
||||||
startTime time.Time
|
startTime time.Time
|
||||||
Prompts []string
|
Prompts []string
|
||||||
|
Database scribble.Driver
|
||||||
|
ChannelData map[string]ChannelData
|
||||||
|
Config ConfigStruct
|
||||||
|
}
|
||||||
|
|
||||||
|
type ChannelData struct {
|
||||||
|
Name string `json:"name"`
|
||||||
|
AdminKey string `json:"value,omitempty"`
|
||||||
|
Command string `json:"customcommand,omitempty"`
|
||||||
|
ExtraStrings string `json:"extrastrings,omitempty"`
|
||||||
|
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
|
||||||
|
@ -80,6 +110,17 @@ 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 {
|
||||||
|
@ -108,9 +149,29 @@ 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]
|
||||||
|
@ -120,21 +181,33 @@ func (bb *KardBot) HandleChat() error {
|
||||||
case "PRIVMSG":
|
case "PRIVMSG":
|
||||||
msg := matches[4]
|
msg := matches[4]
|
||||||
rgb.GPrintf("[%s] %s: %s\n", TimeStamp(), userName, msg)
|
rgb.GPrintf("[%s] %s: %s\n", TimeStamp(), userName, msg)
|
||||||
|
rgb.GPrintf("[%s] raw line: %s\n", TimeStamp(), line)
|
||||||
|
|
||||||
// parse commands from user message
|
// parse commands from user message
|
||||||
cmdMatches := CmdRegex.FindStringSubmatch(msg)
|
cmdMatches := CmdRegex.FindStringSubmatch(msg)
|
||||||
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 "card":
|
case bb.ChannelData[channel].Command:
|
||||||
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
|
||||||
if userName == bb.Channel {
|
if userName == channel {
|
||||||
switch cmd {
|
switch cmd {
|
||||||
case "tbdown":
|
case "tbdown":
|
||||||
rgb.CPrintf(
|
rgb.CPrintf(
|
||||||
|
@ -144,6 +217,18 @@ func (bb *KardBot) HandleChat() error {
|
||||||
|
|
||||||
bb.Disconnect()
|
bb.Disconnect()
|
||||||
return nil
|
return nil
|
||||||
|
case "kcardadmin":
|
||||||
|
magicCode := bb.ReadOrCreateChannelKey(channel)
|
||||||
|
rgb.CPrintf(
|
||||||
|
"[%s] Magic code is %s - https://karaokards.ing.martyn.berlin/admin/%s/%s\n",
|
||||||
|
TimeStamp(),
|
||||||
|
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.")
|
||||||
default:
|
default:
|
||||||
// do nothing
|
// do nothing
|
||||||
}
|
}
|
||||||
|
@ -162,10 +247,18 @@ 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.Credentials.Password + "\r\n"))
|
bb.conn.Write([]byte("PASS " + bb.IrcCredentials.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 {
|
||||||
|
@ -179,23 +272,59 @@ func (bb *KardBot) JoinChannel(channels ...string) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Reads from the private credentials file and stores the data in the bot's Credentials field.
|
// Reads from the private credentials file and stores the data in the bot's appropriate Credentials field.
|
||||||
func (bb *KardBot) ReadCredentials() error {
|
func (bb *KardBot) ReadCredentials(credType string) error {
|
||||||
|
|
||||||
|
var err error
|
||||||
|
var credFile []byte
|
||||||
// reads from the file
|
// reads from the file
|
||||||
credFile, err := ioutil.ReadFile(bb.PrivatePath)
|
if credType == "IRC" {
|
||||||
|
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(bb.Credentials); nil != err && io.EOF != err {
|
if err = dec.Decode(&creds); 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
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -229,18 +358,36 @@ 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()
|
err := bb.ReadCredentials("IRC")
|
||||||
if nil != err {
|
if nil != err {
|
||||||
fmt.Println(err)
|
fmt.Println(err)
|
||||||
fmt.Println("Aborting...")
|
fmt.Println("Aborting!")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
err = bb.ReadCredentials("App")
|
||||||
|
if nil != err {
|
||||||
|
fmt.Println(err)
|
||||||
|
fmt.Println("Aborting!")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
err = bb.readChannelData()
|
||||||
|
if nil != err {
|
||||||
|
fmt.Println(err)
|
||||||
|
fmt.Println("Aborting!")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
for {
|
for {
|
||||||
bb.Connect()
|
bb.Connect()
|
||||||
bb.Login()
|
bb.Login()
|
||||||
if len(bb.Credentials.Channels) > 0 {
|
if len(bb.ChannelData) > 0 {
|
||||||
bb.JoinChannel(bb.Credentials.Channels...)
|
for channelName,channelData := range bb.ChannelData {
|
||||||
|
if !channelData.HasLeft {
|
||||||
|
bb.JoinChannel(channelName)
|
||||||
|
}
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
bb.JoinChannel()
|
bb.JoinChannel()
|
||||||
}
|
}
|
||||||
|
@ -257,8 +404,87 @@ func (bb *KardBot) Start() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (bb *KardBot) readChannelData() error {
|
||||||
|
records, err := bb.Database.ReadAll("channelData")
|
||||||
|
if err != nil {
|
||||||
|
// no db? initialise one?
|
||||||
|
record := ChannelData{Name: bb.Channel, JoinTime: time.Now(), Command: "card"}
|
||||||
|
rgb.YPrintf("[%s] No channel table for #%s exists, creating...\n", TimeStamp(), bb.Channel)
|
||||||
|
if err := bb.Database.Write("channelData", bb.Channel, record); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
bb.ChannelData = make(map[string]ChannelData)
|
||||||
|
bb.ChannelData[bb.Channel] = record
|
||||||
|
} else {
|
||||||
|
bb.ChannelData = make(map[string]ChannelData)
|
||||||
|
}
|
||||||
|
for _, data := range records {
|
||||||
|
record := ChannelData{}
|
||||||
|
err := json.Unmarshal([]byte(data), &record)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if record.Name != "" {
|
||||||
|
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
|
||||||
|
}
|
||||||
|
|
||||||
|
func (bb *KardBot) ReadOrCreateChannelKey(channel string) string {
|
||||||
|
magicCode := ""
|
||||||
|
var err error
|
||||||
|
var record ChannelData
|
||||||
|
if record, ok := bb.ChannelData[channel]; !ok {
|
||||||
|
rgb.YPrintf("[%s] No channel data for #%s exists, creating\n", TimeStamp(), channel)
|
||||||
|
err = bb.Database.Read("channelData", channel, &record)
|
||||||
|
if err == nil {
|
||||||
|
bb.ChannelData[channel] = record
|
||||||
|
}
|
||||||
|
}
|
||||||
|
record = bb.ChannelData[channel]
|
||||||
|
if err != nil || record.AdminKey == "" {
|
||||||
|
rgb.YPrintf("[%s] No channel key for #%s exists, creating one\n", TimeStamp(), channel)
|
||||||
|
newuu, _ := uuid.NewRandom()
|
||||||
|
magicCode = base64.StdEncoding.EncodeToString([]byte(newuu.String()))
|
||||||
|
record.HasLeft = true
|
||||||
|
record.AdminKey = magicCode
|
||||||
|
if record.Name == "" {
|
||||||
|
record.Name = channel
|
||||||
|
}
|
||||||
|
if err := bb.Database.Write("channelData", channel, record); err != nil {
|
||||||
|
rgb.RPrintf("[%s] Error writing channel data for #%s\n", TimeStamp(), channel)
|
||||||
|
}
|
||||||
|
bb.ChannelData[record.Name] = record
|
||||||
|
rgb.YPrintf("[%s] Cached channel key for #%s\n", TimeStamp(), record.Name)
|
||||||
|
} else {
|
||||||
|
magicCode = record.AdminKey
|
||||||
|
rgb.YPrintf("[%s] Loaded data for #%s\n", TimeStamp(), channel)
|
||||||
|
}
|
||||||
|
return magicCode
|
||||||
|
}
|
||||||
|
|
||||||
func TimeStamp() string {
|
func TimeStamp() string {
|
||||||
return TimeStampFmt(PSTFormat)
|
return TimeStampFmt(UTCFormat)
|
||||||
}
|
}
|
||||||
|
|
||||||
func TimeStampFmt(format string) string {
|
func TimeStampFmt(format string) string {
|
||||||
|
|
|
@ -7,9 +7,12 @@ import (
|
||||||
"github.com/gorilla/handlers"
|
"github.com/gorilla/handlers"
|
||||||
"github.com/gorilla/mux"
|
"github.com/gorilla/mux"
|
||||||
|
|
||||||
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
"html/template"
|
"html/template"
|
||||||
|
"io/ioutil"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
"net/url"
|
||||||
"os"
|
"os"
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
@ -17,7 +20,31 @@ import (
|
||||||
|
|
||||||
//var store = sessions.NewCookieStore(os.Getenv("SESSION_KEY"))
|
//var store = sessions.NewCookieStore(os.Getenv("SESSION_KEY"))
|
||||||
|
|
||||||
var ircBot irc.KardBot
|
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) {
|
func HealthHandler(response http.ResponseWriter, request *http.Request) {
|
||||||
response.Header().Add("Content-type", "text/plain")
|
response.Header().Add("Content-type", "text/plain")
|
||||||
|
@ -26,6 +53,7 @@ func HealthHandler(response http.ResponseWriter, request *http.Request) {
|
||||||
|
|
||||||
func NotFoundHandler(response http.ResponseWriter, request *http.Request) {
|
func NotFoundHandler(response http.ResponseWriter, request *http.Request) {
|
||||||
response.Header().Add("X-Template-File", "html"+request.URL.Path)
|
response.Header().Add("X-Template-File", "html"+request.URL.Path)
|
||||||
|
response.WriteHeader(404)
|
||||||
tmpl := template.Must(template.ParseFiles("web/404.html"))
|
tmpl := template.Must(template.ParseFiles("web/404.html"))
|
||||||
tmpl.Execute(response, nil)
|
tmpl.Execute(response, nil)
|
||||||
}
|
}
|
||||||
|
@ -48,6 +76,8 @@ 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,
|
||||||
|
@ -72,7 +102,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), len(ircBot.Credentials.Channels), 0}
|
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)
|
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)
|
||||||
|
@ -80,15 +110,238 @@ func TemplateHandler(response http.ResponseWriter, request *http.Request) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func HandleHTTP(passedIrcBot irc.KardBot) {
|
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")
|
||||||
|
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!\n---\n")
|
||||||
|
fmt.Fprint(response, usersObject.Data)
|
||||||
|
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("/example/{.*}", TemplateHandler)
|
r.HandleFunc("/web/{.*}", 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("/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,
|
||||||
|
|
204
main.go
204
main.go
|
@ -2,7 +2,6 @@ package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"fmt"
|
|
||||||
"io/ioutil"
|
"io/ioutil"
|
||||||
"math/rand"
|
"math/rand"
|
||||||
"os"
|
"os"
|
||||||
|
@ -13,6 +12,7 @@ import (
|
||||||
irc "git.martyn.berlin/martyn/karaokards/internal/irc"
|
irc "git.martyn.berlin/martyn/karaokards/internal/irc"
|
||||||
webserver "git.martyn.berlin/martyn/karaokards/internal/webserver"
|
webserver "git.martyn.berlin/martyn/karaokards/internal/webserver"
|
||||||
rgb "github.com/foresthoffman/rgblog"
|
rgb "github.com/foresthoffman/rgblog"
|
||||||
|
scribble "github.com/nanobox-io/golang-scribble"
|
||||||
)
|
)
|
||||||
|
|
||||||
type customStringsStruct struct {
|
type customStringsStruct struct {
|
||||||
|
@ -23,30 +23,129 @@ var selectablePrompts []string
|
||||||
|
|
||||||
var customStrings customStringsStruct
|
var customStrings customStringsStruct
|
||||||
|
|
||||||
func readBonusStrings() []string {
|
var config irc.ConfigStruct
|
||||||
|
|
||||||
ex, err := os.Executable()
|
func readConfig() {
|
||||||
if err != nil {
|
var data []byte
|
||||||
fmt.Println("Could not read `strings.json`, will only have builtin prompts. File reading error", err)
|
var err error
|
||||||
return []string{}
|
configFile := ""
|
||||||
|
if os.Getenv("KARAOKARDS_CONFIGFILE") != "" {
|
||||||
|
if _, err := os.Stat(os.Getenv("KARAOKARDS_CONFIGFILE")); os.IsNotExist(err) {
|
||||||
|
rgb.RPrintf("[%s] Error, KARAOKARDS_CONFIGFILE env var set and '%s' doesn't exist!\n", irc.TimeStamp(), os.Getenv("KARAOKARDS_CONFIGFILE"))
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
configFile = os.Getenv("KARAOKARDS_CONFIGFILE")
|
||||||
|
} else {
|
||||||
|
ex, err := os.Executable()
|
||||||
|
if err != nil {
|
||||||
|
rgb.YPrintf("[%s] Warning, KARAOKARDS_CONFIGFILE env var unset and cannot find executable!\n", irc.TimeStamp())
|
||||||
|
}
|
||||||
|
exPath := filepath.Dir(ex)
|
||||||
|
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())
|
||||||
|
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")
|
||||||
|
os.Exit(1)
|
||||||
|
} else {
|
||||||
|
configFile = "/etc/karaokards/config.json"
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
configFile = exPath + "/config.json"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
exPath := filepath.Dir(ex)
|
data, err = ioutil.ReadFile(configFile)
|
||||||
data, err := ioutil.ReadFile(exPath + "/strings.json")
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
fmt.Println("Could not read `strings.json`, will only have builtin prompts. File reading error", err)
|
rgb.RPrintf("[%s] Could not read `%s`. File reading error: %s\n", irc.TimeStamp(), configFile, err)
|
||||||
return []string{}
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
err = json.Unmarshal(data, &config)
|
||||||
|
if err != nil {
|
||||||
|
rgb.RPrintf("[%s] Could not unmarshal `%s`. Unmarshal error: %s\n", irc.TimeStamp(), configFile, err)
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
rgb.YPrintf("[%s] Read config file from `%s`\n", irc.TimeStamp(), configFile)
|
||||||
|
rgb.YPrintf("[%s] config %v\n", irc.TimeStamp(), config)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
//openDatabase "database" in this sense being a scribble db
|
||||||
|
func openDatabase() *scribble.Driver {
|
||||||
|
dataPath := ""
|
||||||
|
if config.DataPath == "" {
|
||||||
|
if os.Getenv("KARAOKARDS_DATA_FOLDER") != "" {
|
||||||
|
if _, err := os.Stat(os.Getenv("KARAOKARDS_DATA_FOLDER")); os.IsNotExist(err) {
|
||||||
|
rgb.RPrintf("[%s] Error, KARAOKARDS_DATA_FOLDER env var set and '%s' doesn't exist!\n", irc.TimeStamp(), os.Getenv("KARAOKARDS_DATA_FOLDER"))
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
dataPath = os.Getenv("KARAOKARDS_DATA_FOLDER")
|
||||||
|
} else {
|
||||||
|
ex, err := os.Executable()
|
||||||
|
if err != nil {
|
||||||
|
rgb.RPrintf("[%s] Error, KARAOKARDS_DATA_FOLDER env var unset and cannot find executable!\n", irc.TimeStamp())
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
exPath := filepath.Dir(ex)
|
||||||
|
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")
|
||||||
|
err = os.Mkdir(exPath+"/data", 0770)
|
||||||
|
if err != nil {
|
||||||
|
rgb.RPrintf("[%s] Error cannot create %s: %s!\n", irc.TimeStamp(), exPath+"/data", err)
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
dataPath = exPath + "/data"
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if _, err := os.Stat(config.DataPath); os.IsNotExist(err) {
|
||||||
|
rgb.RPrintf("[%s] Error, config-specified path '%s' doesn't exist!\n", irc.TimeStamp(), config.DataPath)
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
dataPath = config.DataPath
|
||||||
|
}
|
||||||
|
db, err := scribble.New(dataPath, nil)
|
||||||
|
if err != nil {
|
||||||
|
rgb.RPrintf("[%s] Error opening database in '%s' : %s\n", irc.TimeStamp(), dataPath, err)
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
return db
|
||||||
|
}
|
||||||
|
|
||||||
|
func readBonusStrings() []string {
|
||||||
|
var data []byte
|
||||||
|
var err error
|
||||||
|
if config.StringPath == "" {
|
||||||
|
ex, err := os.Executable()
|
||||||
|
if err != nil {
|
||||||
|
rgb.YPrintf("[%s] Could not read `strings.json`, will only have builtin prompts. File reading error: %s\n", irc.TimeStamp(), err)
|
||||||
|
return []string{}
|
||||||
|
}
|
||||||
|
exPath := filepath.Dir(ex)
|
||||||
|
data, err = ioutil.ReadFile(exPath + "/strings.json")
|
||||||
|
if err != nil {
|
||||||
|
rgb.YPrintf("[%s] Could not read `strings.json`, will only have builtin prompts. File reading error: %s\n", irc.TimeStamp(), err)
|
||||||
|
return []string{}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
data, err = ioutil.ReadFile(config.StringPath)
|
||||||
|
if err != nil {
|
||||||
|
rgb.YPrintf("[%s] Could not read `strings.json`, will only have builtin prompts. File reading error: %s\n", irc.TimeStamp(), err)
|
||||||
|
return []string{}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
err = json.Unmarshal(data, &customStrings)
|
err = json.Unmarshal(data, &customStrings)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
fmt.Println("Could not unmarshal `strings.json`, will only have builtin prompts. Unmarshal error", err)
|
rgb.YPrintf("[%s] Could not unmarshal `strings.json`, will only have builtin prompts. Unmarshal error: %s\n", irc.TimeStamp(), err)
|
||||||
return []string{}
|
return []string{}
|
||||||
}
|
}
|
||||||
fmt.Println("Read ", len(customStrings.Strings), " prompts from `strings.json`")
|
rgb.YPrintf("[%s] Read %d prompts from `strings.json`\n", irc.TimeStamp(), len(customStrings.Strings))
|
||||||
return customStrings.Strings
|
return customStrings.Strings
|
||||||
}
|
}
|
||||||
|
|
||||||
func main() {
|
var buildDate string
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
rgb.YPrintf("[%s] starting karaokard bot build %s\n", irc.TimeStamp(), buildDate)
|
||||||
|
readConfig()
|
||||||
rand.Seed(time.Now().UnixNano())
|
rand.Seed(time.Now().UnixNano())
|
||||||
for _, val := range builtins.Karaokards {
|
for _, val := range builtins.Karaokards {
|
||||||
selectablePrompts = append(selectablePrompts, val)
|
selectablePrompts = append(selectablePrompts, val)
|
||||||
|
@ -54,39 +153,84 @@ func main() {
|
||||||
for _, val := range readBonusStrings() {
|
for _, val := range readBonusStrings() {
|
||||||
selectablePrompts = append(selectablePrompts, val)
|
selectablePrompts = append(selectablePrompts, val)
|
||||||
}
|
}
|
||||||
fmt.Println(len(selectablePrompts), " prompts available.")
|
persistentData := openDatabase()
|
||||||
oauthPath := ""
|
var dbGlobalPrompts []string
|
||||||
if os.Getenv("TWITCH_OAUTH_JSON") != "" {
|
if err := persistentData.Read("prompts", "global", &dbGlobalPrompts); err != nil {
|
||||||
if _, err := os.Stat(os.Getenv("TWITCH_OAUTH_JSON")); os.IsNotExist(err) {
|
persistentData.Write("prompts", "common", dbGlobalPrompts)
|
||||||
os.Exit(1)
|
}
|
||||||
}
|
selectablePrompts := append(selectablePrompts, dbGlobalPrompts...)
|
||||||
oauthPath = os.Getenv("TWITCH_OAUTH_JSON")
|
|
||||||
} else {
|
rgb.YPrintf("[%s] %d prompts available.\n", irc.TimeStamp(), len(selectablePrompts))
|
||||||
if _, err := os.Stat(os.Getenv("HOME") + "/.twitch/oauth.json"); os.IsNotExist(err) {
|
ircOauthPath := ""
|
||||||
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 config.IrcOAuthPath == "" {
|
||||||
if _, err := os.Stat("/etc/twitch/oauth.json"); os.IsNotExist(err) {
|
if os.Getenv("TWITCH_OAUTH_JSON") != "" {
|
||||||
rgb.YPrintf("[%s] Error %s doesn't exist either, bailing!\n", irc.TimeStamp(), "/etc/twitch/oauth.json")
|
if _, err := os.Stat(os.Getenv("TWITCH_OAUTH_JSON")); os.IsNotExist(err) {
|
||||||
os.Exit(1)
|
os.Exit(1)
|
||||||
}
|
}
|
||||||
oauthPath = "/etc/twitch/oauth.json"
|
ircOauthPath = os.Getenv("TWITCH_OAUTH_JSON")
|
||||||
} else {
|
} else {
|
||||||
oauthPath = os.Getenv("HOME") + "/.twitch/oauth.json"
|
if _, err := os.Stat(os.Getenv("HOME") + "/.twitch/ircoauth.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")
|
||||||
|
if _, err := os.Stat("/etc/twitch/ircoauth.json"); os.IsNotExist(err) {
|
||||||
|
rgb.YPrintf("[%s] Error %s doesn't exist either, bailing!\n", irc.TimeStamp(), "/etc/twitch/ircoauth.json")
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
ircOauthPath = "/etc/twitch/ircoauth.json"
|
||||||
|
} else {
|
||||||
|
ircOauthPath = os.Getenv("HOME") + "/.twitch/ircoauth.json"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
|
if _, err := os.Stat(config.IrcOAuthPath); os.IsNotExist(err) {
|
||||||
|
rgb.YPrintf("[%s] Error config-specified oauth file %s doesn't exist, bailing!\n", irc.TimeStamp(), config.IrcOAuthPath)
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
ircOauthPath = config.IrcOAuthPath
|
||||||
}
|
}
|
||||||
|
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: "imartynontwitch",
|
Channel: "karaokards",
|
||||||
MsgRate: time.Duration(20/30) * time.Millisecond,
|
MsgRate: time.Duration(20/30) * time.Millisecond,
|
||||||
Name: "Karaokards",
|
Name: "Karaokards",
|
||||||
Port: "6667",
|
Port: "6667",
|
||||||
PrivatePath: oauthPath,
|
IrcPrivatePath: ircOauthPath,
|
||||||
|
AppPrivatePath: appOauthPath,
|
||||||
Server: "irc.chat.twitch.tv",
|
Server: "irc.chat.twitch.tv",
|
||||||
Prompts: selectablePrompts,
|
Prompts: selectablePrompts,
|
||||||
|
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()
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,107 @@
|
||||||
|
<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>
|
|
@ -0,0 +1,143 @@
|
||||||
|
<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> </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>
|
|
@ -0,0 +1,75 @@
|
||||||
|
<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>
|
|
@ -57,6 +57,7 @@
|
||||||
<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>
|
||||||
|
|
|
@ -0,0 +1,111 @@
|
||||||
|
<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.
After Width: | Height: | Size: 263 KiB |
Loading…
Reference in New Issue