MovieNight/chatroom.go
Zorchenhimer c32838def6 Rework settings mutex and saving settings
- Move mutex into Settings struct
- Replace sync.Mutex with a sync.RWMutex
- Move adding approved emotes into a Settings method
- Save settings after adding emotes
- Wrap saving in a lock
2020-01-30 14:32:46 -05:00

476 lines
11 KiB
Go

package main
import (
"fmt"
"strings"
"sync"
"time"
"github.com/zorchenhimer/MovieNight/common"
)
const (
ColorServerMessage string = "#ea6260"
)
type ChatRoom struct {
clients []*Client // this needs to be a pointer. key is suid.
clientsMtx sync.Mutex
queue chan common.ChatData
modqueue chan common.ChatData // mod and admin broadcast messages
playing string
playingLink string
modPasswords []string // single-use mod passwords
modPasswordsMtx sync.Mutex
}
//initializing the chatroom
func newChatRoom() (*ChatRoom, error) {
cr := &ChatRoom{
queue: make(chan common.ChatData, 1000),
modqueue: make(chan common.ChatData, 1000),
clients: []*Client{},
}
err := loadEmotes()
if err != nil {
return nil, fmt.Errorf("error loading emotes: %s", err)
}
common.LogInfof("Loaded %d emotes\n", len(common.Emotes))
//the "heartbeat" for broadcasting messages
go cr.Broadcast()
return cr, nil
}
// A new client joined
func (cr *ChatRoom) Join(conn *chatConnection, data common.JoinData) (*Client, error) {
defer cr.clientsMtx.Unlock()
cr.clientsMtx.Lock()
sendHiddenMessage := func(cd common.ClientDataType, i interface{}) {
// If the message cant be converted, then just don't send
if d, err := common.NewChatHiddenMessage(cd, i).ToJSON(); err == nil {
conn.WriteJSON(d)
}
}
if settings.RoomAccess == AccessPin && data.Name == settings.RoomAccessPin {
sendHiddenMessage(common.CdNotify, "That's the access pin! Please enter a name.")
return nil, UserFormatError{Name: data.Name}
}
if !common.IsValidName(data.Name) {
sendHiddenMessage(common.CdNotify, common.InvalidNameError)
return nil, UserFormatError{Name: data.Name}
}
nameLower := strings.ToLower(data.Name)
for _, client := range cr.clients {
if strings.ToLower(client.name) == nameLower {
sendHiddenMessage(common.CdNotify, "Name already taken")
return nil, UserTakenError{Name: data.Name}
}
}
// If color is invalid, then set it to a random color
if !common.IsValidColor(data.Color) {
data.Color = common.RandomColor()
}
client, err := NewClient(conn, cr, data.Name, data.Color)
if err != nil {
sendHiddenMessage(common.CdNotify, "Could not join client")
return nil, fmt.Errorf("Unable to join client: %v", err)
}
// Overwrite to use client instead
sendHiddenMessage = func(cd common.ClientDataType, i interface{}) {
client.SendChatData(common.NewChatHiddenMessage(cd, i))
}
host := client.Host()
if banned, names := settings.IsBanned(host); banned {
sendHiddenMessage(common.CdNotify, "You are banned")
return nil, newBannedUserError(host, data.Name, names)
}
cr.clients = append(cr.clients, client)
common.LogChatf("[join] %s %s\n", host, data.Color)
playingCommand, err := common.NewChatCommand(common.CmdPlaying, []string{cr.playing, cr.playingLink}).ToJSON()
if err != nil {
common.LogErrorf("Unable to encode playing command on join: %s\n", err)
} else {
client.Send(playingCommand)
}
cr.AddEventMsg(common.EvJoin, data.Name, data.Color)
sendHiddenMessage(common.CdJoin, nil)
sendHiddenMessage(common.CdEmote, common.Emotes)
stats.updateMaxUsers(len(cr.clients))
return client, nil
}
// TODO: fix this up a bit. kick and leave are the same, incorrect, error: "That
// name was already used!" leaving the chatroom
func (cr *ChatRoom) Leave(name, color string) {
defer cr.clientsMtx.Unlock()
cr.clientsMtx.Lock() //preventing simultaneous access to the `clients` map
client, id, err := cr.getClient(name)
if err != nil {
common.LogErrorf("[leave] Unable to get client suid %v\n", err)
return
}
host := client.Host()
name = client.name // grab the name from here for proper capitalization
client.conn.Close()
cr.delClient(id)
cr.AddEventMsg(common.EvLeave, name, color)
common.LogChatf("[leave] %s %s\n", host, name)
}
// kicked from the chatroom
func (cr *ChatRoom) Kick(name string) error {
defer cr.clientsMtx.Unlock()
cr.clientsMtx.Lock() //preventing simultaneous access to the `clients` map
client, id, err := cr.getClient(name)
if err != nil {
return fmt.Errorf("Unable to get client for name " + name)
}
if client.CmdLevel == common.CmdlMod {
return fmt.Errorf("You cannot kick another mod.")
}
if client.CmdLevel == common.CmdlAdmin {
return fmt.Errorf("Jebaited No.")
}
color := client.color
host := client.Host()
client.conn.Close()
cr.delClient(id)
cr.AddEventMsg(common.EvKick, name, color)
common.LogInfof("[kick] %s %s has been kicked\n", host, name)
return nil
}
func (cr *ChatRoom) Ban(name string) error {
defer cr.clientsMtx.Unlock()
cr.clientsMtx.Lock()
client, id, err := cr.getClient(name)
if err != nil {
common.LogErrorf("[ban] Unable to get client for name %q\n", name)
return fmt.Errorf("Cannot find that name")
}
if client.CmdLevel == common.CmdlAdmin {
return fmt.Errorf("You cannot ban an admin Jebaited")
}
names := []string{}
host := client.Host()
color := client.color
// Remove the named client
client.conn.Close()
cr.delClient(id)
// Remove additional clients on that IP address
for id, c := range cr.clients {
if c.Host() == host {
names = append(names, client.name)
client.conn.Close()
cr.delClient(id)
}
}
err = settings.AddBan(host, names)
if err != nil {
common.LogErrorf("[BAN] Error banning %q: %s\n", name, err)
cr.AddEventMsg(common.EvKick, name, color)
} else {
cr.AddEventMsg(common.EvBan, name, color)
}
return nil
}
// Add a chat message from a viewer
func (cr *ChatRoom) AddMsg(from *Client, isAction, isServer bool, msg string) {
t := common.MsgChat
if isAction {
t = common.MsgAction
}
if isServer {
t = common.MsgServer
}
cr.AddChatMsg(common.NewChatMessage(from.name, from.color, msg, from.CmdLevel, t))
}
// Add a chat message object to the queue
func (cr *ChatRoom) AddChatMsg(data common.ChatData) {
select {
case cr.queue <- data:
default:
common.LogErrorln("Unable to queue chat message. Channel full.")
}
}
func (cr *ChatRoom) AddCmdMsg(command common.CommandType, args []string) {
select {
case cr.queue <- common.NewChatCommand(command, args):
default:
common.LogErrorln("Unable to queue command message. Channel full.")
}
}
func (cr *ChatRoom) AddModNotice(message string) {
select {
case cr.modqueue <- common.NewChatMessage("", "", message, common.CmdlUser, common.MsgNotice):
default:
common.LogErrorln("Unable to queue notice. Channel full.")
}
}
func (cr *ChatRoom) AddEventMsg(event common.EventType, name, color string) {
select {
case cr.queue <- common.NewChatEvent(event, name, color):
default:
common.LogErrorln("Unable to queue event message. Channel full.")
}
}
func (cr *ChatRoom) Unmod(name string) error {
defer cr.clientsMtx.Unlock()
cr.clientsMtx.Lock()
client, _, err := cr.getClient(name)
if err != nil {
return err
}
client.Unmod()
client.SendServerMessage(`You have been unmodded.`)
return nil
}
func (cr *ChatRoom) Mod(name string) error {
defer cr.clientsMtx.Unlock()
cr.clientsMtx.Lock()
client, _, err := cr.getClient(name)
if err != nil {
return err
}
if client.CmdLevel < common.CmdlMod {
client.CmdLevel = common.CmdlMod
client.SendServerMessage(`You have been modded.`)
}
return nil
}
func (cr *ChatRoom) ForceColorChange(name, color string) error {
defer cr.clientsMtx.Unlock()
cr.clientsMtx.Lock()
client, _, err := cr.getClient(name)
if err != nil {
return err
}
client.IsColorForced = true
client.color = color
return nil
}
func (cr *ChatRoom) UserCount() int {
return len(cr.clients)
}
//broadcasting all the messages in the queue in one block
func (cr *ChatRoom) Broadcast() {
send := func(data common.ChatData, client *Client) {
err := client.SendChatData(data)
if err != nil {
common.LogErrorf("Error sending data to client: %v\n", err)
}
}
for {
select {
case msg := <-cr.queue:
cr.clientsMtx.Lock()
for _, client := range cr.clients {
go send(msg, client)
}
cr.clientsMtx.Unlock()
case msg := <-cr.modqueue:
cr.clientsMtx.Lock()
for _, client := range cr.clients {
if client.CmdLevel >= common.CmdlMod {
send(msg, client)
}
}
cr.clientsMtx.Unlock()
default:
time.Sleep(50 * time.Millisecond)
// No messages to send
// This default block is required so the above case
// does not block.
}
}
}
func (cr *ChatRoom) ClearPlaying() {
cr.playing = ""
cr.playingLink = ""
cr.AddCmdMsg(common.CmdPlaying, []string{"", ""})
}
func (cr *ChatRoom) SetPlaying(title, link string) {
cr.playing = title
cr.playingLink = link
cr.AddCmdMsg(common.CmdPlaying, []string{title, link})
}
func (cr *ChatRoom) GetNames() []string {
names := []string{}
defer cr.clientsMtx.Unlock()
cr.clientsMtx.Lock()
for _, val := range cr.clients {
names = append(names, val.name)
}
return names
}
func (cr *ChatRoom) delClient(sliceId int) {
cr.clients = append(cr.clients[:sliceId], cr.clients[sliceId+1:]...)
}
func (cr *ChatRoom) getClient(name string) (*Client, int, error) {
for id, client := range cr.clients {
if client.name == name {
return client, id, nil
}
}
return nil, -1, fmt.Errorf("client with that name not found")
}
func (cr *ChatRoom) generateModPass() string {
defer cr.modPasswordsMtx.Unlock()
cr.modPasswordsMtx.Lock()
pass, err := generatePass(time.Now().Unix())
if err != nil {
return fmt.Sprintf("Error generating moderator password: %s", err)
}
// Make sure the password is unique
for existsInSlice(cr.modPasswords, pass) {
pass, err = generatePass(time.Now().Unix())
if err != nil {
return fmt.Sprintf("Error generating moderator password: %s", err)
}
}
cr.modPasswords = append(cr.modPasswords, pass)
return pass
}
func (cr *ChatRoom) redeemModPass(pass string) bool {
if pass == "" {
return false
}
defer cr.modPasswordsMtx.Unlock()
cr.modPasswordsMtx.Lock()
if existsInSlice(cr.modPasswords, pass) {
cr.modPasswords = removeFromSlice(cr.modPasswords, pass)
return true
}
return false
}
func removeFromSlice(slice []string, needle string) []string {
slc := []string{}
for _, item := range slice {
if item != needle {
slc = append(slc, item)
}
}
return slc
}
func existsInSlice(slice []string, needle string) bool {
for _, item := range slice {
if item == needle {
return true
}
}
return false
}
func (cr *ChatRoom) changeName(oldName, newName string, forced bool) error {
cr.clientsMtx.Lock()
defer cr.clientsMtx.Unlock()
if !common.IsValidName(newName) {
return fmt.Errorf("%q nick is not a valid name", newName)
}
newLower := strings.ToLower(newName)
oldLower := strings.ToLower(oldName)
var currentClient *Client
for _, client := range cr.clients {
if strings.ToLower(client.name) == newLower {
if strings.ToLower(client.name) != oldLower {
return fmt.Errorf("%q is already taken.", newName)
}
}
if strings.ToLower(client.name) == oldLower {
currentClient = client
}
}
if currentClient != nil {
err := currentClient.setName(newName)
if err != nil {
return fmt.Errorf("could not set client name to %#v: %v", newName, err)
}
common.LogDebugf("%q -> %q\n", oldName, newName)
if forced {
cr.AddEventMsg(common.EvNameChangeForced, oldName+":"+newName, currentClient.color)
currentClient.IsNameForced = true
} else {
cr.AddEventMsg(common.EvNameChange, oldName+":"+newName, currentClient.color)
}
return nil
}
return fmt.Errorf("Client not found with name %q", oldName)
}