From 430d67913b8582a0760c1244a19c64454ac1ffed Mon Sep 17 00:00:00 2001 From: Luxferre Date: Sun, 15 Sep 2024 14:40:39 +0300 Subject: [PATCH] started Kick support --- README.md | 32 +++++-- config.json | 8 +- src/Makefile | 2 +- src/go.mod | 12 ++- src/go.sum | 27 ++++++ src/kick.go | 200 +++++++++++++++++++++++++++++++++++++++++++ src/owncast.go | 4 +- src/server.go | 4 + src/twitch.go | 6 +- src/youtube.go | 10 +-- webroot/img/kk32.png | Bin 0 -> 884 bytes webroot/script.js | 12 ++- webroot/style.css | 12 +++ 13 files changed, 305 insertions(+), 24 deletions(-) create mode 100644 src/kick.go create mode 100644 webroot/img/kk32.png diff --git a/README.md b/README.md index 1142ac3..3996c4c 100644 --- a/README.md +++ b/README.md @@ -11,9 +11,10 @@ The status of StreamGoose is currently **alpha**. Use at your own risk. Nevertheless, the following features are already implemented: -- Twitch, YouTube and Owncast chat aggregation (see "Backend implementation status" section) +- Twitch, YouTube, Owncast and Kick chat aggregation (see "Backend implementation status" section) - Handling Twitch emotes - Handling YouTube non-Unicode emotes (see "Backend implementation status" section) +- Handling Kick emotes - Message styling based on Twitch and YouTube roles - Twitch username styling based on color preference - Message highlighting based on regular expressions @@ -132,6 +133,7 @@ The following fields in the `config.json` are backend-agnostic: - `server_port`: a local port to run the HTTP REST server on. Default value: `60053` (for "GOOSE"). - `server_listen_ip`: a local IP address to bind the server to. Default value: `127.0.0.1`. Recommended to leave at this value, only change it if you plan on connecting external backends from other machines in your LAN. - `debug`: print out various debug information about incoming messages and events. Can be `true` or `false`. +- `useragent`: the value of the `User-Agent` HTTP header used by the backends with unofficial APIs, like Kick. The following fields in the `config.json` are specific to the Twitch internal backend: @@ -153,6 +155,12 @@ The following fields in the `config.json` are specific to the Owncast internal b - `oc_server`: your Owncast server root URL. - `oc_api_poll_timeout`: the interval (in seconds) to poll the Owncast chat with. Recommended to set to at least 2 seconds. +The following fields in the `config.json` are specific to the Kick internal backend: + +- `kk_backend_enabled`: whether or not the Owncast internal backend is turned on. +- `kk_channel`: your Kick channel name. +- `kk_api_poll_timeout`: the interval (in seconds) to poll the Kick chat with. Recommended to set to at least 2 seconds. + Example `config.json` file: ```json @@ -160,6 +168,7 @@ Example `config.json` file: "server_port": 60053, "server_listen_ip": "127.0.0.1", "debug": true, + "useragent": "Mozilla/5.0 (X11; Linux x86_64; rv:130.0) Gecko/20100101 Firefox/130.0", "tw_backend_enabled": true, "tw_nickname": "Suborg", "tw_channel": "#foxyshadow", @@ -169,7 +178,10 @@ Example `config.json` file: "yt_channel": "@foxyshadow", "oc_backend_enabled": true, "oc_server": "https://streams.luxferre.top", - "oc_api_poll_timeout": 2 + "oc_api_poll_timeout": 2, + "kk_backend_enabled": true, + "kk_channel": "foxyshadow", + "kk_api_poll_timeout": 2 } ``` @@ -197,6 +209,9 @@ The following procedure needs to be only done once: 4. Save the file. The Owncast connection should now work and fetch messages whenever you start a livestream. 5. If the connection doesn't work, make sure that the `oc_backend_enabled` field is set to `true` in `config.json` and you didn't revoke the token in the admin panel. +### Connecting Kick internal backend + +As of now, nothing needs to be done except setting the `kk_backend_enabled` field to `true` in `config.json`, but this section might be subject to change once Kick deploys and enforces any official APIs. ## Customization @@ -213,6 +228,7 @@ The `webroot/style.css` is the main point of customization. Here is a short (and - `.timestamp`: message timestamp (hidden by default) - `.tw`: a Twitch message (applies to the whole message block, as well as **all the subsequent classes** listed here) - `.yt`: a YouTube message +- `.kk`: a Kick message - `.oc`: an Owncast message - `.me`: third-party action - `.broadcaster`: a message that comes from the Twitch/YouTube channel owner @@ -265,7 +281,11 @@ Additionally, YouTube does not have its own chat emote API as of now, so the emo ### Owncast (internal) -Work in progress. Need additional testing. Basic messages work as expected. +Work in progress. Needs additional testing. Basic messages work as expected. + +### Kick (internal) + +Work in progress. Relies on an API that hasn't been officially released yet. Needs additional testing. Basic messages work as expected. ## FAQ @@ -291,11 +311,11 @@ Everyone is free to send their suggestions and patches. Once you get a Codeberg Yes, due to the way Go links its networking client and server code and everything else statically (where possible). On Linux, you may try using GCC Go instead to achieve fully dynamic linking. Keep in mind that GCC Go is not fully supported yet, and if you come to our issue tracker with an issue that's only reproducible with GCC Go, it's less likely to be resolved. -### Beyond Twitch, YouTube and Owncast, are there any other livestreaming platforms that you plan to support internally? +### Beyond Twitch, YouTube, Owncast and Kick, are there any other livestreaming platforms that you plan to support internally? -Sure, anything can happen. The problem with adding a new platform support, as always, is finding enough time to study the API and enough people that would find this support valuable. For example, with enough demand, Kick and Trovo support are also likely to be added. +Sure, anything can happen. The problem with adding a new platform support, as always, is finding enough time to study the API and enough people that would find this support valuable. For example, with enough demand, Trovo support is also likely to be added, however, it would require significantly more time and effort that could be leveraged for improving e.g. YouTube backend that would have significantly larger impact. -Besides Twitch, YouTube and Owncast, however, your best bet would be to create an **external** backend for the platform you're interested in, and then incorporate it into this repo in the `/external` directory. When the backend is mature enough, we can always sit and think about how to convert it into an internal one to be distributed inside the StreamGoose binary. +Besides the main four, anyway, your best bet would be to create an **external** backend for the platform you're interested in (again, you could use any programming language you want), and then incorporate it into this repo in the `/external` directory. When the backend is mature enough, we can always sit and think about how to convert it into an internal one to be distributed inside the StreamGoose binary. ## Credits diff --git a/config.json b/config.json index a9d9d65..ff7e357 100644 --- a/config.json +++ b/config.json @@ -2,6 +2,7 @@ "server_port": 60053, "server_listen_ip": "127.0.0.1", "debug": true, + "useragent": "Mozilla/5.0 (X11; Linux x86_64; rv:130.0) Gecko/20100101 Firefox/130.0", "tw_backend_enabled": true, "tw_nickname": "Suborg", "tw_channel": "#suborg", @@ -9,7 +10,10 @@ "yt_api_root_url": "https://yt.lemnoslife.com/noKey", "yt_api_poll_timeout": 3, "yt_channel": "@foxyshadow", - "oc_backend_enabled": true, + "oc_backend_enabled": false, "oc_server": "https://streams.luxferre.top", - "oc_api_poll_timeout": 2 + "oc_api_poll_timeout": 2, + "kk_backend_enabled": true, + "kk_channel": "sam", + "kk_api_poll_timeout": 2 } diff --git a/src/Makefile b/src/Makefile index 7a18819..067f259 100644 --- a/src/Makefile +++ b/src/Makefile @@ -1,7 +1,7 @@ LDFLAGS = -s -w LDFLAGS_WIN = -H windowsgui -s -w GOFLAGS = -trimpath -SOURCES = server.go twitch.go youtube.go owncast.go +SOURCES = server.go twitch.go youtube.go owncast.go kick.go BIN = streamgoose unix: diff --git a/src/go.mod b/src/go.mod index 25609be..26017cb 100644 --- a/src/go.mod +++ b/src/go.mod @@ -2,4 +2,14 @@ module streamgoose go 1.23.1 -require centrifuge.hectabit.org/HectaBit/webview_go v0.0.0-20240502074857-17ae24882d16 +require ( + centrifuge.hectabit.org/HectaBit/webview_go v0.0.0-20240502074857-17ae24882d16 + github.com/DaRealFreak/cloudflare-bp-go v1.0.4 +) + +require ( + github.com/EDDYCJY/fake-useragent v0.2.0 // indirect + github.com/PuerkitoBio/goquery v1.7.1 // indirect + github.com/andybalholm/cascadia v1.3.1 // indirect + golang.org/x/net v0.0.0-20211015210444-4f30a5c0130f // indirect +) diff --git a/src/go.sum b/src/go.sum index d892078..ec892db 100644 --- a/src/go.sum +++ b/src/go.sum @@ -1,2 +1,29 @@ centrifuge.hectabit.org/HectaBit/webview_go v0.0.0-20240502074857-17ae24882d16 h1:gOFnYZ+G9R7Prc3Mn7z/v+LkPf/kQ2oUh5NCQd6n39k= centrifuge.hectabit.org/HectaBit/webview_go v0.0.0-20240502074857-17ae24882d16/go.mod h1:cAr/pT090G1kcy9uuCEUbZyGhUQRGOo4KVnkocx8BMA= +github.com/DaRealFreak/cloudflare-bp-go v1.0.4 h1:33X8Z0YMV1DEVvL/kYLku+rjb4wF712+VIh3xBoifQ0= +github.com/DaRealFreak/cloudflare-bp-go v1.0.4/go.mod h1:oBI9KAKb9FqdoB42uUqHU6pdP+YDWlKjpZRSk8JTuwk= +github.com/EDDYCJY/fake-useragent v0.2.0 h1:Jcnkk2bgXmDpX0z+ELlUErTkoLb/mxFBNd2YdcpvJBs= +github.com/EDDYCJY/fake-useragent v0.2.0/go.mod h1:5wn3zzlDxhKW6NYknushqinPcAqZcAPHy8lLczCdJdc= +github.com/PuerkitoBio/goquery v1.7.1 h1:oE+T06D+1T7LNrn91B4aERsRIeCLJ/oPSa6xB9FPnz4= +github.com/PuerkitoBio/goquery v1.7.1/go.mod h1:XY0pP4kfraEmmV1O7Uf6XyjoslwsneBbgeDjLYuN8xY= +github.com/andybalholm/cascadia v1.2.0/go.mod h1:YCyR8vOZT9aZ1CHEd8ap0gMVm2aFgxBp0T0eFw1RUQY= +github.com/andybalholm/cascadia v1.3.1 h1:nhxRkql1kdYCc8Snf7D5/D3spOX+dBgjA6u8x004T2c= +github.com/andybalholm/cascadia v1.3.1/go.mod h1:R4bJ1UQfqADjvDa4P6HZHLh/3OxWWEqc0Sk8XGwHqvA= +github.com/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/stretchr/testify v1.4.0 h1:2E4SXV/wtOkTonXsotYi4li6zVWxYlZuYNCXe9XRJyk= +github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= +golang.org/x/net v0.0.0-20180218175443-cbe0f9307d01/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20210614182718-04defd469f4e/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= +golang.org/x/net v0.0.0-20210916014120-12bc252f5db8/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= +golang.org/x/net v0.0.0-20211015210444-4f30a5c0130f h1:OfiFi4JbukWwe3lzw+xunroH1mnC1e2Gy5cxNJApiSY= +golang.org/x/net v0.0.0-20211015210444-4f30a5c0130f/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= +golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= +golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +gopkg.in/yaml.v2 v2.2.2 h1:ZCJp+EgiOT7lHqUV2J862kp8Qj64Jo6az82+3Td9dZw= +gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= diff --git a/src/kick.go b/src/kick.go new file mode 100644 index 0000000..3876390 --- /dev/null +++ b/src/kick.go @@ -0,0 +1,200 @@ +/* + StreamGoose Kick backend — Go reference implementation + + Created by Luxferre in 2024, released into public domain +*/ + +package main + +import ( + "fmt" + "log" + "slices" + "strings" + "regexp" + "time" + "encoding/json" + "net/http" + "io/ioutil" + "github.com/DaRealFreak/cloudflare-bp-go" +) + +/* global API root URL */ +const KK_API_ROOT = "https://kick.com/api" + +/* global debug flag */ +var KK_DEBUG bool + +/* global useragent config */ +var KK_USERAGENT string + +/* recent time cache */ +var KK_RECENT_TIME string + +const KK_ID_CACHE_LIMIT = 1000 + +/* global message ID cache */ +var kk_msg_id_cache []string + +/* API request helper function */ +func kk_api_get(endpoint string) map[string]interface{} { + url := KK_API_ROOT + endpoint /* get full URL */ + req, err := http.NewRequest("GET", url, nil) + req.Header.Set("User-Agent", KK_USERAGENT) + req.Header.Set("Alt-Used", "kick.com") + req.Header.Set("Accept", "application/json; charset=utf-8") + req.Header.Set("Accept-Language", "en-US") + req.Header.Set("Connection", "keep-alive") + + client := &http.Client{} + client.Transport = cloudflarebp.AddCloudFlareByPass(client.Transport) + 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 KK_DEBUG { + log.Printf("[kick] API response: %s", string(body)) + } + + /* unmarshal the response */ + + var respbody map[string]interface{} + err = json.Unmarshal(body, &respbody) + if err != nil { + log.Fatal(err) + } + + return respbody +} + +/* emote handling function */ +/* the format is [emote:ID:NAME] */ +/* then the client will substitute this id with https://files.kick.com/emotes/ID/fullsize image */ +func kk_handle_emotes(text string) string { + mbracket := regexp.QuoteMeta("]") + re := regexp.MustCompile(regexp.QuoteMeta("[emote:") + "[^:]+:[^" + mbracket + "]+" + mbracket) + text = re.ReplaceAllStringFunc(text, func(emotestr string) string { + parts := strings.Split(emotestr, ":") + return "#KKEMOTE-" + parts[1] + "#" + }) + return text +} + +/* main message procesing function */ +func kk_process_message(rawmsg map[string]interface{}) { + /* if the message came from a real user, this is the one we should process */ + if rawmsg["type"] == "message" { + if !slices.Contains(kk_msg_id_cache, rawmsg["id"].(string)) { /* we have a fresh message here */ + var author map[string]interface{} + /* check if the "sender" field is present */ + if _, ok := rawmsg["sender"]; ok { + author = rawmsg["sender"].(map[string]interface{}) + } else { + author["username"] = "System" + } + + KK_RECENT_TIME = rawmsg["created_at"].(string) + + /* convert the timestamp */ + sent_dt, _ := time.Parse("2006-01-02T15:04:05Z", KK_RECENT_TIME) + + /* update the recent time variable */ + KK_RECENT_TIME = fmt.Sprintf("%d", sent_dt.Unix()) + + /* prepare output message */ + out_message := ChatMessage { + Type: "kk", + Username: author["username"].(string), + Timestamp: sent_dt.UTC().Format("2006-01-02T15:04:05Z"), + Text: rawmsg["content"].(string), + Icon: "", + } + + /* get user metadata */ + metadata := author["identity"].(map[string]interface{}) + + /* process username color preference */ + if _, ok := metadata["color"]; ok && len(metadata["color"].(string)) > 0 { + out_message.Type += "-color" + metadata["color"].(string) + } + + /* process username badges */ + if _, ok := metadata["badges"]; ok && len(metadata["badges"].([]interface{})) > 0 { + for _, badge := range metadata["badges"].([]interface{}) { + if badge.(map[string]interface{})["active"].(bool) { + out_message.Type += "-" + badge.(map[string]interface{})["text"].(string) + } + } + } + + /* process emotes */ + out_message.Text = kk_handle_emotes(out_message.Text) + + /* finally, send it to the message broker via the message_transport channel */ + message_transport <- out_message + + /* update the ID cache */ + kk_msg_id_cache = append(kk_msg_id_cache, rawmsg["id"].(string)) + cachelen := len(kk_msg_id_cache) + if cachelen > KK_ID_CACHE_LIMIT { /* trim the excess */ + kk_msg_id_cache = kk_msg_id_cache[cachelen-KK_ID_CACHE_LIMIT:cachelen] + } + + } + } +} + +/* entry point */ +func kick_run(config map[string]interface{}) { + if _, ok := config["debug"]; ok { + KK_DEBUG = config["debug"].(bool) + } + + if _, ok := config["useragent"]; ok { + KK_USERAGENT = config["useragent"].(string) + } + + API_POLL_TIMEOUT := 2 + + KK_RECENT_TIME = "" /* init the recent time */ + + if _, ok := config["kk_api_poll_timeout"]; ok { + API_POLL_TIMEOUT = int(config["kk_api_poll_timeout"].(float64)) + } + + /* get chat ID by channel name */ + channel_id_endpoint := fmt.Sprintf("/v2/channels/%s", config["kk_channel"].(string)) + if KK_DEBUG { + log.Printf("[kick] querying %s", channel_id_endpoint) + } + channel_id_data := kk_api_get(channel_id_endpoint) + + channel_id := 0 + if _, ok := channel_id_data["id"]; ok && channel_id_data["id"].(float64) > 0 { + channel_id = int(channel_id_data["id"].(float64)) + } + + if channel_id < 1 { /* the channel ID is still empty */ + log.Fatal("[kick] Unknown error while getting channel ID!") + } + + log.Println("StreamGoose Kick Go backend started") + + var raw_chat_data map[string]interface{} + kk_msg_id_cache = append(kk_msg_id_cache, "") /* init the cache */ + for { /* main loop */ + chat_api_endpoint := fmt.Sprintf("/v2/channels/%d/messages?start_time=%s", channel_id, KK_RECENT_TIME) + raw_chat_data = kk_api_get(chat_api_endpoint) + if raw_chat_data["status"].(map[string]interface{})["error"].(bool) == false { + msg_arr := raw_chat_data["data"].(map[string]interface{})["messages"].([]interface{}) + for _, rawmsg := range msg_arr { /* iterate over raw message objects */ + kk_process_message(rawmsg.(map[string]interface{})) + } + } + time.Sleep(time.Duration(API_POLL_TIMEOUT) * time.Second) /* wait for specified amount before polling next */ + } +} diff --git a/src/owncast.go b/src/owncast.go index ab81910..3ec407c 100644 --- a/src/owncast.go +++ b/src/owncast.go @@ -126,7 +126,7 @@ func oc_process_message(rawmsg map[string]interface{}) { 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 := "[owncast] 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 { @@ -146,7 +146,7 @@ func owncast_run(config map[string]interface{}) { if _, ok := config["oc_server"]; ok { OC_API_ROOT = config["oc_server"].(string) } else { - log.Fatal("Owncast server missing in config!") + log.Fatal("[owncast] Owncast server missing in config!") } if _, ok := config["oc_api_poll_timeout"]; ok { diff --git a/src/server.go b/src/server.go index f657898..eb0932a 100644 --- a/src/server.go +++ b/src/server.go @@ -168,6 +168,10 @@ func main() { 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 */ debug := false if _, ok := config["debug"]; ok && config["debug"].(bool) == true { diff --git a/src/twitch.go b/src/twitch.go index 47c9398..fa98071 100644 --- a/src/twitch.go +++ b/src/twitch.go @@ -55,7 +55,7 @@ func tw_handle_emotes(text string, emote_data string) string { eend_int = textlen - 1 } /* use rune conversion for correct offsets */ - replacements[string([]rune(text)[estart_int:eend_int + 1])] = "#EMOTE-" + emote_id + "#" + replacements[string([]rune(text)[estart_int:eend_int + 1])] = "#TWEMOTE-" + emote_id + "#" } } for from, to := range replacements { /* iterate over replacements */ @@ -144,7 +144,7 @@ func tw_process_message(rawmsg string) { func twitch_run(config map[string]interface{}) { token_path := "auth/twitch-token.txt" token, err := ioutil.ReadFile(token_path) - token_err_msg := "No Twitch token file provided or it is empty. " + token_err_msg := "[twitch] No Twitch token file provided or it is empty. " token_err_msg += "Please visit https://twitchapps.com/tmi/, generate a token there " token_err_msg += "and paste it into the auth/twitch-token.txt file!" if err != nil { @@ -158,7 +158,7 @@ func twitch_run(config map[string]interface{}) { /* Connect to the Twitch IRC server */ conn, err := net.Dial("tcp", "irc.chat.twitch.tv:6667") if err != nil { - log.Fatal("Error connecting to Twitch: ", err) + log.Fatal("[twitch] Error connecting to Twitch: ", err) } tcpwritestr(conn, fmt.Sprintf("PASS %s\n", config["tw_token"])) diff --git a/src/youtube.go b/src/youtube.go index 25998a2..9d36268 100644 --- a/src/youtube.go +++ b/src/youtube.go @@ -114,7 +114,7 @@ func youtube_run(config map[string]interface{}) { if _, ok := config["yt_api_root_url"]; ok { YT_API_ROOT = config["yt_api_root_url"].(string) } else { - log.Fatal("API Root URL missing in config!") + log.Fatal("[youtube] API Root URL missing in config!") } if _, ok := config["yt_api_poll_timeout"]; ok { @@ -131,7 +131,7 @@ func youtube_run(config map[string]interface{}) { if _, ok := channel_id_data["items"]; ok && len(channel_id_data["items"].([]interface{})) > 0 { channel_id = channel_id_data["items"].([]interface{})[0].(map[string]interface{})["id"].(string) } else { - log.Fatal("Error while getting channel ID!") + log.Fatal("[youtube] Error while getting channel ID!") } /* then, get live broadcast video ID by channel ID */ @@ -140,7 +140,7 @@ func youtube_run(config map[string]interface{}) { id_desc := video_id_data["items"].([]interface{})[0].(map[string]interface{})["id"] video_id = id_desc.(map[string]interface{})["videoId"].(string) } else { - log.Fatal("Error while getting video ID!") + log.Fatal("[youtube] Error while getting video ID!") } } @@ -151,11 +151,11 @@ func youtube_run(config map[string]interface{}) { details := chat_id_data["items"].([]interface{})[0].(map[string]interface{})["liveStreamingDetails"] chat_id = details.(map[string]interface{})["activeLiveChatId"].(string) } else { - log.Fatal("Error while getting live chat ID!") + log.Fatal("[youtube] Error while getting live chat ID!") } if len(chat_id) < 1 { /* the chat ID is still empty */ - log.Fatal("Unknown error while getting live chat ID!") + log.Fatal("[youtube] Unknown error while getting live chat ID!") } log.Println("StreamGoose YouTube Go backend started") diff --git a/webroot/img/kk32.png b/webroot/img/kk32.png new file mode 100644 index 0000000000000000000000000000000000000000..314871e14bf2b954e323dc0570ee415043f49f0c GIT binary patch literal 884 zcmV-)1B?8LP)EX>4Tx04R}tkv&MmP!xqvQ>7}E4t5Yx$WWc^q9TGztwIqhgj%6h2a}inL6e50 z#l=x@EjakISaoo5*44pP5CnffoE@ALU8KbOl0u6ZA6(wYdG8$VyAKc=Wu{qOF+kI+ zW-1XEGuc%!@QPl9sAe%DGs~Ehq$E7o*FAiEzl-uL?|Xl)el=$?z$X&Nm|<3lH;AV< zs|M$N;xH@9D)Bk-xJeBXKXP4h`HgeIVS#6c&2(y>I7}=SI#}soRx~x@DdLE#>69;I zTvj=6an{N;*6NeLFqqR%a0@IUxHTPr^~;U)#+K=+Gne~bcwU7%UF?eAmTZk_=CXW&Y2`zsA#=9Bb# zTZ5#ex~(aDz~v4w_+-eY>_~npA)g1{&*+=7z`!lgx8}~Rb&k^qAWgGM-T()O zz*vE@*FE0d(>b?)@3iLk1Izq!%B4_So&W#<24YJ`L;wH)0002_L%V+f000SaNLh0L z0N$to0N$tp1s~Jo00007bV*G`2j~e83IGKjO)xP400D4GL_t(o!_AkwPQx%1hCipJ zt;E1W5s1sg0)oe3;stmq-T)Br5{QWnfsj~OKp+rOS#o8F)y8$=rq0FrCoA&h;2 zux_jqexIbG;xE8gooToXV&G8md5Zzu0UJ#eqOv%x$CJ&Z;RWyntTpX{BJ2YDE*LnM zX?x^Kf&lPkb~p*(Octcaw(39;o`D{40P5YBcT@R3@pT15;+k7B#dPyMq4SokgYN>L zUqymJnLEvZbiY|!zD&zp*%RqIV9;p^I_Lpi!*3Ex31VIIdHe&{H%qC81VniN0000< KMNUMnLSTZ3F@e(n literal 0 HcmV?d00001 diff --git a/webroot/script.js b/webroot/script.js index 4d20cf9..0f0456b 100644 --- a/webroot/script.js +++ b/webroot/script.js @@ -6,12 +6,16 @@ function esc(s) { .replace(/"/g,'"').replace(/'/g,''') } -/* handle Twitch and YouTube emotes */ +/* handle Twitch, YouTube and Kick emotes */ function handleEmotes(text) { - var url = 'https://static-cdn.jtvnw.net/emoticons/v2/$id/default/dark/1.0' + var twurl = 'https://static-cdn.jtvnw.net/emoticons/v2/$id/default/dark/1.0' + var kickurl = 'https://files.kick.com/emotes/$id/fullsize' /* Handle Twitch emotes coming from backends */ - text = text.replace(/#EMOTE-[^\s]+#/g, - a=>'') + text = text.replace(/#TWEMOTE-[^\s]+#/g, + a=>'') + /* Handle Kick emotes coming from backends */ + text = text.replace(/#KKEMOTE-[^\s]+#/g, + a=>'') /* Handle YouTube emotes */ text = text.replace(/:[^\s]+:/g, a => { if(YT_EMOTES.indexOf(a) > -1) /* found in the database */ diff --git a/webroot/style.css b/webroot/style.css index bbb8874..388ab24 100644 --- a/webroot/style.css +++ b/webroot/style.css @@ -93,6 +93,18 @@ html, body { top: 2px; } +.kk:before { + content:""; + display:inline-block; + background:url(/img/kk32.png); + height: 16px; + width: 16px; + background-size: 16px; + background-repeat no-repeat; + position: relative; + top: 2px; +} + .timestamp { display: none !important; margin-right: 5px