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