This commit is contained in:
bel
2021-09-14 06:30:17 -06:00
commit 7ab1723a5e
327 changed files with 127104 additions and 0 deletions

46
MovieNight/scrapedagain/.gitignore vendored Executable file
View 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

View File

@@ -0,0 +1,10 @@
language: go
before_install:
- make get
go:
- 1.12.x
env:
- GO111MODULE=on

View 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 []

View 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

View 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 ""
}

View 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
}

View 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
}

View 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)
}
}
}

View 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
)

View 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, " ")
}

View File

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

View 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...)
}

View 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...)
}

View 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)
}

View 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)
}

View 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
View 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
}

View 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,
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.5 KiB

23
MovieNight/scrapedagain/go.mod Executable file
View 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
View 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=

View 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
View 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
View 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

View File

@@ -0,0 +1,10 @@
language: go
before_install:
- make get
go:
- 1.12.x
env:
- GO111MODULE=on

View 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 []

View 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

View 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
}

View 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
}

View 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,
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.5 KiB

View 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
)

View 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=

View 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)
}
}
}

View 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
}

View 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

View File

@@ -0,0 +1,62 @@
# MovieNight stream server
[![Build status](https://api.travis-ci.org/zorchenhimer/MovieNight.svg?branch=master)](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")
```

View 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)
}
}

View 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()
}

View 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
}

View 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)
}

View 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()
}

View 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

View File

@@ -0,0 +1,62 @@
# MovieNight stream server
[![Build status](https://api.travis-ci.org/zorchenhimer/MovieNight.svg?branch=master)](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
View 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)
}
}

View 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()
}

View 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
}

View File

@@ -0,0 +1 @@
../static

View 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)
}

View 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()
}

View 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
})
}

View 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")
}