2015-10-25 03:21:50 -07:00
|
|
|
package server
|
|
|
|
|
|
|
|
import (
|
2015-11-08 22:34:06 -08:00
|
|
|
"errors"
|
|
|
|
"fmt"
|
|
|
|
"net/http"
|
|
|
|
"sort"
|
|
|
|
"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},
|
|
|
|
"srl_race": {CacheTypeLastOnly, MsgTargetTypeChat},
|
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
|
|
|
}
|
|
|
|
|
|
|
|
type BacklogCacheType int
|
|
|
|
|
|
|
|
const (
|
|
|
|
// This is not a cache type.
|
|
|
|
CacheTypeInvalid BacklogCacheType = iota
|
|
|
|
// This message cannot be cached.
|
|
|
|
CacheTypeNever
|
|
|
|
// Save only the last copy of this message, and always send it when the backlog is requested.
|
|
|
|
CacheTypeLastOnly
|
|
|
|
// Save this backlog data to disk with its timestamp.
|
|
|
|
// Send it when the backlog is requested, or after a reconnect if it was updated.
|
|
|
|
CacheTypePersistent
|
|
|
|
)
|
|
|
|
|
|
|
|
type MessageTargetType int
|
|
|
|
|
|
|
|
const (
|
|
|
|
// This is not a message target.
|
|
|
|
MsgTargetTypeInvalid MessageTargetType = iota
|
|
|
|
// This message is targeted to all users in a chat
|
|
|
|
MsgTargetTypeChat
|
|
|
|
// This message is targeted to all users in multiple chats
|
|
|
|
MsgTargetTypeMultichat
|
|
|
|
// This message is sent to all FFZ users.
|
|
|
|
MsgTargetTypeGlobal
|
|
|
|
)
|
|
|
|
|
|
|
|
// note: see types.go for methods on these
|
|
|
|
|
|
|
|
// Returned by BacklogCacheType.UnmarshalJSON()
|
|
|
|
var ErrorUnrecognizedCacheType = errors.New("Invalid value for cachetype")
|
|
|
|
|
|
|
|
// Returned by MessageTargetType.UnmarshalJSON()
|
|
|
|
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
|
|
|
|
|
|
|
|
// CacheTypeLastOnly. 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
|
|
|
|
|
|
|
|
// CacheTypePersistent. Never cleaned.
|
2016-01-03 09:31:22 -08:00
|
|
|
var PersistentLastMessages = make(map[Command]map[string]LastSavedMessage)
|
2015-11-08 22:34:06 -08:00
|
|
|
var PersistentLSMLock sync.RWMutex
|
|
|
|
|
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()
|
|
|
|
|
|
|
|
PersistentLSMLock.Lock()
|
|
|
|
PersistentLastMessages = make(map[Command]map[string]LastSavedMessage)
|
|
|
|
PersistentLSMLock.Unlock()
|
|
|
|
}
|
|
|
|
|
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()
|
|
|
|
for _, cmd := range GetCommandsOfType(PushCommandCacheInfo{CacheTypePersistent, MsgTargetTypeChat}) {
|
|
|
|
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
|
|
|
|
}
|
2015-10-25 03:21:50 -07:00
|
|
|
}
|
|
|
|
}
|
2015-11-08 22:34:06 -08:00
|
|
|
PersistentLSMLock.RUnlock()
|
|
|
|
|
|
|
|
CachedLSMLock.RLock()
|
|
|
|
for _, cmd := range GetCommandsOfType(PushCommandCacheInfo{CacheTypeLastOnly, MsgTargetTypeChat}) {
|
|
|
|
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
|
|
|
}
|
|
|
|
|
2015-11-16 12:50:00 -08:00
|
|
|
// insertionSort implements insertion sort.
|
|
|
|
// CacheTypeTimestamps should use insertion sort for O(N) average performance.
|
|
|
|
// (The average case is the array is still sorted after insertion of the new item.)
|
|
|
|
func insertionSort(ary sort.Interface) {
|
2015-11-08 22:34:06 -08:00
|
|
|
for i := 1; i < ary.Len(); i++ {
|
|
|
|
for j := i; j > 0 && ary.Less(j, j-1); j-- {
|
|
|
|
ary.Swap(j, j-1)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
2015-11-04 15:11:49 -08:00
|
|
|
|
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-16 12:50:00 -08:00
|
|
|
func findFirstNewMessage(ary timestampArray, disconnectTime time.Time) (idx int) {
|
2015-11-08 22:34:06 -08:00
|
|
|
len := ary.Len()
|
|
|
|
i := len
|
|
|
|
|
|
|
|
// Walk backwards until we find GetTime() before disconnectTime
|
|
|
|
step := 1
|
|
|
|
for i > 0 {
|
|
|
|
i -= step
|
|
|
|
if i < 0 {
|
|
|
|
i = 0
|
|
|
|
}
|
|
|
|
if !ary.GetTime(i).After(disconnectTime) {
|
|
|
|
break
|
|
|
|
}
|
|
|
|
step = int(float64(step)*1.5) + 1
|
2015-10-25 03:21:50 -07:00
|
|
|
}
|
|
|
|
|
2015-11-08 22:34:06 -08:00
|
|
|
// Walk forwards until we find GetTime() after disconnectTime
|
|
|
|
for i < len && !ary.GetTime(i).After(disconnectTime) {
|
|
|
|
i++
|
|
|
|
}
|
|
|
|
|
|
|
|
if i == len {
|
|
|
|
return -1
|
|
|
|
}
|
|
|
|
return i
|
2015-10-26 10:06:45 -07: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()
|
|
|
|
|
|
|
|
chanMap, ok := CachedLastMessages[cmd]
|
|
|
|
if !ok {
|
|
|
|
if deleting {
|
|
|
|
return
|
|
|
|
}
|
|
|
|
chanMap = make(map[string]LastSavedMessage)
|
|
|
|
CachedLastMessages[cmd] = chanMap
|
|
|
|
}
|
|
|
|
|
|
|
|
if deleting {
|
|
|
|
delete(chanMap, channel)
|
|
|
|
} else {
|
|
|
|
chanMap[channel] = LastSavedMessage{timestamp, data}
|
|
|
|
}
|
2015-10-25 20:17:17 -07:00
|
|
|
}
|
|
|
|
|
2015-11-08 22:34:06 -08:00
|
|
|
func GetCommandsOfType(match PushCommandCacheInfo) []Command {
|
|
|
|
var ret []Command
|
2015-11-18 18:33:20 -08:00
|
|
|
for cmd, info := range S2CCommandsCacheInfo {
|
2015-11-08 22:34:06 -08:00
|
|
|
if info == match {
|
|
|
|
ret = append(ret, cmd)
|
2015-10-25 20:17:17 -07:00
|
|
|
}
|
2015-10-25 03:21:50 -07:00
|
|
|
}
|
2015-11-08 22:34:06 -08:00
|
|
|
return ret
|
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
|
|
|
}
|
|
|
|
|
2015-11-08 22:34:06 -08:00
|
|
|
// Publish a message to clients, and update the in-server cache for the message.
|
|
|
|
// 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
|
|
|
|
}
|
|
|
|
|
|
|
|
cmd := Command(formData.Get("cmd"))
|
|
|
|
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, ",")
|
|
|
|
for _, channel := range channels {
|
|
|
|
SaveLastMessage(CachedLastMessages, &CachedLSMLock, cmd, channel, timestamp, json, deleteMode)
|
|
|
|
}
|
|
|
|
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
|
|
|
}
|