2024-09-14 14:41:59 +03:00
|
|
|
/*
|
|
|
|
|
StreamGoose main server/UI launcher — Go reference implementation
|
|
|
|
|
|
|
|
|
|
Created by Luxferre in 2024, released into public domain
|
|
|
|
|
*/
|
|
|
|
|
package main
|
|
|
|
|
|
|
|
|
|
import (
|
|
|
|
|
"os"
|
|
|
|
|
"fmt"
|
|
|
|
|
"log"
|
|
|
|
|
"strings"
|
2024-09-15 14:58:30 +03:00
|
|
|
"slices"
|
2024-09-14 14:41:59 +03:00
|
|
|
"encoding/json"
|
|
|
|
|
"net/http"
|
|
|
|
|
"io/ioutil"
|
|
|
|
|
/* external */
|
|
|
|
|
"centrifuge.hectabit.org/HectaBit/webview_go" /* for UI launch */
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
/* main chat message structure */
|
|
|
|
|
type ChatMessage struct {
|
|
|
|
|
Id uint64 `json:"id"` /* message id (auto-filled) */
|
|
|
|
|
Type string `json:"type"` /* message type string */
|
|
|
|
|
Timestamp string `json:"timestamp"` /* timestamp string (formatted) */
|
|
|
|
|
Username string `json:"username"` /* username */
|
|
|
|
|
Text string `json:"text"` /* message text */
|
|
|
|
|
Icon string `json:"icon"` /* optional message icon URI */
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
var global_msg_id uint64 = 0 /* track global message ID */
|
|
|
|
|
var message_transport chan ChatMessage /* global message channel */
|
2024-09-14 20:39:38 +03:00
|
|
|
var sse_transports []chan string /* SSE output channels */
|
2024-09-14 14:41:59 +03:00
|
|
|
var bro webview.WebView /* global browser instance handle */
|
|
|
|
|
|
|
|
|
|
/* escape HTML entities in JS parameters */
|
|
|
|
|
func esc(s string) string {
|
|
|
|
|
s = strings.ReplaceAll(s, "&", "&")
|
|
|
|
|
s = strings.ReplaceAll(s, "'", "'")
|
|
|
|
|
s = strings.ReplaceAll(s, "\"", """)
|
|
|
|
|
s = strings.ReplaceAll(s, "<", "<")
|
|
|
|
|
s = strings.ReplaceAll(s, ">", ">")
|
|
|
|
|
return s
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/* chat message appender thread */
|
|
|
|
|
func appendChatMessages() {
|
|
|
|
|
var msg ChatMessage
|
|
|
|
|
for {
|
|
|
|
|
msg = <-message_transport /* receive it from the channel */
|
|
|
|
|
/* populate the running id */
|
|
|
|
|
global_msg_id += 1
|
|
|
|
|
msg.Id = global_msg_id
|
|
|
|
|
|
2024-09-14 20:39:38 +03:00
|
|
|
/* TODO any additional message logging should be done at this point! */
|
|
|
|
|
|
|
|
|
|
/* serialize the message and send it to the SSE channel */
|
|
|
|
|
json_msg, err := json.Marshal(msg)
|
|
|
|
|
if err != nil {
|
|
|
|
|
log.Println("Error serializing the message:", err)
|
|
|
|
|
}
|
2024-09-14 14:41:59 +03:00
|
|
|
|
2024-09-14 20:39:38 +03:00
|
|
|
/* broadcast the message across the clients */
|
|
|
|
|
for _, transport := range sse_transports {
|
|
|
|
|
if transport != nil {
|
|
|
|
|
transport <- string(json_msg)
|
|
|
|
|
}
|
|
|
|
|
}
|
2024-09-14 14:41:59 +03:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/* POST /new handler (for external backends) */
|
|
|
|
|
func msgAddHandler(w http.ResponseWriter, r *http.Request) {
|
|
|
|
|
decoder := json.NewDecoder(r.Body)
|
|
|
|
|
var msg ChatMessage /* create a new message */
|
|
|
|
|
err := decoder.Decode(&msg) /* fill in the message structure */
|
|
|
|
|
if err != nil {
|
|
|
|
|
log.Println("Error decoding POST request:", err)
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
/* post it to the transport channel */
|
|
|
|
|
message_transport <- msg
|
|
|
|
|
w.Write([]byte("{\"status\": 1}")) /* respond with a status */
|
|
|
|
|
}
|
|
|
|
|
|
2024-09-14 20:39:38 +03:00
|
|
|
/* GET /sse handler */
|
|
|
|
|
func sseHandler(w http.ResponseWriter, r *http.Request) {
|
|
|
|
|
/* prepare the headers */
|
|
|
|
|
w.Header().Set("Content-Type", "text/event-stream")
|
|
|
|
|
w.Header().Set("Cache-Control", "no-cache")
|
|
|
|
|
w.Header().Set("Connection", "keep-alive")
|
|
|
|
|
w.Header().Set("Access-Control-Allow-Origin", "*")
|
|
|
|
|
|
|
|
|
|
/* create the channel */
|
|
|
|
|
transport := make(chan string)
|
|
|
|
|
/* append it to the broadcast list */
|
|
|
|
|
sse_transports = append(sse_transports, transport)
|
|
|
|
|
|
|
|
|
|
/* close the channel after exiting the function */
|
|
|
|
|
defer func() {
|
|
|
|
|
close(transport)
|
2024-09-15 14:58:30 +03:00
|
|
|
idx := slices.IndexFunc(sse_transports, func(c chan string) bool { return c == transport })
|
|
|
|
|
sse_transports[idx] = nil
|
2024-09-14 20:39:38 +03:00
|
|
|
transport = nil
|
|
|
|
|
}()
|
|
|
|
|
|
|
|
|
|
/* use HTTP response flusher */
|
|
|
|
|
flusher, _ := w.(http.Flusher)
|
|
|
|
|
|
|
|
|
|
/* message loop */
|
|
|
|
|
for {
|
|
|
|
|
select {
|
|
|
|
|
case message := <-transport:
|
|
|
|
|
fmt.Fprintf(w, "data: %s\n\n", message)
|
|
|
|
|
flusher.Flush()
|
|
|
|
|
case <-r.Context().Done(): /* handle disconnects */
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2024-09-14 14:41:59 +03:00
|
|
|
/* entry point */
|
|
|
|
|
func main() {
|
|
|
|
|
config_path := "config.json"
|
|
|
|
|
if len(os.Args) > 1 {
|
|
|
|
|
config_path = os.Args[1]
|
|
|
|
|
}
|
|
|
|
|
content, err := ioutil.ReadFile(config_path)
|
|
|
|
|
if err != nil {
|
|
|
|
|
log.Fatal("Error when opening config file: ", err)
|
|
|
|
|
}
|
|
|
|
|
var config map[string]interface{}
|
|
|
|
|
err = json.Unmarshal(content, &config)
|
|
|
|
|
if err != nil {
|
|
|
|
|
log.Fatal("Error during Unmarshal(): ", err)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/* create the message transport channel */
|
|
|
|
|
message_transport = make(chan ChatMessage)
|
|
|
|
|
|
|
|
|
|
/* target server listening address */
|
|
|
|
|
target_addr := fmt.Sprintf("%s:%d", config["server_listen_ip"].(string), int(config["server_port"].(float64)))
|
|
|
|
|
|
|
|
|
|
go appendChatMessages() /* start the appender thread */
|
|
|
|
|
|
|
|
|
|
go func() { /* start the server thread */
|
|
|
|
|
/* serve the POST /new */
|
|
|
|
|
http.HandleFunc("/new", msgAddHandler)
|
2024-09-14 20:39:38 +03:00
|
|
|
/* serve the GET /sse */
|
|
|
|
|
http.HandleFunc("/sse", sseHandler)
|
2024-09-14 14:41:59 +03:00
|
|
|
/* serve the webroot directory */
|
|
|
|
|
fs := http.FileServer(http.Dir("./webroot"))
|
|
|
|
|
http.Handle("/", fs)
|
|
|
|
|
log.Println(fmt.Sprintf("Listening on %s\n", target_addr))
|
|
|
|
|
err = http.ListenAndServe(target_addr, nil)
|
|
|
|
|
if err != nil {
|
|
|
|
|
log.Fatal(err)
|
|
|
|
|
}
|
|
|
|
|
}()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
if _, ok := config["tw_backend_enabled"]; ok && config["tw_backend_enabled"].(bool) == true {
|
|
|
|
|
go twitch_run(config) /* start the Twitch backend */
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if _, ok := config["yt_backend_enabled"]; ok && config["yt_backend_enabled"].(bool) == true {
|
|
|
|
|
go youtube_run(config) /* start the YouTube backend */
|
|
|
|
|
}
|
|
|
|
|
|
2024-09-14 23:42:47 +03:00
|
|
|
if _, ok := config["oc_backend_enabled"]; ok && config["oc_backend_enabled"].(bool) == true {
|
|
|
|
|
go owncast_run(config) /* start the Owncast backend */
|
|
|
|
|
}
|
|
|
|
|
|
2024-09-15 14:40:39 +03:00
|
|
|
if _, ok := config["kk_backend_enabled"]; ok && config["kk_backend_enabled"].(bool) == true {
|
|
|
|
|
go kick_run(config) /* start the Kick backend */
|
|
|
|
|
}
|
|
|
|
|
|
2024-09-14 14:41:59 +03:00
|
|
|
/* start the GUI */
|
|
|
|
|
debug := false
|
|
|
|
|
if _, ok := config["debug"]; ok && config["debug"].(bool) == true {
|
|
|
|
|
debug = true
|
|
|
|
|
}
|
|
|
|
|
bro = webview.New(debug) /* create a browser instance */
|
|
|
|
|
bro.SetSize(320, 720, webview.HintNone)
|
|
|
|
|
defer bro.Destroy()
|
|
|
|
|
bro.SetTitle("StreamGoose v0.0.1")
|
|
|
|
|
bro.Navigate("http://" + target_addr)
|
|
|
|
|
bro.Run()
|
|
|
|
|
|
|
|
|
|
}
|