diff --git a/chatroom.go b/chatroom.go index dc5fe56..e084b5e 100644 --- a/chatroom.go +++ b/chatroom.go @@ -30,6 +30,8 @@ type ChatRoom struct { modPasswords []string // single-use mod passwords modPasswordsMtx sync.Mutex + + isShutdown bool } //initializing the chatroom @@ -53,6 +55,11 @@ func newChatRoom() (*ChatRoom, error) { } func (cr *ChatRoom) JoinTemp(conn *chatConnection) (string, error) { + // Don't allow new joins when the server is closing. + if cr.isShutdown { + return "", fmt.Errorf("Server is shutting down") + } + defer cr.clientsMtx.Unlock() cr.clientsMtx.Lock() @@ -77,6 +84,11 @@ func (cr *ChatRoom) JoinTemp(conn *chatConnection) (string, error) { //registering a new client //returns pointer to a Client, or Nil, if the name is already taken func (cr *ChatRoom) Join(name, uid string) (*Client, error) { + // Don't allow new joins when the server is closing. + if cr.isShutdown { + return nil, fmt.Errorf("Server is shutting down") + } + defer cr.clientsMtx.Unlock() cr.clientsMtx.Lock() @@ -500,3 +512,26 @@ func (cr *ChatRoom) changeName(oldName, newName string, forced bool) error { return fmt.Errorf("Client not found with name %q", oldName) } + +// Shutdown the chatroom. First, dissallow new joins by setting +// isShutdown, then close each client connection. This would be +// a good place to put a final command that gets sent to the client. +func (cr *ChatRoom) Shutdown() { + cr.isShutdown = true + common.LogInfoln("ChatRoom is shutting down.") + + cr.clientsMtx.Lock() + defer cr.clientsMtx.Unlock() + + for uuid, client := range cr.clients { + common.LogDebugf("Closing connection for %s", uuid) + client.conn.Close() + } + + for uuid, conn := range cr.tempConn { + common.LogDebugf("Closing connection for temp %s", uuid) + conn.Close() + } + + common.LogInfoln("ChatRoom Shutdown() finished") +} diff --git a/common/colors.go b/common/colors.go index f2c18e1..e05654f 100644 --- a/common/colors.go +++ b/common/colors.go @@ -7,39 +7,36 @@ import ( "github.com/lucasb-eyer/go-colorful" ) +// Colors holds all the valid html color names for MovieNight // the values in colors must be lowercase so it matches with the color input // this saves from having to call strings.ToLower(color) every time to check -var colors = []string{ +var Colors = []string{ "aliceblue", "antiquewhite", "aqua", "aquamarine", "azure", - "beige", "bisque", "black", "blanchedalmond", "blue", - "blueviolet", "brown", "burlywood", "cadetblue", "chartreuse", - "chocolate", "coral", "cornflowerblue", "cornsilk", "crimson", - "cyan", "darkblue", "darkcyan", "darkgoldenrod", "darkgray", - "darkgrey", "darkgreen", "darkkhaki", "darkmagenta", "darkolivegreen", - "darkorange", "darkorchid", "darkred", "darksalmon", "darkseagreen", - "darkslateblue", "darkslategray", "darkslategrey", "darkturquoise", "darkviolet", - "deeppink", "deepskyblue", "dimgray", "dimgrey", "dodgerblue", - "firebrick", "floralwhite", "forestgreen", "fuchsia", "gainsboro", - "ghostwhite", "gold", "goldenrod", "gray", "grey", - "green", "greenyellow", "honeydew", "hotpink", "indianred", - "indigo", "ivory", "khaki", "lavender", "lavenderblush", - "lawngreen", "lemonchiffon", "lightblue", "lightcoral", "lightcyan", - "lightgoldenrodyellow", "lightgray", "lightgrey", "lightgreen", "lightpink", - "lightsalmon", "lightseagreen", "lightskyblue", "lightslategray", "lightslategrey", - "lightsteelblue", "lightyellow", "lime", "limegreen", "linen", - "magenta", "maroon", "mediumaquamarine", "mediumblue", "mediumorchid", + "beige", "bisque", "blanchedalmond", "blueviolet", "brown", + "burlywood", "cadetblue", "chartreuse", "chocolate", "coral", + "cornflowerblue", "cornsilk", "crimson", "cyan", "darkcyan", + "darkgoldenrod", "darkgray", "darkkhaki", "darkmagenta", "darkolivegreen", + "darkorange", "darkorchid", "darksalmon", "darkseagreen", "darkslateblue", + "darkslategray", "darkturquoise", "darkviolet", "deeppink", "deepskyblue", + "dimgray", "dodgerblue", "firebrick", "floralwhite", "forestgreen", + "fuchsia", "gainsboro", "ghostwhite", "gold", "goldenrod", + "gray", "greenyellow", "honeydew", "hotpink", "indigo", + "ivory", "khaki", "lavender", "lavenderblush", "lawngreen", + "lemonchiffon", "lightblue", "lightcoral", "lightcyan", "lightgoldenrodyellow", + "lightgrey", "lightgreen", "lightpink", "lightsalmon", "lightseagreen", + "lightskyblue", "lightslategray", "lightsteelblue", "lightyellow", "lime", + "limegreen", "linen", "magenta", "mediumaquamarine", "mediumorchid", "mediumpurple", "mediumseagreen", "mediumslateblue", "mediumspringgreen", "mediumturquoise", - "mediumvioletred", "midnightblue", "mintcream", "mistyrose", "moccasin", - "navajowhite", "navy", "oldlace", "olive", "olivedrab", - "orange", "orangered", "orchid", "palegoldenrod", "palegreen", - "paleturquoise", "palevioletred", "papayawhip", "peachpuff", "peru", - "pink", "plum", "powderblue", "purple", "rebeccapurple", - "red", "rosybrown", "royalblue", "saddlebrown", "salmon", - "sandybrown", "seagreen", "seashell", "sienna", "silver", - "skyblue", "slateblue", "slategray", "slategrey", "snow", - "springgreen", "steelblue", "tan", "teal", "thistle", - "tomato", "turquoise", "violet", "wheat", "white", - "whitesmoke", "yellow", "yellowgreen", + "mediumvioletred", "mintcream", "mistyrose", "moccasin", "navajowhite", + "oldlace", "olive", "olivedrab", "orange", "orangered", + "orchid", "palegoldenrod", "palegreen", "paleturquoise", "palevioletred", + "papayawhip", "peachpuff", "peru", "pink", "plum", + "powderblue", "purple", "rebeccapurple", "red", "rosybrown", + "royalblue", "saddlebrown", "salmon", "sandybrown", "seagreen", + "seashell", "sienna", "silver", "skyblue", "slateblue", + "slategray", "snow", "springgreen", "steelblue", "tan", + "teal", "thistle", "tomato", "turquoise", "violet", + "wheat", "white", "whitesmoke", "yellow", "yellowgreen", } var ( @@ -50,7 +47,7 @@ var ( // It also accepts hex codes in the form of #RGB and #RRGGBB func IsValidColor(s string) bool { s = strings.ToLower(s) - for _, c := range colors { + for _, c := range Colors { if s == c { return true } diff --git a/common/emotes_test.go b/common/emotes_test.go new file mode 100644 index 0000000..f531b6f --- /dev/null +++ b/common/emotes_test.go @@ -0,0 +1,44 @@ +package common + +import ( + "os" + "testing" +) + +var data_good = map[string]string{ + "one": ``, + "two": ``, + "three": ``, + + ":one:": ``, + ":two:": ``, + ":three:": ``, + + "[one]": ``, + "[two]": ``, + "[three]": ``, + + ":one: two [three]": ` `, + + "nope one what": `nope what`, + "nope :two: what": `nope what`, + "nope [three] what": `nope what`, +} + +func TestMain(m *testing.M) { + Emotes = map[string]string{ + "one": "one.png", + "two": "two.png", + "three": "three.gif", + } + os.Exit(m.Run()) +} + +func TestEmotes_ParseEmotes(t *testing.T) { + for input, expected := range data_good { + got := ParseEmotes(input) + if got != expected { + t.Errorf("%s failed to parse into %q. Received: %q", input, expected, got) + } + } +} diff --git a/main.go b/main.go index 40c7dd4..0dee74b 100644 --- a/main.go +++ b/main.go @@ -1,11 +1,13 @@ package main import ( + "context" "flag" "fmt" "net/http" "os" "os/signal" + "time" "github.com/gorilla/sessions" "github.com/nareix/joy4/format" @@ -14,9 +16,10 @@ import ( ) var ( - addr string - sKey string - stats = newStreamStats() + addr string + sKey string + stats = newStreamStats() + chatServer *http.Server ) func setupSettings() error { @@ -51,9 +54,6 @@ func main() { os.Exit(1) } - exit := make(chan bool) - go handleInterrupt(exit) - // Load emotes before starting server. var err error if chat, err = newChatRoom(); err != nil { @@ -77,54 +77,88 @@ func main() { common.LogInfoln("RoomAccess: ", settings.RoomAccess) common.LogInfoln("RoomAccessPin: ", settings.RoomAccessPin) - go startServer() - go startRmtpServer() - - <-exit -} - -func startRmtpServer() { server := &rtmp.Server{ HandlePlay: handlePlay, HandlePublish: handlePublish, } - err := server.ListenAndServe() - if err != nil { - // If the server cannot start, don't pretend we can continue. - panic("Error trying to start rtmp server: " + err.Error()) + + // Define this here so we can set some timeouts and things. + chatServer := &http.Server{ + Addr: addr, + ReadTimeout: 10 * time.Second, + WriteTimeout: 10 * time.Second, + MaxHeaderBytes: 1 << 20, } -} - -func startServer() { - // Chat websocket - http.HandleFunc("/ws", wsHandler) - http.HandleFunc("/static/js/", wsStaticFiles) - http.HandleFunc("/static/css/", wsStaticFiles) - http.HandleFunc("/static/img/", wsImages) - http.HandleFunc("/static/main.wasm", wsWasmFile) - http.HandleFunc("/emotes/", wsEmotes) - http.HandleFunc("/favicon.ico", wsStaticFiles) - http.HandleFunc("/chat", handleIndexTemplate) - http.HandleFunc("/video", handleIndexTemplate) - http.HandleFunc("/help", handleHelpTemplate) - http.HandleFunc("/pin", handlePin) - - http.HandleFunc("/", handleDefault) - - err := http.ListenAndServe(addr, nil) - if err != nil { - // If the server cannot start, don't pretend we can continue. - panic("Error trying to start chat/http server: " + err.Error()) - } -} - -func handleInterrupt(exit chan bool) { - ch := make(chan os.Signal) - signal.Notify(ch, os.Interrupt) - <-ch - common.LogInfoln("Closing server") - if settings.StreamStats { - stats.Print() - } - exit <- true + + chatServer.RegisterOnShutdown(func() { chat.Shutdown() }) + // rtmp.Server does not implement .RegisterOnShutdown() + //server.RegisterOnShutdown(func() { common.LogDebugln("server shutdown callback called.") }) + + // These have been moved back to annon functitons so I could use + // `server`, `chatServer`, and `exit` in them without needing to + // pass them as parameters. + + // Signal handler + exit := make(chan bool) + go func() { + ch := make(chan os.Signal) + signal.Notify(ch, os.Interrupt) + <-ch + common.LogInfoln("Closing server") + if settings.StreamStats { + stats.Print() + } + + if err := chatServer.Shutdown(context.Background()); err != nil { + common.LogErrorf("Error shutting down chat server: %v", err) + } + + common.LogInfoln("Shutdown() sent. Sending exit.") + exit <- true + }() + + // Chat and HTTP server + go func() { + // Use a ServeMux here instead of the default, global, + // http handler. It's a good idea when we're starting more + // than one server. + mux := http.NewServeMux() + mux.HandleFunc("/ws", wsHandler) + mux.HandleFunc("/static/js/", wsStaticFiles) + mux.HandleFunc("/static/css/", wsStaticFiles) + mux.HandleFunc("/static/img/", wsImages) + mux.HandleFunc("/static/main.wasm", wsWasmFile) + mux.HandleFunc("/emotes/", wsEmotes) + mux.HandleFunc("/favicon.ico", wsStaticFiles) + mux.HandleFunc("/chat", handleIndexTemplate) + mux.HandleFunc("/video", handleIndexTemplate) + mux.HandleFunc("/help", handleHelpTemplate) + mux.HandleFunc("/pin", handlePin) + + mux.HandleFunc("/", handleDefault) + + chatServer.Handler = mux + err := chatServer.ListenAndServe() + // http.ErrServerClosed is returned when server.Shuddown() + // is called. + if err != http.ErrServerClosed { + // If the server cannot start, don't pretend we can continue. + panic("Error trying to start chat/http server: " + err.Error()) + } + common.LogDebugln("ChatServer closed.") + }() + + // RTMP server + go func() { + err := server.ListenAndServe() + // http.ErrServerClosed is returned when server.Shuddown() + // is called. + if err != http.ErrServerClosed { + // If the server cannot start, don't pretend we can continue. + panic("Error trying to start rtmp server: " + err.Error()) + } + common.LogDebugln("RTMP server closed.") + }() + + <-exit } diff --git a/static/css/site.css b/static/css/site.css index 8b254b0..413937a 100644 --- a/static/css/site.css +++ b/static/css/site.css @@ -42,6 +42,23 @@ span.svmsg { color: #ea6260; } +input[type=text] { + background: transparent; + border: var(--var-border); + border-radius: var(--var-border-radius); + color: var(--var-message-color); + padding: 5px; + font-weight: bold; +} + +#colorInputDiv { + font-size: 14px; +} + +#colorInput { + border: 2px solid var(--var-message-color); +} + .root { max-width: var(--var-max-width); max-height: var(--var-max-height); @@ -223,15 +240,6 @@ span.svmsg { margin: 5px auto; } -#name { - background: transparent; - border: var(--var-border); - border-radius: var(--var-border-radius); - color: var(--var-message-color); - padding: 5px; - font-weight: bold; -} - #chat { display: grid; grid-template-rows: 1.5em 2em 1fr 6em 2.5em 1em; @@ -295,6 +303,12 @@ span.svmsg { color: #b1b1b1; } +#colorName { + font-weight: bold; + background: var(--var-background-color); + padding: -10px; +} + #colorSubmit:disabled { display: none; } diff --git a/static/js/chat.js b/static/js/chat.js index 6c5a0eb..abe0587 100644 --- a/static/js/chat.js +++ b/static/js/chat.js @@ -158,6 +158,10 @@ function help() { } function showColors(show) { + if (show === undefined) { + show = $("#hiddencolor").css("display") === "none"; + } + $("#hiddencolor").css("display", show ? "block" : ""); } @@ -188,11 +192,22 @@ function updateColor() { function changeColor() { if (isValidColor(colorAsHex())) { - sendMessage("/color " + colorAsHex()); - showColors(false); + sendColor(colorAsHex()); } } +function colorSelectChange() { + let val = $("#colorSelect").val() + if (val !== "") { + sendColor(val); + } +} + +function sendColor(color) { + sendMessage("/color " + color); + showColors(false); +} + function setTimestamp(v) { showTimestamp(v) document.cookie = "timestamp=" + v @@ -268,37 +283,3 @@ window.addEventListener("load", () => { // Make sure name is focused on start $("#name").focus(); }); - -function pleaseremovethis() { - colors = ["aliceblue", "antiquewhite", "aqua", "aquamarine", "azure", - "beige", "bisque", "black", "blanchedalmond", "blue", - "blueviolet", "brown", "burlywood", "cadetblue", "chartreuse", - "chocolate", "coral", "cornflowerblue", "cornsilk", "crimson", - "cyan", "darkblue", "darkcyan", "darkgoldenrod", "darkgray", - "darkgrey", "darkgreen", "darkkhaki", "darkmagenta", "darkolivegreen", - "darkorange", "darkorchid", "darkred", "darksalmon", "darkseagreen", - "darkslateblue", "darkslategray", "darkslategrey", "darkturquoise", "darkviolet", - "deeppink", "deepskyblue", "dimgray", "dimgrey", "dodgerblue", - "firebrick", "floralwhite", "forestgreen", "fuchsia", "gainsboro", - "ghostwhite", "gold", "goldenrod", "gray", "grey", - "green", "greenyellow", "honeydew", "hotpink", "indianred", - "indigo", "ivory", "khaki", "lavender", "lavenderblush", - "lawngreen", "lemonchiffon", "lightblue", "lightcoral", "lightcyan", - "lightgoldenrodyellow", "lightgray", "lightgrey", "lightgreen", "lightpink", - "lightsalmon", "lightseagreen", "lightskyblue", "lightslategray", "lightslategrey", - "lightsteelblue", "lightyellow", "lime", "limegreen", "linen", - "magenta", "maroon", "mediumaquamarine", "mediumblue", "mediumorchid", - "mediumpurple", "mediumseagreen", "mediumslateblue", "mediumspringgreen", "mediumturquoise", - "mediumvioletred", "midnightblue", "mintcream", "mistyrose", "moccasin", - "navajowhite", "navy", "oldlace", "olive", "olivedrab", - "orange", "orangered", "orchid", "palegoldenrod", "palegreen", - "paleturquoise", "palevioletred", "papayawhip", "peachpuff", "peru", - "pink", "plum", "powderblue", "purple", "rebeccapurple", - "red", "rosybrown", "royalblue", "saddlebrown", "salmon", - "sandybrown", "seagreen", "seashell", "sienna", "silver", - "skyblue", "slateblue", "slategray", "slategrey", "snow", - "springgreen", "steelblue", "tan", "teal", "thistle", - "tomato", "turquoise", "violet", "wheat", "white", - "whitesmoke", "yellow", "yellowgreen",] - -} diff --git a/static/main.html b/static/main.html index 3607ce8..7704486 100644 --- a/static/main.html +++ b/static/main.html @@ -50,7 +50,7 @@
- + {{if .Video}} @@ -75,7 +75,10 @@
-
+
+ +
+
NAME
diff --git a/wasm/main.go b/wasm/main.go index 5df650b..7092d2a 100644 --- a/wasm/main.go +++ b/wasm/main.go @@ -189,6 +189,16 @@ func main() { js.Set("debugValues", js.CallbackOf(debugValues)) js.Set("showTimestamp", js.CallbackOf(showTimestamp)) + go func() { + time.Sleep(time.Second * 1) + inner := `` + for _, c := range common.Colors { + inner += fmt.Sprintf(`\n`, c, c) + } + + js.Get("colorSelect").Set("innerHTML", inner) + }() + // This is needed so the goroutine does not end for { // heatbeat to keep connection alive to deal with nginx