Compare commits

..

No commits in common. "main" and "v0.0.12" have entirely different histories.

14 changed files with 252 additions and 603 deletions

View File

@ -100,8 +100,6 @@ class AppAdmin extends React.Component {
const adminToken = window.location.href.split("/")[5] const adminToken = window.location.href.split("/")[5]
const csvURL = "/csv/"+ channelName + "/" + adminToken const csvURL = "/csv/"+ channelName + "/" + adminToken
const tsvURL = "/tsv/"+ channelName + "/" + adminToken const tsvURL = "/tsv/"+ channelName + "/" + adminToken
const scriptURL = "/script.bat/"+ channelName + "/" + adminToken
const bockScriptURL = "https://tsdownloader.azurewebsites.net/api/ScriptApi/GenerateScript?userName="+ channelName
const setPage = this.setPage const setPage = this.setPage
const reloadDuetComponent = this.state.reloadDuetComponent const reloadDuetComponent = this.state.reloadDuetComponent
const reloadSongComponent = this.state.reloadSongComponent const reloadSongComponent = this.state.reloadSongComponent
@ -123,10 +121,9 @@ class AppAdmin extends React.Component {
<Tab label="Top Songs" {...a11yProps(0)}/> <Tab label="Top Songs" {...a11yProps(0)}/>
<Tab label="Top Singers" {...a11yProps(1)}/> <Tab label="Top Singers" {...a11yProps(1)}/>
<Tab label="Data" {...a11yProps(2)}/> <Tab label="Data" {...a11yProps(2)}/>
<Tab label="Download Videos" {...a11yProps(3)}/> <Tab label="Export" {...a11yProps(3)}/>
<Tab label="Export" {...a11yProps(4)}/> <Tab label="Cache Details" {...a11yProps(4)}/>
<Tab label="Cache Details" {...a11yProps(5)}/> <Tab label="Chatbot" {...a11yProps(4)}/>
<Tab label="Chatbot" {...a11yProps(6)}/>
</Tabs> </Tabs>
</AppBar> </AppBar>
<TabPanel value={page} index={0}> <TabPanel value={page} index={0}>
@ -142,32 +139,6 @@ class AppAdmin extends React.Component {
onReloadedChange={this.reloadDuetComponent}/> onReloadedChange={this.reloadDuetComponent}/>
</TabPanel> </TabPanel>
<TabPanel value={page} index={3}> <TabPanel value={page} index={3}>
<Typography variant="h3" component="h2" gutterBottom>
RIP Twitch sings!
</Typography>
<Typography variant="h5" component="h2" gutterBottom>
Here is a script to download all your published duets and solo performances.
</Typography>
<Typography variant="p">
First, you need to <a href="https://github.com/ytdl-org/youtube-dl/releases/download/2020.07.28/youtube-dl.exe">download youtube-dl from here</a>.
Then, download your <a href={scriptURL}>personal script from here</a>.<br/><br/>
</Typography>
<Typography variant="p">
Put both files in the same folder and run script.bat. I know, that's not a nice interface, but, it works. Right now, I can't bear to do much more,
hurts to even think about the situation. It creates a subfolder per person and dates the files.
It even works if you sang the same song with the same person on the same day.<br/><br/>
</Typography>
<Typography variant="p">
If you need to pause, press "Control" and "C" and say "Y" to the question. Next run the script will actually happily resume where it left off.<br/><br/>
</Typography>
<Typography variant="h5" component="h2" gutterBottom>
You might be interested in BockTown's script that does the other side of the coin - finds and downloads duets completed by others of your open seeds
</Typography>
<Typography variant="p">
You can download your version of his script <a href={bockScriptURL}>from here</a>. It'll take a bit to generate so be patient, click the link once and wait. ;-)
</Typography>
</TabPanel>
<TabPanel value={page} index={4}>
<Typography variant="h5" component="h2" gutterBottom> <Typography variant="h5" component="h2" gutterBottom>
Download your data as : Download your data as :
</Typography> </Typography>

View File

@ -1,66 +0,0 @@
apiVersion: apps/v1
kind: Deployment
metadata:
name: {{ include "twitchsingstools.fullname" . }}-db
annotations:
checksum/config: {{ include (print $.Template.BasePath "/configmap.yaml") . | sha256sum }}
checksum/secret: {{ include (print $.Template.BasePath "/secret.yaml") . | sha256sum }}
labels:
{{- include "twitchsingstools.labels" . | nindent 4 }}
spec:
replicas: {{ .Values.replicaCount }}
selector:
matchLabels:
{{- include "twitchsingstools.selectorLabels" . | nindent 6 }}
template:
metadata:
labels:
{{- include "twitchsingstools.selectorLabels" . | nindent 8 }}
annotations:
checksum/config: {{ include (print $.Template.BasePath "/configmap.yaml") . | sha256sum }}
checksum/secret: {{ include (print $.Template.BasePath "/secret.yaml") . | sha256sum }}
spec:
{{- with .Values.imagePullSecrets }}
imagePullSecrets:
{{- toYaml . | nindent 8 }}
{{- end }}
serviceAccountName: {{ include "twitchsingstools.serviceAccountName" . }}
securityContext:
{{- toYaml .Values.podSecurityContext | nindent 8 }}
containers:
- name: {{ .Chart.Name }}
securityContext:
{{- toYaml .Values.securityContext | nindent 12 }}
image: "{{ .Values.db.image.repository }}:{{ .Values.db.image.tag | default "latest" }}"
imagePullPolicy: {{ .Values.db.image.pullPolicy }}
ports:
- name: redis
containerPort: 9221
protocol: TCP
livenessProbe:
tcpSocket:
port: redis
readinessProbe:
tcpSocket:
port: redis
resources:
{{- toYaml .Values.resources | nindent 12 }}
volumeMounts:
- mountPath: /pika/db
name: data
volumes:
- name: data
persistentVolumeClaim:
claimName: tstools-db
{{- with .Values.nodeSelector }}
nodeSelector:
{{- toYaml . | nindent 8 }}
{{- end }}
{{- with .Values.affinity }}
affinity:
{{- toYaml . | nindent 8 }}
{{- end }}
{{- with .Values.tolerations }}
tolerations:
{{- toYaml . | nindent 8 }}
{{- end }}

View File

@ -32,8 +32,6 @@ spec:
env: env:
- name: TSTOOLS_DATA_FOLDER - name: TSTOOLS_DATA_FOLDER
value: /data value: /data
- name: TSTOOLS_REDIS_HOST
value: {{ .Values.db.service.name }}
securityContext: securityContext:
{{- toYaml .Values.securityContext | nindent 12 }} {{- toYaml .Values.securityContext | nindent 12 }}
image: "{{ .Values.image.repository }}:{{ .Values.image.tag | default "latest" }}" image: "{{ .Values.image.repository }}:{{ .Values.image.tag | default "latest" }}"

View File

@ -1,12 +0,0 @@
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: tstools-db
labels:
{{- include "twitchsingstools.labels" . | nindent 4 }}
spec:
accessModes:
- ReadWriteOnce
resources:
requests:
storage: {{ .Values.db.storageSize }}

View File

@ -1,15 +0,0 @@
apiVersion: v1
kind: Service
metadata:
name: {{ .Values.db.service.name }}
labels:
{{- include "twitchsingstools.labels" . | nindent 4 }}
spec:
type: {{ .Values.db.service.type }}
ports:
- port: {{ .Values.db.service.port }}
targetPort: redis
protocol: TCP
name: redis
selector:
{{- include "twitchsingstools.selectorLabels" . | nindent 4 }}

View File

@ -6,7 +6,7 @@ replicaCount: 1
image: image:
repository: imartyn/twitchsingstools repository: imartyn/twitchsingstools
pullPolicy: Always pullPolicy: IfNotPresent
tag: dev tag: dev
imagePullSecrets: [] imagePullSecrets: []

View File

@ -1,81 +0,0 @@
# Default values for twitchsingstools.
# This is a YAML-formatted file.
# Declare variables to be passed into your templates.
replicaCount: 1
image:
repository: imartyn/twitchsingstools
tag: 0.0-linux-amd64
pullPolicy: Always
imagePullSecrets: []
nameOverride: ""
fullnameOverride: ""
serviceAccount:
# Specifies whether a service account should be created
create: true
# Annotations to add to the service account
annotations: {}
# The name of the service account to use.
# If not set and create is true, a name is generated using the fullname template
name:
podSecurityContext: {}
# fsGroup: 2000
securityContext: {}
# capabilities:
# drop:
# - ALL
# readOnlyRootFilesystem: true
# runAsNonRoot: true
# runAsUser: 1000
secretFiles: {}
irc:
nick: tstools
twitchapp: {}
storageSize: 10Gi
service:
type: ClusterIP
port: 80
externalHostname: twitchsingstools.martyn.berlin
ingress:
enabled: true
annotations:
kubernetes.io/ingress.class: traefik
cert-manager.io/cluster-issuer: letsencrypt
hosts:
- host: twitchsingstools.martyn.berlin
paths:
- /
tls:
- secretName: tstools-tls
hosts:
- twitchsingstools.martyn.berlin
resources: {}
# We usually recommend not to specify default resources and to leave this as a conscious
# choice for the user. This also increases chances charts run on environments with little
# resources, such as Minikube. If you do want to specify resources, uncomment the following
# lines, adjust them as necessary, and remove the curly braces after 'resources:'.
# limits:
# cpu: 100m
# memory: 128Mi
# requests:
# cpu: 100m
# memory: 128Mi
nodeSelector: {}
tolerations: []
affinity: {}

View File

@ -41,15 +41,6 @@ twitchapp: {}
storageSize: 10Gi storageSize: 10Gi
db:
service:
port: 4920
name: tstools-db
storageSize: 10Gi
image:
repository: pikadb/pika
pullPolicy: IfNotPresent
service: service:
type: ClusterIP type: ClusterIP
port: 80 port: 80

View File

@ -1,234 +0,0 @@
package data
import (
"encoding/base64"
"encoding/json"
"errors"
"log"
"os"
"time"
rgb "github.com/foresthoffman/rgblog"
redis "github.com/gomodule/redigo/redis"
uuid "github.com/google/uuid"
)
// ConfigStruct is the base for the config file
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"`
DatabaseSVC string `json:"databasesvc,omitempty"`
}
// CommandType Kinda an enum
type CommandType string
// CommandType literals
const (
RandomSinger CommandType = "RandomSinger"
RandomPrompt CommandType = "RandomPrompt"
RandomSong CommandType = "RandomSong"
AgingSinger CommandType = "AgingSinger"
AgingSong CommandType = "AgingSong"
)
// IsValid Is CommandType a valid enum?
func (ct CommandType) IsValid() error {
switch ct {
case RandomSinger, RandomPrompt, RandomSong, AgingSinger, AgingSong:
return nil
}
return errors.New("Invalid command type")
}
// ChannelData is what we store in the BitRaft (Redis) database
type ChannelData struct {
ControlChannel bool
Name string `json:"name"`
AdminKey string `json:"value,omitempty"`
Commands []CommandStruct `json:"commands,omitempty"`
ExtraStrings string `json:"extrastrings,omitempty"`
JoinTime time.Time `json:"jointime"`
HasLeft bool `json:"hasleft"`
VideoCache []SingsVideoStruct `json:"videoCache"`
VideoCacheUpdated time.Time `json:"videoCacheUpdated"`
Bearer string `json:"bearer"`
TwitchUserID string `json:"twitchUserID"`
}
// SingsVideoStruct The data we pull from Twitch
type SingsVideoStruct struct {
Date time.Time `json:"date"` // Golang date of creation
FullTitle string `json:"fullTitle"` // Full Title
Duet bool `json:"duet"` // Is it a duet?
OtherSinger string `json:"otherSinger"` // Twitch NAME of the other singer, extracted from the title
SongTitle string `json:"songTitle"` // extracted from title
LastSungSong time.Time `json:"LastSungSong"` // Last time this SONG was sung
LastSungSinger time.Time `json:"LastSungSinger"` // Last time a duet was sung with this SINGER, regardless of song, only Duets have this date initialised
VideoURL string `json:"VideoURL"` // RIP Twitch Sings
}
// CommandStruct keypair for irc command -> actual thing to do
type CommandStruct struct {
CommandName CommandType `json:"commandName"`
KeyWord string `json:"keyword"`
}
// GlobalData Some kind of architect would kill me for this
type GlobalData struct {
ChannelData map[string]ChannelData
Config ConfigStruct
Database redis.Conn
ControlChannel string
}
// ConnectDatabase Connects to the database set in the config struct
func (gd *GlobalData) ConnectDatabase() {
var err error
rgb.YPrintf("[%s] Connecting to \"redis\" %s...\n", TimeStamp(), gd.Config.DatabaseSVC, err)
gd.Database, err = redis.Dial("tcp", gd.Config.DatabaseSVC+":4920")
if err != nil {
rgb.RPrintf("[%s] Failed connecting to \"redis\" %s : %s\n", TimeStamp(), gd.Config.DatabaseSVC, err)
os.Exit(1)
}
rgb.YPrintf("[%s] No error... wtf?\n", TimeStamp())
}
// UpdateVideoCache Updates the in-memory data and updates redis
func (gd *GlobalData) UpdateVideoCache(user string, videos []SingsVideoStruct) {
record := gd.ChannelData[user]
rgb.YPrintf("Replacing cache of %d performances with a new cache of %d performances\n", len(record.VideoCache), len(videos))
record.VideoCache = videos
record.VideoCacheUpdated = time.Now()
asJson, _ := json.Marshal(record)
_, err := gd.Database.Do("SET", user, asJson)
if err != nil {
log.Fatal(err)
}
gd.ChannelData[user] = record
}
func (gd *GlobalData) UpdateBearerToken(user string, token string) {
record := gd.ChannelData[user]
record.Bearer = token
asJson, _ := json.Marshal(record)
gd.Database.Do("SET", user, asJson)
gd.ChannelData[user] = record
}
func (gd *GlobalData) UpdateTwitchUserID(user string, userid string) {
record := gd.ChannelData[user]
record.TwitchUserID = userid
asJson, _ := json.Marshal(record)
gd.Database.Do("SET", user, asJson)
gd.ChannelData[user] = record
}
func (gd *GlobalData) UpdateChannelKey(user string, channelKey string) {
record := gd.ChannelData[user]
record.AdminKey = channelKey
asJson, _ := json.Marshal(record)
gd.Database.Do("SET", user, asJson)
gd.ChannelData[user] = record
}
func (gd *GlobalData) UpdateChannelName(user string, newName string) {
record := gd.ChannelData[user]
record.Name = newName
asJson, _ := json.Marshal(record)
gd.Database.Do("SET", newName, asJson)
gd.ChannelData[newName] = record
//dunno why we'd need this but I guess in case?
if newName != user {
delete(gd.ChannelData, user)
gd.Database.Do("DEL", newName)
}
}
func (gd *GlobalData) UpdateJoined(user string, invert bool) {
record := gd.ChannelData[user]
if record.Name == "" {
record = ChannelData{Name: user, JoinTime: time.Now(), Commands: nil, ControlChannel: true}
}
record.JoinTime = time.Now()
asJson, _ := json.Marshal(record)
if invert {
record.HasLeft = true
} else {
record.HasLeft = false
}
gd.Database.Do("SET", user, asJson)
gd.ChannelData[user] = record
}
func (gd *GlobalData) ReadChannelData() error {
keys, err := redis.Strings(gd.Database.Do("KEYS", "*"))
if err != nil {
rgb.RPrintf("[%s] ERROR with redis fetch : %s\n", TimeStamp(), err.Error())
rgb.YPrintf("[%s] Maybe an empty redis, creating a record...\n", TimeStamp())
keys = []string{}
}
if len(keys) == 0 {
rgb.YPrintf("[%s] Looks like an empty redis, creating a record...\n", TimeStamp())
record := ChannelData{Name: gd.ControlChannel, JoinTime: time.Now(), Commands: nil}
asJSON, _ := json.Marshal(record)
gd.Database.Do("SET", gd.ControlChannel, asJSON)
keys = []string{gd.ControlChannel}
}
rgb.YPrintf("[%s] \"redis\" has %d records!\n", TimeStamp(), len(keys))
for _, channel := range keys {
fetchedData, err := redis.String(gd.Database.Do("GET", channel))
if err != nil {
rgb.YPrintf("[%s] failed to read key %s from redis, good luck!...\n", TimeStamp(), channel)
}
rgb.YPrintf("[%s] data from \"redis\" for %s is %s\n", TimeStamp(), channel, fetchedData)
cd := gd.ChannelData
if cd == nil {
cd = make(map[string]ChannelData)
}
d := &ChannelData{}
err = json.Unmarshal([]byte(fetchedData), d)
if err != nil {
rgb.RPrintf("[%s] channel data could not be unmarshalled : %s\n", TimeStamp(), err.Error())
}
cd[channel] = *d
gd.ChannelData = cd
rgb.YPrintf("[%s] channel data : %v\n", TimeStamp(), gd.ChannelData)
}
// Managed to leave the main channel!?
rgb.YPrintf("[%s] Read channel data for %d channels\n", TimeStamp(), len(gd.ChannelData))
return nil
}
func (gd *GlobalData) ReadOrCreateChannelKey(channel string) string {
record := gd.ChannelData[channel]
magicCode := ""
if 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()))
gd.UpdateJoined(channel, true)
gd.UpdateChannelKey(channel, magicCode)
gd.UpdateChannelName(channel, channel)
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
}
const UTCFormat = "Jan 2 15:04:05 UTC"
func TimeStamp() string {
return TimeStampFmt(UTCFormat)
}
func TimeStampFmt(format string) string {
return time.Now().Format(format)
}

View File

@ -2,6 +2,7 @@ package irc
import ( import (
"bufio" "bufio"
"encoding/base64"
"encoding/json" "encoding/json"
"errors" "errors"
"fmt" "fmt"
@ -14,8 +15,9 @@ import (
"strings" "strings"
"time" "time"
data "git.martyn.berlin/martyn/twitchsingstools/internal/data"
rgb "github.com/foresthoffman/rgblog" rgb "github.com/foresthoffman/rgblog"
uuid "github.com/google/uuid"
scribble "github.com/nanobox-io/golang-scribble"
) )
const UTCFormat = "Jan 2 15:04:05 UTC" const UTCFormat = "Jan 2 15:04:05 UTC"
@ -56,6 +58,15 @@ type AppOAuthCred struct {
ClientSecret string `json:"client_secret,omitempty"` ClientSecret string `json:"client_secret,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
@ -69,7 +80,57 @@ type KardBot struct {
Server string Server string
startTime time.Time startTime time.Time
Prompts []string Prompts []string
GlobalData data.GlobalData Database scribble.Driver
ChannelData map[string]ChannelData
Config ConfigStruct
}
type SingsVideoStruct struct {
Date time.Time `json:"date"` // Golang date of creation
FullTitle string `json:"fullTitle"` // Full Title
Duet bool `json:"duet"` // Is it a duet?
OtherSinger string `json:"otherSinger"` // Twitch NAME of the other singer, extracted from the title
SongTitle string `json:"songTitle"` // extracted from title
LastSungSong time.Time `json:"LastSungSong"` // Last time this SONG was sung
LastSungSinger time.Time `json:"LastSungSinger"` // Last time a duet was sung with this SINGER, regardless of song, only Duets have this date initialised
}
// CommandType Kinda an enum
type CommandType string
const (
RandomSinger CommandType = "RandomSinger"
RandomPrompt CommandType = "RandomPrompt"
RandomSong CommandType = "RandomSong"
AgingSinger CommandType = "AgingSinger"
AgingSong CommandType = "AgingSong"
)
func (ct CommandType) IsValid() error {
switch ct {
case RandomSinger, RandomPrompt, RandomSong, AgingSinger, AgingSong:
return nil
}
return errors.New("Invalid command type")
}
type CommandStruct struct {
CommandName CommandType `json:"commandName"`
KeyWord string `json:"keyword"`
}
type ChannelData struct {
ControlChannel bool
Name string `json:"name"`
AdminKey string `json:"value,omitempty"`
Commands []CommandStruct `json:"commands,omitempty"`
ExtraStrings string `json:"extrastrings,omitempty"`
JoinTime time.Time `json:"jointime"`
HasLeft bool `json:"hasleft"`
VideoCache []SingsVideoStruct `json:"videoCache"`
VideoCacheUpdated time.Time `json:"videoCacheUpdated"`
Bearer string `json:"bearer"`
TwitchUserID string `json:"twitchUserID"`
} }
// 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
@ -99,7 +160,7 @@ func (bb *KardBot) Disconnect() {
// Look at the channels I'm actually in // Look at the channels I'm actually in
func (bb *KardBot) ActiveChannels() int { func (bb *KardBot) ActiveChannels() int {
count := 0 count := 0
for _, channel := range bb.GlobalData.ChannelData { for _, channel := range bb.ChannelData {
if !channel.HasLeft { if !channel.HasLeft {
count = count + 1 count = count + 1
} }
@ -107,6 +168,29 @@ func (bb *KardBot) ActiveChannels() int {
return count return count
} }
func (bb *KardBot) UpdateVideoCache(user string, videos []SingsVideoStruct) {
record := bb.ChannelData[user]
fmt.Printf("Replacing cache of %d performances with a new cache of %d performances", len(record.VideoCache), len(videos))
record.VideoCache = videos
record.VideoCacheUpdated = time.Now()
bb.Database.Write("channelData", user, record)
bb.ChannelData[user] = record
}
func (bb *KardBot) UpdateBearerToken(user string, token string) {
record := bb.ChannelData[user]
record.Bearer = token
bb.Database.Write("channelData", user, record)
bb.ChannelData[user] = record
}
func (bb *KardBot) UpdateTwitchUserID(user string, userid string) {
record := bb.ChannelData[user]
record.TwitchUserID = userid
bb.Database.Write("channelData", user, record)
bb.ChannelData[user] = record
}
// 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 {
@ -138,7 +222,11 @@ func (bb *KardBot) HandleChat() error {
matches := ConnectRegex.FindStringSubmatch(line) matches := ConnectRegex.FindStringSubmatch(line)
if nil != matches { if nil != matches {
realUserName := matches[1] realUserName := matches[1]
bb.GlobalData.UpdateJoined(realUserName, false) if bb.ChannelData[realUserName].Name == "" {
record := ChannelData{Name: realUserName, JoinTime: time.Now(), Commands: nil, ControlChannel: true}
bb.Database.Write("channelData", realUserName, record)
bb.ChannelData[realUserName] = record
}
bb.JoinChannel(realUserName) bb.JoinChannel(realUserName)
} }
@ -170,13 +258,13 @@ func (bb *KardBot) HandleChat() error {
if nil != cmdMatches { if nil != cmdMatches {
cmd := cmdMatches[1] cmd := cmdMatches[1]
cardCommand := "" cardCommand := ""
commands := bb.GlobalData.ChannelData[channel].Commands commands := bb.ChannelData[channel].Commands
for _, command := range commands { for _, command := range commands {
if command.CommandName == data.RandomPrompt { if command.CommandName == RandomPrompt {
cardCommand = command.KeyWord cardCommand = command.KeyWord
} }
} }
rgb.YPrintf("[%s] Checking cmd %s against [%s]\n", TimeStamp(), cmd, bb.GlobalData.ChannelData[channel].Commands) rgb.YPrintf("[%s] Checking cmd %s against [%s]\n", TimeStamp(), cmd, bb.ChannelData[channel].Commands)
switch cmd { switch cmd {
case cardCommand: case cardCommand:
if cardCommand != "" { if cardCommand != "" {
@ -184,9 +272,13 @@ func (bb *KardBot) HandleChat() error {
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": case "join":
if bb.GlobalData.ChannelData[channel].ControlChannel { if bb.ChannelData[channel].ControlChannel {
rgb.CPrintf("[%s] Join asked for by %s on %s' channel!\n", TimeStamp(), userName, channel) rgb.CPrintf("[%s] Join asked for by %s on %s' channel!\n", TimeStamp(), userName, channel)
bb.GlobalData.UpdateJoined(userName, false) if bb.ChannelData[userName].Name == "" {
record := ChannelData{Name: userName, JoinTime: time.Now(), Commands: nil, ControlChannel: true}
bb.Database.Write("channelData", userName, record)
bb.ChannelData[userName] = record
}
bb.JoinChannel(userName) bb.JoinChannel(userName)
} }
} }
@ -203,7 +295,7 @@ func (bb *KardBot) HandleChat() error {
bb.Disconnect() bb.Disconnect()
return nil return nil
case "kcardadmin": case "kcardadmin":
magicCode := bb.GlobalData.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(),
@ -363,11 +455,18 @@ func (bb *KardBot) Start() {
return return
} }
err = bb.readChannelData()
if nil != err {
fmt.Println(err)
fmt.Println("Aborting!")
return
}
for { for {
bb.Connect() bb.Connect()
bb.Login() bb.Login()
if len(bb.GlobalData.ChannelData) > 0 { if len(bb.ChannelData) > 0 {
for channelName, channelData := range bb.GlobalData.ChannelData { for channelName, channelData := range bb.ChannelData {
if !channelData.HasLeft { if !channelData.HasLeft {
bb.JoinChannel(channelName) bb.JoinChannel(channelName)
} }
@ -388,6 +487,74 @@ 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(), Commands: nil}
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
}
}
// 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(), Commands: nil}
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(UTCFormat) return TimeStampFmt(UTCFormat)
} }

View File

@ -9,7 +9,6 @@ import (
"sync" "sync"
"unicode" "unicode"
data "git.martyn.berlin/martyn/twitchsingstools/internal/data"
irc "git.martyn.berlin/martyn/twitchsingstools/internal/irc" irc "git.martyn.berlin/martyn/twitchsingstools/internal/irc"
"github.com/dustin/go-humanize" "github.com/dustin/go-humanize"
"github.com/gorilla/handlers" "github.com/gorilla/handlers"
@ -78,7 +77,6 @@ type videosResponse struct {
} }
var ircBot *irc.KardBot var ircBot *irc.KardBot
var globalData *data.GlobalData
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")
@ -132,7 +130,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://" + globalData.Config.ExternalURL} 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)
@ -155,7 +153,6 @@ func humanTimeFromTimeString(s string) string {
type AugmentedSingsVideoStruct struct { type AugmentedSingsVideoStruct struct {
Date time.Time Date time.Time
NiceDate string NiceDate string
ShortDate string
FullTitle string FullTitle string
Duet bool Duet bool
OtherSinger string OtherSinger string
@ -164,15 +161,12 @@ type AugmentedSingsVideoStruct struct {
NiceLastSungSong string NiceLastSungSong string
LastSungSinger time.Time LastSungSinger time.Time
NiceLastSungSinger string NiceLastSungSinger string
VideoURL string
VideoNumber string //yes, I don't care any more.
} }
func AugmentSingsVideoStructForCSV(input data.SingsVideoStruct) AugmentedSingsVideoStruct { func AugmentSingsVideoStructForCSV(input irc.SingsVideoStruct) AugmentedSingsVideoStruct {
var ret AugmentedSingsVideoStruct var ret AugmentedSingsVideoStruct
ret.Date = input.Date ret.Date = input.Date
ret.NiceDate = input.Date.Format("2006-01-02 15:04:05") ret.NiceDate = input.Date.Format("2006-01-02 15:04:05")
ret.ShortDate = input.Date.Format("2006-01-02")
ret.FullTitle = input.FullTitle ret.FullTitle = input.FullTitle
ret.Duet = input.Duet ret.Duet = input.Duet
ret.OtherSinger = input.OtherSinger ret.OtherSinger = input.OtherSinger
@ -180,9 +174,6 @@ func AugmentSingsVideoStructForCSV(input data.SingsVideoStruct) AugmentedSingsVi
ret.LastSungSong = input.LastSungSong ret.LastSungSong = input.LastSungSong
ret.NiceLastSungSong = input.LastSungSong.Format("2006-01-02 15:04:05") ret.NiceLastSungSong = input.LastSungSong.Format("2006-01-02 15:04:05")
ret.LastSungSinger = input.LastSungSinger ret.LastSungSinger = input.LastSungSinger
ret.VideoURL = input.VideoURL
urlParts := strings.Split(input.VideoURL, "/")
ret.VideoNumber = urlParts[len(urlParts)-1]
if !ret.Duet { if !ret.Duet {
ret.NiceLastSungSinger = "Solo performance" ret.NiceLastSungSinger = "Solo performance"
} else { } else {
@ -191,7 +182,7 @@ func AugmentSingsVideoStructForCSV(input data.SingsVideoStruct) AugmentedSingsVi
return ret return ret
} }
func AugmentSingsVideoStructSliceForCSV(input []data.SingsVideoStruct) []AugmentedSingsVideoStruct { func AugmentSingsVideoStructSliceForCSV(input []irc.SingsVideoStruct) []AugmentedSingsVideoStruct {
ret := make([]AugmentedSingsVideoStruct, 0) ret := make([]AugmentedSingsVideoStruct, 0)
for _, record := range input { for _, record := range input {
ret = append(ret, AugmentSingsVideoStructForCSV(record)) ret = append(ret, AugmentSingsVideoStructForCSV(record))
@ -199,7 +190,7 @@ func AugmentSingsVideoStructSliceForCSV(input []data.SingsVideoStruct) []Augment
return ret return ret
} }
func AugmentSingsVideoStruct(input data.SingsVideoStruct) AugmentedSingsVideoStruct { func AugmentSingsVideoStruct(input irc.SingsVideoStruct) AugmentedSingsVideoStruct {
var ret AugmentedSingsVideoStruct var ret AugmentedSingsVideoStruct
ret.Date = input.Date ret.Date = input.Date
ret.NiceDate = humanize.Time(input.Date) ret.NiceDate = humanize.Time(input.Date)
@ -218,7 +209,7 @@ func AugmentSingsVideoStruct(input data.SingsVideoStruct) AugmentedSingsVideoStr
return ret return ret
} }
func AugmentSingsVideoStructSlice(input []data.SingsVideoStruct) []AugmentedSingsVideoStruct { func AugmentSingsVideoStructSlice(input []irc.SingsVideoStruct) []AugmentedSingsVideoStruct {
ret := make([]AugmentedSingsVideoStruct, 0) ret := make([]AugmentedSingsVideoStruct, 0)
for _, record := range input { for _, record := range input {
ret = append(ret, AugmentSingsVideoStruct(record)) ret = append(ret, AugmentSingsVideoStruct(record))
@ -275,8 +266,8 @@ func ValidateTwitchBearerToken(bearer string) (bool, error) {
return resp.StatusCode == 200, nil return resp.StatusCode == 200, nil
} }
func twitchVidToSingsVid(twitchFormat videoStruct) (data.SingsVideoStruct, error) { func twitchVidToSingsVid(twitchFormat videoStruct) (irc.SingsVideoStruct, error) {
var ret data.SingsVideoStruct var ret irc.SingsVideoStruct
layout := "2006-01-02T15:04:05Z" layout := "2006-01-02T15:04:05Z"
var d time.Time var d time.Time
d, err := time.Parse(layout, twitchFormat.CreatedAt) d, err := time.Parse(layout, twitchFormat.CreatedAt)
@ -284,7 +275,6 @@ func twitchVidToSingsVid(twitchFormat videoStruct) (data.SingsVideoStruct, error
return ret, err return ret, err
} }
ret.Date = d ret.Date = d
ret.VideoURL = twitchFormat.URL
var DuetRegex = regexp.MustCompile(`^Duet with ([^ ]*): (.*)$`) var DuetRegex = regexp.MustCompile(`^Duet with ([^ ]*): (.*)$`)
matches := DuetRegex.FindAllStringSubmatch(twitchFormat.Title, -1) matches := DuetRegex.FindAllStringSubmatch(twitchFormat.Title, -1)
@ -317,7 +307,7 @@ type SingerSings struct {
Sings int Sings int
} }
func calculateTopNSongs(songCache []data.SingsVideoStruct, howMany int) []SongSings { func calculateTopNSongs(songCache []irc.SingsVideoStruct, howMany int) []SongSings {
songMap := map[string]int{} songMap := map[string]int{}
for _, record := range songCache { for _, record := range songCache {
sings := songMap[record.SongTitle] sings := songMap[record.SongTitle]
@ -348,7 +338,7 @@ func IsLower(s string) bool {
return true return true
} }
func unmangleSingerName(MangledCaseName string, songCache []data.SingsVideoStruct) string { func unmangleSingerName(MangledCaseName string, songCache []irc.SingsVideoStruct) string {
options := make(map[string]string, 0) options := make(map[string]string, 0)
for _, record := range songCache { for _, record := range songCache {
if strings.ToUpper(MangledCaseName) == strings.ToUpper(record.OtherSinger) { if strings.ToUpper(MangledCaseName) == strings.ToUpper(record.OtherSinger) {
@ -423,7 +413,7 @@ type CacheDetails struct {
func getCacheDetails(channel string) CacheDetails { func getCacheDetails(channel string) CacheDetails {
var ret CacheDetails var ret CacheDetails
channelData := globalData.ChannelData[channel] channelData := ircBot.ChannelData[channel]
ret.Age = time.Now().Sub(channelData.VideoCacheUpdated) ret.Age = time.Now().Sub(channelData.VideoCacheUpdated)
ret.AgeStr = humanize.Time(channelData.VideoCacheUpdated) ret.AgeStr = humanize.Time(channelData.VideoCacheUpdated)
ret.SongCount = len(channelData.VideoCache) ret.SongCount = len(channelData.VideoCache)
@ -432,36 +422,36 @@ func getCacheDetails(channel string) CacheDetails {
func forceUpdateCache(channel string) { func forceUpdateCache(channel string) {
fmt.Printf("Forcing cache update!") fmt.Printf("Forcing cache update!")
channelData := globalData.ChannelData[channel] channelData := ircBot.ChannelData[channel]
tenHours := time.Hour * -10 tenHours := time.Hour * -10
videoCacheUpdated := time.Now().Add(tenHours) // Subtract 10 hours from now, cache is 10 hours old. videoCacheUpdated := time.Now().Add(tenHours) // Subtract 10 hours from now, cache is 10 hours old.
channelData.VideoCacheUpdated = videoCacheUpdated channelData.VideoCacheUpdated = videoCacheUpdated
globalData.ChannelData[channel] = channelData ircBot.ChannelData[channel] = channelData
updateCacheIfNecessary(channel) updateCacheIfNecessary(channel)
} }
func updateCacheIfNecessary(channel string) { func updateCacheIfNecessary(channel string) {
cacheLock.Lock() cacheLock.Lock()
channelData := globalData.ChannelData[channel] channelData := ircBot.ChannelData[channel]
if time.Now().Sub(channelData.VideoCacheUpdated).Hours() > 1 { if time.Now().Sub(channelData.VideoCacheUpdated).Hours() > 1 {
fmt.Printf("Cache of %d performances is older than an hour - %.1f hours old to be precise... fetching.\n", len(channelData.VideoCache), time.Now().Sub(globalData.ChannelData[channel].VideoCacheUpdated).Hours()) fmt.Printf("Cache of %d performances is older than an hour - %.1f hours old to be precise... fetching.\n", len(channelData.VideoCache), time.Now().Sub(ircBot.ChannelData[channel].VideoCacheUpdated).Hours())
vids, err := fetchAllVoDs(channelData.TwitchUserID, channelData.Bearer) vids, err := fetchAllVoDs(channelData.TwitchUserID, channelData.Bearer)
if err != nil { if err != nil {
errCache := make([]data.SingsVideoStruct, 0) errCache := make([]irc.SingsVideoStruct, 0)
var ret data.SingsVideoStruct var ret irc.SingsVideoStruct
ret.FullTitle = "Error fetching videos: " + err.Error() ret.FullTitle = "Error fetching videos: " + err.Error()
errCache = append(errCache, ret) errCache = append(errCache, ret)
vids = errCache vids = errCache
} }
updateCalculatedFields(vids) updateCalculatedFields(vids)
globalData.UpdateVideoCache(channel, vids) ircBot.UpdateVideoCache(channel, vids)
} else { } else {
fmt.Printf("Cache of %d performances is younger than an hour - %.1f hours old to be precise... not fetching.\n", len(channelData.VideoCache), time.Now().Sub(globalData.ChannelData[channel].VideoCacheUpdated).Hours()) fmt.Printf("Cache of %d performances is younger than an hour - %.1f hours old to be precise... not fetching.\n", len(channelData.VideoCache), time.Now().Sub(ircBot.ChannelData[channel].VideoCacheUpdated).Hours())
} }
cacheLock.Unlock() cacheLock.Unlock()
} }
func calculateTopNSingers(songCache []data.SingsVideoStruct, howMany int) []SingerSings { func calculateTopNSingers(songCache []irc.SingsVideoStruct, howMany int) []SingerSings {
songMap := map[string]int{} songMap := map[string]int{}
songCount := 0 songCount := 0
for _, record := range songCache { for _, record := range songCache {
@ -485,7 +475,7 @@ func calculateTopNSingers(songCache []data.SingsVideoStruct, howMany int) []Sing
return slice return slice
} }
func calculateLastSungSongDate(songCache []data.SingsVideoStruct, SongTitle string) time.Time { func calculateLastSungSongDate(songCache []irc.SingsVideoStruct, SongTitle string) time.Time {
var t time.Time var t time.Time
for _, record := range songCache { for _, record := range songCache {
if record.SongTitle == SongTitle { if record.SongTitle == SongTitle {
@ -497,7 +487,7 @@ func calculateLastSungSongDate(songCache []data.SingsVideoStruct, SongTitle stri
return t return t
} }
func calculateLastSungSingerDate(songCache []data.SingsVideoStruct, Singer string) time.Time { func calculateLastSungSingerDate(songCache []irc.SingsVideoStruct, Singer string) time.Time {
var t time.Time var t time.Time
for _, record := range songCache { for _, record := range songCache {
if strings.ToUpper(record.OtherSinger) == strings.ToUpper(Singer) { if strings.ToUpper(record.OtherSinger) == strings.ToUpper(Singer) {
@ -512,7 +502,7 @@ func calculateLastSungSingerDate(songCache []data.SingsVideoStruct, Singer strin
return t return t
} }
func updateCalculatedFields(songCache []data.SingsVideoStruct) { func updateCalculatedFields(songCache []irc.SingsVideoStruct) {
for i, record := range songCache { for i, record := range songCache {
if record.Duet { if record.Duet {
songCache[i].LastSungSinger = calculateLastSungSingerDate(songCache, record.OtherSinger) songCache[i].LastSungSinger = calculateLastSungSingerDate(songCache, record.OtherSinger)
@ -521,7 +511,7 @@ func updateCalculatedFields(songCache []data.SingsVideoStruct) {
} }
} }
func fetchVoDsPagesRecursive(userID string, bearer string, from string) ([]data.SingsVideoStruct, error) { func fetchVoDsPagesRecursive(userID string, bearer string, from string) ([]irc.SingsVideoStruct, error) {
url := "" url := ""
if from == "" { if from == "" {
url = "videos?user_id=" + userID + "&first=100&type=upload" url = "videos?user_id=" + userID + "&first=100&type=upload"
@ -539,7 +529,7 @@ func fetchVoDsPagesRecursive(userID string, bearer string, from string) ([]data.
if err != nil { if err != nil {
return nil, err return nil, err
} }
titles := make([]data.SingsVideoStruct, 0) titles := make([]irc.SingsVideoStruct, 0)
for _, videoData := range fullResponse.Data { for _, videoData := range fullResponse.Data {
ret, err := twitchVidToSingsVid(videoData) ret, err := twitchVidToSingsVid(videoData)
if err != nil { if err != nil {
@ -559,7 +549,7 @@ func fetchVoDsPagesRecursive(userID string, bearer string, from string) ([]data.
return titles, nil return titles, nil
} }
func fetchAllVoDs(userID string, bearer string) ([]data.SingsVideoStruct, error) { func fetchAllVoDs(userID string, bearer string) ([]irc.SingsVideoStruct, error) {
tokenValid, err := ValidateTwitchBearerToken(bearer) tokenValid, err := ValidateTwitchBearerToken(bearer)
if err != nil { if err != nil {
fmt.Println("Error validating token : " + err.Error()) fmt.Println("Error validating token : " + err.Error())
@ -571,7 +561,7 @@ func fetchAllVoDs(userID string, bearer string) ([]data.SingsVideoStruct, error)
} }
titles, err := fetchVoDsPagesRecursive(userID, bearer, "") titles, err := fetchVoDsPagesRecursive(userID, bearer, "")
if err != nil { if err != nil {
return make([]data.SingsVideoStruct, 0), err return make([]irc.SingsVideoStruct, 0), err
} }
return titles, nil return titles, nil
} }
@ -588,7 +578,7 @@ func TwitchAdminHandler(response http.ResponseWriter, request *http.Request) {
"client_secret": {ircBot.AppCredentials.ClientSecret}, "client_secret": {ircBot.AppCredentials.ClientSecret},
"code": {vars["code"]}, "code": {vars["code"]},
"grant_type": {"authorization_code"}, "grant_type": {"authorization_code"},
"redirect_uri": {"https://" + globalData.Config.ExternalURL + "/twitchadmin"}}) "redirect_uri": {"https://" + ircBot.Config.ExternalUrl + "/twitchadmin"}})
if err != nil { if err != nil {
response.WriteHeader(500) response.WriteHeader(500)
response.Header().Add("Content-type", "text/plain") response.Header().Add("Content-type", "text/plain")
@ -639,10 +629,10 @@ func TwitchAdminHandler(response http.ResponseWriter, request *http.Request) {
} }
user := usersObject.Data[0] user := usersObject.Data[0]
magicCode := globalData.ReadOrCreateChannelKey(user.Login) magicCode := ircBot.ReadOrCreateChannelKey(user.Login)
globalData.UpdateBearerToken(user.Login, oauthResponse.Access_token) ircBot.UpdateBearerToken(user.Login, oauthResponse.Access_token)
globalData.UpdateTwitchUserID(user.Login, user.Id) ircBot.UpdateTwitchUserID(user.Login, user.Id)
url := "https://" + globalData.Config.ExternalURL + "/admin/" + user.Login + "/" + magicCode url := "https://" + ircBot.Config.ExternalUrl + "/admin/" + user.Login + "/" + magicCode
http.Redirect(response, request, url, http.StatusFound) http.Redirect(response, request, url, http.StatusFound)
} else { } else {
@ -674,7 +664,7 @@ func TwitchBackendHandler(response http.ResponseWriter, request *http.Request) {
// ircBot.AppCredentials.Password // ircBot.AppCredentials.Password
// vars["oauthtoken"] // vars["oauthtoken"]
// authorization_code // authorization_code
// "https://"+globalData.Config.ExternalURL+/twitchadmin // "https://"+ircBot.Config.ExternalUrl+/twitchadmin
fmt.Println("Asking twitch for more...") fmt.Println("Asking twitch for more...")
resp, err := http.PostForm( resp, err := http.PostForm(
"https://id.twitch.tv/oauth2/token", "https://id.twitch.tv/oauth2/token",
@ -683,7 +673,7 @@ func TwitchBackendHandler(response http.ResponseWriter, request *http.Request) {
"client_secret": {ircBot.AppCredentials.ClientSecret}, "client_secret": {ircBot.AppCredentials.ClientSecret},
"code": {vars["code"]}, "code": {vars["code"]},
"grant_type": {"authorization_code"}, "grant_type": {"authorization_code"},
"redirect_uri": {"https://" + globalData.Config.ExternalURL + "/twitchadmin"}}) "redirect_uri": {"https://" + ircBot.Config.ExternalUrl + "/twitchadmin"}})
if err != nil { if err != nil {
response.Header().Add("Content-type", "text/plain") response.Header().Add("Content-type", "text/plain")
fmt.Fprint(response, "ERROR: "+err.Error()) fmt.Fprint(response, "ERROR: "+err.Error())
@ -703,13 +693,13 @@ func TwitchBackendHandler(response http.ResponseWriter, request *http.Request) {
func CSVHandler(response http.ResponseWriter, request *http.Request) { func CSVHandler(response http.ResponseWriter, request *http.Request) {
vars := mux.Vars(request) vars := mux.Vars(request)
if vars["key"] != globalData.ChannelData[vars["channel"]].AdminKey { if vars["key"] != ircBot.ChannelData[vars["channel"]].AdminKey {
UnauthorizedHandler(response, request) UnauthorizedHandler(response, request)
return return
} }
type TemplateData struct { type TemplateData struct {
Channel string Channel string
Commands []data.CommandStruct Commands []irc.CommandStruct
ExtraStrings string ExtraStrings string
SinceTime time.Time SinceTime time.Time
SinceTimeUTC string SinceTimeUTC string
@ -720,7 +710,7 @@ func CSVHandler(response http.ResponseWriter, request *http.Request) {
TopNSingers []SingerSings TopNSingers []SingerSings
} }
updateCacheIfNecessary(vars["channel"]) updateCacheIfNecessary(vars["channel"])
channelData := globalData.ChannelData[vars["channel"]] channelData := ircBot.ChannelData[vars["channel"]]
topNSongs := calculateTopNSongs(channelData.VideoCache, 10) topNSongs := calculateTopNSongs(channelData.VideoCache, 10)
topNSingers := calculateTopNSingers(channelData.VideoCache, 10) topNSingers := calculateTopNSingers(channelData.VideoCache, 10)
var td = TemplateData{channelData.Name, channelData.Commands, channelData.ExtraStrings, channelData.JoinTime, channelData.JoinTime.Format(irc.UTCFormat), false, channelData.HasLeft, AugmentSingsVideoStructSliceForCSV(channelData.VideoCache), topNSongs, topNSingers} var td = TemplateData{channelData.Name, channelData.Commands, channelData.ExtraStrings, channelData.JoinTime, channelData.JoinTime.Format(irc.UTCFormat), false, channelData.HasLeft, AugmentSingsVideoStructSliceForCSV(channelData.VideoCache), topNSongs, topNSingers}
@ -739,14 +729,13 @@ func CSVHandler(response http.ResponseWriter, request *http.Request) {
func JSONHandler(response http.ResponseWriter, request *http.Request) { func JSONHandler(response http.ResponseWriter, request *http.Request) {
vars := mux.Vars(request) vars := mux.Vars(request)
if vars["key"] != globalData.ChannelData[vars["channel"]].AdminKey { if vars["key"] != ircBot.ChannelData[vars["channel"]].AdminKey {
fmt.Printf("%s != %s\n", vars["key"], globalData.ChannelData[vars["channel"]].AdminKey)
UnauthorizedHandler(response, request) UnauthorizedHandler(response, request)
return return
} }
type TemplateData struct { type TemplateData struct {
Channel string Channel string
Commands []data.CommandStruct Commands []irc.CommandStruct
ExtraStrings string ExtraStrings string
SinceTime time.Time SinceTime time.Time
SinceTimeUTC string SinceTimeUTC string
@ -757,7 +746,7 @@ func JSONHandler(response http.ResponseWriter, request *http.Request) {
TopNSingers []SingerSings TopNSingers []SingerSings
} }
updateCacheIfNecessary(vars["channel"]) updateCacheIfNecessary(vars["channel"])
channelData := globalData.ChannelData[vars["channel"]] channelData := ircBot.ChannelData[vars["channel"]]
var topNSongs []SongSings var topNSongs []SongSings
var topNSingers []SingerSings var topNSingers []SingerSings
@ -780,45 +769,9 @@ func JSONHandler(response http.ResponseWriter, request *http.Request) {
} }
} }
func ScriptHandler(response http.ResponseWriter, request *http.Request) {
vars := mux.Vars(request)
if vars["key"] != globalData.ChannelData[vars["channel"]].AdminKey {
UnauthorizedHandler(response, request)
return
}
type TemplateData struct {
Channel string
Commands []data.CommandStruct
ExtraStrings string
SinceTime time.Time
SinceTimeUTC string
Leaving bool
HasLeft bool
SongData []AugmentedSingsVideoStruct
TopNSongs []SongSings
TopNSingers []SingerSings
}
updateCacheIfNecessary(vars["channel"])
channelData := globalData.ChannelData[vars["channel"]]
topNSongs := calculateTopNSongs(channelData.VideoCache, 10)
topNSingers := calculateTopNSingers(channelData.VideoCache, 10)
var td = TemplateData{channelData.Name, channelData.Commands, channelData.ExtraStrings, channelData.JoinTime, channelData.JoinTime.Format(irc.UTCFormat), false, channelData.HasLeft, AugmentSingsVideoStructSliceForCSV(channelData.VideoCache), topNSongs, topNSingers}
if request.URL.Path[0:11] == "/script.bat" {
response.Header().Add("Content-Disposition", "attachment; filename=\"script.bat\"")
response.Header().Add("Content-type", "application/x-bat")
tmpl := template.Must(template.ParseFiles("web/script.bat"))
tmpl.Execute(response, td)
} else {
response.Header().Add("Content-Disposition", "attachment; filename=\"script.sh\"")
response.Header().Add("Content-type", "text/x-shellscript")
tmpl := template.Must(template.ParseFiles("web/script.sh"))
tmpl.Execute(response, td)
}
}
func CacheDetailsHandler(response http.ResponseWriter, request *http.Request) { func CacheDetailsHandler(response http.ResponseWriter, request *http.Request) {
vars := mux.Vars(request) vars := mux.Vars(request)
if vars["key"] != globalData.ChannelData[vars["channel"]].AdminKey { if vars["key"] != ircBot.ChannelData[vars["channel"]].AdminKey {
UnauthorizedHandler(response, request) UnauthorizedHandler(response, request)
return return
} }
@ -830,23 +783,23 @@ func CacheDetailsHandler(response http.ResponseWriter, request *http.Request) {
func BotDetailsHandler(response http.ResponseWriter, request *http.Request) { func BotDetailsHandler(response http.ResponseWriter, request *http.Request) {
vars := mux.Vars(request) vars := mux.Vars(request)
if vars["key"] != globalData.ChannelData[vars["channel"]].AdminKey { if vars["key"] != ircBot.ChannelData[vars["channel"]].AdminKey {
UnauthorizedHandler(response, request) UnauthorizedHandler(response, request)
return return
} }
type ChannelDataSmaller struct { type ChannelDataSmaller struct {
Commands []data.CommandStruct `json:"commands,omitempty"` Commands []irc.CommandStruct `json:"commands,omitempty"`
ExtraStrings string `json:"extrastrings,omitempty"` ExtraStrings string `json:"extrastrings,omitempty"`
JoinTime time.Time `json:"jointime"` JoinTime time.Time `json:"jointime"`
HasLeft bool `json:"hasleft"` HasLeft bool `json:"hasleft"`
VideoCacheUpdated time.Time `json:"videoCacheUpdated"` VideoCacheUpdated time.Time `json:"videoCacheUpdated"`
} }
var deets ChannelDataSmaller var deets ChannelDataSmaller
deets.Commands = globalData.ChannelData[vars["channel"]].Commands deets.Commands = ircBot.ChannelData[vars["channel"]].Commands
deets.ExtraStrings = globalData.ChannelData[vars["channel"]].ExtraStrings deets.ExtraStrings = ircBot.ChannelData[vars["channel"]].ExtraStrings
deets.JoinTime = globalData.ChannelData[vars["channel"]].JoinTime deets.JoinTime = ircBot.ChannelData[vars["channel"]].JoinTime
deets.HasLeft = globalData.ChannelData[vars["channel"]].HasLeft deets.HasLeft = ircBot.ChannelData[vars["channel"]].HasLeft
deets.VideoCacheUpdated = globalData.ChannelData[vars["channel"]].VideoCacheUpdated deets.VideoCacheUpdated = ircBot.ChannelData[vars["channel"]].VideoCacheUpdated
response.Header().Add("Content-type", "application/json") response.Header().Add("Content-type", "application/json")
enc := json.NewEncoder(response) enc := json.NewEncoder(response)
enc.Encode(deets) enc.Encode(deets)
@ -862,7 +815,7 @@ func ReactIndexHandler(entrypoint string) func(w http.ResponseWriter, r *http.Re
func ForceRefreshHandler(response http.ResponseWriter, request *http.Request) { func ForceRefreshHandler(response http.ResponseWriter, request *http.Request) {
vars := mux.Vars(request) vars := mux.Vars(request)
if vars["key"] != globalData.ChannelData[vars["channel"]].AdminKey { if vars["key"] != ircBot.ChannelData[vars["channel"]].AdminKey {
UnauthorizedHandler(response, request) UnauthorizedHandler(response, request)
return return
} }
@ -874,17 +827,21 @@ func ForceRefreshHandler(response http.ResponseWriter, request *http.Request) {
func JoinHandler(response http.ResponseWriter, request *http.Request) { func JoinHandler(response http.ResponseWriter, request *http.Request) {
vars := mux.Vars(request) vars := mux.Vars(request)
if vars["key"] != globalData.ChannelData[vars["channel"]].AdminKey { if vars["key"] != ircBot.ChannelData[vars["channel"]].AdminKey {
UnauthorizedHandler(response, request) UnauthorizedHandler(response, request)
return return
} }
globalData.UpdateJoined(vars["channel"], false) record := ircBot.ChannelData[vars["channel"]]
ircBot.JoinChannel(vars["channel"]) record.Name = vars["channel"]
record.JoinTime = time.Now()
record.HasLeft = false
ircBot.Database.Write("channelData", vars["channel"], record)
ircBot.ChannelData[vars["channel"]] = record
ircBot.JoinChannel(record.Name)
} }
func HandleHTTP(passedIrcBot *irc.KardBot, passedGlobalData *data.GlobalData) { func HandleHTTP(passedIrcBot *irc.KardBot) {
ircBot = passedIrcBot ircBot = passedIrcBot
globalData = passedGlobalData
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)
@ -900,7 +857,6 @@ func HandleHTTP(passedIrcBot *irc.KardBot, passedGlobalData *data.GlobalData) {
r.HandleFunc("/debug/{channel}/{key}", JSONHandler) r.HandleFunc("/debug/{channel}/{key}", JSONHandler)
r.HandleFunc("/topsongs/{channel}/{key}", JSONHandler) r.HandleFunc("/topsongs/{channel}/{key}", JSONHandler)
r.HandleFunc("/topsingers/{channel}/{key}", JSONHandler) r.HandleFunc("/topsingers/{channel}/{key}", JSONHandler)
r.HandleFunc("/script.bat/{channel}/{key}", ScriptHandler)
r.Path("/twitchtobackend").Queries("access_token", "{access_token}", "scope", "{scope}", "token_type", "{token_type}").HandlerFunc(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) r.Path("/twitchadmin").Queries("code", "{code}", "scope", "{scope}").HandlerFunc(TwitchAdminHandler)
r.PathPrefix("/static").Handler(http.StripPrefix("/static", http.FileServer(http.Dir("./web/react-frontend/static")))) r.PathPrefix("/static").Handler(http.StripPrefix("/static", http.FileServer(http.Dir("./web/react-frontend/static"))))

View File

@ -5,7 +5,7 @@ import (
"testing" "testing"
"time" "time"
data "git.martyn.berlin/martyn/twitchsingstools/internal/data" irc "git.martyn.berlin/martyn/twitchsingstools/internal/irc"
) )
func TestDateRegex(t *testing.T) { func TestDateRegex(t *testing.T) {
@ -72,9 +72,9 @@ func TestSoloRegexes(t *testing.T) {
} }
func TestCalculatedDates(t *testing.T) { func TestCalculatedDates(t *testing.T) {
var record data.SingsVideoStruct var record irc.SingsVideoStruct
format := "2006-01-02 15:04:05 +0000 UTC" format := "2006-01-02 15:04:05 +0000 UTC"
var mockCache []data.SingsVideoStruct var mockCache []irc.SingsVideoStruct
record.Date, _ = time.Parse(format, "2020-07-13 19:43:11 +0000 UTC") record.Date, _ = time.Parse(format, "2020-07-13 19:43:11 +0000 UTC")
record.SongTitle = "Words Fail" record.SongTitle = "Words Fail"
record.OtherSinger = "FullOfEmily" record.OtherSinger = "FullOfEmily"

31
main.go
View File

@ -2,7 +2,6 @@ package main
import ( import (
"encoding/json" "encoding/json"
"fmt"
"io/ioutil" "io/ioutil"
"math/rand" "math/rand"
"os" "os"
@ -10,7 +9,6 @@ import (
"time" "time"
builtins "git.martyn.berlin/martyn/twitchsingstools/internal/builtins" builtins "git.martyn.berlin/martyn/twitchsingstools/internal/builtins"
data "git.martyn.berlin/martyn/twitchsingstools/internal/data"
irc "git.martyn.berlin/martyn/twitchsingstools/internal/irc" irc "git.martyn.berlin/martyn/twitchsingstools/internal/irc"
webserver "git.martyn.berlin/martyn/twitchsingstools/internal/webserver" webserver "git.martyn.berlin/martyn/twitchsingstools/internal/webserver"
rgb "github.com/foresthoffman/rgblog" rgb "github.com/foresthoffman/rgblog"
@ -25,7 +23,7 @@ var selectablePrompts []string
var customStrings customStringsStruct var customStrings customStringsStruct
var config data.ConfigStruct var config irc.ConfigStruct
func readConfig() { func readConfig() {
var data []byte var data []byte
@ -176,32 +174,12 @@ func main() {
} }
} else { } else {
if _, err := os.Stat(config.AppOAuthPath); os.IsNotExist(err) { if _, err := os.Stat(config.AppOAuthPath); os.IsNotExist(err) {
rgb.RPrintf("[%s] Error config-specified oauth file %s doesn't exist, bailing!\n", irc.TimeStamp(), config.AppOAuthPath) rgb.YPrintf("[%s] Error config-specified oauth file %s doesn't exist, bailing!\n", irc.TimeStamp(), config.AppOAuthPath)
os.Exit(1) os.Exit(1)
} }
appOauthPath = config.AppOAuthPath appOauthPath = config.AppOAuthPath
} }
rgb.YPrintf("[%s] Starting connection to redis...\n", irc.TimeStamp())
//TODO: unhardcode this
if os.Getenv("TSTOOLS_REDIS_HOST") != "" {
config.DatabaseSVC = os.Getenv("TSTOOLS_REDIS_HOST")
} else {
// assume localhost, which should fail.
config.DatabaseSVC = "localhost"
}
var globalData data.GlobalData
globalData.Config = config
globalData.ConnectDatabase()
defer globalData.Database.Close()
rgb.GPrintf("[%s] Connected to \"redis\" %s\n", irc.TimeStamp(), "config.DatabaseSVC")
err := globalData.ReadChannelData()
if nil != err {
fmt.Println(err)
fmt.Println("Aborting!")
os.Exit(1)
}
rgb.GPrintf("[%s] Read the channel data from \"redis\" successfully, now have %d records\n", irc.TimeStamp(), len(globalData.ChannelData))
// 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.
var myBot irc.KardBot var myBot irc.KardBot
@ -215,12 +193,13 @@ func main() {
AppPrivatePath: appOauthPath, AppPrivatePath: appOauthPath,
Server: "irc.chat.twitch.tv", Server: "irc.chat.twitch.tv",
Prompts: selectablePrompts, Prompts: selectablePrompts,
GlobalData: globalData, 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, &globalData) webserver.HandleHTTP(&myBot)
}() }()
if ircOauthPath != "" { if ircOauthPath != "" {
myBot.Start() myBot.Start()

View File

@ -1,5 +0,0 @@
@ECHO OFF
{{ range .SongData -}}
IF NOT EXIST "{{.OtherSinger}}\" MKDIR {{.OtherSinger}}
IF NOT EXIST "{{.OtherSinger}}\{{.ShortDate}}-{{.OtherSinger}}-{{.SongTitle}}-{{.VideoNumber}}.mp4" youtube-dl -o "{{.OtherSinger}}\{{.ShortDate}}-{{.OtherSinger}}-{{.SongTitle}}-{{.VideoNumber}}.mp4" {{.VideoURL}}
{{ end }}