archive
This commit is contained in:
46
MovieNight/.gitignore
vendored
Executable file
46
MovieNight/.gitignore
vendored
Executable file
@@ -0,0 +1,46 @@
|
|||||||
|
# Binaries for programs and plugins
|
||||||
|
*.exe
|
||||||
|
*.exe~
|
||||||
|
*.dll
|
||||||
|
*.so
|
||||||
|
*.dylib
|
||||||
|
*.aseprite
|
||||||
|
|
||||||
|
# Test binary, built with `go test -c`
|
||||||
|
*.test
|
||||||
|
|
||||||
|
# Output of the go coverage tool, specifically when used with LiteIDE
|
||||||
|
*.out
|
||||||
|
|
||||||
|
# Log files
|
||||||
|
*.log
|
||||||
|
|
||||||
|
# GoCode debug file
|
||||||
|
debug
|
||||||
|
|
||||||
|
# Linux binary
|
||||||
|
MovieNight
|
||||||
|
|
||||||
|
# Windows binary
|
||||||
|
MovieNight.exe
|
||||||
|
|
||||||
|
# Darwin binary
|
||||||
|
MovieNightDarwin
|
||||||
|
|
||||||
|
# Twitch channel info
|
||||||
|
static/subscriber.json
|
||||||
|
|
||||||
|
# This file now holds the stream key. Don't include it.
|
||||||
|
settings.json
|
||||||
|
|
||||||
|
# vscode
|
||||||
|
.vscode
|
||||||
|
|
||||||
|
# Autobuilt wasm files
|
||||||
|
static/main.wasm
|
||||||
|
|
||||||
|
# tags for vim
|
||||||
|
tags
|
||||||
|
|
||||||
|
# channel and emote list from twitch
|
||||||
|
subscribers.json
|
||||||
10
MovieNight/.travis.yml
Executable file
10
MovieNight/.travis.yml
Executable file
@@ -0,0 +1,10 @@
|
|||||||
|
language: go
|
||||||
|
|
||||||
|
before_install:
|
||||||
|
- make get
|
||||||
|
|
||||||
|
go:
|
||||||
|
- 1.12.x
|
||||||
|
|
||||||
|
env:
|
||||||
|
- GO111MODULE=on
|
||||||
18
MovieNight/Dockerfile
Executable file
18
MovieNight/Dockerfile
Executable file
@@ -0,0 +1,18 @@
|
|||||||
|
FROM frolvlad/alpine-glibc:alpine-3.9_glibc-2.29
|
||||||
|
|
||||||
|
RUN apk update \
|
||||||
|
&& apk add --no-cache \
|
||||||
|
ca-certificates \
|
||||||
|
ffmpeg \
|
||||||
|
bash
|
||||||
|
|
||||||
|
RUN mkdir -p /var/log
|
||||||
|
WORKDIR /main
|
||||||
|
|
||||||
|
COPY . .
|
||||||
|
|
||||||
|
ENV GOPATH=""
|
||||||
|
ENV MNT="/mnt/"
|
||||||
|
ENTRYPOINT ["/main/MovieNight"]
|
||||||
|
CMD []
|
||||||
|
|
||||||
53
MovieNight/Makefile
Executable file
53
MovieNight/Makefile
Executable file
@@ -0,0 +1,53 @@
|
|||||||
|
# If a different version of Go is installed (via `go get`) set the GO_VERSION
|
||||||
|
# environment variable to that version. For example, setting it to "1.13.7"
|
||||||
|
# will run `go1.13.7 build [...]` instead of `go build [...]`.
|
||||||
|
#
|
||||||
|
# For info on installing extra versions, see this page:
|
||||||
|
# https://golang.org/doc/install#extra_versions
|
||||||
|
|
||||||
|
TAGS=
|
||||||
|
|
||||||
|
# Windows needs the .exe extension.
|
||||||
|
ifeq ($(OS),Windows_NT)
|
||||||
|
EXT=.exe
|
||||||
|
endif
|
||||||
|
|
||||||
|
.PHONY: fmt vet get clean dev setdev test ServerMovieNight
|
||||||
|
|
||||||
|
all: fmt vet test MovieNight$(EXT) static/main.wasm settings.json
|
||||||
|
|
||||||
|
# Build the server deployment
|
||||||
|
server: ServerMovieNight static/main.wasm
|
||||||
|
|
||||||
|
# Bulid used for deploying to my server.
|
||||||
|
ServerMovieNight: *.go common/*.go
|
||||||
|
GOOS=linux GOARCH=386 go$(GO_VERSION) build -o MovieNight $(TAGS)
|
||||||
|
|
||||||
|
setdev:
|
||||||
|
$(eval export TAGS=-tags "dev")
|
||||||
|
|
||||||
|
dev: setdev all
|
||||||
|
|
||||||
|
MovieNight$(EXT): *.go common/*.go
|
||||||
|
go$(GO_VERSION) build -o $@ $(TAGS)
|
||||||
|
|
||||||
|
static/main.wasm: wasm/*.go common/*.go
|
||||||
|
GOOS=js GOARCH=wasm go$(GO_VERSION) build -o $@ $(TAGS) wasm/*.go
|
||||||
|
|
||||||
|
clean:
|
||||||
|
-rm MovieNight$(EXT) ./static/main.wasm
|
||||||
|
|
||||||
|
fmt:
|
||||||
|
gofmt -w .
|
||||||
|
|
||||||
|
vet:
|
||||||
|
go$(GO_VERSION) vet $(TAGS) ./...
|
||||||
|
GOOS=js GOARCH=wasm go$(GO_VERSION) vet $(TAGS) ./...
|
||||||
|
|
||||||
|
test:
|
||||||
|
go$(GO_VERSION) test $(TAGS) ./...
|
||||||
|
|
||||||
|
# Do not put settings_example.json here as a prereq to avoid overwriting
|
||||||
|
# the settings if the example is updated.
|
||||||
|
settings.json:
|
||||||
|
cp settings_example.json settings.json
|
||||||
205
MovieNight/chatclient.go
Executable file
205
MovieNight/chatclient.go
Executable file
@@ -0,0 +1,205 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"html"
|
||||||
|
"regexp"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
"unicode"
|
||||||
|
|
||||||
|
"github.com/zorchenhimer/MovieNight/common"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
regexSpoiler = regexp.MustCompile(`\|\|(.*?)\|\|`)
|
||||||
|
spoilerStart = `<span class="spoiler" onclick='$(this).removeClass("spoiler").addClass("spoiler-active")'>`
|
||||||
|
spoilerEnd = `</span>`
|
||||||
|
)
|
||||||
|
|
||||||
|
type Client struct {
|
||||||
|
name string // Display name
|
||||||
|
conn *chatConnection
|
||||||
|
belongsTo *ChatRoom
|
||||||
|
color string
|
||||||
|
CmdLevel common.CommandLevel
|
||||||
|
IsColorForced bool
|
||||||
|
IsNameForced bool
|
||||||
|
regexName *regexp.Regexp
|
||||||
|
|
||||||
|
// Times since last event. use time.Duration.Since()
|
||||||
|
nextChat time.Time // rate limit chat messages
|
||||||
|
nextNick time.Time // rate limit nickname changes
|
||||||
|
nextColor time.Time // rate limit color changes
|
||||||
|
nextAuth time.Time // rate limit failed auth attempts. Sould prolly have a backoff policy.
|
||||||
|
authTries int // number of failed auth attempts
|
||||||
|
|
||||||
|
nextDuplicate time.Time
|
||||||
|
lastMsg string
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewClient(connection *chatConnection, room *ChatRoom, name, color string) (*Client, error) {
|
||||||
|
c := &Client{
|
||||||
|
conn: connection,
|
||||||
|
belongsTo: room,
|
||||||
|
color: color,
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := c.setName(name); err != nil {
|
||||||
|
return nil, fmt.Errorf("could not set client name to %#v: %v", name, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set initial vaules to their rate limit duration in the past.
|
||||||
|
c.nextChat = time.Now()
|
||||||
|
c.nextNick = time.Now()
|
||||||
|
c.nextColor = time.Now()
|
||||||
|
c.nextAuth = time.Now()
|
||||||
|
|
||||||
|
return c, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
//Client has a new message to broadcast
|
||||||
|
func (cl *Client) NewMsg(data common.ClientData) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func (cl *Client) SendChatData(data common.ChatData) error {
|
||||||
|
// Don't send chat or event data to clients that have not fully joined the
|
||||||
|
// chatroom (ie, they have not set a name).
|
||||||
|
if cl.name == "" && (data.Type == common.DTChat || data.Type == common.DTEvent) {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Colorize name on chat messages
|
||||||
|
if data.Type == common.DTChat {
|
||||||
|
var err error
|
||||||
|
data = cl.replaceColorizedName(data)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("could not colorize name: %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
cd, err := data.ToJSON()
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("could not create ChatDataJSON of type %d: %v", data.Type, err)
|
||||||
|
}
|
||||||
|
return cl.Send(cd)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (cl *Client) Send(data common.ChatDataJSON) error {
|
||||||
|
err := cl.conn.WriteData(data)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("could not send message: %v", err)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (cl *Client) SendServerMessage(s string) error {
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Make links clickable
|
||||||
|
func formatLinks(input string) string {
|
||||||
|
newMsg := []string{}
|
||||||
|
for _, word := range strings.Split(input, " ") {
|
||||||
|
if strings.HasPrefix(word, "http://") || strings.HasPrefix(word, "https://") {
|
||||||
|
word = html.UnescapeString(word)
|
||||||
|
word = fmt.Sprintf(`<a href="%s" target="_blank">%s</a>`, word, word)
|
||||||
|
}
|
||||||
|
newMsg = append(newMsg, word)
|
||||||
|
}
|
||||||
|
return strings.Join(newMsg, " ")
|
||||||
|
}
|
||||||
|
|
||||||
|
//Exiting out
|
||||||
|
func (cl *Client) Exit() {
|
||||||
|
cl.belongsTo.Leave(cl.name, cl.color)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Outgoing messages
|
||||||
|
func (cl *Client) Message(msg string) {
|
||||||
|
msg = common.ParseEmotes(msg)
|
||||||
|
cl.belongsTo.AddMsg(cl, false, false, msg)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Outgoing /me command
|
||||||
|
func (cl *Client) Me(msg string) {
|
||||||
|
msg = common.ParseEmotes(msg)
|
||||||
|
cl.belongsTo.AddMsg(cl, true, false, msg)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (cl *Client) Mod() {
|
||||||
|
if cl.CmdLevel < common.CmdlMod {
|
||||||
|
cl.CmdLevel = common.CmdlMod
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (cl *Client) Unmod() {
|
||||||
|
cl.CmdLevel = common.CmdlUser
|
||||||
|
}
|
||||||
|
|
||||||
|
func (cl *Client) Host() string {
|
||||||
|
return cl.conn.Host()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (cl *Client) setName(s string) error {
|
||||||
|
cl.name = s
|
||||||
|
if cl.conn != nil {
|
||||||
|
cl.conn.clientName = s
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (cl *Client) setColor(s string) error {
|
||||||
|
cl.color = s
|
||||||
|
return cl.SendChatData(common.NewChatHiddenMessage(common.CdColor, cl.color))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (cl *Client) replaceColorizedName(chatData common.ChatData) common.ChatData {
|
||||||
|
data := chatData.Data.(common.DataMessage)
|
||||||
|
words := strings.Split(data.Message, " ")
|
||||||
|
newWords := []string{}
|
||||||
|
|
||||||
|
for _, word := range words {
|
||||||
|
if strings.ToLower(word) == strings.ToLower(cl.name) || strings.ToLower(word) == strings.ToLower("@"+cl.name) {
|
||||||
|
newWords = append(newWords, `<span class="mention">`+word+`</span>`)
|
||||||
|
} else {
|
||||||
|
newWords = append(newWords, word)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
data.Message = strings.Join(newWords, " ")
|
||||||
|
chatData.Data = data
|
||||||
|
return chatData
|
||||||
|
}
|
||||||
|
|
||||||
|
var dumbSpaces = []string{
|
||||||
|
"\n",
|
||||||
|
"\t",
|
||||||
|
"\r",
|
||||||
|
"\u200b",
|
||||||
|
}
|
||||||
|
|
||||||
|
func removeDumbSpaces(msg string) string {
|
||||||
|
for _, ds := range dumbSpaces {
|
||||||
|
msg = strings.ReplaceAll(msg, ds, " ")
|
||||||
|
}
|
||||||
|
|
||||||
|
newMsg := ""
|
||||||
|
for _, r := range msg {
|
||||||
|
if unicode.IsSpace(r) {
|
||||||
|
newMsg += " "
|
||||||
|
} else {
|
||||||
|
newMsg += string(r)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return newMsg
|
||||||
|
}
|
||||||
|
|
||||||
|
func addSpoilerTags(msg string) string {
|
||||||
|
return regexSpoiler.ReplaceAllString(msg, fmt.Sprintf(`%s$1%s`, spoilerStart, spoilerEnd))
|
||||||
|
}
|
||||||
55
MovieNight/chatclient_test.go
Executable file
55
MovieNight/chatclient_test.go
Executable file
@@ -0,0 +1,55 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/zorchenhimer/MovieNight/common"
|
||||||
|
)
|
||||||
|
|
||||||
|
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])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Name highlighting should not interfere with emotes
|
||||||
|
func TestClient_emoteHighlight(t *testing.T) {
|
||||||
|
data := [][]string{
|
||||||
|
{"zorchenhimer", `<span class="mention">zorchenhimer</span>`},
|
||||||
|
{"@zorchenhimer", `<span class="mention">@zorchenhimer</span>`},
|
||||||
|
{"Zorchenhimer", `<span class="mention">Zorchenhimer</span>`},
|
||||||
|
{"@Zorchenhimer", `<span class="mention">@Zorchenhimer</span>`},
|
||||||
|
{"hello zorchenhimer", `hello <span class="mention">zorchenhimer</span>`},
|
||||||
|
{"hello zorchenhimer ass", `hello <span class="mention">zorchenhimer</span> ass`},
|
||||||
|
{`<img src="/emotes/twitch/zorchenhimer/zorcheWhat.png" height="28px" title="zorcheWhat">`, `<img src="/emotes/twitch/zorchenhimer/zorcheWhat.png" height="28px" title="zorcheWhat">`},
|
||||||
|
{`zorchenhimer <img src="/emotes/twitch/zorchenhimer/zorcheWhat.png" height="28px" title="zorcheWhat">`, `<span class="mention">zorchenhimer</span> <img src="/emotes/twitch/zorchenhimer/zorcheWhat.png" height="28px" title="zorcheWhat">`},
|
||||||
|
}
|
||||||
|
|
||||||
|
client, err := NewClient(nil, nil, "Zorchenhimer", "#9547ff")
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("Client init error: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, d := range data {
|
||||||
|
chatData := client.replaceColorizedName(common.NewChatMessage(client.name, client.color, d[0], common.CmdlUser, common.MsgChat))
|
||||||
|
if chatData.Data.(common.DataMessage).Message != d[1] {
|
||||||
|
t.Errorf("\nExpected:\n\t%s\nReceived\n\t%s", d[1], chatData.Data.(common.DataMessage).Message)
|
||||||
|
} else {
|
||||||
|
t.Logf("Passed %s", d[0])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
645
MovieNight/chatcommands.go
Executable file
645
MovieNight/chatcommands.go
Executable file
@@ -0,0 +1,645 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"html"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/zorchenhimer/MovieNight/common"
|
||||||
|
)
|
||||||
|
|
||||||
|
type CommandControl struct {
|
||||||
|
user map[string]Command
|
||||||
|
mod map[string]Command
|
||||||
|
admin map[string]Command
|
||||||
|
}
|
||||||
|
|
||||||
|
type Command struct {
|
||||||
|
HelpText string
|
||||||
|
Function CommandFunction
|
||||||
|
}
|
||||||
|
|
||||||
|
type CommandFunction func(client *Client, args []string) (string, error)
|
||||||
|
|
||||||
|
var commands = &CommandControl{
|
||||||
|
user: map[string]Command{
|
||||||
|
common.CNMe.String(): Command{
|
||||||
|
HelpText: "Display an action message.",
|
||||||
|
Function: func(client *Client, args []string) (string, error) {
|
||||||
|
if len(args) != 0 {
|
||||||
|
client.Me(strings.Join(args, " "))
|
||||||
|
return "", nil
|
||||||
|
}
|
||||||
|
return "", fmt.Errorf("Missing a message")
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
common.CNHelp.String(): Command{
|
||||||
|
HelpText: "This help text.",
|
||||||
|
Function: cmdHelp,
|
||||||
|
},
|
||||||
|
|
||||||
|
common.CNEmotes.String(): Command{
|
||||||
|
HelpText: "Display a list of available emotes.",
|
||||||
|
Function: func(client *Client, args []string) (string, error) {
|
||||||
|
client.SendChatData(common.NewChatCommand(common.CmdEmotes, []string{"/emotes"}))
|
||||||
|
return "Opening emote list in new window.", nil
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
common.CNCount.String(): Command{
|
||||||
|
HelpText: "Display number of users in chat.",
|
||||||
|
Function: func(client *Client, args []string) (string, error) {
|
||||||
|
return fmt.Sprintf("Users in chat: %d", client.belongsTo.UserCount()), nil
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
common.CNColor.String(): Command{
|
||||||
|
HelpText: "Change user color.",
|
||||||
|
Function: func(cl *Client, args []string) (string, error) {
|
||||||
|
if len(args) > 2 {
|
||||||
|
return "", fmt.Errorf("Too many arguments!")
|
||||||
|
}
|
||||||
|
|
||||||
|
// If the caller is privileged enough, they can change the color of another user
|
||||||
|
if len(args) == 2 {
|
||||||
|
if cl.CmdLevel == common.CmdlUser {
|
||||||
|
return "", fmt.Errorf("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 "", fmt.Errorf("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 "", fmt.Errorf("Cannot determine name. Prefix name with @.")
|
||||||
|
}
|
||||||
|
if color == "" {
|
||||||
|
return "", fmt.Errorf("Cannot determine color. Prefix name with @.")
|
||||||
|
}
|
||||||
|
|
||||||
|
if color == "" {
|
||||||
|
common.LogInfof("[color:mod] %s missing color\n", cl.name)
|
||||||
|
return "", fmt.Errorf("Missing color")
|
||||||
|
}
|
||||||
|
|
||||||
|
if name == "" {
|
||||||
|
common.LogInfof("[color:mod] %s missing name\n", cl.name)
|
||||||
|
return "", fmt.Errorf("Missing name")
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := cl.belongsTo.ForceColorChange(name, color); err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
return fmt.Sprintf("Color changed for user %s to %s\n", name, color), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Don't allow an unprivileged user to change their color if
|
||||||
|
// it was changed by a mod
|
||||||
|
if cl.IsColorForced {
|
||||||
|
common.LogInfof("[color] %s tried to change a forced color\n", cl.name)
|
||||||
|
return "", fmt.Errorf("You are not allowed to change your color.")
|
||||||
|
}
|
||||||
|
|
||||||
|
if time.Now().Before(cl.nextColor) && cl.CmdLevel == common.CmdlUser {
|
||||||
|
return "", fmt.Errorf("Slow down. You can change your color in %0.0f seconds.", time.Until(cl.nextColor).Seconds())
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(args) == 0 {
|
||||||
|
cl.setColor(common.RandomColor())
|
||||||
|
return "Random color chosen: " + cl.color, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Change the color of the user
|
||||||
|
if !common.IsValidColor(args[0]) {
|
||||||
|
return "", fmt.Errorf("To choose a specific color use the format <i>/color #c029ce</i>. Hex values expected.")
|
||||||
|
}
|
||||||
|
|
||||||
|
cl.nextColor = time.Now().Add(time.Second * settings.RateLimitColor)
|
||||||
|
|
||||||
|
err := cl.setColor(args[0])
|
||||||
|
if err != nil {
|
||||||
|
common.LogErrorf("[color] could not send color update to client: %v\n", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
common.LogInfof("[color] %s new color: %s\n", cl.name, cl.color)
|
||||||
|
return "Color changed successfully.", nil
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
common.CNWhoAmI.String(): Command{
|
||||||
|
HelpText: "Shows debug user info",
|
||||||
|
Function: func(cl *Client, args []string) (string, error) {
|
||||||
|
return fmt.Sprintf("Name: %s IsMod: %t IsAdmin: %t",
|
||||||
|
cl.name,
|
||||||
|
cl.CmdLevel >= common.CmdlMod,
|
||||||
|
cl.CmdLevel == common.CmdlAdmin), nil
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
common.CNAuth.String(): Command{
|
||||||
|
HelpText: "Authenticate to admin",
|
||||||
|
Function: func(cl *Client, args []string) (string, error) {
|
||||||
|
if cl.CmdLevel == common.CmdlAdmin {
|
||||||
|
return "", fmt.Errorf("You are already authenticated.")
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: handle back off policy
|
||||||
|
if time.Now().Before(cl.nextAuth) {
|
||||||
|
cl.nextAuth = time.Now().Add(time.Second * settings.RateLimitAuth)
|
||||||
|
return "", fmt.Errorf("Slow down.")
|
||||||
|
}
|
||||||
|
cl.authTries += 1 // this isn't used yet
|
||||||
|
cl.nextAuth = time.Now().Add(time.Second * settings.RateLimitAuth)
|
||||||
|
|
||||||
|
pw := html.UnescapeString(strings.Join(args, " "))
|
||||||
|
|
||||||
|
if settings.AdminPassword == pw {
|
||||||
|
cl.CmdLevel = common.CmdlAdmin
|
||||||
|
cl.belongsTo.AddModNotice(cl.name + " used the admin password")
|
||||||
|
common.LogInfof("[auth] %s used the admin password\n", cl.name)
|
||||||
|
return "Admin rights granted.", nil
|
||||||
|
}
|
||||||
|
|
||||||
|
if cl.belongsTo.redeemModPass(pw) {
|
||||||
|
cl.CmdLevel = common.CmdlMod
|
||||||
|
cl.belongsTo.AddModNotice(cl.name + " used a mod password")
|
||||||
|
common.LogInfof("[auth] %s used a mod password\n", cl.name)
|
||||||
|
return "Moderator privileges granted.", nil
|
||||||
|
}
|
||||||
|
|
||||||
|
cl.belongsTo.AddModNotice(cl.name + " attempted to auth without success")
|
||||||
|
common.LogInfof("[auth] %s gave an invalid password\n", cl.name)
|
||||||
|
return "", fmt.Errorf("Invalid password.")
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
common.CNUsers.String(): Command{
|
||||||
|
HelpText: "Show a list of users in chat",
|
||||||
|
Function: func(cl *Client, args []string) (string, error) {
|
||||||
|
names := cl.belongsTo.GetNames()
|
||||||
|
return strings.Join(names, " "), nil
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
common.CNNick.String(): Command{
|
||||||
|
HelpText: "Change display name",
|
||||||
|
Function: func(cl *Client, args []string) (string, error) {
|
||||||
|
if time.Now().Before(cl.nextNick) && cl.CmdLevel == common.CmdlUser {
|
||||||
|
//cl.nextNick = time.Now().Add(time.Second * settings.RateLimitNick)
|
||||||
|
return "", fmt.Errorf("Slow down. You can change your nick in %0.0f seconds.", time.Until(cl.nextNick).Seconds())
|
||||||
|
}
|
||||||
|
cl.nextNick = time.Now().Add(time.Second * settings.RateLimitNick)
|
||||||
|
|
||||||
|
if len(args) == 0 {
|
||||||
|
return "", fmt.Errorf("Missing name to change to.")
|
||||||
|
}
|
||||||
|
|
||||||
|
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.CmdLevel == common.CmdlUser {
|
||||||
|
return "", fmt.Errorf("Only admins and mods can do that PeepoSus")
|
||||||
|
}
|
||||||
|
|
||||||
|
oldName = strings.TrimLeft(args[0], "@")
|
||||||
|
newName = strings.TrimLeft(args[1], "@")
|
||||||
|
forced = true
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(args) == 1 && cl.IsNameForced && cl.CmdLevel != common.CmdlAdmin {
|
||||||
|
return "", fmt.Errorf("You cannot change your name once it has been changed by an admin.")
|
||||||
|
}
|
||||||
|
|
||||||
|
err := cl.belongsTo.changeName(oldName, newName, forced)
|
||||||
|
if err != nil {
|
||||||
|
return "", fmt.Errorf("Unable to change name: " + err.Error())
|
||||||
|
}
|
||||||
|
|
||||||
|
return "", nil
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
common.CNStats.String(): Command{
|
||||||
|
HelpText: "Show some stats for stream.",
|
||||||
|
Function: func(cl *Client, args []string) (string, error) {
|
||||||
|
cl.belongsTo.clientsMtx.Lock()
|
||||||
|
users := len(cl.belongsTo.clients)
|
||||||
|
cl.belongsTo.clientsMtx.Unlock()
|
||||||
|
|
||||||
|
// Just print max users and time alive here
|
||||||
|
return fmt.Sprintf("Current users in chat: <b>%d</b><br />Max users in chat: <b>%d</b><br />Server uptime: <b>%s</b><br />Stream uptime: <b>%s</b>",
|
||||||
|
users,
|
||||||
|
stats.getMaxUsers(),
|
||||||
|
time.Since(stats.start),
|
||||||
|
stats.getStreamLength(),
|
||||||
|
), nil
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
common.CNPin.String(): Command{
|
||||||
|
HelpText: "Display the current room access type and pin/password (if applicable).",
|
||||||
|
Function: func(cl *Client, args []string) (string, error) {
|
||||||
|
switch settings.RoomAccess {
|
||||||
|
case AccessPin:
|
||||||
|
return "Room is secured via PIN. Current PIN: " + settings.RoomAccessPin, nil
|
||||||
|
case AccessRequest:
|
||||||
|
return "Room is secured via access requests. Users must request to be granted access.", nil
|
||||||
|
}
|
||||||
|
return "Room is open access. Anybody can join.", nil
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
mod: map[string]Command{
|
||||||
|
common.CNSv.String(): Command{
|
||||||
|
HelpText: "Send a server announcement message. It will show up red with a border in chat.",
|
||||||
|
Function: func(cl *Client, args []string) (string, error) {
|
||||||
|
if len(args) == 0 {
|
||||||
|
return "", fmt.Errorf("Missing message")
|
||||||
|
}
|
||||||
|
svmsg := formatLinks(strings.Join(common.ParseEmotesArray(args), " "))
|
||||||
|
cl.belongsTo.AddModNotice("Server message from " + cl.name)
|
||||||
|
cl.belongsTo.AddMsg(cl, false, true, svmsg)
|
||||||
|
return "", nil
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
common.CNPlaying.String(): Command{
|
||||||
|
HelpText: "Set the title text and info link.",
|
||||||
|
Function: func(cl *Client, args []string) (string, error) {
|
||||||
|
// Clear/hide title if sent with no arguments.
|
||||||
|
if len(args) == 0 {
|
||||||
|
cl.belongsTo.ClearPlaying()
|
||||||
|
return "Title cleared", nil
|
||||||
|
}
|
||||||
|
link := ""
|
||||||
|
title := ""
|
||||||
|
|
||||||
|
// pick out the link (can be anywhere, as long as there are no spaces).
|
||||||
|
for _, word := range args {
|
||||||
|
word = html.UnescapeString(word)
|
||||||
|
if strings.HasPrefix(word, "http://") || strings.HasPrefix(word, "https://") {
|
||||||
|
link = word
|
||||||
|
} else {
|
||||||
|
title = title + " " + word
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
title = strings.TrimSpace(title)
|
||||||
|
link = strings.TrimSpace(link)
|
||||||
|
|
||||||
|
if len(title) > settings.TitleLength {
|
||||||
|
return "", fmt.Errorf("Title too long (%d/%d)", len(title), settings.TitleLength)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Send a notice to the mods and admins
|
||||||
|
if len(link) == 0 {
|
||||||
|
cl.belongsTo.AddModNotice(cl.name + " set the playing title to '" + title + "' with no link")
|
||||||
|
} else {
|
||||||
|
cl.belongsTo.AddModNotice(cl.name + " set the playing title to '" + title + "' with link '" + link + "'")
|
||||||
|
}
|
||||||
|
|
||||||
|
cl.belongsTo.SetPlaying(title, link)
|
||||||
|
return "", nil
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
common.CNUnmod.String(): Command{
|
||||||
|
HelpText: "Revoke a user's moderator privilages. Moderators can only unmod themselves.",
|
||||||
|
Function: func(cl *Client, args []string) (string, error) {
|
||||||
|
if len(args) > 0 && cl.CmdLevel != common.CmdlAdmin && cl.name != args[0] {
|
||||||
|
return "", fmt.Errorf("You can only unmod yourself, not others.")
|
||||||
|
}
|
||||||
|
|
||||||
|
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.", nil
|
||||||
|
}
|
||||||
|
name := strings.TrimLeft(args[0], "@")
|
||||||
|
|
||||||
|
if err := cl.belongsTo.Unmod(name); err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
cl.belongsTo.AddModNotice(cl.name + " has unmodded " + name)
|
||||||
|
return "", nil
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
common.CNKick.String(): Command{
|
||||||
|
HelpText: "Kick a user from chat.",
|
||||||
|
Function: func(cl *Client, args []string) (string, error) {
|
||||||
|
if len(args) == 0 {
|
||||||
|
return "", fmt.Errorf("Missing name to kick.")
|
||||||
|
}
|
||||||
|
return "", cl.belongsTo.Kick(strings.TrimLeft(args[0], "@"))
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
common.CNBan.String(): Command{
|
||||||
|
HelpText: "Ban a user from chat. They will not be able to re-join chat, but will still be able to view the stream.",
|
||||||
|
Function: func(cl *Client, args []string) (string, error) {
|
||||||
|
if len(args) == 0 {
|
||||||
|
return "", fmt.Errorf("missing name to ban.")
|
||||||
|
}
|
||||||
|
|
||||||
|
name := strings.TrimLeft(args[0], "@")
|
||||||
|
common.LogInfof("[ban] Attempting to ban %s\n", name)
|
||||||
|
return "", cl.belongsTo.Ban(name)
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
common.CNUnban.String(): Command{
|
||||||
|
HelpText: "Remove a ban on a user.",
|
||||||
|
Function: func(cl *Client, args []string) (string, error) {
|
||||||
|
if len(args) == 0 {
|
||||||
|
return "", fmt.Errorf("missing name to unban.")
|
||||||
|
}
|
||||||
|
name := strings.TrimLeft(args[0], "@")
|
||||||
|
common.LogInfof("[ban] Attempting to unban %s\n", name)
|
||||||
|
|
||||||
|
err := settings.RemoveBan(name)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
cl.belongsTo.AddModNotice(cl.name + " has unbanned " + name)
|
||||||
|
return "", nil
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
common.CNPurge.String(): Command{
|
||||||
|
HelpText: "Purge the chat.",
|
||||||
|
Function: func(cl *Client, args []string) (string, error) {
|
||||||
|
common.LogInfoln("[purge] clearing chat")
|
||||||
|
cl.belongsTo.AddCmdMsg(common.CmdPurgeChat, nil)
|
||||||
|
return "", nil
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
admin: map[string]Command{
|
||||||
|
common.CNMod.String(): Command{
|
||||||
|
HelpText: "Grant moderator privilages to a user.",
|
||||||
|
Function: func(cl *Client, args []string) (string, error) {
|
||||||
|
if len(args) == 0 {
|
||||||
|
return "", fmt.Errorf("Missing user to mod.")
|
||||||
|
}
|
||||||
|
|
||||||
|
name := strings.TrimLeft(args[0], "@")
|
||||||
|
if err := cl.belongsTo.Mod(name); err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
cl.belongsTo.AddModNotice(cl.name + " has modded " + name)
|
||||||
|
return "", nil
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
common.CNReloadPlayer.String(): Command{
|
||||||
|
HelpText: "Reload the stream player for everybody in chat.",
|
||||||
|
Function: func(cl *Client, args []string) (string, error) {
|
||||||
|
cl.belongsTo.AddModNotice(cl.name + " has modded forced a player reload")
|
||||||
|
cl.belongsTo.AddCmdMsg(common.CmdRefreshPlayer, nil)
|
||||||
|
return "Reloading player for all chatters.", nil
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
common.CNReloadEmotes.String(): Command{
|
||||||
|
HelpText: "Reload the emotes on the server.",
|
||||||
|
Function: func(cl *Client, args []string) (string, error) {
|
||||||
|
go commandReloadEmotes(cl)
|
||||||
|
return "Reloading emotes...", nil
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
common.CNModpass.String(): Command{
|
||||||
|
HelpText: "Generate a single-use mod password.",
|
||||||
|
Function: func(cl *Client, args []string) (string, error) {
|
||||||
|
cl.belongsTo.AddModNotice(cl.name + " generated a mod password")
|
||||||
|
password := cl.belongsTo.generateModPass()
|
||||||
|
return "Single use password: " + password, nil
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
common.CNRoomAccess.String(): Command{
|
||||||
|
HelpText: "Change the room access type.",
|
||||||
|
Function: func(cl *Client, args []string) (string, error) {
|
||||||
|
// Print current access type if no arguments given
|
||||||
|
if len(args) == 0 {
|
||||||
|
return "Current room access type: " + string(settings.RoomAccess), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
switch AccessMode(strings.ToLower(args[0])) {
|
||||||
|
case AccessOpen:
|
||||||
|
settings.RoomAccess = AccessOpen
|
||||||
|
common.LogInfoln("[access] Room set to open")
|
||||||
|
return "Room access set to open", nil
|
||||||
|
|
||||||
|
case AccessPin:
|
||||||
|
// A pin/password was provided, use it.
|
||||||
|
if len(args) == 2 {
|
||||||
|
// TODO: make this a bit more robust. Currently, only accepts a single word as a pin/password
|
||||||
|
settings.RoomAccessPin = args[1]
|
||||||
|
|
||||||
|
// A pin/password was not provided, generate a new one.
|
||||||
|
} else {
|
||||||
|
_, err := settings.generateNewPin()
|
||||||
|
if err != nil {
|
||||||
|
common.LogErrorln("Error generating new access pin: ", err.Error())
|
||||||
|
return "", fmt.Errorf("Unable to generate a new pin, access unchanged: " + err.Error())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
settings.RoomAccess = AccessPin
|
||||||
|
common.LogInfoln("[access] Room set to pin: " + settings.RoomAccessPin)
|
||||||
|
return "Room access set to Pin: " + settings.RoomAccessPin, nil
|
||||||
|
|
||||||
|
case AccessRequest:
|
||||||
|
settings.RoomAccess = AccessRequest
|
||||||
|
common.LogInfoln("[access] Room set to request")
|
||||||
|
return "Room access set to request. WARNING: this isn't implemented yet.", nil
|
||||||
|
|
||||||
|
default:
|
||||||
|
return "", fmt.Errorf("Invalid access mode")
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
common.CNIP.String(): Command{
|
||||||
|
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, error) {
|
||||||
|
cl.belongsTo.clientsMtx.Lock()
|
||||||
|
common.LogInfoln("Clients:")
|
||||||
|
for id, client := range cl.belongsTo.clients {
|
||||||
|
common.LogInfof(" [%d] %s %s\n", id, client.name, client.conn.Host())
|
||||||
|
}
|
||||||
|
cl.belongsTo.clientsMtx.Unlock()
|
||||||
|
return "see console for output", nil
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
common.CNAddEmotes.String(): Command{
|
||||||
|
HelpText: "Add emotes from a given twitch channel.",
|
||||||
|
Function: func(cl *Client, args []string) (string, error) {
|
||||||
|
// Fire this off in it's own goroutine so the client doesn't
|
||||||
|
// block waiting for the emote download to finish.
|
||||||
|
go func() {
|
||||||
|
|
||||||
|
// Pretty sure this breaks on partial downloads (eg, one good channel and one non-existent)
|
||||||
|
err := getEmotes(args)
|
||||||
|
if err != nil {
|
||||||
|
cl.SendChatData(common.NewChatMessage("", "",
|
||||||
|
err.Error(),
|
||||||
|
common.CmdlUser, common.MsgCommandResponse))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// If the emotes were able to be downloaded, add the channels to settings
|
||||||
|
settings.AddApprovedEmotes(args)
|
||||||
|
|
||||||
|
// reload emotes now that new ones were added
|
||||||
|
err = loadEmotes()
|
||||||
|
if err != nil {
|
||||||
|
cl.SendChatData(common.NewChatMessage("", "",
|
||||||
|
err.Error(),
|
||||||
|
common.CmdlUser, common.MsgCommandResponse))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
cl.belongsTo.AddModNotice(cl.name + " has added emotes from the following channels: " + strings.Join(args, ", "))
|
||||||
|
|
||||||
|
commandReloadEmotes(cl)
|
||||||
|
}()
|
||||||
|
return "Emote download initiated for the following channels: " + strings.Join(args, ", "), nil
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
func (cc *CommandControl) RunCommand(command string, args []string, sender *Client) (string, error) {
|
||||||
|
// get correct command from combined commands
|
||||||
|
cmd := common.GetFullChatCommand(command)
|
||||||
|
|
||||||
|
// Look for user command
|
||||||
|
if userCmd, ok := cc.user[cmd]; ok {
|
||||||
|
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.CmdLevel >= common.CmdlMod {
|
||||||
|
common.LogInfof("[mod] %s /%s %s\n", sender.name, command, strings.Join(args, " "))
|
||||||
|
return modCmd.Function(sender, args)
|
||||||
|
}
|
||||||
|
|
||||||
|
common.LogInfof("[mod REJECTED] %s /%s %s\n", sender.name, command, strings.Join(args, " "))
|
||||||
|
return "", fmt.Errorf("You are not a mod Jebaited")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Look for admin command
|
||||||
|
if adminCmd, ok := cc.admin[cmd]; ok {
|
||||||
|
if sender.CmdLevel == common.CmdlAdmin {
|
||||||
|
common.LogInfof("[admin] %s /%s %s\n", sender.name, command, strings.Join(args, " "))
|
||||||
|
return adminCmd.Function(sender, args)
|
||||||
|
}
|
||||||
|
common.LogInfof("[admin REJECTED] %s /%s %s\n", sender.name, command, strings.Join(args, " "))
|
||||||
|
return "", fmt.Errorf("You are not the admin Jebaited")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Command not found
|
||||||
|
common.LogInfof("[cmd|error] %s /%s %s\n", sender.name, command, strings.Join(args, " "))
|
||||||
|
return "", fmt.Errorf("Invalid command.")
|
||||||
|
}
|
||||||
|
|
||||||
|
func cmdHelp(cl *Client, args []string) (string, error) {
|
||||||
|
url := "/help"
|
||||||
|
|
||||||
|
if cl.CmdLevel >= common.CmdlMod {
|
||||||
|
url += "?mod=1"
|
||||||
|
}
|
||||||
|
|
||||||
|
if cl.CmdLevel == common.CmdlAdmin {
|
||||||
|
url += "&admin=1"
|
||||||
|
}
|
||||||
|
|
||||||
|
cl.SendChatData(common.NewChatCommand(common.CmdHelp, []string{url}))
|
||||||
|
return `Opening help in new window.`, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func getHelp(lvl common.CommandLevel) map[string]string {
|
||||||
|
var cmdList map[string]Command
|
||||||
|
switch lvl {
|
||||||
|
case common.CmdlUser:
|
||||||
|
cmdList = commands.user
|
||||||
|
case common.CmdlMod:
|
||||||
|
cmdList = commands.mod
|
||||||
|
case common.CmdlAdmin:
|
||||||
|
cmdList = commands.admin
|
||||||
|
}
|
||||||
|
|
||||||
|
helptext := map[string]string{}
|
||||||
|
for name, cmd := range cmdList {
|
||||||
|
helptext[name] = cmd.HelpText
|
||||||
|
}
|
||||||
|
return helptext
|
||||||
|
}
|
||||||
|
|
||||||
|
func commandReloadEmotes(cl *Client) {
|
||||||
|
cl.SendServerMessage("Reloading emotes")
|
||||||
|
err := loadEmotes()
|
||||||
|
if err != nil {
|
||||||
|
common.LogErrorf("Unbale to reload emotes: %s\n", err)
|
||||||
|
//return "", err
|
||||||
|
|
||||||
|
cl.SendChatData(common.NewChatMessage("", "",
|
||||||
|
err.Error(),
|
||||||
|
common.CmdlUser, common.MsgCommandResponse))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
cl.belongsTo.AddChatMsg(common.NewChatHiddenMessage(common.CdEmote, common.Emotes))
|
||||||
|
cl.belongsTo.AddModNotice(cl.name + " has reloaded emotes")
|
||||||
|
|
||||||
|
num := len(common.Emotes)
|
||||||
|
common.LogInfof("Loaded %d emotes\n", num)
|
||||||
|
cl.belongsTo.AddModNotice(fmt.Sprintf("%s reloaded %d emotes.", cl.name, num))
|
||||||
|
}
|
||||||
475
MovieNight/chatroom.go
Executable file
475
MovieNight/chatroom.go
Executable file
@@ -0,0 +1,475 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
"strings"
|
||||||
|
"sync"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/zorchenhimer/MovieNight/common"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
ColorServerMessage string = "#ea6260"
|
||||||
|
)
|
||||||
|
|
||||||
|
type ChatRoom struct {
|
||||||
|
clients []*Client // this needs to be a pointer. key is suid.
|
||||||
|
clientsMtx sync.Mutex
|
||||||
|
|
||||||
|
queue chan common.ChatData
|
||||||
|
modqueue chan common.ChatData // mod and admin broadcast messages
|
||||||
|
|
||||||
|
playing string
|
||||||
|
playingLink string
|
||||||
|
|
||||||
|
modPasswords []string // single-use mod passwords
|
||||||
|
modPasswordsMtx sync.Mutex
|
||||||
|
}
|
||||||
|
|
||||||
|
//initializing the chatroom
|
||||||
|
func newChatRoom() (*ChatRoom, error) {
|
||||||
|
cr := &ChatRoom{
|
||||||
|
queue: make(chan common.ChatData, 1000),
|
||||||
|
modqueue: make(chan common.ChatData, 1000),
|
||||||
|
clients: []*Client{},
|
||||||
|
}
|
||||||
|
|
||||||
|
err := loadEmotes()
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("error loading emotes: %s", err)
|
||||||
|
}
|
||||||
|
common.LogInfof("Loaded %d emotes\n", len(common.Emotes))
|
||||||
|
|
||||||
|
//the "heartbeat" for broadcasting messages
|
||||||
|
go cr.Broadcast()
|
||||||
|
return cr, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// A new client joined
|
||||||
|
func (cr *ChatRoom) Join(conn *chatConnection, data common.JoinData) (*Client, error) {
|
||||||
|
defer cr.clientsMtx.Unlock()
|
||||||
|
cr.clientsMtx.Lock()
|
||||||
|
|
||||||
|
sendHiddenMessage := func(cd common.ClientDataType, i interface{}) {
|
||||||
|
// If the message cant be converted, then just don't send
|
||||||
|
if d, err := common.NewChatHiddenMessage(cd, i).ToJSON(); err == nil {
|
||||||
|
conn.WriteJSON(d)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if settings.RoomAccess == AccessPin && data.Name == settings.RoomAccessPin {
|
||||||
|
sendHiddenMessage(common.CdNotify, "That's the access pin! Please enter a name.")
|
||||||
|
return nil, UserFormatError{Name: data.Name}
|
||||||
|
}
|
||||||
|
|
||||||
|
if !common.IsValidName(data.Name) {
|
||||||
|
sendHiddenMessage(common.CdNotify, common.InvalidNameError)
|
||||||
|
return nil, UserFormatError{Name: data.Name}
|
||||||
|
}
|
||||||
|
|
||||||
|
nameLower := strings.ToLower(data.Name)
|
||||||
|
for _, client := range cr.clients {
|
||||||
|
if strings.ToLower(client.name) == nameLower {
|
||||||
|
sendHiddenMessage(common.CdNotify, "Name already taken")
|
||||||
|
return nil, UserTakenError{Name: data.Name}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// If color is invalid, then set it to a random color
|
||||||
|
if !common.IsValidColor(data.Color) {
|
||||||
|
data.Color = common.RandomColor()
|
||||||
|
}
|
||||||
|
|
||||||
|
client, err := NewClient(conn, cr, data.Name, data.Color)
|
||||||
|
if err != nil {
|
||||||
|
sendHiddenMessage(common.CdNotify, "Could not join client")
|
||||||
|
return nil, fmt.Errorf("Unable to join client: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Overwrite to use client instead
|
||||||
|
sendHiddenMessage = func(cd common.ClientDataType, i interface{}) {
|
||||||
|
client.SendChatData(common.NewChatHiddenMessage(cd, i))
|
||||||
|
}
|
||||||
|
|
||||||
|
host := client.Host()
|
||||||
|
|
||||||
|
if banned, names := settings.IsBanned(host); banned {
|
||||||
|
sendHiddenMessage(common.CdNotify, "You are banned")
|
||||||
|
return nil, newBannedUserError(host, data.Name, names)
|
||||||
|
}
|
||||||
|
|
||||||
|
cr.clients = append(cr.clients, client)
|
||||||
|
|
||||||
|
common.LogChatf("[join] %s %s\n", host, data.Color)
|
||||||
|
playingCommand, err := common.NewChatCommand(common.CmdPlaying, []string{cr.playing, cr.playingLink}).ToJSON()
|
||||||
|
if err != nil {
|
||||||
|
common.LogErrorf("Unable to encode playing command on join: %s\n", err)
|
||||||
|
} else {
|
||||||
|
client.Send(playingCommand)
|
||||||
|
}
|
||||||
|
cr.AddEventMsg(common.EvJoin, data.Name, data.Color)
|
||||||
|
|
||||||
|
sendHiddenMessage(common.CdJoin, nil)
|
||||||
|
sendHiddenMessage(common.CdEmote, common.Emotes)
|
||||||
|
|
||||||
|
stats.updateMaxUsers(len(cr.clients))
|
||||||
|
|
||||||
|
return client, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: fix this up a bit. kick and leave are the same, incorrect, error: "That
|
||||||
|
// name was already used!" leaving the chatroom
|
||||||
|
func (cr *ChatRoom) Leave(name, color string) {
|
||||||
|
defer cr.clientsMtx.Unlock()
|
||||||
|
cr.clientsMtx.Lock() //preventing simultaneous access to the `clients` map
|
||||||
|
|
||||||
|
client, id, err := cr.getClient(name)
|
||||||
|
if err != nil {
|
||||||
|
common.LogErrorf("[leave] Unable to get client suid %v\n", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
host := client.Host()
|
||||||
|
name = client.name // grab the name from here for proper capitalization
|
||||||
|
client.conn.Close()
|
||||||
|
cr.delClient(id)
|
||||||
|
|
||||||
|
cr.AddEventMsg(common.EvLeave, name, color)
|
||||||
|
common.LogChatf("[leave] %s %s\n", host, name)
|
||||||
|
}
|
||||||
|
|
||||||
|
// kicked from the chatroom
|
||||||
|
func (cr *ChatRoom) Kick(name string) error {
|
||||||
|
defer cr.clientsMtx.Unlock()
|
||||||
|
cr.clientsMtx.Lock() //preventing simultaneous access to the `clients` map
|
||||||
|
|
||||||
|
client, id, err := cr.getClient(name)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("Unable to get client for name " + name)
|
||||||
|
}
|
||||||
|
|
||||||
|
if client.CmdLevel == common.CmdlMod {
|
||||||
|
return fmt.Errorf("You cannot kick another mod.")
|
||||||
|
}
|
||||||
|
|
||||||
|
if client.CmdLevel == common.CmdlAdmin {
|
||||||
|
return fmt.Errorf("Jebaited No.")
|
||||||
|
}
|
||||||
|
|
||||||
|
color := client.color
|
||||||
|
host := client.Host()
|
||||||
|
client.conn.Close()
|
||||||
|
cr.delClient(id)
|
||||||
|
|
||||||
|
cr.AddEventMsg(common.EvKick, name, color)
|
||||||
|
common.LogInfof("[kick] %s %s has been kicked\n", host, name)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (cr *ChatRoom) Ban(name string) error {
|
||||||
|
defer cr.clientsMtx.Unlock()
|
||||||
|
cr.clientsMtx.Lock()
|
||||||
|
|
||||||
|
client, id, err := cr.getClient(name)
|
||||||
|
if err != nil {
|
||||||
|
common.LogErrorf("[ban] Unable to get client for name %q\n", name)
|
||||||
|
return fmt.Errorf("Cannot find that name")
|
||||||
|
}
|
||||||
|
|
||||||
|
if client.CmdLevel == common.CmdlAdmin {
|
||||||
|
return fmt.Errorf("You cannot ban an admin Jebaited")
|
||||||
|
}
|
||||||
|
|
||||||
|
names := []string{}
|
||||||
|
host := client.Host()
|
||||||
|
color := client.color
|
||||||
|
|
||||||
|
// Remove the named client
|
||||||
|
client.conn.Close()
|
||||||
|
cr.delClient(id)
|
||||||
|
|
||||||
|
// Remove additional clients on that IP address
|
||||||
|
for id, c := range cr.clients {
|
||||||
|
if c.Host() == host {
|
||||||
|
names = append(names, client.name)
|
||||||
|
client.conn.Close()
|
||||||
|
cr.delClient(id)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
err = settings.AddBan(host, names)
|
||||||
|
if err != nil {
|
||||||
|
common.LogErrorf("[BAN] Error banning %q: %s\n", name, err)
|
||||||
|
cr.AddEventMsg(common.EvKick, name, color)
|
||||||
|
} else {
|
||||||
|
cr.AddEventMsg(common.EvBan, name, color)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add a chat message from a viewer
|
||||||
|
func (cr *ChatRoom) AddMsg(from *Client, isAction, isServer bool, msg string) {
|
||||||
|
t := common.MsgChat
|
||||||
|
|
||||||
|
if isAction {
|
||||||
|
t = common.MsgAction
|
||||||
|
}
|
||||||
|
|
||||||
|
if isServer {
|
||||||
|
t = common.MsgServer
|
||||||
|
}
|
||||||
|
|
||||||
|
cr.AddChatMsg(common.NewChatMessage(from.name, from.color, msg, from.CmdLevel, t))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add a chat message object to the queue
|
||||||
|
func (cr *ChatRoom) AddChatMsg(data common.ChatData) {
|
||||||
|
select {
|
||||||
|
case cr.queue <- data:
|
||||||
|
default:
|
||||||
|
common.LogErrorln("Unable to queue chat message. Channel full.")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (cr *ChatRoom) AddCmdMsg(command common.CommandType, args []string) {
|
||||||
|
select {
|
||||||
|
case cr.queue <- common.NewChatCommand(command, args):
|
||||||
|
default:
|
||||||
|
common.LogErrorln("Unable to queue command message. Channel full.")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (cr *ChatRoom) AddModNotice(message string) {
|
||||||
|
select {
|
||||||
|
case cr.modqueue <- common.NewChatMessage("", "", message, common.CmdlUser, common.MsgNotice):
|
||||||
|
default:
|
||||||
|
common.LogErrorln("Unable to queue notice. Channel full.")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (cr *ChatRoom) AddEventMsg(event common.EventType, name, color string) {
|
||||||
|
select {
|
||||||
|
case cr.queue <- common.NewChatEvent(event, name, color):
|
||||||
|
default:
|
||||||
|
common.LogErrorln("Unable to queue event message. Channel full.")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (cr *ChatRoom) Unmod(name string) error {
|
||||||
|
defer cr.clientsMtx.Unlock()
|
||||||
|
cr.clientsMtx.Lock()
|
||||||
|
|
||||||
|
client, _, err := cr.getClient(name)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
client.Unmod()
|
||||||
|
client.SendServerMessage(`You have been unmodded.`)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (cr *ChatRoom) Mod(name string) error {
|
||||||
|
defer cr.clientsMtx.Unlock()
|
||||||
|
cr.clientsMtx.Lock()
|
||||||
|
|
||||||
|
client, _, err := cr.getClient(name)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if client.CmdLevel < common.CmdlMod {
|
||||||
|
client.CmdLevel = common.CmdlMod
|
||||||
|
client.SendServerMessage(`You have been modded.`)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (cr *ChatRoom) ForceColorChange(name, color string) error {
|
||||||
|
defer cr.clientsMtx.Unlock()
|
||||||
|
cr.clientsMtx.Lock()
|
||||||
|
|
||||||
|
client, _, err := cr.getClient(name)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
client.IsColorForced = true
|
||||||
|
client.color = color
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (cr *ChatRoom) UserCount() int {
|
||||||
|
return len(cr.clients)
|
||||||
|
}
|
||||||
|
|
||||||
|
//broadcasting all the messages in the queue in one block
|
||||||
|
func (cr *ChatRoom) Broadcast() {
|
||||||
|
send := func(data common.ChatData, client *Client) {
|
||||||
|
err := client.SendChatData(data)
|
||||||
|
if err != nil {
|
||||||
|
common.LogErrorf("Error sending data to client: %v\n", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for {
|
||||||
|
select {
|
||||||
|
case msg := <-cr.queue:
|
||||||
|
cr.clientsMtx.Lock()
|
||||||
|
for _, client := range cr.clients {
|
||||||
|
go send(msg, client)
|
||||||
|
}
|
||||||
|
cr.clientsMtx.Unlock()
|
||||||
|
case msg := <-cr.modqueue:
|
||||||
|
cr.clientsMtx.Lock()
|
||||||
|
for _, client := range cr.clients {
|
||||||
|
if client.CmdLevel >= common.CmdlMod {
|
||||||
|
send(msg, client)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
cr.clientsMtx.Unlock()
|
||||||
|
default:
|
||||||
|
time.Sleep(50 * time.Millisecond)
|
||||||
|
// No messages to send
|
||||||
|
// This default block is required so the above case
|
||||||
|
// does not block.
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (cr *ChatRoom) ClearPlaying() {
|
||||||
|
cr.playing = ""
|
||||||
|
cr.playingLink = ""
|
||||||
|
cr.AddCmdMsg(common.CmdPlaying, []string{"", ""})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (cr *ChatRoom) SetPlaying(title, link string) {
|
||||||
|
cr.playing = title
|
||||||
|
cr.playingLink = link
|
||||||
|
cr.AddCmdMsg(common.CmdPlaying, []string{title, link})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (cr *ChatRoom) GetNames() []string {
|
||||||
|
names := []string{}
|
||||||
|
defer cr.clientsMtx.Unlock()
|
||||||
|
cr.clientsMtx.Lock()
|
||||||
|
|
||||||
|
for _, val := range cr.clients {
|
||||||
|
names = append(names, val.name)
|
||||||
|
}
|
||||||
|
|
||||||
|
return names
|
||||||
|
}
|
||||||
|
|
||||||
|
func (cr *ChatRoom) delClient(sliceId int) {
|
||||||
|
cr.clients = append(cr.clients[:sliceId], cr.clients[sliceId+1:]...)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (cr *ChatRoom) getClient(name string) (*Client, int, error) {
|
||||||
|
for id, client := range cr.clients {
|
||||||
|
if client.name == name {
|
||||||
|
return client, id, nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil, -1, fmt.Errorf("client with that name not found")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (cr *ChatRoom) generateModPass() string {
|
||||||
|
defer cr.modPasswordsMtx.Unlock()
|
||||||
|
cr.modPasswordsMtx.Lock()
|
||||||
|
|
||||||
|
pass, err := generatePass(time.Now().Unix())
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Sprintf("Error generating moderator password: %s", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Make sure the password is unique
|
||||||
|
for existsInSlice(cr.modPasswords, pass) {
|
||||||
|
pass, err = generatePass(time.Now().Unix())
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Sprintf("Error generating moderator password: %s", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
cr.modPasswords = append(cr.modPasswords, pass)
|
||||||
|
return pass
|
||||||
|
}
|
||||||
|
|
||||||
|
func (cr *ChatRoom) redeemModPass(pass string) bool {
|
||||||
|
if pass == "" {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
defer cr.modPasswordsMtx.Unlock()
|
||||||
|
cr.modPasswordsMtx.Lock()
|
||||||
|
|
||||||
|
if existsInSlice(cr.modPasswords, pass) {
|
||||||
|
cr.modPasswords = removeFromSlice(cr.modPasswords, pass)
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
func removeFromSlice(slice []string, needle string) []string {
|
||||||
|
slc := []string{}
|
||||||
|
for _, item := range slice {
|
||||||
|
if item != needle {
|
||||||
|
slc = append(slc, item)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return slc
|
||||||
|
}
|
||||||
|
|
||||||
|
func existsInSlice(slice []string, needle string) bool {
|
||||||
|
for _, item := range slice {
|
||||||
|
if item == needle {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
func (cr *ChatRoom) changeName(oldName, newName string, forced bool) error {
|
||||||
|
cr.clientsMtx.Lock()
|
||||||
|
defer cr.clientsMtx.Unlock()
|
||||||
|
|
||||||
|
if !common.IsValidName(newName) {
|
||||||
|
return fmt.Errorf("%q nick is not a valid name", newName)
|
||||||
|
}
|
||||||
|
|
||||||
|
newLower := strings.ToLower(newName)
|
||||||
|
oldLower := strings.ToLower(oldName)
|
||||||
|
|
||||||
|
var currentClient *Client
|
||||||
|
for _, client := range cr.clients {
|
||||||
|
if strings.ToLower(client.name) == newLower {
|
||||||
|
if strings.ToLower(client.name) != oldLower {
|
||||||
|
return fmt.Errorf("%q is already taken.", newName)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if strings.ToLower(client.name) == oldLower {
|
||||||
|
currentClient = client
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if currentClient != nil {
|
||||||
|
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)
|
||||||
|
currentClient.IsNameForced = true
|
||||||
|
} else {
|
||||||
|
cr.AddEventMsg(common.EvNameChange, oldName+":"+newName, currentClient.color)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return fmt.Errorf("Client not found with name %q", oldName)
|
||||||
|
}
|
||||||
87
MovieNight/common/chatcommands.go
Executable file
87
MovieNight/common/chatcommands.go
Executable file
@@ -0,0 +1,87 @@
|
|||||||
|
package common
|
||||||
|
|
||||||
|
import "strings"
|
||||||
|
|
||||||
|
const CommandNameSeparator = ","
|
||||||
|
|
||||||
|
type ChatCommandNames []string
|
||||||
|
|
||||||
|
func (c ChatCommandNames) String() string {
|
||||||
|
return strings.Join(c, CommandNameSeparator)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Names for commands
|
||||||
|
var (
|
||||||
|
// User Commands
|
||||||
|
CNMe ChatCommandNames = []string{"me"}
|
||||||
|
CNHelp ChatCommandNames = []string{"help"}
|
||||||
|
CNCount ChatCommandNames = []string{"count"}
|
||||||
|
CNColor ChatCommandNames = []string{"color", "colour"}
|
||||||
|
CNWhoAmI ChatCommandNames = []string{"w", "whoami"}
|
||||||
|
CNAuth ChatCommandNames = []string{"auth"}
|
||||||
|
CNUsers ChatCommandNames = []string{"users"}
|
||||||
|
CNNick ChatCommandNames = []string{"nick", "name"}
|
||||||
|
CNStats ChatCommandNames = []string{"stats"}
|
||||||
|
CNPin ChatCommandNames = []string{"pin", "password"}
|
||||||
|
CNEmotes ChatCommandNames = []string{"emotes"}
|
||||||
|
// Mod Commands
|
||||||
|
CNSv ChatCommandNames = []string{"sv"}
|
||||||
|
CNPlaying ChatCommandNames = []string{"playing"}
|
||||||
|
CNUnmod ChatCommandNames = []string{"unmod"}
|
||||||
|
CNKick ChatCommandNames = []string{"kick"}
|
||||||
|
CNBan ChatCommandNames = []string{"ban"}
|
||||||
|
CNUnban ChatCommandNames = []string{"unban"}
|
||||||
|
CNPurge ChatCommandNames = []string{"purge"}
|
||||||
|
// Admin Commands
|
||||||
|
CNMod ChatCommandNames = []string{"mod"}
|
||||||
|
CNReloadPlayer ChatCommandNames = []string{"reloadplayer"}
|
||||||
|
CNReloadEmotes ChatCommandNames = []string{"reloademotes"}
|
||||||
|
CNModpass ChatCommandNames = []string{"modpass"}
|
||||||
|
CNIP ChatCommandNames = []string{"iplist"}
|
||||||
|
CNAddEmotes ChatCommandNames = []string{"addemotes"}
|
||||||
|
CNRoomAccess ChatCommandNames = []string{"changeaccess", "hodor"}
|
||||||
|
)
|
||||||
|
|
||||||
|
var ChatCommands = []ChatCommandNames{
|
||||||
|
// User
|
||||||
|
CNMe,
|
||||||
|
CNHelp,
|
||||||
|
CNCount,
|
||||||
|
CNColor,
|
||||||
|
CNWhoAmI,
|
||||||
|
CNAuth,
|
||||||
|
CNUsers,
|
||||||
|
CNNick,
|
||||||
|
CNStats,
|
||||||
|
CNPin,
|
||||||
|
CNEmotes,
|
||||||
|
|
||||||
|
// Mod
|
||||||
|
CNSv,
|
||||||
|
CNPlaying,
|
||||||
|
CNUnmod,
|
||||||
|
CNKick,
|
||||||
|
CNBan,
|
||||||
|
CNUnban,
|
||||||
|
CNPurge,
|
||||||
|
|
||||||
|
// Admin
|
||||||
|
CNMod,
|
||||||
|
CNReloadPlayer,
|
||||||
|
CNReloadEmotes,
|
||||||
|
CNModpass,
|
||||||
|
CNIP,
|
||||||
|
CNAddEmotes,
|
||||||
|
CNRoomAccess,
|
||||||
|
}
|
||||||
|
|
||||||
|
func GetFullChatCommand(c string) string {
|
||||||
|
for _, names := range ChatCommands {
|
||||||
|
for _, n := range names {
|
||||||
|
if c == n {
|
||||||
|
return names.String()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return ""
|
||||||
|
}
|
||||||
249
MovieNight/common/chatdata.go
Executable file
249
MovieNight/common/chatdata.go
Executable file
@@ -0,0 +1,249 @@
|
|||||||
|
package common
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
type DataInterface interface {
|
||||||
|
HTML() string
|
||||||
|
}
|
||||||
|
|
||||||
|
type ChatData struct {
|
||||||
|
Type DataType
|
||||||
|
Data DataInterface
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c ChatData) ToJSON() (ChatDataJSON, error) {
|
||||||
|
rawData, err := json.Marshal(c.Data)
|
||||||
|
return ChatDataJSON{
|
||||||
|
Type: c.Type,
|
||||||
|
Data: rawData,
|
||||||
|
}, err
|
||||||
|
}
|
||||||
|
|
||||||
|
type ChatDataJSON struct {
|
||||||
|
Type DataType
|
||||||
|
Data json.RawMessage
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c ChatDataJSON) ToData() (ChatData, error) {
|
||||||
|
data, err := c.GetData()
|
||||||
|
return ChatData{
|
||||||
|
Type: c.Type,
|
||||||
|
Data: data,
|
||||||
|
}, err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c ChatDataJSON) GetData() (DataInterface, error) {
|
||||||
|
var data DataInterface
|
||||||
|
var err error
|
||||||
|
|
||||||
|
switch c.Type {
|
||||||
|
case DTInvalid:
|
||||||
|
return nil, errors.New("data type is invalid")
|
||||||
|
case DTChat:
|
||||||
|
d := DataMessage{}
|
||||||
|
err = json.Unmarshal(c.Data, &d)
|
||||||
|
data = d
|
||||||
|
case DTCommand:
|
||||||
|
d := DataCommand{}
|
||||||
|
err = json.Unmarshal(c.Data, &d)
|
||||||
|
data = d
|
||||||
|
case DTEvent:
|
||||||
|
d := DataEvent{}
|
||||||
|
err = json.Unmarshal(c.Data, &d)
|
||||||
|
data = d
|
||||||
|
case DTClient:
|
||||||
|
d := ClientData{}
|
||||||
|
err = json.Unmarshal(c.Data, &d)
|
||||||
|
data = d
|
||||||
|
case DTHidden:
|
||||||
|
d := HiddenMessage{}
|
||||||
|
err = json.Unmarshal(c.Data, &d)
|
||||||
|
data = d
|
||||||
|
default:
|
||||||
|
err = fmt.Errorf("unhandled data type: %d", c.Type)
|
||||||
|
}
|
||||||
|
|
||||||
|
return data, err
|
||||||
|
}
|
||||||
|
|
||||||
|
type ClientData struct {
|
||||||
|
Type ClientDataType
|
||||||
|
Message string
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c ClientData) HTML() string {
|
||||||
|
// Client data is for client to server communication only, so clients should not see this
|
||||||
|
return `<span style="color: red;">The developer messed up. You should not be seeing this.</span>`
|
||||||
|
}
|
||||||
|
|
||||||
|
type DataMessage struct {
|
||||||
|
From string
|
||||||
|
Color string
|
||||||
|
Message string
|
||||||
|
Level CommandLevel
|
||||||
|
Type MessageType
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: Read this HTML from a template somewhere
|
||||||
|
func (dc DataMessage) HTML() string {
|
||||||
|
switch dc.Type {
|
||||||
|
case MsgAction:
|
||||||
|
return `<span style="color:` + dc.Color + `"><span class="name">` + dc.From +
|
||||||
|
`</span> <span class="cmdme">` + dc.Message + `</span></span>`
|
||||||
|
|
||||||
|
case MsgServer:
|
||||||
|
return `<span class="announcement">` + dc.Message + `</span>`
|
||||||
|
|
||||||
|
case MsgError:
|
||||||
|
return `<span class="error">` + dc.Message + `</span>`
|
||||||
|
|
||||||
|
case MsgNotice:
|
||||||
|
return `<span class="notice">` + dc.Message + `</span>`
|
||||||
|
|
||||||
|
case MsgCommandResponse:
|
||||||
|
return `<span class="command">` + dc.Message + `</span>`
|
||||||
|
|
||||||
|
case MsgCommandError:
|
||||||
|
return `<span class="commanderror">` + dc.Message + `</span>`
|
||||||
|
|
||||||
|
default:
|
||||||
|
badge := ""
|
||||||
|
switch dc.Level {
|
||||||
|
case CmdlMod:
|
||||||
|
badge = `<img src="/static/img/mod.png" class="badge" />`
|
||||||
|
case CmdlAdmin:
|
||||||
|
badge = `<img src="/static/img/admin.png" class="badge" />`
|
||||||
|
}
|
||||||
|
return `<span>` + badge + `<span class="name" style="color:` + dc.Color + `">` + dc.From +
|
||||||
|
`</span><b>:</b> <span class="msg">` + dc.Message + `</span></span>`
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewChatMessage(name, color, msg string, lvl CommandLevel, msgtype MessageType) ChatData {
|
||||||
|
return ChatData{
|
||||||
|
Type: DTChat,
|
||||||
|
Data: DataMessage{
|
||||||
|
From: name,
|
||||||
|
Color: color,
|
||||||
|
Message: msg,
|
||||||
|
Type: msgtype,
|
||||||
|
Level: lvl,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
type DataCommand struct {
|
||||||
|
Command CommandType
|
||||||
|
Arguments []string
|
||||||
|
}
|
||||||
|
|
||||||
|
func (de DataCommand) HTML() string {
|
||||||
|
switch de.Command {
|
||||||
|
case CmdPurgeChat:
|
||||||
|
return `<span class="notice">Chat has been purged by a moderator.</span>`
|
||||||
|
default:
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewChatCommand(command CommandType, args []string) ChatData {
|
||||||
|
return ChatData{
|
||||||
|
Type: DTCommand,
|
||||||
|
Data: DataCommand{
|
||||||
|
Command: command,
|
||||||
|
Arguments: args,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
type DataEvent struct {
|
||||||
|
Event EventType
|
||||||
|
User string
|
||||||
|
Color string
|
||||||
|
}
|
||||||
|
|
||||||
|
func (de DataEvent) HTML() string {
|
||||||
|
switch de.Event {
|
||||||
|
case EvKick:
|
||||||
|
return `<span class="event"><span class="name" style="color:` + de.Color + `">` +
|
||||||
|
de.User + `</span> has been kicked.</span>`
|
||||||
|
case EvLeave:
|
||||||
|
return `<span class="event"><span class="name" style="color:` + de.Color + `">` +
|
||||||
|
de.User + `</span> has left the chat.</span>`
|
||||||
|
case EvBan:
|
||||||
|
return `<span class="event"><span class="name" style="color:` + de.Color + `">` +
|
||||||
|
de.User + `</span> has been banned.</span>`
|
||||||
|
case EvJoin:
|
||||||
|
return `<span class="event"><span class="name" style="color:` + de.Color + `">` +
|
||||||
|
de.User + `</span> has joined the chat.</span>`
|
||||||
|
case EvNameChange:
|
||||||
|
names := strings.Split(de.User, ":")
|
||||||
|
if len(names) != 2 {
|
||||||
|
return `<span class="event">Somebody changed their name, but IDK who ` +
|
||||||
|
ParseEmotes("Jebaited") + `.</span>`
|
||||||
|
}
|
||||||
|
|
||||||
|
return `<span class="event"><span class="name" style="color:` + de.Color + `">` +
|
||||||
|
names[0] + `</span> has changed their name to <span class="name" style="color:` +
|
||||||
|
de.Color + `">` + names[1] + `</span>.</span>`
|
||||||
|
case EvNameChangeForced:
|
||||||
|
names := strings.Split(de.User, ":")
|
||||||
|
if len(names) != 2 {
|
||||||
|
return `<span class="event">An admin changed somebody's name, but IDK who ` +
|
||||||
|
ParseEmotes("Jebaited") + `.</span>`
|
||||||
|
}
|
||||||
|
|
||||||
|
return `<span class="event"><span class="name" style="color:` + de.Color + `">` +
|
||||||
|
names[0] + `</span> has had their name changed to <span class="name" style="color:` +
|
||||||
|
de.Color + `">` + names[1] + `</span> by an admin.</span>`
|
||||||
|
}
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewChatEvent(event EventType, name, color string) ChatData {
|
||||||
|
return ChatData{
|
||||||
|
Type: DTEvent,
|
||||||
|
Data: DataEvent{
|
||||||
|
Event: event,
|
||||||
|
User: name,
|
||||||
|
Color: color,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// DataHidden is for the server to send instructions and data
|
||||||
|
// to the client without the purpose of outputting it on the chat
|
||||||
|
type HiddenMessage struct {
|
||||||
|
Type ClientDataType
|
||||||
|
Data interface{}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h HiddenMessage) HTML() string {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewChatHiddenMessage(clientType ClientDataType, data interface{}) ChatData {
|
||||||
|
return ChatData{
|
||||||
|
Type: DTHidden,
|
||||||
|
Data: HiddenMessage{
|
||||||
|
Type: clientType,
|
||||||
|
Data: data,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func DecodeData(rawjson string) (ChatDataJSON, error) {
|
||||||
|
var data ChatDataJSON
|
||||||
|
err := json.Unmarshal([]byte(rawjson), &data)
|
||||||
|
return data, err
|
||||||
|
}
|
||||||
|
|
||||||
|
type JoinData struct {
|
||||||
|
Name string
|
||||||
|
Color string
|
||||||
|
}
|
||||||
135
MovieNight/common/colors.go
Executable file
135
MovieNight/common/colors.go
Executable file
@@ -0,0 +1,135 @@
|
|||||||
|
package common
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"math/rand"
|
||||||
|
"regexp"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
rand.Seed(int64(time.Now().Nanosecond()))
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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{
|
||||||
|
"aliceblue", "antiquewhite", "aqua", "aquamarine", "azure",
|
||||||
|
"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", "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 (
|
||||||
|
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 #RGB and #RRGGBB
|
||||||
|
func IsValidColor(s string) bool {
|
||||||
|
s = strings.TrimLeft(strings.ToLower(s), "#")
|
||||||
|
for _, c := range Colors {
|
||||||
|
if s == c {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if regexColor.MatchString(s) {
|
||||||
|
r, g, b, err := hex(s)
|
||||||
|
if err != nil {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
total := float32(r + g + b)
|
||||||
|
return total > 0.7 && float32(b)/total < 0.7
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
// RandomColor returns a hex color code
|
||||||
|
func RandomColor() string {
|
||||||
|
var color string
|
||||||
|
for !IsValidColor(color) {
|
||||||
|
color = ""
|
||||||
|
for i := 0; i < 3; i++ {
|
||||||
|
s := strconv.FormatInt(rand.Int63n(255), 16)
|
||||||
|
if len(s) == 1 {
|
||||||
|
s = "0" + s
|
||||||
|
}
|
||||||
|
color += s
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return "#" + color
|
||||||
|
}
|
||||||
|
|
||||||
|
// hex returns R, G, B as values
|
||||||
|
func hex(s string) (int, int, int, error) {
|
||||||
|
// Make the string just the base16 numbers
|
||||||
|
s = strings.TrimLeft(s, "#")
|
||||||
|
|
||||||
|
if len(s) == 3 {
|
||||||
|
var err error
|
||||||
|
s, err = hexThreeToSix(s)
|
||||||
|
if err != nil {
|
||||||
|
return 0, 0, 0, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(s) == 6 {
|
||||||
|
R64, err := strconv.ParseInt(s[0:2], 16, 32)
|
||||||
|
if err != nil {
|
||||||
|
return 0, 0, 0, err
|
||||||
|
}
|
||||||
|
|
||||||
|
G64, err := strconv.ParseInt(s[2:4], 16, 32)
|
||||||
|
if err != nil {
|
||||||
|
return 0, 0, 0, err
|
||||||
|
}
|
||||||
|
|
||||||
|
B64, err := strconv.ParseInt(s[4:6], 16, 32)
|
||||||
|
if err != nil {
|
||||||
|
return 0, 0, 0, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return int(R64), int(G64), int(B64), nil
|
||||||
|
}
|
||||||
|
return 0, 0, 0, errors.New("incorrect format")
|
||||||
|
}
|
||||||
|
|
||||||
|
func hexThreeToSix(s string) (string, error) {
|
||||||
|
if len(s) != 3 {
|
||||||
|
return "", fmt.Errorf("%d is the incorrect length of string for convertsion", len(s))
|
||||||
|
}
|
||||||
|
|
||||||
|
h := ""
|
||||||
|
for i := 0; i < 3; i++ {
|
||||||
|
h += string(s[i])
|
||||||
|
h += string(s[i])
|
||||||
|
}
|
||||||
|
return h, nil
|
||||||
|
}
|
||||||
42
MovieNight/common/colors_test.go
Executable file
42
MovieNight/common/colors_test.go
Executable file
@@ -0,0 +1,42 @@
|
|||||||
|
package common
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestColorHexThreeToSix(t *testing.T) {
|
||||||
|
expected := "RRGGBB"
|
||||||
|
result, _ := hexThreeToSix("RGB")
|
||||||
|
if result != expected {
|
||||||
|
t.Errorf("expected %#v, got %#v", expected, result)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestHex(t *testing.T) {
|
||||||
|
// The testing data layout is inputer, Expected Red, Exp Green, Exp Blue, expect error
|
||||||
|
data := [][]interface{}{
|
||||||
|
[]interface{}{"010203", 1, 2, 3, false},
|
||||||
|
[]interface{}{"100", 17, 0, 0, false},
|
||||||
|
[]interface{}{"100", 1, 0, 0, true},
|
||||||
|
[]interface{}{"1000", 0, 0, 0, true},
|
||||||
|
[]interface{}{"010203", 1, 2, 4, true},
|
||||||
|
[]interface{}{"0102GG", 1, 2, 4, true},
|
||||||
|
}
|
||||||
|
|
||||||
|
for i := range data {
|
||||||
|
input := data[i][0].(string)
|
||||||
|
r, g, b, err := hex(input)
|
||||||
|
if err != nil {
|
||||||
|
if !data[i][4].(bool) {
|
||||||
|
t.Errorf("with input %#v: %v", input, err)
|
||||||
|
}
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
rr, rg, rb := data[i][1].(int), data[i][2].(int), data[i][3].(int)
|
||||||
|
|
||||||
|
if !data[i][4].(bool) && (r != rr || g != rg || b != rb) {
|
||||||
|
t.Errorf("expected %d, %d, %d - got %d, %d, %d", r, g, b, rr, rg, rb)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
73
MovieNight/common/constants.go
Executable file
73
MovieNight/common/constants.go
Executable file
@@ -0,0 +1,73 @@
|
|||||||
|
package common
|
||||||
|
|
||||||
|
type ClientDataType int
|
||||||
|
|
||||||
|
// Data types for communicating with the client
|
||||||
|
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
|
||||||
|
CdAuth // get the auth levels of the user
|
||||||
|
CdColor // get the users color
|
||||||
|
CdEmote // get a list of emotes
|
||||||
|
CdJoin // a message saying the client wants to join
|
||||||
|
CdNotify // a notify message for the client to show
|
||||||
|
)
|
||||||
|
|
||||||
|
type DataType int
|
||||||
|
|
||||||
|
// Data types for command messages
|
||||||
|
const (
|
||||||
|
DTInvalid DataType = iota
|
||||||
|
DTChat // chat message
|
||||||
|
DTCommand // non-chat function
|
||||||
|
DTEvent // join/leave/kick/ban events
|
||||||
|
DTClient // a message coming from the client
|
||||||
|
DTHidden // a message that is purely instruction and data, not shown to user
|
||||||
|
)
|
||||||
|
|
||||||
|
type CommandType int
|
||||||
|
|
||||||
|
// Command Types
|
||||||
|
const (
|
||||||
|
CmdPlaying CommandType = iota
|
||||||
|
CmdRefreshPlayer
|
||||||
|
CmdPurgeChat
|
||||||
|
CmdHelp
|
||||||
|
CmdEmotes
|
||||||
|
)
|
||||||
|
|
||||||
|
type CommandLevel int
|
||||||
|
|
||||||
|
// Command access levels
|
||||||
|
const (
|
||||||
|
CmdlUser CommandLevel = iota
|
||||||
|
CmdlMod
|
||||||
|
CmdlAdmin
|
||||||
|
)
|
||||||
|
|
||||||
|
type EventType int
|
||||||
|
|
||||||
|
// Event Types
|
||||||
|
const (
|
||||||
|
EvJoin EventType = iota
|
||||||
|
EvLeave
|
||||||
|
EvKick
|
||||||
|
EvBan
|
||||||
|
EvServerMessage
|
||||||
|
EvNameChange
|
||||||
|
EvNameChangeForced
|
||||||
|
)
|
||||||
|
|
||||||
|
type MessageType int
|
||||||
|
|
||||||
|
// Message Types
|
||||||
|
const (
|
||||||
|
MsgChat MessageType = iota // standard chat
|
||||||
|
MsgAction // /me command
|
||||||
|
MsgServer // server message
|
||||||
|
MsgError // something went wrong
|
||||||
|
MsgNotice // Like MsgServer, but for mods and admins only.
|
||||||
|
MsgCommandResponse // The response from command
|
||||||
|
MsgCommandError // The error response from command
|
||||||
|
)
|
||||||
74
MovieNight/common/emotes.go
Executable file
74
MovieNight/common/emotes.go
Executable file
@@ -0,0 +1,74 @@
|
|||||||
|
package common
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"path/filepath"
|
||||||
|
"regexp"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
type EmotesMap map[string]string
|
||||||
|
|
||||||
|
var Emotes EmotesMap
|
||||||
|
|
||||||
|
var reStripStatic = regexp.MustCompile(`^(\\|/)?static`)
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
Emotes = NewEmotesMap()
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewEmotesMap() EmotesMap {
|
||||||
|
return map[string]string{}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (em EmotesMap) Add(fullpath string) EmotesMap {
|
||||||
|
fullpath = reStripStatic.ReplaceAllLiteralString(fullpath, "")
|
||||||
|
|
||||||
|
base := filepath.Base(fullpath)
|
||||||
|
code := base[0 : len(base)-len(filepath.Ext(base))]
|
||||||
|
|
||||||
|
_, exists := em[code]
|
||||||
|
|
||||||
|
num := 0
|
||||||
|
for exists {
|
||||||
|
num += 1
|
||||||
|
_, exists = em[fmt.Sprintf("%s-%d", code, num)]
|
||||||
|
}
|
||||||
|
|
||||||
|
if num > 0 {
|
||||||
|
code = fmt.Sprintf("%s-%d", code, num)
|
||||||
|
}
|
||||||
|
|
||||||
|
em[code] = fullpath
|
||||||
|
//fmt.Printf("Added emote %s at path %q\n", code, fullpath)
|
||||||
|
return em
|
||||||
|
}
|
||||||
|
|
||||||
|
func EmoteToHtml(file, title string) string {
|
||||||
|
return fmt.Sprintf(`<img src="%s" height="28px" title="%s" />`, file, title)
|
||||||
|
}
|
||||||
|
|
||||||
|
func ParseEmotesArray(words []string) []string {
|
||||||
|
newWords := []string{}
|
||||||
|
for _, word := range words {
|
||||||
|
// make :emote: and [emote] valid for replacement.
|
||||||
|
wordTrimmed := strings.Trim(word, ":[]")
|
||||||
|
|
||||||
|
found := false
|
||||||
|
for key, val := range Emotes {
|
||||||
|
if key == wordTrimmed {
|
||||||
|
newWords = append(newWords, EmoteToHtml(val, key))
|
||||||
|
found = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if !found {
|
||||||
|
newWords = append(newWords, word)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return newWords
|
||||||
|
}
|
||||||
|
|
||||||
|
func ParseEmotes(msg string) string {
|
||||||
|
words := ParseEmotesArray(strings.Split(msg, " "))
|
||||||
|
return strings.Join(words, " ")
|
||||||
|
}
|
||||||
44
MovieNight/common/emotes_test.go
Executable file
44
MovieNight/common/emotes_test.go
Executable 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": "/emotes/one.png",
|
||||||
|
"two": "/emotes/two.png",
|
||||||
|
"three": "/emotes/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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
200
MovieNight/common/logging.go
Executable file
200
MovieNight/common/logging.go
Executable file
@@ -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...)
|
||||||
|
}
|
||||||
18
MovieNight/common/logging_dev.go
Executable file
18
MovieNight/common/logging_dev.go
Executable file
@@ -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...)
|
||||||
|
}
|
||||||
90
MovieNight/common/templates.go
Executable file
90
MovieNight/common/templates.go
Executable file
@@ -0,0 +1,90 @@
|
|||||||
|
package common
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
html "html/template"
|
||||||
|
"net/http"
|
||||||
|
"strings"
|
||||||
|
text "text/template"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Holds the server's templates
|
||||||
|
var serverTemplates map[string]*html.Template
|
||||||
|
|
||||||
|
// Holds the client's chat templates
|
||||||
|
var chatTemplates map[string]*text.Template
|
||||||
|
|
||||||
|
var isServer bool = false
|
||||||
|
|
||||||
|
// keys and files to load for that template
|
||||||
|
var serverTemplateDefs map[string][]string = map[string][]string{
|
||||||
|
"pin": []string{"./static/base.html", "./static/thedoor.html"},
|
||||||
|
"main": []string{"./static/base.html", "./static/main.html"},
|
||||||
|
"help": []string{"./static/base.html", "./static/help.html"},
|
||||||
|
"emotes": []string{"./static/base.html", "./static/emotes.html"},
|
||||||
|
}
|
||||||
|
|
||||||
|
var chatTemplateDefs map[string]string = map[string]string{
|
||||||
|
fmt.Sprint(DTInvalid, 0): "wot",
|
||||||
|
|
||||||
|
fmt.Sprint(DTChat, MsgChat): `<span>{{.Badge}} <span class="name" style="color:{{.Color}}">{{.From}}` +
|
||||||
|
`</span><b>:</b> <span class="msg">{{.Message}}</span></span>`,
|
||||||
|
fmt.Sprint(DTChat, MsgAction): `<span style="color:{{.Color}}"><span class="name">{{.From}}` +
|
||||||
|
`</span> <span class="cmdme">{{.Message}}</span></span>`,
|
||||||
|
}
|
||||||
|
|
||||||
|
// Called from the server
|
||||||
|
func InitTemplates() error {
|
||||||
|
isServer = true
|
||||||
|
serverTemplates = make(map[string]*html.Template)
|
||||||
|
chatTemplates = make(map[string]*text.Template)
|
||||||
|
|
||||||
|
// Parse server templates
|
||||||
|
for key, files := range serverTemplateDefs {
|
||||||
|
t, err := html.ParseFiles(files...)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("Unable to parse templates for %s: %v", key, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
serverTemplates[key] = t
|
||||||
|
}
|
||||||
|
|
||||||
|
// Parse client templates
|
||||||
|
//for key, def := range chatTemplateDefs {
|
||||||
|
// t := text.New(key)
|
||||||
|
// err, _ := t.Parse(def)
|
||||||
|
// if err != nil {
|
||||||
|
// return fmt.Errorf("Unabel to parse chat template %q: %v", key, err)
|
||||||
|
// }
|
||||||
|
|
||||||
|
// chatTemplates[key] = t
|
||||||
|
//}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO
|
||||||
|
func LoadChatTemplates() error {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func ExecuteChatTemplate(typeA, typeB int, data interface{}) (string, error) {
|
||||||
|
key := fmt.Sprint(typeA, typeB)
|
||||||
|
t := chatTemplates[key]
|
||||||
|
builder := &strings.Builder{}
|
||||||
|
|
||||||
|
if err := t.Execute(builder, data); err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
return builder.String(), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func ExecuteServerTemplate(w http.ResponseWriter, key string, data interface{}) error {
|
||||||
|
t, ok := serverTemplates[key]
|
||||||
|
if !ok {
|
||||||
|
return fmt.Errorf("Template with the key %q does not exist", key)
|
||||||
|
}
|
||||||
|
|
||||||
|
return t.Execute(w, data)
|
||||||
|
}
|
||||||
18
MovieNight/common/utils.go
Executable file
18
MovieNight/common/utils.go
Executable file
@@ -0,0 +1,18 @@
|
|||||||
|
package common
|
||||||
|
|
||||||
|
// Misc utils
|
||||||
|
|
||||||
|
import (
|
||||||
|
"regexp"
|
||||||
|
)
|
||||||
|
|
||||||
|
var usernameRegex *regexp.Regexp = regexp.MustCompile(`^[0-9a-zA-Z_-]*[a-zA-Z0-9]+[0-9a-zA-Z_-]*$`)
|
||||||
|
|
||||||
|
const InvalidNameError string = `Invalid name.<br />Name must be between 3 and 36 characters in length; contain only numbers, letters, underscores or dashes; and contain at least one number or letter.<br />Names cannot contain spaces.`
|
||||||
|
|
||||||
|
// IsValidName checks that name is within the correct ranges, follows the regex defined
|
||||||
|
// and is not a valid color name
|
||||||
|
func IsValidName(name string) bool {
|
||||||
|
return 3 <= len(name) && len(name) <= 36 &&
|
||||||
|
usernameRegex.MatchString(name)
|
||||||
|
}
|
||||||
52
MovieNight/connection.go
Executable file
52
MovieNight/connection.go
Executable file
@@ -0,0 +1,52 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"net"
|
||||||
|
"sync"
|
||||||
|
|
||||||
|
"github.com/gorilla/websocket"
|
||||||
|
"github.com/zorchenhimer/MovieNight/common"
|
||||||
|
)
|
||||||
|
|
||||||
|
type chatConnection struct {
|
||||||
|
*websocket.Conn
|
||||||
|
mutex sync.RWMutex
|
||||||
|
forwardedFor string
|
||||||
|
clientName string
|
||||||
|
}
|
||||||
|
|
||||||
|
func (cc *chatConnection) ReadData(data interface{}) error {
|
||||||
|
cc.mutex.RLock()
|
||||||
|
defer cc.mutex.RUnlock()
|
||||||
|
|
||||||
|
stats.msgInInc()
|
||||||
|
return cc.ReadJSON(data)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (cc *chatConnection) WriteData(data interface{}) error {
|
||||||
|
cc.mutex.Lock()
|
||||||
|
defer cc.mutex.Unlock()
|
||||||
|
|
||||||
|
stats.msgOutInc()
|
||||||
|
err := cc.WriteJSON(data)
|
||||||
|
if err != nil {
|
||||||
|
if operr, ok := err.(*net.OpError); ok {
|
||||||
|
common.LogDebugln("OpError: " + operr.Err.Error())
|
||||||
|
}
|
||||||
|
return fmt.Errorf("Error writing data to %s %s: %v", cc.clientName, cc.Host(), err)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (cc *chatConnection) Host() string {
|
||||||
|
if len(cc.forwardedFor) > 0 {
|
||||||
|
return cc.forwardedFor
|
||||||
|
}
|
||||||
|
|
||||||
|
host, _, err := net.SplitHostPort(cc.RemoteAddr().String())
|
||||||
|
if err != nil {
|
||||||
|
return cc.RemoteAddr().String()
|
||||||
|
}
|
||||||
|
return host
|
||||||
|
}
|
||||||
239
MovieNight/emotes.go
Executable file
239
MovieNight/emotes.go
Executable file
@@ -0,0 +1,239 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"io/ioutil"
|
||||||
|
"log"
|
||||||
|
"net/http"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/pkg/errors"
|
||||||
|
"github.com/zorchenhimer/MovieNight/common"
|
||||||
|
)
|
||||||
|
|
||||||
|
const emoteDir = "./static/emotes/"
|
||||||
|
|
||||||
|
type TwitchUser struct {
|
||||||
|
ID string
|
||||||
|
Login string
|
||||||
|
}
|
||||||
|
|
||||||
|
type EmoteInfo struct {
|
||||||
|
ID int
|
||||||
|
Code string
|
||||||
|
}
|
||||||
|
|
||||||
|
func loadEmotes() error {
|
||||||
|
//fmt.Println(processEmoteDir(emoteDir))
|
||||||
|
newEmotes, err := processEmoteDir(emoteDir)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
common.Emotes = newEmotes
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func processEmoteDir(path string) (common.EmotesMap, error) {
|
||||||
|
dirInfo, err := ioutil.ReadDir(path)
|
||||||
|
if err != nil {
|
||||||
|
return nil, errors.Wrap(err, "could not open emoteDir:")
|
||||||
|
}
|
||||||
|
|
||||||
|
subDirs := []string{}
|
||||||
|
|
||||||
|
for _, item := range dirInfo {
|
||||||
|
// Get first level subdirs (eg, "twitch", "discord", etc)
|
||||||
|
if item.IsDir() {
|
||||||
|
subDirs = append(subDirs, item.Name())
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
em := common.NewEmotesMap()
|
||||||
|
// Find top level emotes
|
||||||
|
em, err = findEmotes(path, em)
|
||||||
|
if err != nil {
|
||||||
|
return nil, errors.Wrap(err, "could not findEmotes() in top level directory:")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get second level subdirs (eg, "twitch", "zorchenhimer", etc)
|
||||||
|
for _, dir := range subDirs {
|
||||||
|
subd, err := ioutil.ReadDir(filepath.Join(path, dir))
|
||||||
|
if err != nil {
|
||||||
|
fmt.Printf("Error reading dir %q: %v\n", subd, err)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
for _, d := range subd {
|
||||||
|
if d.IsDir() {
|
||||||
|
//emotes = append(emotes, findEmotes(filepath.Join(path, dir, d.Name()))...)
|
||||||
|
p := filepath.Join(path, dir, d.Name())
|
||||||
|
em, err = findEmotes(p, em)
|
||||||
|
if err != nil {
|
||||||
|
fmt.Printf("Error finding emotes in %q: %v\n", p, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Printf("processEmoteDir: %d\n", len(em))
|
||||||
|
return em, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func findEmotes(dir string, em common.EmotesMap) (common.EmotesMap, error) {
|
||||||
|
//em := NewEmotesMap()
|
||||||
|
|
||||||
|
fmt.Printf("finding emotes in %q\n", dir)
|
||||||
|
emotePNGs, err := filepath.Glob(filepath.Join(dir, "*.png"))
|
||||||
|
if err != nil {
|
||||||
|
return em, fmt.Errorf("unable to glob emote directory: %s\n", err)
|
||||||
|
}
|
||||||
|
fmt.Printf("%d emotePNGs\n", len(emotePNGs))
|
||||||
|
|
||||||
|
emoteGIFs, err := filepath.Glob(filepath.Join(dir, "*.gif"))
|
||||||
|
if err != nil {
|
||||||
|
return em, errors.Wrap(err, "unable to glob emote directory:")
|
||||||
|
}
|
||||||
|
fmt.Printf("%d emoteGIFs\n", len(emoteGIFs))
|
||||||
|
|
||||||
|
for _, file := range emotePNGs {
|
||||||
|
em = em.Add(file)
|
||||||
|
//emotes = append(emotes, common.Emote{FullPath: dir, Code: file})
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, file := range emoteGIFs {
|
||||||
|
em = em.Add(file)
|
||||||
|
}
|
||||||
|
|
||||||
|
return em, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func getEmotes(names []string) error {
|
||||||
|
users := getUserIDs(names)
|
||||||
|
users = append(users, TwitchUser{ID: "0", Login: "twitch"})
|
||||||
|
|
||||||
|
for _, user := range users {
|
||||||
|
emotes, cheers, err := getChannelEmotes(user.ID)
|
||||||
|
if err != nil {
|
||||||
|
return errors.Wrapf(err, "could not get emote data for \"%s\"", user.ID)
|
||||||
|
}
|
||||||
|
|
||||||
|
emoteUserDir := filepath.Join(emoteDir, "twitch", user.Login)
|
||||||
|
if _, err := os.Stat(emoteUserDir); os.IsNotExist(err) {
|
||||||
|
os.MkdirAll(emoteUserDir, os.ModePerm)
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, emote := range emotes {
|
||||||
|
if !strings.ContainsAny(emote.Code, `:;\[]|?&`) {
|
||||||
|
filePath := filepath.Join(emoteUserDir, emote.Code+".png")
|
||||||
|
file, err := os.Create(filePath)
|
||||||
|
if err != nil {
|
||||||
|
|
||||||
|
return errors.Wrapf(err, "could not create emote file in path \"%s\":", filePath)
|
||||||
|
}
|
||||||
|
|
||||||
|
err = downloadEmote(emote.ID, file)
|
||||||
|
if err != nil {
|
||||||
|
return errors.Wrapf(err, "could not download emote %s:", emote.Code)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for amount, sizes := range cheers {
|
||||||
|
name := fmt.Sprintf("%sCheer%s.gif", user.Login, amount)
|
||||||
|
filePath := filepath.Join(emoteUserDir, name)
|
||||||
|
file, err := os.Create(filePath)
|
||||||
|
if err != nil {
|
||||||
|
return errors.Wrapf(err, "could not create emote file in path \"%s\":", filePath)
|
||||||
|
}
|
||||||
|
|
||||||
|
err = downloadCheerEmote(sizes["4"], file)
|
||||||
|
if err != nil {
|
||||||
|
return errors.Wrapf(err, "could not download emote %s:", name)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func getUserIDs(names []string) []TwitchUser {
|
||||||
|
logins := strings.Join(names, "&login=")
|
||||||
|
request, err := http.NewRequest("GET", fmt.Sprintf("https://api.twitch.tv/helix/users?login=%s", logins), nil)
|
||||||
|
if err != nil {
|
||||||
|
log.Fatalln("Error generating new request:", err)
|
||||||
|
}
|
||||||
|
request.Header.Set("Client-ID", settings.TwitchClientID)
|
||||||
|
|
||||||
|
client := http.Client{}
|
||||||
|
resp, err := client.Do(request)
|
||||||
|
if err != nil {
|
||||||
|
log.Fatalln("Error sending request:", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
decoder := json.NewDecoder(resp.Body)
|
||||||
|
type userResponse struct {
|
||||||
|
Data []TwitchUser
|
||||||
|
}
|
||||||
|
var data userResponse
|
||||||
|
|
||||||
|
err = decoder.Decode(&data)
|
||||||
|
if err != nil {
|
||||||
|
log.Fatalln("Error decoding data:", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return data.Data
|
||||||
|
}
|
||||||
|
|
||||||
|
func getChannelEmotes(ID string) ([]EmoteInfo, map[string]map[string]string, error) {
|
||||||
|
resp, err := http.Get("https://api.twitchemotes.com/api/v4/channels/" + ID)
|
||||||
|
if err != nil {
|
||||||
|
return nil, nil, errors.Wrap(err, "could not get emotes")
|
||||||
|
}
|
||||||
|
decoder := json.NewDecoder(resp.Body)
|
||||||
|
|
||||||
|
type EmoteResponse struct {
|
||||||
|
Emotes []EmoteInfo
|
||||||
|
Cheermotes map[string]map[string]string
|
||||||
|
}
|
||||||
|
var data EmoteResponse
|
||||||
|
|
||||||
|
err = decoder.Decode(&data)
|
||||||
|
if err != nil {
|
||||||
|
return nil, nil, errors.Wrap(err, "could not decode emotes")
|
||||||
|
}
|
||||||
|
|
||||||
|
return data.Emotes, data.Cheermotes, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func downloadEmote(ID int, file *os.File) error {
|
||||||
|
resp, err := http.Get(fmt.Sprintf("https://static-cdn.jtvnw.net/emoticons/v1/%d/3.0", ID))
|
||||||
|
if err != nil {
|
||||||
|
return errors.Errorf("could not download emote file %s: %v", file.Name(), err)
|
||||||
|
}
|
||||||
|
defer resp.Body.Close()
|
||||||
|
|
||||||
|
_, err = io.Copy(file, resp.Body)
|
||||||
|
if err != nil {
|
||||||
|
return errors.Errorf("could not save emote: %v", err)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func downloadCheerEmote(url string, file *os.File) error {
|
||||||
|
resp, err := http.Get(url)
|
||||||
|
if err != nil {
|
||||||
|
return errors.Errorf("could not download cheer file %s: %v", file.Name(), err)
|
||||||
|
}
|
||||||
|
defer resp.Body.Close()
|
||||||
|
|
||||||
|
_, err = io.Copy(file, resp.Body)
|
||||||
|
if err != nil {
|
||||||
|
return errors.Errorf("could not save cheer: %v", err)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
37
MovieNight/entrypoint.sh
Executable file
37
MovieNight/entrypoint.sh
Executable file
@@ -0,0 +1,37 @@
|
|||||||
|
#! /bin/bash
|
||||||
|
|
||||||
|
function main() {
|
||||||
|
listen &
|
||||||
|
while true; do
|
||||||
|
sleep 10
|
||||||
|
publish
|
||||||
|
done
|
||||||
|
}
|
||||||
|
|
||||||
|
function listen() {
|
||||||
|
if [ ! -d /main ]; then
|
||||||
|
go build && ./MovieNight
|
||||||
|
elif [ ! -e /main/exec-MovieNight ]; then
|
||||||
|
/main/MovieNight
|
||||||
|
else
|
||||||
|
/main/exec-MovieNight
|
||||||
|
fi
|
||||||
|
kill -9 1
|
||||||
|
}
|
||||||
|
|
||||||
|
function publish() {
|
||||||
|
ffmpeg \
|
||||||
|
-i rtsp://${RTSP_IP:-192.168.0.83}:${RTSP_PORT:-8554}/unicast \
|
||||||
|
-preset ultrafast \
|
||||||
|
-filter:v scale=-1:${RES:-720} \
|
||||||
|
-vcodec libx264 \
|
||||||
|
-acodec copy \
|
||||||
|
-f flv \
|
||||||
|
-b:v ${KBPS:-500}k \
|
||||||
|
-b:a 0k \
|
||||||
|
rtmp://localhost:1935/live/ALongStreamKey
|
||||||
|
}
|
||||||
|
|
||||||
|
if [ "$0" == "$BASH_SOURCE" ]; then
|
||||||
|
main "$@"
|
||||||
|
fi
|
||||||
48
MovieNight/errors.go
Executable file
48
MovieNight/errors.go
Executable file
@@ -0,0 +1,48 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"reflect"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
func errorName(err error) string {
|
||||||
|
return reflect.ValueOf(err).Type().Name()
|
||||||
|
}
|
||||||
|
|
||||||
|
// UserNameError is a base error for errors that deal with user names
|
||||||
|
type UserNameError struct {
|
||||||
|
Name string
|
||||||
|
}
|
||||||
|
|
||||||
|
// UserFormatError is an error for when the name format does not match what is required
|
||||||
|
type UserFormatError UserNameError
|
||||||
|
|
||||||
|
func (e UserFormatError) Error() string {
|
||||||
|
return fmt.Sprintf("\"%s\", is in an invalid format", e.Name)
|
||||||
|
}
|
||||||
|
|
||||||
|
// UserTakenError is an error for when a user tries to join with a name that is already taken
|
||||||
|
type UserTakenError UserNameError
|
||||||
|
|
||||||
|
func (e UserTakenError) Error() string {
|
||||||
|
return fmt.Sprintf("\"%s\", is already taken", e.Name)
|
||||||
|
}
|
||||||
|
|
||||||
|
// BannedUserError is an error for when a user tries to join with a banned ip address
|
||||||
|
type BannedUserError struct {
|
||||||
|
Host, Name string
|
||||||
|
Names []string
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e BannedUserError) Error() string {
|
||||||
|
return fmt.Sprintf("banned user tried to connect with IP %s: %s (banned with name(s) %s)", e.Host, e.Name, strings.Join(e.Names, ", "))
|
||||||
|
}
|
||||||
|
|
||||||
|
func newBannedUserError(host, name string, names []string) BannedUserError {
|
||||||
|
return BannedUserError{
|
||||||
|
Host: host,
|
||||||
|
Name: name,
|
||||||
|
Names: names,
|
||||||
|
}
|
||||||
|
}
|
||||||
BIN
MovieNight/favicon.png
Executable file
BIN
MovieNight/favicon.png
Executable file
Binary file not shown.
|
After Width: | Height: | Size: 3.5 KiB |
23
MovieNight/go.mod
Executable file
23
MovieNight/go.mod
Executable file
@@ -0,0 +1,23 @@
|
|||||||
|
module github.com/zorchenhimer/MovieNight
|
||||||
|
|
||||||
|
go 1.12
|
||||||
|
|
||||||
|
require (
|
||||||
|
github.com/Microsoft/go-winio v0.4.12 // indirect
|
||||||
|
github.com/cenkalti/backoff v2.1.1+incompatible // indirect
|
||||||
|
github.com/chromedp/cdproto v0.0.0-20190412020601-c4267f5c421a // indirect
|
||||||
|
github.com/containerd/continuity v0.0.0-20181203112020-004b46473808 // indirect
|
||||||
|
github.com/gorilla/sessions v1.1.3
|
||||||
|
github.com/gorilla/websocket v1.4.0
|
||||||
|
github.com/konsorten/go-windows-terminal-sequences v1.0.2 // indirect
|
||||||
|
github.com/mailru/easyjson v0.0.0-20190403194419-1ea4449da983 // indirect
|
||||||
|
github.com/nareix/joy4 v0.0.0-20181022032202-3ddbc8f9d431
|
||||||
|
github.com/ory/dockertest v3.3.4+incompatible // indirect
|
||||||
|
github.com/pkg/errors v0.8.1
|
||||||
|
github.com/sirupsen/logrus v1.4.1 // indirect
|
||||||
|
github.com/stretchr/objx v0.2.0 // indirect
|
||||||
|
golang.org/x/crypto v0.0.0-20190411191339-88737f569e3a // indirect
|
||||||
|
golang.org/x/image v0.0.0-20190321063152-3fc05d484e9f // indirect
|
||||||
|
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3 // indirect
|
||||||
|
golang.org/x/sys v0.0.0-20190412213103-97732733099d
|
||||||
|
)
|
||||||
102
MovieNight/go.sum
Executable file
102
MovieNight/go.sum
Executable file
@@ -0,0 +1,102 @@
|
|||||||
|
github.com/Azure/go-ansiterm v0.0.0-20170929234023-d6e3b3328b78 h1:w+iIsaOQNcT7OZ575w+acHgRric5iCyQh+xv+KJ4HB8=
|
||||||
|
github.com/Azure/go-ansiterm v0.0.0-20170929234023-d6e3b3328b78/go.mod h1:LmzpDX56iTiv29bbRTIsUNlaFfuhWRQBWjQdVyAevI8=
|
||||||
|
github.com/Microsoft/go-winio v0.4.11 h1:zoIOcVf0xPN1tnMVbTtEdI+P8OofVk3NObnwOQ6nK2Q=
|
||||||
|
github.com/Microsoft/go-winio v0.4.11/go.mod h1:VhR8bwka0BXejwEJY73c50VrPtXAaKcyvVC4A4RozmA=
|
||||||
|
github.com/Microsoft/go-winio v0.4.12/go.mod h1:VhR8bwka0BXejwEJY73c50VrPtXAaKcyvVC4A4RozmA=
|
||||||
|
github.com/Nvveen/Gotty v0.0.0-20120604004816-cd527374f1e5 h1:TngWCqHvy9oXAN6lEVMRuU21PR1EtLVZJmdB18Gu3Rw=
|
||||||
|
github.com/Nvveen/Gotty v0.0.0-20120604004816-cd527374f1e5/go.mod h1:lmUJ/7eu/Q8D7ML55dXQrVaamCz2vxCfdQBasLZfHKk=
|
||||||
|
github.com/cenkalti/backoff v2.0.0+incompatible h1:5IIPUHhlnUZbcHQsQou5k1Tn58nJkeJL9U+ig5CHJbY=
|
||||||
|
github.com/cenkalti/backoff v2.0.0+incompatible/go.mod h1:90ReRw6GdpyfrHakVjL/QHaoyV4aDUVVkXQJJJ3NXXM=
|
||||||
|
github.com/cenkalti/backoff v2.1.1+incompatible/go.mod h1:90ReRw6GdpyfrHakVjL/QHaoyV4aDUVVkXQJJJ3NXXM=
|
||||||
|
github.com/chromedp/cdproto v0.0.0-20190217000753-2d8e8962ceb2 h1:4Ck8YOuS0G3+0xMb80cDSff7QpUolhSc0PGyfagbcdA=
|
||||||
|
github.com/chromedp/cdproto v0.0.0-20190217000753-2d8e8962ceb2/go.mod h1:xquOK9dIGFlLaIGI4c6IyfLI/Gz0LiYYuJtzhsUODgI=
|
||||||
|
github.com/chromedp/cdproto v0.0.0-20190412020601-c4267f5c421a/go.mod h1:xquOK9dIGFlLaIGI4c6IyfLI/Gz0LiYYuJtzhsUODgI=
|
||||||
|
github.com/chromedp/chromedp v0.1.3 h1:Nkqt42/7tvzg57mexc4LbM8nZbx7vSZ+eiUpeczGGL8=
|
||||||
|
github.com/chromedp/chromedp v0.1.3/go.mod h1:ZahQlJx8YBfDtuFN80zn6P7fskSotBkdhgKDoLWFANk=
|
||||||
|
github.com/containerd/continuity v0.0.0-20181027224239-bea7585dbfac h1:PThQaO4yCvJzJBUW1XoFQxLotWRhvX2fgljJX8yrhFI=
|
||||||
|
github.com/containerd/continuity v0.0.0-20181027224239-bea7585dbfac/go.mod h1:GL3xCUCBDV3CZiTSEKksMWbLE66hEyuu9qyDOOqM47Y=
|
||||||
|
github.com/containerd/continuity v0.0.0-20181203112020-004b46473808/go.mod h1:GL3xCUCBDV3CZiTSEKksMWbLE66hEyuu9qyDOOqM47Y=
|
||||||
|
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||||
|
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||||
|
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||||
|
github.com/dennwc/dom v0.3.0 h1:u89+QvT1OPRSSTFf54o9RuK7C0Uoq2jFo4VCa4rnjtI=
|
||||||
|
github.com/dennwc/dom v0.3.0/go.mod h1:/z5w9Stx19m8RUwolsmsqTs9rDxKgJO5T9UEumilgk4=
|
||||||
|
github.com/dennwc/testproxy v1.0.1 h1:mQhNVWHPolTYjJrDZYKcugIplWRSlFAis6k/Zf1s0c0=
|
||||||
|
github.com/dennwc/testproxy v1.0.1/go.mod h1:EHGV9tzWhMPLmEoVJ2KGyC149XqwKZwBDViCjhKD5d8=
|
||||||
|
github.com/disintegration/imaging v1.6.0 h1:nVPXRUUQ36Z7MNf0O77UzgnOb1mkMMor7lmJMJXc/mA=
|
||||||
|
github.com/disintegration/imaging v1.6.0/go.mod h1:xuIt+sRxDFrHS0drzXUlCJthkJ8k7lkkUojDSR247MQ=
|
||||||
|
github.com/docker/go-connections v0.4.0 h1:El9xVISelRB7BuFusrZozjnkIM5YnzCViNKohAFqRJQ=
|
||||||
|
github.com/docker/go-connections v0.4.0/go.mod h1:Gbd7IOopHjR8Iph03tsViu4nIes5XhDvyHbTtUxmeec=
|
||||||
|
github.com/docker/go-units v0.3.3 h1:Xk8S3Xj5sLGlG5g67hJmYMmUgXv5N4PhkjJHHqrwnTk=
|
||||||
|
github.com/docker/go-units v0.3.3/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk=
|
||||||
|
github.com/google/go-cmp v0.2.0 h1:+dTQ8DZQJz0Mb/HjFlkptS1FeQ4cWSnN941F8aEG4SQ=
|
||||||
|
github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M=
|
||||||
|
github.com/gorilla/context v1.1.1 h1:AWwleXJkX/nhcU9bZSnZoi3h/qGYqQAGhq6zZe/aQW8=
|
||||||
|
github.com/gorilla/context v1.1.1/go.mod h1:kBGZzfjB9CEq2AlWe17Uuf7NDRt0dE0s8S51q0aT7Yg=
|
||||||
|
github.com/gorilla/securecookie v1.1.1 h1:miw7JPhV+b/lAHSXz4qd/nN9jRiAFV5FwjeKyCS8BvQ=
|
||||||
|
github.com/gorilla/securecookie v1.1.1/go.mod h1:ra0sb63/xPlUeL+yeDciTfxMRAA+MP+HVt/4epWDjd4=
|
||||||
|
github.com/gorilla/sessions v1.1.3 h1:uXoZdcdA5XdXF3QzuSlheVRUvjl+1rKY7zBXL68L9RU=
|
||||||
|
github.com/gorilla/sessions v1.1.3/go.mod h1:8KCfur6+4Mqcc6S0FEfKuN15Vl5MgXW92AE8ovaJD0w=
|
||||||
|
github.com/gorilla/websocket v1.4.0 h1:WDFjx/TMzVgy9VdMMQi2K2Emtwi2QcUQsztZ/zLaH/Q=
|
||||||
|
github.com/gorilla/websocket v1.4.0/go.mod h1:E7qHFY5m1UJ88s3WnNqhKjPHQ0heANvMoAMk2YaljkQ=
|
||||||
|
github.com/gotestyourself/gotestyourself v2.2.0+incompatible h1:AQwinXlbQR2HvPjQZOmDhRqsv5mZf+Jb1RnSLxcqZcI=
|
||||||
|
github.com/gotestyourself/gotestyourself v2.2.0+incompatible/go.mod h1:zZKM6oeNM8k+FRljX1mnzVYeS8wiGgQyvST1/GafPbY=
|
||||||
|
github.com/knq/sysutil v0.0.0-20181215143952-f05b59f0f307 h1:vl4eIlySbjertFaNwiMjXsGrFVK25aOWLq7n+3gh2ls=
|
||||||
|
github.com/knq/sysutil v0.0.0-20181215143952-f05b59f0f307/go.mod h1:BjPj+aVjl9FW/cCGiF3nGh5v+9Gd3VCgBQbod/GlMaQ=
|
||||||
|
github.com/konsorten/go-windows-terminal-sequences v1.0.1 h1:mweAR1A6xJ3oS2pRaGiHgQ4OO8tzTaLawm8vnODuwDk=
|
||||||
|
github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
|
||||||
|
github.com/konsorten/go-windows-terminal-sequences v1.0.2/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
|
||||||
|
github.com/lib/pq v1.0.0 h1:X5PMW56eZitiTeO7tKzZxFCSpbFZJtkMMooicw2us9A=
|
||||||
|
github.com/lib/pq v1.0.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo=
|
||||||
|
github.com/mailru/easyjson v0.0.0-20180823135443-60711f1a8329/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc=
|
||||||
|
github.com/mailru/easyjson v0.0.0-20190221075403-6243d8e04c3f h1:B6PQkurxGG1rqEX96oE14gbj8bqvYC5dtks9r5uGmlE=
|
||||||
|
github.com/mailru/easyjson v0.0.0-20190221075403-6243d8e04c3f/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc=
|
||||||
|
github.com/mailru/easyjson v0.0.0-20190403194419-1ea4449da983/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc=
|
||||||
|
github.com/nareix/joy4 v0.0.0-20181022032202-3ddbc8f9d431 h1:nWhrOsCKdV6bivw03k7MROF2tYzCFGfYBYFrTEHyucs=
|
||||||
|
github.com/nareix/joy4 v0.0.0-20181022032202-3ddbc8f9d431/go.mod h1:aFJ1ZwLjvHN4yEzE5Bkz8rD8/d8Vlj3UIuvz2yfET7I=
|
||||||
|
github.com/opencontainers/go-digest v1.0.0-rc1 h1:WzifXhOVOEOuFYOJAW6aQqW0TooG2iki3E3Ii+WN7gQ=
|
||||||
|
github.com/opencontainers/go-digest v1.0.0-rc1/go.mod h1:cMLVZDEM3+U2I4VmLI6N8jQYUd2OVphdqWwCJHrFt2s=
|
||||||
|
github.com/opencontainers/image-spec v1.0.1 h1:JMemWkRwHx4Zj+fVxWoMCFm/8sYGGrUVojFA6h/TRcI=
|
||||||
|
github.com/opencontainers/image-spec v1.0.1/go.mod h1:BtxoFyWECRxE4U/7sNtV5W15zMzWCbyJoFRP3s7yZA0=
|
||||||
|
github.com/opencontainers/runc v0.1.1 h1:GlxAyO6x8rfZYN9Tt0Kti5a/cP41iuiO2yYT0IJGY8Y=
|
||||||
|
github.com/opencontainers/runc v0.1.1/go.mod h1:qT5XzbpPznkRYVz/mWwUaVBUv2rmF59PVA73FjuZG0U=
|
||||||
|
github.com/ory/dockertest v3.3.2+incompatible h1:uO+NcwH6GuFof/Uz8yzjNi1g0sGT5SLAJbdBvD8bUYc=
|
||||||
|
github.com/ory/dockertest v3.3.2+incompatible/go.mod h1:1vX4m9wsvi00u5bseYwXaSnhNrne+V0E6LAcBILJdPs=
|
||||||
|
github.com/ory/dockertest v3.3.4+incompatible/go.mod h1:1vX4m9wsvi00u5bseYwXaSnhNrne+V0E6LAcBILJdPs=
|
||||||
|
github.com/pkg/errors v0.8.0 h1:WdK/asTD0HN+q6hsWO3/vpuAkAr+tw6aNJNDFFf0+qw=
|
||||||
|
github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||||
|
github.com/pkg/errors v0.8.1 h1:iURUrRGxPUNPdy5/HRSm+Yj6okJ6UtLINN0Q9M4+h3I=
|
||||||
|
github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||||
|
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||||
|
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||||
|
github.com/sirupsen/logrus v1.2.0 h1:juTguoYk5qI21pwyTXY3B3Y5cOTH3ZUyZCg1v/mihuo=
|
||||||
|
github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo=
|
||||||
|
github.com/sirupsen/logrus v1.4.1/go.mod h1:ni0Sbl8bgC9z8RoU9G6nDWqqs/fq4eDPysMBDgk/93Q=
|
||||||
|
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||||
|
github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||||
|
github.com/stretchr/objx v0.2.0/go.mod h1:qt09Ya8vawLte6SNmTgCsAVtYtaKzEcn8ATUoHMkEqE=
|
||||||
|
github.com/stretchr/testify v1.2.2 h1:bSDNvY7ZPG5RlJ8otE/7V6gMiyenm9RtJ7IUVIAoJ1w=
|
||||||
|
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
|
||||||
|
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
|
||||||
|
golang.org/x/crypto v0.0.0-20180904163835-0709b304e793 h1:u+LnwYTOOW7Ukr/fppxEb1Nwz0AtPflrblfvUudpo+I=
|
||||||
|
golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
|
||||||
|
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||||
|
golang.org/x/crypto v0.0.0-20190411191339-88737f569e3a/go.mod h1:WFFai1msRO1wXaEeE5yQxYXgSfI8pQAWXbQop6sCtWE=
|
||||||
|
golang.org/x/image v0.0.0-20180708004352-c73c2afc3b81/go.mod h1:ux5Hcp/YLpHSI86hEcLt0YII63i6oz57MZXIpbrjZUs=
|
||||||
|
golang.org/x/image v0.0.0-20190220214146-31aff87c08e9/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js=
|
||||||
|
golang.org/x/image v0.0.0-20190227222117-0694c2d4d067 h1:KYGJGHOQy8oSi1fDlSpcZF0+juKwk/hEMv5SiwHogR0=
|
||||||
|
golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js=
|
||||||
|
golang.org/x/image v0.0.0-20190321063152-3fc05d484e9f/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js=
|
||||||
|
golang.org/x/net v0.0.0-20181114220301-adae6a3d119a h1:gOpx8G595UYyvj8UK4+OFyY4rx037g3fmfhe5SasG3U=
|
||||||
|
golang.org/x/net v0.0.0-20181114220301-adae6a3d119a/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||||
|
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||||
|
golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||||
|
golang.org/x/sys v0.0.0-20181107165924-66b7b1311ac8 h1:YoY1wS6JYVRpIfFngRf2HHo9R9dAne3xbkGOQ5rJXjU=
|
||||||
|
golang.org/x/sys v0.0.0-20181107165924-66b7b1311ac8/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||||
|
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||||
|
golang.org/x/sys v0.0.0-20190403152447-81d4e9dc473e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
|
golang.org/x/sys v0.0.0-20190412213103-97732733099d h1:+R4KGOnez64A81RvjARKc4UT5/tI9ujCIVX+P5KiHuI=
|
||||||
|
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
|
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||||
|
gotest.tools v2.2.0+incompatible h1:VsBPFP1AI068pPrMxtb/S8Zkgf9xEmTLJjfM+P5UIEo=
|
||||||
|
gotest.tools v2.2.0+incompatible/go.mod h1:DsYFclhRJ6vuDpmuTbkuFWG+y2sxOXAzmJt81HFBacw=
|
||||||
378
MovieNight/handlers.go
Executable file
378
MovieNight/handlers.go
Executable file
@@ -0,0 +1,378 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"log"
|
||||||
|
"net/http"
|
||||||
|
"path"
|
||||||
|
"path/filepath"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/zorchenhimer/MovieNight/common"
|
||||||
|
|
||||||
|
"github.com/gorilla/websocket"
|
||||||
|
"github.com/nareix/joy4/av/avutil"
|
||||||
|
"github.com/nareix/joy4/av/pubsub"
|
||||||
|
"github.com/nareix/joy4/format/flv"
|
||||||
|
"github.com/nareix/joy4/format/rtmp"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
// Read/Write mutex for rtmp stream
|
||||||
|
l = NewSuperLock()
|
||||||
|
|
||||||
|
// Map of active streams
|
||||||
|
channels = map[string]*Channel{}
|
||||||
|
)
|
||||||
|
|
||||||
|
type Channel struct {
|
||||||
|
que *pubsub.Queue
|
||||||
|
}
|
||||||
|
|
||||||
|
type writeFlusher struct {
|
||||||
|
httpflusher http.Flusher
|
||||||
|
io.Writer
|
||||||
|
}
|
||||||
|
|
||||||
|
func (self writeFlusher) Flush() error {
|
||||||
|
self.httpflusher.Flush()
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Serving static files
|
||||||
|
func wsStaticFiles(w http.ResponseWriter, r *http.Request) {
|
||||||
|
switch r.URL.Path {
|
||||||
|
case "/favicon.ico":
|
||||||
|
http.ServeFile(w, r, "./favicon.png")
|
||||||
|
return
|
||||||
|
case "/justvideo":
|
||||||
|
http.ServeFile(w, r, "./static/justvideo.html")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
goodPath := r.URL.Path[8:len(r.URL.Path)]
|
||||||
|
common.LogDebugf("[static] serving %q from folder ./static/\n", goodPath)
|
||||||
|
|
||||||
|
http.ServeFile(w, r, "./static/"+goodPath)
|
||||||
|
}
|
||||||
|
|
||||||
|
func wsWasmFile(w http.ResponseWriter, r *http.Request) {
|
||||||
|
if settings.NoCache {
|
||||||
|
w.Header().Set("Cache-Control", "no-cache, must-revalidate")
|
||||||
|
}
|
||||||
|
common.LogDebugln("[static] serving wasm file")
|
||||||
|
http.ServeFile(w, r, "./static/main.wasm")
|
||||||
|
}
|
||||||
|
|
||||||
|
func wsImages(w http.ResponseWriter, r *http.Request) {
|
||||||
|
base := filepath.Base(r.URL.Path)
|
||||||
|
common.LogDebugln("[img] ", base)
|
||||||
|
http.ServeFile(w, r, "./static/img/"+base)
|
||||||
|
}
|
||||||
|
|
||||||
|
func wsEmotes(w http.ResponseWriter, r *http.Request) {
|
||||||
|
http.ServeFile(w, r, path.Join("./static/", r.URL.Path))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handling the websocket
|
||||||
|
var upgrader = websocket.Upgrader{
|
||||||
|
ReadBufferSize: 1024,
|
||||||
|
WriteBufferSize: 1024,
|
||||||
|
CheckOrigin: func(r *http.Request) bool { return true }, //not checking origin
|
||||||
|
}
|
||||||
|
|
||||||
|
//this is also the handler for joining to the chat
|
||||||
|
func wsHandler(w http.ResponseWriter, r *http.Request) {
|
||||||
|
log.Println("ws handler")
|
||||||
|
|
||||||
|
conn, err := upgrader.Upgrade(w, r, nil)
|
||||||
|
if err != nil {
|
||||||
|
common.LogErrorln("Error upgrading to websocket:", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
common.LogDebugln("Connection has been upgraded to websocket")
|
||||||
|
|
||||||
|
go func() {
|
||||||
|
// Handle incomming messages
|
||||||
|
for {
|
||||||
|
var data common.ClientData
|
||||||
|
err := conn.ReadJSON(&data)
|
||||||
|
if err != nil { //if error then assuming that the connection is closed
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}()
|
||||||
|
}
|
||||||
|
|
||||||
|
// returns if it's OK to proceed
|
||||||
|
func checkRoomAccess(w http.ResponseWriter, r *http.Request) bool {
|
||||||
|
session, err := sstore.Get(r, "moviesession")
|
||||||
|
if err != nil {
|
||||||
|
// Don't return as server error here, just make a new session.
|
||||||
|
common.LogErrorf("Unable to get session for client %s: %v\n", r.RemoteAddr, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if settings.RoomAccess == AccessPin {
|
||||||
|
pin := session.Values["pin"]
|
||||||
|
// No pin found in session
|
||||||
|
if pin == nil || len(pin.(string)) == 0 {
|
||||||
|
if r.Method == "POST" {
|
||||||
|
// Check for correct pin
|
||||||
|
err = r.ParseForm()
|
||||||
|
if err != nil {
|
||||||
|
common.LogErrorf("Error parsing form")
|
||||||
|
http.Error(w, "Unable to get session data", http.StatusInternalServerError)
|
||||||
|
}
|
||||||
|
|
||||||
|
postPin := strings.TrimSpace(r.Form.Get("txtInput"))
|
||||||
|
common.LogDebugf("Received pin: %s\n", postPin)
|
||||||
|
if postPin == settings.RoomAccessPin {
|
||||||
|
// Pin is correct. Save it to session and return true.
|
||||||
|
session.Values["pin"] = settings.RoomAccessPin
|
||||||
|
session.Save(r, w)
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
// Pin is incorrect.
|
||||||
|
handlePinTemplate(w, r, "Incorrect PIN")
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
// nope. display pin entry and return
|
||||||
|
handlePinTemplate(w, r, "")
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
// Pin found in session, but it has changed since last time.
|
||||||
|
if pin.(string) != settings.RoomAccessPin {
|
||||||
|
// Clear out the old pin.
|
||||||
|
session.Values["pin"] = nil
|
||||||
|
session.Save(r, w)
|
||||||
|
|
||||||
|
// Prompt for new one.
|
||||||
|
handlePinTemplate(w, r, "Pin has changed. Enter new PIN.")
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
// Correct pin found in session
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: this.
|
||||||
|
if settings.RoomAccess == AccessRequest {
|
||||||
|
http.Error(w, "Requesting access not implemented yet", http.StatusNotImplemented)
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
// Room is open.
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
func handlePinTemplate(w http.ResponseWriter, r *http.Request, errorMessage string) {
|
||||||
|
log.Println("handle pin temp")
|
||||||
|
type Data struct {
|
||||||
|
Title string
|
||||||
|
SubmitText string
|
||||||
|
Notice string
|
||||||
|
}
|
||||||
|
|
||||||
|
if errorMessage == "" {
|
||||||
|
errorMessage = "Please enter the PIN"
|
||||||
|
}
|
||||||
|
|
||||||
|
data := Data{
|
||||||
|
Title: "Enter Pin",
|
||||||
|
SubmitText: "Submit Pin",
|
||||||
|
Notice: errorMessage,
|
||||||
|
}
|
||||||
|
|
||||||
|
err := common.ExecuteServerTemplate(w, "pin", data)
|
||||||
|
if err != nil {
|
||||||
|
common.LogErrorf("Error executing file, %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func handleHelpTemplate(w http.ResponseWriter, r *http.Request) {
|
||||||
|
http.NotFound(w, r)
|
||||||
|
}
|
||||||
|
|
||||||
|
func handleEmoteTemplate(w http.ResponseWriter, r *http.Request) {
|
||||||
|
log.Println("handle emote temp")
|
||||||
|
type Data struct {
|
||||||
|
Title string
|
||||||
|
Emotes map[string]string
|
||||||
|
}
|
||||||
|
|
||||||
|
data := Data{
|
||||||
|
Title: "Available Emotes",
|
||||||
|
Emotes: common.Emotes,
|
||||||
|
}
|
||||||
|
|
||||||
|
err := common.ExecuteServerTemplate(w, "emotes", data)
|
||||||
|
if err != nil {
|
||||||
|
common.LogErrorf("Error executing file, %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func handlePin(w http.ResponseWriter, r *http.Request) {
|
||||||
|
log.Println("handle pin")
|
||||||
|
session, err := sstore.Get(r, "moviesession")
|
||||||
|
if err != nil {
|
||||||
|
common.LogDebugf("Unable to get session: %v\n", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
val := session.Values["pin"]
|
||||||
|
if val == nil {
|
||||||
|
session.Values["pin"] = "1234"
|
||||||
|
err := session.Save(r, w)
|
||||||
|
if err != nil {
|
||||||
|
fmt.Fprintf(w, "unable to save session: %v", err)
|
||||||
|
}
|
||||||
|
fmt.Fprint(w, "Pin was not set")
|
||||||
|
common.LogDebugln("pin was not set")
|
||||||
|
} else {
|
||||||
|
fmt.Fprintf(w, "pin set: %v", val)
|
||||||
|
common.LogDebugf("pin is set: %v\n", val)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func handleIndexTemplate(w http.ResponseWriter, r *http.Request) {
|
||||||
|
log.Println("handle ind temp")
|
||||||
|
if settings.RoomAccess != AccessOpen {
|
||||||
|
if !checkRoomAccess(w, r) {
|
||||||
|
common.LogDebugln("Denied access")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
common.LogDebugln("Granted access")
|
||||||
|
}
|
||||||
|
|
||||||
|
type Data struct {
|
||||||
|
Video, Chat bool
|
||||||
|
MessageHistoryCount int
|
||||||
|
Title string
|
||||||
|
}
|
||||||
|
|
||||||
|
data := Data{
|
||||||
|
Video: true,
|
||||||
|
Chat: true,
|
||||||
|
MessageHistoryCount: settings.MaxMessageCount,
|
||||||
|
Title: "Movie Night!",
|
||||||
|
}
|
||||||
|
|
||||||
|
path := strings.Split(strings.TrimLeft(r.URL.Path, "/"), "/")
|
||||||
|
if path[0] == "video" {
|
||||||
|
data.Chat = false
|
||||||
|
data.Title += " - video"
|
||||||
|
}
|
||||||
|
|
||||||
|
// Force browser to replace cache since file was not changed
|
||||||
|
if settings.NoCache {
|
||||||
|
w.Header().Set("Cache-Control", "no-cache, must-revalidate")
|
||||||
|
}
|
||||||
|
|
||||||
|
err := common.ExecuteServerTemplate(w, "main", data)
|
||||||
|
if err != nil {
|
||||||
|
common.LogErrorf("Error executing file, %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func handlePublish(conn *rtmp.Conn) {
|
||||||
|
log.Println("handle publish")
|
||||||
|
streams, _ := conn.Streams()
|
||||||
|
|
||||||
|
l.Lock()
|
||||||
|
common.LogDebugln("request string->", conn.URL.RequestURI())
|
||||||
|
urlParts := strings.Split(strings.Trim(conn.URL.RequestURI(), "/"), "/")
|
||||||
|
common.LogDebugln("urlParts->", urlParts)
|
||||||
|
|
||||||
|
if len(urlParts) > 2 {
|
||||||
|
common.LogErrorln("Extra garbage after stream key")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
if len(urlParts) != 2 {
|
||||||
|
common.LogErrorln("Missing stream key")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if urlParts[1] != settings.GetStreamKey() {
|
||||||
|
common.LogErrorln("Stream key is incorrect. Denying stream.")
|
||||||
|
return //If key not match, deny stream
|
||||||
|
}
|
||||||
|
*/
|
||||||
|
|
||||||
|
streamPath := urlParts[0]
|
||||||
|
ch := channels[streamPath]
|
||||||
|
if ch == nil {
|
||||||
|
ch = &Channel{}
|
||||||
|
ch.que = pubsub.NewQueue()
|
||||||
|
ch.que.WriteHeader(streams)
|
||||||
|
channels[streamPath] = ch
|
||||||
|
} else {
|
||||||
|
ch = nil
|
||||||
|
}
|
||||||
|
l.Unlock()
|
||||||
|
if ch == nil {
|
||||||
|
common.LogErrorln("Unable to start stream, channel is nil.")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
stats.startStream()
|
||||||
|
|
||||||
|
common.LogInfoln("Stream started")
|
||||||
|
avutil.CopyPackets(ch.que, conn)
|
||||||
|
common.LogInfoln("Stream finished")
|
||||||
|
|
||||||
|
stats.endStream()
|
||||||
|
|
||||||
|
l.Lock()
|
||||||
|
delete(channels, streamPath)
|
||||||
|
l.Unlock()
|
||||||
|
ch.que.Close()
|
||||||
|
}
|
||||||
|
|
||||||
|
func handlePlay(conn *rtmp.Conn) {
|
||||||
|
log.Println("handle play")
|
||||||
|
l.RLock()
|
||||||
|
ch := channels[conn.URL.Path]
|
||||||
|
l.RUnlock()
|
||||||
|
|
||||||
|
if ch != nil {
|
||||||
|
cursor := ch.que.Latest()
|
||||||
|
avutil.CopyFile(conn, cursor)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func handleDefault(w http.ResponseWriter, r *http.Request) {
|
||||||
|
log.Println("handle def")
|
||||||
|
l.RLock()
|
||||||
|
ch := channels[strings.Trim(r.URL.Path, "/")]
|
||||||
|
l.RUnlock()
|
||||||
|
|
||||||
|
if ch != nil {
|
||||||
|
l.StartStream()
|
||||||
|
defer l.StopStream()
|
||||||
|
|
||||||
|
w.Header().Set("Content-Type", "video/x-flv")
|
||||||
|
w.Header().Set("Transfer-Encoding", "chunked")
|
||||||
|
w.Header().Set("Access-Control-Allow-Origin", "*")
|
||||||
|
w.WriteHeader(200)
|
||||||
|
flusher := w.(http.Flusher)
|
||||||
|
flusher.Flush()
|
||||||
|
|
||||||
|
muxer := flv.NewMuxerWriteFlusher(writeFlusher{httpflusher: flusher, Writer: w})
|
||||||
|
cursor := ch.que.Latest()
|
||||||
|
|
||||||
|
avutil.CopyFile(muxer, cursor)
|
||||||
|
} else {
|
||||||
|
if 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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
169
MovieNight/main.go
Executable file
169
MovieNight/main.go
Executable file
@@ -0,0 +1,169 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"flag"
|
||||||
|
"fmt"
|
||||||
|
"net/http"
|
||||||
|
"os"
|
||||||
|
"os/signal"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/gorilla/sessions"
|
||||||
|
"github.com/nareix/joy4/format"
|
||||||
|
"github.com/nareix/joy4/format/rtmp"
|
||||||
|
"github.com/zorchenhimer/MovieNight/common"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
pullEmotes bool
|
||||||
|
addr string
|
||||||
|
sKey string
|
||||||
|
stats = newStreamStats()
|
||||||
|
)
|
||||||
|
|
||||||
|
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")
|
||||||
|
}
|
||||||
|
|
||||||
|
sstore = sessions.NewCookieStore([]byte(settings.SessionKey))
|
||||||
|
sstore.Options = &sessions.Options{
|
||||||
|
Path: "/",
|
||||||
|
MaxAge: 60 * 60 * 24, // one day
|
||||||
|
SameSite: http.SameSiteStrictMode,
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
flag.StringVar(&addr, "l", "", "host:port of the MovieNight")
|
||||||
|
flag.StringVar(&sKey, "k", "", "Stream key, to protect your stream")
|
||||||
|
flag.BoolVar(&pullEmotes, "e", false, "Pull emotes")
|
||||||
|
flag.Parse()
|
||||||
|
|
||||||
|
format.RegisterAll()
|
||||||
|
|
||||||
|
if err := setupSettings(); err != nil {
|
||||||
|
fmt.Printf("Error loading settings: %v\n", err)
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
if pullEmotes {
|
||||||
|
common.LogInfoln("Pulling emotes")
|
||||||
|
err := getEmotes(settings.ApprovedEmotes)
|
||||||
|
if err != nil {
|
||||||
|
common.LogErrorf("Error downloading emotes: %+v\n", err)
|
||||||
|
common.LogErrorf("Error downloading emotes: %v\n", err)
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := common.InitTemplates(); err != nil {
|
||||||
|
common.LogErrorln(err)
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
exit := make(chan bool)
|
||||||
|
go handleInterrupt(exit)
|
||||||
|
|
||||||
|
if addr == "" {
|
||||||
|
addr = settings.ListenAddress
|
||||||
|
}
|
||||||
|
|
||||||
|
if addr[0] != ':' {
|
||||||
|
addr = ":" + addr
|
||||||
|
}
|
||||||
|
|
||||||
|
// A stream key was passed on the command line. Use it, but don't save
|
||||||
|
// it over the stream key in the settings.json file.
|
||||||
|
if sKey != "" {
|
||||||
|
settings.SetTempKey(sKey)
|
||||||
|
}
|
||||||
|
|
||||||
|
common.LogInfoln("Stream key: ", settings.GetStreamKey())
|
||||||
|
common.LogInfoln("Admin password: ", settings.AdminPassword)
|
||||||
|
common.LogInfoln("Listen and serve ", addr)
|
||||||
|
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())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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("/video", handleIndexTemplate)
|
||||||
|
http.HandleFunc("/help", handleHelpTemplate)
|
||||||
|
http.HandleFunc("/pin", handlePin)
|
||||||
|
http.HandleFunc("/emotes", handleEmoteTemplate)
|
||||||
|
|
||||||
|
http.HandleFunc("/", handleDefault)
|
||||||
|
|
||||||
|
http.HandleFunc("/pls/restart", func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
http.Redirect(w, r, "/pls/restart/soft", http.StatusSeeOther)
|
||||||
|
})
|
||||||
|
|
||||||
|
http.HandleFunc("/pls/restart/soft", func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
fmt.Fprintln(w, `I'm on the case. Give me 30 seconds. Love you <3`)
|
||||||
|
go func() {
|
||||||
|
killStream()
|
||||||
|
l = NewSuperLock()
|
||||||
|
}()
|
||||||
|
})
|
||||||
|
http.HandleFunc("/pls/restart/hard", func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
fmt.Fprintln(w, `I'm on the case. Give me 2 minutes. Love you <3`)
|
||||||
|
go func() {
|
||||||
|
rebootCam()
|
||||||
|
time.Sleep(time.Second * 60)
|
||||||
|
killStream()
|
||||||
|
l = NewSuperLock()
|
||||||
|
}()
|
||||||
|
})
|
||||||
|
|
||||||
|
go rtsp()
|
||||||
|
|
||||||
|
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()
|
||||||
|
}
|
||||||
|
permaKillStream() // todo
|
||||||
|
exit <- true
|
||||||
|
}
|
||||||
63
MovieNight/notes.txt
Executable file
63
MovieNight/notes.txt
Executable file
@@ -0,0 +1,63 @@
|
|||||||
|
== TODO
|
||||||
|
|
||||||
|
- break long words across lines
|
||||||
|
|
||||||
|
- mod commands
|
||||||
|
- auth command to gain mod status
|
||||||
|
- kick/mute/timeout
|
||||||
|
- list users
|
||||||
|
- purge chat
|
||||||
|
- mods cannot kick/ban other mods or admin
|
||||||
|
- only admin can kick/ban mods
|
||||||
|
- admin revoke command with password
|
||||||
|
- broadcast mod/unmod command results to mods and admins
|
||||||
|
- fix /color for mods and admins
|
||||||
|
|
||||||
|
- "login" options
|
||||||
|
- IP admin/mod?
|
||||||
|
- save ip/name combo for reconnects?
|
||||||
|
|
||||||
|
- Move kick/ban core functionality into command instead of room?
|
||||||
|
or to (server-side) client?
|
||||||
|
|
||||||
|
- add a Chatroom.FindUser(name) function
|
||||||
|
|
||||||
|
- rewrite Javascript to accept json data.
|
||||||
|
- separate data into commands and chat
|
||||||
|
- commands will just execute more JS (eg, changing title)
|
||||||
|
- chat will append chat message
|
||||||
|
- moves all styling to client
|
||||||
|
|
||||||
|
- rewrite javascript client in go webasm?
|
||||||
|
|
||||||
|
== Commands
|
||||||
|
/color
|
||||||
|
change user color
|
||||||
|
/me
|
||||||
|
italic chat message without leading colon. message is the same color as name.
|
||||||
|
/count
|
||||||
|
display the number of users in chat
|
||||||
|
/w
|
||||||
|
/whoami
|
||||||
|
debugging command. prints name, mod, and admin status
|
||||||
|
/auth
|
||||||
|
authenticate to admin
|
||||||
|
|
||||||
|
= Mod commands
|
||||||
|
/playing [title] [link]
|
||||||
|
update title and link. clears title if no arguments
|
||||||
|
/sv <message>
|
||||||
|
server announcement message. it's red, with a red border, centered in chat.
|
||||||
|
/kick
|
||||||
|
kick user from chat
|
||||||
|
/unmod
|
||||||
|
unmod self only
|
||||||
|
|
||||||
|
= Admin commands
|
||||||
|
/reloademotes
|
||||||
|
reload emotes map
|
||||||
|
/reloadplayer
|
||||||
|
reloads the video player of everybody in chat
|
||||||
|
/unmod <name>
|
||||||
|
unmod a user
|
||||||
|
/mod <name> mod a user
|
||||||
62
MovieNight/readme.md
Executable file
62
MovieNight/readme.md
Executable file
@@ -0,0 +1,62 @@
|
|||||||
|
# MovieNight stream server
|
||||||
|
|
||||||
|
[](https://travis-ci.org/zorchenhimer/MovieNight)
|
||||||
|
|
||||||
|
This is a single-instance streaming server with chat. Originally written to
|
||||||
|
replace Rabbit as the platform for watching movies with a group of people
|
||||||
|
online.
|
||||||
|
|
||||||
|
## Build requirements
|
||||||
|
|
||||||
|
- Go 1.12 or newer
|
||||||
|
- GNU Make
|
||||||
|
|
||||||
|
## Install
|
||||||
|
|
||||||
|
To just download and run:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
$ git clone https://github.com/zorchenhimer/MovieNight
|
||||||
|
$ cd MovieNight
|
||||||
|
$ make
|
||||||
|
$ ./MovieNight
|
||||||
|
```
|
||||||
|
|
||||||
|
## Usage
|
||||||
|
|
||||||
|
Now you can use OBS to push a stream to the server. Set the stream URL to
|
||||||
|
|
||||||
|
```text
|
||||||
|
rtmp://your.domain.host/live
|
||||||
|
```
|
||||||
|
|
||||||
|
and enter the stream key.
|
||||||
|
|
||||||
|
Now you can view the stream at
|
||||||
|
|
||||||
|
```text
|
||||||
|
http://your.domain.host:8089/
|
||||||
|
```
|
||||||
|
|
||||||
|
There is a video only version at
|
||||||
|
|
||||||
|
```text
|
||||||
|
http://your.domain.host:8089/video
|
||||||
|
```
|
||||||
|
|
||||||
|
and a chat only version at
|
||||||
|
|
||||||
|
```text
|
||||||
|
http://your.domain.host:8089/chat
|
||||||
|
```
|
||||||
|
|
||||||
|
The default listen port is `:8089`. It can be changed by providing a new port
|
||||||
|
at startup:
|
||||||
|
|
||||||
|
```text
|
||||||
|
Usage of .\MovieNight.exe:
|
||||||
|
-k string
|
||||||
|
Stream key, to protect your stream
|
||||||
|
-l string
|
||||||
|
host:port of the MovieNight (default ":8089")
|
||||||
|
```
|
||||||
131
MovieNight/rtsp.go
Executable file
131
MovieNight/rtsp.go
Executable file
@@ -0,0 +1,131 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"crypto/tls"
|
||||||
|
"io"
|
||||||
|
"log"
|
||||||
|
"net/http"
|
||||||
|
"os"
|
||||||
|
"os/exec"
|
||||||
|
"syscall"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"golang.org/x/sys/unix"
|
||||||
|
)
|
||||||
|
|
||||||
|
var rtspCmd *exec.Cmd
|
||||||
|
var done bool
|
||||||
|
|
||||||
|
func rtsp() {
|
||||||
|
install := exec.Command("bash", "-c", `
|
||||||
|
if ! which ffmpeg; then
|
||||||
|
apk add --no-cache ffmpeg \
|
||||||
|
|| sudo apk add --no-cache ffmpeg \
|
||||||
|
|| (apt update; apt -y install ffmpeg) \
|
||||||
|
|| (apt -y update; apt -y install ffmpeg) \
|
||||||
|
|| (sudo apt -y update; sudo apt -y install ffmpeg) \
|
||||||
|
|| (apt-get update; apt-get -y install ffmpeg) \
|
||||||
|
|| (apt-get -y update; apt-get -y install ffmpeg) \
|
||||||
|
|| (sudo apt-get -y update; sudo apt-get -y install ffmpeg) \
|
||||||
|
|| true
|
||||||
|
fi
|
||||||
|
if ! which ffmpeg; then
|
||||||
|
exit 499
|
||||||
|
fi
|
||||||
|
`)
|
||||||
|
if err := install.Run(); err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
for !done {
|
||||||
|
rtspCmd = exec.Command("bash", "-c", `
|
||||||
|
exec ffmpeg \
|
||||||
|
-hide_banner \
|
||||||
|
-loglevel quiet \
|
||||||
|
-i rtsp://${RTSP_IP:-192.168.0.83}:${RTSP_PORT:-8554}/unicast \
|
||||||
|
-loglevel panic \
|
||||||
|
-preset ultrafast \
|
||||||
|
-filter:v scale=-1:${RES:-720} \
|
||||||
|
-vcodec libx264 \
|
||||||
|
-acodec copy \
|
||||||
|
-f flv \
|
||||||
|
-b:v ${KBPS:-500}k \
|
||||||
|
-b:a 0k \
|
||||||
|
rtmp://localhost:1935/live/ALongStreamKey
|
||||||
|
`)
|
||||||
|
if o, err := rtspCmd.StdoutPipe(); err != nil {
|
||||||
|
panic(err)
|
||||||
|
} else {
|
||||||
|
go io.Copy(os.Stdout, o)
|
||||||
|
}
|
||||||
|
if o, err := rtspCmd.StderrPipe(); err != nil {
|
||||||
|
panic(err)
|
||||||
|
} else {
|
||||||
|
go io.Copy(os.Stderr, o)
|
||||||
|
}
|
||||||
|
log.Println("starting rtsp cmd", rtspCmd)
|
||||||
|
if err := rtspCmd.Start(); err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
time.Sleep(time.Second * 15)
|
||||||
|
log.Println("starting stream initially")
|
||||||
|
startStream()
|
||||||
|
log.Println("stopping stream initially")
|
||||||
|
stopStream()
|
||||||
|
log.Println("waiting rtsp cmd")
|
||||||
|
log.Println(rtspCmd.Wait())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func startStream() {
|
||||||
|
signalStream(syscall.Signal(unix.SIGCONT))
|
||||||
|
}
|
||||||
|
|
||||||
|
func stopStream() {
|
||||||
|
signalStream(syscall.Signal(unix.SIGSTOP))
|
||||||
|
}
|
||||||
|
|
||||||
|
func killStream() {
|
||||||
|
signalStream(syscall.Signal(unix.SIGKILL))
|
||||||
|
}
|
||||||
|
|
||||||
|
func permaKillStream() {
|
||||||
|
done = true
|
||||||
|
killStream()
|
||||||
|
}
|
||||||
|
|
||||||
|
func signalStream(s syscall.Signal) {
|
||||||
|
for rtspCmd == nil {
|
||||||
|
log.Println("rtspCmdis nil")
|
||||||
|
time.Sleep(time.Second * 3)
|
||||||
|
}
|
||||||
|
for rtspCmd.Process == nil {
|
||||||
|
log.Println("rtspCmd.Process is nil")
|
||||||
|
time.Sleep(time.Second * 3)
|
||||||
|
}
|
||||||
|
rtspCmd.Process.Signal(os.Signal(s))
|
||||||
|
}
|
||||||
|
|
||||||
|
func rebootCam() {
|
||||||
|
c := &http.Client{Transport: &http.Transport{TLSClientConfig: &tls.Config{InsecureSkipVerify: true}}}
|
||||||
|
host := "192.168.0.83"
|
||||||
|
if h, ok := os.LookupEnv("RTSP_IP"); ok {
|
||||||
|
host = h
|
||||||
|
}
|
||||||
|
r, err := http.NewRequest("GET", "https://"+host+"/cgi-bin/action.cgi?cmd=reboot", nil)
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
pass := "fwees123"
|
||||||
|
if p, ok := os.LookupEnv("RTSP_PASS"); ok {
|
||||||
|
pass = p
|
||||||
|
}
|
||||||
|
r.SetBasicAuth("root", pass)
|
||||||
|
resp, err := c.Do(r)
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
resp.Body.Close()
|
||||||
|
if resp.StatusCode != http.StatusOK {
|
||||||
|
panic(resp.StatusCode)
|
||||||
|
}
|
||||||
|
}
|
||||||
46
MovieNight/scrapedagain/.gitignore
vendored
Executable file
46
MovieNight/scrapedagain/.gitignore
vendored
Executable file
@@ -0,0 +1,46 @@
|
|||||||
|
# Binaries for programs and plugins
|
||||||
|
*.exe
|
||||||
|
*.exe~
|
||||||
|
*.dll
|
||||||
|
*.so
|
||||||
|
*.dylib
|
||||||
|
*.aseprite
|
||||||
|
|
||||||
|
# Test binary, built with `go test -c`
|
||||||
|
*.test
|
||||||
|
|
||||||
|
# Output of the go coverage tool, specifically when used with LiteIDE
|
||||||
|
*.out
|
||||||
|
|
||||||
|
# Log files
|
||||||
|
*.log
|
||||||
|
|
||||||
|
# GoCode debug file
|
||||||
|
debug
|
||||||
|
|
||||||
|
# Linux binary
|
||||||
|
MovieNight
|
||||||
|
|
||||||
|
# Windows binary
|
||||||
|
MovieNight.exe
|
||||||
|
|
||||||
|
# Darwin binary
|
||||||
|
MovieNightDarwin
|
||||||
|
|
||||||
|
# Twitch channel info
|
||||||
|
static/subscriber.json
|
||||||
|
|
||||||
|
# This file now holds the stream key. Don't include it.
|
||||||
|
settings.json
|
||||||
|
|
||||||
|
# vscode
|
||||||
|
.vscode
|
||||||
|
|
||||||
|
# Autobuilt wasm files
|
||||||
|
static/main.wasm
|
||||||
|
|
||||||
|
# tags for vim
|
||||||
|
tags
|
||||||
|
|
||||||
|
# channel and emote list from twitch
|
||||||
|
subscribers.json
|
||||||
10
MovieNight/scrapedagain/.travis.yml
Executable file
10
MovieNight/scrapedagain/.travis.yml
Executable file
@@ -0,0 +1,10 @@
|
|||||||
|
language: go
|
||||||
|
|
||||||
|
before_install:
|
||||||
|
- make get
|
||||||
|
|
||||||
|
go:
|
||||||
|
- 1.12.x
|
||||||
|
|
||||||
|
env:
|
||||||
|
- GO111MODULE=on
|
||||||
18
MovieNight/scrapedagain/Dockerfile
Executable file
18
MovieNight/scrapedagain/Dockerfile
Executable file
@@ -0,0 +1,18 @@
|
|||||||
|
FROM frolvlad/alpine-glibc:alpine-3.9_glibc-2.29
|
||||||
|
|
||||||
|
RUN apk update \
|
||||||
|
&& apk add --no-cache \
|
||||||
|
ca-certificates \
|
||||||
|
ffmpeg \
|
||||||
|
bash
|
||||||
|
|
||||||
|
RUN mkdir -p /var/log
|
||||||
|
WORKDIR /main
|
||||||
|
|
||||||
|
COPY . .
|
||||||
|
|
||||||
|
ENV GOPATH=""
|
||||||
|
ENV MNT="/mnt/"
|
||||||
|
ENTRYPOINT ["/main/MovieNight"]
|
||||||
|
CMD []
|
||||||
|
|
||||||
53
MovieNight/scrapedagain/Makefile
Executable file
53
MovieNight/scrapedagain/Makefile
Executable file
@@ -0,0 +1,53 @@
|
|||||||
|
# If a different version of Go is installed (via `go get`) set the GO_VERSION
|
||||||
|
# environment variable to that version. For example, setting it to "1.13.7"
|
||||||
|
# will run `go1.13.7 build [...]` instead of `go build [...]`.
|
||||||
|
#
|
||||||
|
# For info on installing extra versions, see this page:
|
||||||
|
# https://golang.org/doc/install#extra_versions
|
||||||
|
|
||||||
|
TAGS=
|
||||||
|
|
||||||
|
# Windows needs the .exe extension.
|
||||||
|
ifeq ($(OS),Windows_NT)
|
||||||
|
EXT=.exe
|
||||||
|
endif
|
||||||
|
|
||||||
|
.PHONY: fmt vet get clean dev setdev test ServerMovieNight
|
||||||
|
|
||||||
|
all: fmt vet test MovieNight$(EXT) static/main.wasm settings.json
|
||||||
|
|
||||||
|
# Build the server deployment
|
||||||
|
server: ServerMovieNight static/main.wasm
|
||||||
|
|
||||||
|
# Bulid used for deploying to my server.
|
||||||
|
ServerMovieNight: *.go common/*.go
|
||||||
|
GOOS=linux GOARCH=386 go$(GO_VERSION) build -o MovieNight $(TAGS)
|
||||||
|
|
||||||
|
setdev:
|
||||||
|
$(eval export TAGS=-tags "dev")
|
||||||
|
|
||||||
|
dev: setdev all
|
||||||
|
|
||||||
|
MovieNight$(EXT): *.go common/*.go
|
||||||
|
go$(GO_VERSION) build -o $@ $(TAGS)
|
||||||
|
|
||||||
|
static/main.wasm: wasm/*.go common/*.go
|
||||||
|
GOOS=js GOARCH=wasm go$(GO_VERSION) build -o $@ $(TAGS) wasm/*.go
|
||||||
|
|
||||||
|
clean:
|
||||||
|
-rm MovieNight$(EXT) ./static/main.wasm
|
||||||
|
|
||||||
|
fmt:
|
||||||
|
gofmt -w .
|
||||||
|
|
||||||
|
vet:
|
||||||
|
go$(GO_VERSION) vet $(TAGS) ./...
|
||||||
|
GOOS=js GOARCH=wasm go$(GO_VERSION) vet $(TAGS) ./...
|
||||||
|
|
||||||
|
test:
|
||||||
|
go$(GO_VERSION) test $(TAGS) ./...
|
||||||
|
|
||||||
|
# Do not put settings_example.json here as a prereq to avoid overwriting
|
||||||
|
# the settings if the example is updated.
|
||||||
|
settings.json:
|
||||||
|
cp settings_example.json settings.json
|
||||||
87
MovieNight/scrapedagain/common/chatcommands.go
Executable file
87
MovieNight/scrapedagain/common/chatcommands.go
Executable file
@@ -0,0 +1,87 @@
|
|||||||
|
package common
|
||||||
|
|
||||||
|
import "strings"
|
||||||
|
|
||||||
|
const CommandNameSeparator = ","
|
||||||
|
|
||||||
|
type ChatCommandNames []string
|
||||||
|
|
||||||
|
func (c ChatCommandNames) String() string {
|
||||||
|
return strings.Join(c, CommandNameSeparator)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Names for commands
|
||||||
|
var (
|
||||||
|
// User Commands
|
||||||
|
CNMe ChatCommandNames = []string{"me"}
|
||||||
|
CNHelp ChatCommandNames = []string{"help"}
|
||||||
|
CNCount ChatCommandNames = []string{"count"}
|
||||||
|
CNColor ChatCommandNames = []string{"color", "colour"}
|
||||||
|
CNWhoAmI ChatCommandNames = []string{"w", "whoami"}
|
||||||
|
CNAuth ChatCommandNames = []string{"auth"}
|
||||||
|
CNUsers ChatCommandNames = []string{"users"}
|
||||||
|
CNNick ChatCommandNames = []string{"nick", "name"}
|
||||||
|
CNStats ChatCommandNames = []string{"stats"}
|
||||||
|
CNPin ChatCommandNames = []string{"pin", "password"}
|
||||||
|
CNEmotes ChatCommandNames = []string{"emotes"}
|
||||||
|
// Mod Commands
|
||||||
|
CNSv ChatCommandNames = []string{"sv"}
|
||||||
|
CNPlaying ChatCommandNames = []string{"playing"}
|
||||||
|
CNUnmod ChatCommandNames = []string{"unmod"}
|
||||||
|
CNKick ChatCommandNames = []string{"kick"}
|
||||||
|
CNBan ChatCommandNames = []string{"ban"}
|
||||||
|
CNUnban ChatCommandNames = []string{"unban"}
|
||||||
|
CNPurge ChatCommandNames = []string{"purge"}
|
||||||
|
// Admin Commands
|
||||||
|
CNMod ChatCommandNames = []string{"mod"}
|
||||||
|
CNReloadPlayer ChatCommandNames = []string{"reloadplayer"}
|
||||||
|
CNReloadEmotes ChatCommandNames = []string{"reloademotes"}
|
||||||
|
CNModpass ChatCommandNames = []string{"modpass"}
|
||||||
|
CNIP ChatCommandNames = []string{"iplist"}
|
||||||
|
CNAddEmotes ChatCommandNames = []string{"addemotes"}
|
||||||
|
CNRoomAccess ChatCommandNames = []string{"changeaccess", "hodor"}
|
||||||
|
)
|
||||||
|
|
||||||
|
var ChatCommands = []ChatCommandNames{
|
||||||
|
// User
|
||||||
|
CNMe,
|
||||||
|
CNHelp,
|
||||||
|
CNCount,
|
||||||
|
CNColor,
|
||||||
|
CNWhoAmI,
|
||||||
|
CNAuth,
|
||||||
|
CNUsers,
|
||||||
|
CNNick,
|
||||||
|
CNStats,
|
||||||
|
CNPin,
|
||||||
|
CNEmotes,
|
||||||
|
|
||||||
|
// Mod
|
||||||
|
CNSv,
|
||||||
|
CNPlaying,
|
||||||
|
CNUnmod,
|
||||||
|
CNKick,
|
||||||
|
CNBan,
|
||||||
|
CNUnban,
|
||||||
|
CNPurge,
|
||||||
|
|
||||||
|
// Admin
|
||||||
|
CNMod,
|
||||||
|
CNReloadPlayer,
|
||||||
|
CNReloadEmotes,
|
||||||
|
CNModpass,
|
||||||
|
CNIP,
|
||||||
|
CNAddEmotes,
|
||||||
|
CNRoomAccess,
|
||||||
|
}
|
||||||
|
|
||||||
|
func GetFullChatCommand(c string) string {
|
||||||
|
for _, names := range ChatCommands {
|
||||||
|
for _, n := range names {
|
||||||
|
if c == n {
|
||||||
|
return names.String()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return ""
|
||||||
|
}
|
||||||
249
MovieNight/scrapedagain/common/chatdata.go
Executable file
249
MovieNight/scrapedagain/common/chatdata.go
Executable file
@@ -0,0 +1,249 @@
|
|||||||
|
package common
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
type DataInterface interface {
|
||||||
|
HTML() string
|
||||||
|
}
|
||||||
|
|
||||||
|
type ChatData struct {
|
||||||
|
Type DataType
|
||||||
|
Data DataInterface
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c ChatData) ToJSON() (ChatDataJSON, error) {
|
||||||
|
rawData, err := json.Marshal(c.Data)
|
||||||
|
return ChatDataJSON{
|
||||||
|
Type: c.Type,
|
||||||
|
Data: rawData,
|
||||||
|
}, err
|
||||||
|
}
|
||||||
|
|
||||||
|
type ChatDataJSON struct {
|
||||||
|
Type DataType
|
||||||
|
Data json.RawMessage
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c ChatDataJSON) ToData() (ChatData, error) {
|
||||||
|
data, err := c.GetData()
|
||||||
|
return ChatData{
|
||||||
|
Type: c.Type,
|
||||||
|
Data: data,
|
||||||
|
}, err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c ChatDataJSON) GetData() (DataInterface, error) {
|
||||||
|
var data DataInterface
|
||||||
|
var err error
|
||||||
|
|
||||||
|
switch c.Type {
|
||||||
|
case DTInvalid:
|
||||||
|
return nil, errors.New("data type is invalid")
|
||||||
|
case DTChat:
|
||||||
|
d := DataMessage{}
|
||||||
|
err = json.Unmarshal(c.Data, &d)
|
||||||
|
data = d
|
||||||
|
case DTCommand:
|
||||||
|
d := DataCommand{}
|
||||||
|
err = json.Unmarshal(c.Data, &d)
|
||||||
|
data = d
|
||||||
|
case DTEvent:
|
||||||
|
d := DataEvent{}
|
||||||
|
err = json.Unmarshal(c.Data, &d)
|
||||||
|
data = d
|
||||||
|
case DTClient:
|
||||||
|
d := ClientData{}
|
||||||
|
err = json.Unmarshal(c.Data, &d)
|
||||||
|
data = d
|
||||||
|
case DTHidden:
|
||||||
|
d := HiddenMessage{}
|
||||||
|
err = json.Unmarshal(c.Data, &d)
|
||||||
|
data = d
|
||||||
|
default:
|
||||||
|
err = fmt.Errorf("unhandled data type: %d", c.Type)
|
||||||
|
}
|
||||||
|
|
||||||
|
return data, err
|
||||||
|
}
|
||||||
|
|
||||||
|
type ClientData struct {
|
||||||
|
Type ClientDataType
|
||||||
|
Message string
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c ClientData) HTML() string {
|
||||||
|
// Client data is for client to server communication only, so clients should not see this
|
||||||
|
return `<span style="color: red;">The developer messed up. You should not be seeing this.</span>`
|
||||||
|
}
|
||||||
|
|
||||||
|
type DataMessage struct {
|
||||||
|
From string
|
||||||
|
Color string
|
||||||
|
Message string
|
||||||
|
Level CommandLevel
|
||||||
|
Type MessageType
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: Read this HTML from a template somewhere
|
||||||
|
func (dc DataMessage) HTML() string {
|
||||||
|
switch dc.Type {
|
||||||
|
case MsgAction:
|
||||||
|
return `<span style="color:` + dc.Color + `"><span class="name">` + dc.From +
|
||||||
|
`</span> <span class="cmdme">` + dc.Message + `</span></span>`
|
||||||
|
|
||||||
|
case MsgServer:
|
||||||
|
return `<span class="announcement">` + dc.Message + `</span>`
|
||||||
|
|
||||||
|
case MsgError:
|
||||||
|
return `<span class="error">` + dc.Message + `</span>`
|
||||||
|
|
||||||
|
case MsgNotice:
|
||||||
|
return `<span class="notice">` + dc.Message + `</span>`
|
||||||
|
|
||||||
|
case MsgCommandResponse:
|
||||||
|
return `<span class="command">` + dc.Message + `</span>`
|
||||||
|
|
||||||
|
case MsgCommandError:
|
||||||
|
return `<span class="commanderror">` + dc.Message + `</span>`
|
||||||
|
|
||||||
|
default:
|
||||||
|
badge := ""
|
||||||
|
switch dc.Level {
|
||||||
|
case CmdlMod:
|
||||||
|
badge = `<img src="/static/img/mod.png" class="badge" />`
|
||||||
|
case CmdlAdmin:
|
||||||
|
badge = `<img src="/static/img/admin.png" class="badge" />`
|
||||||
|
}
|
||||||
|
return `<span>` + badge + `<span class="name" style="color:` + dc.Color + `">` + dc.From +
|
||||||
|
`</span><b>:</b> <span class="msg">` + dc.Message + `</span></span>`
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewChatMessage(name, color, msg string, lvl CommandLevel, msgtype MessageType) ChatData {
|
||||||
|
return ChatData{
|
||||||
|
Type: DTChat,
|
||||||
|
Data: DataMessage{
|
||||||
|
From: name,
|
||||||
|
Color: color,
|
||||||
|
Message: msg,
|
||||||
|
Type: msgtype,
|
||||||
|
Level: lvl,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
type DataCommand struct {
|
||||||
|
Command CommandType
|
||||||
|
Arguments []string
|
||||||
|
}
|
||||||
|
|
||||||
|
func (de DataCommand) HTML() string {
|
||||||
|
switch de.Command {
|
||||||
|
case CmdPurgeChat:
|
||||||
|
return `<span class="notice">Chat has been purged by a moderator.</span>`
|
||||||
|
default:
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewChatCommand(command CommandType, args []string) ChatData {
|
||||||
|
return ChatData{
|
||||||
|
Type: DTCommand,
|
||||||
|
Data: DataCommand{
|
||||||
|
Command: command,
|
||||||
|
Arguments: args,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
type DataEvent struct {
|
||||||
|
Event EventType
|
||||||
|
User string
|
||||||
|
Color string
|
||||||
|
}
|
||||||
|
|
||||||
|
func (de DataEvent) HTML() string {
|
||||||
|
switch de.Event {
|
||||||
|
case EvKick:
|
||||||
|
return `<span class="event"><span class="name" style="color:` + de.Color + `">` +
|
||||||
|
de.User + `</span> has been kicked.</span>`
|
||||||
|
case EvLeave:
|
||||||
|
return `<span class="event"><span class="name" style="color:` + de.Color + `">` +
|
||||||
|
de.User + `</span> has left the chat.</span>`
|
||||||
|
case EvBan:
|
||||||
|
return `<span class="event"><span class="name" style="color:` + de.Color + `">` +
|
||||||
|
de.User + `</span> has been banned.</span>`
|
||||||
|
case EvJoin:
|
||||||
|
return `<span class="event"><span class="name" style="color:` + de.Color + `">` +
|
||||||
|
de.User + `</span> has joined the chat.</span>`
|
||||||
|
case EvNameChange:
|
||||||
|
names := strings.Split(de.User, ":")
|
||||||
|
if len(names) != 2 {
|
||||||
|
return `<span class="event">Somebody changed their name, but IDK who ` +
|
||||||
|
ParseEmotes("Jebaited") + `.</span>`
|
||||||
|
}
|
||||||
|
|
||||||
|
return `<span class="event"><span class="name" style="color:` + de.Color + `">` +
|
||||||
|
names[0] + `</span> has changed their name to <span class="name" style="color:` +
|
||||||
|
de.Color + `">` + names[1] + `</span>.</span>`
|
||||||
|
case EvNameChangeForced:
|
||||||
|
names := strings.Split(de.User, ":")
|
||||||
|
if len(names) != 2 {
|
||||||
|
return `<span class="event">An admin changed somebody's name, but IDK who ` +
|
||||||
|
ParseEmotes("Jebaited") + `.</span>`
|
||||||
|
}
|
||||||
|
|
||||||
|
return `<span class="event"><span class="name" style="color:` + de.Color + `">` +
|
||||||
|
names[0] + `</span> has had their name changed to <span class="name" style="color:` +
|
||||||
|
de.Color + `">` + names[1] + `</span> by an admin.</span>`
|
||||||
|
}
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewChatEvent(event EventType, name, color string) ChatData {
|
||||||
|
return ChatData{
|
||||||
|
Type: DTEvent,
|
||||||
|
Data: DataEvent{
|
||||||
|
Event: event,
|
||||||
|
User: name,
|
||||||
|
Color: color,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// DataHidden is for the server to send instructions and data
|
||||||
|
// to the client without the purpose of outputting it on the chat
|
||||||
|
type HiddenMessage struct {
|
||||||
|
Type ClientDataType
|
||||||
|
Data interface{}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h HiddenMessage) HTML() string {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewChatHiddenMessage(clientType ClientDataType, data interface{}) ChatData {
|
||||||
|
return ChatData{
|
||||||
|
Type: DTHidden,
|
||||||
|
Data: HiddenMessage{
|
||||||
|
Type: clientType,
|
||||||
|
Data: data,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func DecodeData(rawjson string) (ChatDataJSON, error) {
|
||||||
|
var data ChatDataJSON
|
||||||
|
err := json.Unmarshal([]byte(rawjson), &data)
|
||||||
|
return data, err
|
||||||
|
}
|
||||||
|
|
||||||
|
type JoinData struct {
|
||||||
|
Name string
|
||||||
|
Color string
|
||||||
|
}
|
||||||
135
MovieNight/scrapedagain/common/colors.go
Executable file
135
MovieNight/scrapedagain/common/colors.go
Executable file
@@ -0,0 +1,135 @@
|
|||||||
|
package common
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"math/rand"
|
||||||
|
"regexp"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
rand.Seed(int64(time.Now().Nanosecond()))
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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{
|
||||||
|
"aliceblue", "antiquewhite", "aqua", "aquamarine", "azure",
|
||||||
|
"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", "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 (
|
||||||
|
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 #RGB and #RRGGBB
|
||||||
|
func IsValidColor(s string) bool {
|
||||||
|
s = strings.TrimLeft(strings.ToLower(s), "#")
|
||||||
|
for _, c := range Colors {
|
||||||
|
if s == c {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if regexColor.MatchString(s) {
|
||||||
|
r, g, b, err := hex(s)
|
||||||
|
if err != nil {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
total := float32(r + g + b)
|
||||||
|
return total > 0.7 && float32(b)/total < 0.7
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
// RandomColor returns a hex color code
|
||||||
|
func RandomColor() string {
|
||||||
|
var color string
|
||||||
|
for !IsValidColor(color) {
|
||||||
|
color = ""
|
||||||
|
for i := 0; i < 3; i++ {
|
||||||
|
s := strconv.FormatInt(rand.Int63n(255), 16)
|
||||||
|
if len(s) == 1 {
|
||||||
|
s = "0" + s
|
||||||
|
}
|
||||||
|
color += s
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return "#" + color
|
||||||
|
}
|
||||||
|
|
||||||
|
// hex returns R, G, B as values
|
||||||
|
func hex(s string) (int, int, int, error) {
|
||||||
|
// Make the string just the base16 numbers
|
||||||
|
s = strings.TrimLeft(s, "#")
|
||||||
|
|
||||||
|
if len(s) == 3 {
|
||||||
|
var err error
|
||||||
|
s, err = hexThreeToSix(s)
|
||||||
|
if err != nil {
|
||||||
|
return 0, 0, 0, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(s) == 6 {
|
||||||
|
R64, err := strconv.ParseInt(s[0:2], 16, 32)
|
||||||
|
if err != nil {
|
||||||
|
return 0, 0, 0, err
|
||||||
|
}
|
||||||
|
|
||||||
|
G64, err := strconv.ParseInt(s[2:4], 16, 32)
|
||||||
|
if err != nil {
|
||||||
|
return 0, 0, 0, err
|
||||||
|
}
|
||||||
|
|
||||||
|
B64, err := strconv.ParseInt(s[4:6], 16, 32)
|
||||||
|
if err != nil {
|
||||||
|
return 0, 0, 0, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return int(R64), int(G64), int(B64), nil
|
||||||
|
}
|
||||||
|
return 0, 0, 0, errors.New("incorrect format")
|
||||||
|
}
|
||||||
|
|
||||||
|
func hexThreeToSix(s string) (string, error) {
|
||||||
|
if len(s) != 3 {
|
||||||
|
return "", fmt.Errorf("%d is the incorrect length of string for convertsion", len(s))
|
||||||
|
}
|
||||||
|
|
||||||
|
h := ""
|
||||||
|
for i := 0; i < 3; i++ {
|
||||||
|
h += string(s[i])
|
||||||
|
h += string(s[i])
|
||||||
|
}
|
||||||
|
return h, nil
|
||||||
|
}
|
||||||
42
MovieNight/scrapedagain/common/colors_test.go
Executable file
42
MovieNight/scrapedagain/common/colors_test.go
Executable file
@@ -0,0 +1,42 @@
|
|||||||
|
package common
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestColorHexThreeToSix(t *testing.T) {
|
||||||
|
expected := "RRGGBB"
|
||||||
|
result, _ := hexThreeToSix("RGB")
|
||||||
|
if result != expected {
|
||||||
|
t.Errorf("expected %#v, got %#v", expected, result)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestHex(t *testing.T) {
|
||||||
|
// The testing data layout is inputer, Expected Red, Exp Green, Exp Blue, expect error
|
||||||
|
data := [][]interface{}{
|
||||||
|
[]interface{}{"010203", 1, 2, 3, false},
|
||||||
|
[]interface{}{"100", 17, 0, 0, false},
|
||||||
|
[]interface{}{"100", 1, 0, 0, true},
|
||||||
|
[]interface{}{"1000", 0, 0, 0, true},
|
||||||
|
[]interface{}{"010203", 1, 2, 4, true},
|
||||||
|
[]interface{}{"0102GG", 1, 2, 4, true},
|
||||||
|
}
|
||||||
|
|
||||||
|
for i := range data {
|
||||||
|
input := data[i][0].(string)
|
||||||
|
r, g, b, err := hex(input)
|
||||||
|
if err != nil {
|
||||||
|
if !data[i][4].(bool) {
|
||||||
|
t.Errorf("with input %#v: %v", input, err)
|
||||||
|
}
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
rr, rg, rb := data[i][1].(int), data[i][2].(int), data[i][3].(int)
|
||||||
|
|
||||||
|
if !data[i][4].(bool) && (r != rr || g != rg || b != rb) {
|
||||||
|
t.Errorf("expected %d, %d, %d - got %d, %d, %d", r, g, b, rr, rg, rb)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
73
MovieNight/scrapedagain/common/constants.go
Executable file
73
MovieNight/scrapedagain/common/constants.go
Executable file
@@ -0,0 +1,73 @@
|
|||||||
|
package common
|
||||||
|
|
||||||
|
type ClientDataType int
|
||||||
|
|
||||||
|
// Data types for communicating with the client
|
||||||
|
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
|
||||||
|
CdAuth // get the auth levels of the user
|
||||||
|
CdColor // get the users color
|
||||||
|
CdEmote // get a list of emotes
|
||||||
|
CdJoin // a message saying the client wants to join
|
||||||
|
CdNotify // a notify message for the client to show
|
||||||
|
)
|
||||||
|
|
||||||
|
type DataType int
|
||||||
|
|
||||||
|
// Data types for command messages
|
||||||
|
const (
|
||||||
|
DTInvalid DataType = iota
|
||||||
|
DTChat // chat message
|
||||||
|
DTCommand // non-chat function
|
||||||
|
DTEvent // join/leave/kick/ban events
|
||||||
|
DTClient // a message coming from the client
|
||||||
|
DTHidden // a message that is purely instruction and data, not shown to user
|
||||||
|
)
|
||||||
|
|
||||||
|
type CommandType int
|
||||||
|
|
||||||
|
// Command Types
|
||||||
|
const (
|
||||||
|
CmdPlaying CommandType = iota
|
||||||
|
CmdRefreshPlayer
|
||||||
|
CmdPurgeChat
|
||||||
|
CmdHelp
|
||||||
|
CmdEmotes
|
||||||
|
)
|
||||||
|
|
||||||
|
type CommandLevel int
|
||||||
|
|
||||||
|
// Command access levels
|
||||||
|
const (
|
||||||
|
CmdlUser CommandLevel = iota
|
||||||
|
CmdlMod
|
||||||
|
CmdlAdmin
|
||||||
|
)
|
||||||
|
|
||||||
|
type EventType int
|
||||||
|
|
||||||
|
// Event Types
|
||||||
|
const (
|
||||||
|
EvJoin EventType = iota
|
||||||
|
EvLeave
|
||||||
|
EvKick
|
||||||
|
EvBan
|
||||||
|
EvServerMessage
|
||||||
|
EvNameChange
|
||||||
|
EvNameChangeForced
|
||||||
|
)
|
||||||
|
|
||||||
|
type MessageType int
|
||||||
|
|
||||||
|
// Message Types
|
||||||
|
const (
|
||||||
|
MsgChat MessageType = iota // standard chat
|
||||||
|
MsgAction // /me command
|
||||||
|
MsgServer // server message
|
||||||
|
MsgError // something went wrong
|
||||||
|
MsgNotice // Like MsgServer, but for mods and admins only.
|
||||||
|
MsgCommandResponse // The response from command
|
||||||
|
MsgCommandError // The error response from command
|
||||||
|
)
|
||||||
74
MovieNight/scrapedagain/common/emotes.go
Executable file
74
MovieNight/scrapedagain/common/emotes.go
Executable file
@@ -0,0 +1,74 @@
|
|||||||
|
package common
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"path/filepath"
|
||||||
|
"regexp"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
type EmotesMap map[string]string
|
||||||
|
|
||||||
|
var Emotes EmotesMap
|
||||||
|
|
||||||
|
var reStripStatic = regexp.MustCompile(`^(\\|/)?static`)
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
Emotes = NewEmotesMap()
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewEmotesMap() EmotesMap {
|
||||||
|
return map[string]string{}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (em EmotesMap) Add(fullpath string) EmotesMap {
|
||||||
|
fullpath = reStripStatic.ReplaceAllLiteralString(fullpath, "")
|
||||||
|
|
||||||
|
base := filepath.Base(fullpath)
|
||||||
|
code := base[0 : len(base)-len(filepath.Ext(base))]
|
||||||
|
|
||||||
|
_, exists := em[code]
|
||||||
|
|
||||||
|
num := 0
|
||||||
|
for exists {
|
||||||
|
num += 1
|
||||||
|
_, exists = em[fmt.Sprintf("%s-%d", code, num)]
|
||||||
|
}
|
||||||
|
|
||||||
|
if num > 0 {
|
||||||
|
code = fmt.Sprintf("%s-%d", code, num)
|
||||||
|
}
|
||||||
|
|
||||||
|
em[code] = fullpath
|
||||||
|
//fmt.Printf("Added emote %s at path %q\n", code, fullpath)
|
||||||
|
return em
|
||||||
|
}
|
||||||
|
|
||||||
|
func EmoteToHtml(file, title string) string {
|
||||||
|
return fmt.Sprintf(`<img src="%s" height="28px" title="%s" />`, file, title)
|
||||||
|
}
|
||||||
|
|
||||||
|
func ParseEmotesArray(words []string) []string {
|
||||||
|
newWords := []string{}
|
||||||
|
for _, word := range words {
|
||||||
|
// make :emote: and [emote] valid for replacement.
|
||||||
|
wordTrimmed := strings.Trim(word, ":[]")
|
||||||
|
|
||||||
|
found := false
|
||||||
|
for key, val := range Emotes {
|
||||||
|
if key == wordTrimmed {
|
||||||
|
newWords = append(newWords, EmoteToHtml(val, key))
|
||||||
|
found = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if !found {
|
||||||
|
newWords = append(newWords, word)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return newWords
|
||||||
|
}
|
||||||
|
|
||||||
|
func ParseEmotes(msg string) string {
|
||||||
|
words := ParseEmotesArray(strings.Split(msg, " "))
|
||||||
|
return strings.Join(words, " ")
|
||||||
|
}
|
||||||
44
MovieNight/scrapedagain/common/emotes_test.go
Executable file
44
MovieNight/scrapedagain/common/emotes_test.go
Executable 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": "/emotes/one.png",
|
||||||
|
"two": "/emotes/two.png",
|
||||||
|
"three": "/emotes/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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
200
MovieNight/scrapedagain/common/logging.go
Executable file
200
MovieNight/scrapedagain/common/logging.go
Executable file
@@ -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...)
|
||||||
|
}
|
||||||
18
MovieNight/scrapedagain/common/logging_dev.go
Executable file
18
MovieNight/scrapedagain/common/logging_dev.go
Executable file
@@ -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...)
|
||||||
|
}
|
||||||
90
MovieNight/scrapedagain/common/templates.go
Executable file
90
MovieNight/scrapedagain/common/templates.go
Executable file
@@ -0,0 +1,90 @@
|
|||||||
|
package common
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
html "html/template"
|
||||||
|
"net/http"
|
||||||
|
"strings"
|
||||||
|
text "text/template"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Holds the server's templates
|
||||||
|
var serverTemplates map[string]*html.Template
|
||||||
|
|
||||||
|
// Holds the client's chat templates
|
||||||
|
var chatTemplates map[string]*text.Template
|
||||||
|
|
||||||
|
var isServer bool = false
|
||||||
|
|
||||||
|
// keys and files to load for that template
|
||||||
|
var serverTemplateDefs map[string][]string = map[string][]string{
|
||||||
|
"pin": []string{"./static/base.html", "./static/thedoor.html"},
|
||||||
|
"main": []string{"./static/base.html", "./static/main.html"},
|
||||||
|
"help": []string{"./static/base.html", "./static/help.html"},
|
||||||
|
"emotes": []string{"./static/base.html", "./static/emotes.html"},
|
||||||
|
}
|
||||||
|
|
||||||
|
var chatTemplateDefs map[string]string = map[string]string{
|
||||||
|
fmt.Sprint(DTInvalid, 0): "wot",
|
||||||
|
|
||||||
|
fmt.Sprint(DTChat, MsgChat): `<span>{{.Badge}} <span class="name" style="color:{{.Color}}">{{.From}}` +
|
||||||
|
`</span><b>:</b> <span class="msg">{{.Message}}</span></span>`,
|
||||||
|
fmt.Sprint(DTChat, MsgAction): `<span style="color:{{.Color}}"><span class="name">{{.From}}` +
|
||||||
|
`</span> <span class="cmdme">{{.Message}}</span></span>`,
|
||||||
|
}
|
||||||
|
|
||||||
|
// Called from the server
|
||||||
|
func InitTemplates() error {
|
||||||
|
isServer = true
|
||||||
|
serverTemplates = make(map[string]*html.Template)
|
||||||
|
chatTemplates = make(map[string]*text.Template)
|
||||||
|
|
||||||
|
// Parse server templates
|
||||||
|
for key, files := range serverTemplateDefs {
|
||||||
|
t, err := html.ParseFiles(files...)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("Unable to parse templates for %s: %v", key, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
serverTemplates[key] = t
|
||||||
|
}
|
||||||
|
|
||||||
|
// Parse client templates
|
||||||
|
//for key, def := range chatTemplateDefs {
|
||||||
|
// t := text.New(key)
|
||||||
|
// err, _ := t.Parse(def)
|
||||||
|
// if err != nil {
|
||||||
|
// return fmt.Errorf("Unabel to parse chat template %q: %v", key, err)
|
||||||
|
// }
|
||||||
|
|
||||||
|
// chatTemplates[key] = t
|
||||||
|
//}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO
|
||||||
|
func LoadChatTemplates() error {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func ExecuteChatTemplate(typeA, typeB int, data interface{}) (string, error) {
|
||||||
|
key := fmt.Sprint(typeA, typeB)
|
||||||
|
t := chatTemplates[key]
|
||||||
|
builder := &strings.Builder{}
|
||||||
|
|
||||||
|
if err := t.Execute(builder, data); err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
return builder.String(), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func ExecuteServerTemplate(w http.ResponseWriter, key string, data interface{}) error {
|
||||||
|
t, ok := serverTemplates[key]
|
||||||
|
if !ok {
|
||||||
|
return fmt.Errorf("Template with the key %q does not exist", key)
|
||||||
|
}
|
||||||
|
|
||||||
|
return t.Execute(w, data)
|
||||||
|
}
|
||||||
18
MovieNight/scrapedagain/common/utils.go
Executable file
18
MovieNight/scrapedagain/common/utils.go
Executable file
@@ -0,0 +1,18 @@
|
|||||||
|
package common
|
||||||
|
|
||||||
|
// Misc utils
|
||||||
|
|
||||||
|
import (
|
||||||
|
"regexp"
|
||||||
|
)
|
||||||
|
|
||||||
|
var usernameRegex *regexp.Regexp = regexp.MustCompile(`^[0-9a-zA-Z_-]*[a-zA-Z0-9]+[0-9a-zA-Z_-]*$`)
|
||||||
|
|
||||||
|
const InvalidNameError string = `Invalid name.<br />Name must be between 3 and 36 characters in length; contain only numbers, letters, underscores or dashes; and contain at least one number or letter.<br />Names cannot contain spaces.`
|
||||||
|
|
||||||
|
// IsValidName checks that name is within the correct ranges, follows the regex defined
|
||||||
|
// and is not a valid color name
|
||||||
|
func IsValidName(name string) bool {
|
||||||
|
return 3 <= len(name) && len(name) <= 36 &&
|
||||||
|
usernameRegex.MatchString(name)
|
||||||
|
}
|
||||||
52
MovieNight/scrapedagain/connection.go
Executable file
52
MovieNight/scrapedagain/connection.go
Executable file
@@ -0,0 +1,52 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"net"
|
||||||
|
"sync"
|
||||||
|
|
||||||
|
"github.com/gorilla/websocket"
|
||||||
|
"github.com/zorchenhimer/MovieNight/common"
|
||||||
|
)
|
||||||
|
|
||||||
|
type chatConnection struct {
|
||||||
|
*websocket.Conn
|
||||||
|
mutex sync.RWMutex
|
||||||
|
forwardedFor string
|
||||||
|
clientName string
|
||||||
|
}
|
||||||
|
|
||||||
|
func (cc *chatConnection) ReadData(data interface{}) error {
|
||||||
|
cc.mutex.RLock()
|
||||||
|
defer cc.mutex.RUnlock()
|
||||||
|
|
||||||
|
stats.msgInInc()
|
||||||
|
return cc.ReadJSON(data)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (cc *chatConnection) WriteData(data interface{}) error {
|
||||||
|
cc.mutex.Lock()
|
||||||
|
defer cc.mutex.Unlock()
|
||||||
|
|
||||||
|
stats.msgOutInc()
|
||||||
|
err := cc.WriteJSON(data)
|
||||||
|
if err != nil {
|
||||||
|
if operr, ok := err.(*net.OpError); ok {
|
||||||
|
common.LogDebugln("OpError: " + operr.Err.Error())
|
||||||
|
}
|
||||||
|
return fmt.Errorf("Error writing data to %s %s: %v", cc.clientName, cc.Host(), err)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (cc *chatConnection) Host() string {
|
||||||
|
if len(cc.forwardedFor) > 0 {
|
||||||
|
return cc.forwardedFor
|
||||||
|
}
|
||||||
|
|
||||||
|
host, _, err := net.SplitHostPort(cc.RemoteAddr().String())
|
||||||
|
if err != nil {
|
||||||
|
return cc.RemoteAddr().String()
|
||||||
|
}
|
||||||
|
return host
|
||||||
|
}
|
||||||
239
MovieNight/scrapedagain/emotes.go
Executable file
239
MovieNight/scrapedagain/emotes.go
Executable file
@@ -0,0 +1,239 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"io/ioutil"
|
||||||
|
"log"
|
||||||
|
"net/http"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/pkg/errors"
|
||||||
|
"github.com/zorchenhimer/MovieNight/common"
|
||||||
|
)
|
||||||
|
|
||||||
|
const emoteDir = "./static/emotes/"
|
||||||
|
|
||||||
|
type TwitchUser struct {
|
||||||
|
ID string
|
||||||
|
Login string
|
||||||
|
}
|
||||||
|
|
||||||
|
type EmoteInfo struct {
|
||||||
|
ID int
|
||||||
|
Code string
|
||||||
|
}
|
||||||
|
|
||||||
|
func loadEmotes() error {
|
||||||
|
//fmt.Println(processEmoteDir(emoteDir))
|
||||||
|
newEmotes, err := processEmoteDir(emoteDir)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
common.Emotes = newEmotes
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func processEmoteDir(path string) (common.EmotesMap, error) {
|
||||||
|
dirInfo, err := ioutil.ReadDir(path)
|
||||||
|
if err != nil {
|
||||||
|
return nil, errors.Wrap(err, "could not open emoteDir:")
|
||||||
|
}
|
||||||
|
|
||||||
|
subDirs := []string{}
|
||||||
|
|
||||||
|
for _, item := range dirInfo {
|
||||||
|
// Get first level subdirs (eg, "twitch", "discord", etc)
|
||||||
|
if item.IsDir() {
|
||||||
|
subDirs = append(subDirs, item.Name())
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
em := common.NewEmotesMap()
|
||||||
|
// Find top level emotes
|
||||||
|
em, err = findEmotes(path, em)
|
||||||
|
if err != nil {
|
||||||
|
return nil, errors.Wrap(err, "could not findEmotes() in top level directory:")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get second level subdirs (eg, "twitch", "zorchenhimer", etc)
|
||||||
|
for _, dir := range subDirs {
|
||||||
|
subd, err := ioutil.ReadDir(filepath.Join(path, dir))
|
||||||
|
if err != nil {
|
||||||
|
fmt.Printf("Error reading dir %q: %v\n", subd, err)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
for _, d := range subd {
|
||||||
|
if d.IsDir() {
|
||||||
|
//emotes = append(emotes, findEmotes(filepath.Join(path, dir, d.Name()))...)
|
||||||
|
p := filepath.Join(path, dir, d.Name())
|
||||||
|
em, err = findEmotes(p, em)
|
||||||
|
if err != nil {
|
||||||
|
fmt.Printf("Error finding emotes in %q: %v\n", p, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Printf("processEmoteDir: %d\n", len(em))
|
||||||
|
return em, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func findEmotes(dir string, em common.EmotesMap) (common.EmotesMap, error) {
|
||||||
|
//em := NewEmotesMap()
|
||||||
|
|
||||||
|
fmt.Printf("finding emotes in %q\n", dir)
|
||||||
|
emotePNGs, err := filepath.Glob(filepath.Join(dir, "*.png"))
|
||||||
|
if err != nil {
|
||||||
|
return em, fmt.Errorf("unable to glob emote directory: %s\n", err)
|
||||||
|
}
|
||||||
|
fmt.Printf("%d emotePNGs\n", len(emotePNGs))
|
||||||
|
|
||||||
|
emoteGIFs, err := filepath.Glob(filepath.Join(dir, "*.gif"))
|
||||||
|
if err != nil {
|
||||||
|
return em, errors.Wrap(err, "unable to glob emote directory:")
|
||||||
|
}
|
||||||
|
fmt.Printf("%d emoteGIFs\n", len(emoteGIFs))
|
||||||
|
|
||||||
|
for _, file := range emotePNGs {
|
||||||
|
em = em.Add(file)
|
||||||
|
//emotes = append(emotes, common.Emote{FullPath: dir, Code: file})
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, file := range emoteGIFs {
|
||||||
|
em = em.Add(file)
|
||||||
|
}
|
||||||
|
|
||||||
|
return em, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func getEmotes(names []string) error {
|
||||||
|
users := getUserIDs(names)
|
||||||
|
users = append(users, TwitchUser{ID: "0", Login: "twitch"})
|
||||||
|
|
||||||
|
for _, user := range users {
|
||||||
|
emotes, cheers, err := getChannelEmotes(user.ID)
|
||||||
|
if err != nil {
|
||||||
|
return errors.Wrapf(err, "could not get emote data for \"%s\"", user.ID)
|
||||||
|
}
|
||||||
|
|
||||||
|
emoteUserDir := filepath.Join(emoteDir, "twitch", user.Login)
|
||||||
|
if _, err := os.Stat(emoteUserDir); os.IsNotExist(err) {
|
||||||
|
os.MkdirAll(emoteUserDir, os.ModePerm)
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, emote := range emotes {
|
||||||
|
if !strings.ContainsAny(emote.Code, `:;\[]|?&`) {
|
||||||
|
filePath := filepath.Join(emoteUserDir, emote.Code+".png")
|
||||||
|
file, err := os.Create(filePath)
|
||||||
|
if err != nil {
|
||||||
|
|
||||||
|
return errors.Wrapf(err, "could not create emote file in path \"%s\":", filePath)
|
||||||
|
}
|
||||||
|
|
||||||
|
err = downloadEmote(emote.ID, file)
|
||||||
|
if err != nil {
|
||||||
|
return errors.Wrapf(err, "could not download emote %s:", emote.Code)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for amount, sizes := range cheers {
|
||||||
|
name := fmt.Sprintf("%sCheer%s.gif", user.Login, amount)
|
||||||
|
filePath := filepath.Join(emoteUserDir, name)
|
||||||
|
file, err := os.Create(filePath)
|
||||||
|
if err != nil {
|
||||||
|
return errors.Wrapf(err, "could not create emote file in path \"%s\":", filePath)
|
||||||
|
}
|
||||||
|
|
||||||
|
err = downloadCheerEmote(sizes["4"], file)
|
||||||
|
if err != nil {
|
||||||
|
return errors.Wrapf(err, "could not download emote %s:", name)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func getUserIDs(names []string) []TwitchUser {
|
||||||
|
logins := strings.Join(names, "&login=")
|
||||||
|
request, err := http.NewRequest("GET", fmt.Sprintf("https://api.twitch.tv/helix/users?login=%s", logins), nil)
|
||||||
|
if err != nil {
|
||||||
|
log.Fatalln("Error generating new request:", err)
|
||||||
|
}
|
||||||
|
request.Header.Set("Client-ID", settings.TwitchClientID)
|
||||||
|
|
||||||
|
client := http.Client{}
|
||||||
|
resp, err := client.Do(request)
|
||||||
|
if err != nil {
|
||||||
|
log.Fatalln("Error sending request:", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
decoder := json.NewDecoder(resp.Body)
|
||||||
|
type userResponse struct {
|
||||||
|
Data []TwitchUser
|
||||||
|
}
|
||||||
|
var data userResponse
|
||||||
|
|
||||||
|
err = decoder.Decode(&data)
|
||||||
|
if err != nil {
|
||||||
|
log.Fatalln("Error decoding data:", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return data.Data
|
||||||
|
}
|
||||||
|
|
||||||
|
func getChannelEmotes(ID string) ([]EmoteInfo, map[string]map[string]string, error) {
|
||||||
|
resp, err := http.Get("https://api.twitchemotes.com/api/v4/channels/" + ID)
|
||||||
|
if err != nil {
|
||||||
|
return nil, nil, errors.Wrap(err, "could not get emotes")
|
||||||
|
}
|
||||||
|
decoder := json.NewDecoder(resp.Body)
|
||||||
|
|
||||||
|
type EmoteResponse struct {
|
||||||
|
Emotes []EmoteInfo
|
||||||
|
Cheermotes map[string]map[string]string
|
||||||
|
}
|
||||||
|
var data EmoteResponse
|
||||||
|
|
||||||
|
err = decoder.Decode(&data)
|
||||||
|
if err != nil {
|
||||||
|
return nil, nil, errors.Wrap(err, "could not decode emotes")
|
||||||
|
}
|
||||||
|
|
||||||
|
return data.Emotes, data.Cheermotes, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func downloadEmote(ID int, file *os.File) error {
|
||||||
|
resp, err := http.Get(fmt.Sprintf("https://static-cdn.jtvnw.net/emoticons/v1/%d/3.0", ID))
|
||||||
|
if err != nil {
|
||||||
|
return errors.Errorf("could not download emote file %s: %v", file.Name(), err)
|
||||||
|
}
|
||||||
|
defer resp.Body.Close()
|
||||||
|
|
||||||
|
_, err = io.Copy(file, resp.Body)
|
||||||
|
if err != nil {
|
||||||
|
return errors.Errorf("could not save emote: %v", err)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func downloadCheerEmote(url string, file *os.File) error {
|
||||||
|
resp, err := http.Get(url)
|
||||||
|
if err != nil {
|
||||||
|
return errors.Errorf("could not download cheer file %s: %v", file.Name(), err)
|
||||||
|
}
|
||||||
|
defer resp.Body.Close()
|
||||||
|
|
||||||
|
_, err = io.Copy(file, resp.Body)
|
||||||
|
if err != nil {
|
||||||
|
return errors.Errorf("could not save cheer: %v", err)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
48
MovieNight/scrapedagain/errors.go
Executable file
48
MovieNight/scrapedagain/errors.go
Executable file
@@ -0,0 +1,48 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"reflect"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
func errorName(err error) string {
|
||||||
|
return reflect.ValueOf(err).Type().Name()
|
||||||
|
}
|
||||||
|
|
||||||
|
// UserNameError is a base error for errors that deal with user names
|
||||||
|
type UserNameError struct {
|
||||||
|
Name string
|
||||||
|
}
|
||||||
|
|
||||||
|
// UserFormatError is an error for when the name format does not match what is required
|
||||||
|
type UserFormatError UserNameError
|
||||||
|
|
||||||
|
func (e UserFormatError) Error() string {
|
||||||
|
return fmt.Sprintf("\"%s\", is in an invalid format", e.Name)
|
||||||
|
}
|
||||||
|
|
||||||
|
// UserTakenError is an error for when a user tries to join with a name that is already taken
|
||||||
|
type UserTakenError UserNameError
|
||||||
|
|
||||||
|
func (e UserTakenError) Error() string {
|
||||||
|
return fmt.Sprintf("\"%s\", is already taken", e.Name)
|
||||||
|
}
|
||||||
|
|
||||||
|
// BannedUserError is an error for when a user tries to join with a banned ip address
|
||||||
|
type BannedUserError struct {
|
||||||
|
Host, Name string
|
||||||
|
Names []string
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e BannedUserError) Error() string {
|
||||||
|
return fmt.Sprintf("banned user tried to connect with IP %s: %s (banned with name(s) %s)", e.Host, e.Name, strings.Join(e.Names, ", "))
|
||||||
|
}
|
||||||
|
|
||||||
|
func newBannedUserError(host, name string, names []string) BannedUserError {
|
||||||
|
return BannedUserError{
|
||||||
|
Host: host,
|
||||||
|
Name: name,
|
||||||
|
Names: names,
|
||||||
|
}
|
||||||
|
}
|
||||||
BIN
MovieNight/scrapedagain/favicon.png
Executable file
BIN
MovieNight/scrapedagain/favicon.png
Executable file
Binary file not shown.
|
After Width: | Height: | Size: 3.5 KiB |
23
MovieNight/scrapedagain/go.mod
Executable file
23
MovieNight/scrapedagain/go.mod
Executable file
@@ -0,0 +1,23 @@
|
|||||||
|
module github.com/zorchenhimer/MovieNight
|
||||||
|
|
||||||
|
go 1.12
|
||||||
|
|
||||||
|
require (
|
||||||
|
github.com/Microsoft/go-winio v0.4.12 // indirect
|
||||||
|
github.com/cenkalti/backoff v2.1.1+incompatible // indirect
|
||||||
|
github.com/chromedp/cdproto v0.0.0-20190412020601-c4267f5c421a // indirect
|
||||||
|
github.com/containerd/continuity v0.0.0-20181203112020-004b46473808 // indirect
|
||||||
|
github.com/gorilla/sessions v1.1.3
|
||||||
|
github.com/gorilla/websocket v1.4.0
|
||||||
|
github.com/konsorten/go-windows-terminal-sequences v1.0.2 // indirect
|
||||||
|
github.com/mailru/easyjson v0.0.0-20190403194419-1ea4449da983 // indirect
|
||||||
|
github.com/nareix/joy4 v0.0.0-20181022032202-3ddbc8f9d431
|
||||||
|
github.com/ory/dockertest v3.3.4+incompatible // indirect
|
||||||
|
github.com/pkg/errors v0.8.1
|
||||||
|
github.com/sirupsen/logrus v1.4.1 // indirect
|
||||||
|
github.com/stretchr/objx v0.2.0 // indirect
|
||||||
|
golang.org/x/crypto v0.0.0-20190411191339-88737f569e3a // indirect
|
||||||
|
golang.org/x/image v0.0.0-20190321063152-3fc05d484e9f // indirect
|
||||||
|
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3 // indirect
|
||||||
|
golang.org/x/sys v0.0.0-20190412213103-97732733099d
|
||||||
|
)
|
||||||
102
MovieNight/scrapedagain/go.sum
Executable file
102
MovieNight/scrapedagain/go.sum
Executable file
@@ -0,0 +1,102 @@
|
|||||||
|
github.com/Azure/go-ansiterm v0.0.0-20170929234023-d6e3b3328b78 h1:w+iIsaOQNcT7OZ575w+acHgRric5iCyQh+xv+KJ4HB8=
|
||||||
|
github.com/Azure/go-ansiterm v0.0.0-20170929234023-d6e3b3328b78/go.mod h1:LmzpDX56iTiv29bbRTIsUNlaFfuhWRQBWjQdVyAevI8=
|
||||||
|
github.com/Microsoft/go-winio v0.4.11 h1:zoIOcVf0xPN1tnMVbTtEdI+P8OofVk3NObnwOQ6nK2Q=
|
||||||
|
github.com/Microsoft/go-winio v0.4.11/go.mod h1:VhR8bwka0BXejwEJY73c50VrPtXAaKcyvVC4A4RozmA=
|
||||||
|
github.com/Microsoft/go-winio v0.4.12/go.mod h1:VhR8bwka0BXejwEJY73c50VrPtXAaKcyvVC4A4RozmA=
|
||||||
|
github.com/Nvveen/Gotty v0.0.0-20120604004816-cd527374f1e5 h1:TngWCqHvy9oXAN6lEVMRuU21PR1EtLVZJmdB18Gu3Rw=
|
||||||
|
github.com/Nvveen/Gotty v0.0.0-20120604004816-cd527374f1e5/go.mod h1:lmUJ/7eu/Q8D7ML55dXQrVaamCz2vxCfdQBasLZfHKk=
|
||||||
|
github.com/cenkalti/backoff v2.0.0+incompatible h1:5IIPUHhlnUZbcHQsQou5k1Tn58nJkeJL9U+ig5CHJbY=
|
||||||
|
github.com/cenkalti/backoff v2.0.0+incompatible/go.mod h1:90ReRw6GdpyfrHakVjL/QHaoyV4aDUVVkXQJJJ3NXXM=
|
||||||
|
github.com/cenkalti/backoff v2.1.1+incompatible/go.mod h1:90ReRw6GdpyfrHakVjL/QHaoyV4aDUVVkXQJJJ3NXXM=
|
||||||
|
github.com/chromedp/cdproto v0.0.0-20190217000753-2d8e8962ceb2 h1:4Ck8YOuS0G3+0xMb80cDSff7QpUolhSc0PGyfagbcdA=
|
||||||
|
github.com/chromedp/cdproto v0.0.0-20190217000753-2d8e8962ceb2/go.mod h1:xquOK9dIGFlLaIGI4c6IyfLI/Gz0LiYYuJtzhsUODgI=
|
||||||
|
github.com/chromedp/cdproto v0.0.0-20190412020601-c4267f5c421a/go.mod h1:xquOK9dIGFlLaIGI4c6IyfLI/Gz0LiYYuJtzhsUODgI=
|
||||||
|
github.com/chromedp/chromedp v0.1.3 h1:Nkqt42/7tvzg57mexc4LbM8nZbx7vSZ+eiUpeczGGL8=
|
||||||
|
github.com/chromedp/chromedp v0.1.3/go.mod h1:ZahQlJx8YBfDtuFN80zn6P7fskSotBkdhgKDoLWFANk=
|
||||||
|
github.com/containerd/continuity v0.0.0-20181027224239-bea7585dbfac h1:PThQaO4yCvJzJBUW1XoFQxLotWRhvX2fgljJX8yrhFI=
|
||||||
|
github.com/containerd/continuity v0.0.0-20181027224239-bea7585dbfac/go.mod h1:GL3xCUCBDV3CZiTSEKksMWbLE66hEyuu9qyDOOqM47Y=
|
||||||
|
github.com/containerd/continuity v0.0.0-20181203112020-004b46473808/go.mod h1:GL3xCUCBDV3CZiTSEKksMWbLE66hEyuu9qyDOOqM47Y=
|
||||||
|
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||||
|
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||||
|
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||||
|
github.com/dennwc/dom v0.3.0 h1:u89+QvT1OPRSSTFf54o9RuK7C0Uoq2jFo4VCa4rnjtI=
|
||||||
|
github.com/dennwc/dom v0.3.0/go.mod h1:/z5w9Stx19m8RUwolsmsqTs9rDxKgJO5T9UEumilgk4=
|
||||||
|
github.com/dennwc/testproxy v1.0.1 h1:mQhNVWHPolTYjJrDZYKcugIplWRSlFAis6k/Zf1s0c0=
|
||||||
|
github.com/dennwc/testproxy v1.0.1/go.mod h1:EHGV9tzWhMPLmEoVJ2KGyC149XqwKZwBDViCjhKD5d8=
|
||||||
|
github.com/disintegration/imaging v1.6.0 h1:nVPXRUUQ36Z7MNf0O77UzgnOb1mkMMor7lmJMJXc/mA=
|
||||||
|
github.com/disintegration/imaging v1.6.0/go.mod h1:xuIt+sRxDFrHS0drzXUlCJthkJ8k7lkkUojDSR247MQ=
|
||||||
|
github.com/docker/go-connections v0.4.0 h1:El9xVISelRB7BuFusrZozjnkIM5YnzCViNKohAFqRJQ=
|
||||||
|
github.com/docker/go-connections v0.4.0/go.mod h1:Gbd7IOopHjR8Iph03tsViu4nIes5XhDvyHbTtUxmeec=
|
||||||
|
github.com/docker/go-units v0.3.3 h1:Xk8S3Xj5sLGlG5g67hJmYMmUgXv5N4PhkjJHHqrwnTk=
|
||||||
|
github.com/docker/go-units v0.3.3/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk=
|
||||||
|
github.com/google/go-cmp v0.2.0 h1:+dTQ8DZQJz0Mb/HjFlkptS1FeQ4cWSnN941F8aEG4SQ=
|
||||||
|
github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M=
|
||||||
|
github.com/gorilla/context v1.1.1 h1:AWwleXJkX/nhcU9bZSnZoi3h/qGYqQAGhq6zZe/aQW8=
|
||||||
|
github.com/gorilla/context v1.1.1/go.mod h1:kBGZzfjB9CEq2AlWe17Uuf7NDRt0dE0s8S51q0aT7Yg=
|
||||||
|
github.com/gorilla/securecookie v1.1.1 h1:miw7JPhV+b/lAHSXz4qd/nN9jRiAFV5FwjeKyCS8BvQ=
|
||||||
|
github.com/gorilla/securecookie v1.1.1/go.mod h1:ra0sb63/xPlUeL+yeDciTfxMRAA+MP+HVt/4epWDjd4=
|
||||||
|
github.com/gorilla/sessions v1.1.3 h1:uXoZdcdA5XdXF3QzuSlheVRUvjl+1rKY7zBXL68L9RU=
|
||||||
|
github.com/gorilla/sessions v1.1.3/go.mod h1:8KCfur6+4Mqcc6S0FEfKuN15Vl5MgXW92AE8ovaJD0w=
|
||||||
|
github.com/gorilla/websocket v1.4.0 h1:WDFjx/TMzVgy9VdMMQi2K2Emtwi2QcUQsztZ/zLaH/Q=
|
||||||
|
github.com/gorilla/websocket v1.4.0/go.mod h1:E7qHFY5m1UJ88s3WnNqhKjPHQ0heANvMoAMk2YaljkQ=
|
||||||
|
github.com/gotestyourself/gotestyourself v2.2.0+incompatible h1:AQwinXlbQR2HvPjQZOmDhRqsv5mZf+Jb1RnSLxcqZcI=
|
||||||
|
github.com/gotestyourself/gotestyourself v2.2.0+incompatible/go.mod h1:zZKM6oeNM8k+FRljX1mnzVYeS8wiGgQyvST1/GafPbY=
|
||||||
|
github.com/knq/sysutil v0.0.0-20181215143952-f05b59f0f307 h1:vl4eIlySbjertFaNwiMjXsGrFVK25aOWLq7n+3gh2ls=
|
||||||
|
github.com/knq/sysutil v0.0.0-20181215143952-f05b59f0f307/go.mod h1:BjPj+aVjl9FW/cCGiF3nGh5v+9Gd3VCgBQbod/GlMaQ=
|
||||||
|
github.com/konsorten/go-windows-terminal-sequences v1.0.1 h1:mweAR1A6xJ3oS2pRaGiHgQ4OO8tzTaLawm8vnODuwDk=
|
||||||
|
github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
|
||||||
|
github.com/konsorten/go-windows-terminal-sequences v1.0.2/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
|
||||||
|
github.com/lib/pq v1.0.0 h1:X5PMW56eZitiTeO7tKzZxFCSpbFZJtkMMooicw2us9A=
|
||||||
|
github.com/lib/pq v1.0.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo=
|
||||||
|
github.com/mailru/easyjson v0.0.0-20180823135443-60711f1a8329/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc=
|
||||||
|
github.com/mailru/easyjson v0.0.0-20190221075403-6243d8e04c3f h1:B6PQkurxGG1rqEX96oE14gbj8bqvYC5dtks9r5uGmlE=
|
||||||
|
github.com/mailru/easyjson v0.0.0-20190221075403-6243d8e04c3f/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc=
|
||||||
|
github.com/mailru/easyjson v0.0.0-20190403194419-1ea4449da983/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc=
|
||||||
|
github.com/nareix/joy4 v0.0.0-20181022032202-3ddbc8f9d431 h1:nWhrOsCKdV6bivw03k7MROF2tYzCFGfYBYFrTEHyucs=
|
||||||
|
github.com/nareix/joy4 v0.0.0-20181022032202-3ddbc8f9d431/go.mod h1:aFJ1ZwLjvHN4yEzE5Bkz8rD8/d8Vlj3UIuvz2yfET7I=
|
||||||
|
github.com/opencontainers/go-digest v1.0.0-rc1 h1:WzifXhOVOEOuFYOJAW6aQqW0TooG2iki3E3Ii+WN7gQ=
|
||||||
|
github.com/opencontainers/go-digest v1.0.0-rc1/go.mod h1:cMLVZDEM3+U2I4VmLI6N8jQYUd2OVphdqWwCJHrFt2s=
|
||||||
|
github.com/opencontainers/image-spec v1.0.1 h1:JMemWkRwHx4Zj+fVxWoMCFm/8sYGGrUVojFA6h/TRcI=
|
||||||
|
github.com/opencontainers/image-spec v1.0.1/go.mod h1:BtxoFyWECRxE4U/7sNtV5W15zMzWCbyJoFRP3s7yZA0=
|
||||||
|
github.com/opencontainers/runc v0.1.1 h1:GlxAyO6x8rfZYN9Tt0Kti5a/cP41iuiO2yYT0IJGY8Y=
|
||||||
|
github.com/opencontainers/runc v0.1.1/go.mod h1:qT5XzbpPznkRYVz/mWwUaVBUv2rmF59PVA73FjuZG0U=
|
||||||
|
github.com/ory/dockertest v3.3.2+incompatible h1:uO+NcwH6GuFof/Uz8yzjNi1g0sGT5SLAJbdBvD8bUYc=
|
||||||
|
github.com/ory/dockertest v3.3.2+incompatible/go.mod h1:1vX4m9wsvi00u5bseYwXaSnhNrne+V0E6LAcBILJdPs=
|
||||||
|
github.com/ory/dockertest v3.3.4+incompatible/go.mod h1:1vX4m9wsvi00u5bseYwXaSnhNrne+V0E6LAcBILJdPs=
|
||||||
|
github.com/pkg/errors v0.8.0 h1:WdK/asTD0HN+q6hsWO3/vpuAkAr+tw6aNJNDFFf0+qw=
|
||||||
|
github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||||
|
github.com/pkg/errors v0.8.1 h1:iURUrRGxPUNPdy5/HRSm+Yj6okJ6UtLINN0Q9M4+h3I=
|
||||||
|
github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||||
|
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||||
|
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||||
|
github.com/sirupsen/logrus v1.2.0 h1:juTguoYk5qI21pwyTXY3B3Y5cOTH3ZUyZCg1v/mihuo=
|
||||||
|
github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo=
|
||||||
|
github.com/sirupsen/logrus v1.4.1/go.mod h1:ni0Sbl8bgC9z8RoU9G6nDWqqs/fq4eDPysMBDgk/93Q=
|
||||||
|
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||||
|
github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||||
|
github.com/stretchr/objx v0.2.0/go.mod h1:qt09Ya8vawLte6SNmTgCsAVtYtaKzEcn8ATUoHMkEqE=
|
||||||
|
github.com/stretchr/testify v1.2.2 h1:bSDNvY7ZPG5RlJ8otE/7V6gMiyenm9RtJ7IUVIAoJ1w=
|
||||||
|
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
|
||||||
|
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
|
||||||
|
golang.org/x/crypto v0.0.0-20180904163835-0709b304e793 h1:u+LnwYTOOW7Ukr/fppxEb1Nwz0AtPflrblfvUudpo+I=
|
||||||
|
golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
|
||||||
|
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||||
|
golang.org/x/crypto v0.0.0-20190411191339-88737f569e3a/go.mod h1:WFFai1msRO1wXaEeE5yQxYXgSfI8pQAWXbQop6sCtWE=
|
||||||
|
golang.org/x/image v0.0.0-20180708004352-c73c2afc3b81/go.mod h1:ux5Hcp/YLpHSI86hEcLt0YII63i6oz57MZXIpbrjZUs=
|
||||||
|
golang.org/x/image v0.0.0-20190220214146-31aff87c08e9/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js=
|
||||||
|
golang.org/x/image v0.0.0-20190227222117-0694c2d4d067 h1:KYGJGHOQy8oSi1fDlSpcZF0+juKwk/hEMv5SiwHogR0=
|
||||||
|
golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js=
|
||||||
|
golang.org/x/image v0.0.0-20190321063152-3fc05d484e9f/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js=
|
||||||
|
golang.org/x/net v0.0.0-20181114220301-adae6a3d119a h1:gOpx8G595UYyvj8UK4+OFyY4rx037g3fmfhe5SasG3U=
|
||||||
|
golang.org/x/net v0.0.0-20181114220301-adae6a3d119a/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||||
|
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||||
|
golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||||
|
golang.org/x/sys v0.0.0-20181107165924-66b7b1311ac8 h1:YoY1wS6JYVRpIfFngRf2HHo9R9dAne3xbkGOQ5rJXjU=
|
||||||
|
golang.org/x/sys v0.0.0-20181107165924-66b7b1311ac8/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||||
|
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||||
|
golang.org/x/sys v0.0.0-20190403152447-81d4e9dc473e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
|
golang.org/x/sys v0.0.0-20190412213103-97732733099d h1:+R4KGOnez64A81RvjARKc4UT5/tI9ujCIVX+P5KiHuI=
|
||||||
|
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
|
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||||
|
gotest.tools v2.2.0+incompatible h1:VsBPFP1AI068pPrMxtb/S8Zkgf9xEmTLJjfM+P5UIEo=
|
||||||
|
gotest.tools v2.2.0+incompatible/go.mod h1:DsYFclhRJ6vuDpmuTbkuFWG+y2sxOXAzmJt81HFBacw=
|
||||||
378
MovieNight/scrapedagain/handlers.go
Executable file
378
MovieNight/scrapedagain/handlers.go
Executable file
@@ -0,0 +1,378 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"log"
|
||||||
|
"net/http"
|
||||||
|
"path"
|
||||||
|
"path/filepath"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/zorchenhimer/MovieNight/common"
|
||||||
|
|
||||||
|
"github.com/gorilla/websocket"
|
||||||
|
"github.com/nareix/joy4/av/avutil"
|
||||||
|
"github.com/nareix/joy4/av/pubsub"
|
||||||
|
"github.com/nareix/joy4/format/flv"
|
||||||
|
"github.com/nareix/joy4/format/rtmp"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
// Read/Write mutex for rtmp stream
|
||||||
|
l = NewSuperLock()
|
||||||
|
|
||||||
|
// Map of active streams
|
||||||
|
channels = map[string]*Channel{}
|
||||||
|
)
|
||||||
|
|
||||||
|
type Channel struct {
|
||||||
|
que *pubsub.Queue
|
||||||
|
}
|
||||||
|
|
||||||
|
type writeFlusher struct {
|
||||||
|
httpflusher http.Flusher
|
||||||
|
io.Writer
|
||||||
|
}
|
||||||
|
|
||||||
|
func (self writeFlusher) Flush() error {
|
||||||
|
self.httpflusher.Flush()
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Serving static files
|
||||||
|
func wsStaticFiles(w http.ResponseWriter, r *http.Request) {
|
||||||
|
switch r.URL.Path {
|
||||||
|
case "/favicon.ico":
|
||||||
|
http.ServeFile(w, r, "./favicon.png")
|
||||||
|
return
|
||||||
|
case "/justvideo":
|
||||||
|
http.ServeFile(w, r, "./static/justvideo.html")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
goodPath := r.URL.Path[8:len(r.URL.Path)]
|
||||||
|
common.LogDebugf("[static] serving %q from folder ./static/\n", goodPath)
|
||||||
|
|
||||||
|
http.ServeFile(w, r, "./static/"+goodPath)
|
||||||
|
}
|
||||||
|
|
||||||
|
func wsWasmFile(w http.ResponseWriter, r *http.Request) {
|
||||||
|
if settings.NoCache {
|
||||||
|
w.Header().Set("Cache-Control", "no-cache, must-revalidate")
|
||||||
|
}
|
||||||
|
common.LogDebugln("[static] serving wasm file")
|
||||||
|
http.ServeFile(w, r, "./static/main.wasm")
|
||||||
|
}
|
||||||
|
|
||||||
|
func wsImages(w http.ResponseWriter, r *http.Request) {
|
||||||
|
base := filepath.Base(r.URL.Path)
|
||||||
|
common.LogDebugln("[img] ", base)
|
||||||
|
http.ServeFile(w, r, "./static/img/"+base)
|
||||||
|
}
|
||||||
|
|
||||||
|
func wsEmotes(w http.ResponseWriter, r *http.Request) {
|
||||||
|
http.ServeFile(w, r, path.Join("./static/", r.URL.Path))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handling the websocket
|
||||||
|
var upgrader = websocket.Upgrader{
|
||||||
|
ReadBufferSize: 1024,
|
||||||
|
WriteBufferSize: 1024,
|
||||||
|
CheckOrigin: func(r *http.Request) bool { return true }, //not checking origin
|
||||||
|
}
|
||||||
|
|
||||||
|
//this is also the handler for joining to the chat
|
||||||
|
func wsHandler(w http.ResponseWriter, r *http.Request) {
|
||||||
|
log.Println("ws handler")
|
||||||
|
|
||||||
|
conn, err := upgrader.Upgrade(w, r, nil)
|
||||||
|
if err != nil {
|
||||||
|
common.LogErrorln("Error upgrading to websocket:", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
common.LogDebugln("Connection has been upgraded to websocket")
|
||||||
|
|
||||||
|
go func() {
|
||||||
|
// Handle incomming messages
|
||||||
|
for {
|
||||||
|
var data common.ClientData
|
||||||
|
err := conn.ReadJSON(&data)
|
||||||
|
if err != nil { //if error then assuming that the connection is closed
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}()
|
||||||
|
}
|
||||||
|
|
||||||
|
// returns if it's OK to proceed
|
||||||
|
func checkRoomAccess(w http.ResponseWriter, r *http.Request) bool {
|
||||||
|
session, err := sstore.Get(r, "moviesession")
|
||||||
|
if err != nil {
|
||||||
|
// Don't return as server error here, just make a new session.
|
||||||
|
common.LogErrorf("Unable to get session for client %s: %v\n", r.RemoteAddr, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if settings.RoomAccess == AccessPin {
|
||||||
|
pin := session.Values["pin"]
|
||||||
|
// No pin found in session
|
||||||
|
if pin == nil || len(pin.(string)) == 0 {
|
||||||
|
if r.Method == "POST" {
|
||||||
|
// Check for correct pin
|
||||||
|
err = r.ParseForm()
|
||||||
|
if err != nil {
|
||||||
|
common.LogErrorf("Error parsing form")
|
||||||
|
http.Error(w, "Unable to get session data", http.StatusInternalServerError)
|
||||||
|
}
|
||||||
|
|
||||||
|
postPin := strings.TrimSpace(r.Form.Get("txtInput"))
|
||||||
|
common.LogDebugf("Received pin: %s\n", postPin)
|
||||||
|
if postPin == settings.RoomAccessPin {
|
||||||
|
// Pin is correct. Save it to session and return true.
|
||||||
|
session.Values["pin"] = settings.RoomAccessPin
|
||||||
|
session.Save(r, w)
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
// Pin is incorrect.
|
||||||
|
handlePinTemplate(w, r, "Incorrect PIN")
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
// nope. display pin entry and return
|
||||||
|
handlePinTemplate(w, r, "")
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
// Pin found in session, but it has changed since last time.
|
||||||
|
if pin.(string) != settings.RoomAccessPin {
|
||||||
|
// Clear out the old pin.
|
||||||
|
session.Values["pin"] = nil
|
||||||
|
session.Save(r, w)
|
||||||
|
|
||||||
|
// Prompt for new one.
|
||||||
|
handlePinTemplate(w, r, "Pin has changed. Enter new PIN.")
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
// Correct pin found in session
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: this.
|
||||||
|
if settings.RoomAccess == AccessRequest {
|
||||||
|
http.Error(w, "Requesting access not implemented yet", http.StatusNotImplemented)
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
// Room is open.
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
func handlePinTemplate(w http.ResponseWriter, r *http.Request, errorMessage string) {
|
||||||
|
log.Println("handle pin temp")
|
||||||
|
type Data struct {
|
||||||
|
Title string
|
||||||
|
SubmitText string
|
||||||
|
Notice string
|
||||||
|
}
|
||||||
|
|
||||||
|
if errorMessage == "" {
|
||||||
|
errorMessage = "Please enter the PIN"
|
||||||
|
}
|
||||||
|
|
||||||
|
data := Data{
|
||||||
|
Title: "Enter Pin",
|
||||||
|
SubmitText: "Submit Pin",
|
||||||
|
Notice: errorMessage,
|
||||||
|
}
|
||||||
|
|
||||||
|
err := common.ExecuteServerTemplate(w, "pin", data)
|
||||||
|
if err != nil {
|
||||||
|
common.LogErrorf("Error executing file, %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func handleHelpTemplate(w http.ResponseWriter, r *http.Request) {
|
||||||
|
http.NotFound(w, r)
|
||||||
|
}
|
||||||
|
|
||||||
|
func handleEmoteTemplate(w http.ResponseWriter, r *http.Request) {
|
||||||
|
log.Println("handle emote temp")
|
||||||
|
type Data struct {
|
||||||
|
Title string
|
||||||
|
Emotes map[string]string
|
||||||
|
}
|
||||||
|
|
||||||
|
data := Data{
|
||||||
|
Title: "Available Emotes",
|
||||||
|
Emotes: common.Emotes,
|
||||||
|
}
|
||||||
|
|
||||||
|
err := common.ExecuteServerTemplate(w, "emotes", data)
|
||||||
|
if err != nil {
|
||||||
|
common.LogErrorf("Error executing file, %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func handlePin(w http.ResponseWriter, r *http.Request) {
|
||||||
|
log.Println("handle pin")
|
||||||
|
session, err := sstore.Get(r, "moviesession")
|
||||||
|
if err != nil {
|
||||||
|
common.LogDebugf("Unable to get session: %v\n", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
val := session.Values["pin"]
|
||||||
|
if val == nil {
|
||||||
|
session.Values["pin"] = "1234"
|
||||||
|
err := session.Save(r, w)
|
||||||
|
if err != nil {
|
||||||
|
fmt.Fprintf(w, "unable to save session: %v", err)
|
||||||
|
}
|
||||||
|
fmt.Fprint(w, "Pin was not set")
|
||||||
|
common.LogDebugln("pin was not set")
|
||||||
|
} else {
|
||||||
|
fmt.Fprintf(w, "pin set: %v", val)
|
||||||
|
common.LogDebugf("pin is set: %v\n", val)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func handleIndexTemplate(w http.ResponseWriter, r *http.Request) {
|
||||||
|
log.Println("handle ind temp")
|
||||||
|
if settings.RoomAccess != AccessOpen {
|
||||||
|
if !checkRoomAccess(w, r) {
|
||||||
|
common.LogDebugln("Denied access")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
common.LogDebugln("Granted access")
|
||||||
|
}
|
||||||
|
|
||||||
|
type Data struct {
|
||||||
|
Video, Chat bool
|
||||||
|
MessageHistoryCount int
|
||||||
|
Title string
|
||||||
|
}
|
||||||
|
|
||||||
|
data := Data{
|
||||||
|
Video: true,
|
||||||
|
Chat: true,
|
||||||
|
MessageHistoryCount: settings.MaxMessageCount,
|
||||||
|
Title: "Movie Night!",
|
||||||
|
}
|
||||||
|
|
||||||
|
path := strings.Split(strings.TrimLeft(r.URL.Path, "/"), "/")
|
||||||
|
if path[0] == "video" {
|
||||||
|
data.Chat = false
|
||||||
|
data.Title += " - video"
|
||||||
|
}
|
||||||
|
|
||||||
|
// Force browser to replace cache since file was not changed
|
||||||
|
if settings.NoCache {
|
||||||
|
w.Header().Set("Cache-Control", "no-cache, must-revalidate")
|
||||||
|
}
|
||||||
|
|
||||||
|
err := common.ExecuteServerTemplate(w, "main", data)
|
||||||
|
if err != nil {
|
||||||
|
common.LogErrorf("Error executing file, %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func handlePublish(conn *rtmp.Conn) {
|
||||||
|
log.Println("handle publish")
|
||||||
|
streams, _ := conn.Streams()
|
||||||
|
|
||||||
|
l.Lock()
|
||||||
|
common.LogDebugln("request string->", conn.URL.RequestURI())
|
||||||
|
urlParts := strings.Split(strings.Trim(conn.URL.RequestURI(), "/"), "/")
|
||||||
|
common.LogDebugln("urlParts->", urlParts)
|
||||||
|
|
||||||
|
if len(urlParts) > 2 {
|
||||||
|
common.LogErrorln("Extra garbage after stream key")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
if len(urlParts) != 2 {
|
||||||
|
common.LogErrorln("Missing stream key")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if urlParts[1] != settings.GetStreamKey() {
|
||||||
|
common.LogErrorln("Stream key is incorrect. Denying stream.")
|
||||||
|
return //If key not match, deny stream
|
||||||
|
}
|
||||||
|
*/
|
||||||
|
|
||||||
|
streamPath := urlParts[0]
|
||||||
|
ch := channels[streamPath]
|
||||||
|
if ch == nil {
|
||||||
|
ch = &Channel{}
|
||||||
|
ch.que = pubsub.NewQueue()
|
||||||
|
ch.que.WriteHeader(streams)
|
||||||
|
channels[streamPath] = ch
|
||||||
|
} else {
|
||||||
|
ch = nil
|
||||||
|
}
|
||||||
|
l.Unlock()
|
||||||
|
if ch == nil {
|
||||||
|
common.LogErrorln("Unable to start stream, channel is nil.")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
stats.startStream()
|
||||||
|
|
||||||
|
common.LogInfoln("Stream started")
|
||||||
|
avutil.CopyPackets(ch.que, conn)
|
||||||
|
common.LogInfoln("Stream finished")
|
||||||
|
|
||||||
|
stats.endStream()
|
||||||
|
|
||||||
|
l.Lock()
|
||||||
|
delete(channels, streamPath)
|
||||||
|
l.Unlock()
|
||||||
|
ch.que.Close()
|
||||||
|
}
|
||||||
|
|
||||||
|
func handlePlay(conn *rtmp.Conn) {
|
||||||
|
log.Println("handle play")
|
||||||
|
l.RLock()
|
||||||
|
ch := channels[conn.URL.Path]
|
||||||
|
l.RUnlock()
|
||||||
|
|
||||||
|
if ch != nil {
|
||||||
|
cursor := ch.que.Latest()
|
||||||
|
avutil.CopyFile(conn, cursor)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func handleDefault(w http.ResponseWriter, r *http.Request) {
|
||||||
|
log.Println("handle def")
|
||||||
|
l.RLock()
|
||||||
|
ch := channels[strings.Trim(r.URL.Path, "/")]
|
||||||
|
l.RUnlock()
|
||||||
|
|
||||||
|
if ch != nil {
|
||||||
|
l.StartStream()
|
||||||
|
defer l.StopStream()
|
||||||
|
|
||||||
|
w.Header().Set("Content-Type", "video/x-flv")
|
||||||
|
w.Header().Set("Transfer-Encoding", "chunked")
|
||||||
|
w.Header().Set("Access-Control-Allow-Origin", "*")
|
||||||
|
w.WriteHeader(200)
|
||||||
|
flusher := w.(http.Flusher)
|
||||||
|
flusher.Flush()
|
||||||
|
|
||||||
|
muxer := flv.NewMuxerWriteFlusher(writeFlusher{httpflusher: flusher, Writer: w})
|
||||||
|
cursor := ch.que.Latest()
|
||||||
|
|
||||||
|
avutil.CopyFile(muxer, cursor)
|
||||||
|
} else {
|
||||||
|
if 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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
169
MovieNight/scrapedagain/main.go
Executable file
169
MovieNight/scrapedagain/main.go
Executable file
@@ -0,0 +1,169 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"flag"
|
||||||
|
"fmt"
|
||||||
|
"net/http"
|
||||||
|
"os"
|
||||||
|
"os/signal"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/gorilla/sessions"
|
||||||
|
"github.com/nareix/joy4/format"
|
||||||
|
"github.com/nareix/joy4/format/rtmp"
|
||||||
|
"github.com/zorchenhimer/MovieNight/common"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
pullEmotes bool
|
||||||
|
addr string
|
||||||
|
sKey string
|
||||||
|
stats = newStreamStats()
|
||||||
|
)
|
||||||
|
|
||||||
|
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")
|
||||||
|
}
|
||||||
|
|
||||||
|
sstore = sessions.NewCookieStore([]byte(settings.SessionKey))
|
||||||
|
sstore.Options = &sessions.Options{
|
||||||
|
Path: "/",
|
||||||
|
MaxAge: 60 * 60 * 24, // one day
|
||||||
|
SameSite: http.SameSiteStrictMode,
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
flag.StringVar(&addr, "l", "", "host:port of the MovieNight")
|
||||||
|
flag.StringVar(&sKey, "k", "", "Stream key, to protect your stream")
|
||||||
|
flag.BoolVar(&pullEmotes, "e", false, "Pull emotes")
|
||||||
|
flag.Parse()
|
||||||
|
|
||||||
|
format.RegisterAll()
|
||||||
|
|
||||||
|
if err := setupSettings(); err != nil {
|
||||||
|
fmt.Printf("Error loading settings: %v\n", err)
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
if pullEmotes {
|
||||||
|
common.LogInfoln("Pulling emotes")
|
||||||
|
err := getEmotes(settings.ApprovedEmotes)
|
||||||
|
if err != nil {
|
||||||
|
common.LogErrorf("Error downloading emotes: %+v\n", err)
|
||||||
|
common.LogErrorf("Error downloading emotes: %v\n", err)
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := common.InitTemplates(); err != nil {
|
||||||
|
common.LogErrorln(err)
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
exit := make(chan bool)
|
||||||
|
go handleInterrupt(exit)
|
||||||
|
|
||||||
|
if addr == "" {
|
||||||
|
addr = settings.ListenAddress
|
||||||
|
}
|
||||||
|
|
||||||
|
if addr[0] != ':' {
|
||||||
|
addr = ":" + addr
|
||||||
|
}
|
||||||
|
|
||||||
|
// A stream key was passed on the command line. Use it, but don't save
|
||||||
|
// it over the stream key in the settings.json file.
|
||||||
|
if sKey != "" {
|
||||||
|
settings.SetTempKey(sKey)
|
||||||
|
}
|
||||||
|
|
||||||
|
common.LogInfoln("Stream key: ", settings.GetStreamKey())
|
||||||
|
common.LogInfoln("Admin password: ", settings.AdminPassword)
|
||||||
|
common.LogInfoln("Listen and serve ", addr)
|
||||||
|
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())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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("/video", handleIndexTemplate)
|
||||||
|
http.HandleFunc("/help", handleHelpTemplate)
|
||||||
|
http.HandleFunc("/pin", handlePin)
|
||||||
|
http.HandleFunc("/emotes", handleEmoteTemplate)
|
||||||
|
|
||||||
|
http.HandleFunc("/", handleDefault)
|
||||||
|
|
||||||
|
http.HandleFunc("/pls/restart", func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
http.Redirect(w, r, "/pls/restart/soft", http.StatusSeeOther)
|
||||||
|
})
|
||||||
|
|
||||||
|
http.HandleFunc("/pls/restart/soft", func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
fmt.Fprintln(w, `I'm on the case. Give me 30 seconds. Love you <3`)
|
||||||
|
go func() {
|
||||||
|
killStream()
|
||||||
|
l = NewSuperLock()
|
||||||
|
}()
|
||||||
|
})
|
||||||
|
http.HandleFunc("/pls/restart/hard", func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
fmt.Fprintln(w, `I'm on the case. Give me 2 minutes. Love you <3`)
|
||||||
|
go func() {
|
||||||
|
rebootCam()
|
||||||
|
time.Sleep(time.Second * 60)
|
||||||
|
killStream()
|
||||||
|
l = NewSuperLock()
|
||||||
|
}()
|
||||||
|
})
|
||||||
|
|
||||||
|
go rtsp()
|
||||||
|
|
||||||
|
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()
|
||||||
|
}
|
||||||
|
permaKillStream() // todo
|
||||||
|
exit <- true
|
||||||
|
}
|
||||||
46
MovieNight/scrapedagain/main/.gitignore
vendored
Executable file
46
MovieNight/scrapedagain/main/.gitignore
vendored
Executable file
@@ -0,0 +1,46 @@
|
|||||||
|
# Binaries for programs and plugins
|
||||||
|
*.exe
|
||||||
|
*.exe~
|
||||||
|
*.dll
|
||||||
|
*.so
|
||||||
|
*.dylib
|
||||||
|
*.aseprite
|
||||||
|
|
||||||
|
# Test binary, built with `go test -c`
|
||||||
|
*.test
|
||||||
|
|
||||||
|
# Output of the go coverage tool, specifically when used with LiteIDE
|
||||||
|
*.out
|
||||||
|
|
||||||
|
# Log files
|
||||||
|
*.log
|
||||||
|
|
||||||
|
# GoCode debug file
|
||||||
|
debug
|
||||||
|
|
||||||
|
# Linux binary
|
||||||
|
MovieNight
|
||||||
|
|
||||||
|
# Windows binary
|
||||||
|
MovieNight.exe
|
||||||
|
|
||||||
|
# Darwin binary
|
||||||
|
MovieNightDarwin
|
||||||
|
|
||||||
|
# Twitch channel info
|
||||||
|
static/subscriber.json
|
||||||
|
|
||||||
|
# This file now holds the stream key. Don't include it.
|
||||||
|
settings.json
|
||||||
|
|
||||||
|
# vscode
|
||||||
|
.vscode
|
||||||
|
|
||||||
|
# Autobuilt wasm files
|
||||||
|
static/main.wasm
|
||||||
|
|
||||||
|
# tags for vim
|
||||||
|
tags
|
||||||
|
|
||||||
|
# channel and emote list from twitch
|
||||||
|
subscribers.json
|
||||||
10
MovieNight/scrapedagain/main/.travis.yml
Executable file
10
MovieNight/scrapedagain/main/.travis.yml
Executable file
@@ -0,0 +1,10 @@
|
|||||||
|
language: go
|
||||||
|
|
||||||
|
before_install:
|
||||||
|
- make get
|
||||||
|
|
||||||
|
go:
|
||||||
|
- 1.12.x
|
||||||
|
|
||||||
|
env:
|
||||||
|
- GO111MODULE=on
|
||||||
18
MovieNight/scrapedagain/main/Dockerfile
Executable file
18
MovieNight/scrapedagain/main/Dockerfile
Executable file
@@ -0,0 +1,18 @@
|
|||||||
|
FROM frolvlad/alpine-glibc:alpine-3.9_glibc-2.29
|
||||||
|
|
||||||
|
RUN apk update \
|
||||||
|
&& apk add --no-cache \
|
||||||
|
ca-certificates \
|
||||||
|
ffmpeg \
|
||||||
|
bash
|
||||||
|
|
||||||
|
RUN mkdir -p /var/log
|
||||||
|
WORKDIR /main
|
||||||
|
|
||||||
|
COPY . .
|
||||||
|
|
||||||
|
ENV GOPATH=""
|
||||||
|
ENV MNT="/mnt/"
|
||||||
|
ENTRYPOINT ["/main/MovieNight"]
|
||||||
|
CMD []
|
||||||
|
|
||||||
53
MovieNight/scrapedagain/main/Makefile
Executable file
53
MovieNight/scrapedagain/main/Makefile
Executable file
@@ -0,0 +1,53 @@
|
|||||||
|
# If a different version of Go is installed (via `go get`) set the GO_VERSION
|
||||||
|
# environment variable to that version. For example, setting it to "1.13.7"
|
||||||
|
# will run `go1.13.7 build [...]` instead of `go build [...]`.
|
||||||
|
#
|
||||||
|
# For info on installing extra versions, see this page:
|
||||||
|
# https://golang.org/doc/install#extra_versions
|
||||||
|
|
||||||
|
TAGS=
|
||||||
|
|
||||||
|
# Windows needs the .exe extension.
|
||||||
|
ifeq ($(OS),Windows_NT)
|
||||||
|
EXT=.exe
|
||||||
|
endif
|
||||||
|
|
||||||
|
.PHONY: fmt vet get clean dev setdev test ServerMovieNight
|
||||||
|
|
||||||
|
all: fmt vet test MovieNight$(EXT) static/main.wasm settings.json
|
||||||
|
|
||||||
|
# Build the server deployment
|
||||||
|
server: ServerMovieNight static/main.wasm
|
||||||
|
|
||||||
|
# Bulid used for deploying to my server.
|
||||||
|
ServerMovieNight: *.go common/*.go
|
||||||
|
GOOS=linux GOARCH=386 go$(GO_VERSION) build -o MovieNight $(TAGS)
|
||||||
|
|
||||||
|
setdev:
|
||||||
|
$(eval export TAGS=-tags "dev")
|
||||||
|
|
||||||
|
dev: setdev all
|
||||||
|
|
||||||
|
MovieNight$(EXT): *.go common/*.go
|
||||||
|
go$(GO_VERSION) build -o $@ $(TAGS)
|
||||||
|
|
||||||
|
static/main.wasm: wasm/*.go common/*.go
|
||||||
|
GOOS=js GOARCH=wasm go$(GO_VERSION) build -o $@ $(TAGS) wasm/*.go
|
||||||
|
|
||||||
|
clean:
|
||||||
|
-rm MovieNight$(EXT) ./static/main.wasm
|
||||||
|
|
||||||
|
fmt:
|
||||||
|
gofmt -w .
|
||||||
|
|
||||||
|
vet:
|
||||||
|
go$(GO_VERSION) vet $(TAGS) ./...
|
||||||
|
GOOS=js GOARCH=wasm go$(GO_VERSION) vet $(TAGS) ./...
|
||||||
|
|
||||||
|
test:
|
||||||
|
go$(GO_VERSION) test $(TAGS) ./...
|
||||||
|
|
||||||
|
# Do not put settings_example.json here as a prereq to avoid overwriting
|
||||||
|
# the settings if the example is updated.
|
||||||
|
settings.json:
|
||||||
|
cp settings_example.json settings.json
|
||||||
52
MovieNight/scrapedagain/main/connection.go
Executable file
52
MovieNight/scrapedagain/main/connection.go
Executable file
@@ -0,0 +1,52 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"net"
|
||||||
|
"sync"
|
||||||
|
|
||||||
|
"github.com/gorilla/websocket"
|
||||||
|
"github.com/zorchenhimer/MovieNight/common"
|
||||||
|
)
|
||||||
|
|
||||||
|
type chatConnection struct {
|
||||||
|
*websocket.Conn
|
||||||
|
mutex sync.RWMutex
|
||||||
|
forwardedFor string
|
||||||
|
clientName string
|
||||||
|
}
|
||||||
|
|
||||||
|
func (cc *chatConnection) ReadData(data interface{}) error {
|
||||||
|
cc.mutex.RLock()
|
||||||
|
defer cc.mutex.RUnlock()
|
||||||
|
|
||||||
|
stats.msgInInc()
|
||||||
|
return cc.ReadJSON(data)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (cc *chatConnection) WriteData(data interface{}) error {
|
||||||
|
cc.mutex.Lock()
|
||||||
|
defer cc.mutex.Unlock()
|
||||||
|
|
||||||
|
stats.msgOutInc()
|
||||||
|
err := cc.WriteJSON(data)
|
||||||
|
if err != nil {
|
||||||
|
if operr, ok := err.(*net.OpError); ok {
|
||||||
|
common.LogDebugln("OpError: " + operr.Err.Error())
|
||||||
|
}
|
||||||
|
return fmt.Errorf("Error writing data to %s %s: %v", cc.clientName, cc.Host(), err)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (cc *chatConnection) Host() string {
|
||||||
|
if len(cc.forwardedFor) > 0 {
|
||||||
|
return cc.forwardedFor
|
||||||
|
}
|
||||||
|
|
||||||
|
host, _, err := net.SplitHostPort(cc.RemoteAddr().String())
|
||||||
|
if err != nil {
|
||||||
|
return cc.RemoteAddr().String()
|
||||||
|
}
|
||||||
|
return host
|
||||||
|
}
|
||||||
239
MovieNight/scrapedagain/main/emotes.go
Executable file
239
MovieNight/scrapedagain/main/emotes.go
Executable file
@@ -0,0 +1,239 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"io/ioutil"
|
||||||
|
"log"
|
||||||
|
"net/http"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/pkg/errors"
|
||||||
|
"github.com/zorchenhimer/MovieNight/common"
|
||||||
|
)
|
||||||
|
|
||||||
|
const emoteDir = "./static/emotes/"
|
||||||
|
|
||||||
|
type TwitchUser struct {
|
||||||
|
ID string
|
||||||
|
Login string
|
||||||
|
}
|
||||||
|
|
||||||
|
type EmoteInfo struct {
|
||||||
|
ID int
|
||||||
|
Code string
|
||||||
|
}
|
||||||
|
|
||||||
|
func loadEmotes() error {
|
||||||
|
//fmt.Println(processEmoteDir(emoteDir))
|
||||||
|
newEmotes, err := processEmoteDir(emoteDir)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
common.Emotes = newEmotes
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func processEmoteDir(path string) (common.EmotesMap, error) {
|
||||||
|
dirInfo, err := ioutil.ReadDir(path)
|
||||||
|
if err != nil {
|
||||||
|
return nil, errors.Wrap(err, "could not open emoteDir:")
|
||||||
|
}
|
||||||
|
|
||||||
|
subDirs := []string{}
|
||||||
|
|
||||||
|
for _, item := range dirInfo {
|
||||||
|
// Get first level subdirs (eg, "twitch", "discord", etc)
|
||||||
|
if item.IsDir() {
|
||||||
|
subDirs = append(subDirs, item.Name())
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
em := common.NewEmotesMap()
|
||||||
|
// Find top level emotes
|
||||||
|
em, err = findEmotes(path, em)
|
||||||
|
if err != nil {
|
||||||
|
return nil, errors.Wrap(err, "could not findEmotes() in top level directory:")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get second level subdirs (eg, "twitch", "zorchenhimer", etc)
|
||||||
|
for _, dir := range subDirs {
|
||||||
|
subd, err := ioutil.ReadDir(filepath.Join(path, dir))
|
||||||
|
if err != nil {
|
||||||
|
fmt.Printf("Error reading dir %q: %v\n", subd, err)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
for _, d := range subd {
|
||||||
|
if d.IsDir() {
|
||||||
|
//emotes = append(emotes, findEmotes(filepath.Join(path, dir, d.Name()))...)
|
||||||
|
p := filepath.Join(path, dir, d.Name())
|
||||||
|
em, err = findEmotes(p, em)
|
||||||
|
if err != nil {
|
||||||
|
fmt.Printf("Error finding emotes in %q: %v\n", p, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Printf("processEmoteDir: %d\n", len(em))
|
||||||
|
return em, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func findEmotes(dir string, em common.EmotesMap) (common.EmotesMap, error) {
|
||||||
|
//em := NewEmotesMap()
|
||||||
|
|
||||||
|
fmt.Printf("finding emotes in %q\n", dir)
|
||||||
|
emotePNGs, err := filepath.Glob(filepath.Join(dir, "*.png"))
|
||||||
|
if err != nil {
|
||||||
|
return em, fmt.Errorf("unable to glob emote directory: %s\n", err)
|
||||||
|
}
|
||||||
|
fmt.Printf("%d emotePNGs\n", len(emotePNGs))
|
||||||
|
|
||||||
|
emoteGIFs, err := filepath.Glob(filepath.Join(dir, "*.gif"))
|
||||||
|
if err != nil {
|
||||||
|
return em, errors.Wrap(err, "unable to glob emote directory:")
|
||||||
|
}
|
||||||
|
fmt.Printf("%d emoteGIFs\n", len(emoteGIFs))
|
||||||
|
|
||||||
|
for _, file := range emotePNGs {
|
||||||
|
em = em.Add(file)
|
||||||
|
//emotes = append(emotes, common.Emote{FullPath: dir, Code: file})
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, file := range emoteGIFs {
|
||||||
|
em = em.Add(file)
|
||||||
|
}
|
||||||
|
|
||||||
|
return em, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func getEmotes(names []string) error {
|
||||||
|
users := getUserIDs(names)
|
||||||
|
users = append(users, TwitchUser{ID: "0", Login: "twitch"})
|
||||||
|
|
||||||
|
for _, user := range users {
|
||||||
|
emotes, cheers, err := getChannelEmotes(user.ID)
|
||||||
|
if err != nil {
|
||||||
|
return errors.Wrapf(err, "could not get emote data for \"%s\"", user.ID)
|
||||||
|
}
|
||||||
|
|
||||||
|
emoteUserDir := filepath.Join(emoteDir, "twitch", user.Login)
|
||||||
|
if _, err := os.Stat(emoteUserDir); os.IsNotExist(err) {
|
||||||
|
os.MkdirAll(emoteUserDir, os.ModePerm)
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, emote := range emotes {
|
||||||
|
if !strings.ContainsAny(emote.Code, `:;\[]|?&`) {
|
||||||
|
filePath := filepath.Join(emoteUserDir, emote.Code+".png")
|
||||||
|
file, err := os.Create(filePath)
|
||||||
|
if err != nil {
|
||||||
|
|
||||||
|
return errors.Wrapf(err, "could not create emote file in path \"%s\":", filePath)
|
||||||
|
}
|
||||||
|
|
||||||
|
err = downloadEmote(emote.ID, file)
|
||||||
|
if err != nil {
|
||||||
|
return errors.Wrapf(err, "could not download emote %s:", emote.Code)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for amount, sizes := range cheers {
|
||||||
|
name := fmt.Sprintf("%sCheer%s.gif", user.Login, amount)
|
||||||
|
filePath := filepath.Join(emoteUserDir, name)
|
||||||
|
file, err := os.Create(filePath)
|
||||||
|
if err != nil {
|
||||||
|
return errors.Wrapf(err, "could not create emote file in path \"%s\":", filePath)
|
||||||
|
}
|
||||||
|
|
||||||
|
err = downloadCheerEmote(sizes["4"], file)
|
||||||
|
if err != nil {
|
||||||
|
return errors.Wrapf(err, "could not download emote %s:", name)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func getUserIDs(names []string) []TwitchUser {
|
||||||
|
logins := strings.Join(names, "&login=")
|
||||||
|
request, err := http.NewRequest("GET", fmt.Sprintf("https://api.twitch.tv/helix/users?login=%s", logins), nil)
|
||||||
|
if err != nil {
|
||||||
|
log.Fatalln("Error generating new request:", err)
|
||||||
|
}
|
||||||
|
request.Header.Set("Client-ID", settings.TwitchClientID)
|
||||||
|
|
||||||
|
client := http.Client{}
|
||||||
|
resp, err := client.Do(request)
|
||||||
|
if err != nil {
|
||||||
|
log.Fatalln("Error sending request:", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
decoder := json.NewDecoder(resp.Body)
|
||||||
|
type userResponse struct {
|
||||||
|
Data []TwitchUser
|
||||||
|
}
|
||||||
|
var data userResponse
|
||||||
|
|
||||||
|
err = decoder.Decode(&data)
|
||||||
|
if err != nil {
|
||||||
|
log.Fatalln("Error decoding data:", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return data.Data
|
||||||
|
}
|
||||||
|
|
||||||
|
func getChannelEmotes(ID string) ([]EmoteInfo, map[string]map[string]string, error) {
|
||||||
|
resp, err := http.Get("https://api.twitchemotes.com/api/v4/channels/" + ID)
|
||||||
|
if err != nil {
|
||||||
|
return nil, nil, errors.Wrap(err, "could not get emotes")
|
||||||
|
}
|
||||||
|
decoder := json.NewDecoder(resp.Body)
|
||||||
|
|
||||||
|
type EmoteResponse struct {
|
||||||
|
Emotes []EmoteInfo
|
||||||
|
Cheermotes map[string]map[string]string
|
||||||
|
}
|
||||||
|
var data EmoteResponse
|
||||||
|
|
||||||
|
err = decoder.Decode(&data)
|
||||||
|
if err != nil {
|
||||||
|
return nil, nil, errors.Wrap(err, "could not decode emotes")
|
||||||
|
}
|
||||||
|
|
||||||
|
return data.Emotes, data.Cheermotes, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func downloadEmote(ID int, file *os.File) error {
|
||||||
|
resp, err := http.Get(fmt.Sprintf("https://static-cdn.jtvnw.net/emoticons/v1/%d/3.0", ID))
|
||||||
|
if err != nil {
|
||||||
|
return errors.Errorf("could not download emote file %s: %v", file.Name(), err)
|
||||||
|
}
|
||||||
|
defer resp.Body.Close()
|
||||||
|
|
||||||
|
_, err = io.Copy(file, resp.Body)
|
||||||
|
if err != nil {
|
||||||
|
return errors.Errorf("could not save emote: %v", err)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func downloadCheerEmote(url string, file *os.File) error {
|
||||||
|
resp, err := http.Get(url)
|
||||||
|
if err != nil {
|
||||||
|
return errors.Errorf("could not download cheer file %s: %v", file.Name(), err)
|
||||||
|
}
|
||||||
|
defer resp.Body.Close()
|
||||||
|
|
||||||
|
_, err = io.Copy(file, resp.Body)
|
||||||
|
if err != nil {
|
||||||
|
return errors.Errorf("could not save cheer: %v", err)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
48
MovieNight/scrapedagain/main/errors.go
Executable file
48
MovieNight/scrapedagain/main/errors.go
Executable file
@@ -0,0 +1,48 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"reflect"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
func errorName(err error) string {
|
||||||
|
return reflect.ValueOf(err).Type().Name()
|
||||||
|
}
|
||||||
|
|
||||||
|
// UserNameError is a base error for errors that deal with user names
|
||||||
|
type UserNameError struct {
|
||||||
|
Name string
|
||||||
|
}
|
||||||
|
|
||||||
|
// UserFormatError is an error for when the name format does not match what is required
|
||||||
|
type UserFormatError UserNameError
|
||||||
|
|
||||||
|
func (e UserFormatError) Error() string {
|
||||||
|
return fmt.Sprintf("\"%s\", is in an invalid format", e.Name)
|
||||||
|
}
|
||||||
|
|
||||||
|
// UserTakenError is an error for when a user tries to join with a name that is already taken
|
||||||
|
type UserTakenError UserNameError
|
||||||
|
|
||||||
|
func (e UserTakenError) Error() string {
|
||||||
|
return fmt.Sprintf("\"%s\", is already taken", e.Name)
|
||||||
|
}
|
||||||
|
|
||||||
|
// BannedUserError is an error for when a user tries to join with a banned ip address
|
||||||
|
type BannedUserError struct {
|
||||||
|
Host, Name string
|
||||||
|
Names []string
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e BannedUserError) Error() string {
|
||||||
|
return fmt.Sprintf("banned user tried to connect with IP %s: %s (banned with name(s) %s)", e.Host, e.Name, strings.Join(e.Names, ", "))
|
||||||
|
}
|
||||||
|
|
||||||
|
func newBannedUserError(host, name string, names []string) BannedUserError {
|
||||||
|
return BannedUserError{
|
||||||
|
Host: host,
|
||||||
|
Name: name,
|
||||||
|
Names: names,
|
||||||
|
}
|
||||||
|
}
|
||||||
BIN
MovieNight/scrapedagain/main/favicon.png
Executable file
BIN
MovieNight/scrapedagain/main/favicon.png
Executable file
Binary file not shown.
|
After Width: | Height: | Size: 3.5 KiB |
23
MovieNight/scrapedagain/main/go.mod
Executable file
23
MovieNight/scrapedagain/main/go.mod
Executable file
@@ -0,0 +1,23 @@
|
|||||||
|
module github.com/zorchenhimer/MovieNight
|
||||||
|
|
||||||
|
go 1.12
|
||||||
|
|
||||||
|
require (
|
||||||
|
github.com/Microsoft/go-winio v0.4.12 // indirect
|
||||||
|
github.com/cenkalti/backoff v2.1.1+incompatible // indirect
|
||||||
|
github.com/chromedp/cdproto v0.0.0-20190412020601-c4267f5c421a // indirect
|
||||||
|
github.com/containerd/continuity v0.0.0-20181203112020-004b46473808 // indirect
|
||||||
|
github.com/gorilla/sessions v1.1.3
|
||||||
|
github.com/gorilla/websocket v1.4.0
|
||||||
|
github.com/konsorten/go-windows-terminal-sequences v1.0.2 // indirect
|
||||||
|
github.com/mailru/easyjson v0.0.0-20190403194419-1ea4449da983 // indirect
|
||||||
|
github.com/nareix/joy4 v0.0.0-20181022032202-3ddbc8f9d431
|
||||||
|
github.com/ory/dockertest v3.3.4+incompatible // indirect
|
||||||
|
github.com/pkg/errors v0.8.1
|
||||||
|
github.com/sirupsen/logrus v1.4.1 // indirect
|
||||||
|
github.com/stretchr/objx v0.2.0 // indirect
|
||||||
|
golang.org/x/crypto v0.0.0-20190411191339-88737f569e3a // indirect
|
||||||
|
golang.org/x/image v0.0.0-20190321063152-3fc05d484e9f // indirect
|
||||||
|
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3 // indirect
|
||||||
|
golang.org/x/sys v0.0.0-20190412213103-97732733099d
|
||||||
|
)
|
||||||
102
MovieNight/scrapedagain/main/go.sum
Executable file
102
MovieNight/scrapedagain/main/go.sum
Executable file
@@ -0,0 +1,102 @@
|
|||||||
|
github.com/Azure/go-ansiterm v0.0.0-20170929234023-d6e3b3328b78 h1:w+iIsaOQNcT7OZ575w+acHgRric5iCyQh+xv+KJ4HB8=
|
||||||
|
github.com/Azure/go-ansiterm v0.0.0-20170929234023-d6e3b3328b78/go.mod h1:LmzpDX56iTiv29bbRTIsUNlaFfuhWRQBWjQdVyAevI8=
|
||||||
|
github.com/Microsoft/go-winio v0.4.11 h1:zoIOcVf0xPN1tnMVbTtEdI+P8OofVk3NObnwOQ6nK2Q=
|
||||||
|
github.com/Microsoft/go-winio v0.4.11/go.mod h1:VhR8bwka0BXejwEJY73c50VrPtXAaKcyvVC4A4RozmA=
|
||||||
|
github.com/Microsoft/go-winio v0.4.12/go.mod h1:VhR8bwka0BXejwEJY73c50VrPtXAaKcyvVC4A4RozmA=
|
||||||
|
github.com/Nvveen/Gotty v0.0.0-20120604004816-cd527374f1e5 h1:TngWCqHvy9oXAN6lEVMRuU21PR1EtLVZJmdB18Gu3Rw=
|
||||||
|
github.com/Nvveen/Gotty v0.0.0-20120604004816-cd527374f1e5/go.mod h1:lmUJ/7eu/Q8D7ML55dXQrVaamCz2vxCfdQBasLZfHKk=
|
||||||
|
github.com/cenkalti/backoff v2.0.0+incompatible h1:5IIPUHhlnUZbcHQsQou5k1Tn58nJkeJL9U+ig5CHJbY=
|
||||||
|
github.com/cenkalti/backoff v2.0.0+incompatible/go.mod h1:90ReRw6GdpyfrHakVjL/QHaoyV4aDUVVkXQJJJ3NXXM=
|
||||||
|
github.com/cenkalti/backoff v2.1.1+incompatible/go.mod h1:90ReRw6GdpyfrHakVjL/QHaoyV4aDUVVkXQJJJ3NXXM=
|
||||||
|
github.com/chromedp/cdproto v0.0.0-20190217000753-2d8e8962ceb2 h1:4Ck8YOuS0G3+0xMb80cDSff7QpUolhSc0PGyfagbcdA=
|
||||||
|
github.com/chromedp/cdproto v0.0.0-20190217000753-2d8e8962ceb2/go.mod h1:xquOK9dIGFlLaIGI4c6IyfLI/Gz0LiYYuJtzhsUODgI=
|
||||||
|
github.com/chromedp/cdproto v0.0.0-20190412020601-c4267f5c421a/go.mod h1:xquOK9dIGFlLaIGI4c6IyfLI/Gz0LiYYuJtzhsUODgI=
|
||||||
|
github.com/chromedp/chromedp v0.1.3 h1:Nkqt42/7tvzg57mexc4LbM8nZbx7vSZ+eiUpeczGGL8=
|
||||||
|
github.com/chromedp/chromedp v0.1.3/go.mod h1:ZahQlJx8YBfDtuFN80zn6P7fskSotBkdhgKDoLWFANk=
|
||||||
|
github.com/containerd/continuity v0.0.0-20181027224239-bea7585dbfac h1:PThQaO4yCvJzJBUW1XoFQxLotWRhvX2fgljJX8yrhFI=
|
||||||
|
github.com/containerd/continuity v0.0.0-20181027224239-bea7585dbfac/go.mod h1:GL3xCUCBDV3CZiTSEKksMWbLE66hEyuu9qyDOOqM47Y=
|
||||||
|
github.com/containerd/continuity v0.0.0-20181203112020-004b46473808/go.mod h1:GL3xCUCBDV3CZiTSEKksMWbLE66hEyuu9qyDOOqM47Y=
|
||||||
|
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||||
|
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||||
|
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||||
|
github.com/dennwc/dom v0.3.0 h1:u89+QvT1OPRSSTFf54o9RuK7C0Uoq2jFo4VCa4rnjtI=
|
||||||
|
github.com/dennwc/dom v0.3.0/go.mod h1:/z5w9Stx19m8RUwolsmsqTs9rDxKgJO5T9UEumilgk4=
|
||||||
|
github.com/dennwc/testproxy v1.0.1 h1:mQhNVWHPolTYjJrDZYKcugIplWRSlFAis6k/Zf1s0c0=
|
||||||
|
github.com/dennwc/testproxy v1.0.1/go.mod h1:EHGV9tzWhMPLmEoVJ2KGyC149XqwKZwBDViCjhKD5d8=
|
||||||
|
github.com/disintegration/imaging v1.6.0 h1:nVPXRUUQ36Z7MNf0O77UzgnOb1mkMMor7lmJMJXc/mA=
|
||||||
|
github.com/disintegration/imaging v1.6.0/go.mod h1:xuIt+sRxDFrHS0drzXUlCJthkJ8k7lkkUojDSR247MQ=
|
||||||
|
github.com/docker/go-connections v0.4.0 h1:El9xVISelRB7BuFusrZozjnkIM5YnzCViNKohAFqRJQ=
|
||||||
|
github.com/docker/go-connections v0.4.0/go.mod h1:Gbd7IOopHjR8Iph03tsViu4nIes5XhDvyHbTtUxmeec=
|
||||||
|
github.com/docker/go-units v0.3.3 h1:Xk8S3Xj5sLGlG5g67hJmYMmUgXv5N4PhkjJHHqrwnTk=
|
||||||
|
github.com/docker/go-units v0.3.3/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk=
|
||||||
|
github.com/google/go-cmp v0.2.0 h1:+dTQ8DZQJz0Mb/HjFlkptS1FeQ4cWSnN941F8aEG4SQ=
|
||||||
|
github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M=
|
||||||
|
github.com/gorilla/context v1.1.1 h1:AWwleXJkX/nhcU9bZSnZoi3h/qGYqQAGhq6zZe/aQW8=
|
||||||
|
github.com/gorilla/context v1.1.1/go.mod h1:kBGZzfjB9CEq2AlWe17Uuf7NDRt0dE0s8S51q0aT7Yg=
|
||||||
|
github.com/gorilla/securecookie v1.1.1 h1:miw7JPhV+b/lAHSXz4qd/nN9jRiAFV5FwjeKyCS8BvQ=
|
||||||
|
github.com/gorilla/securecookie v1.1.1/go.mod h1:ra0sb63/xPlUeL+yeDciTfxMRAA+MP+HVt/4epWDjd4=
|
||||||
|
github.com/gorilla/sessions v1.1.3 h1:uXoZdcdA5XdXF3QzuSlheVRUvjl+1rKY7zBXL68L9RU=
|
||||||
|
github.com/gorilla/sessions v1.1.3/go.mod h1:8KCfur6+4Mqcc6S0FEfKuN15Vl5MgXW92AE8ovaJD0w=
|
||||||
|
github.com/gorilla/websocket v1.4.0 h1:WDFjx/TMzVgy9VdMMQi2K2Emtwi2QcUQsztZ/zLaH/Q=
|
||||||
|
github.com/gorilla/websocket v1.4.0/go.mod h1:E7qHFY5m1UJ88s3WnNqhKjPHQ0heANvMoAMk2YaljkQ=
|
||||||
|
github.com/gotestyourself/gotestyourself v2.2.0+incompatible h1:AQwinXlbQR2HvPjQZOmDhRqsv5mZf+Jb1RnSLxcqZcI=
|
||||||
|
github.com/gotestyourself/gotestyourself v2.2.0+incompatible/go.mod h1:zZKM6oeNM8k+FRljX1mnzVYeS8wiGgQyvST1/GafPbY=
|
||||||
|
github.com/knq/sysutil v0.0.0-20181215143952-f05b59f0f307 h1:vl4eIlySbjertFaNwiMjXsGrFVK25aOWLq7n+3gh2ls=
|
||||||
|
github.com/knq/sysutil v0.0.0-20181215143952-f05b59f0f307/go.mod h1:BjPj+aVjl9FW/cCGiF3nGh5v+9Gd3VCgBQbod/GlMaQ=
|
||||||
|
github.com/konsorten/go-windows-terminal-sequences v1.0.1 h1:mweAR1A6xJ3oS2pRaGiHgQ4OO8tzTaLawm8vnODuwDk=
|
||||||
|
github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
|
||||||
|
github.com/konsorten/go-windows-terminal-sequences v1.0.2/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
|
||||||
|
github.com/lib/pq v1.0.0 h1:X5PMW56eZitiTeO7tKzZxFCSpbFZJtkMMooicw2us9A=
|
||||||
|
github.com/lib/pq v1.0.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo=
|
||||||
|
github.com/mailru/easyjson v0.0.0-20180823135443-60711f1a8329/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc=
|
||||||
|
github.com/mailru/easyjson v0.0.0-20190221075403-6243d8e04c3f h1:B6PQkurxGG1rqEX96oE14gbj8bqvYC5dtks9r5uGmlE=
|
||||||
|
github.com/mailru/easyjson v0.0.0-20190221075403-6243d8e04c3f/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc=
|
||||||
|
github.com/mailru/easyjson v0.0.0-20190403194419-1ea4449da983/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc=
|
||||||
|
github.com/nareix/joy4 v0.0.0-20181022032202-3ddbc8f9d431 h1:nWhrOsCKdV6bivw03k7MROF2tYzCFGfYBYFrTEHyucs=
|
||||||
|
github.com/nareix/joy4 v0.0.0-20181022032202-3ddbc8f9d431/go.mod h1:aFJ1ZwLjvHN4yEzE5Bkz8rD8/d8Vlj3UIuvz2yfET7I=
|
||||||
|
github.com/opencontainers/go-digest v1.0.0-rc1 h1:WzifXhOVOEOuFYOJAW6aQqW0TooG2iki3E3Ii+WN7gQ=
|
||||||
|
github.com/opencontainers/go-digest v1.0.0-rc1/go.mod h1:cMLVZDEM3+U2I4VmLI6N8jQYUd2OVphdqWwCJHrFt2s=
|
||||||
|
github.com/opencontainers/image-spec v1.0.1 h1:JMemWkRwHx4Zj+fVxWoMCFm/8sYGGrUVojFA6h/TRcI=
|
||||||
|
github.com/opencontainers/image-spec v1.0.1/go.mod h1:BtxoFyWECRxE4U/7sNtV5W15zMzWCbyJoFRP3s7yZA0=
|
||||||
|
github.com/opencontainers/runc v0.1.1 h1:GlxAyO6x8rfZYN9Tt0Kti5a/cP41iuiO2yYT0IJGY8Y=
|
||||||
|
github.com/opencontainers/runc v0.1.1/go.mod h1:qT5XzbpPznkRYVz/mWwUaVBUv2rmF59PVA73FjuZG0U=
|
||||||
|
github.com/ory/dockertest v3.3.2+incompatible h1:uO+NcwH6GuFof/Uz8yzjNi1g0sGT5SLAJbdBvD8bUYc=
|
||||||
|
github.com/ory/dockertest v3.3.2+incompatible/go.mod h1:1vX4m9wsvi00u5bseYwXaSnhNrne+V0E6LAcBILJdPs=
|
||||||
|
github.com/ory/dockertest v3.3.4+incompatible/go.mod h1:1vX4m9wsvi00u5bseYwXaSnhNrne+V0E6LAcBILJdPs=
|
||||||
|
github.com/pkg/errors v0.8.0 h1:WdK/asTD0HN+q6hsWO3/vpuAkAr+tw6aNJNDFFf0+qw=
|
||||||
|
github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||||
|
github.com/pkg/errors v0.8.1 h1:iURUrRGxPUNPdy5/HRSm+Yj6okJ6UtLINN0Q9M4+h3I=
|
||||||
|
github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||||
|
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||||
|
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||||
|
github.com/sirupsen/logrus v1.2.0 h1:juTguoYk5qI21pwyTXY3B3Y5cOTH3ZUyZCg1v/mihuo=
|
||||||
|
github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo=
|
||||||
|
github.com/sirupsen/logrus v1.4.1/go.mod h1:ni0Sbl8bgC9z8RoU9G6nDWqqs/fq4eDPysMBDgk/93Q=
|
||||||
|
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||||
|
github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||||
|
github.com/stretchr/objx v0.2.0/go.mod h1:qt09Ya8vawLte6SNmTgCsAVtYtaKzEcn8ATUoHMkEqE=
|
||||||
|
github.com/stretchr/testify v1.2.2 h1:bSDNvY7ZPG5RlJ8otE/7V6gMiyenm9RtJ7IUVIAoJ1w=
|
||||||
|
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
|
||||||
|
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
|
||||||
|
golang.org/x/crypto v0.0.0-20180904163835-0709b304e793 h1:u+LnwYTOOW7Ukr/fppxEb1Nwz0AtPflrblfvUudpo+I=
|
||||||
|
golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
|
||||||
|
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||||
|
golang.org/x/crypto v0.0.0-20190411191339-88737f569e3a/go.mod h1:WFFai1msRO1wXaEeE5yQxYXgSfI8pQAWXbQop6sCtWE=
|
||||||
|
golang.org/x/image v0.0.0-20180708004352-c73c2afc3b81/go.mod h1:ux5Hcp/YLpHSI86hEcLt0YII63i6oz57MZXIpbrjZUs=
|
||||||
|
golang.org/x/image v0.0.0-20190220214146-31aff87c08e9/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js=
|
||||||
|
golang.org/x/image v0.0.0-20190227222117-0694c2d4d067 h1:KYGJGHOQy8oSi1fDlSpcZF0+juKwk/hEMv5SiwHogR0=
|
||||||
|
golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js=
|
||||||
|
golang.org/x/image v0.0.0-20190321063152-3fc05d484e9f/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js=
|
||||||
|
golang.org/x/net v0.0.0-20181114220301-adae6a3d119a h1:gOpx8G595UYyvj8UK4+OFyY4rx037g3fmfhe5SasG3U=
|
||||||
|
golang.org/x/net v0.0.0-20181114220301-adae6a3d119a/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||||
|
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||||
|
golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||||
|
golang.org/x/sys v0.0.0-20181107165924-66b7b1311ac8 h1:YoY1wS6JYVRpIfFngRf2HHo9R9dAne3xbkGOQ5rJXjU=
|
||||||
|
golang.org/x/sys v0.0.0-20181107165924-66b7b1311ac8/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||||
|
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||||
|
golang.org/x/sys v0.0.0-20190403152447-81d4e9dc473e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
|
golang.org/x/sys v0.0.0-20190412213103-97732733099d h1:+R4KGOnez64A81RvjARKc4UT5/tI9ujCIVX+P5KiHuI=
|
||||||
|
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
|
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||||
|
gotest.tools v2.2.0+incompatible h1:VsBPFP1AI068pPrMxtb/S8Zkgf9xEmTLJjfM+P5UIEo=
|
||||||
|
gotest.tools v2.2.0+incompatible/go.mod h1:DsYFclhRJ6vuDpmuTbkuFWG+y2sxOXAzmJt81HFBacw=
|
||||||
378
MovieNight/scrapedagain/main/handlers.go
Executable file
378
MovieNight/scrapedagain/main/handlers.go
Executable file
@@ -0,0 +1,378 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"log"
|
||||||
|
"net/http"
|
||||||
|
"path"
|
||||||
|
"path/filepath"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/zorchenhimer/MovieNight/common"
|
||||||
|
|
||||||
|
"github.com/gorilla/websocket"
|
||||||
|
"github.com/nareix/joy4/av/avutil"
|
||||||
|
"github.com/nareix/joy4/av/pubsub"
|
||||||
|
"github.com/nareix/joy4/format/flv"
|
||||||
|
"github.com/nareix/joy4/format/rtmp"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
// Read/Write mutex for rtmp stream
|
||||||
|
l = NewSuperLock()
|
||||||
|
|
||||||
|
// Map of active streams
|
||||||
|
channels = map[string]*Channel{}
|
||||||
|
)
|
||||||
|
|
||||||
|
type Channel struct {
|
||||||
|
que *pubsub.Queue
|
||||||
|
}
|
||||||
|
|
||||||
|
type writeFlusher struct {
|
||||||
|
httpflusher http.Flusher
|
||||||
|
io.Writer
|
||||||
|
}
|
||||||
|
|
||||||
|
func (self writeFlusher) Flush() error {
|
||||||
|
self.httpflusher.Flush()
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Serving static files
|
||||||
|
func wsStaticFiles(w http.ResponseWriter, r *http.Request) {
|
||||||
|
switch r.URL.Path {
|
||||||
|
case "/favicon.ico":
|
||||||
|
http.ServeFile(w, r, "./favicon.png")
|
||||||
|
return
|
||||||
|
case "/justvideo":
|
||||||
|
http.ServeFile(w, r, "./static/justvideo.html")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
goodPath := r.URL.Path[8:len(r.URL.Path)]
|
||||||
|
common.LogDebugf("[static] serving %q from folder ./static/\n", goodPath)
|
||||||
|
|
||||||
|
http.ServeFile(w, r, "./static/"+goodPath)
|
||||||
|
}
|
||||||
|
|
||||||
|
func wsWasmFile(w http.ResponseWriter, r *http.Request) {
|
||||||
|
if settings.NoCache {
|
||||||
|
w.Header().Set("Cache-Control", "no-cache, must-revalidate")
|
||||||
|
}
|
||||||
|
common.LogDebugln("[static] serving wasm file")
|
||||||
|
http.ServeFile(w, r, "./static/main.wasm")
|
||||||
|
}
|
||||||
|
|
||||||
|
func wsImages(w http.ResponseWriter, r *http.Request) {
|
||||||
|
base := filepath.Base(r.URL.Path)
|
||||||
|
common.LogDebugln("[img] ", base)
|
||||||
|
http.ServeFile(w, r, "./static/img/"+base)
|
||||||
|
}
|
||||||
|
|
||||||
|
func wsEmotes(w http.ResponseWriter, r *http.Request) {
|
||||||
|
http.ServeFile(w, r, path.Join("./static/", r.URL.Path))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handling the websocket
|
||||||
|
var upgrader = websocket.Upgrader{
|
||||||
|
ReadBufferSize: 1024,
|
||||||
|
WriteBufferSize: 1024,
|
||||||
|
CheckOrigin: func(r *http.Request) bool { return true }, //not checking origin
|
||||||
|
}
|
||||||
|
|
||||||
|
//this is also the handler for joining to the chat
|
||||||
|
func wsHandler(w http.ResponseWriter, r *http.Request) {
|
||||||
|
log.Println("ws handler")
|
||||||
|
|
||||||
|
conn, err := upgrader.Upgrade(w, r, nil)
|
||||||
|
if err != nil {
|
||||||
|
common.LogErrorln("Error upgrading to websocket:", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
common.LogDebugln("Connection has been upgraded to websocket")
|
||||||
|
|
||||||
|
go func() {
|
||||||
|
// Handle incomming messages
|
||||||
|
for {
|
||||||
|
var data common.ClientData
|
||||||
|
err := conn.ReadJSON(&data)
|
||||||
|
if err != nil { //if error then assuming that the connection is closed
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}()
|
||||||
|
}
|
||||||
|
|
||||||
|
// returns if it's OK to proceed
|
||||||
|
func checkRoomAccess(w http.ResponseWriter, r *http.Request) bool {
|
||||||
|
session, err := sstore.Get(r, "moviesession")
|
||||||
|
if err != nil {
|
||||||
|
// Don't return as server error here, just make a new session.
|
||||||
|
common.LogErrorf("Unable to get session for client %s: %v\n", r.RemoteAddr, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if settings.RoomAccess == AccessPin {
|
||||||
|
pin := session.Values["pin"]
|
||||||
|
// No pin found in session
|
||||||
|
if pin == nil || len(pin.(string)) == 0 {
|
||||||
|
if r.Method == "POST" {
|
||||||
|
// Check for correct pin
|
||||||
|
err = r.ParseForm()
|
||||||
|
if err != nil {
|
||||||
|
common.LogErrorf("Error parsing form")
|
||||||
|
http.Error(w, "Unable to get session data", http.StatusInternalServerError)
|
||||||
|
}
|
||||||
|
|
||||||
|
postPin := strings.TrimSpace(r.Form.Get("txtInput"))
|
||||||
|
common.LogDebugf("Received pin: %s\n", postPin)
|
||||||
|
if postPin == settings.RoomAccessPin {
|
||||||
|
// Pin is correct. Save it to session and return true.
|
||||||
|
session.Values["pin"] = settings.RoomAccessPin
|
||||||
|
session.Save(r, w)
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
// Pin is incorrect.
|
||||||
|
handlePinTemplate(w, r, "Incorrect PIN")
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
// nope. display pin entry and return
|
||||||
|
handlePinTemplate(w, r, "")
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
// Pin found in session, but it has changed since last time.
|
||||||
|
if pin.(string) != settings.RoomAccessPin {
|
||||||
|
// Clear out the old pin.
|
||||||
|
session.Values["pin"] = nil
|
||||||
|
session.Save(r, w)
|
||||||
|
|
||||||
|
// Prompt for new one.
|
||||||
|
handlePinTemplate(w, r, "Pin has changed. Enter new PIN.")
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
// Correct pin found in session
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: this.
|
||||||
|
if settings.RoomAccess == AccessRequest {
|
||||||
|
http.Error(w, "Requesting access not implemented yet", http.StatusNotImplemented)
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
// Room is open.
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
func handlePinTemplate(w http.ResponseWriter, r *http.Request, errorMessage string) {
|
||||||
|
log.Println("handle pin temp")
|
||||||
|
type Data struct {
|
||||||
|
Title string
|
||||||
|
SubmitText string
|
||||||
|
Notice string
|
||||||
|
}
|
||||||
|
|
||||||
|
if errorMessage == "" {
|
||||||
|
errorMessage = "Please enter the PIN"
|
||||||
|
}
|
||||||
|
|
||||||
|
data := Data{
|
||||||
|
Title: "Enter Pin",
|
||||||
|
SubmitText: "Submit Pin",
|
||||||
|
Notice: errorMessage,
|
||||||
|
}
|
||||||
|
|
||||||
|
err := common.ExecuteServerTemplate(w, "pin", data)
|
||||||
|
if err != nil {
|
||||||
|
common.LogErrorf("Error executing file, %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func handleHelpTemplate(w http.ResponseWriter, r *http.Request) {
|
||||||
|
http.NotFound(w, r)
|
||||||
|
}
|
||||||
|
|
||||||
|
func handleEmoteTemplate(w http.ResponseWriter, r *http.Request) {
|
||||||
|
log.Println("handle emote temp")
|
||||||
|
type Data struct {
|
||||||
|
Title string
|
||||||
|
Emotes map[string]string
|
||||||
|
}
|
||||||
|
|
||||||
|
data := Data{
|
||||||
|
Title: "Available Emotes",
|
||||||
|
Emotes: common.Emotes,
|
||||||
|
}
|
||||||
|
|
||||||
|
err := common.ExecuteServerTemplate(w, "emotes", data)
|
||||||
|
if err != nil {
|
||||||
|
common.LogErrorf("Error executing file, %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func handlePin(w http.ResponseWriter, r *http.Request) {
|
||||||
|
log.Println("handle pin")
|
||||||
|
session, err := sstore.Get(r, "moviesession")
|
||||||
|
if err != nil {
|
||||||
|
common.LogDebugf("Unable to get session: %v\n", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
val := session.Values["pin"]
|
||||||
|
if val == nil {
|
||||||
|
session.Values["pin"] = "1234"
|
||||||
|
err := session.Save(r, w)
|
||||||
|
if err != nil {
|
||||||
|
fmt.Fprintf(w, "unable to save session: %v", err)
|
||||||
|
}
|
||||||
|
fmt.Fprint(w, "Pin was not set")
|
||||||
|
common.LogDebugln("pin was not set")
|
||||||
|
} else {
|
||||||
|
fmt.Fprintf(w, "pin set: %v", val)
|
||||||
|
common.LogDebugf("pin is set: %v\n", val)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func handleIndexTemplate(w http.ResponseWriter, r *http.Request) {
|
||||||
|
log.Println("handle ind temp")
|
||||||
|
if settings.RoomAccess != AccessOpen {
|
||||||
|
if !checkRoomAccess(w, r) {
|
||||||
|
common.LogDebugln("Denied access")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
common.LogDebugln("Granted access")
|
||||||
|
}
|
||||||
|
|
||||||
|
type Data struct {
|
||||||
|
Video, Chat bool
|
||||||
|
MessageHistoryCount int
|
||||||
|
Title string
|
||||||
|
}
|
||||||
|
|
||||||
|
data := Data{
|
||||||
|
Video: true,
|
||||||
|
Chat: true,
|
||||||
|
MessageHistoryCount: settings.MaxMessageCount,
|
||||||
|
Title: "Movie Night!",
|
||||||
|
}
|
||||||
|
|
||||||
|
path := strings.Split(strings.TrimLeft(r.URL.Path, "/"), "/")
|
||||||
|
if path[0] == "video" {
|
||||||
|
data.Chat = false
|
||||||
|
data.Title += " - video"
|
||||||
|
}
|
||||||
|
|
||||||
|
// Force browser to replace cache since file was not changed
|
||||||
|
if settings.NoCache {
|
||||||
|
w.Header().Set("Cache-Control", "no-cache, must-revalidate")
|
||||||
|
}
|
||||||
|
|
||||||
|
err := common.ExecuteServerTemplate(w, "main", data)
|
||||||
|
if err != nil {
|
||||||
|
common.LogErrorf("Error executing file, %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func handlePublish(conn *rtmp.Conn) {
|
||||||
|
log.Println("handle publish")
|
||||||
|
streams, _ := conn.Streams()
|
||||||
|
|
||||||
|
l.Lock()
|
||||||
|
common.LogDebugln("request string->", conn.URL.RequestURI())
|
||||||
|
urlParts := strings.Split(strings.Trim(conn.URL.RequestURI(), "/"), "/")
|
||||||
|
common.LogDebugln("urlParts->", urlParts)
|
||||||
|
|
||||||
|
if len(urlParts) > 2 {
|
||||||
|
common.LogErrorln("Extra garbage after stream key")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
if len(urlParts) != 2 {
|
||||||
|
common.LogErrorln("Missing stream key")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if urlParts[1] != settings.GetStreamKey() {
|
||||||
|
common.LogErrorln("Stream key is incorrect. Denying stream.")
|
||||||
|
return //If key not match, deny stream
|
||||||
|
}
|
||||||
|
*/
|
||||||
|
|
||||||
|
streamPath := urlParts[0]
|
||||||
|
ch := channels[streamPath]
|
||||||
|
if ch == nil {
|
||||||
|
ch = &Channel{}
|
||||||
|
ch.que = pubsub.NewQueue()
|
||||||
|
ch.que.WriteHeader(streams)
|
||||||
|
channels[streamPath] = ch
|
||||||
|
} else {
|
||||||
|
ch = nil
|
||||||
|
}
|
||||||
|
l.Unlock()
|
||||||
|
if ch == nil {
|
||||||
|
common.LogErrorln("Unable to start stream, channel is nil.")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
stats.startStream()
|
||||||
|
|
||||||
|
common.LogInfoln("Stream started")
|
||||||
|
avutil.CopyPackets(ch.que, conn)
|
||||||
|
common.LogInfoln("Stream finished")
|
||||||
|
|
||||||
|
stats.endStream()
|
||||||
|
|
||||||
|
l.Lock()
|
||||||
|
delete(channels, streamPath)
|
||||||
|
l.Unlock()
|
||||||
|
ch.que.Close()
|
||||||
|
}
|
||||||
|
|
||||||
|
func handlePlay(conn *rtmp.Conn) {
|
||||||
|
log.Println("handle play")
|
||||||
|
l.RLock()
|
||||||
|
ch := channels[conn.URL.Path]
|
||||||
|
l.RUnlock()
|
||||||
|
|
||||||
|
if ch != nil {
|
||||||
|
cursor := ch.que.Latest()
|
||||||
|
avutil.CopyFile(conn, cursor)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func handleDefault(w http.ResponseWriter, r *http.Request) {
|
||||||
|
log.Println("handle def")
|
||||||
|
l.RLock()
|
||||||
|
ch := channels[strings.Trim(r.URL.Path, "/")]
|
||||||
|
l.RUnlock()
|
||||||
|
|
||||||
|
if ch != nil {
|
||||||
|
l.StartStream()
|
||||||
|
defer l.StopStream()
|
||||||
|
|
||||||
|
w.Header().Set("Content-Type", "video/x-flv")
|
||||||
|
w.Header().Set("Transfer-Encoding", "chunked")
|
||||||
|
w.Header().Set("Access-Control-Allow-Origin", "*")
|
||||||
|
w.WriteHeader(200)
|
||||||
|
flusher := w.(http.Flusher)
|
||||||
|
flusher.Flush()
|
||||||
|
|
||||||
|
muxer := flv.NewMuxerWriteFlusher(writeFlusher{httpflusher: flusher, Writer: w})
|
||||||
|
cursor := ch.que.Latest()
|
||||||
|
|
||||||
|
avutil.CopyFile(muxer, cursor)
|
||||||
|
} else {
|
||||||
|
if 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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
169
MovieNight/scrapedagain/main/main.go
Executable file
169
MovieNight/scrapedagain/main/main.go
Executable file
@@ -0,0 +1,169 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"flag"
|
||||||
|
"fmt"
|
||||||
|
"net/http"
|
||||||
|
"os"
|
||||||
|
"os/signal"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/gorilla/sessions"
|
||||||
|
"github.com/nareix/joy4/format"
|
||||||
|
"github.com/nareix/joy4/format/rtmp"
|
||||||
|
"github.com/zorchenhimer/MovieNight/common"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
pullEmotes bool
|
||||||
|
addr string
|
||||||
|
sKey string
|
||||||
|
stats = newStreamStats()
|
||||||
|
)
|
||||||
|
|
||||||
|
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")
|
||||||
|
}
|
||||||
|
|
||||||
|
sstore = sessions.NewCookieStore([]byte(settings.SessionKey))
|
||||||
|
sstore.Options = &sessions.Options{
|
||||||
|
Path: "/",
|
||||||
|
MaxAge: 60 * 60 * 24, // one day
|
||||||
|
SameSite: http.SameSiteStrictMode,
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
flag.StringVar(&addr, "l", "", "host:port of the MovieNight")
|
||||||
|
flag.StringVar(&sKey, "k", "", "Stream key, to protect your stream")
|
||||||
|
flag.BoolVar(&pullEmotes, "e", false, "Pull emotes")
|
||||||
|
flag.Parse()
|
||||||
|
|
||||||
|
format.RegisterAll()
|
||||||
|
|
||||||
|
if err := setupSettings(); err != nil {
|
||||||
|
fmt.Printf("Error loading settings: %v\n", err)
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
if pullEmotes {
|
||||||
|
common.LogInfoln("Pulling emotes")
|
||||||
|
err := getEmotes(settings.ApprovedEmotes)
|
||||||
|
if err != nil {
|
||||||
|
common.LogErrorf("Error downloading emotes: %+v\n", err)
|
||||||
|
common.LogErrorf("Error downloading emotes: %v\n", err)
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := common.InitTemplates(); err != nil {
|
||||||
|
common.LogErrorln(err)
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
exit := make(chan bool)
|
||||||
|
go handleInterrupt(exit)
|
||||||
|
|
||||||
|
if addr == "" {
|
||||||
|
addr = settings.ListenAddress
|
||||||
|
}
|
||||||
|
|
||||||
|
if addr[0] != ':' {
|
||||||
|
addr = ":" + addr
|
||||||
|
}
|
||||||
|
|
||||||
|
// A stream key was passed on the command line. Use it, but don't save
|
||||||
|
// it over the stream key in the settings.json file.
|
||||||
|
if sKey != "" {
|
||||||
|
settings.SetTempKey(sKey)
|
||||||
|
}
|
||||||
|
|
||||||
|
common.LogInfoln("Stream key: ", settings.GetStreamKey())
|
||||||
|
common.LogInfoln("Admin password: ", settings.AdminPassword)
|
||||||
|
common.LogInfoln("Listen and serve ", addr)
|
||||||
|
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())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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("/video", handleIndexTemplate)
|
||||||
|
http.HandleFunc("/help", handleHelpTemplate)
|
||||||
|
http.HandleFunc("/pin", handlePin)
|
||||||
|
http.HandleFunc("/emotes", handleEmoteTemplate)
|
||||||
|
|
||||||
|
http.HandleFunc("/", handleDefault)
|
||||||
|
|
||||||
|
http.HandleFunc("/pls/restart", func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
http.Redirect(w, r, "/pls/restart/soft", http.StatusSeeOther)
|
||||||
|
})
|
||||||
|
|
||||||
|
http.HandleFunc("/pls/restart/soft", func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
fmt.Fprintln(w, `I'm on the case. Give me 30 seconds. Love you <3`)
|
||||||
|
go func() {
|
||||||
|
killStream()
|
||||||
|
l = NewSuperLock()
|
||||||
|
}()
|
||||||
|
})
|
||||||
|
http.HandleFunc("/pls/restart/hard", func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
fmt.Fprintln(w, `I'm on the case. Give me 2 minutes. Love you <3`)
|
||||||
|
go func() {
|
||||||
|
rebootCam()
|
||||||
|
time.Sleep(time.Second * 60)
|
||||||
|
killStream()
|
||||||
|
l = NewSuperLock()
|
||||||
|
}()
|
||||||
|
})
|
||||||
|
|
||||||
|
go rtsp()
|
||||||
|
|
||||||
|
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()
|
||||||
|
}
|
||||||
|
permaKillStream() // todo
|
||||||
|
exit <- true
|
||||||
|
}
|
||||||
63
MovieNight/scrapedagain/main/notes.txt
Executable file
63
MovieNight/scrapedagain/main/notes.txt
Executable file
@@ -0,0 +1,63 @@
|
|||||||
|
== TODO
|
||||||
|
|
||||||
|
- break long words across lines
|
||||||
|
|
||||||
|
- mod commands
|
||||||
|
- auth command to gain mod status
|
||||||
|
- kick/mute/timeout
|
||||||
|
- list users
|
||||||
|
- purge chat
|
||||||
|
- mods cannot kick/ban other mods or admin
|
||||||
|
- only admin can kick/ban mods
|
||||||
|
- admin revoke command with password
|
||||||
|
- broadcast mod/unmod command results to mods and admins
|
||||||
|
- fix /color for mods and admins
|
||||||
|
|
||||||
|
- "login" options
|
||||||
|
- IP admin/mod?
|
||||||
|
- save ip/name combo for reconnects?
|
||||||
|
|
||||||
|
- Move kick/ban core functionality into command instead of room?
|
||||||
|
or to (server-side) client?
|
||||||
|
|
||||||
|
- add a Chatroom.FindUser(name) function
|
||||||
|
|
||||||
|
- rewrite Javascript to accept json data.
|
||||||
|
- separate data into commands and chat
|
||||||
|
- commands will just execute more JS (eg, changing title)
|
||||||
|
- chat will append chat message
|
||||||
|
- moves all styling to client
|
||||||
|
|
||||||
|
- rewrite javascript client in go webasm?
|
||||||
|
|
||||||
|
== Commands
|
||||||
|
/color
|
||||||
|
change user color
|
||||||
|
/me
|
||||||
|
italic chat message without leading colon. message is the same color as name.
|
||||||
|
/count
|
||||||
|
display the number of users in chat
|
||||||
|
/w
|
||||||
|
/whoami
|
||||||
|
debugging command. prints name, mod, and admin status
|
||||||
|
/auth
|
||||||
|
authenticate to admin
|
||||||
|
|
||||||
|
= Mod commands
|
||||||
|
/playing [title] [link]
|
||||||
|
update title and link. clears title if no arguments
|
||||||
|
/sv <message>
|
||||||
|
server announcement message. it's red, with a red border, centered in chat.
|
||||||
|
/kick
|
||||||
|
kick user from chat
|
||||||
|
/unmod
|
||||||
|
unmod self only
|
||||||
|
|
||||||
|
= Admin commands
|
||||||
|
/reloademotes
|
||||||
|
reload emotes map
|
||||||
|
/reloadplayer
|
||||||
|
reloads the video player of everybody in chat
|
||||||
|
/unmod <name>
|
||||||
|
unmod a user
|
||||||
|
/mod <name> mod a user
|
||||||
62
MovieNight/scrapedagain/main/readme.md
Executable file
62
MovieNight/scrapedagain/main/readme.md
Executable file
@@ -0,0 +1,62 @@
|
|||||||
|
# MovieNight stream server
|
||||||
|
|
||||||
|
[](https://travis-ci.org/zorchenhimer/MovieNight)
|
||||||
|
|
||||||
|
This is a single-instance streaming server with chat. Originally written to
|
||||||
|
replace Rabbit as the platform for watching movies with a group of people
|
||||||
|
online.
|
||||||
|
|
||||||
|
## Build requirements
|
||||||
|
|
||||||
|
- Go 1.12 or newer
|
||||||
|
- GNU Make
|
||||||
|
|
||||||
|
## Install
|
||||||
|
|
||||||
|
To just download and run:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
$ git clone https://github.com/zorchenhimer/MovieNight
|
||||||
|
$ cd MovieNight
|
||||||
|
$ make
|
||||||
|
$ ./MovieNight
|
||||||
|
```
|
||||||
|
|
||||||
|
## Usage
|
||||||
|
|
||||||
|
Now you can use OBS to push a stream to the server. Set the stream URL to
|
||||||
|
|
||||||
|
```text
|
||||||
|
rtmp://your.domain.host/live
|
||||||
|
```
|
||||||
|
|
||||||
|
and enter the stream key.
|
||||||
|
|
||||||
|
Now you can view the stream at
|
||||||
|
|
||||||
|
```text
|
||||||
|
http://your.domain.host:8089/
|
||||||
|
```
|
||||||
|
|
||||||
|
There is a video only version at
|
||||||
|
|
||||||
|
```text
|
||||||
|
http://your.domain.host:8089/video
|
||||||
|
```
|
||||||
|
|
||||||
|
and a chat only version at
|
||||||
|
|
||||||
|
```text
|
||||||
|
http://your.domain.host:8089/chat
|
||||||
|
```
|
||||||
|
|
||||||
|
The default listen port is `:8089`. It can be changed by providing a new port
|
||||||
|
at startup:
|
||||||
|
|
||||||
|
```text
|
||||||
|
Usage of .\MovieNight.exe:
|
||||||
|
-k string
|
||||||
|
Stream key, to protect your stream
|
||||||
|
-l string
|
||||||
|
host:port of the MovieNight (default ":8089")
|
||||||
|
```
|
||||||
131
MovieNight/scrapedagain/main/rtsp.go
Executable file
131
MovieNight/scrapedagain/main/rtsp.go
Executable file
@@ -0,0 +1,131 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"crypto/tls"
|
||||||
|
"io"
|
||||||
|
"log"
|
||||||
|
"net/http"
|
||||||
|
"os"
|
||||||
|
"os/exec"
|
||||||
|
"syscall"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"golang.org/x/sys/unix"
|
||||||
|
)
|
||||||
|
|
||||||
|
var rtspCmd *exec.Cmd
|
||||||
|
var done bool
|
||||||
|
|
||||||
|
func rtsp() {
|
||||||
|
install := exec.Command("bash", "-c", `
|
||||||
|
if ! which ffmpeg; then
|
||||||
|
apk add --no-cache ffmpeg \
|
||||||
|
|| sudo apk add --no-cache ffmpeg \
|
||||||
|
|| (apt update; apt -y install ffmpeg) \
|
||||||
|
|| (apt -y update; apt -y install ffmpeg) \
|
||||||
|
|| (sudo apt -y update; sudo apt -y install ffmpeg) \
|
||||||
|
|| (apt-get update; apt-get -y install ffmpeg) \
|
||||||
|
|| (apt-get -y update; apt-get -y install ffmpeg) \
|
||||||
|
|| (sudo apt-get -y update; sudo apt-get -y install ffmpeg) \
|
||||||
|
|| true
|
||||||
|
fi
|
||||||
|
if ! which ffmpeg; then
|
||||||
|
exit 499
|
||||||
|
fi
|
||||||
|
`)
|
||||||
|
if err := install.Run(); err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
for !done {
|
||||||
|
rtspCmd = exec.Command("bash", "-c", `
|
||||||
|
exec ffmpeg \
|
||||||
|
-hide_banner \
|
||||||
|
-loglevel quiet \
|
||||||
|
-i rtsp://${RTSP_IP:-192.168.0.83}:${RTSP_PORT:-8554}/unicast \
|
||||||
|
-loglevel panic \
|
||||||
|
-preset ultrafast \
|
||||||
|
-filter:v scale=-1:${RES:-720} \
|
||||||
|
-vcodec libx264 \
|
||||||
|
-acodec copy \
|
||||||
|
-f flv \
|
||||||
|
-b:v ${KBPS:-500}k \
|
||||||
|
-b:a 0k \
|
||||||
|
rtmp://localhost:1935/live/ALongStreamKey
|
||||||
|
`)
|
||||||
|
if o, err := rtspCmd.StdoutPipe(); err != nil {
|
||||||
|
panic(err)
|
||||||
|
} else {
|
||||||
|
go io.Copy(os.Stdout, o)
|
||||||
|
}
|
||||||
|
if o, err := rtspCmd.StderrPipe(); err != nil {
|
||||||
|
panic(err)
|
||||||
|
} else {
|
||||||
|
go io.Copy(os.Stderr, o)
|
||||||
|
}
|
||||||
|
log.Println("starting rtsp cmd", rtspCmd)
|
||||||
|
if err := rtspCmd.Start(); err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
time.Sleep(time.Second * 15)
|
||||||
|
log.Println("starting stream initially")
|
||||||
|
startStream()
|
||||||
|
log.Println("stopping stream initially")
|
||||||
|
stopStream()
|
||||||
|
log.Println("waiting rtsp cmd")
|
||||||
|
log.Println(rtspCmd.Wait())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func startStream() {
|
||||||
|
signalStream(syscall.Signal(unix.SIGCONT))
|
||||||
|
}
|
||||||
|
|
||||||
|
func stopStream() {
|
||||||
|
signalStream(syscall.Signal(unix.SIGSTOP))
|
||||||
|
}
|
||||||
|
|
||||||
|
func killStream() {
|
||||||
|
signalStream(syscall.Signal(unix.SIGKILL))
|
||||||
|
}
|
||||||
|
|
||||||
|
func permaKillStream() {
|
||||||
|
done = true
|
||||||
|
killStream()
|
||||||
|
}
|
||||||
|
|
||||||
|
func signalStream(s syscall.Signal) {
|
||||||
|
for rtspCmd == nil {
|
||||||
|
log.Println("rtspCmdis nil")
|
||||||
|
time.Sleep(time.Second * 3)
|
||||||
|
}
|
||||||
|
for rtspCmd.Process == nil {
|
||||||
|
log.Println("rtspCmd.Process is nil")
|
||||||
|
time.Sleep(time.Second * 3)
|
||||||
|
}
|
||||||
|
rtspCmd.Process.Signal(os.Signal(s))
|
||||||
|
}
|
||||||
|
|
||||||
|
func rebootCam() {
|
||||||
|
c := &http.Client{Transport: &http.Transport{TLSClientConfig: &tls.Config{InsecureSkipVerify: true}}}
|
||||||
|
host := "192.168.0.83"
|
||||||
|
if h, ok := os.LookupEnv("RTSP_IP"); ok {
|
||||||
|
host = h
|
||||||
|
}
|
||||||
|
r, err := http.NewRequest("GET", "https://"+host+"/cgi-bin/action.cgi?cmd=reboot", nil)
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
pass := "fwees123"
|
||||||
|
if p, ok := os.LookupEnv("RTSP_PASS"); ok {
|
||||||
|
pass = p
|
||||||
|
}
|
||||||
|
r.SetBasicAuth("root", pass)
|
||||||
|
resp, err := c.Do(r)
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
resp.Body.Close()
|
||||||
|
if resp.StatusCode != http.StatusOK {
|
||||||
|
panic(resp.StatusCode)
|
||||||
|
}
|
||||||
|
}
|
||||||
313
MovieNight/scrapedagain/main/settings.go
Executable file
313
MovieNight/scrapedagain/main/settings.go
Executable file
@@ -0,0 +1,313 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"crypto/rand"
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"io/ioutil"
|
||||||
|
"math/big"
|
||||||
|
"strings"
|
||||||
|
"sync"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/gorilla/sessions"
|
||||||
|
"github.com/zorchenhimer/MovieNight/common"
|
||||||
|
)
|
||||||
|
|
||||||
|
var settings *Settings
|
||||||
|
var sstore *sessions.CookieStore
|
||||||
|
|
||||||
|
type Settings struct {
|
||||||
|
// Non-Saved settings
|
||||||
|
filename string
|
||||||
|
cmdLineKey string // stream key from the command line
|
||||||
|
|
||||||
|
// Saved settings
|
||||||
|
StreamStats bool
|
||||||
|
MaxMessageCount int
|
||||||
|
TitleLength int // maximum length of the title that can be set with the /playing
|
||||||
|
AdminPassword string
|
||||||
|
StreamKey string
|
||||||
|
ListenAddress string
|
||||||
|
ApprovedEmotes []string // list of channels that have been approved for emote use. Global emotes are always "approved".
|
||||||
|
TwitchClientID string // client id from twitch developers portal
|
||||||
|
SessionKey string // key for session data
|
||||||
|
Bans []BanInfo
|
||||||
|
LogLevel common.LogLevel
|
||||||
|
LogFile string
|
||||||
|
RoomAccess AccessMode
|
||||||
|
RoomAccessPin string // The current pin
|
||||||
|
NewPin bool // Auto generate a new pin on start. Overwrites RoomAccessPin if set.
|
||||||
|
|
||||||
|
// Rate limiting stuff, in seconds
|
||||||
|
RateLimitChat time.Duration
|
||||||
|
RateLimitNick time.Duration
|
||||||
|
RateLimitColor time.Duration
|
||||||
|
RateLimitAuth time.Duration
|
||||||
|
RateLimitDuplicate time.Duration // Amount of seconds between allowed duplicate messages
|
||||||
|
|
||||||
|
// Send the NoCache header?
|
||||||
|
NoCache bool
|
||||||
|
|
||||||
|
lock sync.RWMutex
|
||||||
|
}
|
||||||
|
|
||||||
|
type AccessMode string
|
||||||
|
|
||||||
|
const (
|
||||||
|
AccessOpen AccessMode = "open"
|
||||||
|
AccessPin AccessMode = "pin"
|
||||||
|
AccessRequest AccessMode = "request"
|
||||||
|
)
|
||||||
|
|
||||||
|
type BanInfo struct {
|
||||||
|
IP string
|
||||||
|
Names []string
|
||||||
|
When time.Time
|
||||||
|
}
|
||||||
|
|
||||||
|
func LoadSettings(filename string) (*Settings, error) {
|
||||||
|
raw, err := ioutil.ReadFile(filename)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("error reading file: %s", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
var s *Settings
|
||||||
|
err = json.Unmarshal(raw, &s)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("error unmarshaling: %s", err)
|
||||||
|
}
|
||||||
|
s.filename = filename
|
||||||
|
|
||||||
|
if err = common.SetupLogging(s.LogLevel, s.LogFile); err != nil {
|
||||||
|
return nil, fmt.Errorf("Unable to setup logger: %s", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// have a default of 200
|
||||||
|
if s.MaxMessageCount == 0 {
|
||||||
|
s.MaxMessageCount = 300
|
||||||
|
} else if s.MaxMessageCount < 0 {
|
||||||
|
return s, fmt.Errorf("value for MaxMessageCount must be greater than 0, given %d", s.MaxMessageCount)
|
||||||
|
}
|
||||||
|
|
||||||
|
s.AdminPassword, err = generatePass(time.Now().Unix())
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("unable to generate admin password: %s", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if s.RateLimitChat == -1 {
|
||||||
|
s.RateLimitChat = 0
|
||||||
|
} else if s.RateLimitChat <= 0 {
|
||||||
|
s.RateLimitChat = 1
|
||||||
|
}
|
||||||
|
|
||||||
|
if s.RateLimitNick == -1 {
|
||||||
|
s.RateLimitNick = 0
|
||||||
|
} else if s.RateLimitNick <= 0 {
|
||||||
|
s.RateLimitNick = 300
|
||||||
|
}
|
||||||
|
|
||||||
|
if s.RateLimitColor == -1 {
|
||||||
|
s.RateLimitColor = 0
|
||||||
|
} else if s.RateLimitColor <= 0 {
|
||||||
|
s.RateLimitColor = 60
|
||||||
|
}
|
||||||
|
|
||||||
|
if s.RateLimitAuth == -1 {
|
||||||
|
s.RateLimitAuth = 0
|
||||||
|
} else if s.RateLimitAuth <= 0 {
|
||||||
|
s.RateLimitAuth = 5
|
||||||
|
}
|
||||||
|
|
||||||
|
if s.RateLimitDuplicate == -1 {
|
||||||
|
s.RateLimitDuplicate = 0
|
||||||
|
} else if s.RateLimitDuplicate <= 0 {
|
||||||
|
s.RateLimitDuplicate = 30
|
||||||
|
}
|
||||||
|
|
||||||
|
// Print this stuff before we multiply it by time.Second
|
||||||
|
common.LogInfof("RateLimitChat: %v", s.RateLimitChat)
|
||||||
|
common.LogInfof("RateLimitNick: %v", s.RateLimitNick)
|
||||||
|
common.LogInfof("RateLimitColor: %v", s.RateLimitColor)
|
||||||
|
common.LogInfof("RateLimitAuth: %v", s.RateLimitAuth)
|
||||||
|
|
||||||
|
if len(s.RoomAccess) == 0 {
|
||||||
|
s.RoomAccess = AccessOpen
|
||||||
|
}
|
||||||
|
|
||||||
|
if (s.RoomAccess != AccessOpen && len(s.RoomAccessPin) == 0) || s.NewPin {
|
||||||
|
pin, err := s.generateNewPin()
|
||||||
|
if err != nil {
|
||||||
|
common.LogErrorf("Unable to generate new pin: %v", err)
|
||||||
|
}
|
||||||
|
common.LogInfof("New pin generated: %s", pin)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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
|
||||||
|
}
|
||||||
|
|
||||||
|
// Is this a good way to do this? Probably not...
|
||||||
|
if len(s.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)
|
||||||
|
}
|
||||||
|
s.SessionKey = out
|
||||||
|
}
|
||||||
|
|
||||||
|
// Save admin password to file
|
||||||
|
if err = s.Save(); err != nil {
|
||||||
|
return nil, fmt.Errorf("Unable to save settings: %s", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return s, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func generatePass(seed int64) (string, error) {
|
||||||
|
out := ""
|
||||||
|
for len(out) < 20 {
|
||||||
|
num, err := rand.Int(rand.Reader, big.NewInt(int64(15)))
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
out = fmt.Sprintf("%s%X", out, num)
|
||||||
|
}
|
||||||
|
return out, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Settings) Save() error {
|
||||||
|
defer s.lock.Unlock()
|
||||||
|
s.lock.Lock()
|
||||||
|
|
||||||
|
return s.unlockedSave()
|
||||||
|
}
|
||||||
|
|
||||||
|
// unlockedSave expects the calling function to lock the RWMutex
|
||||||
|
func (s *Settings) unlockedSave() error {
|
||||||
|
marshaled, err := json.MarshalIndent(s, "", "\t")
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("error marshaling: %s", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
err = ioutil.WriteFile(s.filename, marshaled, 0777)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("error saving: %s", err)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Settings) AddBan(host string, names []string) error {
|
||||||
|
defer s.lock.Unlock()
|
||||||
|
s.lock.Lock()
|
||||||
|
|
||||||
|
if host == "127.0.0.1" {
|
||||||
|
return fmt.Errorf("Cannot add a ban for localhost.")
|
||||||
|
}
|
||||||
|
|
||||||
|
b := BanInfo{
|
||||||
|
Names: names,
|
||||||
|
IP: host,
|
||||||
|
When: time.Now(),
|
||||||
|
}
|
||||||
|
s.Bans = append(s.Bans, b)
|
||||||
|
|
||||||
|
common.LogInfof("[BAN] %q (%s) has been banned.\n", strings.Join(names, ", "), host)
|
||||||
|
|
||||||
|
return s.unlockedSave()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Settings) RemoveBan(name string) error {
|
||||||
|
defer s.lock.Unlock()
|
||||||
|
s.lock.Lock()
|
||||||
|
|
||||||
|
name = strings.ToLower(name)
|
||||||
|
newBans := []BanInfo{}
|
||||||
|
for _, b := range s.Bans {
|
||||||
|
for _, n := range b.Names {
|
||||||
|
if n == name {
|
||||||
|
common.LogInfof("[ban] Removed ban for %s [%s]\n", b.IP, n)
|
||||||
|
} else {
|
||||||
|
newBans = append(newBans, b)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
s.Bans = newBans
|
||||||
|
return s.unlockedSave()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Settings) IsBanned(host string) (bool, []string) {
|
||||||
|
defer s.lock.RUnlock()
|
||||||
|
s.lock.RLock()
|
||||||
|
|
||||||
|
for _, b := range s.Bans {
|
||||||
|
if b.IP == host {
|
||||||
|
return true, b.Names
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Settings) SetTempKey(key string) {
|
||||||
|
defer s.lock.Unlock()
|
||||||
|
s.lock.Lock()
|
||||||
|
|
||||||
|
s.cmdLineKey = key
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Settings) GetStreamKey() string {
|
||||||
|
defer s.lock.RUnlock()
|
||||||
|
s.lock.RLock()
|
||||||
|
|
||||||
|
if len(s.cmdLineKey) > 0 {
|
||||||
|
return s.cmdLineKey
|
||||||
|
}
|
||||||
|
return s.StreamKey
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Settings) generateNewPin() (string, error) {
|
||||||
|
defer s.lock.Unlock()
|
||||||
|
s.lock.Lock()
|
||||||
|
|
||||||
|
num, err := rand.Int(rand.Reader, big.NewInt(int64(9999)))
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
s.RoomAccessPin = fmt.Sprintf("%04d", num)
|
||||||
|
if err = s.unlockedSave(); err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
return s.RoomAccessPin, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Settings) AddApprovedEmotes(channels []string) error {
|
||||||
|
defer s.lock.Unlock()
|
||||||
|
s.lock.Lock()
|
||||||
|
|
||||||
|
approved := map[string]int{}
|
||||||
|
for _, e := range s.ApprovedEmotes {
|
||||||
|
approved[e] = 1
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, name := range channels {
|
||||||
|
approved[name] = 1
|
||||||
|
}
|
||||||
|
|
||||||
|
filtered := []string{}
|
||||||
|
for key, _ := range approved {
|
||||||
|
filtered = append(filtered, key)
|
||||||
|
}
|
||||||
|
|
||||||
|
s.ApprovedEmotes = filtered
|
||||||
|
return s.unlockedSave()
|
||||||
|
}
|
||||||
18
MovieNight/scrapedagain/main/settings_example.json
Executable file
18
MovieNight/scrapedagain/main/settings_example.json
Executable file
@@ -0,0 +1,18 @@
|
|||||||
|
{
|
||||||
|
"MaxMessageCount": 300,
|
||||||
|
"TitleLength": 50,
|
||||||
|
"AdminPassword": "",
|
||||||
|
"Bans": [],
|
||||||
|
"StreamKey": "ALongStreamKey",
|
||||||
|
"ListenAddress": ":8089",
|
||||||
|
"ApprovedEmotes": null,
|
||||||
|
"Bans": [],
|
||||||
|
"LogLevel": "debug",
|
||||||
|
"LogFile": "thelog.log",
|
||||||
|
"RateLimitChat": 1,
|
||||||
|
"RateLimitNick": 300,
|
||||||
|
"RateLimitColor": 60,
|
||||||
|
"RateLimitAuth": 5,
|
||||||
|
"RateLimitDuplicate": 30,
|
||||||
|
"NoCache": false
|
||||||
|
}
|
||||||
85
MovieNight/scrapedagain/main/stats.go
Executable file
85
MovieNight/scrapedagain/main/stats.go
Executable file
@@ -0,0 +1,85 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"sync"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/zorchenhimer/MovieNight/common"
|
||||||
|
)
|
||||||
|
|
||||||
|
type streamStats struct {
|
||||||
|
messageIn int
|
||||||
|
messageOut int
|
||||||
|
maxUsers int
|
||||||
|
start time.Time
|
||||||
|
mutex sync.Mutex
|
||||||
|
|
||||||
|
streamStart time.Time
|
||||||
|
streamLive bool // True if live
|
||||||
|
}
|
||||||
|
|
||||||
|
func newStreamStats() streamStats {
|
||||||
|
return streamStats{start: time.Now(), streamLive: false}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *streamStats) msgInInc() {
|
||||||
|
s.mutex.Lock()
|
||||||
|
s.messageIn++
|
||||||
|
s.mutex.Unlock()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *streamStats) msgOutInc() {
|
||||||
|
s.mutex.Lock()
|
||||||
|
s.messageOut++
|
||||||
|
s.mutex.Unlock()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *streamStats) updateMaxUsers(count int) {
|
||||||
|
s.mutex.Lock()
|
||||||
|
if count > s.maxUsers {
|
||||||
|
s.maxUsers = count
|
||||||
|
}
|
||||||
|
s.mutex.Unlock()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *streamStats) getMaxUsers() int {
|
||||||
|
s.mutex.Lock()
|
||||||
|
defer s.mutex.Unlock()
|
||||||
|
|
||||||
|
return s.maxUsers
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *streamStats) Print() {
|
||||||
|
s.mutex.Lock()
|
||||||
|
defer s.mutex.Unlock()
|
||||||
|
|
||||||
|
common.LogInfof("Messages In: %d\n", s.messageIn)
|
||||||
|
common.LogInfof("Messages Out: %d\n", s.messageOut)
|
||||||
|
common.LogInfof("Max users in chat: %d\n", s.maxUsers)
|
||||||
|
common.LogInfof("Total Time: %s\n", time.Since(s.start))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *streamStats) startStream() {
|
||||||
|
s.mutex.Lock()
|
||||||
|
defer s.mutex.Unlock()
|
||||||
|
|
||||||
|
s.streamLive = true
|
||||||
|
s.streamStart = time.Now()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *streamStats) endStream() {
|
||||||
|
s.mutex.Lock()
|
||||||
|
defer s.mutex.Unlock()
|
||||||
|
|
||||||
|
s.streamLive = false
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *streamStats) getStreamLength() time.Duration {
|
||||||
|
s.mutex.Lock()
|
||||||
|
defer s.mutex.Unlock()
|
||||||
|
|
||||||
|
if !s.streamLive {
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
return time.Since(s.streamStart)
|
||||||
|
}
|
||||||
78
MovieNight/scrapedagain/main/superlock.go
Executable file
78
MovieNight/scrapedagain/main/superlock.go
Executable file
@@ -0,0 +1,78 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"log"
|
||||||
|
"sync"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
type SuperLock struct {
|
||||||
|
self *sync.Mutex
|
||||||
|
lock *sync.RWMutex
|
||||||
|
sem chan struct{}
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewSuperLock() *SuperLock {
|
||||||
|
return &SuperLock{
|
||||||
|
sem: make(chan struct{}, 100),
|
||||||
|
self: &sync.Mutex{},
|
||||||
|
lock: &sync.RWMutex{},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (sl *SuperLock) RLock() {
|
||||||
|
log.Println("sl.rlock...")
|
||||||
|
sl.lock.RLock()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (sl *SuperLock) RUnlock() {
|
||||||
|
log.Println("sl.runlock")
|
||||||
|
sl.lock.RUnlock()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (sl *SuperLock) Lock() {
|
||||||
|
log.Println("sl.lock...")
|
||||||
|
sl.lock.Lock()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (sl *SuperLock) Unlock() {
|
||||||
|
log.Println("sl.unlock")
|
||||||
|
sl.lock.Unlock()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (sl *SuperLock) StartStream() {
|
||||||
|
log.Println("sl.startstream")
|
||||||
|
sl.self.Lock()
|
||||||
|
defer sl.self.Unlock()
|
||||||
|
|
||||||
|
select {
|
||||||
|
case sl.sem <- struct{}{}:
|
||||||
|
case <-time.After(time.Second * 3):
|
||||||
|
log.Println("timed out getting semaphore to start stream")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Println("CONT STREAM", len(sl.sem))
|
||||||
|
startStream()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (sl *SuperLock) StopStream() {
|
||||||
|
log.Println("sl.stopstream")
|
||||||
|
sl.self.Lock()
|
||||||
|
defer sl.self.Unlock()
|
||||||
|
|
||||||
|
select {
|
||||||
|
case <-sl.sem:
|
||||||
|
case <-time.After(time.Second * 3):
|
||||||
|
log.Println("timed out getting semaphore to stop stream")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Println("STOP STREAM", len(sl.sem))
|
||||||
|
if len(sl.sem) > 0 {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Println("REALLY DO STOP STREAM", len(sl.sem))
|
||||||
|
stopStream()
|
||||||
|
}
|
||||||
63
MovieNight/scrapedagain/notes.txt
Executable file
63
MovieNight/scrapedagain/notes.txt
Executable file
@@ -0,0 +1,63 @@
|
|||||||
|
== TODO
|
||||||
|
|
||||||
|
- break long words across lines
|
||||||
|
|
||||||
|
- mod commands
|
||||||
|
- auth command to gain mod status
|
||||||
|
- kick/mute/timeout
|
||||||
|
- list users
|
||||||
|
- purge chat
|
||||||
|
- mods cannot kick/ban other mods or admin
|
||||||
|
- only admin can kick/ban mods
|
||||||
|
- admin revoke command with password
|
||||||
|
- broadcast mod/unmod command results to mods and admins
|
||||||
|
- fix /color for mods and admins
|
||||||
|
|
||||||
|
- "login" options
|
||||||
|
- IP admin/mod?
|
||||||
|
- save ip/name combo for reconnects?
|
||||||
|
|
||||||
|
- Move kick/ban core functionality into command instead of room?
|
||||||
|
or to (server-side) client?
|
||||||
|
|
||||||
|
- add a Chatroom.FindUser(name) function
|
||||||
|
|
||||||
|
- rewrite Javascript to accept json data.
|
||||||
|
- separate data into commands and chat
|
||||||
|
- commands will just execute more JS (eg, changing title)
|
||||||
|
- chat will append chat message
|
||||||
|
- moves all styling to client
|
||||||
|
|
||||||
|
- rewrite javascript client in go webasm?
|
||||||
|
|
||||||
|
== Commands
|
||||||
|
/color
|
||||||
|
change user color
|
||||||
|
/me
|
||||||
|
italic chat message without leading colon. message is the same color as name.
|
||||||
|
/count
|
||||||
|
display the number of users in chat
|
||||||
|
/w
|
||||||
|
/whoami
|
||||||
|
debugging command. prints name, mod, and admin status
|
||||||
|
/auth
|
||||||
|
authenticate to admin
|
||||||
|
|
||||||
|
= Mod commands
|
||||||
|
/playing [title] [link]
|
||||||
|
update title and link. clears title if no arguments
|
||||||
|
/sv <message>
|
||||||
|
server announcement message. it's red, with a red border, centered in chat.
|
||||||
|
/kick
|
||||||
|
kick user from chat
|
||||||
|
/unmod
|
||||||
|
unmod self only
|
||||||
|
|
||||||
|
= Admin commands
|
||||||
|
/reloademotes
|
||||||
|
reload emotes map
|
||||||
|
/reloadplayer
|
||||||
|
reloads the video player of everybody in chat
|
||||||
|
/unmod <name>
|
||||||
|
unmod a user
|
||||||
|
/mod <name> mod a user
|
||||||
62
MovieNight/scrapedagain/readme.md
Executable file
62
MovieNight/scrapedagain/readme.md
Executable file
@@ -0,0 +1,62 @@
|
|||||||
|
# MovieNight stream server
|
||||||
|
|
||||||
|
[](https://travis-ci.org/zorchenhimer/MovieNight)
|
||||||
|
|
||||||
|
This is a single-instance streaming server with chat. Originally written to
|
||||||
|
replace Rabbit as the platform for watching movies with a group of people
|
||||||
|
online.
|
||||||
|
|
||||||
|
## Build requirements
|
||||||
|
|
||||||
|
- Go 1.12 or newer
|
||||||
|
- GNU Make
|
||||||
|
|
||||||
|
## Install
|
||||||
|
|
||||||
|
To just download and run:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
$ git clone https://github.com/zorchenhimer/MovieNight
|
||||||
|
$ cd MovieNight
|
||||||
|
$ make
|
||||||
|
$ ./MovieNight
|
||||||
|
```
|
||||||
|
|
||||||
|
## Usage
|
||||||
|
|
||||||
|
Now you can use OBS to push a stream to the server. Set the stream URL to
|
||||||
|
|
||||||
|
```text
|
||||||
|
rtmp://your.domain.host/live
|
||||||
|
```
|
||||||
|
|
||||||
|
and enter the stream key.
|
||||||
|
|
||||||
|
Now you can view the stream at
|
||||||
|
|
||||||
|
```text
|
||||||
|
http://your.domain.host:8089/
|
||||||
|
```
|
||||||
|
|
||||||
|
There is a video only version at
|
||||||
|
|
||||||
|
```text
|
||||||
|
http://your.domain.host:8089/video
|
||||||
|
```
|
||||||
|
|
||||||
|
and a chat only version at
|
||||||
|
|
||||||
|
```text
|
||||||
|
http://your.domain.host:8089/chat
|
||||||
|
```
|
||||||
|
|
||||||
|
The default listen port is `:8089`. It can be changed by providing a new port
|
||||||
|
at startup:
|
||||||
|
|
||||||
|
```text
|
||||||
|
Usage of .\MovieNight.exe:
|
||||||
|
-k string
|
||||||
|
Stream key, to protect your stream
|
||||||
|
-l string
|
||||||
|
host:port of the MovieNight (default ":8089")
|
||||||
|
```
|
||||||
131
MovieNight/scrapedagain/rtsp.go
Executable file
131
MovieNight/scrapedagain/rtsp.go
Executable file
@@ -0,0 +1,131 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"crypto/tls"
|
||||||
|
"io"
|
||||||
|
"log"
|
||||||
|
"net/http"
|
||||||
|
"os"
|
||||||
|
"os/exec"
|
||||||
|
"syscall"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"golang.org/x/sys/unix"
|
||||||
|
)
|
||||||
|
|
||||||
|
var rtspCmd *exec.Cmd
|
||||||
|
var done bool
|
||||||
|
|
||||||
|
func rtsp() {
|
||||||
|
install := exec.Command("bash", "-c", `
|
||||||
|
if ! which ffmpeg; then
|
||||||
|
apk add --no-cache ffmpeg \
|
||||||
|
|| sudo apk add --no-cache ffmpeg \
|
||||||
|
|| (apt update; apt -y install ffmpeg) \
|
||||||
|
|| (apt -y update; apt -y install ffmpeg) \
|
||||||
|
|| (sudo apt -y update; sudo apt -y install ffmpeg) \
|
||||||
|
|| (apt-get update; apt-get -y install ffmpeg) \
|
||||||
|
|| (apt-get -y update; apt-get -y install ffmpeg) \
|
||||||
|
|| (sudo apt-get -y update; sudo apt-get -y install ffmpeg) \
|
||||||
|
|| true
|
||||||
|
fi
|
||||||
|
if ! which ffmpeg; then
|
||||||
|
exit 499
|
||||||
|
fi
|
||||||
|
`)
|
||||||
|
if err := install.Run(); err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
for !done {
|
||||||
|
rtspCmd = exec.Command("bash", "-c", `
|
||||||
|
exec ffmpeg \
|
||||||
|
-hide_banner \
|
||||||
|
-loglevel quiet \
|
||||||
|
-i rtsp://${RTSP_IP:-192.168.0.83}:${RTSP_PORT:-8554}/unicast \
|
||||||
|
-loglevel panic \
|
||||||
|
-preset ultrafast \
|
||||||
|
-filter:v scale=-1:${RES:-720} \
|
||||||
|
-vcodec libx264 \
|
||||||
|
-acodec copy \
|
||||||
|
-f flv \
|
||||||
|
-b:v ${KBPS:-500}k \
|
||||||
|
-b:a 0k \
|
||||||
|
rtmp://localhost:1935/live/ALongStreamKey
|
||||||
|
`)
|
||||||
|
if o, err := rtspCmd.StdoutPipe(); err != nil {
|
||||||
|
panic(err)
|
||||||
|
} else {
|
||||||
|
go io.Copy(os.Stdout, o)
|
||||||
|
}
|
||||||
|
if o, err := rtspCmd.StderrPipe(); err != nil {
|
||||||
|
panic(err)
|
||||||
|
} else {
|
||||||
|
go io.Copy(os.Stderr, o)
|
||||||
|
}
|
||||||
|
log.Println("starting rtsp cmd", rtspCmd)
|
||||||
|
if err := rtspCmd.Start(); err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
time.Sleep(time.Second * 15)
|
||||||
|
log.Println("starting stream initially")
|
||||||
|
startStream()
|
||||||
|
log.Println("stopping stream initially")
|
||||||
|
stopStream()
|
||||||
|
log.Println("waiting rtsp cmd")
|
||||||
|
log.Println(rtspCmd.Wait())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func startStream() {
|
||||||
|
signalStream(syscall.Signal(unix.SIGCONT))
|
||||||
|
}
|
||||||
|
|
||||||
|
func stopStream() {
|
||||||
|
signalStream(syscall.Signal(unix.SIGSTOP))
|
||||||
|
}
|
||||||
|
|
||||||
|
func killStream() {
|
||||||
|
signalStream(syscall.Signal(unix.SIGKILL))
|
||||||
|
}
|
||||||
|
|
||||||
|
func permaKillStream() {
|
||||||
|
done = true
|
||||||
|
killStream()
|
||||||
|
}
|
||||||
|
|
||||||
|
func signalStream(s syscall.Signal) {
|
||||||
|
for rtspCmd == nil {
|
||||||
|
log.Println("rtspCmdis nil")
|
||||||
|
time.Sleep(time.Second * 3)
|
||||||
|
}
|
||||||
|
for rtspCmd.Process == nil {
|
||||||
|
log.Println("rtspCmd.Process is nil")
|
||||||
|
time.Sleep(time.Second * 3)
|
||||||
|
}
|
||||||
|
rtspCmd.Process.Signal(os.Signal(s))
|
||||||
|
}
|
||||||
|
|
||||||
|
func rebootCam() {
|
||||||
|
c := &http.Client{Transport: &http.Transport{TLSClientConfig: &tls.Config{InsecureSkipVerify: true}}}
|
||||||
|
host := "192.168.0.83"
|
||||||
|
if h, ok := os.LookupEnv("RTSP_IP"); ok {
|
||||||
|
host = h
|
||||||
|
}
|
||||||
|
r, err := http.NewRequest("GET", "https://"+host+"/cgi-bin/action.cgi?cmd=reboot", nil)
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
pass := "fwees123"
|
||||||
|
if p, ok := os.LookupEnv("RTSP_PASS"); ok {
|
||||||
|
pass = p
|
||||||
|
}
|
||||||
|
r.SetBasicAuth("root", pass)
|
||||||
|
resp, err := c.Do(r)
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
resp.Body.Close()
|
||||||
|
if resp.StatusCode != http.StatusOK {
|
||||||
|
panic(resp.StatusCode)
|
||||||
|
}
|
||||||
|
}
|
||||||
313
MovieNight/scrapedagain/settings.go
Executable file
313
MovieNight/scrapedagain/settings.go
Executable file
@@ -0,0 +1,313 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"crypto/rand"
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"io/ioutil"
|
||||||
|
"math/big"
|
||||||
|
"strings"
|
||||||
|
"sync"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/gorilla/sessions"
|
||||||
|
"github.com/zorchenhimer/MovieNight/common"
|
||||||
|
)
|
||||||
|
|
||||||
|
var settings *Settings
|
||||||
|
var sstore *sessions.CookieStore
|
||||||
|
|
||||||
|
type Settings struct {
|
||||||
|
// Non-Saved settings
|
||||||
|
filename string
|
||||||
|
cmdLineKey string // stream key from the command line
|
||||||
|
|
||||||
|
// Saved settings
|
||||||
|
StreamStats bool
|
||||||
|
MaxMessageCount int
|
||||||
|
TitleLength int // maximum length of the title that can be set with the /playing
|
||||||
|
AdminPassword string
|
||||||
|
StreamKey string
|
||||||
|
ListenAddress string
|
||||||
|
ApprovedEmotes []string // list of channels that have been approved for emote use. Global emotes are always "approved".
|
||||||
|
TwitchClientID string // client id from twitch developers portal
|
||||||
|
SessionKey string // key for session data
|
||||||
|
Bans []BanInfo
|
||||||
|
LogLevel common.LogLevel
|
||||||
|
LogFile string
|
||||||
|
RoomAccess AccessMode
|
||||||
|
RoomAccessPin string // The current pin
|
||||||
|
NewPin bool // Auto generate a new pin on start. Overwrites RoomAccessPin if set.
|
||||||
|
|
||||||
|
// Rate limiting stuff, in seconds
|
||||||
|
RateLimitChat time.Duration
|
||||||
|
RateLimitNick time.Duration
|
||||||
|
RateLimitColor time.Duration
|
||||||
|
RateLimitAuth time.Duration
|
||||||
|
RateLimitDuplicate time.Duration // Amount of seconds between allowed duplicate messages
|
||||||
|
|
||||||
|
// Send the NoCache header?
|
||||||
|
NoCache bool
|
||||||
|
|
||||||
|
lock sync.RWMutex
|
||||||
|
}
|
||||||
|
|
||||||
|
type AccessMode string
|
||||||
|
|
||||||
|
const (
|
||||||
|
AccessOpen AccessMode = "open"
|
||||||
|
AccessPin AccessMode = "pin"
|
||||||
|
AccessRequest AccessMode = "request"
|
||||||
|
)
|
||||||
|
|
||||||
|
type BanInfo struct {
|
||||||
|
IP string
|
||||||
|
Names []string
|
||||||
|
When time.Time
|
||||||
|
}
|
||||||
|
|
||||||
|
func LoadSettings(filename string) (*Settings, error) {
|
||||||
|
raw, err := ioutil.ReadFile(filename)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("error reading file: %s", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
var s *Settings
|
||||||
|
err = json.Unmarshal(raw, &s)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("error unmarshaling: %s", err)
|
||||||
|
}
|
||||||
|
s.filename = filename
|
||||||
|
|
||||||
|
if err = common.SetupLogging(s.LogLevel, s.LogFile); err != nil {
|
||||||
|
return nil, fmt.Errorf("Unable to setup logger: %s", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// have a default of 200
|
||||||
|
if s.MaxMessageCount == 0 {
|
||||||
|
s.MaxMessageCount = 300
|
||||||
|
} else if s.MaxMessageCount < 0 {
|
||||||
|
return s, fmt.Errorf("value for MaxMessageCount must be greater than 0, given %d", s.MaxMessageCount)
|
||||||
|
}
|
||||||
|
|
||||||
|
s.AdminPassword, err = generatePass(time.Now().Unix())
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("unable to generate admin password: %s", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if s.RateLimitChat == -1 {
|
||||||
|
s.RateLimitChat = 0
|
||||||
|
} else if s.RateLimitChat <= 0 {
|
||||||
|
s.RateLimitChat = 1
|
||||||
|
}
|
||||||
|
|
||||||
|
if s.RateLimitNick == -1 {
|
||||||
|
s.RateLimitNick = 0
|
||||||
|
} else if s.RateLimitNick <= 0 {
|
||||||
|
s.RateLimitNick = 300
|
||||||
|
}
|
||||||
|
|
||||||
|
if s.RateLimitColor == -1 {
|
||||||
|
s.RateLimitColor = 0
|
||||||
|
} else if s.RateLimitColor <= 0 {
|
||||||
|
s.RateLimitColor = 60
|
||||||
|
}
|
||||||
|
|
||||||
|
if s.RateLimitAuth == -1 {
|
||||||
|
s.RateLimitAuth = 0
|
||||||
|
} else if s.RateLimitAuth <= 0 {
|
||||||
|
s.RateLimitAuth = 5
|
||||||
|
}
|
||||||
|
|
||||||
|
if s.RateLimitDuplicate == -1 {
|
||||||
|
s.RateLimitDuplicate = 0
|
||||||
|
} else if s.RateLimitDuplicate <= 0 {
|
||||||
|
s.RateLimitDuplicate = 30
|
||||||
|
}
|
||||||
|
|
||||||
|
// Print this stuff before we multiply it by time.Second
|
||||||
|
common.LogInfof("RateLimitChat: %v", s.RateLimitChat)
|
||||||
|
common.LogInfof("RateLimitNick: %v", s.RateLimitNick)
|
||||||
|
common.LogInfof("RateLimitColor: %v", s.RateLimitColor)
|
||||||
|
common.LogInfof("RateLimitAuth: %v", s.RateLimitAuth)
|
||||||
|
|
||||||
|
if len(s.RoomAccess) == 0 {
|
||||||
|
s.RoomAccess = AccessOpen
|
||||||
|
}
|
||||||
|
|
||||||
|
if (s.RoomAccess != AccessOpen && len(s.RoomAccessPin) == 0) || s.NewPin {
|
||||||
|
pin, err := s.generateNewPin()
|
||||||
|
if err != nil {
|
||||||
|
common.LogErrorf("Unable to generate new pin: %v", err)
|
||||||
|
}
|
||||||
|
common.LogInfof("New pin generated: %s", pin)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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
|
||||||
|
}
|
||||||
|
|
||||||
|
// Is this a good way to do this? Probably not...
|
||||||
|
if len(s.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)
|
||||||
|
}
|
||||||
|
s.SessionKey = out
|
||||||
|
}
|
||||||
|
|
||||||
|
// Save admin password to file
|
||||||
|
if err = s.Save(); err != nil {
|
||||||
|
return nil, fmt.Errorf("Unable to save settings: %s", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return s, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func generatePass(seed int64) (string, error) {
|
||||||
|
out := ""
|
||||||
|
for len(out) < 20 {
|
||||||
|
num, err := rand.Int(rand.Reader, big.NewInt(int64(15)))
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
out = fmt.Sprintf("%s%X", out, num)
|
||||||
|
}
|
||||||
|
return out, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Settings) Save() error {
|
||||||
|
defer s.lock.Unlock()
|
||||||
|
s.lock.Lock()
|
||||||
|
|
||||||
|
return s.unlockedSave()
|
||||||
|
}
|
||||||
|
|
||||||
|
// unlockedSave expects the calling function to lock the RWMutex
|
||||||
|
func (s *Settings) unlockedSave() error {
|
||||||
|
marshaled, err := json.MarshalIndent(s, "", "\t")
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("error marshaling: %s", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
err = ioutil.WriteFile(s.filename, marshaled, 0777)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("error saving: %s", err)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Settings) AddBan(host string, names []string) error {
|
||||||
|
defer s.lock.Unlock()
|
||||||
|
s.lock.Lock()
|
||||||
|
|
||||||
|
if host == "127.0.0.1" {
|
||||||
|
return fmt.Errorf("Cannot add a ban for localhost.")
|
||||||
|
}
|
||||||
|
|
||||||
|
b := BanInfo{
|
||||||
|
Names: names,
|
||||||
|
IP: host,
|
||||||
|
When: time.Now(),
|
||||||
|
}
|
||||||
|
s.Bans = append(s.Bans, b)
|
||||||
|
|
||||||
|
common.LogInfof("[BAN] %q (%s) has been banned.\n", strings.Join(names, ", "), host)
|
||||||
|
|
||||||
|
return s.unlockedSave()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Settings) RemoveBan(name string) error {
|
||||||
|
defer s.lock.Unlock()
|
||||||
|
s.lock.Lock()
|
||||||
|
|
||||||
|
name = strings.ToLower(name)
|
||||||
|
newBans := []BanInfo{}
|
||||||
|
for _, b := range s.Bans {
|
||||||
|
for _, n := range b.Names {
|
||||||
|
if n == name {
|
||||||
|
common.LogInfof("[ban] Removed ban for %s [%s]\n", b.IP, n)
|
||||||
|
} else {
|
||||||
|
newBans = append(newBans, b)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
s.Bans = newBans
|
||||||
|
return s.unlockedSave()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Settings) IsBanned(host string) (bool, []string) {
|
||||||
|
defer s.lock.RUnlock()
|
||||||
|
s.lock.RLock()
|
||||||
|
|
||||||
|
for _, b := range s.Bans {
|
||||||
|
if b.IP == host {
|
||||||
|
return true, b.Names
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Settings) SetTempKey(key string) {
|
||||||
|
defer s.lock.Unlock()
|
||||||
|
s.lock.Lock()
|
||||||
|
|
||||||
|
s.cmdLineKey = key
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Settings) GetStreamKey() string {
|
||||||
|
defer s.lock.RUnlock()
|
||||||
|
s.lock.RLock()
|
||||||
|
|
||||||
|
if len(s.cmdLineKey) > 0 {
|
||||||
|
return s.cmdLineKey
|
||||||
|
}
|
||||||
|
return s.StreamKey
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Settings) generateNewPin() (string, error) {
|
||||||
|
defer s.lock.Unlock()
|
||||||
|
s.lock.Lock()
|
||||||
|
|
||||||
|
num, err := rand.Int(rand.Reader, big.NewInt(int64(9999)))
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
s.RoomAccessPin = fmt.Sprintf("%04d", num)
|
||||||
|
if err = s.unlockedSave(); err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
return s.RoomAccessPin, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Settings) AddApprovedEmotes(channels []string) error {
|
||||||
|
defer s.lock.Unlock()
|
||||||
|
s.lock.Lock()
|
||||||
|
|
||||||
|
approved := map[string]int{}
|
||||||
|
for _, e := range s.ApprovedEmotes {
|
||||||
|
approved[e] = 1
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, name := range channels {
|
||||||
|
approved[name] = 1
|
||||||
|
}
|
||||||
|
|
||||||
|
filtered := []string{}
|
||||||
|
for key, _ := range approved {
|
||||||
|
filtered = append(filtered, key)
|
||||||
|
}
|
||||||
|
|
||||||
|
s.ApprovedEmotes = filtered
|
||||||
|
return s.unlockedSave()
|
||||||
|
}
|
||||||
18
MovieNight/scrapedagain/settings_example.json
Executable file
18
MovieNight/scrapedagain/settings_example.json
Executable file
@@ -0,0 +1,18 @@
|
|||||||
|
{
|
||||||
|
"MaxMessageCount": 300,
|
||||||
|
"TitleLength": 50,
|
||||||
|
"AdminPassword": "",
|
||||||
|
"Bans": [],
|
||||||
|
"StreamKey": "ALongStreamKey",
|
||||||
|
"ListenAddress": ":8089",
|
||||||
|
"ApprovedEmotes": null,
|
||||||
|
"Bans": [],
|
||||||
|
"LogLevel": "debug",
|
||||||
|
"LogFile": "thelog.log",
|
||||||
|
"RateLimitChat": 1,
|
||||||
|
"RateLimitNick": 300,
|
||||||
|
"RateLimitColor": 60,
|
||||||
|
"RateLimitAuth": 5,
|
||||||
|
"RateLimitDuplicate": 30,
|
||||||
|
"NoCache": false
|
||||||
|
}
|
||||||
1
MovieNight/scrapedagain/static
Symbolic link
1
MovieNight/scrapedagain/static
Symbolic link
@@ -0,0 +1 @@
|
|||||||
|
../static
|
||||||
85
MovieNight/scrapedagain/stats.go
Executable file
85
MovieNight/scrapedagain/stats.go
Executable file
@@ -0,0 +1,85 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"sync"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/zorchenhimer/MovieNight/common"
|
||||||
|
)
|
||||||
|
|
||||||
|
type streamStats struct {
|
||||||
|
messageIn int
|
||||||
|
messageOut int
|
||||||
|
maxUsers int
|
||||||
|
start time.Time
|
||||||
|
mutex sync.Mutex
|
||||||
|
|
||||||
|
streamStart time.Time
|
||||||
|
streamLive bool // True if live
|
||||||
|
}
|
||||||
|
|
||||||
|
func newStreamStats() streamStats {
|
||||||
|
return streamStats{start: time.Now(), streamLive: false}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *streamStats) msgInInc() {
|
||||||
|
s.mutex.Lock()
|
||||||
|
s.messageIn++
|
||||||
|
s.mutex.Unlock()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *streamStats) msgOutInc() {
|
||||||
|
s.mutex.Lock()
|
||||||
|
s.messageOut++
|
||||||
|
s.mutex.Unlock()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *streamStats) updateMaxUsers(count int) {
|
||||||
|
s.mutex.Lock()
|
||||||
|
if count > s.maxUsers {
|
||||||
|
s.maxUsers = count
|
||||||
|
}
|
||||||
|
s.mutex.Unlock()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *streamStats) getMaxUsers() int {
|
||||||
|
s.mutex.Lock()
|
||||||
|
defer s.mutex.Unlock()
|
||||||
|
|
||||||
|
return s.maxUsers
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *streamStats) Print() {
|
||||||
|
s.mutex.Lock()
|
||||||
|
defer s.mutex.Unlock()
|
||||||
|
|
||||||
|
common.LogInfof("Messages In: %d\n", s.messageIn)
|
||||||
|
common.LogInfof("Messages Out: %d\n", s.messageOut)
|
||||||
|
common.LogInfof("Max users in chat: %d\n", s.maxUsers)
|
||||||
|
common.LogInfof("Total Time: %s\n", time.Since(s.start))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *streamStats) startStream() {
|
||||||
|
s.mutex.Lock()
|
||||||
|
defer s.mutex.Unlock()
|
||||||
|
|
||||||
|
s.streamLive = true
|
||||||
|
s.streamStart = time.Now()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *streamStats) endStream() {
|
||||||
|
s.mutex.Lock()
|
||||||
|
defer s.mutex.Unlock()
|
||||||
|
|
||||||
|
s.streamLive = false
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *streamStats) getStreamLength() time.Duration {
|
||||||
|
s.mutex.Lock()
|
||||||
|
defer s.mutex.Unlock()
|
||||||
|
|
||||||
|
if !s.streamLive {
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
return time.Since(s.streamStart)
|
||||||
|
}
|
||||||
78
MovieNight/scrapedagain/superlock.go
Executable file
78
MovieNight/scrapedagain/superlock.go
Executable file
@@ -0,0 +1,78 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"log"
|
||||||
|
"sync"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
type SuperLock struct {
|
||||||
|
self *sync.Mutex
|
||||||
|
lock *sync.RWMutex
|
||||||
|
sem chan struct{}
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewSuperLock() *SuperLock {
|
||||||
|
return &SuperLock{
|
||||||
|
sem: make(chan struct{}, 100),
|
||||||
|
self: &sync.Mutex{},
|
||||||
|
lock: &sync.RWMutex{},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (sl *SuperLock) RLock() {
|
||||||
|
log.Println("sl.rlock...")
|
||||||
|
sl.lock.RLock()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (sl *SuperLock) RUnlock() {
|
||||||
|
log.Println("sl.runlock")
|
||||||
|
sl.lock.RUnlock()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (sl *SuperLock) Lock() {
|
||||||
|
log.Println("sl.lock...")
|
||||||
|
sl.lock.Lock()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (sl *SuperLock) Unlock() {
|
||||||
|
log.Println("sl.unlock")
|
||||||
|
sl.lock.Unlock()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (sl *SuperLock) StartStream() {
|
||||||
|
log.Println("sl.startstream")
|
||||||
|
sl.self.Lock()
|
||||||
|
defer sl.self.Unlock()
|
||||||
|
|
||||||
|
select {
|
||||||
|
case sl.sem <- struct{}{}:
|
||||||
|
case <-time.After(time.Second * 3):
|
||||||
|
log.Println("timed out getting semaphore to start stream")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Println("CONT STREAM", len(sl.sem))
|
||||||
|
startStream()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (sl *SuperLock) StopStream() {
|
||||||
|
log.Println("sl.stopstream")
|
||||||
|
sl.self.Lock()
|
||||||
|
defer sl.self.Unlock()
|
||||||
|
|
||||||
|
select {
|
||||||
|
case <-sl.sem:
|
||||||
|
case <-time.After(time.Second * 3):
|
||||||
|
log.Println("timed out getting semaphore to stop stream")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Println("STOP STREAM", len(sl.sem))
|
||||||
|
if len(sl.sem) > 0 {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Println("REALLY DO STOP STREAM", len(sl.sem))
|
||||||
|
stopStream()
|
||||||
|
}
|
||||||
279
MovieNight/scrapedagain/wasm/main.go
Executable file
279
MovieNight/scrapedagain/wasm/main.go
Executable file
@@ -0,0 +1,279 @@
|
|||||||
|
// +build js,wasm
|
||||||
|
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"sort"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"syscall/js"
|
||||||
|
|
||||||
|
"github.com/zorchenhimer/MovieNight/common"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
timestamp bool
|
||||||
|
color string
|
||||||
|
auth common.CommandLevel
|
||||||
|
global js.Value
|
||||||
|
)
|
||||||
|
|
||||||
|
func getElement(s string) js.Value {
|
||||||
|
return global.Get("document").Call("getElementById", s)
|
||||||
|
}
|
||||||
|
|
||||||
|
func join(v []js.Value) {
|
||||||
|
color := global.Call("getCookie", "color").String()
|
||||||
|
if color == "" {
|
||||||
|
// If a color is not set, do a random color
|
||||||
|
color = common.RandomColor()
|
||||||
|
} else if !common.IsValidColor(color) {
|
||||||
|
// Don't show the user the error, just clear the cookie
|
||||||
|
common.LogInfof("%#v is not a valid color, clearing cookie", color)
|
||||||
|
global.Call("deleteCookie", "color")
|
||||||
|
}
|
||||||
|
|
||||||
|
joinData, err := json.Marshal(common.JoinData{
|
||||||
|
Name: getElement("name").Get("value").String(),
|
||||||
|
Color: color,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
notify("Error prepping data for join")
|
||||||
|
common.LogErrorf("Could not prep data: %#v\n", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
data, err := json.Marshal(common.ClientData{
|
||||||
|
Type: common.CdJoin,
|
||||||
|
Message: string(joinData),
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
common.LogErrorf("Could not marshal data: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
global.Call("websocketSend", string(data))
|
||||||
|
}
|
||||||
|
|
||||||
|
func recieve(v []js.Value) {
|
||||||
|
if len(v) == 0 {
|
||||||
|
fmt.Println("No data received")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
chatJSON, err := common.DecodeData(v[0].String())
|
||||||
|
if err != nil {
|
||||||
|
fmt.Printf("Error decoding data: %s\n", err)
|
||||||
|
global.Call("appendMessages", fmt.Sprintf("<div>%v</div>", v))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
chat, err := chatJSON.ToData()
|
||||||
|
if err != nil {
|
||||||
|
fmt.Printf("Error converting ChatDataJSON to ChatData of type %d: %v", chatJSON.Type, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
switch chat.Type {
|
||||||
|
case common.DTHidden:
|
||||||
|
h := chat.Data.(common.HiddenMessage)
|
||||||
|
switch h.Type {
|
||||||
|
case common.CdUsers:
|
||||||
|
names = nil
|
||||||
|
for _, i := range h.Data.([]interface{}) {
|
||||||
|
names = append(names, i.(string))
|
||||||
|
}
|
||||||
|
sort.Strings(names)
|
||||||
|
case common.CdAuth:
|
||||||
|
auth = h.Data.(common.CommandLevel)
|
||||||
|
case common.CdColor:
|
||||||
|
color = h.Data.(string)
|
||||||
|
global.Get("document").Set("cookie", fmt.Sprintf("color=%s; expires=Fri, 31 Dec 9999 23:59:59 GMT", color))
|
||||||
|
case common.CdEmote:
|
||||||
|
data := h.Data.(map[string]interface{})
|
||||||
|
emoteNames = make([]string, 0, len(data))
|
||||||
|
emotes = make(map[string]string)
|
||||||
|
for k, v := range data {
|
||||||
|
emoteNames = append(emoteNames, k)
|
||||||
|
emotes[k] = v.(string)
|
||||||
|
}
|
||||||
|
sort.Strings(emoteNames)
|
||||||
|
case common.CdJoin:
|
||||||
|
notify("")
|
||||||
|
global.Call("openChat")
|
||||||
|
case common.CdNotify:
|
||||||
|
notify(h.Data.(string))
|
||||||
|
}
|
||||||
|
case common.DTEvent:
|
||||||
|
d := chat.Data.(common.DataEvent)
|
||||||
|
// A server message is the only event that doesn't deal with names.
|
||||||
|
if d.Event != common.EvServerMessage {
|
||||||
|
websocketSend("", common.CdUsers)
|
||||||
|
}
|
||||||
|
// on join or leave, update list of possible user names
|
||||||
|
fallthrough
|
||||||
|
case common.DTChat:
|
||||||
|
msg := chat.Data.HTML()
|
||||||
|
if d, ok := chat.Data.(common.DataMessage); ok {
|
||||||
|
if timestamp && (d.Type == common.MsgChat || d.Type == common.MsgAction) {
|
||||||
|
h, m, _ := time.Now().Clock()
|
||||||
|
msg = fmt.Sprintf(`<span class="time">%02d:%02d</span> %s`, h, m, msg)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
appendMessage(msg)
|
||||||
|
case common.DTCommand:
|
||||||
|
d := chat.Data.(common.DataCommand)
|
||||||
|
|
||||||
|
switch d.Command {
|
||||||
|
case common.CmdPlaying:
|
||||||
|
if d.Arguments == nil || len(d.Arguments) == 0 {
|
||||||
|
global.Call("setPlaying", "", "")
|
||||||
|
|
||||||
|
} else if len(d.Arguments) == 1 {
|
||||||
|
global.Call("setPlaying", d.Arguments[0], "")
|
||||||
|
|
||||||
|
} else if len(d.Arguments) == 2 {
|
||||||
|
global.Call("setPlaying", d.Arguments[0], d.Arguments[1])
|
||||||
|
}
|
||||||
|
case common.CmdRefreshPlayer:
|
||||||
|
global.Call("initPlayer", nil)
|
||||||
|
case common.CmdPurgeChat:
|
||||||
|
global.Call("purgeChat", nil)
|
||||||
|
appendMessage(d.HTML())
|
||||||
|
case common.CmdHelp:
|
||||||
|
url := "/help"
|
||||||
|
if d.Arguments != nil && len(d.Arguments) > 0 {
|
||||||
|
url = d.Arguments[0]
|
||||||
|
}
|
||||||
|
appendMessage(d.HTML())
|
||||||
|
global.Get("window").Call("open", url, "_blank", "menubar=0,status=0,toolbar=0,width=300,height=600")
|
||||||
|
case common.CmdEmotes:
|
||||||
|
url := "/emotes"
|
||||||
|
appendMessage(d.HTML())
|
||||||
|
global.Get("window").Call("open", url, "_blank", "menubar=0,status=0,toolbar=0,width=300,height=600")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func appendMessage(msg string) {
|
||||||
|
global.Call("appendMessages", "<div>"+msg+"</div>")
|
||||||
|
}
|
||||||
|
|
||||||
|
func websocketSend(msg string, dataType common.ClientDataType) error {
|
||||||
|
if strings.TrimSpace(msg) == "" && dataType == common.CdMessage {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
data, err := json.Marshal(common.ClientData{
|
||||||
|
Type: dataType,
|
||||||
|
Message: msg,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("could not marshal data: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
global.Call("websocketSend", string(data))
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func send(this js.Value, v []js.Value) interface{} {
|
||||||
|
if len(v) != 1 {
|
||||||
|
showChatError(fmt.Errorf("expected 1 parameter, got %d", len(v)))
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
err := websocketSend(v[0].String(), common.CdMessage)
|
||||||
|
if err != nil {
|
||||||
|
showChatError(err)
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
func showChatError(err error) {
|
||||||
|
if err != nil {
|
||||||
|
fmt.Printf("Could not send: %v\n", err)
|
||||||
|
global.Call("appendMessages", `<div><span style="color: red;">Could not send message</span></div>`)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func notify(msg string) {
|
||||||
|
global.Call("setNotifyBox", msg)
|
||||||
|
}
|
||||||
|
|
||||||
|
func showTimestamp(v []js.Value) {
|
||||||
|
if len(v) != 1 {
|
||||||
|
// Don't bother with returning a value
|
||||||
|
return
|
||||||
|
}
|
||||||
|
timestamp = v[0].Bool()
|
||||||
|
}
|
||||||
|
|
||||||
|
func isValidColor(this js.Value, v []js.Value) interface{} {
|
||||||
|
if len(v) != 1 {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return common.IsValidColor(v[0].String())
|
||||||
|
}
|
||||||
|
|
||||||
|
func debugValues(v []js.Value) {
|
||||||
|
for k, v := range map[string]interface{}{
|
||||||
|
"timestamp": timestamp,
|
||||||
|
"auth": auth,
|
||||||
|
"color": color,
|
||||||
|
"current suggestion": currentSug,
|
||||||
|
"current suggestion type": currentSugType,
|
||||||
|
"filtered suggestions": filteredSug,
|
||||||
|
"user names": names,
|
||||||
|
"emote names": emoteNames,
|
||||||
|
} {
|
||||||
|
fmt.Printf("%s: %#v\n", k, v)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
global = js.Global()
|
||||||
|
|
||||||
|
common.SetupLogging(common.LLDebug, "")
|
||||||
|
|
||||||
|
global.Set("processMessageKey", js.FuncOf(processMessageKey))
|
||||||
|
global.Set("sendMessage", js.FuncOf(send))
|
||||||
|
global.Set("isValidColor", js.FuncOf(isValidColor))
|
||||||
|
|
||||||
|
global.Set("recieveMessage", jsCallbackOf(recieve))
|
||||||
|
global.Set("processMessage", jsCallbackOf(processMessage))
|
||||||
|
global.Set("debugValues", jsCallbackOf(debugValues))
|
||||||
|
global.Set("showTimestamp", jsCallbackOf(showTimestamp))
|
||||||
|
global.Set("join", jsCallbackOf(join))
|
||||||
|
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
|
||||||
|
global.Get("colorSelect").Set("innerHTML", inner)
|
||||||
|
}()
|
||||||
|
|
||||||
|
// This is needed so the goroutine does not end
|
||||||
|
for {
|
||||||
|
// heatbeat to keep connection alive to deal with nginx
|
||||||
|
if global.Get("inChat").Bool() {
|
||||||
|
websocketSend("", common.CdPing)
|
||||||
|
}
|
||||||
|
time.Sleep(time.Second * 10)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func jsCallbackOf(fnc func(v []js.Value)) js.Func {
|
||||||
|
return js.FuncOf(func(this js.Value, refs []js.Value) interface{} {
|
||||||
|
vals := make([]js.Value, 0, len(refs))
|
||||||
|
for _, ref := range refs {
|
||||||
|
vals = append(vals, ref)
|
||||||
|
}
|
||||||
|
fnc(vals)
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
}
|
||||||
195
MovieNight/scrapedagain/wasm/suggestions.go
Executable file
195
MovieNight/scrapedagain/wasm/suggestions.go
Executable file
@@ -0,0 +1,195 @@
|
|||||||
|
// +build js,wasm
|
||||||
|
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"syscall/js"
|
||||||
|
|
||||||
|
"github.com/zorchenhimer/MovieNight/common"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
keyTab = 9
|
||||||
|
keyEnter = 13
|
||||||
|
keyEsc = 27
|
||||||
|
keySpace = 32
|
||||||
|
keyUp = 38
|
||||||
|
keyDown = 40
|
||||||
|
suggestionName = '@'
|
||||||
|
suggestionEmote = ':'
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
currentSugType rune
|
||||||
|
currentSug string
|
||||||
|
filteredSug []string
|
||||||
|
names []string
|
||||||
|
emoteNames []string
|
||||||
|
emotes map[string]string
|
||||||
|
)
|
||||||
|
|
||||||
|
// The returned value is a bool deciding to prevent the event from propagating
|
||||||
|
func processMessageKey(this js.Value, v []js.Value) interface{} {
|
||||||
|
startIdx := v[0].Get("target").Get("selectionStart").Int()
|
||||||
|
keyCode := v[0].Get("keyCode").Int()
|
||||||
|
ctrl := v[0].Get("ctrlKey").Bool()
|
||||||
|
|
||||||
|
if ctrl && keyCode == keySpace {
|
||||||
|
processMessage(nil)
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(filteredSug) == 0 || currentSug == "" {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
switch keyCode {
|
||||||
|
case keyEsc:
|
||||||
|
filteredSug = nil
|
||||||
|
currentSug = ""
|
||||||
|
currentSugType = 0
|
||||||
|
case keyUp, keyDown:
|
||||||
|
newidx := 0
|
||||||
|
for i, n := range filteredSug {
|
||||||
|
if n == currentSug {
|
||||||
|
newidx = i
|
||||||
|
if keyCode == keyDown {
|
||||||
|
newidx = i + 1
|
||||||
|
if newidx == len(filteredSug) {
|
||||||
|
newidx--
|
||||||
|
}
|
||||||
|
} else if keyCode == keyUp {
|
||||||
|
newidx = i - 1
|
||||||
|
if newidx < 0 {
|
||||||
|
newidx = 0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
currentSug = filteredSug[newidx]
|
||||||
|
case keyTab, keyEnter:
|
||||||
|
msg := global.Get("msg")
|
||||||
|
val := msg.Get("value").String()
|
||||||
|
newval := val[:startIdx]
|
||||||
|
|
||||||
|
if i := strings.LastIndex(newval, string(currentSugType)); i != -1 {
|
||||||
|
var offset int
|
||||||
|
if currentSugType == suggestionName {
|
||||||
|
offset = 1
|
||||||
|
}
|
||||||
|
|
||||||
|
newval = newval[:i+offset] + currentSug
|
||||||
|
}
|
||||||
|
|
||||||
|
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
|
||||||
|
filteredSug = nil
|
||||||
|
default:
|
||||||
|
// We only want to handle the caught keys, so return early
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
updateSuggestionDiv()
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
func processMessage(v []js.Value) {
|
||||||
|
msg := global.Get("msg")
|
||||||
|
text := strings.ToLower(msg.Get("value").String())
|
||||||
|
startIdx := msg.Get("selectionStart").Int()
|
||||||
|
|
||||||
|
filteredSug = nil
|
||||||
|
if len(text) != 0 {
|
||||||
|
if len(names) > 0 || len(emoteNames) > 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 && caretIdx <= startIdx && startIdx <= caretIdx+len(word) {
|
||||||
|
var suggestions []string
|
||||||
|
if word[0] == suggestionName {
|
||||||
|
currentSugType = suggestionName
|
||||||
|
suggestions = names
|
||||||
|
} else if word[0] == suggestionEmote {
|
||||||
|
suggestions = emoteNames
|
||||||
|
currentSugType = suggestionEmote
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, s := range suggestions {
|
||||||
|
if len(word) == 1 || strings.Contains(strings.ToLower(s), word[1:]) {
|
||||||
|
filteredSug = append(filteredSug, s)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(filteredSug) > 0 {
|
||||||
|
currentSug = ""
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
caretIdx += len(word)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
updateSuggestionDiv()
|
||||||
|
}
|
||||||
|
|
||||||
|
func updateSuggestionDiv() {
|
||||||
|
const selectedClass = ` class="selectedName"`
|
||||||
|
|
||||||
|
var divs []string
|
||||||
|
if len(filteredSug) > 0 {
|
||||||
|
// set current name to first if not set already
|
||||||
|
if currentSug == "" {
|
||||||
|
currentSug = filteredSug[len(filteredSug)-1]
|
||||||
|
}
|
||||||
|
|
||||||
|
var hascurrentSuggestion bool
|
||||||
|
divs = make([]string, len(filteredSug))
|
||||||
|
|
||||||
|
// Create inner body of html
|
||||||
|
for i := range filteredSug {
|
||||||
|
divs[i] = "<div"
|
||||||
|
|
||||||
|
sug := filteredSug[i]
|
||||||
|
if sug == currentSug {
|
||||||
|
hascurrentSuggestion = true
|
||||||
|
divs[i] += selectedClass
|
||||||
|
}
|
||||||
|
divs[i] += ">"
|
||||||
|
|
||||||
|
if currentSugType == suggestionEmote {
|
||||||
|
divs[i] += common.EmoteToHtml(emotes[sug], sug)
|
||||||
|
}
|
||||||
|
|
||||||
|
divs[i] += sug + "</div>"
|
||||||
|
}
|
||||||
|
|
||||||
|
if !hascurrentSuggestion {
|
||||||
|
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
|
||||||
|
global.Get("suggestions").Set("innerHTML", strings.Join(divs, "\n"))
|
||||||
|
global.Call("updateSuggestionScroll")
|
||||||
|
}
|
||||||
313
MovieNight/settings.go
Executable file
313
MovieNight/settings.go
Executable file
@@ -0,0 +1,313 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"crypto/rand"
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"io/ioutil"
|
||||||
|
"math/big"
|
||||||
|
"strings"
|
||||||
|
"sync"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/gorilla/sessions"
|
||||||
|
"github.com/zorchenhimer/MovieNight/common"
|
||||||
|
)
|
||||||
|
|
||||||
|
var settings *Settings
|
||||||
|
var sstore *sessions.CookieStore
|
||||||
|
|
||||||
|
type Settings struct {
|
||||||
|
// Non-Saved settings
|
||||||
|
filename string
|
||||||
|
cmdLineKey string // stream key from the command line
|
||||||
|
|
||||||
|
// Saved settings
|
||||||
|
StreamStats bool
|
||||||
|
MaxMessageCount int
|
||||||
|
TitleLength int // maximum length of the title that can be set with the /playing
|
||||||
|
AdminPassword string
|
||||||
|
StreamKey string
|
||||||
|
ListenAddress string
|
||||||
|
ApprovedEmotes []string // list of channels that have been approved for emote use. Global emotes are always "approved".
|
||||||
|
TwitchClientID string // client id from twitch developers portal
|
||||||
|
SessionKey string // key for session data
|
||||||
|
Bans []BanInfo
|
||||||
|
LogLevel common.LogLevel
|
||||||
|
LogFile string
|
||||||
|
RoomAccess AccessMode
|
||||||
|
RoomAccessPin string // The current pin
|
||||||
|
NewPin bool // Auto generate a new pin on start. Overwrites RoomAccessPin if set.
|
||||||
|
|
||||||
|
// Rate limiting stuff, in seconds
|
||||||
|
RateLimitChat time.Duration
|
||||||
|
RateLimitNick time.Duration
|
||||||
|
RateLimitColor time.Duration
|
||||||
|
RateLimitAuth time.Duration
|
||||||
|
RateLimitDuplicate time.Duration // Amount of seconds between allowed duplicate messages
|
||||||
|
|
||||||
|
// Send the NoCache header?
|
||||||
|
NoCache bool
|
||||||
|
|
||||||
|
lock sync.RWMutex
|
||||||
|
}
|
||||||
|
|
||||||
|
type AccessMode string
|
||||||
|
|
||||||
|
const (
|
||||||
|
AccessOpen AccessMode = "open"
|
||||||
|
AccessPin AccessMode = "pin"
|
||||||
|
AccessRequest AccessMode = "request"
|
||||||
|
)
|
||||||
|
|
||||||
|
type BanInfo struct {
|
||||||
|
IP string
|
||||||
|
Names []string
|
||||||
|
When time.Time
|
||||||
|
}
|
||||||
|
|
||||||
|
func LoadSettings(filename string) (*Settings, error) {
|
||||||
|
raw, err := ioutil.ReadFile(filename)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("error reading file: %s", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
var s *Settings
|
||||||
|
err = json.Unmarshal(raw, &s)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("error unmarshaling: %s", err)
|
||||||
|
}
|
||||||
|
s.filename = filename
|
||||||
|
|
||||||
|
if err = common.SetupLogging(s.LogLevel, s.LogFile); err != nil {
|
||||||
|
return nil, fmt.Errorf("Unable to setup logger: %s", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// have a default of 200
|
||||||
|
if s.MaxMessageCount == 0 {
|
||||||
|
s.MaxMessageCount = 300
|
||||||
|
} else if s.MaxMessageCount < 0 {
|
||||||
|
return s, fmt.Errorf("value for MaxMessageCount must be greater than 0, given %d", s.MaxMessageCount)
|
||||||
|
}
|
||||||
|
|
||||||
|
s.AdminPassword, err = generatePass(time.Now().Unix())
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("unable to generate admin password: %s", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if s.RateLimitChat == -1 {
|
||||||
|
s.RateLimitChat = 0
|
||||||
|
} else if s.RateLimitChat <= 0 {
|
||||||
|
s.RateLimitChat = 1
|
||||||
|
}
|
||||||
|
|
||||||
|
if s.RateLimitNick == -1 {
|
||||||
|
s.RateLimitNick = 0
|
||||||
|
} else if s.RateLimitNick <= 0 {
|
||||||
|
s.RateLimitNick = 300
|
||||||
|
}
|
||||||
|
|
||||||
|
if s.RateLimitColor == -1 {
|
||||||
|
s.RateLimitColor = 0
|
||||||
|
} else if s.RateLimitColor <= 0 {
|
||||||
|
s.RateLimitColor = 60
|
||||||
|
}
|
||||||
|
|
||||||
|
if s.RateLimitAuth == -1 {
|
||||||
|
s.RateLimitAuth = 0
|
||||||
|
} else if s.RateLimitAuth <= 0 {
|
||||||
|
s.RateLimitAuth = 5
|
||||||
|
}
|
||||||
|
|
||||||
|
if s.RateLimitDuplicate == -1 {
|
||||||
|
s.RateLimitDuplicate = 0
|
||||||
|
} else if s.RateLimitDuplicate <= 0 {
|
||||||
|
s.RateLimitDuplicate = 30
|
||||||
|
}
|
||||||
|
|
||||||
|
// Print this stuff before we multiply it by time.Second
|
||||||
|
common.LogInfof("RateLimitChat: %v", s.RateLimitChat)
|
||||||
|
common.LogInfof("RateLimitNick: %v", s.RateLimitNick)
|
||||||
|
common.LogInfof("RateLimitColor: %v", s.RateLimitColor)
|
||||||
|
common.LogInfof("RateLimitAuth: %v", s.RateLimitAuth)
|
||||||
|
|
||||||
|
if len(s.RoomAccess) == 0 {
|
||||||
|
s.RoomAccess = AccessOpen
|
||||||
|
}
|
||||||
|
|
||||||
|
if (s.RoomAccess != AccessOpen && len(s.RoomAccessPin) == 0) || s.NewPin {
|
||||||
|
pin, err := s.generateNewPin()
|
||||||
|
if err != nil {
|
||||||
|
common.LogErrorf("Unable to generate new pin: %v", err)
|
||||||
|
}
|
||||||
|
common.LogInfof("New pin generated: %s", pin)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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
|
||||||
|
}
|
||||||
|
|
||||||
|
// Is this a good way to do this? Probably not...
|
||||||
|
if len(s.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)
|
||||||
|
}
|
||||||
|
s.SessionKey = out
|
||||||
|
}
|
||||||
|
|
||||||
|
// Save admin password to file
|
||||||
|
if err = s.Save(); err != nil {
|
||||||
|
return nil, fmt.Errorf("Unable to save settings: %s", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return s, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func generatePass(seed int64) (string, error) {
|
||||||
|
out := ""
|
||||||
|
for len(out) < 20 {
|
||||||
|
num, err := rand.Int(rand.Reader, big.NewInt(int64(15)))
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
out = fmt.Sprintf("%s%X", out, num)
|
||||||
|
}
|
||||||
|
return out, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Settings) Save() error {
|
||||||
|
defer s.lock.Unlock()
|
||||||
|
s.lock.Lock()
|
||||||
|
|
||||||
|
return s.unlockedSave()
|
||||||
|
}
|
||||||
|
|
||||||
|
// unlockedSave expects the calling function to lock the RWMutex
|
||||||
|
func (s *Settings) unlockedSave() error {
|
||||||
|
marshaled, err := json.MarshalIndent(s, "", "\t")
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("error marshaling: %s", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
err = ioutil.WriteFile(s.filename, marshaled, 0777)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("error saving: %s", err)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Settings) AddBan(host string, names []string) error {
|
||||||
|
defer s.lock.Unlock()
|
||||||
|
s.lock.Lock()
|
||||||
|
|
||||||
|
if host == "127.0.0.1" {
|
||||||
|
return fmt.Errorf("Cannot add a ban for localhost.")
|
||||||
|
}
|
||||||
|
|
||||||
|
b := BanInfo{
|
||||||
|
Names: names,
|
||||||
|
IP: host,
|
||||||
|
When: time.Now(),
|
||||||
|
}
|
||||||
|
s.Bans = append(s.Bans, b)
|
||||||
|
|
||||||
|
common.LogInfof("[BAN] %q (%s) has been banned.\n", strings.Join(names, ", "), host)
|
||||||
|
|
||||||
|
return s.unlockedSave()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Settings) RemoveBan(name string) error {
|
||||||
|
defer s.lock.Unlock()
|
||||||
|
s.lock.Lock()
|
||||||
|
|
||||||
|
name = strings.ToLower(name)
|
||||||
|
newBans := []BanInfo{}
|
||||||
|
for _, b := range s.Bans {
|
||||||
|
for _, n := range b.Names {
|
||||||
|
if n == name {
|
||||||
|
common.LogInfof("[ban] Removed ban for %s [%s]\n", b.IP, n)
|
||||||
|
} else {
|
||||||
|
newBans = append(newBans, b)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
s.Bans = newBans
|
||||||
|
return s.unlockedSave()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Settings) IsBanned(host string) (bool, []string) {
|
||||||
|
defer s.lock.RUnlock()
|
||||||
|
s.lock.RLock()
|
||||||
|
|
||||||
|
for _, b := range s.Bans {
|
||||||
|
if b.IP == host {
|
||||||
|
return true, b.Names
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Settings) SetTempKey(key string) {
|
||||||
|
defer s.lock.Unlock()
|
||||||
|
s.lock.Lock()
|
||||||
|
|
||||||
|
s.cmdLineKey = key
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Settings) GetStreamKey() string {
|
||||||
|
defer s.lock.RUnlock()
|
||||||
|
s.lock.RLock()
|
||||||
|
|
||||||
|
if len(s.cmdLineKey) > 0 {
|
||||||
|
return s.cmdLineKey
|
||||||
|
}
|
||||||
|
return s.StreamKey
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Settings) generateNewPin() (string, error) {
|
||||||
|
defer s.lock.Unlock()
|
||||||
|
s.lock.Lock()
|
||||||
|
|
||||||
|
num, err := rand.Int(rand.Reader, big.NewInt(int64(9999)))
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
s.RoomAccessPin = fmt.Sprintf("%04d", num)
|
||||||
|
if err = s.unlockedSave(); err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
return s.RoomAccessPin, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Settings) AddApprovedEmotes(channels []string) error {
|
||||||
|
defer s.lock.Unlock()
|
||||||
|
s.lock.Lock()
|
||||||
|
|
||||||
|
approved := map[string]int{}
|
||||||
|
for _, e := range s.ApprovedEmotes {
|
||||||
|
approved[e] = 1
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, name := range channels {
|
||||||
|
approved[name] = 1
|
||||||
|
}
|
||||||
|
|
||||||
|
filtered := []string{}
|
||||||
|
for key, _ := range approved {
|
||||||
|
filtered = append(filtered, key)
|
||||||
|
}
|
||||||
|
|
||||||
|
s.ApprovedEmotes = filtered
|
||||||
|
return s.unlockedSave()
|
||||||
|
}
|
||||||
18
MovieNight/settings_example.json
Executable file
18
MovieNight/settings_example.json
Executable file
@@ -0,0 +1,18 @@
|
|||||||
|
{
|
||||||
|
"MaxMessageCount": 300,
|
||||||
|
"TitleLength": 50,
|
||||||
|
"AdminPassword": "",
|
||||||
|
"Bans": [],
|
||||||
|
"StreamKey": "ALongStreamKey",
|
||||||
|
"ListenAddress": ":8089",
|
||||||
|
"ApprovedEmotes": null,
|
||||||
|
"Bans": [],
|
||||||
|
"LogLevel": "debug",
|
||||||
|
"LogFile": "thelog.log",
|
||||||
|
"RateLimitChat": 1,
|
||||||
|
"RateLimitNick": 300,
|
||||||
|
"RateLimitColor": 60,
|
||||||
|
"RateLimitAuth": 5,
|
||||||
|
"RateLimitDuplicate": 30,
|
||||||
|
"NoCache": false
|
||||||
|
}
|
||||||
22
MovieNight/static/base.html
Executable file
22
MovieNight/static/base.html
Executable file
@@ -0,0 +1,22 @@
|
|||||||
|
<!doctype html>
|
||||||
|
<html>
|
||||||
|
|
||||||
|
<head>
|
||||||
|
<meta charset='utf-8'>
|
||||||
|
<title>{{ .Title }}</title>
|
||||||
|
<link rel="stylesheet" type="text/css" href="/static/css/hack/hack.css">
|
||||||
|
<link rel="stylesheet" type="text/css" href="/static/css/site.css">
|
||||||
|
<script type="application/javascript" src="/static/js/jquery.js"></script>
|
||||||
|
<script type="application/javascript" src="/static/js/both.js"></script>
|
||||||
|
{{template "header" .}}
|
||||||
|
</head>
|
||||||
|
|
||||||
|
<body class="scrollbar">
|
||||||
|
<img id="remote" src="/static/img/remote.png" onclick="flipRemote();" />
|
||||||
|
<div id="devKeys"></div>
|
||||||
|
<div class="root">
|
||||||
|
{{template "body" .}}
|
||||||
|
</div>
|
||||||
|
</body>
|
||||||
|
|
||||||
|
</html>
|
||||||
BIN
MovieNight/static/css/hack/fonts/hack-bold-subset.woff
Executable file
BIN
MovieNight/static/css/hack/fonts/hack-bold-subset.woff
Executable file
Binary file not shown.
BIN
MovieNight/static/css/hack/fonts/hack-bold-subset.woff2
Executable file
BIN
MovieNight/static/css/hack/fonts/hack-bold-subset.woff2
Executable file
Binary file not shown.
BIN
MovieNight/static/css/hack/fonts/hack-bold.woff
Executable file
BIN
MovieNight/static/css/hack/fonts/hack-bold.woff
Executable file
Binary file not shown.
BIN
MovieNight/static/css/hack/fonts/hack-bold.woff2
Executable file
BIN
MovieNight/static/css/hack/fonts/hack-bold.woff2
Executable file
Binary file not shown.
BIN
MovieNight/static/css/hack/fonts/hack-bolditalic-subset.woff
Executable file
BIN
MovieNight/static/css/hack/fonts/hack-bolditalic-subset.woff
Executable file
Binary file not shown.
BIN
MovieNight/static/css/hack/fonts/hack-bolditalic-subset.woff2
Executable file
BIN
MovieNight/static/css/hack/fonts/hack-bolditalic-subset.woff2
Executable file
Binary file not shown.
BIN
MovieNight/static/css/hack/fonts/hack-bolditalic.woff
Executable file
BIN
MovieNight/static/css/hack/fonts/hack-bolditalic.woff
Executable file
Binary file not shown.
BIN
MovieNight/static/css/hack/fonts/hack-bolditalic.woff2
Executable file
BIN
MovieNight/static/css/hack/fonts/hack-bolditalic.woff2
Executable file
Binary file not shown.
BIN
MovieNight/static/css/hack/fonts/hack-italic-subset.woff
Executable file
BIN
MovieNight/static/css/hack/fonts/hack-italic-subset.woff
Executable file
Binary file not shown.
BIN
MovieNight/static/css/hack/fonts/hack-italic-subset.woff2
Executable file
BIN
MovieNight/static/css/hack/fonts/hack-italic-subset.woff2
Executable file
Binary file not shown.
BIN
MovieNight/static/css/hack/fonts/hack-italic.woff
Executable file
BIN
MovieNight/static/css/hack/fonts/hack-italic.woff
Executable file
Binary file not shown.
BIN
MovieNight/static/css/hack/fonts/hack-italic.woff2
Executable file
BIN
MovieNight/static/css/hack/fonts/hack-italic.woff2
Executable file
Binary file not shown.
BIN
MovieNight/static/css/hack/fonts/hack-regular-subset.woff
Executable file
BIN
MovieNight/static/css/hack/fonts/hack-regular-subset.woff
Executable file
Binary file not shown.
BIN
MovieNight/static/css/hack/fonts/hack-regular-subset.woff2
Executable file
BIN
MovieNight/static/css/hack/fonts/hack-regular-subset.woff2
Executable file
Binary file not shown.
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user