MovieNight/chatclient.go
Zorchenhimer 7ac34c7d05 Fix name highlighting breaking emotes
Fix name highlighting when emotes are involved.  When the emotes were
moved to subfolders the channel name was put into the URL of the emote
image.  If a user with that name joined the chat, all of the emotes in
that folder highlighted the name in the URL, breaking the <img> tag.

This change removes the regex used to identify and replace the user's
name and instead works on whole words delimited by spaces.
2019-09-22 15:42:48 -04:00

314 lines
7.8 KiB
Go

package main
import (
"fmt"
"html"
"regexp"
"strings"
"time"
"unicode"
"github.com/zorchenhimer/MovieNight/common"
)
var (
regexSpoiler = regexp.MustCompile(`\|\|(.*?)\|\|`)
spoilerStart = `<span class="spoiler" onclick='$(this).removeClass("spoiler").addClass("spoiler-active")'>`
spoilerEnd = `</span>`
)
type Client struct {
name string // Display name
conn *chatConnection
belongsTo *ChatRoom
color string
CmdLevel common.CommandLevel
IsColorForced bool
IsNameForced bool
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
func (cl *Client) NewMsg(data common.ClientData) {
switch data.Type {
case common.CdAuth:
common.LogChatf("[chat|hidden] <%s> get auth level\n", cl.name)
err := cl.SendChatData(common.NewChatHiddenMessage(data.Type, cl.CmdLevel))
if err != nil {
common.LogErrorf("Error sending auth level to client: %v\n", err)
}
case common.CdUsers:
common.LogChatf("[chat|hidden] <%s> get list of users\n", cl.name)
names := chat.GetNames()
idx := -1
for i := range names {
if names[i] == cl.name {
idx = i
}
}
err := cl.SendChatData(common.NewChatHiddenMessage(data.Type, append(names[:idx], names[idx+1:]...)))
if err != nil {
common.LogErrorf("Error sending chat data: %v\n", err)
}
case common.CdMessage:
msg := html.EscapeString(data.Message)
msg = removeDumbSpaces(msg)
msg = strings.Trim(msg, " ")
// Add the spoiler tag outside of the command vs message statement
// because the /me command outputs to the messages
msg = addSpoilerTags(msg)
// Don't send zero-length messages
if len(msg) == 0 {
return
}
if strings.HasPrefix(msg, "/") {
// is a command
msg = msg[1:len(msg)]
fullcmd := strings.Split(msg, " ")
cmd := strings.ToLower(fullcmd[0])
args := fullcmd[1:len(fullcmd)]
response, err := commands.RunCommand(cmd, args, cl)
if response != "" || err != nil {
msgType := common.MsgCommandResponse
respText := response
if err != nil {
respText = err.Error()
msgType = common.MsgCommandError
}
err := cl.SendChatData(common.NewChatMessage("", "",
common.ParseEmotes(respText),
common.CmdlUser,
msgType))
if err != nil {
common.LogErrorf("Error command results %v\n", err)
}
return
}
} 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
if len(msg) > 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)
// Enable links for mods and admins
if cl.CmdLevel >= common.CmdlMod {
msg = formatLinks(msg)
}
cl.Message(msg)
}
}
}
func (cl *Client) SendChatData(data common.ChatData) error {
// Don't send chat or event data to clients that have not fully joined the
// chatroom (ie, they have not set a name).
if cl.name == "" && (data.Type == common.DTChat || data.Type == common.DTEvent) {
return nil
}
// Colorize name on chat messages
if data.Type == common.DTChat {
var err error
data = cl.replaceColorizedName(data)
if err != nil {
return fmt.Errorf("could not colorize name: %v", err)
}
}
cd, err := data.ToJSON()
if err != nil {
return fmt.Errorf("could not create ChatDataJSON of type %d: %v", data.Type, err)
}
return cl.Send(cd)
}
func (cl *Client) Send(data common.ChatDataJSON) error {
err := cl.conn.WriteData(data)
if err != nil {
return fmt.Errorf("could not send message: %v", err)
}
return nil
}
func (cl *Client) SendServerMessage(s string) error {
err := cl.SendChatData(common.NewChatMessage("", ColorServerMessage, s, common.CmdlUser, common.MsgServer))
if err != nil {
return fmt.Errorf("could send server message to %s: message - %#v: %v", cl.name, s, err)
}
return nil
}
// Make links clickable
func formatLinks(input string) string {
newMsg := []string{}
for _, word := range strings.Split(input, " ") {
if strings.HasPrefix(word, "http://") || strings.HasPrefix(word, "https://") {
word = html.UnescapeString(word)
word = fmt.Sprintf(`<a href="%s" target="_blank">%s</a>`, word, word)
}
newMsg = append(newMsg, word)
}
return strings.Join(newMsg, " ")
}
//Exiting out
func (cl *Client) Exit() {
cl.belongsTo.Leave(cl.name, cl.color)
}
// Outgoing messages
func (cl *Client) Message(msg string) {
msg = common.ParseEmotes(msg)
cl.belongsTo.AddMsg(cl, false, false, msg)
}
// Outgoing /me command
func (cl *Client) Me(msg string) {
msg = common.ParseEmotes(msg)
cl.belongsTo.AddMsg(cl, true, false, msg)
}
func (cl *Client) Mod() {
if cl.CmdLevel < common.CmdlMod {
cl.CmdLevel = common.CmdlMod
}
}
func (cl *Client) Unmod() {
cl.CmdLevel = common.CmdlUser
}
func (cl *Client) Host() string {
return cl.conn.Host()
}
func (cl *Client) setName(s string) error {
cl.name = s
if cl.conn != nil {
cl.conn.clientName = s
}
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 {
data := chatData.Data.(common.DataMessage)
words := strings.Split(data.Message, " ")
newWords := []string{}
for _, word := range words {
if strings.ToLower(word) == strings.ToLower(cl.name) || strings.ToLower(word) == strings.ToLower("@"+cl.name) {
newWords = append(newWords, `<span class="mention">`+word+`</span>`)
} else {
newWords = append(newWords, word)
}
}
data.Message = strings.Join(newWords, " ")
chatData.Data = data
return chatData
}
var dumbSpaces = []string{
"\n",
"\t",
"\r",
"\u200b",
}
func removeDumbSpaces(msg string) string {
for _, ds := range dumbSpaces {
msg = strings.ReplaceAll(msg, ds, " ")
}
newMsg := ""
for _, r := range msg {
if unicode.IsSpace(r) {
newMsg += " "
} else {
newMsg += string(r)
}
}
return newMsg
}
func addSpoilerTags(msg string) string {
return regexSpoiler.ReplaceAllString(msg, fmt.Sprintf(`%s$1%s`, spoilerStart, spoilerEnd))
}