Initial commit
So far things work. Needs lots of improvements.
This commit is contained in:
commit
3276295421
9
.gitignore
vendored
Normal file
9
.gitignore
vendored
Normal file
@ -0,0 +1,9 @@
|
||||
*.gif
|
||||
*.png
|
||||
*.exe
|
||||
|
||||
# Linux binary
|
||||
MovieNight
|
||||
|
||||
# Twitch channel info
|
||||
static/subscriber.json
|
27
Makefile
Normal file
27
Makefile
Normal file
@ -0,0 +1,27 @@
|
||||
|
||||
#export GOOS=linux
|
||||
#export GOARCH=386
|
||||
|
||||
.PHONY: sync fmt vet
|
||||
|
||||
all: vet fmt MovieNight MovieNight.exe
|
||||
|
||||
MovieNight.exe: *.go
|
||||
GOOS=windows GOARCH=amd64 go build -o MovieNight.exe
|
||||
|
||||
MovieNight: *.go
|
||||
GOOS=linux GOARCH=386 go build -o MovieNight
|
||||
|
||||
clean:
|
||||
rm MovieNight.exe MovieNight
|
||||
|
||||
fmt:
|
||||
gofmt -w .
|
||||
|
||||
vet:
|
||||
go vet
|
||||
|
||||
sync:
|
||||
#rsync -v --no-perms --chmod=ugo=rwX -r ./ zorchenhimer@movienight.zorchenhimer.com:/home/zorchenhimer/movienight/
|
||||
#rsync -v --no-perms --chmod=ugo=rwX -e "ssh -i /c/movienight/movienight-deploy.key" -r ./ zorchenhimer@movienight.zorchenhimer.com:/home/zorchenhimer/movienight/
|
||||
scp -i /c/movienight/movienight-deploy.key -r . zorchenhimer@movienight.zorchenhimer.com:/home/zorchenhimer/movienight
|
168
chatclient.go
Normal file
168
chatclient.go
Normal file
@ -0,0 +1,168 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"html"
|
||||
"net"
|
||||
"strings"
|
||||
"unicode"
|
||||
|
||||
"github.com/gorilla/websocket"
|
||||
)
|
||||
|
||||
type Client struct {
|
||||
name string // Display name
|
||||
conn *websocket.Conn
|
||||
belongsTo *ChatRoom
|
||||
color string
|
||||
IsMod bool
|
||||
IsAdmin bool
|
||||
IsColorForced bool
|
||||
}
|
||||
|
||||
var emotes map[string]string
|
||||
|
||||
func ParseEmotes(msg string) string {
|
||||
words := strings.Split(msg, " ")
|
||||
newWords := []string{}
|
||||
for _, word := range words {
|
||||
word = strings.Trim(word, "[]")
|
||||
|
||||
found := false
|
||||
for key, val := range emotes {
|
||||
if key == word {
|
||||
newWords = append(newWords, fmt.Sprintf("<img src=\"/emotes/%s\" title=\"%s\" />", val, key))
|
||||
//fmt.Printf("[emote] %s\n", val)
|
||||
found = true
|
||||
}
|
||||
}
|
||||
if !found {
|
||||
newWords = append(newWords, word)
|
||||
}
|
||||
}
|
||||
return strings.Join(newWords, " ")
|
||||
}
|
||||
|
||||
//Client has a new message to broadcast
|
||||
func (cl *Client) NewMsg(msg string) {
|
||||
msg = html.EscapeString(msg)
|
||||
msg = removeDumbSpaces(msg)
|
||||
msg = strings.Trim(msg, " ")
|
||||
|
||||
// Don't send zero-length messages
|
||||
if len(msg) == 0 {
|
||||
return
|
||||
}
|
||||
|
||||
if strings.HasPrefix(msg, "/") {
|
||||
// is a command
|
||||
msg = msg[1:len(msg)]
|
||||
fullcmd := strings.Split(msg, " ")
|
||||
cmd := strings.ToLower(fullcmd[0])
|
||||
args := fullcmd[1:len(fullcmd)]
|
||||
|
||||
response := commands.RunCommand(cmd, args, cl)
|
||||
if response != "" {
|
||||
cl.ServerMessage(response)
|
||||
return
|
||||
}
|
||||
|
||||
} else {
|
||||
// Trim long messages
|
||||
if len(msg) > 400 {
|
||||
msg = msg[0:400]
|
||||
}
|
||||
|
||||
fmt.Printf("[chat] <%s> %q\n", cl.name, msg)
|
||||
|
||||
// Enable links for mods and admins
|
||||
if cl.IsMod || cl.IsAdmin {
|
||||
msg = formatLinks(msg)
|
||||
}
|
||||
|
||||
cl.Message(msg)
|
||||
}
|
||||
}
|
||||
|
||||
// Make links clickable
|
||||
func formatLinks(input string) string {
|
||||
newMsg := []string{}
|
||||
for _, word := range strings.Split(input, " ") {
|
||||
if strings.HasPrefix(word, "http://") || strings.HasPrefix(word, "https://") {
|
||||
//word = unescape(word)
|
||||
word = html.UnescapeString(word)
|
||||
word = fmt.Sprintf(`<a href="%s" target="_blank">%s</a>`, word, word)
|
||||
}
|
||||
newMsg = append(newMsg, word)
|
||||
}
|
||||
return strings.Join(newMsg, " ")
|
||||
}
|
||||
|
||||
//Exiting out
|
||||
func (cl *Client) Exit() {
|
||||
cl.belongsTo.Leave(cl.name, cl.color)
|
||||
}
|
||||
|
||||
//Sending message block to the client
|
||||
func (cl *Client) Send(msgs string) {
|
||||
cl.conn.WriteMessage(websocket.TextMessage, []byte(msgs))
|
||||
}
|
||||
|
||||
// Send server message to this client
|
||||
func (cl *Client) ServerMessage(msg string) {
|
||||
msg = ParseEmotes(msg)
|
||||
cl.Send(`<span class="svmsg">` + msg + `</span><br />`)
|
||||
}
|
||||
|
||||
// Outgoing messages
|
||||
func (cl *Client) Message(msg string) {
|
||||
msg = ParseEmotes(msg)
|
||||
cl.belongsTo.AddMsg(
|
||||
`<span class="name" style="color:` + cl.color + `">` + cl.name +
|
||||
`</span><b>:</b> <span class="msg">` + msg + `</span><br />`)
|
||||
}
|
||||
|
||||
// Outgoing /me command
|
||||
func (cl *Client) Me(msg string) {
|
||||
msg = ParseEmotes(msg)
|
||||
cl.belongsTo.AddMsg(fmt.Sprintf(`<span style="color:%s"><span class="name">%s</span> <span class="cmdme">%s</span><br />`, cl.color, cl.name, msg))
|
||||
}
|
||||
|
||||
func (cl *Client) Mod() {
|
||||
cl.IsMod = true
|
||||
}
|
||||
|
||||
func (cl *Client) Unmod() {
|
||||
cl.IsMod = false
|
||||
}
|
||||
|
||||
func (cl *Client) Host() string {
|
||||
host, _, err := net.SplitHostPort(cl.conn.RemoteAddr().String())
|
||||
if err != nil {
|
||||
host = "err"
|
||||
}
|
||||
return host
|
||||
}
|
||||
|
||||
var dumbSpaces []string = []string{
|
||||
"\n",
|
||||
"\t",
|
||||
"\r",
|
||||
"\u200b",
|
||||
}
|
||||
|
||||
func removeDumbSpaces(msg string) string {
|
||||
for _, ds := range dumbSpaces {
|
||||
msg = strings.ReplaceAll(msg, ds, " ")
|
||||
}
|
||||
|
||||
newMsg := ""
|
||||
for _, r := range msg {
|
||||
if unicode.IsSpace(r) {
|
||||
newMsg += " "
|
||||
} else {
|
||||
newMsg += string(r)
|
||||
}
|
||||
}
|
||||
return newMsg
|
||||
}
|
238
chatcommands.go
Normal file
238
chatcommands.go
Normal file
@ -0,0 +1,238 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"html"
|
||||
"regexp"
|
||||
"strings"
|
||||
)
|
||||
|
||||
var commands *CommandControl
|
||||
var colorRegex *regexp.Regexp = regexp.MustCompile(`^#[0-9A-Fa-f]{6}$`)
|
||||
|
||||
type CommandControl struct {
|
||||
user map[string]CommandFunction
|
||||
mod map[string]CommandFunction
|
||||
admin map[string]CommandFunction
|
||||
}
|
||||
|
||||
type CommandFunction func(client *Client, args []string) string
|
||||
|
||||
func init() {
|
||||
commands = &CommandControl{
|
||||
user: map[string]CommandFunction{
|
||||
"me": func(client *Client, args []string) string {
|
||||
client.Me(strings.Join(args, " "))
|
||||
return ""
|
||||
},
|
||||
"help": func(client *Client, args []string) string {
|
||||
return "I haven't written this yet LUL"
|
||||
},
|
||||
"count": func(client *Client, args []string) string {
|
||||
return fmt.Sprintf("Users in chat: %d", client.belongsTo.UserCount())
|
||||
},
|
||||
"color": cmdColor,
|
||||
"colour": cmdColor,
|
||||
"w": func(cl *Client, args []string) string {
|
||||
return fmt.Sprintf("Name: %s IsMod: %t IsAdmin: %t", cl.name, cl.IsMod, cl.IsAdmin)
|
||||
},
|
||||
"whoami": func(cl *Client, args []string) string {
|
||||
return fmt.Sprintf("Name: %s IsMod: %t IsAdmin: %t", cl.name, cl.IsMod, cl.IsAdmin)
|
||||
},
|
||||
"auth": func(cl *Client, args []string) string {
|
||||
if cl.IsAdmin {
|
||||
return "You are already authenticated."
|
||||
}
|
||||
|
||||
pw := html.UnescapeString(strings.Join(args, " "))
|
||||
|
||||
//fmt.Printf("/auth from %s. expecting %q [%X], received %q [%X]\n", cl.name, settings.AdminPassword, settings.AdminPassword, pw, pw)
|
||||
if settings.AdminPassword == pw {
|
||||
cl.IsMod = true
|
||||
cl.IsAdmin = true
|
||||
return "Admin rights granted."
|
||||
}
|
||||
|
||||
// Don't let on that this command exists. Not the most secure, but should be "good enough" LUL.
|
||||
return "Invalid command."
|
||||
},
|
||||
"users": func(cl *Client, args []string) string {
|
||||
names := cl.belongsTo.GetNames()
|
||||
return strings.Join(names, " ")
|
||||
},
|
||||
},
|
||||
|
||||
mod: map[string]CommandFunction{
|
||||
"sv": func(cl *Client, args []string) string {
|
||||
if len(args) == 0 {
|
||||
return "Missing message"
|
||||
}
|
||||
svmsg := formatLinks(ParseEmotes(strings.Join(args, " ")))
|
||||
cl.belongsTo.AddCmdMsg(fmt.Sprintf(`<div class="announcement">%s</div>`, svmsg))
|
||||
return ""
|
||||
},
|
||||
"playing": func(cl *Client, args []string) string {
|
||||
// Clear/hide title if sent with no arguments.
|
||||
if len(args) == 1 {
|
||||
cl.belongsTo.ClearPlaying()
|
||||
//cl.belongsTo.AddMsg(`<script>setPlaying("","");</script>`)
|
||||
return ""
|
||||
}
|
||||
link := ""
|
||||
title := ""
|
||||
|
||||
// pickout the link (can be anywhere, as long as there are no spaces).
|
||||
for _, word := range args {
|
||||
word = html.UnescapeString(word)
|
||||
if strings.HasPrefix(word, "http://") || strings.HasPrefix(word, "https://") {
|
||||
link = word
|
||||
} else {
|
||||
title = title + " " + word
|
||||
}
|
||||
}
|
||||
|
||||
cl.belongsTo.SetPlaying(title, link)
|
||||
//cl.belongsTo.AddMsg(fmt.Sprintf(`<script>setPlaying("%s","%s");</script>`, title, link))
|
||||
return ""
|
||||
},
|
||||
"unmod": func(cl *Client, args []string) string {
|
||||
if len(args) > 0 && !cl.IsAdmin {
|
||||
return "You can only unmod yourself, not others."
|
||||
}
|
||||
|
||||
if len(args) == 0 {
|
||||
cl.Unmod()
|
||||
return "You have unmodded yourself."
|
||||
}
|
||||
|
||||
if err := cl.belongsTo.Unmod(args[0]); err != nil {
|
||||
return err.Error()
|
||||
}
|
||||
|
||||
return fmt.Sprintf(`%s has been unmodded.`, args[0])
|
||||
},
|
||||
"kick": func(cl *Client, args []string) string {
|
||||
if len(args) == 0 {
|
||||
return "Missing name to kick."
|
||||
}
|
||||
return cl.belongsTo.Kick(args[0])
|
||||
},
|
||||
"ban": func(cl *Client, args []string) string {
|
||||
if len(args) == 0 {
|
||||
return "missing name to ban."
|
||||
}
|
||||
fmt.Printf("[ban] Attempting to ban %s\n", strings.Join(args, ""))
|
||||
return cl.belongsTo.Ban(args[0])
|
||||
},
|
||||
"unban": func(cl *Client, args []string) string {
|
||||
if len(args) == 0 {
|
||||
return "missing name to unban."
|
||||
}
|
||||
fmt.Printf("[ban] Attempting to unban %s\n", strings.Join(args, ""))
|
||||
|
||||
err := settings.RemoveBan(args[0])
|
||||
if err != nil {
|
||||
return err.Error()
|
||||
}
|
||||
return ""
|
||||
},
|
||||
},
|
||||
|
||||
admin: map[string]CommandFunction{
|
||||
"mod": func(cl *Client, args []string) string {
|
||||
if err := cl.belongsTo.Mod(args[0]); err != nil {
|
||||
return err.Error()
|
||||
}
|
||||
return fmt.Sprintf(`%s has been modded.`, args[0])
|
||||
},
|
||||
"reloadplayer": func(cl *Client, args []string) string {
|
||||
cl.belongsTo.AddCmdMsg(`<span class="svmsg">[SERVER] Video player reload forced.</span><script>initPlayer();</script><br />`)
|
||||
return "Reloading player for all chatters."
|
||||
},
|
||||
"reloademotes": func(cl *Client, args []string) string {
|
||||
cl.ServerMessage("Reloading emotes")
|
||||
num, err := LoadEmotes()
|
||||
if err != nil {
|
||||
fmt.Printf("Unbale to reload emotes: %s\n", err)
|
||||
return fmt.Sprintf("ERROR: %s", err)
|
||||
}
|
||||
|
||||
fmt.Printf("Loaded %d emotes\n", num)
|
||||
return fmt.Sprintf("Emotes loaded: %d", num)
|
||||
},
|
||||
//"reloadsettings": func(cl *Client, args []string) string {
|
||||
// return ""
|
||||
//},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func (cc *CommandControl) RunCommand(command string, args []string, sender *Client) string {
|
||||
// Look for user command
|
||||
if userCmd, ok := cc.user[command]; ok {
|
||||
fmt.Printf("[user] %s /%s %s\n", sender.name, command, strings.Join(args, " "))
|
||||
return userCmd(sender, args)
|
||||
}
|
||||
|
||||
// Look for mod command
|
||||
if modCmd, ok := cc.mod[command]; ok {
|
||||
if sender.IsMod || sender.IsAdmin {
|
||||
fmt.Printf("[mod] %s /%s %s\n", sender.name, command, strings.Join(args, " "))
|
||||
return modCmd(sender, args)
|
||||
}
|
||||
|
||||
fmt.Printf("[mod REJECTED] %s /%s %s\n", sender.name, command, strings.Join(args, " "))
|
||||
return "You are not a mod Jebaited"
|
||||
}
|
||||
|
||||
// Look for admin command
|
||||
if adminCmd, ok := cc.admin[command]; ok {
|
||||
if sender.IsAdmin {
|
||||
fmt.Printf("[admin] %s /%s %s\n", sender.name, command, strings.Join(args, " "))
|
||||
return adminCmd(sender, args)
|
||||
}
|
||||
fmt.Printf("[admin REJECTED] %s /%s %s\n", sender.name, command, strings.Join(args, " "))
|
||||
return "You are not the admin Jebaited"
|
||||
}
|
||||
|
||||
// Command not found
|
||||
fmt.Printf("[cmd] %s /%s %s\n", sender.name, command, strings.Join(args, " "))
|
||||
return "Invalid command."
|
||||
}
|
||||
|
||||
func cmdColor(cl *Client, args []string) string {
|
||||
if cl.IsMod || cl.IsAdmin && len(args) == 2 {
|
||||
color := ""
|
||||
name := ""
|
||||
for _, s := range args {
|
||||
if strings.HasPrefix(s, "#") {
|
||||
color = s
|
||||
} else {
|
||||
name = s
|
||||
}
|
||||
}
|
||||
if color == "" {
|
||||
|
||||
fmt.Printf("[color:mod] %s missing color\n", cl.name)
|
||||
return "Missing color"
|
||||
}
|
||||
|
||||
if err := cl.belongsTo.ForceColorChange(name, color); err != nil {
|
||||
return err.Error()
|
||||
}
|
||||
return fmt.Sprintf("Color changed for user %s to %s\n", name, color)
|
||||
}
|
||||
if len(args) == 0 || !colorRegex.MatchString(args[0]) {
|
||||
cl.color = randomColor()
|
||||
return "Random color chosen. To choose a specific color use the format <i>/color #c029ce</i>. Hex values expected."
|
||||
}
|
||||
|
||||
if cl.IsColorForced {
|
||||
fmt.Printf("[color] %s tried to change a forced color\n", cl.name)
|
||||
return "You are not allowed to change your color."
|
||||
}
|
||||
|
||||
cl.color = args[0]
|
||||
fmt.Printf("[color] %s new color: %s\n", cl.name, cl.color)
|
||||
return "Color changed successfully."
|
||||
}
|
78
chathandler.go
Normal file
78
chathandler.go
Normal file
@ -0,0 +1,78 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
//"net"
|
||||
"net/http"
|
||||
"path/filepath"
|
||||
|
||||
"github.com/gorilla/websocket"
|
||||
)
|
||||
|
||||
//global variable for handling all chat traffic
|
||||
var chat ChatRoom
|
||||
|
||||
// Serving static files
|
||||
func wsStaticFiles(w http.ResponseWriter, r *http.Request) {
|
||||
switch r.URL.Path {
|
||||
case "/favicon.ico":
|
||||
http.ServeFile(w, r, "./static/favicon.png")
|
||||
return
|
||||
case "/justchat":
|
||||
http.ServeFile(w, r, "./static/justchat.html")
|
||||
return
|
||||
case "/justvideo":
|
||||
http.ServeFile(w, r, "./static/justvideo.html")
|
||||
return
|
||||
}
|
||||
|
||||
goodPath := r.URL.Path[8:len(r.URL.Path)]
|
||||
fmt.Printf("[static] serving %q from folder ./static/\n", goodPath)
|
||||
|
||||
http.ServeFile(w, r, "./static/"+goodPath)
|
||||
}
|
||||
|
||||
func wsEmotes(w http.ResponseWriter, r *http.Request) {
|
||||
emotefile := filepath.Base(r.URL.Path)
|
||||
//fmt.Printf("serving emote: %s\n", emotefile)
|
||||
http.ServeFile(w, r, "./static/emotes/"+emotefile)
|
||||
}
|
||||
|
||||
// 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) {
|
||||
|
||||
conn, err := upgrader.Upgrade(w, r, nil)
|
||||
|
||||
if err != nil {
|
||||
fmt.Println("Error upgrading to websocket:", err)
|
||||
return
|
||||
}
|
||||
go func() {
|
||||
//first message has to be the name
|
||||
_, msg, err := conn.ReadMessage()
|
||||
client := chat.Join(string(msg), conn)
|
||||
if client == nil || err != nil {
|
||||
fmt.Printf("[handler] Client closed connection: %s\n", conn.RemoteAddr().String())
|
||||
conn.Close() //closing connection to indicate failed Join
|
||||
return
|
||||
}
|
||||
|
||||
//then watch for incoming messages
|
||||
for {
|
||||
_, msg, err := conn.ReadMessage()
|
||||
if err != nil { //if error then assuming that the connection is closed
|
||||
client.Exit()
|
||||
return
|
||||
}
|
||||
client.NewMsg(string(msg))
|
||||
}
|
||||
|
||||
}()
|
||||
}
|
316
chatroom.go
Normal file
316
chatroom.go
Normal file
@ -0,0 +1,316 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
//"html"
|
||||
"math/rand"
|
||||
"path/filepath"
|
||||
"regexp"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/gorilla/websocket"
|
||||
)
|
||||
|
||||
const UsernameMaxLength int = 36
|
||||
|
||||
var re_username *regexp.Regexp = regexp.MustCompile(`^[0-9a-zA-Z_-]+$`)
|
||||
|
||||
type ChatRoom struct {
|
||||
clients map[string]*Client // this needs to be a pointer.
|
||||
clientsMtx sync.Mutex
|
||||
queue chan string
|
||||
playing string
|
||||
playingLink string
|
||||
}
|
||||
|
||||
//initializing the chatroom
|
||||
func (cr *ChatRoom) Init() error {
|
||||
cr.queue = make(chan string, 5)
|
||||
cr.clients = make(map[string]*Client)
|
||||
|
||||
num, err := LoadEmotes()
|
||||
if err != nil {
|
||||
return fmt.Errorf("Error loading emotes: %s", err)
|
||||
}
|
||||
fmt.Printf("Loaded %d emotes\n", num)
|
||||
|
||||
//the "heartbeat" for broadcasting messages
|
||||
go func() {
|
||||
for {
|
||||
cr.BroadCast()
|
||||
time.Sleep(100 * time.Millisecond)
|
||||
}
|
||||
}()
|
||||
return nil
|
||||
}
|
||||
|
||||
func LoadEmotes() (int, error) {
|
||||
newEmotes := map[string]string{}
|
||||
|
||||
emotePNGs, err := filepath.Glob("./static/emotes/*.png")
|
||||
if err != nil {
|
||||
return 0, fmt.Errorf("Unable to glob emote directory: %s\n", err)
|
||||
}
|
||||
|
||||
emoteGIFs, err := filepath.Glob("./static/emotes/*.gif")
|
||||
if err != nil {
|
||||
return 0, fmt.Errorf("Unable to glob emote directory: %s\n", err)
|
||||
}
|
||||
globbed_files := []string(emotePNGs)
|
||||
globbed_files = append(globbed_files, emoteGIFs...)
|
||||
|
||||
fmt.Println("Loading emotes...")
|
||||
for _, file := range globbed_files {
|
||||
file = filepath.Base(file)
|
||||
key := file[0 : len(file)-4]
|
||||
newEmotes[key] = file
|
||||
fmt.Printf("%s ", key)
|
||||
}
|
||||
emotes = newEmotes
|
||||
fmt.Println("")
|
||||
return len(emotes), nil
|
||||
}
|
||||
|
||||
func randomColor() string {
|
||||
nums := []int32{}
|
||||
for i := 0; i < 6; i++ {
|
||||
nums = append(nums, rand.Int31n(15))
|
||||
}
|
||||
return fmt.Sprintf("#%X%X%X%X%X%X",
|
||||
nums[0], nums[1], nums[2],
|
||||
nums[3], nums[4], nums[5])
|
||||
}
|
||||
|
||||
//registering a new client
|
||||
//returns pointer to a Client, or Nil, if the name is already taken
|
||||
func (cr *ChatRoom) Join(name string, conn *websocket.Conn) *Client {
|
||||
|
||||
if len(name) > UsernameMaxLength || !re_username.MatchString(name) {
|
||||
return nil
|
||||
}
|
||||
|
||||
defer cr.clientsMtx.Unlock()
|
||||
cr.clientsMtx.Lock() //preventing simultaneous access to the `clients` map
|
||||
if _, exists := cr.clients[strings.ToLower(name)]; exists {
|
||||
return nil
|
||||
}
|
||||
|
||||
client := &Client{
|
||||
name: name,
|
||||
conn: conn,
|
||||
belongsTo: cr,
|
||||
color: randomColor(),
|
||||
}
|
||||
|
||||
host := client.Host()
|
||||
|
||||
if banned, names := settings.IsBanned(host); banned {
|
||||
fmt.Printf("[BAN] Banned user tried to connect with IP %s: %q (banned with name(s) %q)\n", host, name, strings.Join(names, ", "))
|
||||
conn.Close()
|
||||
return nil
|
||||
}
|
||||
|
||||
cr.clients[strings.ToLower(name)] = client
|
||||
|
||||
fmt.Printf("[join] %s %s\n", host, name)
|
||||
client.Send(cr.GetPlayingString())
|
||||
cr.AddMsg(fmt.Sprintf("<i><b style=\"color:%s\">%s</b> has joined the chat.</i><br />", client.color, name))
|
||||
return client
|
||||
}
|
||||
|
||||
// TODO: fix this up a bit. kick and leave are the same, incorrect, error: "That name was already used!"
|
||||
//leaving the chatroom
|
||||
func (cr *ChatRoom) Leave(name, color string) {
|
||||
defer cr.clientsMtx.Unlock()
|
||||
cr.clientsMtx.Lock() //preventing simultaneous access to the `clients` map
|
||||
|
||||
client, err := cr.getClient(name)
|
||||
if err != nil {
|
||||
fmt.Printf("[leave] Unable to get client for name %q\n", name)
|
||||
return
|
||||
}
|
||||
client.conn.Close()
|
||||
cr.delClient(name)
|
||||
|
||||
cr.AddMsg(fmt.Sprintf("<i><b style=\"color:%s\">%s</b> has left the chat.</i><br />", color, name))
|
||||
fmt.Printf("[leave] %s %s\n", client.Host(), client.name)
|
||||
}
|
||||
|
||||
// kicked from the chatroom
|
||||
func (cr *ChatRoom) Kick(name string) string {
|
||||
defer cr.clientsMtx.Unlock()
|
||||
cr.clientsMtx.Lock() //preventing simultaneous access to the `clients` map
|
||||
|
||||
client, err := cr.getClient(name)
|
||||
if err != nil {
|
||||
return "Unable to get client for name " + name
|
||||
}
|
||||
|
||||
if client.IsMod {
|
||||
return "You cannot kick another mod."
|
||||
}
|
||||
|
||||
if client.IsAdmin {
|
||||
return "Jebaited No."
|
||||
}
|
||||
|
||||
host := client.Host()
|
||||
client.conn.Close()
|
||||
cr.delClient(name)
|
||||
|
||||
cr.AddMsg(fmt.Sprintf("<i><b>%s</b> has been kicked.</i><br />", name))
|
||||
fmt.Printf("[kick] %s %s has been kicked\n", host, name)
|
||||
return ""
|
||||
}
|
||||
|
||||
func (cr *ChatRoom) Ban(name string) string {
|
||||
defer cr.clientsMtx.Unlock()
|
||||
cr.clientsMtx.Lock()
|
||||
|
||||
client, err := cr.getClient(name)
|
||||
if err != nil {
|
||||
fmt.Printf("[ban] Unable to get client for name %q\n", name)
|
||||
return "Cannot find that name"
|
||||
}
|
||||
|
||||
names := []string{}
|
||||
host := client.Host()
|
||||
|
||||
client.conn.Close()
|
||||
cr.delClient(name)
|
||||
|
||||
for name, c := range cr.clients {
|
||||
if c.Host() == host {
|
||||
names = append(names, name)
|
||||
client.conn.Close()
|
||||
cr.delClient(name)
|
||||
}
|
||||
}
|
||||
|
||||
defer settingsMtx.Unlock()
|
||||
settingsMtx.Lock()
|
||||
|
||||
err = settings.AddBan(host, names)
|
||||
if err != nil {
|
||||
fmt.Printf("[BAN] Error banning %q: %s\n", name, err)
|
||||
cr.AddMsg(fmt.Sprintf("<i><b>%s</b> has been kicked.</i><br />", name))
|
||||
} else {
|
||||
cr.AddMsg(fmt.Sprintf("<i><b>%s</b> has been banned.</i><br />", name))
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
//adding message to queue
|
||||
func (cr *ChatRoom) AddMsg(msg string) {
|
||||
cr.queue <- msg
|
||||
}
|
||||
|
||||
func (cr *ChatRoom) AddCmdMsg(msg string) {
|
||||
cr.queue <- msg
|
||||
}
|
||||
|
||||
func (cr *ChatRoom) Unmod(name string) error {
|
||||
defer cr.clientsMtx.Unlock()
|
||||
cr.clientsMtx.Lock()
|
||||
|
||||
client, err := cr.getClient(name)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
client.Unmod()
|
||||
client.ServerMessage(`You have been unmodded.`)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (cr *ChatRoom) Mod(name string) error {
|
||||
defer cr.clientsMtx.Unlock()
|
||||
cr.clientsMtx.Lock()
|
||||
|
||||
client, err := cr.getClient(name)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
client.IsMod = true
|
||||
client.ServerMessage(`You have been modded.`)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (cr *ChatRoom) ForceColorChange(name, color string) error {
|
||||
defer cr.clientsMtx.Unlock()
|
||||
cr.clientsMtx.Lock()
|
||||
|
||||
client, err := cr.getClient(name)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
client.IsColorForced = true
|
||||
client.color = color
|
||||
return nil
|
||||
}
|
||||
|
||||
func (cr *ChatRoom) UserCount() int {
|
||||
return len(cr.clients)
|
||||
}
|
||||
|
||||
//broadcasting all the messages in the queue in one block
|
||||
func (cr *ChatRoom) BroadCast() {
|
||||
msgBlock := ""
|
||||
infLoop:
|
||||
for {
|
||||
select {
|
||||
case m := <-cr.queue:
|
||||
msgBlock += m // + "<br />"
|
||||
default:
|
||||
break infLoop
|
||||
}
|
||||
}
|
||||
if len(msgBlock) > 0 {
|
||||
for _, client := range cr.clients {
|
||||
client.Send(msgBlock)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (cr *ChatRoom) ClearPlaying() {
|
||||
cr.playing = ""
|
||||
cr.playingLink = ""
|
||||
cr.AddCmdMsg(`<script>setPlaying("","");</script>`)
|
||||
}
|
||||
|
||||
func (cr *ChatRoom) SetPlaying(title, link string) {
|
||||
cr.playing = title
|
||||
cr.playingLink = link
|
||||
cr.AddCmdMsg(cr.GetPlayingString())
|
||||
}
|
||||
|
||||
func (cr *ChatRoom) GetPlayingString() string {
|
||||
return fmt.Sprintf(`<script>setPlaying("%s","%s");</script>`, cr.playing, cr.playingLink)
|
||||
}
|
||||
|
||||
func (cr *ChatRoom) GetNames() []string {
|
||||
names := []string{}
|
||||
defer cr.clientsMtx.Unlock()
|
||||
cr.clientsMtx.Lock()
|
||||
|
||||
for _, val := range cr.clients {
|
||||
names = append(names, val.name)
|
||||
}
|
||||
|
||||
return names
|
||||
}
|
||||
|
||||
func (cr *ChatRoom) delClient(name string) {
|
||||
delete(cr.clients, strings.ToLower(name))
|
||||
}
|
||||
|
||||
func (cr *ChatRoom) getClient(name string) (*Client, error) {
|
||||
if client, ok := cr.clients[strings.ToLower(name)]; ok {
|
||||
return client, nil
|
||||
}
|
||||
return nil, fmt.Errorf("Client with that name not found.")
|
||||
}
|
141
main.go
Normal file
141
main.go
Normal file
@ -0,0 +1,141 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"flag"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"os"
|
||||
"sync"
|
||||
|
||||
"github.com/nareix/joy4/av/avutil"
|
||||
"github.com/nareix/joy4/av/pubsub"
|
||||
"github.com/nareix/joy4/format"
|
||||
"github.com/nareix/joy4/format/flv"
|
||||
"github.com/nareix/joy4/format/rtmp"
|
||||
)
|
||||
|
||||
var (
|
||||
addr = flag.String("l", ":8089", "host:port of the go-rtmp-server")
|
||||
sKey = flag.String("k", "", "Stream key, to protect your stream")
|
||||
)
|
||||
|
||||
func init() {
|
||||
format.RegisterAll()
|
||||
}
|
||||
|
||||
type writeFlusher struct {
|
||||
httpflusher http.Flusher
|
||||
io.Writer
|
||||
}
|
||||
|
||||
func (self writeFlusher) Flush() error {
|
||||
self.httpflusher.Flush()
|
||||
return nil
|
||||
}
|
||||
|
||||
func main() {
|
||||
flag.Parse()
|
||||
server := &rtmp.Server{}
|
||||
|
||||
l := &sync.RWMutex{}
|
||||
type Channel struct {
|
||||
que *pubsub.Queue
|
||||
}
|
||||
channels := map[string]*Channel{}
|
||||
|
||||
server.HandlePlay = func(conn *rtmp.Conn) {
|
||||
l.RLock()
|
||||
ch := channels[conn.URL.Path]
|
||||
l.RUnlock()
|
||||
|
||||
if ch != nil {
|
||||
cursor := ch.que.Latest()
|
||||
avutil.CopyFile(conn, cursor)
|
||||
}
|
||||
}
|
||||
|
||||
server.HandlePublish = func(conn *rtmp.Conn) {
|
||||
streams, _ := conn.Streams()
|
||||
|
||||
l.Lock()
|
||||
fmt.Println("request string->", conn.URL.RequestURI())
|
||||
fmt.Println("request key->", conn.URL.Query().Get("key"))
|
||||
streamKey := conn.URL.Query().Get("key")
|
||||
if streamKey != *sKey {
|
||||
fmt.Println("Due to key not match, denied stream")
|
||||
return //If key not match, deny stream
|
||||
}
|
||||
ch := channels[conn.URL.Path]
|
||||
if ch == nil {
|
||||
ch = &Channel{}
|
||||
ch.que = pubsub.NewQueue()
|
||||
ch.que.WriteHeader(streams)
|
||||
channels[conn.URL.Path] = ch
|
||||
} else {
|
||||
ch = nil
|
||||
}
|
||||
l.Unlock()
|
||||
if ch == nil {
|
||||
return
|
||||
}
|
||||
|
||||
avutil.CopyPackets(ch.que, conn)
|
||||
|
||||
l.Lock()
|
||||
delete(channels, conn.URL.Path)
|
||||
l.Unlock()
|
||||
ch.que.Close()
|
||||
}
|
||||
|
||||
// Chat websocket
|
||||
http.HandleFunc("/ws", wsHandler)
|
||||
http.HandleFunc("/static/", wsStaticFiles)
|
||||
http.HandleFunc("/emotes/", wsEmotes)
|
||||
http.HandleFunc("/favicon.ico", wsStaticFiles)
|
||||
http.HandleFunc("/jquery.js", wsStaticFiles)
|
||||
http.HandleFunc("/ractive.min.js", wsStaticFiles)
|
||||
http.HandleFunc("/justchat", wsStaticFiles)
|
||||
http.HandleFunc("/justvideo", wsStaticFiles)
|
||||
|
||||
http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
|
||||
l.RLock()
|
||||
ch := channels[r.URL.Path]
|
||||
l.RUnlock()
|
||||
|
||||
if ch != nil {
|
||||
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 != "/" {
|
||||
fmt.Println("[http 404] ", r.URL.Path)
|
||||
http.NotFound(w, r)
|
||||
} else {
|
||||
http.ServeFile(w, r, "./static/index.html")
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
go http.ListenAndServe(*addr, nil)
|
||||
fmt.Println("Listen and serve ", *addr)
|
||||
if err := chat.Init(); err != nil {
|
||||
fmt.Println(err)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
server.ListenAndServe()
|
||||
|
||||
// ffmpeg -re -i movie.flv -c copy -f flv rtmp://localhost/movie
|
||||
// ffmpeg -f avfoundation -i "0:0" .... -f flv rtmp://localhost/screen
|
||||
// ffplay http://localhost:8089/movie
|
||||
// ffplay http://localhost:8089/screen
|
||||
}
|
63
notes.txt
Normal file
63
notes.txt
Normal 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
|
41
readme.md
Normal file
41
readme.md
Normal file
@ -0,0 +1,41 @@
|
||||
# Golang rtmp server demo
|
||||
|
||||
This is a very tiny demo with rtmp protocol server/client side implement.
|
||||
|
||||
## Requirement
|
||||
|
||||
You need golang to build all tools.
|
||||
|
||||
## Install
|
||||
|
||||
```bash
|
||||
go get -u -v github.com/netroby/go-rtmp-server
|
||||
|
||||
~/go/bin/go-rtmp-server -l :8089 -k longSecurityKey
|
||||
```
|
||||
|
||||
|
||||
## Usage
|
||||
|
||||
|
||||
now you can using obs to push stream to rtmp server
|
||||
|
||||
|
||||
the stream url maybe ```rtmp://your.domain.host/live?key=longSecurityKey```
|
||||
|
||||
You can using obs to stream
|
||||
|
||||
Now you may visit the demo at
|
||||
```
|
||||
http://your.domain.host:8089/
|
||||
```
|
||||
|
||||
the :8089 is the default listen port of the http server. and you can change it as you want
|
||||
|
||||
```
|
||||
Usage of .\go-rtmp-server.exe:
|
||||
-k string
|
||||
Stream key, to protect your stream
|
||||
-l string
|
||||
host:port of the go-rtmp-server (default ":8089")
|
||||
```
|
120
settings.go
Normal file
120
settings.go
Normal file
@ -0,0 +1,120 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"math/rand"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
)
|
||||
|
||||
var settings *Settings
|
||||
var settingsMtx sync.Mutex
|
||||
|
||||
type Settings struct {
|
||||
filename string
|
||||
AdminPassword string
|
||||
Bans []BanInfo
|
||||
}
|
||||
|
||||
type BanInfo struct {
|
||||
IP string
|
||||
Names []string
|
||||
When time.Time
|
||||
}
|
||||
|
||||
func init() {
|
||||
var err error
|
||||
settings, err = LoadSettings("settings.json")
|
||||
if err != nil {
|
||||
panic("Unable to load settings: " + err.Error())
|
||||
}
|
||||
}
|
||||
|
||||
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
|
||||
|
||||
s.AdminPassword = generateAdminPass(time.Now().Unix())
|
||||
fmt.Printf("Settings reloaded. New admin password: %s\n", s.AdminPassword)
|
||||
|
||||
return s, nil
|
||||
}
|
||||
|
||||
func generateAdminPass(seed int64) string {
|
||||
out := ""
|
||||
r := rand.New(rand.NewSource(seed))
|
||||
//for i := 0; i < 20; i++ {
|
||||
for len(out) < 20 {
|
||||
out = fmt.Sprintf("%s%X", out, r.Int31())
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
func (s *Settings) Save() error {
|
||||
marshaled, err := json.Marshal(s)
|
||||
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 {
|
||||
b := BanInfo{
|
||||
Names: names,
|
||||
IP: host,
|
||||
When: time.Now(),
|
||||
}
|
||||
settings.Bans = append(settings.Bans, b)
|
||||
|
||||
fmt.Printf("[BAN] %q (%s) has been banned.\n", strings.Join(names, ", "), host)
|
||||
|
||||
return settings.Save()
|
||||
}
|
||||
|
||||
func (s *Settings) RemoveBan(name string) error {
|
||||
defer settingsMtx.Unlock()
|
||||
settingsMtx.Lock()
|
||||
|
||||
name = strings.ToLower(name)
|
||||
newBans := []BanInfo{}
|
||||
for _, b := range s.Bans {
|
||||
for _, n := range b.Names {
|
||||
if n == name {
|
||||
fmt.Printf("[ban] Removed ban for %s [%s]\n", b.IP, n)
|
||||
} else {
|
||||
newBans = append(newBans, b)
|
||||
}
|
||||
}
|
||||
}
|
||||
s.Bans = newBans
|
||||
return settings.Save()
|
||||
}
|
||||
|
||||
func (s *Settings) IsBanned(host string) (bool, []string) {
|
||||
defer settingsMtx.Unlock()
|
||||
settingsMtx.Lock()
|
||||
|
||||
for _, b := range s.Bans {
|
||||
if b.IP == host {
|
||||
return true, b.Names
|
||||
}
|
||||
}
|
||||
return false, nil
|
||||
}
|
3
settings.json
Normal file
3
settings.json
Normal file
@ -0,0 +1,3 @@
|
||||
{
|
||||
"Bans": []
|
||||
}
|
7
static/flv.min.js
vendored
Normal file
7
static/flv.min.js
vendored
Normal file
File diff suppressed because one or more lines are too long
229
static/index.html
Normal file
229
static/index.html
Normal file
@ -0,0 +1,229 @@
|
||||
<!doctype html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset='utf-8'>
|
||||
<title>Movie Night!</title>
|
||||
<style>
|
||||
body {
|
||||
margin:0;
|
||||
padding:0;
|
||||
background:#000;
|
||||
}
|
||||
|
||||
html, body, #messages, #phase2 {
|
||||
height:100%;
|
||||
}
|
||||
|
||||
video {
|
||||
width:100%;
|
||||
}
|
||||
#streambox {
|
||||
width: 80%;
|
||||
float: left;
|
||||
margin: 0;
|
||||
}
|
||||
#chatbox {
|
||||
width: 19%;
|
||||
float: right;
|
||||
height:85%;
|
||||
}
|
||||
#messages {
|
||||
border: 1px solid #666;
|
||||
width: 95%;
|
||||
overflow: auto;
|
||||
color: #f4f4f4;
|
||||
}
|
||||
#msg {
|
||||
width: 94%;
|
||||
height: 3em;
|
||||
}
|
||||
#error {
|
||||
color: #f00;
|
||||
padding: 5px;
|
||||
font-weight: bold;
|
||||
}
|
||||
span.name {
|
||||
font-weight:bold;
|
||||
}
|
||||
span.cmdme {
|
||||
font-style: italic;
|
||||
}
|
||||
span.msg {
|
||||
font-style: normal;
|
||||
color: #cfccd1;
|
||||
}
|
||||
span.svmsg {
|
||||
font-style: italic;
|
||||
color: #ea6260;
|
||||
}
|
||||
|
||||
.announcement {
|
||||
font-weight: bold;
|
||||
color: #ea6260;
|
||||
text-align: center;
|
||||
width: 100%;
|
||||
margin-top: 10px;
|
||||
margin-bottom: 10px;
|
||||
border-top: 3px solid red;
|
||||
border-bottom: 3px solid red;
|
||||
}
|
||||
|
||||
#playingDiv {
|
||||
color: #8b6a96;
|
||||
font-weight: bold;
|
||||
padding: 10px;
|
||||
}
|
||||
#playing {
|
||||
font-size: x-Large;
|
||||
}
|
||||
</style>
|
||||
<script src="/static/jquery.js"></script>
|
||||
<script src="/static/flv.min.js"></script>
|
||||
<script>
|
||||
|
||||
function initPlayer() {
|
||||
if (flvjs.isSupported()) {
|
||||
var videoElement = document.getElementById('videoElement');
|
||||
var flvPlayer = flvjs.createPlayer({
|
||||
type: 'flv',
|
||||
url: '/live'
|
||||
});
|
||||
flvPlayer.attachMediaElement(videoElement);
|
||||
flvPlayer.load();
|
||||
flvPlayer.play();
|
||||
}
|
||||
}
|
||||
|
||||
function escapeHtml(string) {
|
||||
return string;
|
||||
//return String(string).replace(/[&<>"'\/]/g, function (s) {
|
||||
// return entityMap[s];
|
||||
//});
|
||||
}
|
||||
|
||||
//helper function for debugging purposes
|
||||
function toHex(str) {
|
||||
var result = '';
|
||||
for (var i=0; i<str.length; i++) {
|
||||
result += ("0"+str.charCodeAt(i).toString(16)).slice(-2)+" ";
|
||||
}
|
||||
return result.toUpperCase();
|
||||
}
|
||||
|
||||
function setPlaying(title, link) {
|
||||
if (title === "") {
|
||||
$('#playingDiv').hide();
|
||||
document.title = "Movie Night"
|
||||
return;
|
||||
}
|
||||
|
||||
$('#playingDiv').show();
|
||||
$('#playing').text(title);
|
||||
document.title = "Movie Night | " + title
|
||||
|
||||
if (link === "") {
|
||||
$('#playinglink').hide();
|
||||
return;
|
||||
}
|
||||
|
||||
$('#playinglink').show();
|
||||
$('#playinglink').text('[Info Link]');
|
||||
$('#playinglink').attr('href', link);
|
||||
}
|
||||
</script>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div id="streambox">
|
||||
|
||||
<video id="videoElement" controls autoplay x5-video-player-type="h5" x5-video-player-fullscreen="true" playsinline webkit-playsinline>
|
||||
Your browser is too old and doesn't support HTML5 video.
|
||||
</video>
|
||||
<script>initPlayer();</script>
|
||||
|
||||
<button style="float:right" id="reload" onclick="initPlayer();">Reload Player</button>
|
||||
<div id="playingDiv"><span id="playing"></span><br /><a href="" target="_blank" id="playinglink"></a></div>
|
||||
</div>
|
||||
<div id="chatbox">
|
||||
<div id="phase1">
|
||||
<p style="color:#e5e0e5">Please enter your name to Join the chat</P>
|
||||
<input id="name">
|
||||
<button id="join">Join</button>
|
||||
</div>
|
||||
<div id="error"></div>
|
||||
<div id="phase2" style="opacity:0">
|
||||
<div id="messages"></div>
|
||||
<textarea id="msg"></textarea>
|
||||
<br/><button id="send">Send</button>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
$("INPUT").val("")
|
||||
$("#name").keypress(function(evt){
|
||||
if(evt.originalEvent.keyCode==13){
|
||||
$("#join").trigger("click")
|
||||
//submit name
|
||||
}
|
||||
})
|
||||
|
||||
//handling the start of the chat
|
||||
$("#join").click(function(){
|
||||
$("#error").html("");
|
||||
var name= escapeHtml($("#name").val())
|
||||
if(name.length<3){
|
||||
$("#error").html("Name is too short!");
|
||||
return
|
||||
}
|
||||
console.log("join started")
|
||||
chat = new WebSocket("ws://"+window.location.host+":8089/ws");
|
||||
chat.onopen = function(evt) {
|
||||
chat.send(name); //sending the chat name
|
||||
$("#phase1").animate({opacity:0},500,"linear",function(){
|
||||
$("#phase1").css({display:"none"})
|
||||
$("#phase2").css({opacity:1})
|
||||
$("#msg").focus()
|
||||
})
|
||||
};
|
||||
chat.onerror = function(evt) {
|
||||
console.log("Websocket Error:",evt)
|
||||
};
|
||||
chat.onclose = function(evt) {
|
||||
console.log("chat closing")
|
||||
$("#phase1").stop().css({display:"block"}).animate({opacity:1},500)
|
||||
$("#phase2").stop().animate({opacity:0})
|
||||
$("#error").html("That name was already used!")
|
||||
};
|
||||
chat.onmessage = function(evt) {
|
||||
$("#messages").append(evt.data).scrollTop(9e6)
|
||||
};
|
||||
|
||||
})
|
||||
|
||||
$("#msg").keypress(function(evt){
|
||||
if(evt.originalEvent.keyCode==13 && !evt.originalEvent.shiftKey){
|
||||
$("#send").trigger("click")
|
||||
evt.preventDefault();
|
||||
// submit name
|
||||
}
|
||||
})
|
||||
|
||||
$("#send").click(function(){
|
||||
chat.send(escapeHtml($("#msg").val()));
|
||||
$("#msg").val("");
|
||||
})
|
||||
|
||||
//helper function for escaping HTML
|
||||
var entityMap = {
|
||||
"&": "&",
|
||||
"<": "<",
|
||||
">": ">",
|
||||
'"': '"',
|
||||
"'": ''',
|
||||
"/": '/',
|
||||
"\n": '<BR/>'
|
||||
};
|
||||
|
||||
</script>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
4
static/jquery.js
vendored
Normal file
4
static/jquery.js
vendored
Normal file
File diff suppressed because one or more lines are too long
169
static/justchat.html
Normal file
169
static/justchat.html
Normal file
@ -0,0 +1,169 @@
|
||||
<!doctype html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset='utf-8'>
|
||||
<title>Movie Night! - Chat</title>
|
||||
<style>
|
||||
body {
|
||||
margin:0;
|
||||
padding:0;
|
||||
background:#000;
|
||||
}
|
||||
|
||||
html, body, #messages, #phase2 {
|
||||
height:100%;
|
||||
}
|
||||
|
||||
video {
|
||||
width:100%;
|
||||
}
|
||||
#chatbox {
|
||||
width: 100%;
|
||||
height:85%;
|
||||
}
|
||||
#messages {
|
||||
border: 1px solid #666;
|
||||
width: 95%;
|
||||
overflow: auto;
|
||||
color: #f4f4f4;
|
||||
}
|
||||
#msg {
|
||||
width: 94%;
|
||||
height: 3em;
|
||||
}
|
||||
#error {
|
||||
color: #f00;
|
||||
padding: 5px;
|
||||
font-weight: bold;
|
||||
}
|
||||
span.name {
|
||||
font-weight:bold;
|
||||
}
|
||||
span.cmdme {
|
||||
font-style: italic;
|
||||
}
|
||||
span.msg {
|
||||
font-style: normal;
|
||||
color: #cfccd1;
|
||||
}
|
||||
span.svmsg {
|
||||
font-style: italic;
|
||||
color: #ea6260;
|
||||
}
|
||||
|
||||
.announcement {
|
||||
font-weight: bold;
|
||||
color: #ea6260;
|
||||
text-align: center;
|
||||
width: 100%;
|
||||
margin-top: 10px;
|
||||
margin-bottom: 10px;
|
||||
border-top: 3px solid red;
|
||||
border-bottom: 3px solid red;
|
||||
}
|
||||
</style>
|
||||
<script src="/static/jquery.js"></script>
|
||||
<script>
|
||||
function escapeHtml(string) {
|
||||
return string
|
||||
//return String(string).replace(/[&<>"'\/]/g, function (s) {
|
||||
// return entityMap[s];
|
||||
//});
|
||||
}
|
||||
|
||||
//helper function for debugging purposes
|
||||
function toHex(str) {
|
||||
var result = '';
|
||||
for (var i=0; i<str.length; i++) {
|
||||
result += ("0"+str.charCodeAt(i).toString(16)).slice(-2)+" ";
|
||||
}
|
||||
return result.toUpperCase();
|
||||
}
|
||||
</script>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div id="chatbox">
|
||||
<div id="phase1">
|
||||
<p style="color:#e5e0e5">Please enter your name to Join the chat</P>
|
||||
<input id="name">
|
||||
<button id="join">Join</button>
|
||||
</div>
|
||||
<div id="error"></div>
|
||||
<div id="phase2" style="opacity:0">
|
||||
<div id="messages"></div>
|
||||
<textarea id="msg"></textarea>
|
||||
<br/><button id="send">Send</button>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
$("INPUT").val("")
|
||||
$("#name").keypress(function(evt){
|
||||
if(evt.originalEvent.keyCode==13){
|
||||
$("#join").trigger("click")
|
||||
//submit name
|
||||
}
|
||||
})
|
||||
|
||||
//handling the start of the chat
|
||||
$("#join").click(function(){
|
||||
$("#error").html("");
|
||||
var name= escapeHtml($("#name").val())
|
||||
if(name.length<3){
|
||||
$("#error").html("Name is too short!");
|
||||
return
|
||||
}
|
||||
console.log("join started")
|
||||
chat = new WebSocket("ws://"+window.location.host+":8089/ws");
|
||||
chat.onopen = function(evt) {
|
||||
chat.send(name); //sending the chat name
|
||||
$("#phase1").animate({opacity:0},500,"linear",function(){
|
||||
$("#phase1").css({display:"none"})
|
||||
$("#phase2").css({opacity:1})
|
||||
$("#msg").focus()
|
||||
})
|
||||
};
|
||||
chat.onerror = function(evt) {
|
||||
console.log("Websocket Error:",evt)
|
||||
};
|
||||
chat.onclose = function(evt) {
|
||||
console.log("chat closing")
|
||||
$("#phase1").stop().css({display:"block"}).animate({opacity:1},500)
|
||||
$("#phase2").stop().animate({opacity:0})
|
||||
$("#error").html("That name was already used!")
|
||||
};
|
||||
chat.onmessage = function(evt) {
|
||||
$("#messages").append(evt.data).scrollTop(9e6)
|
||||
};
|
||||
|
||||
})
|
||||
|
||||
$("#msg").keypress(function(evt){
|
||||
if(evt.originalEvent.keyCode==13 && !evt.originalEvent.shiftKey){
|
||||
$("#send").trigger("click")
|
||||
evt.preventDefault();
|
||||
// submit name
|
||||
}
|
||||
})
|
||||
|
||||
$("#send").click(function(){
|
||||
chat.send(escapeHtml($("#msg").val()));
|
||||
$("#msg").val("");
|
||||
})
|
||||
|
||||
//helper function for escaping HTML
|
||||
var entityMap = {
|
||||
"&": "&",
|
||||
"<": "<",
|
||||
">": ">",
|
||||
'"': '"',
|
||||
"'": ''',
|
||||
"/": '/',
|
||||
"\n": '<BR/>'
|
||||
};
|
||||
|
||||
</script>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
|
48
static/justvideo.html
Normal file
48
static/justvideo.html
Normal file
@ -0,0 +1,48 @@
|
||||
<!doctype html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset='utf-8'>
|
||||
<title>Movie Night!</title>
|
||||
<style>
|
||||
body {
|
||||
margin:0;
|
||||
padding:0;
|
||||
background:#000;
|
||||
}
|
||||
|
||||
html, body{
|
||||
height:100%;
|
||||
}
|
||||
|
||||
video {
|
||||
width:100%;
|
||||
height:100%;
|
||||
}
|
||||
</style>
|
||||
<script src="/static/jquery.js"></script>
|
||||
<script src="/static/flv.min.js"></script>
|
||||
<script>
|
||||
|
||||
function initPlayer() {
|
||||
if (flvjs.isSupported()) {
|
||||
var videoElement = document.getElementById('videoElement');
|
||||
var flvPlayer = flvjs.createPlayer({
|
||||
type: 'flv',
|
||||
url: '/live'
|
||||
});
|
||||
flvPlayer.attachMediaElement(videoElement);
|
||||
flvPlayer.load();
|
||||
flvPlayer.play();
|
||||
}
|
||||
}
|
||||
|
||||
</script>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<video id="videoElement" controls autoplay x5-video-player-type="h5" x5-video-player-fullscreen="true" playsinline webkit-playsinline>
|
||||
Your browser is too old and doesn't support HTML5 video.
|
||||
</video>
|
||||
<script>initPlayer();</script>
|
||||
</body>
|
||||
</html>
|
15
static/ractive.min.js
vendored
Normal file
15
static/ractive.min.js
vendored
Normal file
File diff suppressed because one or more lines are too long
Loading…
Reference in New Issue
Block a user