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