diff --git a/src/owncast.go b/src/owncast.go new file mode 100644 index 0000000..9d8b613 --- /dev/null +++ b/src/owncast.go @@ -0,0 +1,151 @@ +/* + 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 +} + +/* 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"] == "UserMessage" { + 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 - TODO determine the correct format */ + sent_dt, _ := time.Parse("2006-01-02T15:04:05.000000-07:00", 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: "", + } + + /* check for authenticated role */ + if _, ok := author["authenticated"]; ok && author["authenticated"].(bool) == true { + out_message.Type += "-verified" + } + + /* check for bot role */ + if _, ok := author["isBot"]; ok && author["isBot"].(bool) == true { + out_message.Type += "-bot" + } + + /* add display color hint */ + out_message.Type += fmt.Sprintf("-occolor%d", int(author["displayColor"].(float64))) + + /* 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 */ + } +}