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