diff --git a/.gitignore b/.gitignore index 44bcd13..d387c94 100644 --- a/.gitignore +++ b/.gitignore @@ -12,6 +12,9 @@ # Output of the go coverage tool, specifically when used with LiteIDE *.out +# Log files +*.log + # GoCode debug file debug @@ -29,3 +32,6 @@ settings.json # Autobuilt wasm files static/main.wasm + +# tags for vim +tags diff --git a/Makefile b/Makefile index 8eda388..8ad7e2f 100644 --- a/Makefile +++ b/Makefile @@ -1,15 +1,22 @@ -.PHONY: fmt vet get clean +TAGS= -all: fmt vet MovieNight MovieNight.exe static/main.wasm +.PHONY: fmt vet get clean dev setdev test + +all: fmt vet test MovieNight MovieNight.exe static/main.wasm + +setdev: + $(eval export TAGS=-tags "dev") + +dev: setdev all MovieNight.exe: *.go common/*.go - GOOS=windows GOARCH=amd64 go build -o MovieNight.exe + GOOS=windows GOARCH=amd64 go build -o MovieNight.exe $(TAGS) MovieNight: *.go common/*.go - GOOS=linux GOARCH=386 go build -o MovieNight + GOOS=linux GOARCH=386 go build -o MovieNight $(TAGS) static/main.wasm: wasm/*.go common/*.go - GOOS=js GOARCH=wasm go build -o ./static/main.wasm wasm/*.go + GOOS=js GOARCH=wasm go build -o ./static/main.wasm $(TAGS) wasm/*.go clean: -rm MovieNight.exe MovieNight ./static/main.wasm @@ -23,5 +30,8 @@ get: go get golang.org/x/tools/cmd/goimports vet: - go vet ./... - GOOS=js GOARCH=wasm go vet ./... + go vet $(TAGS) ./... + GOOS=js GOARCH=wasm go vet $(TAGS) ./... + +test: + go test $(TAGS) ./... \ No newline at end of file diff --git a/chatclient.go b/chatclient.go index 9174ae0..244daeb 100644 --- a/chatclient.go +++ b/chatclient.go @@ -10,22 +10,34 @@ import ( "github.com/zorchenhimer/MovieNight/common" ) +var ( + regexSpoiler = regexp.MustCompile(`\|\|(.*?)\|\|`) + spoilerStart = `` + spoilerEnd = `` +) + type Client struct { name string // Display name conn *chatConnection belongsTo *ChatRoom color string - IsMod bool - IsAdmin bool + CmdLevel common.CommandLevel IsColorForced bool IsNameForced bool + regexName *regexp.Regexp } //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: - fmt.Printf("[chat|hidden] <%s> get list of users\n", cl.name) + common.LogChatf("[chat|hidden] <%s> get list of users\n", cl.name) names := chat.GetNames() idx := -1 @@ -37,13 +49,17 @@ func (cl *Client) NewMsg(data common.ClientData) { err := cl.SendChatData(common.NewChatHiddenMessage(data.Type, append(names[:idx], names[idx+1:]...))) if err != nil { - fmt.Printf("Error sending chat data: %v\n", err) + 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 @@ -60,10 +76,10 @@ func (cl *Client) NewMsg(data common.ClientData) { if response != "" { err := cl.SendChatData(common.NewChatMessage("", "", common.ParseEmotes(response), - common.CmdUser, + common.CmdlUser, common.MsgCommandResponse)) if err != nil { - fmt.Printf("Error command results %v\n", err) + common.LogErrorf("Error command results %v\n", err) } return } @@ -74,10 +90,10 @@ func (cl *Client) NewMsg(data common.ClientData) { msg = msg[0:400] } - fmt.Printf("[chat] <%s> %q\n", cl.name, msg) + common.LogChatf("[chat] <%s> %q\n", cl.name, msg) // Enable links for mods and admins - if cl.IsMod || cl.IsAdmin { + if cl.CmdLevel >= common.CmdlMod { msg = formatLinks(msg) } @@ -89,7 +105,11 @@ func (cl *Client) NewMsg(data common.ClientData) { func (cl *Client) SendChatData(data common.ChatData) error { // Colorize name on chat messages if data.Type == common.DTChat { - data = replaceColorizedName(data, cl) + var err error + data = cl.replaceColorizedName(data) + if err != nil { + return fmt.Errorf("could not colorize name: %v", err) + } } cd, err := data.ToJSON() @@ -108,7 +128,7 @@ func (cl *Client) Send(data common.ChatDataJSON) error { } func (cl *Client) SendServerMessage(s string) error { - err := cl.SendChatData(common.NewChatMessage("", ColorServerMessage, s, common.CmdUser, common.MsgServer)) + 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) } @@ -146,17 +166,37 @@ func (cl *Client) Me(msg string) { } func (cl *Client) Mod() { - cl.IsMod = true + if cl.CmdLevel < common.CmdlMod { + cl.CmdLevel = common.CmdlMod + } } func (cl *Client) Unmod() { - cl.IsMod = false + cl.CmdLevel = common.CmdlUser } func (cl *Client) Host() string { return cl.conn.Host() } +func (cl *Client) setName(s string) error { + regex, err := regexp.Compile(fmt.Sprintf("(%s|@%s)", s, s)) + if err != nil { + return fmt.Errorf("could not compile regex: %v", err) + } + + cl.name = s + cl.regexName = regex + return nil +} + +func (cl *Client) replaceColorizedName(chatData common.ChatData) common.ChatData { + data := chatData.Data.(common.DataMessage) + data.Message = cl.regexName.ReplaceAllString(data.Message, `$1`) + chatData.Data = data + return chatData +} + var dumbSpaces = []string{ "\n", "\t", @@ -180,12 +220,6 @@ func removeDumbSpaces(msg string) string { return newMsg } -func replaceColorizedName(chatData common.ChatData, client *Client) common.ChatData { - data := chatData.Data.(common.DataMessage) - - data.Message = regexp.MustCompile(fmt.Sprintf(`(%s|@%s)`, client.name, client.name)). - ReplaceAllString(data.Message, `$1`) - - chatData.Data = data - return chatData +func addSpoilerTags(msg string) string { + return regexSpoiler.ReplaceAllString(msg, fmt.Sprintf(`%s$1%s`, spoilerStart, spoilerEnd)) } diff --git a/chatclient_test.go b/chatclient_test.go new file mode 100644 index 0000000..d757cd1 --- /dev/null +++ b/chatclient_test.go @@ -0,0 +1,23 @@ +package main + +import "testing" + +func TestClient_addSpoilerTag(t *testing.T) { + data := [][]string{ + {"||||", spoilerStart + spoilerEnd}, + {"|||||", spoilerStart + spoilerEnd + "|"}, + {"||||||", spoilerStart + spoilerEnd + "||"}, + {"|||||||", spoilerStart + spoilerEnd + "|||"}, + {"||||||||", spoilerStart + spoilerEnd + spoilerStart + spoilerEnd}, + {"||test||", spoilerStart + "test" + spoilerEnd}, + {"|| ||", spoilerStart + " " + spoilerEnd}, + {"|s|||", "|s|||"}, + } + + for i := range data { + s := addSpoilerTags(data[i][0]) + if s != data[i][1] { + t.Errorf("expected %#v, got %#v with %#v", data[i][1], s, data[i][0]) + } + } +} diff --git a/chatcommands.go b/chatcommands.go index 7ff728c..8e1e696 100644 --- a/chatcommands.go +++ b/chatcommands.go @@ -52,29 +52,28 @@ var commands = &CommandControl{ common.CNAuth.String(): Command{ HelpText: "Authenticate to admin", Function: func(cl *Client, args []string) string { - if cl.IsAdmin { + if cl.CmdLevel == common.CmdlAdmin { return "You are already authenticated." } pw := html.UnescapeString(strings.Join(args, " ")) if settings.AdminPassword == pw { - cl.IsMod = true - cl.IsAdmin = true + cl.CmdLevel = common.CmdlAdmin cl.belongsTo.AddModNotice(cl.name + " used the admin password") - fmt.Printf("[auth] %s used the admin password\n", cl.name) + common.LogInfof("[auth] %s used the admin password\n", cl.name) return "Admin rights granted." } if cl.belongsTo.redeemModPass(pw) { - cl.IsMod = true + cl.CmdLevel = common.CmdlMod cl.belongsTo.AddModNotice(cl.name + " used a mod password") - fmt.Printf("[auth] %s used a mod password\n", cl.name) + common.LogInfof("[auth] %s used a mod password\n", cl.name) return "Moderator privileges granted." } cl.belongsTo.AddModNotice(cl.name + " attempted to auth without success") - fmt.Printf("[auth] %s gave an invalid password\n", cl.name) + common.LogInfof("[auth] %s gave an invalid password\n", cl.name) return "Invalid password." }, }, @@ -94,20 +93,22 @@ var commands = &CommandControl{ return "Missing name to change to." } - newName := args[0] + newName := strings.TrimLeft(args[0], "@") oldName := cl.name forced := false + + // Two arguments to force a name change on another user: `/nick OldName NewName` if len(args) == 2 { - if !cl.IsAdmin { + if cl.CmdLevel != common.CmdlAdmin { return "Only admins can do that PeepoSus" } - oldName = args[0] - newName = args[1] + oldName = strings.TrimLeft(args[0], "@") + newName = strings.TrimLeft(args[1], "@") forced = true } - if len(args) == 1 && cl.IsNameForced && !cl.IsAdmin { + if len(args) == 1 && cl.IsNameForced && cl.CmdLevel != common.CmdlAdmin { return "You cannot change your name once it has been changed by an admin." } @@ -178,22 +179,23 @@ var commands = &CommandControl{ common.CNUnmod.String(): Command{ HelpText: "Revoke a user's moderator privilages. Moderators can only unmod themselves.", Function: func(cl *Client, args []string) string { - if len(args) > 0 && !cl.IsAdmin && cl.name != args[0] { + if len(args) > 0 && cl.CmdLevel != common.CmdlAdmin && cl.name != args[0] { return "You can only unmod yourself, not others." } - if len(args) == 0 || (len(args) == 1 && args[0] == cl.name) { + if len(args) == 0 || (len(args) == 1 && strings.TrimLeft(args[0], "@") == cl.name) { cl.Unmod() cl.belongsTo.AddModNotice(cl.name + " has unmodded themselves") return "You have unmodded yourself." } + name := strings.TrimLeft(args[0], "@") - if err := cl.belongsTo.Unmod(args[0]); err != nil { + if err := cl.belongsTo.Unmod(name); err != nil { return err.Error() } - cl.belongsTo.AddModNotice(cl.name + " has unmodded " + args[0]) - return fmt.Sprintf(`%s has been unmodded.`, args[0]) + cl.belongsTo.AddModNotice(cl.name + " has unmodded " + name) + return "" }, }, @@ -203,7 +205,7 @@ var commands = &CommandControl{ if len(args) == 0 { return "Missing name to kick." } - return cl.belongsTo.Kick(args[0]) + return cl.belongsTo.Kick(strings.TrimLeft(args[0], "@")) }, }, @@ -213,8 +215,10 @@ var commands = &CommandControl{ if len(args) == 0 { return "missing name to ban." } - fmt.Printf("[ban] Attempting to ban %s\n", strings.Join(args, "")) - return cl.belongsTo.Ban(args[0]) + + name := strings.TrimLeft(args[0], "@") + common.LogInfof("[ban] Attempting to ban %s\n", name) + return cl.belongsTo.Ban(name) }, }, @@ -224,13 +228,14 @@ var commands = &CommandControl{ if len(args) == 0 { return "missing name to unban." } - fmt.Printf("[ban] Attempting to unban %s\n", strings.Join(args, "")) + name := strings.TrimLeft(args[0], "@") + common.LogInfof("[ban] Attempting to unban %s\n", name) - err := settings.RemoveBan(args[0]) + err := settings.RemoveBan(name) if err != nil { return err.Error() } - cl.belongsTo.AddModNotice(cl.name + " has unbanned " + args[0]) + cl.belongsTo.AddModNotice(cl.name + " has unbanned " + name) return "" }, }, @@ -238,7 +243,7 @@ var commands = &CommandControl{ common.CNPurge.String(): Command{ HelpText: "Purge the chat.", Function: func(cl *Client, args []string) string { - fmt.Println("[purge] clearing chat") + common.LogInfoln("[purge] clearing chat") cl.belongsTo.AddCmdMsg(common.CmdPurgeChat, nil) return "" }, @@ -265,11 +270,13 @@ var commands = &CommandControl{ if len(args) == 0 { return "Missing user to mod." } - if err := cl.belongsTo.Mod(args[0]); err != nil { + + name := strings.TrimLeft(args[0], "@") + if err := cl.belongsTo.Mod(name); err != nil { return err.Error() } - cl.belongsTo.AddModNotice(cl.name + " has modded " + args[0]) - return fmt.Sprintf(`%s has been modded.`, args[0]) + cl.belongsTo.AddModNotice(cl.name + " has modded " + name) + return "" }, }, @@ -288,12 +295,12 @@ var commands = &CommandControl{ cl.SendServerMessage("Reloading emotes") num, err := common.LoadEmotes() if err != nil { - fmt.Printf("Unbale to reload emotes: %s\n", err) + common.LogErrorf("Unbale to reload emotes: %s\n", err) return fmt.Sprintf("ERROR: %s", err) } cl.belongsTo.AddModNotice(cl.name + " has reloaded emotes") - fmt.Printf("Loaded %d emotes\n", num) + common.LogInfof("Loaded %d emotes\n", num) return fmt.Sprintf("Emotes loaded: %d", num) }, }, @@ -367,17 +374,17 @@ var commands = &CommandControl{ }, common.CNIP.String(): Command{ - HelpText: "list users and IP in the server console", + HelpText: "List users and IP in the server console. Requires logging level to be set to info or above.", Function: func(cl *Client, args []string) string { cl.belongsTo.clientsMtx.Lock() - fmt.Println("Clients:") + common.LogInfoln("Clients:") for uuid, client := range cl.belongsTo.clients { - fmt.Printf(" [%s] %s %s\n", uuid, client.name, client.conn.Host()) + common.LogInfof(" [%s] %s %s\n", uuid, client.name, client.conn.Host()) } - fmt.Println("TmpConn:") + common.LogInfoln("TmpConn:") for uuid, conn := range cl.belongsTo.tempConn { - fmt.Printf(" [%s] %s\n", uuid, conn.Host()) + common.LogInfof(" [%s] %s\n", uuid, conn.Host()) } cl.belongsTo.clientsMtx.Unlock() return "see console for output" @@ -392,44 +399,45 @@ func (cc *CommandControl) RunCommand(command string, args []string, sender *Clie // Look for user command if userCmd, ok := cc.user[cmd]; ok { - fmt.Printf("[user] %s /%s %s\n", sender.name, command, strings.Join(args, " ")) + common.LogInfof("[user] %s /%s %s\n", sender.name, command, strings.Join(args, " ")) return userCmd.Function(sender, args) } // Look for mod command if modCmd, ok := cc.mod[cmd]; ok { - if sender.IsMod || sender.IsAdmin { - fmt.Printf("[mod] %s /%s %s\n", sender.name, command, strings.Join(args, " ")) + if sender.CmdLevel >= common.CmdlMod { + common.LogInfof("[mod] %s /%s %s\n", sender.name, command, strings.Join(args, " ")) return modCmd.Function(sender, args) } - fmt.Printf("[mod REJECTED] %s /%s %s\n", sender.name, command, strings.Join(args, " ")) + common.LogInfof("[mod REJECTED] %s /%s %s\n", sender.name, command, strings.Join(args, " ")) return "You are not a mod Jebaited" } // Look for admin command if adminCmd, ok := cc.admin[cmd]; ok { - if sender.IsAdmin { - fmt.Printf("[admin] %s /%s %s\n", sender.name, command, strings.Join(args, " ")) + if sender.CmdLevel == common.CmdlAdmin { + common.LogInfof("[admin] %s /%s %s\n", sender.name, command, strings.Join(args, " ")) return adminCmd.Function(sender, args) } - fmt.Printf("[admin REJECTED] %s /%s %s\n", sender.name, command, strings.Join(args, " ")) + common.LogInfof("[admin REJECTED] %s /%s %s\n", sender.name, command, strings.Join(args, " ")) return "You are not the admin Jebaited" } // Command not found - fmt.Printf("[cmd] %s /%s %s\n", sender.name, command, strings.Join(args, " ")) + common.LogInfof("[cmd] %s /%s %s\n", sender.name, command, strings.Join(args, " ")) return "Invalid command." } func cmdHelp(cl *Client, args []string) string { url := "/help" - if cl.IsMod { - url = "/help?mod=1" + + if cl.CmdLevel >= common.CmdlMod { + url += "?mod=1" } - if cl.IsAdmin { - url = "/help?mod=1&admin=1" + if cl.CmdLevel == common.CmdlAdmin { + url += "&admin=1" } cl.SendChatData(common.NewChatCommand(common.CmdHelp, []string{url})) @@ -439,11 +447,11 @@ func cmdHelp(cl *Client, args []string) string { func getHelp(lvl common.CommandLevel) map[string]string { var cmdList map[string]Command switch lvl { - case common.CmdUser: + case common.CmdlUser: cmdList = commands.user - case common.CmdMod: + case common.CmdlMod: cmdList = commands.mod - case common.CmdAdmin: + case common.CmdlAdmin: cmdList = commands.admin } @@ -459,22 +467,71 @@ func getHelp(lvl common.CommandLevel) map[string]string { var cmdColor = Command{ HelpText: "Change user color.", Function: func(cl *Client, args []string) string { + if len(args) > 2 { + return "Too many arguments!" + } + // If the caller is priviledged enough, they can change the color of another user - if len(args) == 2 && (cl.IsMod || cl.IsAdmin) { - color := "" - name := "" - for _, s := range args { - if common.IsValidColor(s) { - color = s - } else { - name = s - } + if len(args) == 2 { + if cl.CmdLevel == common.CmdlUser { + return "You cannot change someone else's color. PeepoSus" + } + + name, color := "", "" + + if strings.ToLower(args[0]) == strings.ToLower(args[1]) || + (common.IsValidColor(args[0]) && common.IsValidColor(args[1])) { + return "Name and color are ambiguous. Prefix the name with '@' or color with '#'" + } + + // Check for explicit name + if strings.HasPrefix(args[0], "@") { + name = strings.TrimLeft(args[0], "@") + color = args[1] + common.LogDebugln("[color:mod] Found explicit name: ", name) + } else if strings.HasPrefix(args[1], "@") { + name = strings.TrimLeft(args[1], "@") + color = args[0] + common.LogDebugln("[color:mod] Found explicit name: ", name) + + // Check for explicit color + } else if strings.HasPrefix(args[0], "#") { + name = strings.TrimPrefix(args[1], "@") // this shouldn't be needed, but just in case. + color = args[0] + common.LogDebugln("[color:mod] Found explicit color: ", color) + } else if strings.HasPrefix(args[1], "#") { + name = strings.TrimPrefix(args[0], "@") // this shouldn't be needed, but just in case. + color = args[1] + common.LogDebugln("[color:mod] Found explicit color: ", color) + + // Guess + } else if common.IsValidColor(args[0]) { + name = strings.TrimPrefix(args[1], "@") + color = args[0] + common.LogDebugln("[color:mod] Guessed name: ", name, " and color: ", color) + } else if common.IsValidColor(args[1]) { + name = strings.TrimPrefix(args[0], "@") + color = args[1] + common.LogDebugln("[color:mod] Guessed name: ", name, " and color: ", color) + } + + if name == "" { + return "Cannot determine name. Prefix name with @." } if color == "" { - fmt.Printf("[color:mod] %s missing color\n", cl.name) + return "Cannot determine color. Prefix name with @." + } + + if color == "" { + common.LogInfof("[color:mod] %s missing color\n", cl.name) return "Missing color" } + if name == "" { + common.LogInfof("[color:mod] %s missing name\n", cl.name) + return "Missing name" + } + if err := cl.belongsTo.ForceColorChange(name, color); err != nil { return err.Error() } @@ -484,7 +541,7 @@ var cmdColor = Command{ // Don't allow an unprivilaged user to change their color if // it was changed by a mod if cl.IsColorForced { - fmt.Printf("[color] %s tried to change a forced color\n", cl.name) + common.LogInfof("[color] %s tried to change a forced color\n", cl.name) return "You are not allowed to change your color." } @@ -499,7 +556,7 @@ var cmdColor = Command{ } cl.color = args[0] - fmt.Printf("[color] %s new color: %s\n", cl.name, cl.color) + common.LogInfof("[color] %s new color: %s\n", cl.name, cl.color) return "Color changed successfully." }, } @@ -507,6 +564,9 @@ var cmdColor = Command{ var cmdWhoAmI = Command{ HelpText: "Shows debug user info", Function: func(cl *Client, args []string) string { - return fmt.Sprintf("Name: %s IsMod: %t IsAdmin: %t", cl.name, cl.IsMod, cl.IsAdmin) + return fmt.Sprintf("Name: %s IsMod: %t IsAdmin: %t", + cl.name, + cl.CmdLevel >= common.CmdlMod, + cl.CmdLevel == common.CmdlAdmin) }, } diff --git a/chatroom.go b/chatroom.go index a699fd6..aebde3e 100644 --- a/chatroom.go +++ b/chatroom.go @@ -45,7 +45,7 @@ func newChatRoom() (*ChatRoom, error) { if err != nil { return nil, fmt.Errorf("error loading emotes: %s", err) } - fmt.Printf("Loaded %d emotes\n", num) + common.LogInfof("Loaded %d emotes\n", num) //the "heartbeat" for broadcasting messages go cr.Broadcast() @@ -85,7 +85,7 @@ func (cr *ChatRoom) Join(name, uid string) (*Client, error) { return nil, errors.New("connection is missing from temp connections") } - if !common.IsValidName(name) || common.IsValidColor(name) { + if !common.IsValidName(name) { return nil, UserFormatError{Name: name} } @@ -98,12 +98,16 @@ func (cr *ChatRoom) Join(name, uid string) (*Client, error) { conn.clientName = name client := &Client{ - name: name, conn: conn, belongsTo: cr, color: common.RandomColor(), } + err := client.setName(name) + if err != nil { + return nil, fmt.Errorf("could not set client name to %#v: %v", name, err) + } + host := client.Host() if banned, names := settings.IsBanned(host); banned { @@ -113,10 +117,10 @@ func (cr *ChatRoom) Join(name, uid string) (*Client, error) { cr.clients[uid] = client delete(cr.tempConn, uid) - fmt.Printf("[join] %s %s\n", host, name) + common.LogChatf("[join] %s %s\n", host, name) playingCommand, err := common.NewChatCommand(common.CmdPlaying, []string{cr.playing, cr.playingLink}).ToJSON() if err != nil { - fmt.Printf("Unable to encode playing command on join: %s\n", err) + common.LogErrorf("Unable to encode playing command on join: %s\n", err) } else { client.Send(playingCommand) } @@ -132,7 +136,7 @@ func (cr *ChatRoom) Leave(name, color string) { client, suid, err := cr.getClient(name) if err != nil { - fmt.Printf("[leave] Unable to get client suid %v\n", err) + common.LogErrorf("[leave] Unable to get client suid %v\n", err) return } host := client.Host() @@ -141,7 +145,7 @@ func (cr *ChatRoom) Leave(name, color string) { cr.delClient(suid) cr.AddEventMsg(common.EvLeave, name, color) - fmt.Printf("[leave] %s %s\n", host, name) + common.LogChatf("[leave] %s %s\n", host, name) } // kicked from the chatroom @@ -154,11 +158,11 @@ func (cr *ChatRoom) Kick(name string) string { return "Unable to get client for name " + name } - if client.IsMod { + if client.CmdLevel == common.CmdlMod { return "You cannot kick another mod." } - if client.IsAdmin { + if client.CmdLevel == common.CmdlAdmin { return "Jebaited No." } @@ -168,7 +172,7 @@ func (cr *ChatRoom) Kick(name string) string { cr.delClient(suid) cr.AddEventMsg(common.EvKick, name, color) - fmt.Printf("[kick] %s %s has been kicked\n", host, name) + common.LogInfof("[kick] %s %s has been kicked\n", host, name) return "" } @@ -178,11 +182,11 @@ func (cr *ChatRoom) Ban(name string) string { client, suid, err := cr.getClient(name) if err != nil { - fmt.Printf("[ban] Unable to get client for name %q\n", name) + common.LogErrorf("[ban] Unable to get client for name %q\n", name) return "Cannot find that name" } - if client.IsAdmin { + if client.CmdLevel == common.CmdlAdmin { return "You cannot ban an admin Jebaited" } @@ -206,7 +210,7 @@ func (cr *ChatRoom) Ban(name string) string { err = settings.AddBan(host, names) if err != nil { - fmt.Printf("[BAN] Error banning %q: %s\n", name, err) + common.LogErrorf("[BAN] Error banning %q: %s\n", name, err) cr.AddEventMsg(common.EvKick, name, color) } else { cr.AddEventMsg(common.EvBan, name, color) @@ -226,18 +230,10 @@ func (cr *ChatRoom) AddMsg(from *Client, isAction, isServer bool, msg string) { t = common.MsgServer } - lvl := common.CmdUser - if from.IsMod { - lvl = common.CmdMod - } - if from.IsAdmin { - lvl = common.CmdAdmin - } - select { - case cr.queue <- common.NewChatMessage(from.name, from.color, msg, lvl, t): + case cr.queue <- common.NewChatMessage(from.name, from.color, msg, from.CmdLevel, t): default: - fmt.Println("Unable to queue chat message. Channel full.") + common.LogErrorln("Unable to queue chat message. Channel full.") } } @@ -245,15 +241,15 @@ func (cr *ChatRoom) AddCmdMsg(command common.CommandType, args []string) { select { case cr.queue <- common.NewChatCommand(command, args): default: - fmt.Println("Unable to queue command message. Channel full.") + common.LogErrorln("Unable to queue command message. Channel full.") } } func (cr *ChatRoom) AddModNotice(message string) { select { - case cr.modqueue <- common.NewChatMessage("", "", message, common.CmdUser, common.MsgNotice): + case cr.modqueue <- common.NewChatMessage("", "", message, common.CmdlUser, common.MsgNotice): default: - fmt.Println("Unable to queue notice. Channel full.") + common.LogErrorln("Unable to queue notice. Channel full.") } } @@ -261,7 +257,7 @@ func (cr *ChatRoom) AddEventMsg(event common.EventType, name, color string) { select { case cr.queue <- common.NewChatEvent(event, name, color): default: - fmt.Println("Unable to queue event message. Channel full.") + common.LogErrorln("Unable to queue event message. Channel full.") } } @@ -288,8 +284,10 @@ func (cr *ChatRoom) Mod(name string) error { return err } - client.IsMod = true - client.SendServerMessage(`You have been modded.`) + if client.CmdLevel < common.CmdlMod { + client.CmdLevel = common.CmdlMod + client.SendServerMessage(`You have been modded.`) + } return nil } @@ -316,7 +314,7 @@ func (cr *ChatRoom) Broadcast() { send := func(data common.ChatData, client *Client) { err := client.SendChatData(data) if err != nil { - fmt.Printf("Error sending data to client: %v\n", err) + common.LogErrorf("Error sending data to client: %v\n", err) } } @@ -328,26 +326,36 @@ func (cr *ChatRoom) Broadcast() { go send(msg, client) } - data, err := msg.ToJSON() - if err != nil { - fmt.Printf("Error converting ChatData to ChatDataJSON: %v\n", err) - } else { - for uuid, conn := range cr.tempConn { - go func(c *chatConnection, suid string) { - err = c.WriteData(data) - if err != nil { - fmt.Printf("Error writing data to connection: %v\n", err) - delete(cr.tempConn, suid) - } - }(conn, uuid) - } + // Only send Chat and Event stuff to temp clients + if msg.Type != common.DTChat && msg.Type != common.DTEvent { + // Put this here instead of having two lock/unlock blocks. We want + // to avoid a case where a client is removed from the temp users + // and added to the clients between the two blocks. + cr.clientsMtx.Unlock() + break } + data, err := msg.ToJSON() + if err != nil { + common.LogErrorf("Error converting ChatData to ChatDataJSON: %v\n", err) + cr.clientsMtx.Unlock() + break + } + + for uuid, conn := range cr.tempConn { + go func(c *chatConnection, suid string) { + err = c.WriteData(data) + if err != nil { + common.LogErrorf("Error writing data to connection: %v\n", err) + delete(cr.tempConn, suid) + } + }(conn, uuid) + } cr.clientsMtx.Unlock() case msg := <-cr.modqueue: cr.clientsMtx.Lock() for _, client := range cr.clients { - if client.IsMod || client.IsAdmin { + if client.CmdLevel >= common.CmdlMod { send(msg, client) } } @@ -479,8 +487,11 @@ func (cr *ChatRoom) changeName(oldName, newName string, forced bool) error { } if currentClient != nil { - currentClient.name = newName - fmt.Printf("%q -> %q\n", oldName, newName) + 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) diff --git a/common/chatdata.go b/common/chatdata.go index 58bb1df..961c0be 100644 --- a/common/chatdata.go +++ b/common/chatdata.go @@ -111,9 +111,9 @@ func (dc DataMessage) HTML() string { default: badge := "" switch dc.Level { - case CmdMod: + case CmdlMod: badge = `` - case CmdAdmin: + case CmdlAdmin: badge = `` } return `
` + badge + `` + dc.From + diff --git a/common/colors.go b/common/colors.go index 32890b0..57fb7b1 100644 --- a/common/colors.go +++ b/common/colors.go @@ -42,6 +42,10 @@ var colors = []string{ "whitesmoke", "yellow", "yellowgreen", } +var ( + regexColor = regexp.MustCompile(`^#([0-9A-Fa-f]{3}){1,2}$`) +) + // IsValidColor takes a string s and compares it against a list of css color names. // It also accepts hex codes in the form of #000 (RGB), to #00000000 (RRGGBBAA), with A // being the alpha value @@ -53,7 +57,7 @@ func IsValidColor(s string) bool { } } - if regexp.MustCompile(`^#([0-9A-Fa-f]{3}){1,2}$`).MatchString(s) { + if regexColor.MatchString(s) { c, err := colorful.Hex(s) if err != nil { return false diff --git a/common/constants.go b/common/constants.go index f7546e1..e52d4d7 100644 --- a/common/constants.go +++ b/common/constants.go @@ -7,7 +7,7 @@ const ( CdMessage ClientDataType = iota // a normal message from the client meant to be broadcast CdUsers // get a list of users CdPing // ping the server to keep the connection alive - CdHelp // tells server to send help data again for buttons + CdAuth // get the auth levels of the user ) type DataType int @@ -36,9 +36,9 @@ type CommandLevel int // Command access levels const ( - CmdUser CommandLevel = iota - CmdMod - CmdAdmin + CmdlUser CommandLevel = iota + CmdlMod + CmdlAdmin ) type EventType int diff --git a/common/emotes.go b/common/emotes.go index f2ff30b..b330c7b 100644 --- a/common/emotes.go +++ b/common/emotes.go @@ -47,14 +47,15 @@ func LoadEmotes() (int, error) { globbed_files := []string(emotePNGs) globbed_files = append(globbed_files, emoteGIFs...) - fmt.Println("Loading emotes...") + LogInfoln("Loading emotes...") + emInfo := []string{} for _, file := range globbed_files { file = filepath.Base(file) key := file[0 : len(file)-4] newEmotes[key] = file - fmt.Printf("%s ", key) + emInfo = append(emInfo, key) } Emotes = newEmotes - fmt.Println("") + LogInfoln(strings.Join(emInfo, " ")) return len(Emotes), nil } diff --git a/common/logging.go b/common/logging.go new file mode 100644 index 0000000..01bf824 --- /dev/null +++ b/common/logging.go @@ -0,0 +1,200 @@ +package common + +import ( + "fmt" + "io" + "log" + "os" +) + +var loglevel LogLevel + +type LogLevel string + +const ( + LLError LogLevel = "error" // only log errors + LLChat LogLevel = "chat" // log chat and commands + LLInfo LogLevel = "info" // log info messages (not quite debug, but not chat) + LLDebug LogLevel = "debug" // log everything +) + +const ( + logPrefixError string = "[ERROR] " + logPrefixChat string = "[CHAT] " + logPrefixInfo string = "[INFO] " + logPrefixDebug string = "[DEBUG] " +) + +var ( + logError *log.Logger + logChat *log.Logger + logInfo *log.Logger + logDebug *log.Logger +) + +func SetupLogging(level LogLevel, file string) error { + switch level { + case LLDebug: + if file == "" { + logError = log.New(os.Stderr, logPrefixError, log.LstdFlags) + logChat = log.New(os.Stdout, logPrefixChat, log.LstdFlags) + logDebug = log.New(os.Stdout, logPrefixDebug, log.LstdFlags) + logInfo = log.New(os.Stdout, logPrefixInfo, log.LstdFlags) + } else { + f, err := os.OpenFile(file, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644) + if err != nil { + return fmt.Errorf("Unable to open log file for writing: %s", err) + } + logError = log.New(io.MultiWriter(os.Stderr, f), logPrefixError, log.LstdFlags) + logChat = log.New(io.MultiWriter(os.Stdout, f), logPrefixChat, log.LstdFlags) + logInfo = log.New(io.MultiWriter(os.Stdout, f), logPrefixInfo, log.LstdFlags) + logDebug = log.New(io.MultiWriter(os.Stdout, f), logPrefixDebug, log.LstdFlags) + } + case LLChat: + logDebug = nil + if file == "" { + logError = log.New(os.Stderr, logPrefixError, log.LstdFlags) + logChat = log.New(os.Stdout, logPrefixChat, log.LstdFlags) + logInfo = log.New(os.Stdout, logPrefixInfo, log.LstdFlags) + } else { + f, err := os.OpenFile(file, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644) + if err != nil { + return fmt.Errorf("Unable to open log file for writing: %s", err) + } + logError = log.New(io.MultiWriter(os.Stderr, f), logPrefixError, log.LstdFlags) + logChat = log.New(io.MultiWriter(os.Stdout, f), logPrefixChat, log.LstdFlags) + logInfo = log.New(io.MultiWriter(os.Stdout, f), logPrefixInfo, log.LstdFlags) + } + + case LLInfo: + logDebug = nil + logChat = nil + if file == "" { + logError = log.New(os.Stderr, logPrefixError, log.LstdFlags) + logInfo = log.New(os.Stdout, logPrefixInfo, log.LstdFlags) + } else { + f, err := os.OpenFile(file, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644) + if err != nil { + return fmt.Errorf("Unable to open log file for writing: %s", err) + } + logError = log.New(io.MultiWriter(os.Stderr, f), logPrefixError, log.LstdFlags) + logInfo = log.New(io.MultiWriter(os.Stdout, f), logPrefixInfo, log.LstdFlags) + } + + // Default to error + default: + logChat = nil + logDebug = nil + logInfo = nil + if file == "" { + logError = log.New(os.Stderr, logPrefixError, log.LstdFlags) + } else { + f, err := os.OpenFile(file, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644) + if err != nil { + return fmt.Errorf("Unable to open log file for writing: %s", err) + } + logError = log.New(io.MultiWriter(os.Stderr, f), logPrefixError, log.LstdFlags) + } + } + return nil +} + +func LogErrorf(format string, v ...interface{}) { + if logError == nil { + panic("Logging not setup!") + } + + logError.Printf(format, v...) +} + +func LogErrorln(v ...interface{}) { + if logError == nil { + panic("Logging not setup!") + } + + logError.Println(v...) +} + +func LogChatf(format string, v ...interface{}) { + // if logError isn't set to something, logging wasn't setup. + if logError == nil { + panic("Logging not setup!") + } + + // logging chat and commands is turned off. + if logChat == nil { + return + } + + logChat.Printf(format, v...) +} + +func LogChatln(v ...interface{}) { + // if logError isn't set to something, logging wasn't setup. + if logError == nil { + panic("Logging not setup!") + } + + // logging chat and commands is turned off. + if logChat == nil { + return + } + + logChat.Println(v...) +} + +func LogInfof(format string, v ...interface{}) { + // if logError isn't set to something, logging wasn't setup. + if logError == nil { + panic("Logging not setup!") + } + + // logging info is turned off. + if logInfo == nil { + return + } + + logInfo.Printf(format, v...) +} + +func LogInfoln(v ...interface{}) { + // if logError isn't set to something, logging wasn't setup. + if logError == nil { + panic("Logging not setup!") + } + + // logging info is turned off. + if logInfo == nil { + return + } + + logInfo.Println(v...) +} + +func LogDebugf(format string, v ...interface{}) { + // if logError isn't set to something, logging wasn't setup. + if logError == nil { + panic("Logging not setup!") + } + + // logging debug is turned off. + if logDebug == nil { + return + } + + logDebug.Printf(format, v...) +} + +func LogDebugln(v ...interface{}) { + // if logError isn't set to something, logging wasn't setup. + if logError == nil { + panic("Logging not setup!") + } + + // logging debug is turned off. + if logDebug == nil { + return + } + + logDebug.Println(v...) +} diff --git a/common/logging_dev.go b/common/logging_dev.go new file mode 100644 index 0000000..9dccfc1 --- /dev/null +++ b/common/logging_dev.go @@ -0,0 +1,18 @@ +// +build dev + +package common + +import ( + "log" + "os" +) + +var logDev *log.Logger = log.New(os.Stdout, "[DEV]", log.LstdFlags) + +func LogDevf(format string, v ...interface{}) { + logDev.Printf(format, v...) +} + +func LogDevln(v ...interface{}) { + logDev.Println(v...) +} diff --git a/common/utils.go b/common/utils.go index 808af11..e08ed0a 100644 --- a/common/utils.go +++ b/common/utils.go @@ -12,5 +12,5 @@ var usernameRegex *regexp.Regexp = regexp.MustCompile(`^[0-9a-zA-Z_-]+$`) // and is not a valid color name func IsValidName(name string) bool { return 3 <= len(name) && len(name) <= 36 && - usernameRegex.MatchString(name) && !IsValidColor(name) + usernameRegex.MatchString(name) } diff --git a/connection.go b/connection.go index 715b412..f2f8cc7 100644 --- a/connection.go +++ b/connection.go @@ -6,6 +6,7 @@ import ( "sync" "github.com/gorilla/websocket" + "github.com/zorchenhimer/MovieNight/common" ) type chatConnection struct { @@ -31,7 +32,7 @@ func (cc *chatConnection) WriteData(data interface{}) error { err := cc.WriteJSON(data) if err != nil { if operr, ok := err.(*net.OpError); ok { - fmt.Println("OpError: " + operr.Err.Error()) + common.LogDebugln("OpError: " + operr.Err.Error()) } return fmt.Errorf("Error writing data to %s %s: %v", cc.clientName, cc.Host(), err) } diff --git a/handlers.go b/handlers.go index f8bf85e..ffd6d98 100644 --- a/handlers.go +++ b/handlers.go @@ -58,7 +58,7 @@ func wsStaticFiles(w http.ResponseWriter, r *http.Request) { } goodPath := r.URL.Path[8:len(r.URL.Path)] - fmt.Printf("[static] serving %q from folder ./static/\n", goodPath) + common.LogDebugf("[static] serving %q from folder ./static/\n", goodPath) http.ServeFile(w, r, "./static/"+goodPath) } @@ -70,7 +70,7 @@ func wsWasmFile(w http.ResponseWriter, r *http.Request) { func wsImages(w http.ResponseWriter, r *http.Request) { base := filepath.Base(r.URL.Path) - fmt.Println("[img] ", base) + common.LogDebugln("[img] ", base) http.ServeFile(w, r, "./static/img/"+base) } @@ -91,7 +91,7 @@ func wsHandler(w http.ResponseWriter, r *http.Request) { conn, err := upgrader.Upgrade(w, r, nil) if err != nil { - fmt.Println("Error upgrading to websocket:", err) + common.LogErrorln("Error upgrading to websocket:", err) return } @@ -107,7 +107,7 @@ func wsHandler(w http.ResponseWriter, r *http.Request) { uid, err := chat.JoinTemp(chatConn) if err != nil { - fmt.Printf("[handler] could not do a temp join, %v\n", err) + common.LogErrorf("[handler] could not do a temp join, %v\n", err) conn.Close() } @@ -117,7 +117,7 @@ func wsHandler(w http.ResponseWriter, r *http.Request) { var data common.ClientData err := chatConn.ReadData(&data) if err != nil { - fmt.Printf("[handler] Client closed connection: %s\n", conn.RemoteAddr().String()) + common.LogInfof("[handler] Client closed connection: %s\n", conn.RemoteAddr().String()) conn.Close() return } @@ -126,14 +126,14 @@ func wsHandler(w http.ResponseWriter, r *http.Request) { if err != nil { switch err.(type) { case UserFormatError, UserTakenError: - fmt.Printf("[handler|%s] %v\n", errorName(err), err) + common.LogInfof("[handler|%s] %v\n", errorName(err), err) case BannedUserError: - fmt.Printf("[handler|%s] %v\n", errorName(err), err) + common.LogInfof("[handler|%s] %v\n", errorName(err), err) // close connection since banned users shouldn't be connecting conn.Close() default: // for now all errors not caught need to be warned - fmt.Printf("[handler|uncaught] %v\n", err) + common.LogErrorf("[handler|uncaught] %v\n", err) conn.Close() } } @@ -243,7 +243,7 @@ func handlePinTemplate(w http.ResponseWriter, r *http.Request, errorMessage stri func handleHelpTemplate(w http.ResponseWriter, r *http.Request) { t, err := template.ParseFiles("./static/base.html", "./static/help.html") if err != nil { - fmt.Printf("Error parsing template file, %v\n", err) + common.LogErrorf("Error parsing template file, %v\n", err) return } @@ -256,20 +256,20 @@ func handleHelpTemplate(w http.ResponseWriter, r *http.Request) { data := Data{ Title: "Help", - Commands: getHelp(common.CmdUser), + Commands: getHelp(common.CmdlUser), } if len(r.URL.Query().Get("mod")) > 0 { - data.ModCommands = getHelp(common.CmdMod) + data.ModCommands = getHelp(common.CmdlMod) } if len(r.URL.Query().Get("admin")) > 0 { - data.AdminCommands = getHelp(common.CmdAdmin) + data.AdminCommands = getHelp(common.CmdlAdmin) } err = t.Execute(w, data) if err != nil { - fmt.Printf("Error executing file, %v", err) + common.LogErrorf("Error executing file, %v", err) } } @@ -306,7 +306,7 @@ func handleIndexTemplate(w http.ResponseWriter, r *http.Request) { t, err := template.ParseFiles("./static/base.html", "./static/main.html") if err != nil { - fmt.Printf("Error parsing template file, %v\n", err) + common.LogErrorf("Error parsing template file, %v\n", err) return } @@ -337,7 +337,7 @@ func handleIndexTemplate(w http.ResponseWriter, r *http.Request) { err = t.Execute(w, data) if err != nil { - fmt.Printf("Error executing file, %v", err) + common.LogErrorf("Error executing file, %v", err) } } @@ -345,22 +345,22 @@ func handlePublish(conn *rtmp.Conn) { streams, _ := conn.Streams() l.Lock() - fmt.Println("request string->", conn.URL.RequestURI()) + common.LogDebugln("request string->", conn.URL.RequestURI()) urlParts := strings.Split(strings.Trim(conn.URL.RequestURI(), "/"), "/") - fmt.Println("urlParts->", urlParts) + common.LogDebugln("urlParts->", urlParts) if len(urlParts) > 2 { - fmt.Println("Extra garbage after stream key") + common.LogErrorln("Extra garbage after stream key") return } if len(urlParts) != 2 { - fmt.Println("Missing stream key") + common.LogErrorln("Missing stream key") return } if urlParts[1] != settings.GetStreamKey() { - fmt.Println("Due to key not match, denied stream") + common.LogErrorln("Stream key is incorrect. Denying stream.") return //If key not match, deny stream } @@ -376,13 +376,13 @@ func handlePublish(conn *rtmp.Conn) { } l.Unlock() if ch == nil { - fmt.Println("Unable to start stream, channel is nil.") + common.LogErrorln("Unable to start stream, channel is nil.") return } - fmt.Println("Stream started") + common.LogInfoln("Stream started") avutil.CopyPackets(ch.que, conn) - fmt.Println("Stream finished") + common.LogInfoln("Stream finished") l.Lock() delete(channels, streamPath) @@ -420,7 +420,8 @@ func handleDefault(w http.ResponseWriter, r *http.Request) { avutil.CopyFile(muxer, cursor) } else { if r.URL.Path != "/" { - fmt.Println("[http 404] ", r.URL.Path) + // not really an error for the server, but for the client. + common.LogInfoln("[http 404] ", r.URL.Path) http.NotFound(w, r) } else { handleIndexTemplate(w, r) diff --git a/main.go b/main.go index 39b105c..7ded62e 100644 --- a/main.go +++ b/main.go @@ -1,41 +1,97 @@ package main import ( + "crypto/rand" "flag" "fmt" + "math/big" "net/http" "os" "os/signal" + "github.com/gorilla/sessions" "github.com/nareix/joy4/format" "github.com/nareix/joy4/format/rtmp" + "github.com/zorchenhimer/MovieNight/common" ) var ( addr string sKey string - stats streamStats + stats = newStreamStats() ) -func init() { - format.RegisterAll() +func setupSettings() error { + var err error + settings, err = LoadSettings("settings.json") + if err != nil { + return fmt.Errorf("Unable to load settings: %s", err) + } + if len(settings.StreamKey) == 0 { + return fmt.Errorf("Missing stream key is settings.json") + } - flag.StringVar(&addr, "l", ":8089", "host:port of the MovieNight") - flag.StringVar(&sKey, "k", "", "Stream key, to protect your stream") + if err = settings.SetupLogging(); err != nil { + return fmt.Errorf("Unable to setup logger: %s", err) + } - stats = newStreamStats() + // Is this a good way to do this? Probably not... + if len(settings.SessionKey) == 0 { + out := "" + large := big.NewInt(int64(1 << 60)) + large = large.Add(large, large) + for len(out) < 50 { + num, err := rand.Int(rand.Reader, large) + if err != nil { + panic("Error generating session key: " + err.Error()) + } + out = fmt.Sprintf("%s%X", out, num) + } + settings.SessionKey = out + } + + if len(settings.RoomAccess) == 0 { + settings.RoomAccess = AccessOpen + } + + if settings.RoomAccess != AccessOpen && len(settings.RoomAccessPin) == 0 { + settings.RoomAccessPin = "1234" + } + + sstore = sessions.NewCookieStore([]byte(settings.SessionKey)) + sstore.Options = &sessions.Options{ + Path: "/", + MaxAge: 60 * 60 * 24, // one day + SameSite: http.SameSiteStrictMode, + } + + // Save admin password to file + if err = settings.Save(); err != nil { + return fmt.Errorf("Unable to save settings: %s", err) + } + + return nil } func main() { + flag.StringVar(&addr, "l", ":8089", "host:port of the MovieNight") + flag.StringVar(&sKey, "k", "", "Stream key, to protect your stream") flag.Parse() + format.RegisterAll() + + if err := setupSettings(); err != nil { + fmt.Printf("Error loading settings: %v\n", err) + os.Exit(1) + } + exit := make(chan bool) go handleInterrupt(exit) // Load emotes before starting server. var err error if chat, err = newChatRoom(); err != nil { - fmt.Println(err) + common.LogErrorln(err) os.Exit(1) } @@ -49,11 +105,11 @@ func main() { settings.SetTempKey(sKey) } - fmt.Println("Stream key: ", settings.GetStreamKey()) - fmt.Println("Admin password: ", settings.AdminPassword) + common.LogInfoln("Stream key: ", settings.GetStreamKey()) + common.LogInfoln("Admin password: ", settings.AdminPassword) + common.LogInfoln("Listen and serve ", addr) fmt.Println("RoomAccess: ", settings.RoomAccess) fmt.Println("RoomAccessPin: ", settings.RoomAccessPin) - fmt.Println("Listen and serve ", addr) go startServer() go startRmtpServer() @@ -68,7 +124,8 @@ func startRmtpServer() { } err := server.ListenAndServe() if err != nil { - fmt.Printf("Error trying to start server: %v\n", err) + // If the server cannot start, don't pretend we can continue. + panic("Error trying to start rtmp server: " + err.Error()) } } @@ -90,7 +147,8 @@ func startServer() { err := http.ListenAndServe(addr, nil) if err != nil { - fmt.Printf("Error trying to start rmtp server: %v\n", err) + // If the server cannot start, don't pretend we can continue. + panic("Error trying to start chat/http server: " + err.Error()) } } @@ -98,7 +156,7 @@ func handleInterrupt(exit chan bool) { ch := make(chan os.Signal) signal.Notify(ch, os.Interrupt) <-ch - fmt.Println("Closing server") + common.LogInfoln("Closing server") if settings.StreamStats { stats.Print() } diff --git a/settings.go b/settings.go index 22a7d6d..84dd86e 100644 --- a/settings.go +++ b/settings.go @@ -6,12 +6,12 @@ import ( "fmt" "io/ioutil" "math/big" - "net/http" "strings" "sync" "time" "github.com/gorilla/sessions" + "github.com/zorchenhimer/MovieNight/common" ) var settings *Settings @@ -28,10 +28,12 @@ type Settings struct { MaxMessageCount int TitleLength int // maximum length of the title that can be set with the /playing AdminPassword string - Bans []BanInfo StreamKey string ListenAddress string SessionKey string // key for session data + Bans []BanInfo + LogLevel common.LogLevel + LogFile string RoomAccess AccessMode RoomAccessPin string // auto generate this, } @@ -50,56 +52,6 @@ type BanInfo struct { When time.Time } -func init() { - var err error - settings, err = LoadSettings("settings.json") - if err != nil { - panic("Unable to load settings: " + err.Error()) - } - if len(settings.StreamKey) == 0 { - panic("Missing stream key is settings.json") - } - - if settings.TitleLength <= 0 { - settings.TitleLength = 50 - } - - // Is this a good way to do this? Probably not... - if len(settings.SessionKey) == 0 { - out := "" - large := big.NewInt(int64(1 << 60)) - large = large.Add(large, large) - for len(out) < 50 { - num, err := rand.Int(rand.Reader, large) - if err != nil { - panic("Error generating session key: " + err.Error()) - } - out = fmt.Sprintf("%s%X", out, num) - } - settings.SessionKey = out - } - - if len(settings.RoomAccess) == 0 { - settings.RoomAccess = AccessOpen - } - - if settings.RoomAccess != AccessOpen && len(settings.RoomAccessPin) == 0 { - settings.RoomAccessPin = "1234" - } - - sstore = sessions.NewCookieStore([]byte(settings.SessionKey)) - sstore.Options = &sessions.Options{ - Path: "/", - MaxAge: 60 * 60 * 24, // one day - SameSite: http.SameSiteStrictMode, - } - - // Save admin password to file - if err = settings.Save(); err != nil { - panic("Unable to save settings: " + err.Error()) - } -} - func LoadSettings(filename string) (*Settings, error) { raw, err := ioutil.ReadFile(filename) if err != nil { @@ -124,8 +76,14 @@ func LoadSettings(filename string) (*Settings, error) { if err != nil { return nil, fmt.Errorf("unable to generate admin password: %s", err) } + + // Don't use LogInfof() here. Log isn't setup yet when LoadSettings() is called from init(). fmt.Printf("Settings reloaded. New admin password: %s\n", s.AdminPassword) + if s.TitleLength <= 0 { + s.TitleLength = 50 + } + return s, nil } @@ -167,7 +125,7 @@ func (s *Settings) AddBan(host string, names []string) error { } settings.Bans = append(settings.Bans, b) - fmt.Printf("[BAN] %q (%s) has been banned.\n", strings.Join(names, ", "), host) + common.LogInfof("[BAN] %q (%s) has been banned.\n", strings.Join(names, ", "), host) return settings.Save() } @@ -181,7 +139,7 @@ func (s *Settings) RemoveBan(name string) error { for _, b := range s.Bans { for _, n := range b.Names { if n == name { - fmt.Printf("[ban] Removed ban for %s [%s]\n", b.IP, n) + common.LogInfof("[ban] Removed ban for %s [%s]\n", b.IP, n) } else { newBans = append(newBans, b) } @@ -220,6 +178,10 @@ func (s *Settings) GetStreamKey() string { return s.StreamKey } +func (s *Settings) SetupLogging() error { + return common.SetupLogging(s.LogLevel, s.LogFile) +} + func (s *Settings) generateNewPin() (string, error) { num, err := rand.Int(rand.Reader, big.NewInt(int64(9999))) if err != nil { diff --git a/static/css/site.css b/static/css/site.css index b71d119..3a764fa 100644 --- a/static/css/site.css +++ b/static/css/site.css @@ -116,6 +116,22 @@ span.svmsg { color: var(--var-contrast-color); } +.spoiler { + border-radius: 3px; + padding: 0px 3px; +} + +.spoiler *, +.spoiler { + background: var(--var-popout-color); + color: var(--var-popout-color); +} + +.spoiler-active { + background: var(--var-background-color); + color: aqua; +} + .range-div { margin-bottom: 5px; display: flex; diff --git a/wasm/main.go b/wasm/main.go index cdd5e7f..dab4572 100644 --- a/wasm/main.go +++ b/wasm/main.go @@ -10,150 +10,7 @@ import ( "github.com/zorchenhimer/MovieNight/common" ) -const ( - keyTab = 9 - keyEnter = 13 - keyUp = 38 - keyDown = 40 -) - -var ( - currentName string - names []string - filteredNames []string -) - -// The returned value is a bool deciding to prevent the event from propagating -func processMessageKey(this js.Value, v []js.Value) interface{} { - if len(filteredNames) == 0 || currentName == "" { - return false - } - - startIdx := v[0].Get("target").Get("selectionStart").Int() - keyCode := v[0].Get("keyCode").Int() - switch keyCode { - case keyUp, keyDown: - newidx := 0 - for i, n := range filteredNames { - if n == currentName { - newidx = i - if keyCode == keyDown { - newidx = i + 1 - if newidx == len(filteredNames) { - newidx-- - } - } else if keyCode == keyUp { - newidx = i - 1 - if newidx < 0 { - newidx = 0 - } - } - break - } - } - currentName = filteredNames[newidx] - case keyTab, keyEnter: - msg := js.Get("msg") - val := msg.Get("value").String() - newval := val[:startIdx] - - if i := strings.LastIndex(newval, "@"); i != -1 { - newval = newval[:i+1] + currentName - } - - endVal := val[startIdx:] - if len(val) == startIdx || val[startIdx:][0] != ' ' { - // insert a space into val so selection indexing can be one line - endVal = " " + endVal - } - msg.Set("value", newval+endVal) - msg.Set("selectionStart", len(newval)+1) - msg.Set("selectionEnd", len(newval)+1) - - // Clear out filtered names since it is no longer needed - filteredNames = nil - default: - // We only want to handle the caught keys, so return early - return false - } - - updateSuggestionDiv() - return true -} - -func processMessage(v []js.Value) { - msg := js.Get("msg") - text := strings.ToLower(msg.Get("value").String()) - startIdx := msg.Get("selectionStart").Int() - - filteredNames = nil - if len(text) != 0 { - if len(names) > 0 { - var caretIdx int - textParts := strings.Split(text, " ") - - for i, word := range textParts { - // Increase caret index at beginning if not first word to account for spaces - if i != 0 { - caretIdx++ - } - - // It is possible to have a double space " ", which will lead to an - // empty string element in the slice. Also check that the index of the - // cursor is between the start of the word and the end - if len(word) > 0 && word[0] == '@' && - caretIdx <= startIdx && startIdx <= caretIdx+len(word) { - // fill filtered first so the "modifier" keys can modify it - for _, n := range names { - if len(word) == 1 || strings.HasPrefix(strings.ToLower(n), word[1:]) { - filteredNames = append(filteredNames, n) - } - } - } - - if len(filteredNames) > 0 { - currentName = "" - break - } - - caretIdx += len(word) - } - } - } - - updateSuggestionDiv() -} - -func updateSuggestionDiv() { - const selectedClass = ` class="selectedName"` - - var divs []string - if len(filteredNames) > 0 { - // set current name to first if not set already - if currentName == "" { - currentName = filteredNames[0] - } - - var hasCurrentName bool - divs = make([]string, len(filteredNames)) - - // Create inner body of html - for i := range filteredNames { - divs[i] = "" - } - - if !hasCurrentName { - divs[0] = divs[0][:4] + selectedClass + divs[0][4:] - } - } - // The \n is so it's easier to read th source in web browsers for the dev - js.Get("suggestions").Set("innerHTML", strings.Join(divs, "\n")) -} +var auth common.CommandLevel func recieve(v []js.Value) { if len(v) == 0 { @@ -182,6 +39,8 @@ func recieve(v []js.Value) { for _, i := range h.Data.([]interface{}) { names = append(names, i.(string)) } + case common.CdAuth: + auth = h.Data.(common.CommandLevel) } case common.DTEvent: d := chat.Data.(common.DataEvent) @@ -277,6 +136,7 @@ func isValidName(this js.Value, v []js.Value) interface{} { func debugValues(v []js.Value) { fmt.Printf("currentName %#v\n", currentName) + fmt.Printf("auth %#v\n", auth) fmt.Printf("names %#v\n", names) fmt.Printf("filteredNames %#v\n", filteredNames) } diff --git a/wasm/suggestions.go b/wasm/suggestions.go new file mode 100644 index 0000000..8757370 --- /dev/null +++ b/wasm/suggestions.go @@ -0,0 +1,152 @@ +package main + +import ( + "strings" + + "github.com/dennwc/dom/js" +) + +const ( + keyTab = 9 + keyEnter = 13 + keyUp = 38 + keyDown = 40 +) + +var ( + currentName string + names []string + filteredNames []string +) + +// The returned value is a bool deciding to prevent the event from propagating +func processMessageKey(this js.Value, v []js.Value) interface{} { + if len(filteredNames) == 0 || currentName == "" { + return false + } + + startIdx := v[0].Get("target").Get("selectionStart").Int() + keyCode := v[0].Get("keyCode").Int() + switch keyCode { + case keyUp, keyDown: + newidx := 0 + for i, n := range filteredNames { + if n == currentName { + newidx = i + if keyCode == keyDown { + newidx = i + 1 + if newidx == len(filteredNames) { + newidx-- + } + } else if keyCode == keyUp { + newidx = i - 1 + if newidx < 0 { + newidx = 0 + } + } + break + } + } + currentName = filteredNames[newidx] + case keyTab, keyEnter: + msg := js.Get("msg") + val := msg.Get("value").String() + newval := val[:startIdx] + + if i := strings.LastIndex(newval, "@"); i != -1 { + newval = newval[:i+1] + currentName + } + + endVal := val[startIdx:] + if len(val) == startIdx || val[startIdx:][0] != ' ' { + // insert a space into val so selection indexing can be one line + endVal = " " + endVal + } + msg.Set("value", newval+endVal) + msg.Set("selectionStart", len(newval)+1) + msg.Set("selectionEnd", len(newval)+1) + + // Clear out filtered names since it is no longer needed + filteredNames = nil + default: + // We only want to handle the caught keys, so return early + return false + } + + updateSuggestionDiv() + return true +} + +func processMessage(v []js.Value) { + msg := js.Get("msg") + text := strings.ToLower(msg.Get("value").String()) + startIdx := msg.Get("selectionStart").Int() + + filteredNames = nil + if len(text) != 0 { + if len(names) > 0 { + var caretIdx int + textParts := strings.Split(text, " ") + + for i, word := range textParts { + // Increase caret index at beginning if not first word to account for spaces + if i != 0 { + caretIdx++ + } + + // It is possible to have a double space " ", which will lead to an + // empty string element in the slice. Also check that the index of the + // cursor is between the start of the word and the end + if len(word) > 0 && word[0] == '@' && + caretIdx <= startIdx && startIdx <= caretIdx+len(word) { + // fill filtered first so the "modifier" keys can modify it + for _, n := range names { + if len(word) == 1 || strings.HasPrefix(strings.ToLower(n), word[1:]) { + filteredNames = append(filteredNames, n) + } + } + } + + if len(filteredNames) > 0 { + currentName = "" + break + } + + caretIdx += len(word) + } + } + } + + updateSuggestionDiv() +} + +func updateSuggestionDiv() { + const selectedClass = ` class="selectedName"` + + var divs []string + if len(filteredNames) > 0 { + // set current name to first if not set already + if currentName == "" { + currentName = filteredNames[0] + } + + var hasCurrentName bool + divs = make([]string, len(filteredNames)) + + // Create inner body of html + for i := range filteredNames { + divs[i] = "" + } + + if !hasCurrentName { + divs[0] = divs[0][:4] + selectedClass + divs[0][4:] + } + } + // The \n is so it's easier to read th source in web browsers for the dev + js.Get("suggestions").Set("innerHTML", strings.Join(divs, "\n")) +}