2019-03-10 16:42:12 +01:00
|
|
|
package main
|
|
|
|
|
|
|
|
import (
|
|
|
|
"fmt"
|
|
|
|
"html"
|
2019-03-21 14:04:57 +01:00
|
|
|
"regexp"
|
2019-03-10 16:42:12 +01:00
|
|
|
"strings"
|
2019-03-30 20:39:04 +01:00
|
|
|
"time"
|
2019-03-10 16:42:12 +01:00
|
|
|
"unicode"
|
|
|
|
|
2019-03-13 18:42:38 +01:00
|
|
|
"github.com/zorchenhimer/MovieNight/common"
|
2019-03-10 16:42:12 +01:00
|
|
|
)
|
|
|
|
|
2019-03-25 04:43:30 +01:00
|
|
|
var (
|
|
|
|
regexSpoiler = regexp.MustCompile(`\|\|(.*?)\|\|`)
|
|
|
|
spoilerStart = `<span class="spoiler" onclick='$(this).removeClass("spoiler").addClass("spoiler-active")'>`
|
|
|
|
spoilerEnd = `</span>`
|
|
|
|
)
|
|
|
|
|
2019-03-10 16:42:12 +01:00
|
|
|
type Client struct {
|
|
|
|
name string // Display name
|
2019-03-16 18:44:18 +01:00
|
|
|
conn *chatConnection
|
2019-03-10 16:42:12 +01:00
|
|
|
belongsTo *ChatRoom
|
|
|
|
color string
|
2019-03-24 14:24:57 +01:00
|
|
|
CmdLevel common.CommandLevel
|
2019-03-10 16:42:12 +01:00
|
|
|
IsColorForced bool
|
2019-03-19 22:03:34 +01:00
|
|
|
IsNameForced bool
|
2019-03-25 04:43:30 +01:00
|
|
|
regexName *regexp.Regexp
|
2019-03-30 20:39:04 +01:00
|
|
|
|
|
|
|
// 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
|
2019-03-10 16:42:12 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
//Client has a new message to broadcast
|
2019-03-14 20:21:53 +01:00
|
|
|
func (cl *Client) NewMsg(data common.ClientData) {
|
2019-03-15 07:19:16 +01:00
|
|
|
switch data.Type {
|
2019-03-24 14:24:57 +01:00
|
|
|
case common.CdAuth:
|
2019-03-25 00:03:01 +01:00
|
|
|
common.LogChatf("[chat|hidden] <%s> get auth level\n", cl.name)
|
2019-03-24 14:24:57 +01:00
|
|
|
err := cl.SendChatData(common.NewChatHiddenMessage(data.Type, cl.CmdLevel))
|
|
|
|
if err != nil {
|
2019-03-25 00:03:01 +01:00
|
|
|
common.LogErrorf("Error sending auth level to client: %v\n", err)
|
2019-03-24 14:24:57 +01:00
|
|
|
}
|
2019-03-15 07:19:16 +01:00
|
|
|
case common.CdUsers:
|
2019-03-24 23:51:39 +01:00
|
|
|
common.LogChatf("[chat|hidden] <%s> get list of users\n", cl.name)
|
2019-03-15 22:28:29 +01:00
|
|
|
|
|
|
|
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:]...)))
|
2019-03-15 07:19:16 +01:00
|
|
|
if err != nil {
|
2019-03-24 23:51:39 +01:00
|
|
|
common.LogErrorf("Error sending chat data: %v\n", err)
|
2019-03-10 16:42:12 +01:00
|
|
|
}
|
2019-03-15 07:19:16 +01:00
|
|
|
case common.CdMessage:
|
|
|
|
msg := html.EscapeString(data.Message)
|
|
|
|
msg = removeDumbSpaces(msg)
|
|
|
|
msg = strings.Trim(msg, " ")
|
|
|
|
|
2019-03-25 04:43:30 +01:00
|
|
|
// Add the spoiler tag outside of the command vs message statement
|
|
|
|
// because the /me command outputs to the messages
|
|
|
|
msg = addSpoilerTags(msg)
|
|
|
|
|
2021-03-14 03:13:20 +01:00
|
|
|
msgLen := len(msg)
|
|
|
|
|
2019-03-15 07:19:16 +01:00
|
|
|
// Don't send zero-length messages
|
2021-03-14 03:13:20 +01:00
|
|
|
if msgLen == 0 {
|
2019-03-15 07:19:16 +01:00
|
|
|
return
|
2019-03-10 16:42:12 +01:00
|
|
|
}
|
|
|
|
|
2019-03-15 07:19:16 +01:00
|
|
|
if strings.HasPrefix(msg, "/") {
|
|
|
|
// is a command
|
2021-03-14 03:13:20 +01:00
|
|
|
msg = msg[1:msgLen]
|
2019-03-15 07:19:16 +01:00
|
|
|
fullcmd := strings.Split(msg, " ")
|
|
|
|
cmd := strings.ToLower(fullcmd[0])
|
2021-03-14 03:13:20 +01:00
|
|
|
fullcmdLen := len(fullcmd)
|
|
|
|
args := fullcmd[1:fullcmdLen]
|
2019-03-15 07:19:16 +01:00
|
|
|
|
2019-04-12 03:06:55 +02:00
|
|
|
response, err := commands.RunCommand(cmd, args, cl)
|
|
|
|
if response != "" || err != nil {
|
2019-04-12 04:00:14 +02:00
|
|
|
msgType := common.MsgCommandResponse
|
2019-04-12 03:06:55 +02:00
|
|
|
respText := response
|
|
|
|
if err != nil {
|
|
|
|
respText = err.Error()
|
2019-04-12 04:00:14 +02:00
|
|
|
msgType = common.MsgCommandError
|
2019-04-12 03:06:55 +02:00
|
|
|
}
|
|
|
|
|
2019-03-16 23:11:20 +01:00
|
|
|
err := cl.SendChatData(common.NewChatMessage("", "",
|
2019-04-12 03:06:55 +02:00
|
|
|
common.ParseEmotes(respText),
|
2019-03-24 14:24:57 +01:00
|
|
|
common.CmdlUser,
|
2019-04-12 04:00:14 +02:00
|
|
|
msgType))
|
2019-03-15 22:28:29 +01:00
|
|
|
if err != nil {
|
2019-03-24 23:51:39 +01:00
|
|
|
common.LogErrorf("Error command results %v\n", err)
|
2019-03-15 22:28:29 +01:00
|
|
|
}
|
2019-03-15 07:19:16 +01:00
|
|
|
return
|
|
|
|
}
|
2019-03-10 16:42:12 +01:00
|
|
|
|
2019-03-15 07:19:16 +01:00
|
|
|
} else {
|
2019-03-30 20:39:04 +01:00
|
|
|
// 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
|
|
|
|
}
|
|
|
|
|
2019-03-15 07:19:16 +01:00
|
|
|
// Trim long messages
|
2021-03-14 03:13:20 +01:00
|
|
|
if msgLen > 400 {
|
2019-03-15 07:19:16 +01:00
|
|
|
msg = msg[0:400]
|
|
|
|
}
|
|
|
|
|
2019-03-30 20:39:04 +01:00
|
|
|
// 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))
|
|
|
|
|
2019-03-24 23:51:39 +01:00
|
|
|
common.LogChatf("[chat] <%s> %q\n", cl.name, msg)
|
2019-03-10 16:42:12 +01:00
|
|
|
|
2019-03-15 07:19:16 +01:00
|
|
|
// Enable links for mods and admins
|
2019-03-24 14:24:57 +01:00
|
|
|
if cl.CmdLevel >= common.CmdlMod {
|
2019-03-15 07:19:16 +01:00
|
|
|
msg = formatLinks(msg)
|
|
|
|
}
|
|
|
|
|
|
|
|
cl.Message(msg)
|
|
|
|
}
|
2019-03-10 16:42:12 +01:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2019-03-21 13:47:40 +01:00
|
|
|
func (cl *Client) SendChatData(data common.ChatData) error {
|
2019-04-13 21:56:49 +02:00
|
|
|
// 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
|
|
|
|
}
|
|
|
|
|
2019-03-21 14:04:57 +01:00
|
|
|
// Colorize name on chat messages
|
|
|
|
if data.Type == common.DTChat {
|
2019-03-25 04:43:30 +01:00
|
|
|
var err error
|
|
|
|
data = cl.replaceColorizedName(data)
|
|
|
|
if err != nil {
|
|
|
|
return fmt.Errorf("could not colorize name: %v", err)
|
|
|
|
}
|
2019-03-21 14:04:57 +01:00
|
|
|
}
|
|
|
|
|
2019-03-21 13:47:40 +01:00
|
|
|
cd, err := data.ToJSON()
|
2019-03-15 22:28:29 +01:00
|
|
|
if err != nil {
|
2019-03-21 13:47:40 +01:00
|
|
|
return fmt.Errorf("could not create ChatDataJSON of type %d: %v", data.Type, err)
|
2019-03-15 22:28:29 +01:00
|
|
|
}
|
|
|
|
return cl.Send(cd)
|
|
|
|
}
|
|
|
|
|
2019-03-21 13:47:40 +01:00
|
|
|
func (cl *Client) Send(data common.ChatDataJSON) error {
|
2019-03-16 18:44:18 +01:00
|
|
|
err := cl.conn.WriteData(data)
|
2019-03-15 22:28:29 +01:00
|
|
|
if err != nil {
|
|
|
|
return fmt.Errorf("could not send message: %v", err)
|
|
|
|
}
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
|
|
|
func (cl *Client) SendServerMessage(s string) error {
|
2019-03-24 14:24:57 +01:00
|
|
|
err := cl.SendChatData(common.NewChatMessage("", ColorServerMessage, s, common.CmdlUser, common.MsgServer))
|
2019-03-15 22:28:29 +01:00
|
|
|
if err != nil {
|
2019-03-21 13:47:40 +01:00
|
|
|
return fmt.Errorf("could send server message to %s: message - %#v: %v", cl.name, s, err)
|
2019-03-15 22:28:29 +01:00
|
|
|
}
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
2019-03-10 16:42:12 +01:00
|
|
|
// Make links clickable
|
|
|
|
func formatLinks(input string) string {
|
|
|
|
newMsg := []string{}
|
|
|
|
for _, word := range strings.Split(input, " ") {
|
2019-03-11 20:41:41 +01:00
|
|
|
if strings.HasPrefix(word, "http://") || strings.HasPrefix(word, "https://") {
|
2019-03-10 16:42:12 +01:00
|
|
|
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) {
|
2019-03-19 22:03:34 +01:00
|
|
|
msg = common.ParseEmotes(msg)
|
2019-03-13 06:09:24 +01:00
|
|
|
cl.belongsTo.AddMsg(cl, false, false, msg)
|
2019-03-10 16:42:12 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
// Outgoing /me command
|
|
|
|
func (cl *Client) Me(msg string) {
|
2019-03-19 22:03:34 +01:00
|
|
|
msg = common.ParseEmotes(msg)
|
2019-03-13 06:09:24 +01:00
|
|
|
cl.belongsTo.AddMsg(cl, true, false, msg)
|
2019-03-10 16:42:12 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
func (cl *Client) Mod() {
|
2019-03-24 14:24:57 +01:00
|
|
|
if cl.CmdLevel < common.CmdlMod {
|
|
|
|
cl.CmdLevel = common.CmdlMod
|
|
|
|
}
|
2019-03-10 16:42:12 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
func (cl *Client) Unmod() {
|
2019-03-24 14:24:57 +01:00
|
|
|
cl.CmdLevel = common.CmdlUser
|
2019-03-10 16:42:12 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
func (cl *Client) Host() string {
|
2019-03-20 21:57:29 +01:00
|
|
|
return cl.conn.Host()
|
2019-03-10 16:42:12 +01:00
|
|
|
}
|
|
|
|
|
2019-03-25 04:43:30 +01:00
|
|
|
func (cl *Client) setName(s string) error {
|
|
|
|
cl.name = s
|
2019-09-22 21:42:48 +02:00
|
|
|
if cl.conn != nil {
|
|
|
|
cl.conn.clientName = s
|
|
|
|
}
|
2019-03-25 04:43:30 +01:00
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
2019-03-29 01:01:48 +01:00
|
|
|
func (cl *Client) setColor(s string) error {
|
|
|
|
cl.color = s
|
|
|
|
return cl.SendChatData(common.NewChatHiddenMessage(common.CdColor, cl.color))
|
|
|
|
}
|
|
|
|
|
2019-03-25 04:43:30 +01:00
|
|
|
func (cl *Client) replaceColorizedName(chatData common.ChatData) common.ChatData {
|
|
|
|
data := chatData.Data.(common.DataMessage)
|
2019-09-22 21:42:48 +02:00
|
|
|
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, " ")
|
2019-03-25 04:43:30 +01:00
|
|
|
chatData.Data = data
|
|
|
|
return chatData
|
|
|
|
}
|
|
|
|
|
2019-03-10 18:41:06 +01:00
|
|
|
var dumbSpaces = []string{
|
2019-03-10 16:42:12 +01:00
|
|
|
"\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
|
|
|
|
}
|
2019-03-21 14:04:57 +01:00
|
|
|
|
2019-03-25 04:43:30 +01:00
|
|
|
func addSpoilerTags(msg string) string {
|
|
|
|
return regexSpoiler.ReplaceAllString(msg, fmt.Sprintf(`%s$1%s`, spoilerStart, spoilerEnd))
|
2019-03-21 14:04:57 +01:00
|
|
|
}
|