started Kick support
This commit is contained in:
@@ -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
|
||||
|
||||
+6
-2
@@ -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
|
||||
}
|
||||
|
||||
+1
-1
@@ -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:
|
||||
|
||||
+11
-1
@@ -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
|
||||
)
|
||||
|
||||
+27
@@ -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=
|
||||
|
||||
+200
@@ -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 */
|
||||
}
|
||||
}
|
||||
+2
-2
@@ -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 {
|
||||
|
||||
@@ -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 {
|
||||
|
||||
+3
-3
@@ -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"]))
|
||||
|
||||
+5
-5
@@ -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")
|
||||
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 884 B |
+8
-4
@@ -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=>'<img src="' + url.replace('$id', a.split('-')[1].slice(0,-1)) + '">')
|
||||
text = text.replace(/#TWEMOTE-[^\s]+#/g,
|
||||
a=>'<img src="' + twurl.replace('$id', a.split('-')[1].slice(0,-1)) + '">')
|
||||
/* Handle Kick emotes coming from backends */
|
||||
text = text.replace(/#KKEMOTE-[^\s]+#/g,
|
||||
a=>'<img src="' + kickurl.replace('$id', a.split('-')[1].slice(0,-1)) + '">')
|
||||
/* Handle YouTube emotes */
|
||||
text = text.replace(/:[^\s]+:/g, a => {
|
||||
if(YT_EMOTES.indexOf(a) > -1) /* found in the database */
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user