2019-03-10 16:42:12 +01:00
|
|
|
package main
|
|
|
|
|
|
|
|
import (
|
2019-03-13 21:47:19 +01:00
|
|
|
"errors"
|
2019-03-10 16:42:12 +01:00
|
|
|
"fmt"
|
2019-03-12 04:15:42 +01:00
|
|
|
|
2019-03-13 21:47:19 +01:00
|
|
|
uuid "github.com/satori/go.uuid"
|
|
|
|
|
2019-03-10 16:42:12 +01:00
|
|
|
"math/rand"
|
|
|
|
"path/filepath"
|
|
|
|
"regexp"
|
|
|
|
"strings"
|
|
|
|
"sync"
|
|
|
|
"time"
|
|
|
|
|
|
|
|
"github.com/gorilla/websocket"
|
2019-03-13 06:09:24 +01:00
|
|
|
"github.com/zorchenhimer/MovieNight/common"
|
2019-03-10 16:42:12 +01:00
|
|
|
)
|
|
|
|
|
2019-03-12 04:15:42 +01:00
|
|
|
const (
|
|
|
|
UsernameMaxLength int = 36
|
|
|
|
UsernameMinLength int = 3
|
|
|
|
)
|
2019-03-10 16:42:12 +01:00
|
|
|
|
|
|
|
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
|
2019-03-13 21:47:19 +01:00
|
|
|
tempConn map[string]*websocket.Conn
|
2019-03-10 16:42:12 +01:00
|
|
|
queue chan string
|
|
|
|
playing string
|
|
|
|
playingLink string
|
2019-03-13 21:47:32 +01:00
|
|
|
|
|
|
|
modPasswords []string // single-use mod passwords
|
|
|
|
modPasswordsMtx sync.Mutex
|
2019-03-10 16:42:12 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
//initializing the chatroom
|
2019-03-13 21:47:19 +01:00
|
|
|
func newChatRoom() (*ChatRoom, error) {
|
|
|
|
cr := &ChatRoom{
|
|
|
|
queue: make(chan string, 5),
|
|
|
|
clients: make(map[string]*Client),
|
|
|
|
tempConn: make(map[string]*websocket.Conn),
|
|
|
|
}
|
2019-03-10 16:42:12 +01:00
|
|
|
|
|
|
|
num, err := LoadEmotes()
|
|
|
|
if err != nil {
|
2019-03-13 21:47:19 +01:00
|
|
|
return nil, fmt.Errorf("Error loading emotes: %s", err)
|
2019-03-10 16:42:12 +01:00
|
|
|
}
|
|
|
|
fmt.Printf("Loaded %d emotes\n", num)
|
|
|
|
|
|
|
|
//the "heartbeat" for broadcasting messages
|
|
|
|
go func() {
|
|
|
|
for {
|
|
|
|
cr.BroadCast()
|
|
|
|
time.Sleep(100 * time.Millisecond)
|
|
|
|
}
|
|
|
|
}()
|
2019-03-13 21:47:19 +01:00
|
|
|
return cr, nil
|
2019-03-10 16:42:12 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
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])
|
|
|
|
}
|
|
|
|
|
2019-03-13 21:47:19 +01:00
|
|
|
func (cr *ChatRoom) JoinTemp(conn *websocket.Conn) (string, error) {
|
|
|
|
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 "", errors.New("%#v is already in the temp connections")
|
|
|
|
}
|
|
|
|
|
|
|
|
cr.tempConn[suid] = conn
|
|
|
|
return suid, nil
|
|
|
|
}
|
|
|
|
|
2019-03-10 16:42:12 +01:00
|
|
|
//registering a new client
|
|
|
|
//returns pointer to a Client, or Nil, if the name is already taken
|
2019-03-13 21:47:19 +01:00
|
|
|
func (cr *ChatRoom) Join(name, uid string) (*Client, error) {
|
|
|
|
conn, hasConn := cr.tempConn[uid]
|
|
|
|
if !hasConn {
|
|
|
|
return nil, errors.New("connection is missing from temp connections")
|
|
|
|
}
|
2019-03-10 16:42:12 +01:00
|
|
|
|
2019-03-12 04:15:42 +01:00
|
|
|
if len(name) < UsernameMinLength || len(name) > UsernameMaxLength || !re_username.MatchString(name) {
|
|
|
|
return nil, UserFormatError{Name: name}
|
2019-03-10 16:42:12 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
defer cr.clientsMtx.Unlock()
|
|
|
|
cr.clientsMtx.Lock() //preventing simultaneous access to the `clients` map
|
|
|
|
if _, exists := cr.clients[strings.ToLower(name)]; exists {
|
2019-03-12 04:15:42 +01:00
|
|
|
return nil, UserTakenError{Name: name}
|
2019-03-10 16:42:12 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
client := &Client{
|
|
|
|
name: name,
|
|
|
|
conn: conn,
|
|
|
|
belongsTo: cr,
|
|
|
|
color: randomColor(),
|
|
|
|
}
|
|
|
|
|
|
|
|
host := client.Host()
|
|
|
|
|
|
|
|
if banned, names := settings.IsBanned(host); banned {
|
2019-03-12 04:15:42 +01:00
|
|
|
return nil, newBannedUserError(host, name, names)
|
2019-03-10 16:42:12 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
cr.clients[strings.ToLower(name)] = client
|
2019-03-13 21:47:19 +01:00
|
|
|
delete(cr.tempConn, uid)
|
2019-03-10 16:42:12 +01:00
|
|
|
|
|
|
|
fmt.Printf("[join] %s %s\n", host, name)
|
2019-03-13 06:09:24 +01:00
|
|
|
//client.Send(cr.GetPlayingString())
|
|
|
|
playingCommand, err := common.EncodeCommand(common.CMD_PLAYING, []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.AddMsg(fmt.Sprintf("<i><b style=\"color:%s\">%s</b> has joined the chat.</i><br />", client.color, name))
|
|
|
|
cr.AddEventMsg(common.EV_JOIN, name, client.color)
|
2019-03-12 04:15:42 +01:00
|
|
|
return client, nil
|
2019-03-10 16:42:12 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
// 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)
|
|
|
|
|
2019-03-13 06:09:24 +01:00
|
|
|
//cr.AddMsg(fmt.Sprintf("<i><b style=\"color:%s\">%s</b> has left the chat.</i><br />", color, name))
|
|
|
|
cr.AddEventMsg(common.EV_LEAVE, name, color)
|
2019-03-10 16:42:12 +01:00
|
|
|
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."
|
|
|
|
}
|
|
|
|
|
2019-03-13 06:09:24 +01:00
|
|
|
color := client.color
|
2019-03-10 16:42:12 +01:00
|
|
|
host := client.Host()
|
|
|
|
client.conn.Close()
|
|
|
|
cr.delClient(name)
|
|
|
|
|
2019-03-13 06:09:24 +01:00
|
|
|
//cr.AddMsg(fmt.Sprintf("<i><b>%s</b> has been kicked.</i><br />", name))
|
|
|
|
cr.AddEventMsg(common.EV_KICK, name, color)
|
2019-03-10 16:42:12 +01:00
|
|
|
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()
|
2019-03-13 06:09:24 +01:00
|
|
|
color := client.color
|
2019-03-10 16:42:12 +01:00
|
|
|
|
|
|
|
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)
|
2019-03-13 06:09:24 +01:00
|
|
|
//cr.AddMsg(fmt.Sprintf("<i><b>%s</b> has been kicked.</i><br />", name))
|
|
|
|
cr.AddEventMsg(common.EV_KICK, name, color)
|
2019-03-10 16:42:12 +01:00
|
|
|
} else {
|
2019-03-13 06:09:24 +01:00
|
|
|
//cr.AddMsg(fmt.Sprintf("<i><b>%s</b> has been banned.</i><br />", name))
|
|
|
|
cr.AddEventMsg(common.EV_BAN, name, color)
|
2019-03-10 16:42:12 +01:00
|
|
|
}
|
|
|
|
return ""
|
|
|
|
}
|
|
|
|
|
2019-03-13 06:09:24 +01:00
|
|
|
// Add a chat message from a viewer
|
|
|
|
func (cr *ChatRoom) AddMsg(from *Client, isAction, isServer bool, msg string) {
|
2019-03-13 18:42:38 +01:00
|
|
|
t := common.MSG_CHAT
|
|
|
|
|
|
|
|
if isAction {
|
|
|
|
t = common.MSG_ACTION
|
|
|
|
}
|
|
|
|
|
|
|
|
if isServer {
|
|
|
|
t = common.MSG_SERVER
|
|
|
|
}
|
|
|
|
|
|
|
|
data, err := common.EncodeMessage(
|
2019-03-13 06:09:24 +01:00
|
|
|
from.name,
|
|
|
|
from.color,
|
|
|
|
msg,
|
2019-03-13 18:42:38 +01:00
|
|
|
t)
|
2019-03-13 06:09:24 +01:00
|
|
|
|
|
|
|
if err != nil {
|
|
|
|
fmt.Printf("Error encoding chat message: %s", err)
|
|
|
|
cr.queue <- msg
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
cr.queue <- data
|
2019-03-10 16:42:12 +01:00
|
|
|
}
|
|
|
|
|
2019-03-13 06:09:24 +01:00
|
|
|
func (cr *ChatRoom) AddCmdMsg(command common.CommandType, args []string) {
|
|
|
|
data, err := common.EncodeCommand(command, args)
|
|
|
|
|
|
|
|
if err != nil {
|
|
|
|
fmt.Printf("Error encoding command: %s", err)
|
|
|
|
//cr.queue <- msg
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
cr.queue <- data
|
|
|
|
}
|
|
|
|
|
|
|
|
func (cr *ChatRoom) AddEventMsg(event common.EventType, name, color string) {
|
|
|
|
data, err := common.EncodeEvent(event, name, color)
|
|
|
|
|
|
|
|
if err != nil {
|
|
|
|
fmt.Printf("Error encoding command: %s", err)
|
|
|
|
//cr.queue <- msg
|
|
|
|
return
|
|
|
|
}
|
|
|
|
cr.queue <- data
|
2019-03-10 16:42:12 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
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.ServerMessage(`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.ServerMessage(`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() {
|
|
|
|
msgBlock := ""
|
|
|
|
infLoop:
|
|
|
|
for {
|
|
|
|
select {
|
|
|
|
case m := <-cr.queue:
|
2019-03-12 23:15:45 +01:00
|
|
|
msgBlock += m
|
2019-03-10 16:42:12 +01:00
|
|
|
default:
|
|
|
|
break infLoop
|
|
|
|
}
|
|
|
|
}
|
|
|
|
if len(msgBlock) > 0 {
|
|
|
|
for _, client := range cr.clients {
|
|
|
|
client.Send(msgBlock)
|
|
|
|
}
|
2019-03-13 21:47:19 +01:00
|
|
|
for _, conn := range cr.tempConn {
|
|
|
|
connSend(msgBlock, conn)
|
|
|
|
}
|
2019-03-10 16:42:12 +01:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
func (cr *ChatRoom) ClearPlaying() {
|
|
|
|
cr.playing = ""
|
|
|
|
cr.playingLink = ""
|
2019-03-13 06:09:24 +01:00
|
|
|
//cr.AddCmdMsg(`<script>setPlaying("","");</script>`)
|
|
|
|
cr.AddCmdMsg(common.CMD_PLAYING, []string{"", ""})
|
2019-03-10 16:42:12 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
func (cr *ChatRoom) SetPlaying(title, link string) {
|
|
|
|
cr.playing = title
|
|
|
|
cr.playingLink = link
|
2019-03-13 06:09:24 +01:00
|
|
|
//cr.AddCmdMsg(cr.GetPlayingString())
|
|
|
|
cr.AddCmdMsg(common.CMD_PLAYING, []string{title, link})
|
2019-03-10 16:42:12 +01:00
|
|
|
}
|
|
|
|
|
2019-03-13 06:09:24 +01:00
|
|
|
//func (cr *ChatRoom) GetPlayingString() string {
|
|
|
|
// return fmt.Sprintf(`<script>setPlaying("%s","%s");</script>`, cr.playing, cr.playingLink)
|
|
|
|
//}
|
2019-03-10 16:42:12 +01:00
|
|
|
|
|
|
|
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.")
|
|
|
|
}
|
2019-03-13 21:47:32 +01:00
|
|
|
|
|
|
|
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
|
|
|
|
}
|