235 lines
7.8 KiB
Go
235 lines
7.8 KiB
Go
|
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)
|
||
|
}
|