1
0
Fork 0
mirror of https://github.com/FrankerFaceZ/FrankerFaceZ.git synced 2025-06-27 21:05:53 +00:00
FrankerFaceZ/socketserver/internal/server/commands.go

547 lines
14 KiB
Go
Raw Normal View History

package server
import (
"encoding/json"
"errors"
"github.com/gorilla/websocket"
"github.com/satori/go.uuid"
"log"
2015-10-28 18:12:20 -07:00
"net/url"
2015-10-25 00:58:05 -07:00
"strconv"
"sync"
2015-10-25 03:21:50 -07:00
"time"
)
2015-11-15 18:43:34 -08:00
// Command is a string indicating which RPC is requested.
// The Commands sent from Client -> Server and Server -> Client are disjoint sets.
2015-11-08 22:34:06 -08:00
type Command string
2015-11-15 18:43:34 -08:00
// CommandHandler is a RPC handler assosciated with a Command.
2015-11-08 22:34:06 -08:00
type CommandHandler func(*websocket.Conn, *ClientInfo, ClientMessage) (ClientMessage, error)
2015-11-15 18:43:34 -08:00
var commandHandlers = map[Command]CommandHandler{
2015-11-08 22:34:06 -08:00
HelloCommand: HandleHello,
"setuser": HandleSetUser,
"ready": HandleReady,
"sub": HandleSub,
"unsub": HandleUnsub,
"track_follow": HandleTrackFollow,
"emoticon_uses": HandleEmoticonUses,
"survey": HandleSurvey,
"twitch_emote": HandleRemoteCommand,
"get_link": HandleBunchedRemoteCommand,
"get_display_name": HandleBunchedRemoteCommand,
"update_follow_buttons": HandleRemoteCommand,
"chat_history": HandleRemoteCommand,
}
2015-10-25 03:21:50 -07:00
const ChannelInfoDelay = 2 * time.Second
func HandleCommand(conn *websocket.Conn, client *ClientInfo, msg ClientMessage) {
2015-11-15 18:43:34 -08:00
handler, ok := commandHandlers[msg.Command]
if !ok {
2015-11-08 21:20:32 -08:00
handler = HandleRemoteCommand
}
response, err := CallHandler(handler, conn, client, msg)
if err == nil {
2015-10-26 14:55:20 -07:00
if response.Command == AsyncResponseCommand {
// Don't send anything
// The response will be delivered over client.MessageChannel / serverMessageChan
} else {
response.MessageID = msg.MessageID
SendMessage(conn, response)
2015-10-26 14:55:20 -07:00
}
} else {
SendMessage(conn, ClientMessage{
MessageID: msg.MessageID,
Command: "error",
Arguments: err.Error(),
})
}
}
func HandleHello(conn *websocket.Conn, client *ClientInfo, msg ClientMessage) (rmsg ClientMessage, err error) {
2015-11-15 18:43:34 -08:00
version, clientID, err := msg.ArgumentsAsTwoStrings()
if err != nil {
return
}
client.Version = version
2015-11-15 18:43:34 -08:00
client.ClientID = uuid.FromStringOrNil(clientID)
if client.ClientID == uuid.Nil {
client.ClientID = uuid.NewV4()
}
SubscribeGlobal(client)
2015-11-09 14:44:33 -08:00
SubscribeDefaults(client)
return ClientMessage{
Arguments: client.ClientID.String(),
}, nil
}
2015-10-26 14:55:20 -07:00
func HandleReady(conn *websocket.Conn, client *ClientInfo, msg ClientMessage) (rmsg ClientMessage, err error) {
disconnectAt, err := msg.ArgumentsAsInt()
if err != nil {
return
}
client.Mutex.Lock()
if client.MakePendingRequests != nil {
if !client.MakePendingRequests.Stop() {
// Timer already fired, GetSubscriptionBacklog() has started
rmsg.Command = SuccessCommand
return
}
}
client.PendingSubscriptionsBacklog = nil
client.MakePendingRequests = nil
client.Mutex.Unlock()
client.MsgChannelKeepalive.Add(1)
go func() {
client.MessageChannel <- ClientMessage{MessageID: msg.MessageID, Command: SuccessCommand}
SendBacklogForNewClient(client)
if disconnectAt != 0 {
2015-10-26 14:55:20 -07:00
SendTimedBacklogMessages(client, time.Unix(disconnectAt, 0))
}
client.MsgChannelKeepalive.Done()
}()
return ClientMessage{Command: AsyncResponseCommand}, nil
2015-10-26 14:55:20 -07:00
}
func HandleSetUser(conn *websocket.Conn, client *ClientInfo, msg ClientMessage) (rmsg ClientMessage, err error) {
username, err := msg.ArgumentsAsString()
if err != nil {
return
}
2015-10-25 03:21:50 -07:00
client.Mutex.Lock()
client.TwitchUsername = username
client.UsernameValidated = false
2015-10-25 03:21:50 -07:00
client.Mutex.Unlock()
2015-11-15 18:43:34 -08:00
if Configuration.SendAuthToNewClients {
2015-11-08 16:44:16 -08:00
client.MsgChannelKeepalive.Add(1)
go client.StartAuthorization(func(_ *ClientInfo, _ bool) {
client.MsgChannelKeepalive.Done()
})
}
return ResponseSuccess, nil
}
func HandleSub(conn *websocket.Conn, client *ClientInfo, msg ClientMessage) (rmsg ClientMessage, err error) {
channel, err := msg.ArgumentsAsString()
if err != nil {
return
}
2015-10-25 03:21:50 -07:00
client.Mutex.Lock()
AddToSliceS(&client.CurrentChannels, channel)
client.PendingSubscriptionsBacklog = append(client.PendingSubscriptionsBacklog, channel)
2015-10-27 21:21:06 -07:00
// if client.MakePendingRequests == nil {
// client.MakePendingRequests = time.AfterFunc(ChannelInfoDelay, GetSubscriptionBacklogFor(conn, client))
// } else {
// if !client.MakePendingRequests.Reset(ChannelInfoDelay) {
// client.MakePendingRequests = time.AfterFunc(ChannelInfoDelay, GetSubscriptionBacklogFor(conn, client))
// }
// }
2015-10-25 03:21:50 -07:00
client.Mutex.Unlock()
2015-11-08 22:34:06 -08:00
SubscribeChannel(client, channel)
return ResponseSuccess, nil
}
func HandleUnsub(conn *websocket.Conn, client *ClientInfo, msg ClientMessage) (rmsg ClientMessage, err error) {
channel, err := msg.ArgumentsAsString()
if err != nil {
return
}
2015-10-25 03:21:50 -07:00
client.Mutex.Lock()
RemoveFromSliceS(&client.CurrentChannels, channel)
2015-10-25 03:21:50 -07:00
client.Mutex.Unlock()
UnsubscribeSingleChat(client, channel)
return ResponseSuccess, nil
}
2015-10-25 03:21:50 -07:00
func GetSubscriptionBacklogFor(conn *websocket.Conn, client *ClientInfo) func() {
return func() {
GetSubscriptionBacklog(conn, client)
}
}
// On goroutine
func GetSubscriptionBacklog(conn *websocket.Conn, client *ClientInfo) {
var subs []string
2015-10-25 03:21:50 -07:00
// Lock, grab the data, and reset it
client.Mutex.Lock()
subs = client.PendingSubscriptionsBacklog
client.PendingSubscriptionsBacklog = nil
2015-10-25 03:21:50 -07:00
client.MakePendingRequests = nil
client.Mutex.Unlock()
if len(subs) == 0 {
2015-10-25 03:21:50 -07:00
return
}
2015-11-15 18:43:34 -08:00
if backendURL == "" {
return // for testing runs
}
messages, err := FetchBacklogData(subs)
2015-10-25 03:21:50 -07:00
if err != nil {
// Oh well.
log.Print("error in GetSubscriptionBacklog:", err)
return
}
// Deliver to client
client.MsgChannelKeepalive.Add(1)
if client.MessageChannel != nil {
for _, msg := range messages {
client.MessageChannel <- msg
}
2015-10-25 03:21:50 -07:00
}
client.MsgChannelKeepalive.Done()
2015-10-25 03:21:50 -07:00
}
func HandleSurvey(conn *websocket.Conn, client *ClientInfo, msg ClientMessage) (rmsg ClientMessage, err error) {
// Discard
return ResponseSuccess, nil
}
2015-10-25 03:21:50 -07:00
type FollowEvent struct {
2015-11-05 23:24:35 -08:00
User string `json:"u"`
Channel string `json:"c"`
NowFollowing bool `json:"f"`
Timestamp time.Time `json:"t"`
2015-10-25 03:21:50 -07:00
}
2015-10-25 03:21:50 -07:00
var FollowEvents []FollowEvent
var FollowEventsLock sync.Mutex
func HandleTrackFollow(conn *websocket.Conn, client *ClientInfo, msg ClientMessage) (rmsg ClientMessage, err error) {
2015-10-25 03:21:50 -07:00
channel, following, err := msg.ArgumentsAsStringAndBool()
if err != nil {
return
}
now := time.Now()
FollowEventsLock.Lock()
FollowEvents = append(FollowEvents, FollowEvent{client.TwitchUsername, channel, following, now})
FollowEventsLock.Unlock()
return ResponseSuccess, nil
}
2015-11-15 18:43:34 -08:00
// AggregateEmoteUsage is a map from emoteID to a map from chatroom name to usage count.
2015-10-25 00:58:05 -07:00
var AggregateEmoteUsage map[int]map[string]int = make(map[int]map[string]int)
2015-11-15 18:43:34 -08:00
// AggregateEmoteUsageLock is the lock for AggregateEmoteUsage.
2015-10-25 00:58:05 -07:00
var AggregateEmoteUsageLock sync.Mutex
2015-11-15 18:43:34 -08:00
// ErrNegativeEmoteUsage is emitted when the submitted emote usage is negative.
var ErrNegativeEmoteUsage = errors.New("Emote usage count cannot be negative")
2015-10-25 00:58:05 -07:00
func HandleEmoticonUses(conn *websocket.Conn, client *ClientInfo, msg ClientMessage) (rmsg ClientMessage, err error) {
2015-11-15 18:43:34 -08:00
// arguments is [1]map[emoteID]map[ChatroomName]float64
2015-10-25 00:58:05 -07:00
mapRoot := msg.Arguments.([]interface{})[0].(map[string]interface{})
for strEmote, val1 := range mapRoot {
_, err = strconv.Atoi(strEmote)
if err != nil {
return
}
mapInner := val1.(map[string]interface{})
for _, val2 := range mapInner {
var count int = int(val2.(float64))
if count <= 0 {
2015-11-15 18:43:34 -08:00
err = ErrNegativeEmoteUsage
return
}
}
}
2015-10-25 00:58:05 -07:00
AggregateEmoteUsageLock.Lock()
defer AggregateEmoteUsageLock.Unlock()
for strEmote, val1 := range mapRoot {
2015-11-15 18:43:34 -08:00
var emoteID int
emoteID, err = strconv.Atoi(strEmote)
2015-10-25 00:58:05 -07:00
if err != nil {
return
}
2015-11-15 18:43:34 -08:00
destMapInner, ok := AggregateEmoteUsage[emoteID]
2015-10-25 00:58:05 -07:00
if !ok {
destMapInner = make(map[string]int)
2015-11-15 18:43:34 -08:00
AggregateEmoteUsage[emoteID] = destMapInner
2015-10-25 00:58:05 -07:00
}
mapInner := val1.(map[string]interface{})
for roomName, val2 := range mapInner {
var count int = int(val2.(float64))
if count > 200 {
count = 200
}
2015-10-25 00:58:05 -07:00
destMapInner[roomName] += count
}
}
return ResponseSuccess, nil
}
func sendAggregateData() {
for {
2015-10-28 23:27:04 -07:00
time.Sleep(5 * time.Minute)
DoSendAggregateData()
}
}
func DoSendAggregateData() {
FollowEventsLock.Lock()
follows := FollowEvents
FollowEvents = nil
FollowEventsLock.Unlock()
AggregateEmoteUsageLock.Lock()
emoteUsage := AggregateEmoteUsage
AggregateEmoteUsage = make(map[int]map[string]int)
AggregateEmoteUsageLock.Unlock()
reportForm := url.Values{}
2015-11-15 18:43:34 -08:00
followJSON, err := json.Marshal(follows)
if err != nil {
log.Print(err)
} else {
2015-11-15 18:43:34 -08:00
reportForm.Set("follows", string(followJSON))
}
strEmoteUsage := make(map[string]map[string]int)
2015-11-15 18:43:34 -08:00
for emoteID, usageByChannel := range emoteUsage {
strEmoteID := strconv.Itoa(emoteID)
strEmoteUsage[strEmoteID] = usageByChannel
}
2015-11-15 18:43:34 -08:00
emoteJSON, err := json.Marshal(strEmoteUsage)
if err != nil {
log.Print(err)
} else {
2015-11-15 18:43:34 -08:00
reportForm.Set("emotes", string(emoteJSON))
}
form, err := SealRequest(reportForm)
if err != nil {
log.Print(err)
return
}
err = SendAggregatedData(form)
if err != nil {
log.Print(err)
return
}
// done
}
type BunchedRequest struct {
Command Command
2015-11-01 13:17:35 -08:00
Param string
}
2015-11-01 13:17:35 -08:00
func BunchedRequestFromCM(msg *ClientMessage) BunchedRequest {
return BunchedRequest{Command: msg.Command, Param: msg.origArguments}
}
2015-11-01 13:17:35 -08:00
2015-11-15 15:55:20 -08:00
type CachedBunchedResponse struct {
2015-11-01 13:17:35 -08:00
Response string
Timestamp time.Time
}
type BunchSubscriber struct {
2015-11-01 13:17:35 -08:00
Client *ClientInfo
MessageID int
}
type BunchSubscriberList struct {
sync.Mutex
Members []BunchSubscriber
}
type CacheStatus byte
2015-11-15 18:43:34 -08:00
const (
CacheStatusNotFound = iota
CacheStatusFound
CacheStatusExpired
)
2015-11-01 13:17:35 -08:00
var PendingBunchedRequests map[BunchedRequest]*BunchSubscriberList = make(map[BunchedRequest]*BunchSubscriberList)
2015-11-02 22:59:38 -08:00
var PendingBunchLock sync.Mutex
2015-11-15 15:55:20 -08:00
var BunchCache map[BunchedRequest]CachedBunchedResponse = make(map[BunchedRequest]CachedBunchedResponse)
var BunchCacheLock sync.RWMutex
var BunchCacheCleanupSignal *sync.Cond = sync.NewCond(&BunchCacheLock)
var BunchCacheLastCleanup time.Time
func bunchCacheJanitor() {
go func() {
for {
2015-11-15 18:43:34 -08:00
time.Sleep(30 * time.Minute)
BunchCacheCleanupSignal.Signal()
}
}()
2015-11-15 15:55:20 -08:00
BunchCacheLock.Lock()
for {
// Unlocks CachedBunchLock, waits for signal, re-locks
BunchCacheCleanupSignal.Wait()
2015-11-15 18:43:34 -08:00
if BunchCacheLastCleanup.After(time.Now().Add(-1 * time.Second)) {
// skip if it's been less than 1 second
continue
}
// CachedBunchLock is held here
2015-11-15 18:43:34 -08:00
keepIfAfter := time.Now().Add(-5 * time.Minute)
2015-11-15 15:55:20 -08:00
for req, resp := range BunchCache {
if !resp.Timestamp.After(keepIfAfter) {
2015-11-15 15:55:20 -08:00
delete(BunchCache, req)
}
}
BunchCacheLastCleanup = time.Now()
// Loop and Wait(), which re-locks
}
}
2015-11-01 13:17:35 -08:00
2015-11-02 22:59:38 -08:00
func HandleBunchedRemoteCommand(conn *websocket.Conn, client *ClientInfo, msg ClientMessage) (rmsg ClientMessage, err error) {
br := BunchedRequestFromCM(&msg)
cacheStatus := func() byte {
2015-11-15 15:55:20 -08:00
BunchCacheLock.RLock()
defer BunchCacheLock.RUnlock()
bresp, ok := BunchCache[br]
if ok && bresp.Timestamp.After(time.Now().Add(-5*time.Minute)) {
client.MsgChannelKeepalive.Add(1)
go func() {
var rmsg ClientMessage
rmsg.Command = SuccessCommand
rmsg.MessageID = msg.MessageID
rmsg.origArguments = bresp.Response
rmsg.parseOrigArguments()
client.MessageChannel <- rmsg
client.MsgChannelKeepalive.Done()
}()
return CacheStatusFound
} else if ok {
return CacheStatusExpired
}
return CacheStatusNotFound
}()
if cacheStatus == CacheStatusFound {
return ClientMessage{Command: AsyncResponseCommand}, nil
} else if cacheStatus == CacheStatusExpired {
// Wake up the lazy janitor
BunchCacheCleanupSignal.Signal()
}
2015-11-02 22:59:38 -08:00
PendingBunchLock.Lock()
defer PendingBunchLock.Unlock()
list, ok := PendingBunchedRequests[br]
if ok {
list.Lock()
AddToSliceB(&list.Members, client, msg.MessageID)
list.Unlock()
return ClientMessage{Command: AsyncResponseCommand}, nil
}
2015-11-02 22:59:38 -08:00
PendingBunchedRequests[br] = &BunchSubscriberList{Members: []BunchSubscriber{{Client: client, MessageID: msg.MessageID}}}
2015-11-02 22:59:38 -08:00
go func(request BunchedRequest) {
respStr, err := SendRemoteCommandCached(string(request.Command), request.Param, AuthInfo{})
2015-11-02 22:59:38 -08:00
var msg ClientMessage
if err == nil {
msg.Command = SuccessCommand
msg.origArguments = respStr
msg.parseOrigArguments()
2015-11-02 22:59:38 -08:00
} else {
msg.Command = ErrorCommand
msg.Arguments = err.Error()
}
if err == nil {
2015-11-15 15:55:20 -08:00
BunchCacheLock.Lock()
BunchCache[request] = CachedBunchedResponse{Response: respStr, Timestamp: time.Now()}
BunchCacheLock.Unlock()
}
PendingBunchLock.Lock()
2015-11-02 22:59:38 -08:00
bsl := PendingBunchedRequests[request]
delete(PendingBunchedRequests, request)
PendingBunchLock.Unlock()
2015-11-02 22:59:38 -08:00
bsl.Lock()
for _, member := range bsl.Members {
msg.MessageID = member.MessageID
2015-11-03 16:44:42 -08:00
select {
case member.Client.MessageChannel <- msg:
case <-member.Client.MsgChannelIsDone:
}
2015-11-02 22:59:38 -08:00
}
bsl.Unlock()
}(br)
return ClientMessage{Command: AsyncResponseCommand}, nil
}
func HandleRemoteCommand(conn *websocket.Conn, client *ClientInfo, msg ClientMessage) (rmsg ClientMessage, err error) {
client.MsgChannelKeepalive.Add(1)
2015-11-08 16:44:16 -08:00
go doRemoteCommand(conn, msg, client)
return ClientMessage{Command: AsyncResponseCommand}, nil
}
2015-11-08 16:44:16 -08:00
const AuthorizationFailedErrorString = "Failed to verify your Twitch username."
func doRemoteCommand(conn *websocket.Conn, msg ClientMessage, client *ClientInfo) {
2015-11-08 22:01:32 -08:00
resp, err := SendRemoteCommandCached(string(msg.Command), msg.origArguments, client.AuthInfo)
2015-11-08 16:44:16 -08:00
2015-11-15 18:43:34 -08:00
if err == ErrAuthorizationNeeded {
2015-11-08 16:44:16 -08:00
client.StartAuthorization(func(_ *ClientInfo, success bool) {
if success {
doRemoteCommand(conn, msg, client)
} else {
client.MessageChannel <- ClientMessage{MessageID: msg.MessageID, Command: ErrorCommand, Arguments: AuthorizationFailedErrorString}
client.MsgChannelKeepalive.Done()
}
})
return // without keepalive.Done()
} else if err != nil {
client.MessageChannel <- ClientMessage{MessageID: msg.MessageID, Command: ErrorCommand, Arguments: err.Error()}
} else {
msg := ClientMessage{MessageID: msg.MessageID, Command: SuccessCommand, origArguments: resp}
msg.parseOrigArguments()
client.MessageChannel <- msg
}
client.MsgChannelKeepalive.Done()
}