1
0
Fork 0
mirror of https://github.com/FrankerFaceZ/FrankerFaceZ.git synced 2025-07-01 16:48:32 +00:00

rename listener package to internal/server

This commit is contained in:
Kane York 2015-10-25 00:44:25 -07:00
parent 93c3f6f672
commit 2a6c36bba5
9 changed files with 346 additions and 284 deletions

View file

@ -1,2 +0,0 @@
package broadcaster

View file

@ -2,7 +2,7 @@ package main // import "bitbucket.org/stendec/frankerfacez/socketserver/cmd/sock
import ( import (
"flag" "flag"
"../../listener" "../../internal/server"
"log" "log"
"net/http" "net/http"
) )
@ -25,15 +25,15 @@ func main() {
log.Fatalln("Either both --crt and --key can be provided, or neither.") log.Fatalln("Either both --crt and --key can be provided, or neither.")
} }
conf := &listener.Config { conf := &server.Config {
SSLKeyFile: *privateKeyFile, SSLKeyFile: *privateKeyFile,
SSLCertificateFile: *certificateFile, SSLCertificateFile: *certificateFile,
UseSSL: *certificateFile != "", UseSSL: *certificateFile != "",
Origin: *origin, SocketOrigin: *origin,
} }
listener.SetupServerAndHandle(conf) server.SetupServerAndHandle(conf)
var err error var err error
if conf.UseSSL { if conf.UseSSL {

View file

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

View file

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

View file

@ -1,4 +1,4 @@
package listener // import "bitbucket.org/stendec/frankerfacez/socketserver/listener" package server // import "bitbucket.org/stendec/frankerfacez/socketserver/server"
import ( import (
"net/http" "net/http"
@ -9,7 +9,6 @@ import (
"strconv" "strconv"
"errors" "errors"
"encoding/json" "encoding/json"
"github.com/satori/go.uuid"
"fmt" "fmt"
"sync" "sync"
) )
@ -21,78 +20,39 @@ type Config struct {
SSLKeyFile string SSLKeyFile string
UseSSL bool UseSSL bool
Origin string SocketOrigin string
} }
// A command is how the client refers to a function on the server. It's just a string. // A command is how the client refers to a function on the server. It's just a string.
type Command 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. // A function that is called to respond to a Command.
type CommandHandler func(*websocket.Conn, *ClientInfo, ClientMessage) (ClientMessage, error) type CommandHandler func(*websocket.Conn, *ClientInfo, ClientMessage) (ClientMessage, error)
var CommandHandlers = map[Command]CommandHandler{ var CommandHandlers = map[Command]CommandHandler{
HelloCommand: HandleHello, HelloCommand: HandleHello,
"get_display_name": HandleGetDisplayName, "setuser": HandleSetUser,
"sub": HandleSub, "sub": HandleSub,
"unsub": HandleUnsub, "unsub": HandleUnsub,
"chat_history": HandleChatHistory,
"sub_channel": HandleSubChannel, "sub_channel": HandleSubChannel,
"unsub_channel": HandleUnsubChannel, "unsub_channel": HandleUnsubChannel,
"setuser": HandleSetUser,
"update_follow_buttons": HandleUpdateFollowButtons,
"track_follow": HandleTrackFollow, "track_follow": HandleTrackFollow,
"emoticon_uses": HandleEmoticonUses, "emoticon_uses": HandleEmoticonUses,
"twitch_emote": HandleTwitchEmote,
"get_link": HandleGetLink,
"survey": HandleSurvey, "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. // Sent by the server in ClientMessage.Command to indicate success.
const SuccessCommand Command = "True" 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. // This must be the first command sent by the client once the connection is established.
const HelloCommand Command = "hello" const HelloCommand Command = "hello"
// A handler returning a ClientMessage with this Command will prevent replying to the client. // 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. // Errors that get returned to the client.
var ProtocolError error = errors.New("FFZ Socket protocol error.") var ProtocolError error = errors.New("FFZ Socket protocol error.")
var ExpectedSingleString = errors.New("Error: Expected single string as arguments.") 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 ExpectedTwoStrings = errors.New("Error: Expected array of string, string as arguments.")
var ExpectedStringAndInt = errors.New("Error: Expected array of string, int 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.") var ExpectedStringAndIntGotFloat = errors.New("Error: Second argument was a float, expected an integer.")
// Create a websocket.Server with the options from the provided Config. // Create a websocket.Server with the options from the provided Config.
func SetupServer(config *Config) *websocket.Server { func SetupServer(config *Config) *websocket.Server {
sockConf, err := websocket.NewConfig("/", config.Origin) sockConf, err := websocket.NewConfig("/", config.SocketOrigin)
if err != nil { if err != nil {
panic(err) panic(err)
} }
@ -125,7 +87,7 @@ func SetupServer(config *Config) *websocket.Server {
} }
tlsConf := &tls.Config{ tlsConf := &tls.Config{
Certificates: []tls.Certificate{cert}, Certificates: []tls.Certificate{cert},
ServerName: config.Origin, ServerName: config.SocketOrigin,
} }
tlsConf.BuildNameToCertificate() tlsConf.BuildNameToCertificate()
sockConf.TlsConfig = tlsConf sockConf.TlsConfig = tlsConf
@ -210,33 +172,7 @@ func HandleSocketConnection(conn *websocket.Conn) {
break RunLoop break RunLoop
} }
handler, ok := CommandHandlers[msg.Command] HandleCommand(conn, &client, msg)
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(),
})
}
case smsg := <-serverMessageChan: case smsg := <-serverMessageChan:
FFZCodec.Send(conn, smsg) FFZCodec.Send(conn, smsg)
} }
@ -288,6 +224,7 @@ func UnmarshalClientMessage(data []byte, payloadType byte, v interface{}) (err e
} }
dataStr = dataStr[spaceIdx + 1:] dataStr = dataStr[spaceIdx + 1:]
argumentsJson := dataStr argumentsJson := dataStr
out.origArguments = argumentsJson
err = json.Unmarshal([]byte(argumentsJson), &out.Arguments) err = json.Unmarshal([]byte(argumentsJson), &out.Arguments)
if err != nil { if err != nil {
return 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. // Convenience method: Parse the arguments of the ClientMessage as an array of two strings.
func (cm *ClientMessage) ArgumentsAsTwoStrings() (string1, string2 string, err error) { func (cm *ClientMessage) ArgumentsAsTwoStrings() (string1, string2 string, err error) {
var ok bool var ok bool
@ -403,3 +353,26 @@ func (cm *ClientMessage) ArgumentsAsStringAndInt() (string1 string, int int64, e
return string1, int, nil 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
}
}

View file

@ -1,8 +1,9 @@
package listener package server
import ( import (
"golang.org/x/net/websocket" "golang.org/x/net/websocket"
"fmt" "fmt"
"testing"
) )
func ExampleUnmarshalClientMessage() { func ExampleUnmarshalClientMessage() {
@ -35,3 +36,22 @@ func ExampleMarshalClientMessage() {
// true // true
// -1 do_authorize "1234567890" // -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)
}
}

View file

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

View file

@ -1,4 +1,4 @@
package lib package server
import ( import (
) )

View file

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