Merge branch 'master' into room-access-restrictions

# Conflicts:
#	main.go
This commit is contained in:
Zorchenhimer 2019-03-30 17:45:39 -04:00
commit 34d8fbf20c
8 changed files with 246 additions and 128 deletions

View File

@ -30,6 +30,8 @@ type ChatRoom struct {
modPasswords []string // single-use mod passwords modPasswords []string // single-use mod passwords
modPasswordsMtx sync.Mutex modPasswordsMtx sync.Mutex
isShutdown bool
} }
//initializing the chatroom //initializing the chatroom
@ -53,6 +55,11 @@ func newChatRoom() (*ChatRoom, error) {
} }
func (cr *ChatRoom) JoinTemp(conn *chatConnection) (string, 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() defer cr.clientsMtx.Unlock()
cr.clientsMtx.Lock() cr.clientsMtx.Lock()
@ -77,6 +84,11 @@ func (cr *ChatRoom) JoinTemp(conn *chatConnection) (string, error) {
//registering a new client //registering a new client
//returns pointer to a Client, or Nil, if the name is already taken //returns pointer to a Client, or Nil, if the name is already taken
func (cr *ChatRoom) Join(name, uid string) (*Client, error) { 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() defer cr.clientsMtx.Unlock()
cr.clientsMtx.Lock() 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) 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")
}

View File

@ -7,39 +7,36 @@ import (
"github.com/lucasb-eyer/go-colorful" "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 // 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 // this saves from having to call strings.ToLower(color) every time to check
var colors = []string{ var Colors = []string{
"aliceblue", "antiquewhite", "aqua", "aquamarine", "azure", "aliceblue", "antiquewhite", "aqua", "aquamarine", "azure",
"beige", "bisque", "black", "blanchedalmond", "blue", "beige", "bisque", "blanchedalmond", "blueviolet", "brown",
"blueviolet", "brown", "burlywood", "cadetblue", "chartreuse", "burlywood", "cadetblue", "chartreuse", "chocolate", "coral",
"chocolate", "coral", "cornflowerblue", "cornsilk", "crimson", "cornflowerblue", "cornsilk", "crimson", "cyan", "darkcyan",
"cyan", "darkblue", "darkcyan", "darkgoldenrod", "darkgray", "darkgoldenrod", "darkgray", "darkkhaki", "darkmagenta", "darkolivegreen",
"darkgrey", "darkgreen", "darkkhaki", "darkmagenta", "darkolivegreen", "darkorange", "darkorchid", "darksalmon", "darkseagreen", "darkslateblue",
"darkorange", "darkorchid", "darkred", "darksalmon", "darkseagreen", "darkslategray", "darkturquoise", "darkviolet", "deeppink", "deepskyblue",
"darkslateblue", "darkslategray", "darkslategrey", "darkturquoise", "darkviolet", "dimgray", "dodgerblue", "firebrick", "floralwhite", "forestgreen",
"deeppink", "deepskyblue", "dimgray", "dimgrey", "dodgerblue", "fuchsia", "gainsboro", "ghostwhite", "gold", "goldenrod",
"firebrick", "floralwhite", "forestgreen", "fuchsia", "gainsboro", "gray", "greenyellow", "honeydew", "hotpink", "indigo",
"ghostwhite", "gold", "goldenrod", "gray", "grey", "ivory", "khaki", "lavender", "lavenderblush", "lawngreen",
"green", "greenyellow", "honeydew", "hotpink", "indianred", "lemonchiffon", "lightblue", "lightcoral", "lightcyan", "lightgoldenrodyellow",
"indigo", "ivory", "khaki", "lavender", "lavenderblush", "lightgrey", "lightgreen", "lightpink", "lightsalmon", "lightseagreen",
"lawngreen", "lemonchiffon", "lightblue", "lightcoral", "lightcyan", "lightskyblue", "lightslategray", "lightsteelblue", "lightyellow", "lime",
"lightgoldenrodyellow", "lightgray", "lightgrey", "lightgreen", "lightpink", "limegreen", "linen", "magenta", "mediumaquamarine", "mediumorchid",
"lightsalmon", "lightseagreen", "lightskyblue", "lightslategray", "lightslategrey",
"lightsteelblue", "lightyellow", "lime", "limegreen", "linen",
"magenta", "maroon", "mediumaquamarine", "mediumblue", "mediumorchid",
"mediumpurple", "mediumseagreen", "mediumslateblue", "mediumspringgreen", "mediumturquoise", "mediumpurple", "mediumseagreen", "mediumslateblue", "mediumspringgreen", "mediumturquoise",
"mediumvioletred", "midnightblue", "mintcream", "mistyrose", "moccasin", "mediumvioletred", "mintcream", "mistyrose", "moccasin", "navajowhite",
"navajowhite", "navy", "oldlace", "olive", "olivedrab", "oldlace", "olive", "olivedrab", "orange", "orangered",
"orange", "orangered", "orchid", "palegoldenrod", "palegreen", "orchid", "palegoldenrod", "palegreen", "paleturquoise", "palevioletred",
"paleturquoise", "palevioletred", "papayawhip", "peachpuff", "peru", "papayawhip", "peachpuff", "peru", "pink", "plum",
"pink", "plum", "powderblue", "purple", "rebeccapurple", "powderblue", "purple", "rebeccapurple", "red", "rosybrown",
"red", "rosybrown", "royalblue", "saddlebrown", "salmon", "royalblue", "saddlebrown", "salmon", "sandybrown", "seagreen",
"sandybrown", "seagreen", "seashell", "sienna", "silver", "seashell", "sienna", "silver", "skyblue", "slateblue",
"skyblue", "slateblue", "slategray", "slategrey", "snow", "slategray", "snow", "springgreen", "steelblue", "tan",
"springgreen", "steelblue", "tan", "teal", "thistle", "teal", "thistle", "tomato", "turquoise", "violet",
"tomato", "turquoise", "violet", "wheat", "white", "wheat", "white", "whitesmoke", "yellow", "yellowgreen",
"whitesmoke", "yellow", "yellowgreen",
} }
var ( var (
@ -50,7 +47,7 @@ var (
// It also accepts hex codes in the form of #RGB and #RRGGBB // It also accepts hex codes in the form of #RGB and #RRGGBB
func IsValidColor(s string) bool { func IsValidColor(s string) bool {
s = strings.ToLower(s) s = strings.ToLower(s)
for _, c := range colors { for _, c := range Colors {
if s == c { if s == c {
return true return true
} }

44
common/emotes_test.go Normal file
View File

@ -0,0 +1,44 @@
package common
import (
"os"
"testing"
)
var data_good = map[string]string{
"one": `<img src="/emotes/one.png" height="28px" title="one" />`,
"two": `<img src="/emotes/two.png" height="28px" title="two" />`,
"three": `<img src="/emotes/three.gif" height="28px" title="three" />`,
":one:": `<img src="/emotes/one.png" height="28px" title="one" />`,
":two:": `<img src="/emotes/two.png" height="28px" title="two" />`,
":three:": `<img src="/emotes/three.gif" height="28px" title="three" />`,
"[one]": `<img src="/emotes/one.png" height="28px" title="one" />`,
"[two]": `<img src="/emotes/two.png" height="28px" title="two" />`,
"[three]": `<img src="/emotes/three.gif" height="28px" title="three" />`,
":one: two [three]": `<img src="/emotes/one.png" height="28px" title="one" /> <img src="/emotes/two.png" height="28px" title="two" /> <img src="/emotes/three.gif" height="28px" title="three" />`,
"nope one what": `nope <img src="/emotes/one.png" height="28px" title="one" /> what`,
"nope :two: what": `nope <img src="/emotes/two.png" height="28px" title="two" /> what`,
"nope [three] what": `nope <img src="/emotes/three.gif" height="28px" title="three" /> 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)
}
}
}

136
main.go
View File

@ -1,11 +1,13 @@
package main package main
import ( import (
"context"
"flag" "flag"
"fmt" "fmt"
"net/http" "net/http"
"os" "os"
"os/signal" "os/signal"
"time"
"github.com/gorilla/sessions" "github.com/gorilla/sessions"
"github.com/nareix/joy4/format" "github.com/nareix/joy4/format"
@ -14,9 +16,10 @@ import (
) )
var ( var (
addr string addr string
sKey string sKey string
stats = newStreamStats() stats = newStreamStats()
chatServer *http.Server
) )
func setupSettings() error { func setupSettings() error {
@ -51,9 +54,6 @@ func main() {
os.Exit(1) os.Exit(1)
} }
exit := make(chan bool)
go handleInterrupt(exit)
// Load emotes before starting server. // Load emotes before starting server.
var err error var err error
if chat, err = newChatRoom(); err != nil { if chat, err = newChatRoom(); err != nil {
@ -77,54 +77,88 @@ func main() {
common.LogInfoln("RoomAccess: ", settings.RoomAccess) common.LogInfoln("RoomAccess: ", settings.RoomAccess)
common.LogInfoln("RoomAccessPin: ", settings.RoomAccessPin) common.LogInfoln("RoomAccessPin: ", settings.RoomAccessPin)
go startServer()
go startRmtpServer()
<-exit
}
func startRmtpServer() {
server := &rtmp.Server{ server := &rtmp.Server{
HandlePlay: handlePlay, HandlePlay: handlePlay,
HandlePublish: handlePublish, HandlePublish: handlePublish,
} }
err := server.ListenAndServe()
if err != nil { // Define this here so we can set some timeouts and things.
// If the server cannot start, don't pretend we can continue. chatServer := &http.Server{
panic("Error trying to start rtmp server: " + err.Error()) Addr: addr,
ReadTimeout: 10 * time.Second,
WriteTimeout: 10 * time.Second,
MaxHeaderBytes: 1 << 20,
} }
}
chatServer.RegisterOnShutdown(func() { chat.Shutdown() })
func startServer() { // rtmp.Server does not implement .RegisterOnShutdown()
// Chat websocket //server.RegisterOnShutdown(func() { common.LogDebugln("server shutdown callback called.") })
http.HandleFunc("/ws", wsHandler)
http.HandleFunc("/static/js/", wsStaticFiles) // These have been moved back to annon functitons so I could use
http.HandleFunc("/static/css/", wsStaticFiles) // `server`, `chatServer`, and `exit` in them without needing to
http.HandleFunc("/static/img/", wsImages) // pass them as parameters.
http.HandleFunc("/static/main.wasm", wsWasmFile)
http.HandleFunc("/emotes/", wsEmotes) // Signal handler
http.HandleFunc("/favicon.ico", wsStaticFiles) exit := make(chan bool)
http.HandleFunc("/chat", handleIndexTemplate) go func() {
http.HandleFunc("/video", handleIndexTemplate) ch := make(chan os.Signal)
http.HandleFunc("/help", handleHelpTemplate) signal.Notify(ch, os.Interrupt)
http.HandleFunc("/pin", handlePin) <-ch
common.LogInfoln("Closing server")
http.HandleFunc("/", handleDefault) if settings.StreamStats {
stats.Print()
err := http.ListenAndServe(addr, nil) }
if err != nil {
// If the server cannot start, don't pretend we can continue. if err := chatServer.Shutdown(context.Background()); err != nil {
panic("Error trying to start chat/http server: " + err.Error()) common.LogErrorf("Error shutting down chat server: %v", err)
} }
}
common.LogInfoln("Shutdown() sent. Sending exit.")
func handleInterrupt(exit chan bool) { exit <- true
ch := make(chan os.Signal) }()
signal.Notify(ch, os.Interrupt)
<-ch // Chat and HTTP server
common.LogInfoln("Closing server") go func() {
if settings.StreamStats { // Use a ServeMux here instead of the default, global,
stats.Print() // http handler. It's a good idea when we're starting more
} // than one server.
exit <- true 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
} }

View File

@ -42,6 +42,23 @@ span.svmsg {
color: #ea6260; 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 { .root {
max-width: var(--var-max-width); max-width: var(--var-max-width);
max-height: var(--var-max-height); max-height: var(--var-max-height);
@ -223,15 +240,6 @@ span.svmsg {
margin: 5px auto; 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 { #chat {
display: grid; display: grid;
grid-template-rows: 1.5em 2em 1fr 6em 2.5em 1em; grid-template-rows: 1.5em 2em 1fr 6em 2.5em 1em;
@ -295,6 +303,12 @@ span.svmsg {
color: #b1b1b1; color: #b1b1b1;
} }
#colorName {
font-weight: bold;
background: var(--var-background-color);
padding: -10px;
}
#colorSubmit:disabled { #colorSubmit:disabled {
display: none; display: none;
} }

View File

@ -158,6 +158,10 @@ function help() {
} }
function showColors(show) { function showColors(show) {
if (show === undefined) {
show = $("#hiddencolor").css("display") === "none";
}
$("#hiddencolor").css("display", show ? "block" : ""); $("#hiddencolor").css("display", show ? "block" : "");
} }
@ -188,11 +192,22 @@ function updateColor() {
function changeColor() { function changeColor() {
if (isValidColor(colorAsHex())) { if (isValidColor(colorAsHex())) {
sendMessage("/color " + colorAsHex()); sendColor(colorAsHex());
showColors(false);
} }
} }
function colorSelectChange() {
let val = $("#colorSelect").val()
if (val !== "") {
sendColor(val);
}
}
function sendColor(color) {
sendMessage("/color " + color);
showColors(false);
}
function setTimestamp(v) { function setTimestamp(v) {
showTimestamp(v) showTimestamp(v)
document.cookie = "timestamp=" + v document.cookie = "timestamp=" + v
@ -268,37 +283,3 @@ window.addEventListener("load", () => {
// Make sure name is focused on start // Make sure name is focused on start
$("#name").focus(); $("#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",]
}

View File

@ -50,7 +50,7 @@
<div id="chatButtons"> <div id="chatButtons">
<input type="button" class="button pretty-button" onclick="auth();" value="Auth" /> <input type="button" class="button pretty-button" onclick="auth();" value="Auth" />
<input type="button" class="button pretty-button" onclick="help();" value="Help" /> <input type="button" class="button pretty-button" onclick="help();" value="Help" />
<input type="button" class="button pretty-button" onclick="showColors(true);" value="Color" /> <input type="button" class="button pretty-button" onclick="showColors();" value="Color" />
<input type="button" class="button pretty-button" onclick="nick();" value="Nick" /> <input type="button" class="button pretty-button" onclick="nick();" value="Nick" />
{{if .Video}} {{if .Video}}
<input type="button" class="button pretty-button" onclick="initPlayer();" value="Reload Player" /> <input type="button" class="button pretty-button" onclick="initPlayer();" value="Reload Player" />
@ -75,7 +75,10 @@
<input id="colorBlue" type="range" min="0" max="255" value="0" oninput="updateColor();" /> <input id="colorBlue" type="range" min="0" max="255" value="0" oninput="updateColor();" />
<span id="colorBlueLabel"></span> <span id="colorBlueLabel"></span>
</div> </div>
<div id="colorName" class="range-div" style="font-weight: bold;"> <div class="range-div">
<select id="colorSelect" onchange="colorSelectChange();"></select>
</div>
<div id="colorName" class="range-div">
NAME NAME
</div> </div>
<div id="colorWarning" class="range-div contrast"> <div id="colorWarning" class="range-div contrast">

View File

@ -189,6 +189,16 @@ func main() {
js.Set("debugValues", js.CallbackOf(debugValues)) js.Set("debugValues", js.CallbackOf(debugValues))
js.Set("showTimestamp", js.CallbackOf(showTimestamp)) js.Set("showTimestamp", js.CallbackOf(showTimestamp))
go func() {
time.Sleep(time.Second * 1)
inner := `<option value=""></option>`
for _, c := range common.Colors {
inner += fmt.Sprintf(`<option value="%s">%s</option>\n`, c, c)
}
js.Get("colorSelect").Set("innerHTML", inner)
}()
// This is needed so the goroutine does not end // This is needed so the goroutine does not end
for { for {
// heatbeat to keep connection alive to deal with nginx // heatbeat to keep connection alive to deal with nginx