166 lines
5.2 KiB
Go
166 lines
5.2 KiB
Go
/*
|
|
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 <p> 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 */
|
|
}
|
|
}
|