/* StreamGoose Owncast backend — Go reference implementation Created by Luxferre in 2024, released into public domain */ package main import ( "fmt" "log" "strings" "slices" "time" "encoding/json" "net/http" "io/ioutil" ) /* global API root URL placeholder */ var OC_API_ROOT string /* global API token placeholder */ var OC_API_TOKEN string /* global debug flag */ var OC_DEBUG bool const OC_ID_CACHE_LIMIT = 1000 /* global message ID cache */ var oc_msg_id_cache []string /* API request helper function */ func oc_api_get(endpoint string) []interface{} { url := OC_API_ROOT + endpoint /* get full URL */ req, err := http.NewRequest("GET", url, nil) req.Header.Set("Accept", "application/json; charset=utf-8") req.Header.Set("Authorization", "Bearer " + OC_API_TOKEN) client := &http.Client{} resp, err := client.Do(req) if err != nil { log.Fatal(err) } body, _ := ioutil.ReadAll(resp.Body) resp.Body.Close() /* print the response if the debug flag is set */ if OC_DEBUG { log.Printf("[owncast] API response: %s", string(body)) } /* unmarshal the response */ var respbody []interface{} err = json.Unmarshal(body, &respbody) if err != nil { log.Fatal(err) } return respbody } /* [{"timestamp":"2024-09-15T06:37:36.022049086Z","type":"CHAT","id":"PrBY39eSg","user":{"createdAt":"2024-09-14T11:51:52.135597747Z","id":"SmcN3l6Ig","displayName":"inspiring-joliot","previousNames":["inspiring-joliot"],"scopes":[""],"displayColor":0,"isBot":false,"authenticated":false},"body":"\u003cp\u003etest\u003c/p\u003e"}, {"timestamp":"2024-09-15T06:38:20.410448447Z","type":"CHAT","id":"lIgyqr6Sg","user":{"createdAt":"2024-09-14T11:51:52.135597747Z","id":"SmcN3l6Ig","displayName":"inspiring-joliot","previousNames":["inspiring-joliot"],"scopes":[""],"displayColor":0,"isBot":false,"authenticated":false},"body":"\u003cp\u003exlol\u003c/p\u003e"}] */ /* main message procesing function */ func oc_process_message(rawmsg map[string]interface{}) { /* if the message came from a real user, this is the one we should process */ if rawmsg["type"] == "CHAT" { if !slices.Contains(oc_msg_id_cache, rawmsg["id"].(string)) { /* we have a fresh message here */ author := rawmsg["user"].(map[string]interface{}) /* convert the timestamp */ sent_dt, _ := time.Parse("2006-01-02T15:04:05.000000000Z", rawmsg["timestamp"].(string)) /* prepare output message */ out_message := ChatMessage { Type: "oc", Username: author["displayName"].(string), Timestamp: sent_dt.UTC().Format("2006-01-02T15:04:05Z"), Text: rawmsg["body"].(string), Icon: "", } /* remove starting and ending
tag from the message */ out_message.Text = strings.ReplaceAll(out_message.Text, "\u003cp\u003e", "") out_message.Text = strings.ReplaceAll(out_message.Text, "\u003c/p\u003e", "") /* check for authenticated role */ if _, ok := author["authenticated"]; ok && author["authenticated"].(bool) == true { out_message.Type += "-authenticated" } /* check for bot role */ if _, ok := author["isBot"]; ok && author["isBot"].(bool) == true { out_message.Type += "-bot" } /* add display color hint */ dcolor := int(author["displayColor"].(float64)) if dcolor > 0 { out_message.Type += fmt.Sprintf("-occolor%d", dcolor) } /* finally, send it to the message broker via the message_transport channel */ message_transport <- out_message /* update the ID cache */ oc_msg_id_cache = append(oc_msg_id_cache, rawmsg["id"].(string)) cachelen := len(oc_msg_id_cache) if cachelen > OC_ID_CACHE_LIMIT { /* trim the excess */ oc_msg_id_cache = oc_msg_id_cache[cachelen-OC_ID_CACHE_LIMIT:cachelen] } } } } /* entry point */ func owncast_run(config map[string]interface{}) { token_path := "auth/oc-token.txt" token, err := ioutil.ReadFile(token_path) token_err_msg := "No Owncast token file provided or it is empty. " token_err_msg += "Please generate a token in your Owncast admin panel " token_err_msg += "and paste it into the auth/oc-token.txt file!" if err != nil { log.Fatal(token_err_msg + "\nDetailed error: ", err) } OC_API_TOKEN = strings.TrimSpace(string(token)) if len(OC_API_TOKEN) < 1 { log.Fatal(token_err_msg) } API_POLL_TIMEOUT := 1 /* polling timeout in seconds */ if _, ok := config["debug"]; ok { OC_DEBUG = config["debug"].(bool) } if _, ok := config["oc_server"]; ok { OC_API_ROOT = config["oc_server"].(string) } else { log.Fatal("Owncast server missing in config!") } if _, ok := config["oc_api_poll_timeout"]; ok { API_POLL_TIMEOUT = int(config["oc_api_poll_timeout"].(float64)) } log.Println("StreamGoose Owncast Go backend started") var raw_chat_data []interface{} oc_msg_id_cache = append(oc_msg_id_cache, "") /* init the cache */ for { /* main loop */ raw_chat_data = oc_api_get("/api/integrations/chat") for _, rawmsg := range raw_chat_data { /* iterate over raw message objects */ oc_process_message(rawmsg.(map[string]interface{})) } time.Sleep(time.Duration(API_POLL_TIMEOUT) * time.Second) /* wait for specified amount before polling next */ } }