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] = "" + filteredNames[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] = "" + filteredNames[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"))
+}