Twitch badge support implemented
This commit is contained in:
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -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
@@ -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 {
|
||||
|
||||
@@ -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 {
|
||||
|
||||
Reference in New Issue
Block a user