/* StreamGoose main server/UI launcher — Go reference implementation Created by Luxferre in 2024, released into public domain */ package main import ( "os" "fmt" "log" "time" "strings" "slices" "encoding/json" "net/http" "io/ioutil" ) /* 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 */ var sse_transports []chan string /* SSE output channels */ /* headless behaviour by default */ var goose_start_gui = func(config map[string]interface{}) { /* target server listening address */ const fmt_str = "[headless] Connect to http://%s:%d in any browser to see the chat window" log.Printf(fmt_str, config["server_listen_ip"].(string), int(config["server_port"].(float64))) /* just do nothing except sleep in the main thread */ for { time.Sleep(time.Duration(3600) * time.Second) } } /* 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 /* 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) } /* broadcast the message across the clients */ for _, transport := range sse_transports { if transport != nil { transport <- string(json_msg) } } } } /* 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 */ } /* 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) idx := slices.IndexFunc(sse_transports, func(c chan string) bool { return c == transport }) sse_transports[idx] = nil 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 } } } /* 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) /* serve the GET /sse */ http.HandleFunc("/sse", sseHandler) /* 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 */ } if _, ok := config["oc_backend_enabled"]; ok && config["oc_backend_enabled"].(bool) == true { go owncast_run(config) /* start the Owncast backend */ } if _, ok := config["kk_backend_enabled"]; ok && config["kk_backend_enabled"].(bool) == true { go kick_run(config) /* start the Kick backend */ } /* start the GUI from gui.go file */ goose_start_gui(config) }