Twitch badge support implemented

This commit is contained in:
Luxferre
2024-09-20 12:21:28 +03:00
parent 4e972bd5f5
commit 56f7da4748
9 changed files with 180 additions and 16 deletions
+45
View File
@@ -0,0 +1,45 @@
/*
StreamGoose internal badge provider — Go reference implementation
Created by Luxferre in 2024, released into public domain
*/
package main
import (
"net/http"
)
/* platform-specific badge provider implementations */
func badge_provider_impls() map[string]interface{} {
return map[string]interface{} {
"tw": func(badgename string) string { /* Twitch implementation */
if _, ok := TW_BADGE_CACHE[badgename]; ok { /* found */
return TW_BADGE_CACHE[badgename]
} else { /* not found */
return ""
}
},
/* "kk": func(badgename string) {}, */
}
}
/* serve the GET /badge/{platform}/{badgename} request */
/* returns a 301-type redirect to the resolved URL or 404 if unresolved */
func bp_handle_request(w http.ResponseWriter, r *http.Request) {
platform := r.PathValue("platform")
badgename := r.PathValue("badgename")
impls := badge_provider_impls()
if _, ok := impls[platform]; ok { /* process the badge and return 301 */
badgeurl := impls[platform].(func(string) string)(badgename)
if len(badgeurl) > 0 { /* return 301 redirect */
http.Redirect(w, r, badgeurl, http.StatusMovedPermanently)
} else { /* return 404 */
http.Error(w, "404 - Badge not found", http.StatusNotFound)
w.Write([]byte("404 - Badge not found"))
}
} else { /* return 404 */
http.Error(w, "404 - Badge provider not found", http.StatusNotFound)
}
}
+2
View File
@@ -169,6 +169,8 @@ func main() {
http.HandleFunc("/new", msgAddHandler)
/* serve the GET /sse */
http.HandleFunc("/sse", sseHandler)
/* serve the GET /badge/{platform}/{badgename} (call to the badge provider) */
http.HandleFunc("/badge/{platform}/{badgename}", bp_handle_request)
/* serve the webroot directory */
fs := http.FileServer(http.Dir(filepath.Join(APP_ROOT_DIR, "webroot")))
http.Handle("/", fs)
+102 -4
View File
@@ -12,12 +12,21 @@ import (
"strings"
"strconv"
"time"
"bytes"
"net"
"net/http"
"io/ioutil"
"path/filepath"
"unicode/utf8"
"encoding/json"
)
/* Twitch badge URL cache */
var TW_BADGE_CACHE map[string]string
/* Twitch OAuth global cache */
var TW_OAUTH_TOKEN string
var TW_CLIENT_ID string
/* helper function to write a string to the socket */
func tcpwritestr(conn net.Conn, str string) int {
i, err := conn.Write([]byte(str))
@@ -37,6 +46,80 @@ func tcpreadstr(conn net.Conn) string {
return string(reply)
}
/* non-IRC API request helper function */
func tw_api_req(url string, data string) []byte {
method := "GET"
if len(data) > 0 {
method = "POST"
}
req, err := http.NewRequest(method, "https://api.twitch.tv/helix" + url, bytes.NewReader([]byte(data)))
req.Header.Set("Authorization", "Bearer " + TW_OAUTH_TOKEN)
req.Header.Set("Client-Id", TW_CLIENT_ID)
client := &http.Client{}
resp, err := client.Do(req)
if err != nil {
log.Fatal(err)
}
body, _ := ioutil.ReadAll(resp.Body)
resp.Body.Close()
return body
}
/* badge cache population function */
func tw_fetch_badge_cache(channelname string) map[string]string {
readycache := make(map[string]string)
broadcaster_id := ""
var ustruct map[string]interface{}
/* get broadcaster ID from the channel name */
body := tw_api_req(fmt.Sprintf("/users?login=%s", channelname), "")
if len(body) > 0 {
err := json.Unmarshal(body, &ustruct)
if err == nil {
if _, ok := ustruct["data"]; ok && len(ustruct["data"].([]interface{})) > 0 {
bdata := ustruct["data"].([]interface{})[0].(map[string]interface{})
broadcaster_id = bdata["id"].(string)
}
}
}
/* get the global badges */
body = tw_api_req("/chat/badges/global", "")
if len(body) > 0 {
err := json.Unmarshal(body, &ustruct)
if err == nil {
if _, ok := ustruct["data"]; ok && len(ustruct["data"].([]interface{})) > 0 {
for _, bdata := range ustruct["data"].([]interface{}) { /* fetch individual badges */
bsetid := bdata.(map[string]interface{})["set_id"].(string)
for _, bver := range bdata.(map[string]interface{})["versions"].([]interface{}) {
bid := bver.(map[string]interface{})["id"].(string) /* badge ID */
burl := bver.(map[string]interface{})["image_url_1x"].(string) /* badge primary URL */
readycache[bsetid + "_" + bid] = burl
}
}
}
}
}
if broadcaster_id != "" { /* also get the channel-specific badges */
body = tw_api_req(fmt.Sprintf("/chat/badges?broadcaster_id=%s", broadcaster_id), "")
if len(body) > 0 {
err := json.Unmarshal(body, &ustruct)
if err == nil {
if _, ok := ustruct["data"]; ok && len(ustruct["data"].([]interface{})) > 0 {
for _, bdata := range ustruct["data"].([]interface{}) { /* fetch individual badges */
bsetid := bdata.(map[string]interface{})["set_id"].(string)
for _, bver := range bdata.(map[string]interface{})["versions"].([]interface{}) {
bid := bver.(map[string]interface{})["id"].(string) /* badge ID */
burl := bver.(map[string]interface{})["image_url_1x"].(string) /* badge primary URL */
readycache[bsetid + "_" + bid] = burl
}
}
}
}
}
}
return readycache
}
/* emote handling function */
func tw_handle_emotes(text string, emote_data string) string {
replacements := make(map[string]string) /* future replacement string cache */
@@ -112,11 +195,10 @@ func tw_process_message(rawmsg string) {
if _, ok := metadata["badges"]; ok {
badges := strings.Split(metadata["badges"], ",") /* they are comma-delimited */
if len(badges) > 0 { /* process badge names */
for i, badge := range badges { /* extract the badge name */
bname, _, _ := strings.Cut(badge, "/")
badges[i] = bname /* reassign */
for _, badge := range badges { /* extract the badge name and version */
bname, bver, _ := strings.Cut(badge, "/")
out_message.Type += "-" + bname + "-badgetw" + bname + "_" + bver
}
out_message.Type += "-" + strings.Join(badges, "-")
}
}
@@ -161,6 +243,22 @@ func twitch_run(config map[string]interface{}) {
log.Fatal(token_err_msg)
}
cid_path := filepath.Join(APP_ROOT_DIR, "auth", "twitch-cid.txt")
cid, err := ioutil.ReadFile(cid_path)
cid_err_msg := "[twitch] No Twitch client file provided or it is empty. "
cid_err_msg += "Please visit https://twitchapps.com/tmi/, find a client ID in the source "
cid_err_msg += "and paste it into the " + cid_path +" file!"
if err != nil {
log.Fatal(cid_err_msg + "\nDetailed error: ", err)
}
/* populate TW_OAUTH_TOKEN and TW_CLIENT_ID globals for non-IRC API calls */
TW_CLIENT_ID = strings.TrimSpace(string(cid))
TW_OAUTH_TOKEN, _ = strings.CutPrefix(config["tw_token"].(string), "oauth:")
/* load the Twitch badge cache */
TW_BADGE_CACHE = tw_fetch_badge_cache(config["tw_channel"].(string))
/* Connect to the Twitch IRC server */
conn, err := net.Dial("tcp", "irc.chat.twitch.tv:6667")
if err != nil {
+1 -1
View File
@@ -29,7 +29,7 @@ type YTIChatOptions struct {
ClientVersion string `json:"clientVersion"` /* client version string */
}
/* API request helper functions */
/* API request helper function */
func yt_inner_api_req(url string, data []byte) string {
method := "GET"
if len(data) > 0 {