Merge branch 'master' into room-access-restrictions
# Conflicts: # common/chatcommands.go # main.go # settings.go
This commit is contained in:
commit
4e35418a79
|
@ -35,3 +35,6 @@ static/main.wasm
|
||||||
|
|
||||||
# tags for vim
|
# tags for vim
|
||||||
tags
|
tags
|
||||||
|
|
||||||
|
# channel and emote list from twitch
|
||||||
|
subscribers.json
|
||||||
|
|
|
@ -5,6 +5,7 @@ import (
|
||||||
"html"
|
"html"
|
||||||
"regexp"
|
"regexp"
|
||||||
"strings"
|
"strings"
|
||||||
|
"time"
|
||||||
"unicode"
|
"unicode"
|
||||||
|
|
||||||
"github.com/zorchenhimer/MovieNight/common"
|
"github.com/zorchenhimer/MovieNight/common"
|
||||||
|
@ -25,6 +26,36 @@ type Client struct {
|
||||||
IsColorForced bool
|
IsColorForced bool
|
||||||
IsNameForced bool
|
IsNameForced bool
|
||||||
regexName *regexp.Regexp
|
regexName *regexp.Regexp
|
||||||
|
|
||||||
|
// Times since last event. use time.Duration.Since()
|
||||||
|
nextChat time.Time // rate limit chat messages
|
||||||
|
nextNick time.Time // rate limit nickname changes
|
||||||
|
nextColor time.Time // rate limit color changes
|
||||||
|
nextAuth time.Time // rate limit failed auth attempts. Sould prolly have a backoff policy.
|
||||||
|
authTries int // number of failed auth attempts
|
||||||
|
|
||||||
|
nextDuplicate time.Time
|
||||||
|
lastMsg string
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewClient(connection *chatConnection, room *ChatRoom, name, color string) (*Client, error) {
|
||||||
|
c := &Client{
|
||||||
|
conn: connection,
|
||||||
|
belongsTo: room,
|
||||||
|
color: color,
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := c.setName(name); err != nil {
|
||||||
|
return nil, fmt.Errorf("could not set client name to %#v: %v", name, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set initial vaules to their rate limit duration in the past.
|
||||||
|
c.nextChat = time.Now()
|
||||||
|
c.nextNick = time.Now()
|
||||||
|
c.nextColor = time.Now()
|
||||||
|
c.nextAuth = time.Now()
|
||||||
|
|
||||||
|
return c, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
//Client has a new message to broadcast
|
//Client has a new message to broadcast
|
||||||
|
@ -85,11 +116,42 @@ func (cl *Client) NewMsg(data common.ClientData) {
|
||||||
}
|
}
|
||||||
|
|
||||||
} else {
|
} else {
|
||||||
|
// Limit the rate of sent chat messages. Ignore mods and admins
|
||||||
|
if time.Now().Before(cl.nextChat) && cl.CmdLevel == common.CmdlUser {
|
||||||
|
err := cl.SendChatData(common.NewChatMessage("", "",
|
||||||
|
"Slow down.",
|
||||||
|
common.CmdlUser,
|
||||||
|
common.MsgCommandResponse))
|
||||||
|
if err != nil {
|
||||||
|
common.LogErrorf("Unable to send slowdown for chat: %v", err)
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
// Trim long messages
|
// Trim long messages
|
||||||
if len(msg) > 400 {
|
if len(msg) > 400 {
|
||||||
msg = msg[0:400]
|
msg = msg[0:400]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Limit the rate of duplicate messages. Ignore mods and admins.
|
||||||
|
// Only checks the last message.
|
||||||
|
if strings.TrimSpace(strings.ToLower(msg)) == cl.lastMsg &&
|
||||||
|
time.Now().Before(cl.nextDuplicate) &&
|
||||||
|
cl.CmdLevel == common.CmdlUser {
|
||||||
|
err := cl.SendChatData(common.NewChatMessage("", "",
|
||||||
|
common.ParseEmotes("You already sent that PeepoSus"),
|
||||||
|
common.CmdlUser,
|
||||||
|
common.MsgCommandResponse))
|
||||||
|
if err != nil {
|
||||||
|
common.LogErrorf("Unable to send slowdown for chat: %v", err)
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
cl.nextChat = time.Now().Add(time.Second * settings.RateLimitChat)
|
||||||
|
cl.nextDuplicate = time.Now().Add(time.Second * settings.RateLimitDuplicate)
|
||||||
|
cl.lastMsg = strings.TrimSpace(strings.ToLower(msg))
|
||||||
|
|
||||||
common.LogChatf("[chat] <%s> %q\n", cl.name, msg)
|
common.LogChatf("[chat] <%s> %q\n", cl.name, msg)
|
||||||
|
|
||||||
// Enable links for mods and admins
|
// Enable links for mods and admins
|
||||||
|
@ -180,16 +242,23 @@ func (cl *Client) Host() string {
|
||||||
}
|
}
|
||||||
|
|
||||||
func (cl *Client) setName(s string) error {
|
func (cl *Client) setName(s string) error {
|
||||||
regex, err := regexp.Compile(fmt.Sprintf("(%s|@%s)", s, s))
|
// Case-insensitive search. Match whole words only (`\b` is word boundary).
|
||||||
|
regex, err := regexp.Compile(fmt.Sprintf(`(?i)\b(%s|@%s)\b`, s, s))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("could not compile regex: %v", err)
|
return fmt.Errorf("could not compile regex: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
cl.name = s
|
cl.name = s
|
||||||
cl.regexName = regex
|
cl.regexName = regex
|
||||||
|
cl.conn.clientName = s
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (cl *Client) setColor(s string) error {
|
||||||
|
cl.color = s
|
||||||
|
return cl.SendChatData(common.NewChatHiddenMessage(common.CdColor, cl.color))
|
||||||
|
}
|
||||||
|
|
||||||
func (cl *Client) replaceColorizedName(chatData common.ChatData) common.ChatData {
|
func (cl *Client) replaceColorizedName(chatData common.ChatData) common.ChatData {
|
||||||
data := chatData.Data.(common.DataMessage)
|
data := chatData.Data.(common.DataMessage)
|
||||||
data.Message = cl.regexName.ReplaceAllString(data.Message, `<span class="mention">$1</span>`)
|
data.Message = cl.regexName.ReplaceAllString(data.Message, `<span class="mention">$1</span>`)
|
||||||
|
|
272
chatcommands.go
272
chatcommands.go
|
@ -4,6 +4,7 @@ import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"html"
|
"html"
|
||||||
"strings"
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
"github.com/zorchenhimer/MovieNight/common"
|
"github.com/zorchenhimer/MovieNight/common"
|
||||||
)
|
)
|
||||||
|
@ -45,9 +46,122 @@ var commands = &CommandControl{
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
common.CNColor.String(): cmdColor,
|
common.CNColor.String(): Command{
|
||||||
|
HelpText: "Change user color.",
|
||||||
|
Function: func(cl *Client, args []string) string {
|
||||||
|
if len(args) > 2 {
|
||||||
|
return "Too many arguments!"
|
||||||
|
}
|
||||||
|
|
||||||
common.CNWhoAmI.String(): cmdWhoAmI,
|
// If the caller is priviledged enough, they can change the color of another user
|
||||||
|
if len(args) == 2 {
|
||||||
|
if cl.CmdLevel == common.CmdlUser {
|
||||||
|
return "You cannot change someone else's color. PeepoSus"
|
||||||
|
}
|
||||||
|
|
||||||
|
name, color := "", ""
|
||||||
|
|
||||||
|
if strings.ToLower(args[0]) == strings.ToLower(args[1]) ||
|
||||||
|
(common.IsValidColor(args[0]) && common.IsValidColor(args[1])) {
|
||||||
|
return "Name and color are ambiguous. Prefix the name with '@' or color with '#'"
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check for explicit name
|
||||||
|
if strings.HasPrefix(args[0], "@") {
|
||||||
|
name = strings.TrimLeft(args[0], "@")
|
||||||
|
color = args[1]
|
||||||
|
common.LogDebugln("[color:mod] Found explicit name: ", name)
|
||||||
|
} else if strings.HasPrefix(args[1], "@") {
|
||||||
|
name = strings.TrimLeft(args[1], "@")
|
||||||
|
color = args[0]
|
||||||
|
common.LogDebugln("[color:mod] Found explicit name: ", name)
|
||||||
|
|
||||||
|
// Check for explicit color
|
||||||
|
} else if strings.HasPrefix(args[0], "#") {
|
||||||
|
name = strings.TrimPrefix(args[1], "@") // this shouldn't be needed, but just in case.
|
||||||
|
color = args[0]
|
||||||
|
common.LogDebugln("[color:mod] Found explicit color: ", color)
|
||||||
|
} else if strings.HasPrefix(args[1], "#") {
|
||||||
|
name = strings.TrimPrefix(args[0], "@") // this shouldn't be needed, but just in case.
|
||||||
|
color = args[1]
|
||||||
|
common.LogDebugln("[color:mod] Found explicit color: ", color)
|
||||||
|
|
||||||
|
// Guess
|
||||||
|
} else if common.IsValidColor(args[0]) {
|
||||||
|
name = strings.TrimPrefix(args[1], "@")
|
||||||
|
color = args[0]
|
||||||
|
common.LogDebugln("[color:mod] Guessed name: ", name, " and color: ", color)
|
||||||
|
} else if common.IsValidColor(args[1]) {
|
||||||
|
name = strings.TrimPrefix(args[0], "@")
|
||||||
|
color = args[1]
|
||||||
|
common.LogDebugln("[color:mod] Guessed name: ", name, " and color: ", color)
|
||||||
|
}
|
||||||
|
|
||||||
|
if name == "" {
|
||||||
|
return "Cannot determine name. Prefix name with @."
|
||||||
|
}
|
||||||
|
if color == "" {
|
||||||
|
return "Cannot determine color. Prefix name with @."
|
||||||
|
}
|
||||||
|
|
||||||
|
if color == "" {
|
||||||
|
common.LogInfof("[color:mod] %s missing color\n", cl.name)
|
||||||
|
return "Missing color"
|
||||||
|
}
|
||||||
|
|
||||||
|
if name == "" {
|
||||||
|
common.LogInfof("[color:mod] %s missing name\n", cl.name)
|
||||||
|
return "Missing name"
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := cl.belongsTo.ForceColorChange(name, color); err != nil {
|
||||||
|
return err.Error()
|
||||||
|
}
|
||||||
|
return fmt.Sprintf("Color changed for user %s to %s\n", name, color)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Don't allow an unprivilaged user to change their color if
|
||||||
|
// it was changed by a mod
|
||||||
|
if cl.IsColorForced {
|
||||||
|
common.LogInfof("[color] %s tried to change a forced color\n", cl.name)
|
||||||
|
return "You are not allowed to change your color."
|
||||||
|
}
|
||||||
|
|
||||||
|
if time.Now().Before(cl.nextColor) && cl.CmdLevel == common.CmdlUser {
|
||||||
|
return fmt.Sprintf("Slow down. You can change your color in %0.0f seconds.", time.Until(cl.nextColor).Seconds())
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(args) == 0 {
|
||||||
|
cl.setColor(common.RandomColor())
|
||||||
|
return "Random color chosen: " + cl.color
|
||||||
|
}
|
||||||
|
|
||||||
|
// Change the color of the user
|
||||||
|
if !common.IsValidColor(args[0]) {
|
||||||
|
return "To choose a specific color use the format <i>/color #c029ce</i>. Hex values expected."
|
||||||
|
}
|
||||||
|
|
||||||
|
cl.nextColor = time.Now().Add(time.Second * settings.RateLimitColor)
|
||||||
|
|
||||||
|
err := cl.setColor(args[0])
|
||||||
|
if err != nil {
|
||||||
|
common.LogErrorf("[color] could not send color update to client: %v\n", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
common.LogInfof("[color] %s new color: %s\n", cl.name, cl.color)
|
||||||
|
return "Color changed successfully."
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
common.CNWhoAmI.String(): Command{
|
||||||
|
HelpText: "Shows debug user info",
|
||||||
|
Function: func(cl *Client, args []string) string {
|
||||||
|
return fmt.Sprintf("Name: %s IsMod: %t IsAdmin: %t",
|
||||||
|
cl.name,
|
||||||
|
cl.CmdLevel >= common.CmdlMod,
|
||||||
|
cl.CmdLevel == common.CmdlAdmin)
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
common.CNAuth.String(): Command{
|
common.CNAuth.String(): Command{
|
||||||
HelpText: "Authenticate to admin",
|
HelpText: "Authenticate to admin",
|
||||||
|
@ -56,6 +170,14 @@ var commands = &CommandControl{
|
||||||
return "You are already authenticated."
|
return "You are already authenticated."
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TODO: handle backoff policy
|
||||||
|
if time.Now().Before(cl.nextAuth) {
|
||||||
|
cl.nextAuth = time.Now().Add(time.Second * settings.RateLimitAuth)
|
||||||
|
return "Slow down."
|
||||||
|
}
|
||||||
|
cl.authTries += 1 // this isn't used yet
|
||||||
|
cl.nextAuth = time.Now().Add(time.Second * settings.RateLimitAuth)
|
||||||
|
|
||||||
pw := html.UnescapeString(strings.Join(args, " "))
|
pw := html.UnescapeString(strings.Join(args, " "))
|
||||||
|
|
||||||
if settings.AdminPassword == pw {
|
if settings.AdminPassword == pw {
|
||||||
|
@ -89,6 +211,12 @@ var commands = &CommandControl{
|
||||||
common.CNNick.String(): Command{
|
common.CNNick.String(): Command{
|
||||||
HelpText: "Change display name",
|
HelpText: "Change display name",
|
||||||
Function: func(cl *Client, args []string) string {
|
Function: func(cl *Client, args []string) string {
|
||||||
|
if time.Now().Before(cl.nextNick) {
|
||||||
|
//cl.nextNick = time.Now().Add(time.Second * settings.RateLimitNick)
|
||||||
|
return fmt.Sprintf("Slow down. You can change your nick in %0.0f seconds.", time.Until(cl.nextNick).Seconds())
|
||||||
|
}
|
||||||
|
cl.nextNick = time.Now().Add(time.Second * settings.RateLimitNick)
|
||||||
|
|
||||||
if len(args) == 0 {
|
if len(args) == 0 {
|
||||||
return "Missing name to change to."
|
return "Missing name to change to."
|
||||||
}
|
}
|
||||||
|
@ -390,6 +518,37 @@ var commands = &CommandControl{
|
||||||
return "see console for output"
|
return "see console for output"
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
|
common.CNAddEmotes.String(): Command{
|
||||||
|
HelpText: "Add emotes from a given twitch channel.",
|
||||||
|
Function: func(cl *Client, args []string) string {
|
||||||
|
// Fire this off in it's own goroutine so the client doesn't
|
||||||
|
// block waiting for the emote download to finish.
|
||||||
|
go func() {
|
||||||
|
|
||||||
|
// Pretty sure this breaks on partial downloads (eg, one good channel and one non-existant)
|
||||||
|
_, err := GetEmotes(args)
|
||||||
|
if err != nil {
|
||||||
|
cl.SendChatData(common.NewChatMessage("", "",
|
||||||
|
err.Error(),
|
||||||
|
common.CmdlUser, common.MsgCommandResponse))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// reload emotes now that new ones were added
|
||||||
|
_, err = common.LoadEmotes()
|
||||||
|
if err != nil {
|
||||||
|
cl.SendChatData(common.NewChatMessage("", "",
|
||||||
|
err.Error(),
|
||||||
|
common.CmdlUser, common.MsgCommandResponse))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
cl.belongsTo.AddModNotice(cl.name + " has added emotes from the following channels: " + strings.Join(args, ", "))
|
||||||
|
}()
|
||||||
|
return "Emote download initiated for the following channels: " + strings.Join(args, ", ")
|
||||||
|
},
|
||||||
|
},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -461,112 +620,3 @@ func getHelp(lvl common.CommandLevel) map[string]string {
|
||||||
}
|
}
|
||||||
return helptext
|
return helptext
|
||||||
}
|
}
|
||||||
|
|
||||||
// Commands below have more than one invoking command (aliases).
|
|
||||||
|
|
||||||
var cmdColor = Command{
|
|
||||||
HelpText: "Change user color.",
|
|
||||||
Function: func(cl *Client, args []string) string {
|
|
||||||
if len(args) > 2 {
|
|
||||||
return "Too many arguments!"
|
|
||||||
}
|
|
||||||
|
|
||||||
// If the caller is priviledged enough, they can change the color of another user
|
|
||||||
if len(args) == 2 {
|
|
||||||
if cl.CmdLevel == common.CmdlUser {
|
|
||||||
return "You cannot change someone else's color. PeepoSus"
|
|
||||||
}
|
|
||||||
|
|
||||||
name, color := "", ""
|
|
||||||
|
|
||||||
if strings.ToLower(args[0]) == strings.ToLower(args[1]) ||
|
|
||||||
(common.IsValidColor(args[0]) && common.IsValidColor(args[1])) {
|
|
||||||
return "Name and color are ambiguous. Prefix the name with '@' or color with '#'"
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check for explicit name
|
|
||||||
if strings.HasPrefix(args[0], "@") {
|
|
||||||
name = strings.TrimLeft(args[0], "@")
|
|
||||||
color = args[1]
|
|
||||||
common.LogDebugln("[color:mod] Found explicit name: ", name)
|
|
||||||
} else if strings.HasPrefix(args[1], "@") {
|
|
||||||
name = strings.TrimLeft(args[1], "@")
|
|
||||||
color = args[0]
|
|
||||||
common.LogDebugln("[color:mod] Found explicit name: ", name)
|
|
||||||
|
|
||||||
// Check for explicit color
|
|
||||||
} else if strings.HasPrefix(args[0], "#") {
|
|
||||||
name = strings.TrimPrefix(args[1], "@") // this shouldn't be needed, but just in case.
|
|
||||||
color = args[0]
|
|
||||||
common.LogDebugln("[color:mod] Found explicit color: ", color)
|
|
||||||
} else if strings.HasPrefix(args[1], "#") {
|
|
||||||
name = strings.TrimPrefix(args[0], "@") // this shouldn't be needed, but just in case.
|
|
||||||
color = args[1]
|
|
||||||
common.LogDebugln("[color:mod] Found explicit color: ", color)
|
|
||||||
|
|
||||||
// Guess
|
|
||||||
} else if common.IsValidColor(args[0]) {
|
|
||||||
name = strings.TrimPrefix(args[1], "@")
|
|
||||||
color = args[0]
|
|
||||||
common.LogDebugln("[color:mod] Guessed name: ", name, " and color: ", color)
|
|
||||||
} else if common.IsValidColor(args[1]) {
|
|
||||||
name = strings.TrimPrefix(args[0], "@")
|
|
||||||
color = args[1]
|
|
||||||
common.LogDebugln("[color:mod] Guessed name: ", name, " and color: ", color)
|
|
||||||
}
|
|
||||||
|
|
||||||
if name == "" {
|
|
||||||
return "Cannot determine name. Prefix name with @."
|
|
||||||
}
|
|
||||||
if color == "" {
|
|
||||||
return "Cannot determine color. Prefix name with @."
|
|
||||||
}
|
|
||||||
|
|
||||||
if color == "" {
|
|
||||||
common.LogInfof("[color:mod] %s missing color\n", cl.name)
|
|
||||||
return "Missing color"
|
|
||||||
}
|
|
||||||
|
|
||||||
if name == "" {
|
|
||||||
common.LogInfof("[color:mod] %s missing name\n", cl.name)
|
|
||||||
return "Missing name"
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := cl.belongsTo.ForceColorChange(name, color); err != nil {
|
|
||||||
return err.Error()
|
|
||||||
}
|
|
||||||
return fmt.Sprintf("Color changed for user %s to %s\n", name, color)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Don't allow an unprivilaged user to change their color if
|
|
||||||
// it was changed by a mod
|
|
||||||
if cl.IsColorForced {
|
|
||||||
common.LogInfof("[color] %s tried to change a forced color\n", cl.name)
|
|
||||||
return "You are not allowed to change your color."
|
|
||||||
}
|
|
||||||
|
|
||||||
if len(args) == 0 {
|
|
||||||
cl.color = common.RandomColor()
|
|
||||||
return "Random color chosen: " + cl.color
|
|
||||||
}
|
|
||||||
|
|
||||||
// Change the color of the user
|
|
||||||
if !common.IsValidColor(args[0]) {
|
|
||||||
return "To choose a specific color use the format <i>/color #c029ce</i>. Hex values expected."
|
|
||||||
}
|
|
||||||
|
|
||||||
cl.color = args[0]
|
|
||||||
common.LogInfof("[color] %s new color: %s\n", cl.name, cl.color)
|
|
||||||
return "Color changed successfully."
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
var cmdWhoAmI = Command{
|
|
||||||
HelpText: "Shows debug user info",
|
|
||||||
Function: func(cl *Client, args []string) string {
|
|
||||||
return fmt.Sprintf("Name: %s IsMod: %t IsAdmin: %t",
|
|
||||||
cl.name,
|
|
||||||
cl.CmdLevel >= common.CmdlMod,
|
|
||||||
cl.CmdLevel == common.CmdlAdmin)
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
14
chatroom.go
14
chatroom.go
|
@ -96,16 +96,9 @@ func (cr *ChatRoom) Join(name, uid string) (*Client, error) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
conn.clientName = name
|
client, err := NewClient(conn, cr, name, common.RandomColor())
|
||||||
client := &Client{
|
|
||||||
conn: conn,
|
|
||||||
belongsTo: cr,
|
|
||||||
color: common.RandomColor(),
|
|
||||||
}
|
|
||||||
|
|
||||||
err := client.setName(name)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("could not set client name to %#v: %v", name, err)
|
return nil, fmt.Errorf("Unable to join client: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
host := client.Host()
|
host := client.Host()
|
||||||
|
@ -125,6 +118,9 @@ func (cr *ChatRoom) Join(name, uid string) (*Client, error) {
|
||||||
client.Send(playingCommand)
|
client.Send(playingCommand)
|
||||||
}
|
}
|
||||||
cr.AddEventMsg(common.EvJoin, name, client.color)
|
cr.AddEventMsg(common.EvJoin, name, client.color)
|
||||||
|
|
||||||
|
client.SendChatData(common.NewChatHiddenMessage(common.CdEmote, common.Emotes))
|
||||||
|
|
||||||
return client, nil
|
return client, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -36,17 +36,40 @@ var (
|
||||||
CNReloadEmotes ChatCommandNames = []string{"reloademotes"}
|
CNReloadEmotes ChatCommandNames = []string{"reloademotes"}
|
||||||
CNModpass ChatCommandNames = []string{"modpass"}
|
CNModpass ChatCommandNames = []string{"modpass"}
|
||||||
CNIP ChatCommandNames = []string{"iplist"}
|
CNIP ChatCommandNames = []string{"iplist"}
|
||||||
|
CNAddEmotes ChatCommandNames = []string{"addemotes"}
|
||||||
CNNewPin ChatCommandNames = []string{"newpin", "newpassword"}
|
CNNewPin ChatCommandNames = []string{"newpin", "newpassword"}
|
||||||
CNRoomAccess ChatCommandNames = []string{"changeaccess", "hodor"}
|
CNRoomAccess ChatCommandNames = []string{"changeaccess", "hodor"}
|
||||||
)
|
)
|
||||||
|
|
||||||
var ChatCommands = []ChatCommandNames{
|
var ChatCommands = []ChatCommandNames{
|
||||||
// User
|
// User
|
||||||
CNMe, CNHelp, CNCount, CNColor, CNWhoAmI, CNAuth, CNUsers, CNNick,
|
CNMe,
|
||||||
|
CNHelp,
|
||||||
|
CNCount,
|
||||||
|
CNColor,
|
||||||
|
CNWhoAmI,
|
||||||
|
CNAuth,
|
||||||
|
CNUsers,
|
||||||
|
CNNick,
|
||||||
|
|
||||||
// Mod
|
// Mod
|
||||||
CNSv, CNPlaying, CNUnmod, CNKick, CNBan, CNUnban, CNPurge, CNPin,
|
CNSv, CNPlaying, CNUnmod, CNKick, CNBan, CNUnban, CNPurge, CNPin,
|
||||||
|
CNSv,
|
||||||
|
CNPlaying,
|
||||||
|
CNUnmod,
|
||||||
|
CNKick,
|
||||||
|
CNBan,
|
||||||
|
CNUnban,
|
||||||
|
CNPurge,
|
||||||
|
|
||||||
// Admin
|
// Admin
|
||||||
CNMod, CNReloadPlayer, CNReloadEmotes, CNModpass, CNRoomAccess, CNIP,
|
CNMod, CNReloadPlayer, CNReloadEmotes, CNModpass, CNRoomAccess, CNIP,
|
||||||
|
CNMod,
|
||||||
|
CNReloadPlayer,
|
||||||
|
CNReloadEmotes,
|
||||||
|
CNModpass,
|
||||||
|
CNIP,
|
||||||
|
CNAddEmotes,
|
||||||
}
|
}
|
||||||
|
|
||||||
func GetFullChatCommand(c string) string {
|
func GetFullChatCommand(c string) string {
|
||||||
|
|
|
@ -78,7 +78,7 @@ type ClientData struct {
|
||||||
|
|
||||||
func (c ClientData) HTML() string {
|
func (c ClientData) HTML() string {
|
||||||
// Client data is for client to server communication only, so clients should not see this
|
// Client data is for client to server communication only, so clients should not see this
|
||||||
return `<div style="color: red;"><span>The developer messed up. You should not be seeing this.</span></div>`
|
return `<span style="color: red;">The developer messed up. You should not be seeing this.</span>`
|
||||||
}
|
}
|
||||||
|
|
||||||
type DataMessage struct {
|
type DataMessage struct {
|
||||||
|
@ -93,20 +93,20 @@ type DataMessage struct {
|
||||||
func (dc DataMessage) HTML() string {
|
func (dc DataMessage) HTML() string {
|
||||||
switch dc.Type {
|
switch dc.Type {
|
||||||
case MsgAction:
|
case MsgAction:
|
||||||
return `<div style="color:` + dc.Color + `"><span class="name">` + dc.From +
|
return `<span style="color:` + dc.Color + `"><span class="name">` + dc.From +
|
||||||
`</span> <span class="cmdme">` + dc.Message + `</span></div>`
|
`</span> <span class="cmdme">` + dc.Message + `</span></span>`
|
||||||
|
|
||||||
case MsgServer:
|
case MsgServer:
|
||||||
return `<div class="announcement">` + dc.Message + `</div>`
|
return `<span class="announcement">` + dc.Message + `</span>`
|
||||||
|
|
||||||
case MsgError:
|
case MsgError:
|
||||||
return `<div class="error">` + dc.Message + `</div>`
|
return `<span class="error">` + dc.Message + `</span>`
|
||||||
|
|
||||||
case MsgNotice:
|
case MsgNotice:
|
||||||
return `<div class="notice">` + dc.Message + `</div>`
|
return `<span class="notice">` + dc.Message + `</span>`
|
||||||
|
|
||||||
case MsgCommandResponse:
|
case MsgCommandResponse:
|
||||||
return `<div class="command">` + dc.Message + `</div>`
|
return `<span class="command">` + dc.Message + `</span>`
|
||||||
|
|
||||||
default:
|
default:
|
||||||
badge := ""
|
badge := ""
|
||||||
|
@ -116,8 +116,8 @@ func (dc DataMessage) HTML() string {
|
||||||
case CmdlAdmin:
|
case CmdlAdmin:
|
||||||
badge = `<img src="/static/img/admin.png" class="badge" />`
|
badge = `<img src="/static/img/admin.png" class="badge" />`
|
||||||
}
|
}
|
||||||
return `<div>` + badge + `<span class="name" style="color:` + dc.Color + `">` + dc.From +
|
return `<span>` + badge + `<span class="name" style="color:` + dc.Color + `">` + dc.From +
|
||||||
`</span><b>:</b> <span class="msg">` + dc.Message + `</span></div>`
|
`</span><b>:</b> <span class="msg">` + dc.Message + `</span></span>`
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -142,7 +142,7 @@ type DataCommand struct {
|
||||||
func (de DataCommand) HTML() string {
|
func (de DataCommand) HTML() string {
|
||||||
switch de.Command {
|
switch de.Command {
|
||||||
case CmdPurgeChat:
|
case CmdPurgeChat:
|
||||||
return `<div class="notice">Chat has been purged by a moderator.</div>`
|
return `<span class="notice">Chat has been purged by a moderator.</span>`
|
||||||
default:
|
default:
|
||||||
return ""
|
return ""
|
||||||
}
|
}
|
||||||
|
@ -167,37 +167,37 @@ type DataEvent struct {
|
||||||
func (de DataEvent) HTML() string {
|
func (de DataEvent) HTML() string {
|
||||||
switch de.Event {
|
switch de.Event {
|
||||||
case EvKick:
|
case EvKick:
|
||||||
return `<div class="event"><span class="name" style="color:` + de.Color + `">` +
|
return `<span class="event"><span class="name" style="color:` + de.Color + `">` +
|
||||||
de.User + `</span> has been kicked.</div>`
|
de.User + `</span> has been kicked.</span>`
|
||||||
case EvLeave:
|
case EvLeave:
|
||||||
return `<div class="event"><span class="name" style="color:` + de.Color + `">` +
|
return `<span class="event"><span class="name" style="color:` + de.Color + `">` +
|
||||||
de.User + `</span> has left the chat.</div>`
|
de.User + `</span> has left the chat.</span>`
|
||||||
case EvBan:
|
case EvBan:
|
||||||
return `<div class="event"><span class="name" style="color:` + de.Color + `">` +
|
return `<span class="event"><span class="name" style="color:` + de.Color + `">` +
|
||||||
de.User + `</span> has been banned.</div>`
|
de.User + `</span> has been banned.</span>`
|
||||||
case EvJoin:
|
case EvJoin:
|
||||||
return `<div class="event"><span class="name" style="color:` + de.Color + `">` +
|
return `<span class="event"><span class="name" style="color:` + de.Color + `">` +
|
||||||
de.User + `</span> has joined the chat.</div>`
|
de.User + `</span> has joined the chat.</span>`
|
||||||
case EvNameChange:
|
case EvNameChange:
|
||||||
names := strings.Split(de.User, ":")
|
names := strings.Split(de.User, ":")
|
||||||
if len(names) != 2 {
|
if len(names) != 2 {
|
||||||
return `<div class="event">Somebody changed their name, but IDK who ` +
|
return `<span class="event">Somebody changed their name, but IDK who ` +
|
||||||
ParseEmotes("Jebaited") + `.</div>`
|
ParseEmotes("Jebaited") + `.</span>`
|
||||||
}
|
}
|
||||||
|
|
||||||
return `<div class="event"><span class="name" style="color:` + de.Color + `">` +
|
return `<span class="event"><span class="name" style="color:` + de.Color + `">` +
|
||||||
names[0] + `</span> has changed their name to <span class="name" style="color:` +
|
names[0] + `</span> has changed their name to <span class="name" style="color:` +
|
||||||
de.Color + `">` + names[1] + `</span>.</div>`
|
de.Color + `">` + names[1] + `</span>.</span>`
|
||||||
case EvNameChangeForced:
|
case EvNameChangeForced:
|
||||||
names := strings.Split(de.User, ":")
|
names := strings.Split(de.User, ":")
|
||||||
if len(names) != 2 {
|
if len(names) != 2 {
|
||||||
return `<div class="event">An admin changed somebody's name, but IDK who ` +
|
return `<span class="event">An admin changed somebody's name, but IDK who ` +
|
||||||
ParseEmotes("Jebaited") + `.</div>`
|
ParseEmotes("Jebaited") + `.</span>`
|
||||||
}
|
}
|
||||||
|
|
||||||
return `<div class="event"><span class="name" style="color:` + de.Color + `">` +
|
return `<span class="event"><span class="name" style="color:` + de.Color + `">` +
|
||||||
names[0] + `</span> has had their name changed to <span class="name" style="color:` +
|
names[0] + `</span> has had their name changed to <span class="name" style="color:` +
|
||||||
de.Color + `">` + names[1] + `</span> by an admin.</div>`
|
de.Color + `">` + names[1] + `</span> by an admin.</span>`
|
||||||
}
|
}
|
||||||
return ""
|
return ""
|
||||||
}
|
}
|
||||||
|
|
|
@ -47,8 +47,7 @@ var (
|
||||||
)
|
)
|
||||||
|
|
||||||
// IsValidColor takes a string s and compares it against a list of css color names.
|
// IsValidColor takes a string s and compares it against a list of css color names.
|
||||||
// It also accepts hex codes in the form of #000 (RGB), to #00000000 (RRGGBBAA), with A
|
// It also accepts hex codes in the form of #RGB and #RRGGBB
|
||||||
// being the alpha value
|
|
||||||
func IsValidColor(s string) bool {
|
func IsValidColor(s string) bool {
|
||||||
s = strings.ToLower(s)
|
s = strings.ToLower(s)
|
||||||
for _, c := range colors {
|
for _, c := range colors {
|
||||||
|
|
|
@ -8,6 +8,8 @@ const (
|
||||||
CdUsers // get a list of users
|
CdUsers // get a list of users
|
||||||
CdPing // ping the server to keep the connection alive
|
CdPing // ping the server to keep the connection alive
|
||||||
CdAuth // get the auth levels of the user
|
CdAuth // get the auth levels of the user
|
||||||
|
CdColor // get the users color
|
||||||
|
CdEmote // get a list of emotes
|
||||||
)
|
)
|
||||||
|
|
||||||
type DataType int
|
type DataType int
|
||||||
|
|
|
@ -8,15 +8,20 @@ import (
|
||||||
|
|
||||||
var Emotes map[string]string
|
var Emotes map[string]string
|
||||||
|
|
||||||
|
func EmoteToHtml(file, title string) string {
|
||||||
|
return fmt.Sprintf(`<img src="/emotes/%s" height="28px" title="%s" />`, file, title)
|
||||||
|
}
|
||||||
|
|
||||||
func ParseEmotesArray(words []string) []string {
|
func ParseEmotesArray(words []string) []string {
|
||||||
newWords := []string{}
|
newWords := []string{}
|
||||||
for _, word := range words {
|
for _, word := range words {
|
||||||
word = strings.Trim(word, "[]")
|
// make :emote: and [emote] valid for replacement.
|
||||||
|
wordTrimmed := strings.Trim(word, ":[]")
|
||||||
|
|
||||||
found := false
|
found := false
|
||||||
for key, val := range Emotes {
|
for key, val := range Emotes {
|
||||||
if key == word {
|
if key == wordTrimmed {
|
||||||
newWords = append(newWords, fmt.Sprintf(`<img src="/emotes/%s" height="28px" title="%s" />`, val, key))
|
newWords = append(newWords, EmoteToHtml(val, key))
|
||||||
found = true
|
found = true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,165 @@
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"net/http"
|
||||||
|
"os"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
type twitchChannel struct {
|
||||||
|
ChannelName string `json:"channel_name"`
|
||||||
|
DisplayName string `json:"display_name"`
|
||||||
|
ChannelId string `json:"channel_id"`
|
||||||
|
BroadcasterType string `json:"broadcaster_type"`
|
||||||
|
Plans map[string]string `json:"plans"`
|
||||||
|
Emotes []struct {
|
||||||
|
Code string `json:"code"`
|
||||||
|
Set int `json:"emoticon_set"`
|
||||||
|
Id int `json:"id"`
|
||||||
|
} `json:"emotes"`
|
||||||
|
BaseSetId string `json:"base_set_id"`
|
||||||
|
GeneratedAt string `json:"generated_at"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// Used in settings
|
||||||
|
type EmoteSet struct {
|
||||||
|
Channel string // channel name
|
||||||
|
Prefix string // emote prefix
|
||||||
|
Found bool `json:"-"`
|
||||||
|
}
|
||||||
|
|
||||||
|
const subscriberJson string = `subscribers.json`
|
||||||
|
|
||||||
|
// Download a single channel's emote set
|
||||||
|
func (tc *twitchChannel) downloadEmotes() (*EmoteSet, error) {
|
||||||
|
es := &EmoteSet{Channel: strings.ToLower(tc.ChannelName)}
|
||||||
|
for _, emote := range tc.Emotes {
|
||||||
|
url := fmt.Sprintf(`https://static-cdn.jtvnw.net/emoticons/v1/%d/1.0`, emote.Id)
|
||||||
|
png := `static/emotes/` + emote.Code + `.png`
|
||||||
|
|
||||||
|
if len(es.Prefix) == 0 {
|
||||||
|
// For each letter
|
||||||
|
for i := 0; i < len(emote.Code); i++ {
|
||||||
|
// Find the first capital
|
||||||
|
b := emote.Code[i]
|
||||||
|
if b >= 'A' && b <= 'Z' {
|
||||||
|
es.Prefix = emote.Code[0 : i-1]
|
||||||
|
fmt.Printf("Found prefix for channel %q: %q (%q)\n", es.Channel, es.Prefix, emote)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
resp, err := http.Get(url)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
f, err := os.Create(png)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err = io.Copy(f, resp.Body)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return es, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func GetEmotes(names []string) ([]*EmoteSet, error) {
|
||||||
|
// Do this up-front
|
||||||
|
for i := 0; i < len(names); i++ {
|
||||||
|
names[i] = strings.ToLower(names[i])
|
||||||
|
}
|
||||||
|
|
||||||
|
channels, err := findChannels(names)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("Error reading %q: %v", subscriberJson, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
emoteSets := []*EmoteSet{}
|
||||||
|
for _, c := range channels {
|
||||||
|
es, err := c.downloadEmotes()
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("Error downloading emotes: %v", err)
|
||||||
|
}
|
||||||
|
emoteSets = append(emoteSets, es)
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, es := range emoteSets {
|
||||||
|
found := false
|
||||||
|
for _, name := range names {
|
||||||
|
if es.Channel == name {
|
||||||
|
found = true
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if !found {
|
||||||
|
es.Found = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return emoteSets, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func findChannels(names []string) ([]twitchChannel, error) {
|
||||||
|
file, err := os.Open(subscriberJson)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
defer file.Close()
|
||||||
|
|
||||||
|
data := []twitchChannel{}
|
||||||
|
dec := json.NewDecoder(file)
|
||||||
|
|
||||||
|
// Open bracket
|
||||||
|
_, err = dec.Token()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
done := false
|
||||||
|
for dec.More() && !done {
|
||||||
|
// opening bracket of channel
|
||||||
|
_, err = dec.Token()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Decode the channel stuff
|
||||||
|
var c twitchChannel
|
||||||
|
err = dec.Decode(&c)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Is this a channel we are looking for?
|
||||||
|
found := false
|
||||||
|
for _, search := range names {
|
||||||
|
if strings.ToLower(c.ChannelName) == search {
|
||||||
|
found = true
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Yes it is. Add it to the data
|
||||||
|
if found {
|
||||||
|
data = append(data, c)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check for completion. Don't bother parsing the rest of
|
||||||
|
// the json file if we've already found everything that we're
|
||||||
|
// looking for.
|
||||||
|
if len(data) == len(names) {
|
||||||
|
done = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return data, nil
|
||||||
|
}
|
|
@ -64,7 +64,9 @@ func wsStaticFiles(w http.ResponseWriter, r *http.Request) {
|
||||||
}
|
}
|
||||||
|
|
||||||
func wsWasmFile(w http.ResponseWriter, r *http.Request) {
|
func wsWasmFile(w http.ResponseWriter, r *http.Request) {
|
||||||
|
if settings.NoCache {
|
||||||
w.Header().Set("Cache-Control", "no-cache, must-revalidate")
|
w.Header().Set("Cache-Control", "no-cache, must-revalidate")
|
||||||
|
}
|
||||||
http.ServeFile(w, r, "./static/main.wasm")
|
http.ServeFile(w, r, "./static/main.wasm")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -333,7 +335,9 @@ func handleIndexTemplate(w http.ResponseWriter, r *http.Request) {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Force browser to replace cache since file was not changed
|
// Force browser to replace cache since file was not changed
|
||||||
|
if settings.NoCache {
|
||||||
w.Header().Set("Cache-Control", "no-cache, must-revalidate")
|
w.Header().Set("Cache-Control", "no-cache, must-revalidate")
|
||||||
|
}
|
||||||
|
|
||||||
err = t.Execute(w, data)
|
err = t.Execute(w, data)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|
34
main.go
34
main.go
|
@ -1,10 +1,8 @@
|
||||||
package main
|
package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"crypto/rand"
|
|
||||||
"flag"
|
"flag"
|
||||||
"fmt"
|
"fmt"
|
||||||
"math/big"
|
|
||||||
"net/http"
|
"net/http"
|
||||||
"os"
|
"os"
|
||||||
"os/signal"
|
"os/signal"
|
||||||
|
@ -31,33 +29,6 @@ func setupSettings() error {
|
||||||
return fmt.Errorf("Missing stream key is settings.json")
|
return fmt.Errorf("Missing stream key is settings.json")
|
||||||
}
|
}
|
||||||
|
|
||||||
if err = settings.SetupLogging(); err != nil {
|
|
||||||
return fmt.Errorf("Unable to setup logger: %s", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Is this a good way to do this? Probably not...
|
|
||||||
if len(settings.SessionKey) == 0 {
|
|
||||||
out := ""
|
|
||||||
large := big.NewInt(int64(1 << 60))
|
|
||||||
large = large.Add(large, large)
|
|
||||||
for len(out) < 50 {
|
|
||||||
num, err := rand.Int(rand.Reader, large)
|
|
||||||
if err != nil {
|
|
||||||
panic("Error generating session key: " + err.Error())
|
|
||||||
}
|
|
||||||
out = fmt.Sprintf("%s%X", out, num)
|
|
||||||
}
|
|
||||||
settings.SessionKey = out
|
|
||||||
}
|
|
||||||
|
|
||||||
if len(settings.RoomAccess) == 0 {
|
|
||||||
settings.RoomAccess = AccessOpen
|
|
||||||
}
|
|
||||||
|
|
||||||
if settings.RoomAccess != AccessOpen && len(settings.RoomAccessPin) == 0 {
|
|
||||||
settings.RoomAccessPin = "1234"
|
|
||||||
}
|
|
||||||
|
|
||||||
sstore = sessions.NewCookieStore([]byte(settings.SessionKey))
|
sstore = sessions.NewCookieStore([]byte(settings.SessionKey))
|
||||||
sstore.Options = &sessions.Options{
|
sstore.Options = &sessions.Options{
|
||||||
Path: "/",
|
Path: "/",
|
||||||
|
@ -65,11 +36,6 @@ func setupSettings() error {
|
||||||
SameSite: http.SameSiteStrictMode,
|
SameSite: http.SameSiteStrictMode,
|
||||||
}
|
}
|
||||||
|
|
||||||
// Save admin password to file
|
|
||||||
if err = settings.Save(); err != nil {
|
|
||||||
return fmt.Errorf("Unable to save settings: %s", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
83
settings.go
83
settings.go
|
@ -30,12 +30,23 @@ type Settings struct {
|
||||||
AdminPassword string
|
AdminPassword string
|
||||||
StreamKey string
|
StreamKey string
|
||||||
ListenAddress string
|
ListenAddress string
|
||||||
|
ApprovedEmotes []EmoteSet // list of channels that have been approved for emote use. Global emotes are always "approved".
|
||||||
SessionKey string // key for session data
|
SessionKey string // key for session data
|
||||||
Bans []BanInfo
|
Bans []BanInfo
|
||||||
LogLevel common.LogLevel
|
LogLevel common.LogLevel
|
||||||
LogFile string
|
LogFile string
|
||||||
RoomAccess AccessMode
|
RoomAccess AccessMode
|
||||||
RoomAccessPin string // auto generate this,
|
RoomAccessPin string // auto generate this,
|
||||||
|
|
||||||
|
// Rate limiting stuff, in seconds
|
||||||
|
RateLimitChat time.Duration
|
||||||
|
RateLimitNick time.Duration
|
||||||
|
RateLimitColor time.Duration
|
||||||
|
RateLimitAuth time.Duration
|
||||||
|
RateLimitDuplicate time.Duration // Amount of seconds between allowed duplicate messages
|
||||||
|
|
||||||
|
// Send the NoCache header?
|
||||||
|
NoCache bool
|
||||||
}
|
}
|
||||||
|
|
||||||
type AccessMode string
|
type AccessMode string
|
||||||
|
@ -65,6 +76,10 @@ func LoadSettings(filename string) (*Settings, error) {
|
||||||
}
|
}
|
||||||
s.filename = filename
|
s.filename = filename
|
||||||
|
|
||||||
|
if err = common.SetupLogging(s.LogLevel, s.LogFile); err != nil {
|
||||||
|
return nil, fmt.Errorf("Unable to setup logger: %s", err)
|
||||||
|
}
|
||||||
|
|
||||||
// have a default of 200
|
// have a default of 200
|
||||||
if s.MaxMessageCount == 0 {
|
if s.MaxMessageCount == 0 {
|
||||||
s.MaxMessageCount = 300
|
s.MaxMessageCount = 300
|
||||||
|
@ -77,6 +92,50 @@ func LoadSettings(filename string) (*Settings, error) {
|
||||||
return nil, fmt.Errorf("unable to generate admin password: %s", err)
|
return nil, fmt.Errorf("unable to generate admin password: %s", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if s.RateLimitChat == -1 {
|
||||||
|
s.RateLimitChat = 0
|
||||||
|
} else if s.RateLimitChat <= 0 {
|
||||||
|
s.RateLimitChat = 1
|
||||||
|
}
|
||||||
|
|
||||||
|
if s.RateLimitNick == -1 {
|
||||||
|
s.RateLimitNick = 0
|
||||||
|
} else if s.RateLimitNick <= 0 {
|
||||||
|
s.RateLimitNick = 300
|
||||||
|
}
|
||||||
|
|
||||||
|
if s.RateLimitColor == -1 {
|
||||||
|
s.RateLimitColor = 0
|
||||||
|
} else if s.RateLimitColor <= 0 {
|
||||||
|
s.RateLimitColor = 60
|
||||||
|
}
|
||||||
|
|
||||||
|
if s.RateLimitAuth == -1 {
|
||||||
|
s.RateLimitAuth = 0
|
||||||
|
} else if s.RateLimitAuth <= 0 {
|
||||||
|
s.RateLimitAuth = 5
|
||||||
|
}
|
||||||
|
|
||||||
|
if s.RateLimitDuplicate == -1 {
|
||||||
|
s.RateLimitDuplicate = 0
|
||||||
|
} else if s.RateLimitDuplicate <= 0 {
|
||||||
|
s.RateLimitDuplicate = 30
|
||||||
|
}
|
||||||
|
|
||||||
|
// Print this stuff before we multiply it by time.Second
|
||||||
|
common.LogInfof("RateLimitChat: %v", s.RateLimitChat)
|
||||||
|
common.LogInfof("RateLimitNick: %v", s.RateLimitNick)
|
||||||
|
common.LogInfof("RateLimitColor: %v", s.RateLimitColor)
|
||||||
|
common.LogInfof("RateLimitAuth: %v", s.RateLimitAuth)
|
||||||
|
|
||||||
|
if len(settings.RoomAccess) == 0 {
|
||||||
|
settings.RoomAccess = AccessOpen
|
||||||
|
}
|
||||||
|
|
||||||
|
if settings.RoomAccess != AccessOpen && len(settings.RoomAccessPin) == 0 {
|
||||||
|
settings.RoomAccessPin = "1234"
|
||||||
|
}
|
||||||
|
|
||||||
// Don't use LogInfof() here. Log isn't setup yet when LoadSettings() is called from init().
|
// Don't use LogInfof() here. Log isn't setup yet when LoadSettings() is called from init().
|
||||||
fmt.Printf("Settings reloaded. New admin password: %s\n", s.AdminPassword)
|
fmt.Printf("Settings reloaded. New admin password: %s\n", s.AdminPassword)
|
||||||
|
|
||||||
|
@ -84,6 +143,26 @@ func LoadSettings(filename string) (*Settings, error) {
|
||||||
s.TitleLength = 50
|
s.TitleLength = 50
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Is this a good way to do this? Probably not...
|
||||||
|
if len(settings.SessionKey) == 0 {
|
||||||
|
out := ""
|
||||||
|
large := big.NewInt(int64(1 << 60))
|
||||||
|
large = large.Add(large, large)
|
||||||
|
for len(out) < 50 {
|
||||||
|
num, err := rand.Int(rand.Reader, large)
|
||||||
|
if err != nil {
|
||||||
|
panic("Error generating session key: " + err.Error())
|
||||||
|
}
|
||||||
|
out = fmt.Sprintf("%s%X", out, num)
|
||||||
|
}
|
||||||
|
settings.SessionKey = out
|
||||||
|
}
|
||||||
|
|
||||||
|
// Save admin password to file
|
||||||
|
if err = settings.Save(); err != nil {
|
||||||
|
return nil, fmt.Errorf("Unable to save settings: %s", err)
|
||||||
|
}
|
||||||
|
|
||||||
return s, nil
|
return s, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -178,10 +257,6 @@ func (s *Settings) GetStreamKey() string {
|
||||||
return s.StreamKey
|
return s.StreamKey
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *Settings) SetupLogging() error {
|
|
||||||
return common.SetupLogging(s.LogLevel, s.LogFile)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *Settings) generateNewPin() (string, error) {
|
func (s *Settings) generateNewPin() (string, error) {
|
||||||
num, err := rand.Int(rand.Reader, big.NewInt(int64(9999)))
|
num, err := rand.Int(rand.Reader, big.NewInt(int64(9999)))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|
|
@ -5,4 +5,14 @@
|
||||||
"Bans": [],
|
"Bans": [],
|
||||||
"StreamKey": "ALongStreamKey",
|
"StreamKey": "ALongStreamKey",
|
||||||
"ListenAddress": ":8089"
|
"ListenAddress": ":8089"
|
||||||
|
"ApprovedEmotes": null,
|
||||||
|
"Bans": [],
|
||||||
|
"LogLevel": "debug",
|
||||||
|
"LogFile": "thelog.log",
|
||||||
|
"RateLimitChat": 1,
|
||||||
|
"RateLimitNick": 300,
|
||||||
|
"RateLimitColor": 60,
|
||||||
|
"RateLimitAuth": 5,
|
||||||
|
"RateLimitDuplicate": 30,
|
||||||
|
"NoCache": false
|
||||||
}
|
}
|
||||||
|
|
|
@ -12,6 +12,7 @@
|
||||||
</head>
|
</head>
|
||||||
|
|
||||||
<body class="scrollbar">
|
<body class="scrollbar">
|
||||||
|
<img id="remote" src="/static/img/remote.png" style="display: none;" onclick="flipRemote();" />
|
||||||
<div class="root">
|
<div class="root">
|
||||||
{{template "body" .}}
|
{{template "body" .}}
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -238,6 +238,7 @@ span.svmsg {
|
||||||
grid-gap: 10px;
|
grid-gap: 10px;
|
||||||
margin: 0px 5px;
|
margin: 0px 5px;
|
||||||
overflow: auto;
|
overflow: auto;
|
||||||
|
font-size: 12px;
|
||||||
}
|
}
|
||||||
|
|
||||||
#messages {
|
#messages {
|
||||||
|
@ -258,14 +259,6 @@ span.svmsg {
|
||||||
display: grid;
|
display: grid;
|
||||||
}
|
}
|
||||||
|
|
||||||
#suggestions {
|
|
||||||
background: #3b3b43;
|
|
||||||
position: absolute;
|
|
||||||
min-width: 10em;
|
|
||||||
border-radius: 5px 5px 0px 5px;
|
|
||||||
color: var(--var-message-color);
|
|
||||||
}
|
|
||||||
|
|
||||||
#msg {
|
#msg {
|
||||||
background: transparent;
|
background: transparent;
|
||||||
border: var(--var-border);
|
border: var(--var-border);
|
||||||
|
@ -276,10 +269,24 @@ span.svmsg {
|
||||||
resize: none;
|
resize: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
#suggestions div {
|
#suggestions {
|
||||||
|
background: #3b3b43;
|
||||||
|
position: absolute;
|
||||||
|
min-width: 10em;
|
||||||
|
border-radius: 5px 5px 0px 5px;
|
||||||
|
color: var(--var-message-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
#suggestions>div {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
padding: 5px;
|
padding: 5px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#suggestions>div>img {
|
||||||
|
margin-right: 1em;
|
||||||
|
}
|
||||||
|
|
||||||
#suggestions div.selectedName {
|
#suggestions div.selectedName {
|
||||||
color: var(--var-contrast-color);
|
color: var(--var-contrast-color);
|
||||||
}
|
}
|
||||||
|
@ -291,3 +298,13 @@ span.svmsg {
|
||||||
#colorSubmit:disabled {
|
#colorSubmit:disabled {
|
||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#remote {
|
||||||
|
position: absolute;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
top: 0;
|
||||||
|
margin: 1em auto;
|
||||||
|
width: 50px;
|
||||||
|
z-index: 999;
|
||||||
|
}
|
Binary file not shown.
After Width: | Height: | Size: 17 KiB |
Binary file not shown.
After Width: | Height: | Size: 9.1 KiB |
|
@ -1,8 +1,38 @@
|
||||||
/// <reference path="./jquery.js" />
|
/// <reference path="./jquery.js" />
|
||||||
|
|
||||||
|
let konamiCode = ["ArrowUp", "ArrowUp", "ArrowDown", "ArrowDown", "ArrowLeft", "ArrowRight", "ArrowLeft", "ArrowRight", "b", "a"]
|
||||||
|
let lastKeys = []
|
||||||
|
|
||||||
// Make this on all pages so video page also doesn't do this
|
// Make this on all pages so video page also doesn't do this
|
||||||
$(document).on("keydown", function (e) {
|
$(document).on("keydown", function (e) {
|
||||||
|
checkKonami(e);
|
||||||
|
|
||||||
if (e.which === 8 && !$(e.target).is("input, textarea")) {
|
if (e.which === 8 && !$(e.target).is("input, textarea")) {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
|
function checkKonami(e) {
|
||||||
|
lastKeys.push(e.key);
|
||||||
|
if (lastKeys.length > 10) {
|
||||||
|
lastKeys.shift();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (lastKeys.length === konamiCode.length) {
|
||||||
|
for (let i = 0; i < lastKeys.length; i++) {
|
||||||
|
if (lastKeys[i] != konamiCode[i]) {
|
||||||
|
console.log(i);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
$("#remote").css("display", "");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function flipRemote() {
|
||||||
|
$("#remote").attr("src", "/static/img/remote_active.png");
|
||||||
|
setTimeout(() => {
|
||||||
|
$("#remote").attr("src", "/static/img/remote.png");
|
||||||
|
}, Math.round(Math.random() * 10000) + 1000);
|
||||||
|
}
|
||||||
|
|
|
@ -1,5 +1,21 @@
|
||||||
/// <reference path="./both.js" />
|
/// <reference path="./both.js" />
|
||||||
|
|
||||||
|
function getCookie(cname) {
|
||||||
|
var name = cname + "=";
|
||||||
|
var decodedCookie = decodeURIComponent(document.cookie);
|
||||||
|
var ca = decodedCookie.split(';');
|
||||||
|
for (var i = 0; i < ca.length; i++) {
|
||||||
|
var c = ca[i];
|
||||||
|
while (c.charAt(0) == ' ') {
|
||||||
|
c = c.substring(1);
|
||||||
|
}
|
||||||
|
if (c.indexOf(name) == 0) {
|
||||||
|
return c.substring(name.length, c.length);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
|
||||||
function setPlaying(title, link) {
|
function setPlaying(title, link) {
|
||||||
if (title !== "") {
|
if (title !== "") {
|
||||||
$('#playing').text(title);
|
$('#playing').text(title);
|
||||||
|
@ -50,7 +66,7 @@ function appendMessages(msg) {
|
||||||
}
|
}
|
||||||
|
|
||||||
$("#messages").append(msg);
|
$("#messages").append(msg);
|
||||||
$("#messages").children().last()[0].scrollIntoView({ block: "end", behavior: "smooth" });
|
$("#messages").children().last()[0].scrollIntoView({ block: "end" });
|
||||||
}
|
}
|
||||||
|
|
||||||
function purgeChat() {
|
function purgeChat() {
|
||||||
|
@ -89,6 +105,14 @@ function join() {
|
||||||
}
|
}
|
||||||
setNotifyBox();
|
setNotifyBox();
|
||||||
openChat();
|
openChat();
|
||||||
|
|
||||||
|
let color = getCookie("color");
|
||||||
|
if (color !== "") {
|
||||||
|
// Do a timeout because timings
|
||||||
|
setTimeout(() => {
|
||||||
|
sendMessage("/color " + color);
|
||||||
|
}, 250);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function websocketSend(data) {
|
function websocketSend(data) {
|
||||||
|
@ -117,14 +141,14 @@ function setNotifyBox(msg = "") {
|
||||||
// Button Wrapper Functions
|
// Button Wrapper Functions
|
||||||
function auth() {
|
function auth() {
|
||||||
let pass = prompt("Enter pass");
|
let pass = prompt("Enter pass");
|
||||||
if (pass != "") {
|
if (pass != "" && pass !== null) {
|
||||||
sendMessage("/auth " + pass);
|
sendMessage("/auth " + pass);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function nick() {
|
function nick() {
|
||||||
let nick = prompt("Enter new name");
|
let nick = prompt("Enter new name");
|
||||||
if (nick != "") {
|
if (nick != "" && nick !== null) {
|
||||||
sendMessage("/nick " + nick);
|
sendMessage("/nick " + nick);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -169,6 +193,10 @@ function changeColor() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function setTimestamp(v) {
|
||||||
|
showTimestamp(v)
|
||||||
|
document.cookie = "timestamp=" + v
|
||||||
|
}
|
||||||
|
|
||||||
// Get the websocket setup in a function so it can be recalled
|
// Get the websocket setup in a function so it can be recalled
|
||||||
function setupWebSocket() {
|
function setupWebSocket() {
|
||||||
|
@ -220,9 +248,12 @@ function setupEvents() {
|
||||||
}
|
}
|
||||||
|
|
||||||
function defaultValues() {
|
function defaultValues() {
|
||||||
$("#colorRed").val(0).trigger("input");
|
setTimeout(() => {
|
||||||
$("#colorGreen").val(0).trigger("input");
|
let timestamp = getCookie("timestamp")
|
||||||
$("#colorBlue").val(0).trigger("input");
|
if (timestamp !== "") {
|
||||||
|
showTimestamp(timestamp === "true")
|
||||||
|
}
|
||||||
|
}, 500);
|
||||||
}
|
}
|
||||||
|
|
||||||
window.addEventListener("onresize", updateSuggestionCss);
|
window.addEventListener("onresize", updateSuggestionCss);
|
||||||
|
|
|
@ -57,6 +57,11 @@
|
||||||
{{end}}
|
{{end}}
|
||||||
</div>
|
</div>
|
||||||
<hr />
|
<hr />
|
||||||
|
<label class="contrast">
|
||||||
|
<input type="checkbox" checked="false" onchange="setTimestamp(this.checked);" />
|
||||||
|
Show Timestamp
|
||||||
|
</label>
|
||||||
|
<hr />
|
||||||
<div id="hiddencolor" class="hiddendiv">
|
<div id="hiddencolor" class="hiddendiv">
|
||||||
<div class="range-div" style="background-image: linear-gradient(to right, transparent, red);">
|
<div class="range-div" style="background-image: linear-gradient(to right, transparent, red);">
|
||||||
<input id="colorRed" type="range" min="0" max="255" value="0" oninput="updateColor();" />
|
<input id="colorRed" type="range" min="0" max="255" value="0" oninput="updateColor();" />
|
||||||
|
|
62
wasm/main.go
62
wasm/main.go
|
@ -3,6 +3,7 @@ package main
|
||||||
import (
|
import (
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"sort"
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
@ -10,7 +11,11 @@ import (
|
||||||
"github.com/zorchenhimer/MovieNight/common"
|
"github.com/zorchenhimer/MovieNight/common"
|
||||||
)
|
)
|
||||||
|
|
||||||
var auth common.CommandLevel
|
var (
|
||||||
|
timestamp bool
|
||||||
|
color string
|
||||||
|
auth common.CommandLevel
|
||||||
|
)
|
||||||
|
|
||||||
func recieve(v []js.Value) {
|
func recieve(v []js.Value) {
|
||||||
if len(v) == 0 {
|
if len(v) == 0 {
|
||||||
|
@ -21,7 +26,7 @@ func recieve(v []js.Value) {
|
||||||
chatJSON, err := common.DecodeData(v[0].String())
|
chatJSON, err := common.DecodeData(v[0].String())
|
||||||
if err != nil {
|
if err != nil {
|
||||||
fmt.Printf("Error decoding data: %s\n", err)
|
fmt.Printf("Error decoding data: %s\n", err)
|
||||||
js.Call("appendMessages", v)
|
js.Call("appendMessages", fmt.Sprintf("<div>%v</div>", v))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -39,8 +44,21 @@ func recieve(v []js.Value) {
|
||||||
for _, i := range h.Data.([]interface{}) {
|
for _, i := range h.Data.([]interface{}) {
|
||||||
names = append(names, i.(string))
|
names = append(names, i.(string))
|
||||||
}
|
}
|
||||||
|
sort.Strings(names)
|
||||||
case common.CdAuth:
|
case common.CdAuth:
|
||||||
auth = h.Data.(common.CommandLevel)
|
auth = h.Data.(common.CommandLevel)
|
||||||
|
case common.CdColor:
|
||||||
|
color = h.Data.(string)
|
||||||
|
js.Get("document").Set("cookie", fmt.Sprintf("color=%s;", color))
|
||||||
|
case common.CdEmote:
|
||||||
|
data := h.Data.(map[string]interface{})
|
||||||
|
emoteNames = make([]string, 0, len(data))
|
||||||
|
emotes = make(map[string]string)
|
||||||
|
for k, v := range data {
|
||||||
|
emoteNames = append(emoteNames, k)
|
||||||
|
emotes[k] = v.(string)
|
||||||
|
}
|
||||||
|
sort.Strings(emoteNames)
|
||||||
}
|
}
|
||||||
case common.DTEvent:
|
case common.DTEvent:
|
||||||
d := chat.Data.(common.DataEvent)
|
d := chat.Data.(common.DataEvent)
|
||||||
|
@ -51,7 +69,7 @@ func recieve(v []js.Value) {
|
||||||
// on join or leave, update list of possible user names
|
// on join or leave, update list of possible user names
|
||||||
fallthrough
|
fallthrough
|
||||||
case common.DTChat:
|
case common.DTChat:
|
||||||
js.Call("appendMessages", chat.Data.HTML())
|
appendMessage(chat.Data.HTML())
|
||||||
case common.DTCommand:
|
case common.DTCommand:
|
||||||
d := chat.Data.(common.DataCommand)
|
d := chat.Data.(common.DataCommand)
|
||||||
|
|
||||||
|
@ -70,18 +88,26 @@ func recieve(v []js.Value) {
|
||||||
js.Call("initPlayer", nil)
|
js.Call("initPlayer", nil)
|
||||||
case common.CmdPurgeChat:
|
case common.CmdPurgeChat:
|
||||||
js.Call("purgeChat", nil)
|
js.Call("purgeChat", nil)
|
||||||
js.Call("appendMessages", d.HTML())
|
appendMessage(d.HTML())
|
||||||
case common.CmdHelp:
|
case common.CmdHelp:
|
||||||
url := "/help"
|
url := "/help"
|
||||||
if d.Arguments != nil && len(d.Arguments) > 0 {
|
if d.Arguments != nil && len(d.Arguments) > 0 {
|
||||||
url = d.Arguments[0]
|
url = d.Arguments[0]
|
||||||
}
|
}
|
||||||
js.Call("appendMessages", d.HTML())
|
appendMessage(d.HTML())
|
||||||
js.Get("window").Call("open", url, "_blank", "menubar=0,status=0,toolbar=0,width=300,height=600")
|
js.Get("window").Call("open", url, "_blank", "menubar=0,status=0,toolbar=0,width=300,height=600")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func appendMessage(msg string) {
|
||||||
|
if timestamp {
|
||||||
|
h, m, _ := time.Now().Clock()
|
||||||
|
msg = fmt.Sprintf(`<span class="time">%02d:%02d</span> %s`, h, m, msg)
|
||||||
|
}
|
||||||
|
js.Call("appendMessages", "<div>"+msg+"</div>")
|
||||||
|
}
|
||||||
|
|
||||||
func websocketSend(msg string, dataType common.ClientDataType) error {
|
func websocketSend(msg string, dataType common.ClientDataType) error {
|
||||||
if strings.TrimSpace(msg) == "" && dataType == common.CdMessage {
|
if strings.TrimSpace(msg) == "" && dataType == common.CdMessage {
|
||||||
return nil
|
return nil
|
||||||
|
@ -101,25 +127,33 @@ func websocketSend(msg string, dataType common.ClientDataType) error {
|
||||||
|
|
||||||
func send(this js.Value, v []js.Value) interface{} {
|
func send(this js.Value, v []js.Value) interface{} {
|
||||||
if len(v) != 1 {
|
if len(v) != 1 {
|
||||||
showSendError(fmt.Errorf("expected 1 parameter, got %d", len(v)))
|
showChatError(fmt.Errorf("expected 1 parameter, got %d", len(v)))
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
err := websocketSend(v[0].String(), common.CdMessage)
|
err := websocketSend(v[0].String(), common.CdMessage)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
showSendError(err)
|
showChatError(err)
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
func showSendError(err error) {
|
func showChatError(err error) {
|
||||||
if err != nil {
|
if err != nil {
|
||||||
fmt.Printf("Could not send: %v\n", err)
|
fmt.Printf("Could not send: %v\n", err)
|
||||||
js.Call("appendMessages", `<div><span style="color: red;">Could not send message</span></div>`)
|
js.Call("appendMessages", `<div><span style="color: red;">Could not send message</span></div>`)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func showTimestamp(v []js.Value) {
|
||||||
|
if len(v) != 1 {
|
||||||
|
// Don't bother with returning a value
|
||||||
|
return
|
||||||
|
}
|
||||||
|
timestamp = v[0].Bool()
|
||||||
|
}
|
||||||
|
|
||||||
func isValidColor(this js.Value, v []js.Value) interface{} {
|
func isValidColor(this js.Value, v []js.Value) interface{} {
|
||||||
if len(v) != 1 {
|
if len(v) != 1 {
|
||||||
return false
|
return false
|
||||||
|
@ -135,10 +169,13 @@ func isValidName(this js.Value, v []js.Value) interface{} {
|
||||||
}
|
}
|
||||||
|
|
||||||
func debugValues(v []js.Value) {
|
func debugValues(v []js.Value) {
|
||||||
fmt.Printf("currentName %#v\n", currentName)
|
fmt.Printf("timestamp: %#v\n", timestamp)
|
||||||
fmt.Printf("auth %#v\n", auth)
|
fmt.Printf("auth: %#v\n", auth)
|
||||||
fmt.Printf("names %#v\n", names)
|
fmt.Printf("color: %#v\n", color)
|
||||||
fmt.Printf("filteredNames %#v\n", filteredNames)
|
fmt.Printf("currentSuggestion: %#v\n", currentSug)
|
||||||
|
fmt.Printf("filteredSuggestions: %#v\n", filteredSug)
|
||||||
|
fmt.Printf("names: %#v\n", names)
|
||||||
|
fmt.Printf("emoteNames: %#v\n", emoteNames)
|
||||||
}
|
}
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
|
@ -150,6 +187,7 @@ func main() {
|
||||||
js.Set("recieveMessage", js.CallbackOf(recieve))
|
js.Set("recieveMessage", js.CallbackOf(recieve))
|
||||||
js.Set("processMessage", js.CallbackOf(processMessage))
|
js.Set("processMessage", js.CallbackOf(processMessage))
|
||||||
js.Set("debugValues", js.CallbackOf(debugValues))
|
js.Set("debugValues", js.CallbackOf(debugValues))
|
||||||
|
js.Set("showTimestamp", js.CallbackOf(showTimestamp))
|
||||||
|
|
||||||
// This is needed so the goroutine does not end
|
// This is needed so the goroutine does not end
|
||||||
for {
|
for {
|
||||||
|
|
|
@ -4,6 +4,7 @@ import (
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/dennwc/dom/js"
|
"github.com/dennwc/dom/js"
|
||||||
|
"github.com/zorchenhimer/MovieNight/common"
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
|
@ -11,17 +12,22 @@ const (
|
||||||
keyEnter = 13
|
keyEnter = 13
|
||||||
keyUp = 38
|
keyUp = 38
|
||||||
keyDown = 40
|
keyDown = 40
|
||||||
|
suggestionName = '@'
|
||||||
|
suggestionEmote = ':'
|
||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
currentName string
|
currentSugType rune
|
||||||
|
currentSug string
|
||||||
|
filteredSug []string
|
||||||
names []string
|
names []string
|
||||||
filteredNames []string
|
emoteNames []string
|
||||||
|
emotes map[string]string
|
||||||
)
|
)
|
||||||
|
|
||||||
// The returned value is a bool deciding to prevent the event from propagating
|
// The returned value is a bool deciding to prevent the event from propagating
|
||||||
func processMessageKey(this js.Value, v []js.Value) interface{} {
|
func processMessageKey(this js.Value, v []js.Value) interface{} {
|
||||||
if len(filteredNames) == 0 || currentName == "" {
|
if len(filteredSug) == 0 || currentSug == "" {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -30,12 +36,12 @@ func processMessageKey(this js.Value, v []js.Value) interface{} {
|
||||||
switch keyCode {
|
switch keyCode {
|
||||||
case keyUp, keyDown:
|
case keyUp, keyDown:
|
||||||
newidx := 0
|
newidx := 0
|
||||||
for i, n := range filteredNames {
|
for i, n := range filteredSug {
|
||||||
if n == currentName {
|
if n == currentSug {
|
||||||
newidx = i
|
newidx = i
|
||||||
if keyCode == keyDown {
|
if keyCode == keyDown {
|
||||||
newidx = i + 1
|
newidx = i + 1
|
||||||
if newidx == len(filteredNames) {
|
if newidx == len(filteredSug) {
|
||||||
newidx--
|
newidx--
|
||||||
}
|
}
|
||||||
} else if keyCode == keyUp {
|
} else if keyCode == keyUp {
|
||||||
|
@ -47,14 +53,19 @@ func processMessageKey(this js.Value, v []js.Value) interface{} {
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
currentName = filteredNames[newidx]
|
currentSug = filteredSug[newidx]
|
||||||
case keyTab, keyEnter:
|
case keyTab, keyEnter:
|
||||||
msg := js.Get("msg")
|
msg := js.Get("msg")
|
||||||
val := msg.Get("value").String()
|
val := msg.Get("value").String()
|
||||||
newval := val[:startIdx]
|
newval := val[:startIdx]
|
||||||
|
|
||||||
if i := strings.LastIndex(newval, "@"); i != -1 {
|
if i := strings.LastIndex(newval, string(currentSugType)); i != -1 {
|
||||||
newval = newval[:i+1] + currentName
|
var offset int
|
||||||
|
if currentSugType == suggestionName {
|
||||||
|
offset = 1
|
||||||
|
}
|
||||||
|
|
||||||
|
newval = newval[:i+offset] + currentSug
|
||||||
}
|
}
|
||||||
|
|
||||||
endVal := val[startIdx:]
|
endVal := val[startIdx:]
|
||||||
|
@ -67,7 +78,7 @@ func processMessageKey(this js.Value, v []js.Value) interface{} {
|
||||||
msg.Set("selectionEnd", len(newval)+1)
|
msg.Set("selectionEnd", len(newval)+1)
|
||||||
|
|
||||||
// Clear out filtered names since it is no longer needed
|
// Clear out filtered names since it is no longer needed
|
||||||
filteredNames = nil
|
filteredSug = nil
|
||||||
default:
|
default:
|
||||||
// We only want to handle the caught keys, so return early
|
// We only want to handle the caught keys, so return early
|
||||||
return false
|
return false
|
||||||
|
@ -82,9 +93,9 @@ func processMessage(v []js.Value) {
|
||||||
text := strings.ToLower(msg.Get("value").String())
|
text := strings.ToLower(msg.Get("value").String())
|
||||||
startIdx := msg.Get("selectionStart").Int()
|
startIdx := msg.Get("selectionStart").Int()
|
||||||
|
|
||||||
filteredNames = nil
|
filteredSug = nil
|
||||||
if len(text) != 0 {
|
if len(text) != 0 {
|
||||||
if len(names) > 0 {
|
if len(names) > 0 || len(emoteNames) > 0 {
|
||||||
var caretIdx int
|
var caretIdx int
|
||||||
textParts := strings.Split(text, " ")
|
textParts := strings.Split(text, " ")
|
||||||
|
|
||||||
|
@ -97,18 +108,29 @@ func processMessage(v []js.Value) {
|
||||||
// It is possible to have a double space " ", which will lead to an
|
// It is possible to have a double space " ", which will lead to an
|
||||||
// empty string element in the slice. Also check that the index of the
|
// empty string element in the slice. Also check that the index of the
|
||||||
// cursor is between the start of the word and the end
|
// cursor is between the start of the word and the end
|
||||||
if len(word) > 0 && word[0] == '@' &&
|
if len(word) > 0 && caretIdx <= startIdx && startIdx <= caretIdx+len(word) {
|
||||||
caretIdx <= startIdx && startIdx <= caretIdx+len(word) {
|
var suggestions []string
|
||||||
// fill filtered first so the "modifier" keys can modify it
|
if word[0] == suggestionName {
|
||||||
for _, n := range names {
|
currentSugType = suggestionName
|
||||||
if len(word) == 1 || strings.HasPrefix(strings.ToLower(n), word[1:]) {
|
suggestions = names
|
||||||
filteredNames = append(filteredNames, n)
|
} else if word[0] == suggestionEmote {
|
||||||
|
suggestions = emoteNames
|
||||||
|
currentSugType = suggestionEmote
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, s := range suggestions {
|
||||||
|
if len(word) == 1 || strings.Contains(strings.ToLower(s), word[1:]) {
|
||||||
|
filteredSug = append(filteredSug, s)
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(filteredSug) > 10 {
|
||||||
|
break
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if len(filteredNames) > 0 {
|
if len(filteredSug) > 0 {
|
||||||
currentName = ""
|
currentSug = ""
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -124,26 +146,34 @@ func updateSuggestionDiv() {
|
||||||
const selectedClass = ` class="selectedName"`
|
const selectedClass = ` class="selectedName"`
|
||||||
|
|
||||||
var divs []string
|
var divs []string
|
||||||
if len(filteredNames) > 0 {
|
if len(filteredSug) > 0 {
|
||||||
// set current name to first if not set already
|
// set current name to first if not set already
|
||||||
if currentName == "" {
|
if currentSug == "" {
|
||||||
currentName = filteredNames[0]
|
currentSug = filteredSug[0]
|
||||||
}
|
}
|
||||||
|
|
||||||
var hasCurrentName bool
|
var hascurrentSuggestion bool
|
||||||
divs = make([]string, len(filteredNames))
|
divs = make([]string, len(filteredSug))
|
||||||
|
|
||||||
// Create inner body of html
|
// Create inner body of html
|
||||||
for i := range filteredNames {
|
for i := range filteredSug {
|
||||||
divs[i] = "<div"
|
divs[i] = "<div"
|
||||||
if filteredNames[i] == currentName {
|
|
||||||
hasCurrentName = true
|
sug := filteredSug[i]
|
||||||
|
if sug == currentSug {
|
||||||
|
hascurrentSuggestion = true
|
||||||
divs[i] += selectedClass
|
divs[i] += selectedClass
|
||||||
}
|
}
|
||||||
divs[i] += ">" + filteredNames[i] + "</div>"
|
divs[i] += ">"
|
||||||
|
|
||||||
|
if currentSugType == suggestionEmote {
|
||||||
|
divs[i] += common.EmoteToHtml(emotes[sug], sug)
|
||||||
}
|
}
|
||||||
|
|
||||||
if !hasCurrentName {
|
divs[i] += sug + "</div>"
|
||||||
|
}
|
||||||
|
|
||||||
|
if !hascurrentSuggestion {
|
||||||
divs[0] = divs[0][:4] + selectedClass + divs[0][4:]
|
divs[0] = divs[0][:4] + selectedClass + divs[0][4:]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in New Issue