MovieNight/chatroom.go

494 lines
11 KiB
Go

package main
import (
"errors"
"fmt"
uuid "github.com/satori/go.uuid"
"math/rand"
"path/filepath"
"regexp"
"strings"
"sync"
"time"
"github.com/gorilla/websocket"
"github.com/zorchenhimer/MovieNight/common"
)
const (
UsernameMaxLength int = 36
UsernameMinLength int = 3
ColorServerMessage string = "#ea6260"
)
var re_username *regexp.Regexp = regexp.MustCompile(`^[0-9a-zA-Z_-]+$`)
type ChatRoom struct {
clients map[string]*Client // this needs to be a pointer.
clientsMtx sync.Mutex
tempConn map[string]*websocket.Conn
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, 100),
modqueue: make(chan common.ChatData, 100),
clients: make(map[string]*Client),
tempConn: make(map[string]*websocket.Conn),
}
num, err := LoadEmotes()
if err != nil {
return nil, fmt.Errorf("error loading emotes: %s", err)
}
fmt.Printf("Loaded %d emotes\n", num)
//the "heartbeat" for broadcasting messages
go func() {
for {
cr.BroadCast()
time.Sleep(50 * time.Millisecond)
}
}()
return cr, nil
}
func LoadEmotes() (int, error) {
newEmotes := map[string]string{}
emotePNGs, err := filepath.Glob("./static/emotes/*.png")
if err != nil {
return 0, fmt.Errorf("unable to glob emote directory: %s\n", err)
}
emoteGIFs, err := filepath.Glob("./static/emotes/*.gif")
if err != nil {
return 0, fmt.Errorf("unable to glob emote directory: %s\n", err)
}
globbed_files := []string(emotePNGs)
globbed_files = append(globbed_files, emoteGIFs...)
fmt.Println("Loading emotes...")
for _, file := range globbed_files {
file = filepath.Base(file)
key := file[0 : len(file)-4]
newEmotes[key] = file
fmt.Printf("%s ", key)
}
emotes = newEmotes
fmt.Println("")
return len(emotes), nil
}
func randomColor() string {
nums := []int32{}
for i := 0; i < 6; i++ {
nums = append(nums, rand.Int31n(15))
}
return fmt.Sprintf("#%X%X%X%X%X%X",
nums[0], nums[1], nums[2],
nums[3], nums[4], nums[5])
}
func (cr *ChatRoom) JoinTemp(conn *websocket.Conn) (string, error) {
defer cr.clientsMtx.Unlock()
cr.clientsMtx.Lock()
if conn == nil {
return "", errors.New("conn should not be nil")
}
uid, err := uuid.NewV4()
if err != nil {
return "", fmt.Errorf("could not create uuid: %v", err)
}
suid := uid.String()
if _, ok := cr.tempConn[suid]; ok {
return "", fmt.Errorf("%#v is already in the temp connections", suid)
}
cr.tempConn[suid] = conn
return suid, nil
}
//registering a new client
//returns pointer to a Client, or Nil, if the name is already taken
func (cr *ChatRoom) Join(name, uid string) (*Client, error) {
defer cr.clientsMtx.Unlock()
cr.clientsMtx.Lock()
conn, hasConn := cr.tempConn[uid]
if !hasConn {
return nil, errors.New("connection is missing from temp connections")
}
if len(name) < UsernameMinLength || len(name) > UsernameMaxLength || !re_username.MatchString(name) {
return nil, UserFormatError{Name: name}
}
if _, exists := cr.clients[strings.ToLower(name)]; exists {
return nil, UserTakenError{Name: name}
}
client := &Client{
name: name,
conn: conn,
belongsTo: cr,
color: randomColor(),
}
host := client.Host()
if banned, names := settings.IsBanned(host); banned {
return nil, newBannedUserError(host, name, names)
}
cr.clients[strings.ToLower(name)] = client
delete(cr.tempConn, uid)
fmt.Printf("[join] %s %s\n", host, name)
playingCommand, err := common.NewChatCommand(common.CmdPlaying, []string{cr.playing, cr.playingLink})()
if err != nil {
fmt.Printf("Unable to encode playing command on join: %s\n", err)
} else {
client.Send(playingCommand)
}
cr.AddEventMsg(common.EvJoin, name, client.color)
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, err := cr.getClient(name)
if err != nil {
fmt.Printf("[leave] Unable to get client for name %q\n", name)
return
}
client.conn.Close()
cr.delClient(name)
cr.AddEventMsg(common.EvLeave, name, color)
fmt.Printf("[leave] %s %s\n", client.Host(), client.name)
}
// kicked from the chatroom
func (cr *ChatRoom) Kick(name string) string {
defer cr.clientsMtx.Unlock()
cr.clientsMtx.Lock() //preventing simultaneous access to the `clients` map
client, err := cr.getClient(name)
if err != nil {
return "Unable to get client for name " + name
}
if client.IsMod {
return "You cannot kick another mod."
}
if client.IsAdmin {
return "Jebaited No."
}
color := client.color
host := client.Host()
client.conn.Close()
cr.delClient(name)
cr.AddEventMsg(common.EvKick, name, color)
fmt.Printf("[kick] %s %s has been kicked\n", host, name)
return ""
}
func (cr *ChatRoom) Ban(name string) string {
defer cr.clientsMtx.Unlock()
cr.clientsMtx.Lock()
client, err := cr.getClient(name)
if err != nil {
fmt.Printf("[ban] Unable to get client for name %q\n", name)
return "Cannot find that name"
}
names := []string{}
host := client.Host()
color := client.color
client.conn.Close()
cr.delClient(name)
for name, c := range cr.clients {
if c.Host() == host {
names = append(names, name)
client.conn.Close()
cr.delClient(name)
}
}
defer settingsMtx.Unlock()
settingsMtx.Lock()
err = settings.AddBan(host, names)
if err != nil {
fmt.Printf("[BAN] Error banning %q: %s\n", name, err)
cr.AddEventMsg(common.EvKick, name, color)
} else {
cr.AddEventMsg(common.EvBan, name, color)
}
return ""
}
// 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
}
data, err := common.NewChatMessage(from.name, from.color, msg, t)()
if err != nil {
fmt.Printf("Error encoding chat message: %s", err)
return
}
select {
case cr.queue <- data:
default:
fmt.Println("Unable to queue chat message. Channel full.")
}
}
func (cr *ChatRoom) AddCmdMsg(command common.CommandType, args []string) {
data, err := common.NewChatCommand(command, args)()
if err != nil {
fmt.Printf("Error encoding command: %s", err)
return
}
select {
case cr.queue <- data:
default:
fmt.Println("Unable to queue command message. Channel full.")
}
}
func (cr *ChatRoom) AddModNotice(message string) {
data, err := common.NewChatMessage("", "", message, common.MsgNotice)()
if err != nil {
fmt.Printf("Error encoding notice: %v", err)
return
}
select {
case cr.modqueue <- data:
default:
fmt.Println("Unable to queue notice. Channel full.")
}
}
func (cr *ChatRoom) AddEventMsg(event common.EventType, name, color string) {
data, err := common.NewChatEvent(event, name, color)()
if err != nil {
fmt.Printf("Error encoding command: %s", err)
return
}
select {
case cr.queue <- data:
default:
fmt.Println("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
}
client.IsMod = true
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() {
running := true
for running {
cr.clientsMtx.Lock()
select {
case msg := <-cr.queue:
for _, client := range cr.clients {
client.Send(msg)
}
for _, conn := range cr.tempConn {
conn.WriteJSON(msg)
}
default:
// No messages to send
// This default block is required so the above case
// does not block.
}
// Mod queue
select {
case msg := <-cr.modqueue:
for _, client := range cr.clients {
if client.IsMod || client.IsAdmin {
client.Send(msg)
}
}
default:
running = false
}
cr.clientsMtx.Unlock()
}
}
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(name string) {
delete(cr.clients, strings.ToLower(name))
}
func (cr *ChatRoom) getClient(name string) (*Client, error) {
if client, ok := cr.clients[strings.ToLower(name)]; ok {
return client, nil
}
return nil, 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
}