2015-10-25 03:21:50 -07:00
|
|
|
package server
|
|
|
|
|
|
|
|
import (
|
2015-11-08 22:34:06 -08:00
|
|
|
"errors"
|
|
|
|
"fmt"
|
|
|
|
"net/http"
|
|
|
|
"strconv"
|
2015-11-19 16:55:03 -08:00
|
|
|
"strings"
|
2015-10-25 03:21:50 -07:00
|
|
|
"sync"
|
|
|
|
"time"
|
|
|
|
)
|
|
|
|
|
2015-11-08 22:34:06 -08:00
|
|
|
type PushCommandCacheInfo struct {
|
|
|
|
Caching BacklogCacheType
|
|
|
|
Target MessageTargetType
|
2015-10-25 03:21:50 -07:00
|
|
|
}
|
|
|
|
|
2015-11-18 18:33:20 -08:00
|
|
|
// S2CCommandsCacheInfo details what the behavior is of each command that can be sent to /cached_pub.
|
|
|
|
var S2CCommandsCacheInfo = map[Command]PushCommandCacheInfo{
|
2015-11-08 22:34:06 -08:00
|
|
|
/// Channel data
|
|
|
|
// follow_sets: extra emote sets included in the chat
|
|
|
|
// follow_buttons: extra follow buttons below the stream
|
2015-11-18 18:33:20 -08:00
|
|
|
"follow_sets": {CacheTypePersistent, MsgTargetTypeChat},
|
|
|
|
"follow_buttons": {CacheTypePersistent, MsgTargetTypeChat},
|
2016-01-27 18:04:28 -08:00
|
|
|
"srl_race": {CacheTypeLastOnly, MsgTargetTypeMultichat},
|
2015-11-08 22:34:06 -08:00
|
|
|
|
|
|
|
/// Chatter/viewer counts
|
2015-11-18 18:33:20 -08:00
|
|
|
"chatters": {CacheTypeLastOnly, MsgTargetTypeChat},
|
|
|
|
"viewers": {CacheTypeLastOnly, MsgTargetTypeChat},
|
2015-11-08 22:34:06 -08:00
|
|
|
}
|
|
|
|
|
2016-04-28 14:39:20 -07:00
|
|
|
var PersistentCachingCommands = []Command{"follow_sets", "follow_buttons"}
|
2016-04-28 15:33:49 -07:00
|
|
|
var HourlyCachingCommands = []Command{"chatters", "viewers"} /* srl_race */
|
2016-04-28 14:39:20 -07:00
|
|
|
|
2015-11-08 22:34:06 -08:00
|
|
|
type BacklogCacheType int
|
|
|
|
|
|
|
|
const (
|
2016-01-17 18:01:21 -08:00
|
|
|
// CacheTypeInvalid is the sentinel value.
|
2015-11-08 22:34:06 -08:00
|
|
|
CacheTypeInvalid BacklogCacheType = iota
|
2016-01-17 18:01:21 -08:00
|
|
|
// CacheTypeNever is a message that cannot be cached.
|
2015-11-08 22:34:06 -08:00
|
|
|
CacheTypeNever
|
2016-01-17 18:01:21 -08:00
|
|
|
// CacheTypeLastOnly means to save only the last copy of this message,
|
|
|
|
// and always send it when the backlog is requested.
|
2015-11-08 22:34:06 -08:00
|
|
|
CacheTypeLastOnly
|
2016-01-17 18:01:21 -08:00
|
|
|
// CacheTypePersistent means to save the last copy of this message,
|
|
|
|
// and always send it when the backlog is requested, but do not clean it periodically.
|
2015-11-08 22:34:06 -08:00
|
|
|
CacheTypePersistent
|
|
|
|
)
|
|
|
|
|
|
|
|
type MessageTargetType int
|
|
|
|
|
|
|
|
const (
|
2016-01-17 18:01:21 -08:00
|
|
|
// MsgTargetTypeInvalid is the sentinel value.
|
2015-11-08 22:34:06 -08:00
|
|
|
MsgTargetTypeInvalid MessageTargetType = iota
|
2016-01-17 18:01:21 -08:00
|
|
|
// MsgTargetTypeChat is a message is targeted to all users in a particular chat.
|
2015-11-08 22:34:06 -08:00
|
|
|
MsgTargetTypeChat
|
2016-01-17 18:01:21 -08:00
|
|
|
// MsgTargetTypeMultichat is a message is targeted to all users in multiple chats.
|
2015-11-08 22:34:06 -08:00
|
|
|
MsgTargetTypeMultichat
|
2016-01-17 18:01:21 -08:00
|
|
|
// MsgTargetTypeGlobal is a message sent to all FFZ users.
|
2015-11-08 22:34:06 -08:00
|
|
|
MsgTargetTypeGlobal
|
|
|
|
)
|
|
|
|
|
|
|
|
// note: see types.go for methods on these
|
|
|
|
|
2016-01-17 18:01:21 -08:00
|
|
|
// ErrorUnrecognizedCacheType is returned by BacklogCacheType.UnmarshalJSON()
|
2015-11-08 22:34:06 -08:00
|
|
|
var ErrorUnrecognizedCacheType = errors.New("Invalid value for cachetype")
|
|
|
|
|
2016-01-17 18:01:21 -08:00
|
|
|
// ErrorUnrecognizedTargetType is returned by MessageTargetType.UnmarshalJSON()
|
2015-11-08 22:34:06 -08:00
|
|
|
var ErrorUnrecognizedTargetType = errors.New("Invalid value for message target")
|
|
|
|
|
|
|
|
type LastSavedMessage struct {
|
|
|
|
Timestamp time.Time
|
|
|
|
Data string
|
|
|
|
}
|
2015-10-25 03:21:50 -07:00
|
|
|
|
2015-11-08 22:34:06 -08:00
|
|
|
// map is command -> channel -> data
|
|
|
|
|
2016-04-28 15:33:49 -07:00
|
|
|
// CachedLastMessages is of CacheTypeLastOnly.
|
|
|
|
// Not actually cleaned up by reaper goroutine every ~hour.
|
2016-01-03 09:31:22 -08:00
|
|
|
var CachedLastMessages = make(map[Command]map[string]LastSavedMessage)
|
2015-11-08 22:34:06 -08:00
|
|
|
var CachedLSMLock sync.RWMutex
|
|
|
|
|
2016-01-17 18:01:21 -08:00
|
|
|
// PersistentLastMessages is of CacheTypePersistent. Never cleaned.
|
2016-04-28 15:33:49 -07:00
|
|
|
var PersistentLastMessages = CachedLastMessages
|
|
|
|
var PersistentLSMLock = CachedLSMLock
|
2015-11-08 22:34:06 -08:00
|
|
|
|
2015-11-16 12:50:00 -08:00
|
|
|
// DumpBacklogData drops all /cached_pub data.
|
|
|
|
func DumpBacklogData() {
|
2015-11-08 22:34:06 -08:00
|
|
|
CachedLSMLock.Lock()
|
|
|
|
CachedLastMessages = make(map[Command]map[string]LastSavedMessage)
|
|
|
|
CachedLSMLock.Unlock()
|
|
|
|
|
2016-04-28 15:33:49 -07:00
|
|
|
//PersistentLSMLock.Lock()
|
|
|
|
//PersistentLastMessages = make(map[Command]map[string]LastSavedMessage)
|
|
|
|
//PersistentLSMLock.Unlock()
|
2015-11-08 22:34:06 -08:00
|
|
|
}
|
|
|
|
|
2015-11-16 12:50:00 -08:00
|
|
|
// SendBacklogForNewClient sends any backlog data relevant to a new client.
|
|
|
|
// This should be done when the client sends a `ready` message.
|
|
|
|
// This will only send data for CacheTypePersistent and CacheTypeLastOnly because those do not involve timestamps.
|
2015-11-08 22:34:06 -08:00
|
|
|
func SendBacklogForNewClient(client *ClientInfo) {
|
|
|
|
client.Mutex.Lock() // reading CurrentChannels
|
2015-11-23 23:34:57 -08:00
|
|
|
curChannels := make([]string, len(client.CurrentChannels))
|
|
|
|
copy(curChannels, client.CurrentChannels)
|
|
|
|
client.Mutex.Unlock()
|
|
|
|
|
2015-11-08 22:34:06 -08:00
|
|
|
PersistentLSMLock.RLock()
|
2016-04-28 14:39:20 -07:00
|
|
|
for _, cmd := range GetCommandsOfType(CacheTypePersistent) {
|
|
|
|
chanMap := PersistentLastMessages[cmd]
|
2015-11-08 22:34:06 -08:00
|
|
|
if chanMap == nil {
|
|
|
|
continue
|
|
|
|
}
|
2015-11-23 23:34:57 -08:00
|
|
|
for _, channel := range curChannels {
|
2015-11-08 22:34:06 -08:00
|
|
|
msg, ok := chanMap[channel]
|
|
|
|
if ok {
|
|
|
|
msg := ClientMessage{MessageID: -1, Command: cmd, origArguments: msg.Data}
|
|
|
|
msg.parseOrigArguments()
|
|
|
|
client.MessageChannel <- msg
|
|
|
|
}
|
2015-10-25 03:21:50 -07:00
|
|
|
}
|
|
|
|
}
|
2015-11-08 22:34:06 -08:00
|
|
|
PersistentLSMLock.RUnlock()
|
|
|
|
|
|
|
|
CachedLSMLock.RLock()
|
2016-04-28 14:39:20 -07:00
|
|
|
for _, cmd := range GetCommandsOfType(CacheTypeLastOnly) {
|
2015-11-08 22:34:06 -08:00
|
|
|
chanMap := CachedLastMessages[cmd]
|
|
|
|
if chanMap == nil {
|
|
|
|
continue
|
|
|
|
}
|
2015-11-23 23:34:57 -08:00
|
|
|
for _, channel := range curChannels {
|
2015-11-08 22:34:06 -08:00
|
|
|
msg, ok := chanMap[channel]
|
|
|
|
if ok {
|
|
|
|
msg := ClientMessage{MessageID: -1, Command: cmd, origArguments: msg.Data}
|
|
|
|
msg.parseOrigArguments()
|
|
|
|
client.MessageChannel <- msg
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
CachedLSMLock.RUnlock()
|
2015-10-25 03:21:50 -07:00
|
|
|
}
|
|
|
|
|
2016-04-28 14:39:20 -07:00
|
|
|
func SendBacklogForChannel(client *ClientInfo, channel string) {
|
|
|
|
PersistentLSMLock.RLock()
|
|
|
|
for _, cmd := range GetCommandsOfType(CacheTypePersistent) {
|
|
|
|
chanMap := PersistentLastMessages[cmd]
|
|
|
|
if chanMap == nil {
|
|
|
|
continue
|
|
|
|
}
|
|
|
|
if msg, ok := chanMap[channel]; ok {
|
|
|
|
msg := ClientMessage{MessageID: -1, Command: cmd, origArguments: msg.Data}
|
|
|
|
msg.parseOrigArguments()
|
|
|
|
client.MessageChannel <- msg
|
|
|
|
}
|
|
|
|
}
|
|
|
|
PersistentLSMLock.RUnlock()
|
|
|
|
|
|
|
|
CachedLSMLock.RLock()
|
|
|
|
for _, cmd := range GetCommandsOfType(CacheTypeLastOnly) {
|
|
|
|
chanMap := CachedLastMessages[cmd]
|
|
|
|
if chanMap == nil {
|
|
|
|
continue
|
|
|
|
}
|
|
|
|
if msg, ok := chanMap[channel]; ok {
|
|
|
|
msg := ClientMessage{MessageID: -1, Command: cmd, origArguments: msg.Data}
|
|
|
|
msg.parseOrigArguments()
|
|
|
|
client.MessageChannel <- msg
|
|
|
|
}
|
|
|
|
}
|
|
|
|
CachedLSMLock.RUnlock()
|
|
|
|
}
|
|
|
|
|
2015-11-16 12:50:00 -08:00
|
|
|
type timestampArray interface {
|
2015-11-08 22:34:06 -08:00
|
|
|
Len() int
|
|
|
|
GetTime(int) time.Time
|
|
|
|
}
|
2015-11-04 15:11:49 -08:00
|
|
|
|
2015-11-08 22:34:06 -08:00
|
|
|
func SaveLastMessage(which map[Command]map[string]LastSavedMessage, locker sync.Locker, cmd Command, channel string, timestamp time.Time, data string, deleting bool) {
|
|
|
|
locker.Lock()
|
|
|
|
defer locker.Unlock()
|
|
|
|
|
2016-04-28 15:33:49 -07:00
|
|
|
chanMap, ok := CachedLastMessages[cmd]
|
2015-11-08 22:34:06 -08:00
|
|
|
if !ok {
|
|
|
|
if deleting {
|
|
|
|
return
|
|
|
|
}
|
|
|
|
chanMap = make(map[string]LastSavedMessage)
|
2016-04-28 15:33:49 -07:00
|
|
|
CachedLastMessages[cmd] = chanMap
|
2015-11-08 22:34:06 -08:00
|
|
|
}
|
|
|
|
|
|
|
|
if deleting {
|
|
|
|
delete(chanMap, channel)
|
|
|
|
} else {
|
2016-01-17 18:01:21 -08:00
|
|
|
chanMap[channel] = LastSavedMessage{Timestamp: timestamp, Data: data}
|
2015-11-08 22:34:06 -08:00
|
|
|
}
|
2015-10-25 20:17:17 -07:00
|
|
|
}
|
|
|
|
|
2016-04-28 14:39:20 -07:00
|
|
|
func GetCommandsOfType(match BacklogCacheType) []Command {
|
|
|
|
if match == CacheTypePersistent {
|
|
|
|
return PersistentCachingCommands
|
|
|
|
} else if match == CacheTypeLastOnly {
|
|
|
|
return HourlyCachingCommands
|
|
|
|
} else {
|
|
|
|
panic("unknown caching type")
|
2015-10-25 03:21:50 -07:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2015-11-16 12:50:00 -08:00
|
|
|
func HTTPBackendDropBacklog(w http.ResponseWriter, r *http.Request) {
|
2015-11-08 22:34:06 -08:00
|
|
|
r.ParseForm()
|
|
|
|
formData, err := UnsealRequest(r.Form)
|
|
|
|
if err != nil {
|
|
|
|
w.WriteHeader(403)
|
|
|
|
fmt.Fprintf(w, "Error: %v", err)
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
confirm := formData.Get("confirm")
|
|
|
|
if confirm == "1" {
|
2015-11-16 12:50:00 -08:00
|
|
|
DumpBacklogData()
|
2015-10-29 10:29:16 -07:00
|
|
|
}
|
2015-10-25 03:21:50 -07:00
|
|
|
}
|
|
|
|
|
2016-01-17 18:01:21 -08:00
|
|
|
// HTTPBackendCachedPublish handles the /cached_pub route.
|
|
|
|
// It publishes a message to clients, and then updates the in-server cache for the message.
|
2015-11-08 22:34:06 -08:00
|
|
|
// notes:
|
|
|
|
// `scope` is implicit in the command
|
2015-11-16 13:25:25 -08:00
|
|
|
func HTTPBackendCachedPublish(w http.ResponseWriter, r *http.Request) {
|
2015-11-08 22:34:06 -08:00
|
|
|
r.ParseForm()
|
|
|
|
formData, err := UnsealRequest(r.Form)
|
|
|
|
if err != nil {
|
|
|
|
w.WriteHeader(403)
|
|
|
|
fmt.Fprintf(w, "Error: %v", err)
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
2016-01-17 19:46:01 -08:00
|
|
|
cmd := CommandPool.InternCommand(formData.Get("cmd"))
|
2015-11-08 22:34:06 -08:00
|
|
|
json := formData.Get("args")
|
|
|
|
channel := formData.Get("channel")
|
|
|
|
deleteMode := formData.Get("delete") != ""
|
|
|
|
timeStr := formData.Get("time")
|
2016-01-03 08:57:37 -08:00
|
|
|
timeNum, err := strconv.ParseInt(timeStr, 10, 64)
|
2015-11-08 22:34:06 -08:00
|
|
|
if err != nil {
|
|
|
|
w.WriteHeader(422)
|
|
|
|
fmt.Fprintf(w, "error parsing time: %v", err)
|
2016-01-03 08:57:37 -08:00
|
|
|
return
|
2015-11-08 22:34:06 -08:00
|
|
|
}
|
2016-01-03 08:57:37 -08:00
|
|
|
timestamp := time.Unix(timeNum, 0)
|
2015-10-25 03:21:50 -07:00
|
|
|
|
2015-11-18 18:33:20 -08:00
|
|
|
cacheinfo, ok := S2CCommandsCacheInfo[cmd]
|
2015-11-08 22:34:06 -08:00
|
|
|
if !ok {
|
|
|
|
w.WriteHeader(422)
|
|
|
|
fmt.Fprintf(w, "Caching semantics unknown for command '%s'. Post to /addcachedcommand first.", cmd)
|
|
|
|
return
|
|
|
|
}
|
2015-11-04 15:11:49 -08:00
|
|
|
|
2015-11-08 22:34:06 -08:00
|
|
|
var count int
|
|
|
|
msg := ClientMessage{MessageID: -1, Command: cmd, origArguments: json}
|
|
|
|
msg.parseOrigArguments()
|
|
|
|
|
|
|
|
if cacheinfo.Caching == CacheTypeLastOnly && cacheinfo.Target == MsgTargetTypeChat {
|
|
|
|
SaveLastMessage(CachedLastMessages, &CachedLSMLock, cmd, channel, timestamp, json, deleteMode)
|
|
|
|
count = PublishToChannel(channel, msg)
|
|
|
|
} else if cacheinfo.Caching == CacheTypePersistent && cacheinfo.Target == MsgTargetTypeChat {
|
|
|
|
SaveLastMessage(PersistentLastMessages, &PersistentLSMLock, cmd, channel, timestamp, json, deleteMode)
|
|
|
|
count = PublishToChannel(channel, msg)
|
2015-11-19 16:38:07 -08:00
|
|
|
} else if cacheinfo.Caching == CacheTypeLastOnly && cacheinfo.Target == MsgTargetTypeMultichat {
|
|
|
|
channels := strings.Split(channel, ",")
|
2016-01-27 19:40:17 -08:00
|
|
|
var dummyLock sync.Mutex
|
|
|
|
CachedLSMLock.Lock()
|
2015-11-19 16:38:07 -08:00
|
|
|
for _, channel := range channels {
|
2016-01-27 19:40:17 -08:00
|
|
|
SaveLastMessage(CachedLastMessages, &dummyLock, cmd, channel, timestamp, json, deleteMode)
|
2015-11-19 16:38:07 -08:00
|
|
|
}
|
2016-01-27 19:40:17 -08:00
|
|
|
CachedLSMLock.Unlock()
|
2015-11-19 16:38:07 -08:00
|
|
|
count = PublishToMultiple(channels, msg)
|
2015-10-25 03:21:50 -07:00
|
|
|
}
|
2015-11-08 22:34:06 -08:00
|
|
|
|
|
|
|
w.Write([]byte(strconv.Itoa(count)))
|
2015-10-26 10:06:45 -07:00
|
|
|
}
|