twitchsingstools/internal/data/data.go

235 lines
7.8 KiB
Go
Executable File

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)
}