diff --git a/socketserver/broadcaster/subscriptions.go b/socketserver/broadcaster/subscriptions.go deleted file mode 100644 index ee810828..00000000 --- a/socketserver/broadcaster/subscriptions.go +++ /dev/null @@ -1,2 +0,0 @@ -package broadcaster - diff --git a/socketserver/cmd/socketserver/socketserver.go b/socketserver/cmd/socketserver/socketserver.go index fb75c2e9..0ab2a278 100644 --- a/socketserver/cmd/socketserver/socketserver.go +++ b/socketserver/cmd/socketserver/socketserver.go @@ -2,7 +2,7 @@ package main // import "bitbucket.org/stendec/frankerfacez/socketserver/cmd/sock import ( "flag" - "../../listener" + "../../internal/server" "log" "net/http" ) @@ -25,15 +25,15 @@ func main() { log.Fatalln("Either both --crt and --key can be provided, or neither.") } - conf := &listener.Config { + conf := &server.Config { SSLKeyFile: *privateKeyFile, SSLCertificateFile: *certificateFile, UseSSL: *certificateFile != "", - Origin: *origin, + SocketOrigin: *origin, } - listener.SetupServerAndHandle(conf) + server.SetupServerAndHandle(conf) var err error if conf.UseSSL { diff --git a/socketserver/internal/server/backend.go b/socketserver/internal/server/backend.go new file mode 100644 index 00000000..ba8b4f06 --- /dev/null +++ b/socketserver/internal/server/backend.go @@ -0,0 +1,72 @@ +package server + +import ( + "net/http" + "time" + "fmt" + "net/url" + "github.com/pmylund/go-cache" + "strconv" + "io/ioutil" +) + +var httpClient http.Client +var backendUrl string +var responseCache *cache.Cache + +func SetupBackend(url string) { + httpClient.Timeout = 60 * time.Second + backendUrl = url + responseCache = cache.New(60 * time.Second, 120 * time.Second) +} + +func getCacheKey(remoteCommand, data string) string { + return fmt.Sprintf("%s/%s", remoteCommand, data) +} + +func RequestRemoteDataCached(remoteCommand, data string, auth AuthInfo) (string, error) { + cached, ok := responseCache.Get(getCacheKey(remoteCommand, data)) + if ok { + return cached.(string), nil + } + return RequestRemoteData(remoteCommand, data, auth) +} + +func RequestRemoteData(remoteCommand, data string, auth AuthInfo) (string, error) { + destUrl := fmt.Sprintf("%s/%s", backendUrl, remoteCommand) + var authKey string + if auth.UsernameValidated { + authKey = "usernameClaimed" + } else { + authKey = "username" + } + + formData := url.Values{ + "clientData": []string{data}, + authKey: []string{auth.TwitchUsername}, + } + + resp, err := httpClient.PostForm(destUrl, formData) + if err != nil { + return "", err + } + + respBytes, err := ioutil.ReadAll(resp.Body) + resp.Body.Close() + if err != nil { + return "", err + } + + responseJson := string(respBytes) + + if resp.Header.Get("FFZ-Cache") != "" { + durSecs, err := strconv.ParseInt(resp.Header.Get("FFZ-Cache"), 10, 64) + if err != nil { + return "", fmt.Errorf("The RPC server returned a non-integer cache duration: %v", err) + } + duration := time.Duration(durSecs) * time.Second + responseCache.Set(getCacheKey(remoteCommand, data), responseJson, duration) + } + + return responseJson, nil +} \ No newline at end of file diff --git a/socketserver/internal/server/commands.go b/socketserver/internal/server/commands.go new file mode 100644 index 00000000..2b4d0c43 --- /dev/null +++ b/socketserver/internal/server/commands.go @@ -0,0 +1,134 @@ +package server + +import ( + "golang.org/x/net/websocket" + "github.com/satori/go.uuid" + "log" +) + +var ResponseSuccess = ClientMessage{Command: SuccessCommand} +var ResponseFailure = ClientMessage{Command: "False"} + +func HandleCommand(conn *websocket.Conn, client *ClientInfo, msg ClientMessage) { + handler, ok := CommandHandlers[msg.Command] + if !ok { + log.Print("[!] Unknown command", msg.Command, "- sent by client", client.ClientID, "@", conn.RemoteAddr()) + // uncomment after commands are implemented + // closer() + return + } + + log.Println(conn.RemoteAddr(), msg.MessageID, msg.Command, msg.Arguments) + + client.Mutex.Lock() + response, err := CallHandler(handler, conn, client, msg) + client.Mutex.Unlock() + + if err == nil { + response.MessageID = msg.MessageID + FFZCodec.Send(conn, response) + } else if response.Command == AsyncResponseCommand { + // Don't send anything + // The response will be delivered over client.MessageChannel / serverMessageChan + } else { + FFZCodec.Send(conn, ClientMessage{ + MessageID: msg.MessageID, + Command: "error", + Arguments: err.Error(), + }) + } +} + +func HandleHello(conn *websocket.Conn, client *ClientInfo, msg ClientMessage) (rmsg ClientMessage, err error) { + version, clientId, err := msg.ArgumentsAsTwoStrings() + if err != nil { + return + } + + client.Version = version + client.ClientID = uuid.FromStringOrNil(clientId) + if client.ClientID == uuid.Nil { + client.ClientID = uuid.NewV4() + } + + return ClientMessage{ + Arguments: client.ClientID.String(), + }, nil +} + +func HandleSetUser(conn *websocket.Conn, client *ClientInfo, msg ClientMessage) (rmsg ClientMessage, err error) { + username, err := msg.ArgumentsAsString() + if err != nil { + return + } + + client.TwitchUsername = username + client.UsernameValidated = false + + return ResponseSuccess, nil +} + +func HandleSub(conn *websocket.Conn, client *ClientInfo, msg ClientMessage) (rmsg ClientMessage, err error) { + channel, err := msg.ArgumentsAsString() + + AddToSliceS(&client.CurrentChannels, channel) + + // TODO - get backlog + + return ResponseSuccess, nil +} + +func HandleUnsub(conn *websocket.Conn, client *ClientInfo, msg ClientMessage) (rmsg ClientMessage, err error) { + channel, err := msg.ArgumentsAsString() + + RemoveFromSliceS(&client.CurrentChannels, channel) + + return ResponseSuccess, nil +} + +func HandleSubChannel(conn *websocket.Conn, client *ClientInfo, msg ClientMessage) (rmsg ClientMessage, err error) { + channel, err := msg.ArgumentsAsString() + + AddToSliceS(&client.WatchingChannels, channel) + + // TODO - get backlog + + return ResponseSuccess, nil +} + +func HandleUnsubChannel(conn *websocket.Conn, client *ClientInfo, msg ClientMessage) (rmsg ClientMessage, err error) { + channel, err := msg.ArgumentsAsString() + + RemoveFromSliceS(&client.WatchingChannels, channel) + + return ResponseSuccess, nil +} + +func HandleSurvey(conn *websocket.Conn, client *ClientInfo, msg ClientMessage) (rmsg ClientMessage, err error) { + log.Println("Ignoring survey response from", client.ClientID) + return ResponseSuccess, nil +} + +func HandleTrackFollow(conn *websocket.Conn, client *ClientInfo, msg ClientMessage) (rmsg ClientMessage, err error) { + + return ResponseSuccess, nil +} + +func HandleEmoticonUses(conn *websocket.Conn, client *ClientInfo, msg ClientMessage) (rmsg ClientMessage, err error) { + + return ResponseSuccess, nil +} + +func HandleRemoteCommand(conn *websocket.Conn, client *ClientInfo, msg ClientMessage) (rmsg ClientMessage, err error) { + go func(conn *websocket.Conn, msg ClientMessage, authInfo AuthInfo) { + resp, err := RequestRemoteDataCached(string(msg.Command), msg.origArguments, authInfo) + + if err != nil { + FFZCodec.Send(conn, ClientMessage{MessageID: msg.MessageID, Command: ErrorCommand, Arguments: err.Error()}) + } else { + FFZCodec.Send(conn, ClientMessage{MessageID: msg.MessageID, Command: SuccessCommand, origArguments: resp}) + } + }(conn, msg, client.AuthInfo) + + return ClientMessage{Command: AsyncResponseCommand}, nil +} diff --git a/socketserver/listener/handlecore.go b/socketserver/internal/server/handlecore.go similarity index 75% rename from socketserver/listener/handlecore.go rename to socketserver/internal/server/handlecore.go index 1bb1a1c3..07abc425 100644 --- a/socketserver/listener/handlecore.go +++ b/socketserver/internal/server/handlecore.go @@ -1,4 +1,4 @@ -package listener // import "bitbucket.org/stendec/frankerfacez/socketserver/listener" +package server // import "bitbucket.org/stendec/frankerfacez/socketserver/server" import ( "net/http" @@ -9,7 +9,6 @@ import ( "strconv" "errors" "encoding/json" - "github.com/satori/go.uuid" "fmt" "sync" ) @@ -21,78 +20,39 @@ type Config struct { SSLKeyFile string UseSSL bool - Origin string + SocketOrigin string } // A command is how the client refers to a function on the server. It's just a string. type Command string -type ClientMessage struct { - // Message ID. Increments by 1 for each message sent from the client. - // When replying to a command, the message ID must be echoed. - // When sending a server-initiated message, this is -1. - MessageID int - // The command that the client wants from the server. - // When sent from the server, the literal string 'True' indicates success. - // Before sending, a blank Command will be converted into SuccessCommand. - Command Command - // - Arguments interface{} -} - -type ClientInfo struct { - // The client ID. - // This must be written once by the owning goroutine before the struct is passed off to any other goroutines. - ClientID uuid.UUID - - // The client's version. - // This must be written once by the owning goroutine before the struct is passed off to any other goroutines. - Version string - - // This mutex protects writable data in this struct. - // If it seems to be a performance problem, we can split this. - Mutex sync.Mutex - - // The client's claimed username on Twitch. - TwitchUsername string - - // Whether or not the server has validated the client's claimed username. - UsernameValidated bool - - // The list of chats this client is currently in. - // Protected by Mutex - CurrentChannels []string - - // This list of channels this client needs UI updates for. - // Protected by Mutex - WatchingChannels []string - - // Server-initiated messages should be sent here - MessageChannel chan <- ClientMessage -} - // A function that is called to respond to a Command. type CommandHandler func(*websocket.Conn, *ClientInfo, ClientMessage) (ClientMessage, error) var CommandHandlers = map[Command]CommandHandler{ HelloCommand: HandleHello, - "get_display_name": HandleGetDisplayName, + "setuser": HandleSetUser, + "sub": HandleSub, "unsub": HandleUnsub, - "chat_history": HandleChatHistory, "sub_channel": HandleSubChannel, "unsub_channel": HandleUnsubChannel, - "setuser": HandleSetUser, - "update_follow_buttons": HandleUpdateFollowButtons, + "track_follow": HandleTrackFollow, "emoticon_uses": HandleEmoticonUses, - "twitch_emote": HandleTwitchEmote, - "get_link": HandleGetLink, "survey": HandleSurvey, + + "twitch_emote": HandleRemoteCommand, + "get_link": HandleRemoteCommand, + "get_display_name": HandleRemoteCommand, + "update_follow_buttons": HandleRemoteCommand, + "chat_history": HandleRemoteCommand, } // Sent by the server in ClientMessage.Command to indicate success. const SuccessCommand Command = "True" +// Sent by the server in ClientMessage.Command to indicate failure. +const ErrorCommand Command = "error" // This must be the first command sent by the client once the connection is established. const HelloCommand Command = "hello" // A handler returning a ClientMessage with this Command will prevent replying to the client. @@ -108,13 +68,15 @@ var FFZCodec websocket.Codec = websocket.Codec{ // Errors that get returned to the client. var ProtocolError error = errors.New("FFZ Socket protocol error.") var ExpectedSingleString = errors.New("Error: Expected single string as arguments.") +var ExpectedSingleInt = errors.New("Error: Expected single integer as arguments.") var ExpectedTwoStrings = errors.New("Error: Expected array of string, string as arguments.") var ExpectedStringAndInt = errors.New("Error: Expected array of string, int as arguments.") +var ExpectedStringAndBool = errors.New("Error: Expected array of string, bool as arguments.") var ExpectedStringAndIntGotFloat = errors.New("Error: Second argument was a float, expected an integer.") // Create a websocket.Server with the options from the provided Config. func SetupServer(config *Config) *websocket.Server { - sockConf, err := websocket.NewConfig("/", config.Origin) + sockConf, err := websocket.NewConfig("/", config.SocketOrigin) if err != nil { panic(err) } @@ -125,7 +87,7 @@ func SetupServer(config *Config) *websocket.Server { } tlsConf := &tls.Config{ Certificates: []tls.Certificate{cert}, - ServerName: config.Origin, + ServerName: config.SocketOrigin, } tlsConf.BuildNameToCertificate() sockConf.TlsConfig = tlsConf @@ -210,33 +172,7 @@ func HandleSocketConnection(conn *websocket.Conn) { break RunLoop } - handler, ok := CommandHandlers[msg.Command] - if !ok { - log.Print("[!] Unknown command", msg.Command, "- sent by client", client.ClientID, "@", conn.RemoteAddr()) - // uncomment after commands are implemented - // closer() - continue - } - - log.Println(conn.RemoteAddr(), msg.MessageID, msg.Command, msg.Arguments) - - client.Mutex.Lock() - response, err := CallHandler(handler, conn, &client, msg) - client.Mutex.Unlock() - - if err == nil { - response.MessageID = msg.MessageID - FFZCodec.Send(conn, response) - } else if response.Command == AsyncResponseCommand { - // Don't send anything - // The response will be delivered over client.MessageChannel / serverMessageChan - } else { - FFZCodec.Send(conn, ClientMessage{ - MessageID: msg.MessageID, - Command: "error", - Arguments: err.Error(), - }) - } + HandleCommand(conn, &client, msg) case smsg := <-serverMessageChan: FFZCodec.Send(conn, smsg) } @@ -288,6 +224,7 @@ func UnmarshalClientMessage(data []byte, payloadType byte, v interface{}) (err e } dataStr = dataStr[spaceIdx + 1:] argumentsJson := dataStr + out.origArguments = argumentsJson err = json.Unmarshal([]byte(argumentsJson), &out.Arguments) if err != nil { return @@ -353,6 +290,19 @@ func (cm *ClientMessage) ArgumentsAsString() (string1 string, err error) { } } +// Convenience method: Parse the arguments of the ClientMessage as a single int. +func (cm *ClientMessage) ArgumentsAsInt() (int1 int, err error) { + var ok bool + var num float64 + num, ok = cm.Arguments.(float64) + if !ok { + err = ExpectedSingleInt; return + } else { + int1 = int(num) + return int1, nil + } +} + // Convenience method: Parse the arguments of the ClientMessage as an array of two strings. func (cm *ClientMessage) ArgumentsAsTwoStrings() (string1, string2 string, err error) { var ok bool @@ -403,3 +353,26 @@ func (cm *ClientMessage) ArgumentsAsStringAndInt() (string1 string, int int64, e return string1, int, nil } } + +// Convenience method: Parse the arguments of the ClientMessage as an array of a string and an int. +func (cm *ClientMessage) ArgumentsAsStringAndBool() (str string, flag bool, err error) { + var ok bool + var ary []interface{} + ary, ok = cm.Arguments.([]interface{}) + if !ok { + err = ExpectedStringAndBool; return + } else { + if len(ary) != 2 { + err = ExpectedStringAndBool; return + } + str, ok = ary[0].(string) + if !ok { + err = ExpectedStringAndBool; return + } + flag, ok = ary[1].(bool) + if !ok { + err = ExpectedStringAndBool; return + } + return str, flag, nil + } +} diff --git a/socketserver/listener/handlecore_test.go b/socketserver/internal/server/handlecore_test.go similarity index 61% rename from socketserver/listener/handlecore_test.go rename to socketserver/internal/server/handlecore_test.go index f5a0648f..0350a7e3 100644 --- a/socketserver/listener/handlecore_test.go +++ b/socketserver/internal/server/handlecore_test.go @@ -1,8 +1,9 @@ -package listener +package server import ( "golang.org/x/net/websocket" "fmt" + "testing" ) func ExampleUnmarshalClientMessage() { @@ -35,3 +36,22 @@ func ExampleMarshalClientMessage() { // true // -1 do_authorize "1234567890" } + +func TestArgumentsAsStringAndBool(t *testing.T) { + sourceData := []byte("1 foo [\"string\", false]") + var cm ClientMessage + err := UnmarshalClientMessage(sourceData, websocket.TextFrame, &cm) + if err != nil { + t.Fatal(err) + } + str, boolean, err := cm.ArgumentsAsStringAndBool() + if err != nil { + t.Fatal(err) + } + if str != "string" { + t.Error("Expected first array item to be 'string', got", str) + } + if boolean != false { + t.Error("Expected second array item to be false, got", boolean) + } +} \ No newline at end of file diff --git a/socketserver/internal/server/types.go b/socketserver/internal/server/types.go new file mode 100644 index 00000000..3559de81 --- /dev/null +++ b/socketserver/internal/server/types.go @@ -0,0 +1,59 @@ +package server + +import ( + "github.com/satori/go.uuid" + "sync" +) + +type ClientMessage struct { + // Message ID. Increments by 1 for each message sent from the client. + // When replying to a command, the message ID must be echoed. + // When sending a server-initiated message, this is -1. + MessageID int + // The command that the client wants from the server. + // When sent from the server, the literal string 'True' indicates success. + // Before sending, a blank Command will be converted into SuccessCommand. + Command Command + // Result of json.Unmarshal on the third field send from the client + Arguments interface{} + + origArguments string +} + +type AuthInfo struct { + // The client's claimed username on Twitch. + TwitchUsername string + + // Whether or not the server has validated the client's claimed username. + UsernameValidated bool +} + +type ClientInfo struct { + // The client ID. + // This must be written once by the owning goroutine before the struct is passed off to any other goroutines. + ClientID uuid.UUID + + // The client's version. + // This must be written once by the owning goroutine before the struct is passed off to any other goroutines. + Version string + + // This mutex protects writable data in this struct. + // If it seems to be a performance problem, we can split this. + Mutex sync.Mutex + + AuthInfo + + // Username validation nonce. + ValidationNonce string + + // The list of chats this client is currently in. + // Protected by Mutex + CurrentChannels []string + + // This list of channels this client needs UI updates for. + // Protected by Mutex + WatchingChannels []string + + // Server-initiated messages should be sent here + MessageChannel chan <- ClientMessage +} \ No newline at end of file diff --git a/socketserver/lib/utils.go b/socketserver/internal/server/utils.go similarity index 96% rename from socketserver/lib/utils.go rename to socketserver/internal/server/utils.go index 1b9af86c..65fa9cae 100644 --- a/socketserver/lib/utils.go +++ b/socketserver/internal/server/utils.go @@ -1,4 +1,4 @@ -package lib +package server import ( ) diff --git a/socketserver/listener/commands.go b/socketserver/listener/commands.go deleted file mode 100644 index 06595612..00000000 --- a/socketserver/listener/commands.go +++ /dev/null @@ -1,194 +0,0 @@ -package listener - -import ( - "golang.org/x/net/websocket" - "github.com/satori/go.uuid" - "log" - "../lib" -) - -var ResponseSuccess = ClientMessage{Command: SuccessCommand} -var ResponseFailure = ClientMessage{Command: "False"} - -func HandleHello(conn *websocket.Conn, client *ClientInfo, msg ClientMessage) (rmsg ClientMessage, err error) { - version, clientId, err := msg.ArgumentsAsTwoStrings() - if err != nil { - return - } - - client.Version = version - client.ClientID = uuid.FromStringOrNil(clientId) - if client.ClientID == uuid.Nil { - client.ClientID = uuid.NewV4() - } - - return ClientMessage{ - Arguments: client.ClientID.String(), - }, nil -} - -func HandleSetUser(conn *websocket.Conn, client *ClientInfo, msg ClientMessage) (rmsg ClientMessage, err error) { - username, err := msg.ArgumentsAsString() - if err != nil { - return - } - - client.TwitchUsername = username - client.UsernameValidated = false - - return ResponseSuccess, nil -} - -func HandleSub(conn *websocket.Conn, client *ClientInfo, msg ClientMessage) (rmsg ClientMessage, err error) { - channel, err := msg.ArgumentsAsString() - - lib.AddToSliceS(&client.CurrentChannels, channel) - - return ResponseSuccess, nil -} - -func HandleUnsub(conn *websocket.Conn, client *ClientInfo, msg ClientMessage) (rmsg ClientMessage, err error) { - channel, err := msg.ArgumentsAsString() - - lib.RemoveFromSliceS(&client.CurrentChannels, channel) - - return ResponseSuccess, nil -} - -func HandleSubChannel(conn *websocket.Conn, client *ClientInfo, msg ClientMessage) (rmsg ClientMessage, err error) { - channel, err := msg.ArgumentsAsString() - - lib.AddToSliceS(&client.WatchingChannels, channel) - - return ResponseSuccess, nil -} - -func HandleUnsubChannel(conn *websocket.Conn, client *ClientInfo, msg ClientMessage) (rmsg ClientMessage, err error) { - channel, err := msg.ArgumentsAsString() - - lib.RemoveFromSliceS(&client.WatchingChannels, channel) - - return ResponseSuccess, nil -} - -func HandleChatHistory(conn *websocket.Conn, client *ClientInfo, msg ClientMessage) (rmsg ClientMessage, err error) { - channel, count, err := msg.ArgumentsAsStringAndInt() - - _ = channel - _ = count - - // Ignore, send empty history - return ClientMessage{Arguments: []string{}}, nil -} - -func HandleSurvey(conn *websocket.Conn, client *ClientInfo, msg ClientMessage) (rmsg ClientMessage, err error) { - log.Println("Ignoring survey response from", client.ClientID) - return ResponseSuccess, nil -} - -func HandleUpdateFollowButtons(conn *websocket.Conn, client *ClientInfo, msg ClientMessage) (rmsg ClientMessage, err error) { - // TODO - return ResponseFailure, nil -} - -func HandleTrackFollow(conn *websocket.Conn, client *ClientInfo, msg ClientMessage) (rmsg ClientMessage, err error) { - - return ResponseSuccess, nil -} - -func HandleEmoticonUses(conn *websocket.Conn, client *ClientInfo, msg ClientMessage) (rmsg ClientMessage, err error) { - - return ResponseSuccess, nil -} - -type EmoteData struct { - -} - -func HandleTwitchEmote(conn *websocket.Conn, client *ClientInfo, msg ClientMessage) (rmsg ClientMessage, err error) { - - return ResponseSuccess, nil -} - -type LinkResponse struct { - // If true, the link will be colored red. - Unsafe bool `json:unsafe` - // If present, the provided HTML will be shown as the link tooltip. - TooltipHTML string `json:tooltip,omitempty` - - // Each of the LinkTypes have a special rendering on the client. - Type LinkType `json:type,omitempty` - - // A URL to an image to embed. - // Recognized by several LinkTypes, as well as the empty LinkType - Full string `json:full,omitempty` - - // For LinkTypeYoutube, LinkTypeStrawpoll, LinkTypeTwitchVod - Title string `json:title,omitempty` - // For LinkTypeYoutube - Channel string `json:channel,omitempty` - // For LinkTypeYoutube - // Seconds - Duration int `json:duration,omitempty` - // For LinkTypeYoutube, LinkTypeTwitch, LinkTypeTwitchVod - Views int `json:views,omitempty` - // For LinkTypeYoutube - Likes int `json:likes,omitempty` - // For LinkTypeStrawpoll - Items map[string]int `json:items,omitempty` - // For LinkTypeStrawpoll - Total int `json:total,omitempty` - // For LinkTypeStrawpoll - // TODO - what time format is this - Fetched string `json:fetched,omitempty` - // For LinkTypeTwitch, LinkTypeTwitchVod - DisplayName string `json:display_name,omitempty` - // For LinkTypeTwitch - // TODO - what time format is this - Since string `json:since,omitempty` - // For LinkTypeTwitch - Followers int `json:followers,omitempty` - // For LinkTypeTwitchVod - BroadcastType string `json:broadcast_type,omitempty` - // For LinkTypeTwitchVod - Game string `json:game,omitempty` - // For LinkTypeTwitchVod - // Seconds - Length int `json:length,omitempty` - // For LinkTypeTwitter - User string `json:user,omitempty` - // For LinkTypeTwitter - Tweet string `json:tweet,omitempty` - // For LinkTypeReputation - Trust int `json:trust,omitempty` - // For LinkTypeReputation - Safety int `json:safety,omitempty` -} - -type LinkType string - -const ( - LinkTypeYoutube = "youtube" - LinkTypeStrawpoll = "strawpoll" - LinkTypeTwitch = "twitch" - LinkTypeTwitchVod = "twitch_vod" - LinkTypeTwitter = "twitter" - LinkTypeReputation = "reputation" - LinkTypeShortened = "shortened" // proposed -) -const ( - BroadcastTypeHighlight = "highlight" - BroadcastTypeFull = "broadcast" -) - -func HandleGetLink(conn *websocket.Conn, client *ClientInfo, msg ClientMessage) (rmsg ClientMessage, err error) { - - // TODO - return ResponseFailure, nil -} - -func HandleGetDisplayName(conn *websocket.Conn, client *ClientInfo, msg ClientMessage) (rmsg ClientMessage, err error) { - - // TODO - return ResponseFailure, nil -}