Twitch single-message deletion basic support
This commit is contained in:
@@ -59,9 +59,11 @@ An **internal** StreamGoose backend is a Go program embedded as a goroutine into
|
|||||||
```go
|
```go
|
||||||
type ChatMessage struct {
|
type ChatMessage struct {
|
||||||
Id uint64 `json:"id"` /* message id (auto-filled) */
|
Id uint64 `json:"id"` /* message id (auto-filled) */
|
||||||
|
NativeId string `json:"native_id"` /* message id native to the platform */
|
||||||
|
AuthorId string `json:"author_id"` /* author id native to the platform */
|
||||||
Type string `json:"type"` /* message type string */
|
Type string `json:"type"` /* message type string */
|
||||||
Timestamp string `json:"timestamp"` /* timestamp string (formatted) */
|
Timestamp string `json:"timestamp"` /* timestamp string (formatted) */
|
||||||
Username string `json:"username"` /* username */
|
Username string `json:"username"` /* author username (as displayed) */
|
||||||
Text string `json:"text"` /* message text */
|
Text string `json:"text"` /* message text */
|
||||||
Icon string `json:"icon"` /* optional message icon URI */
|
Icon string `json:"icon"` /* optional message icon URI */
|
||||||
}
|
}
|
||||||
@@ -71,7 +73,9 @@ The `Id` field is populated automatically whenever a message enters the broker.
|
|||||||
|
|
||||||
An **external** StreamGoose backend is a program (written in any network-enabled programming language) that can make a POST request to the corresponding message broker URL (`http://[broker_host]:60053/new` endpoint). The request body shall be a JSON object with the following fields (all passed as strings):
|
An **external** StreamGoose backend is a program (written in any network-enabled programming language) that can make a POST request to the corresponding message broker URL (`http://[broker_host]:60053/new` endpoint). The request body shall be a JSON object with the following fields (all passed as strings):
|
||||||
|
|
||||||
- `type`: message type, consisting of `^`-delimited classes (e.g. `yt^moderator` or `tw^vip^color#A1B2C3` etc, see below in the "Customization" section of this README). This field defines how the message will be styled when being rendered on the frontend.
|
- `native_id`: an ID string assigned to the message by the streaming platform itself (Twitch, YouTube etc). Used to further handling deleted messages.
|
||||||
|
- `author_id`: an ID string assigned to the message author by the streaming platform itself. Used to further handle bans and subsequent message deletions.
|
||||||
|
- `type`: message type, consisting of `^`-delimited classes (e.g. `yt^moderator` or `tw^vip^color#A1B2C3` etc, see below in the "Customization" section of this README). This field defines how the message will be styled when being rendered on the frontend. There are two special message types, `delmsg` and `deluser`, that require `native_id` and `author_id` fields respectively to be set in order to perform message deletion on the frontend.
|
||||||
- `timestamp`: an ISO 8601 **UTC** timestamp string denoting when the message was sent. The only recommended format (in strftime syntax) is `%Y-%m-%dT%H:%M:%SZ`.
|
- `timestamp`: an ISO 8601 **UTC** timestamp string denoting when the message was sent. The only recommended format (in strftime syntax) is `%Y-%m-%dT%H:%M:%SZ`.
|
||||||
- `username`: the message author name that should be displayed in the chat.
|
- `username`: the message author name that should be displayed in the chat.
|
||||||
- `text`: the message text that should be displayed in the chat.
|
- `text`: the message text that should be displayed in the chat.
|
||||||
|
|||||||
@@ -108,6 +108,8 @@ func kk_process_message(rawmsg map[string]interface{}) {
|
|||||||
/* prepare output message */
|
/* prepare output message */
|
||||||
out_message := ChatMessage {
|
out_message := ChatMessage {
|
||||||
Type: "kk",
|
Type: "kk",
|
||||||
|
NativeId: rawmsg["id"].(string),
|
||||||
|
AuthorId: string(int64(author["id"].(float64))),
|
||||||
Username: author["username"].(string),
|
Username: author["username"].(string),
|
||||||
Timestamp: sent_dt.UTC().Format("2006-01-02T15:04:05Z"),
|
Timestamp: sent_dt.UTC().Format("2006-01-02T15:04:05Z"),
|
||||||
Text: rawmsg["content"].(string),
|
Text: rawmsg["content"].(string),
|
||||||
|
|||||||
+22
-1
@@ -65,7 +65,26 @@ func oc_api_get(endpoint string) []interface{} {
|
|||||||
func oc_process_message(rawmsg map[string]interface{}) {
|
func oc_process_message(rawmsg map[string]interface{}) {
|
||||||
/* if the message came from a real user, this is the one we should process */
|
/* if the message came from a real user, this is the one we should process */
|
||||||
if rawmsg["type"] == "CHAT" {
|
if rawmsg["type"] == "CHAT" {
|
||||||
if !slices.Contains(oc_msg_id_cache, rawmsg["id"].(string)) { /* we have a fresh message here */
|
/* first, check if we need to include this message at all */
|
||||||
|
msg_hidden := false
|
||||||
|
user_banned := false
|
||||||
|
if _, ok := rawmsg["hiddenAt"]; ok && rawmsg["hiddenAt"].(string) != "" {
|
||||||
|
msg_hidden = true
|
||||||
|
}
|
||||||
|
if _, ok := rawmsg["user"]; ok {
|
||||||
|
if _, ok := rawmsg["user"].(map[string]interface{})["disabledAt"]; ok {
|
||||||
|
if rawmsg["user"].(map[string]interface{})["disabledAt"].(string) != "" {
|
||||||
|
user_banned = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
msg_included := slices.Contains(oc_msg_id_cache, rawmsg["id"].(string))
|
||||||
|
if msg_hidden || user_banned { /* don't include the message, check if it already is included */
|
||||||
|
if msg_included { /* delete this message from the cache */
|
||||||
|
msg_idx := slices.Index(oc_msg_id_cache, rawmsg["id"].(string))
|
||||||
|
oc_msg_id_cache = slices.Delete(oc_msg_id_cache, msg_idx, msg_idx+1)
|
||||||
|
}
|
||||||
|
} else if !msg_included { /* we have a fresh message here */
|
||||||
var author map[string]interface{}
|
var author map[string]interface{}
|
||||||
/* check if the "user" field is present */
|
/* check if the "user" field is present */
|
||||||
if _, ok := rawmsg["user"]; ok {
|
if _, ok := rawmsg["user"]; ok {
|
||||||
@@ -80,6 +99,8 @@ func oc_process_message(rawmsg map[string]interface{}) {
|
|||||||
/* prepare output message */
|
/* prepare output message */
|
||||||
out_message := ChatMessage {
|
out_message := ChatMessage {
|
||||||
Type: "oc",
|
Type: "oc",
|
||||||
|
NativeId: rawmsg["id"].(string),
|
||||||
|
AuthorId: author["id"].(string),
|
||||||
Username: author["displayName"].(string),
|
Username: author["displayName"].(string),
|
||||||
Timestamp: sent_dt.UTC().Format("2006-01-02T15:04:05Z"),
|
Timestamp: sent_dt.UTC().Format("2006-01-02T15:04:05Z"),
|
||||||
Text: rawmsg["body"].(string),
|
Text: rawmsg["body"].(string),
|
||||||
|
|||||||
+3
-1
@@ -21,9 +21,11 @@ import (
|
|||||||
/* main chat message structure */
|
/* main chat message structure */
|
||||||
type ChatMessage struct {
|
type ChatMessage struct {
|
||||||
Id uint64 `json:"id"` /* message id (auto-filled) */
|
Id uint64 `json:"id"` /* message id (auto-filled) */
|
||||||
|
NativeId string `json:"native_id"` /* message id native to the platform */
|
||||||
|
AuthorId string `json:"author_id"` /* author id native to the platform */
|
||||||
Type string `json:"type"` /* message type string */
|
Type string `json:"type"` /* message type string */
|
||||||
Timestamp string `json:"timestamp"` /* timestamp string (formatted) */
|
Timestamp string `json:"timestamp"` /* timestamp string (formatted) */
|
||||||
Username string `json:"username"` /* username */
|
Username string `json:"username"` /* author username (as displayed) */
|
||||||
Text string `json:"text"` /* message text */
|
Text string `json:"text"` /* message text */
|
||||||
Icon string `json:"icon"` /* optional message icon URI */
|
Icon string `json:"icon"` /* optional message icon URI */
|
||||||
}
|
}
|
||||||
|
|||||||
+26
-12
@@ -150,22 +150,34 @@ func tw_handle_emotes(text string, emote_data string) string {
|
|||||||
|
|
||||||
/* main message procesing function */
|
/* main message procesing function */
|
||||||
func tw_process_message(rawmsg string) {
|
func tw_process_message(rawmsg string) {
|
||||||
/* if the message came from a real user, this is the one we should process */
|
parts := strings.Split(rawmsg, " :") /* get raw message parts */
|
||||||
if strings.HasPrefix(rawmsg, "@badge-info=") {
|
/* first part is metadata, second is a full IRC username,
|
||||||
parts := strings.Split(rawmsg, " :") /* get raw message parts */
|
the rest is the message ending with \r\n */
|
||||||
/* first part is metadata, second is a full IRC username,
|
metadata_str := strings.TrimSpace(parts[0])
|
||||||
the rest is the message ending with \r\n */
|
msg_text := ""
|
||||||
metadata_str := strings.TrimSpace(parts[0])
|
if(len(parts) > 2) {
|
||||||
msg_text := strings.Join(parts[2:], " :")
|
msg_text = strings.Join(parts[2:], " :")
|
||||||
msg_text = strings.ReplaceAll(msg_text, "\u0000", "") /* remove excess null bytes */
|
msg_text = strings.ReplaceAll(msg_text, "\u0000", "") /* remove excess null bytes */
|
||||||
|
}
|
||||||
metadata := make(map[string]string) /* store parsed metadata here */
|
metadata := make(map[string]string) /* store parsed metadata here */
|
||||||
parts = strings.Split(metadata_str, ";")
|
parts = strings.Split(metadata_str, ";")
|
||||||
for _, field := range parts { /* iterate over metadata fields */
|
for _, field := range parts { /* iterate over metadata fields */
|
||||||
|
if strings.Contains(field, "=") {
|
||||||
fieldkey, fieldval, _ := strings.Cut(field, "=")
|
fieldkey, fieldval, _ := strings.Cut(field, "=")
|
||||||
metadata[fieldkey] = fieldval
|
metadata[fieldkey] = fieldval
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
/* if the message came from a real user, this is the one we should process */
|
||||||
|
/* but first, check if this is a single-message deletion event */
|
||||||
|
if strings.HasPrefix(rawmsg, "@login=") && strings.Contains(rawmsg, " CLEARMSG ") {
|
||||||
|
/* prepare output message */
|
||||||
|
out_message := ChatMessage {
|
||||||
|
Type: "delmsg",
|
||||||
|
NativeId: metadata["target-msg-id"],
|
||||||
|
}
|
||||||
|
/* send it to the message broker via the message_transport channel */
|
||||||
|
message_transport <- out_message
|
||||||
|
} else if strings.HasPrefix(rawmsg, "@badge-info=") { /* normal message or usernotice */
|
||||||
/* convert the timestamp */
|
/* convert the timestamp */
|
||||||
ts, err := strconv.ParseInt(metadata["tmi-sent-ts"], 10, 0)
|
ts, err := strconv.ParseInt(metadata["tmi-sent-ts"], 10, 0)
|
||||||
if err != nil { /* fallback to current time if anything */
|
if err != nil { /* fallback to current time if anything */
|
||||||
@@ -185,6 +197,8 @@ func tw_process_message(rawmsg string) {
|
|||||||
/* prepare output message */
|
/* prepare output message */
|
||||||
out_message := ChatMessage {
|
out_message := ChatMessage {
|
||||||
Type: "tw",
|
Type: "tw",
|
||||||
|
NativeId: metadata["id"],
|
||||||
|
AuthorId: metadata["user-id"],
|
||||||
Username: metadata["display-name"],
|
Username: metadata["display-name"],
|
||||||
Timestamp: sent_dt.UTC().Format("2006-01-02T15:04:05Z"),
|
Timestamp: sent_dt.UTC().Format("2006-01-02T15:04:05Z"),
|
||||||
Text: strings.TrimSpace(msg_text),
|
Text: strings.TrimSpace(msg_text),
|
||||||
|
|||||||
@@ -124,6 +124,8 @@ func yt_inner_action_to_chatitem(action map[string]interface{}) ChatMessage {
|
|||||||
/* prepare output message */
|
/* prepare output message */
|
||||||
out_message := ChatMessage {
|
out_message := ChatMessage {
|
||||||
Type: "yt",
|
Type: "yt",
|
||||||
|
NativeId: renderer["id"].(string),
|
||||||
|
AuthorId: renderer["authorExternalChannelId"].(string),
|
||||||
Username: author_name,
|
Username: author_name,
|
||||||
Timestamp: sent_dt.UTC().Format("2006-01-02T15:04:05Z"),
|
Timestamp: sent_dt.UTC().Format("2006-01-02T15:04:05Z"),
|
||||||
Text: yt_inner_runs_to_text(mruns),
|
Text: yt_inner_runs_to_text(mruns),
|
||||||
|
|||||||
+42
-29
@@ -39,40 +39,52 @@ function overrideStyles(text) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/* individual message appender */
|
/* individual message appender */
|
||||||
function gooseAppendMessage(id, type, timestamp, username, text, icon) {
|
function gooseAppendMessage(id, native_id, author_id, type, timestamp, username, text, icon) {
|
||||||
var html = ""
|
var html = ""
|
||||||
if(parseInt(id) > 0 && type.length > 0) {
|
if(parseInt(id) > 0 && type.length > 0) {
|
||||||
var classes = type.split('^'), cl = classes.length, j,
|
if(type == "delmsg") { /* message deletion event */
|
||||||
colorpref = null, badges = [], platform, bname
|
var el = document.querySelector('[data-native-id="' + esc(native_id) + '"]')
|
||||||
for(j=0;j<cl;j++) { /* detect color preference */
|
if(el) el.parentNode.removeChild(el)
|
||||||
if(classes[j].startsWith('color'))
|
} else if(type == "deluser") { /* bulk deletion event by username */
|
||||||
colorpref = classes[j].slice(5) /* remove color word */
|
var els = document.querySelector('[data-author-id="' + esc(author_id) + '"]')
|
||||||
if(classes[j].startsWith('badge')) { /* remove the class but cache it */
|
if(els && els.length > 0) {
|
||||||
badges.push(classes[j].slice(5)) /* remove badge word */
|
var i, l = els.length
|
||||||
classes[j] = "" /* remove the class from the list */
|
for(i=0;i<l;i++) els[i].parentNode.removeChild(els[i])
|
||||||
}
|
}
|
||||||
|
} else { /* message appending event */
|
||||||
|
var classes = type.split('^'), cl = classes.length, j,
|
||||||
|
colorpref = null, badges = [], platform, bname
|
||||||
|
for(j=0;j<cl;j++) { /* detect color preference */
|
||||||
|
if(classes[j].startsWith('color'))
|
||||||
|
colorpref = classes[j].slice(5) /* remove color word */
|
||||||
|
if(classes[j].startsWith('badge')) { /* remove the class but cache it */
|
||||||
|
badges.push(classes[j].slice(5)) /* remove badge word */
|
||||||
|
classes[j] = "" /* remove the class from the list */
|
||||||
|
}
|
||||||
|
}
|
||||||
|
html += '<div data-msg-id="' + id + '" data-native-id="' + esc(native_id) + '" data-author-id="'
|
||||||
|
+ esc(author_id) + '" class="' + classes.join(' ') +'">'
|
||||||
|
/* handle badges and icons */
|
||||||
|
html += '<span class=icon>'
|
||||||
|
for(j=0,cl=badges.length;j<cl;j++) { /* iterate over badges */
|
||||||
|
platform = badges[j].slice(0,2) /* first 2 characters denote the platform */
|
||||||
|
bname = badges[j].slice(2) /* the rest is the badge name itself */
|
||||||
|
html += '<img class=badge src="/badge/{platform}/{badgename}">'
|
||||||
|
.replace('{platform}', platform).replace('{badgename}', bname)
|
||||||
|
}
|
||||||
|
if(icon.length > 0) /* user icon, if present */
|
||||||
|
html += '<img src="' + icon + '">'
|
||||||
|
html += '</span>'
|
||||||
|
html += '<span class=timestamp>' + timestamp + '</span>'
|
||||||
|
html += '<span class=username'
|
||||||
|
if(colorpref) html += ' style="color:' + colorpref + '"'
|
||||||
|
html += '>' + username + '</span>'
|
||||||
|
html += '<span class=msg' + overrideStyles(text) +'>' + handleEmotes(text) + '</span></div>'
|
||||||
|
document.getElementById("msglist").innerHTML += html
|
||||||
}
|
}
|
||||||
html += '<div data-msg-id="' + id + '" class="' + classes.join(' ') +'">'
|
|
||||||
/* handle badges and icons */
|
|
||||||
html += '<span class=icon>'
|
|
||||||
for(j=0,cl=badges.length;j<cl;j++) { /* iterate over badges */
|
|
||||||
platform = badges[j].slice(0,2) /* first 2 characters denote the platform */
|
|
||||||
bname = badges[j].slice(2) /* the rest is the badge name itself */
|
|
||||||
html += '<img class=badge src="/badge/{platform}/{badgename}">'
|
|
||||||
.replace('{platform}', platform).replace('{badgename}', bname)
|
|
||||||
}
|
|
||||||
if(icon.length > 0) /* user icon, if present */
|
|
||||||
html += '<img src="' + icon + '">'
|
|
||||||
html += '</span>'
|
|
||||||
html += '<span class=timestamp>' + timestamp + '</span>'
|
|
||||||
html += '<span class=username'
|
|
||||||
if(colorpref) html += ' style="color:' + colorpref + '"'
|
|
||||||
html += '>' + username + '</span>'
|
|
||||||
html += '<span class=msg' + overrideStyles(text) +'>' + handleEmotes(text) + '</span></div>'
|
|
||||||
}
|
}
|
||||||
document.getElementById("msglist").innerHTML += html
|
|
||||||
/* autoscroll to the end */
|
/* autoscroll to the end */
|
||||||
window.setTimeout(function(){window.scrollTo(0, document.body.scrollHeight)}, 100)
|
window.setTimeout(function(){window.scrollTo(0, document.body.scrollHeight)}, 150)
|
||||||
}
|
}
|
||||||
|
|
||||||
/* entry point, set up an event source */
|
/* entry point, set up an event source */
|
||||||
@@ -80,6 +92,7 @@ window.addEventListener('DOMContentLoaded', function() {
|
|||||||
var msgSource = new EventSource('/sse')
|
var msgSource = new EventSource('/sse')
|
||||||
msgSource.onmessage = e => {
|
msgSource.onmessage = e => {
|
||||||
var msg = JSON.parse(e.data)
|
var msg = JSON.parse(e.data)
|
||||||
gooseAppendMessage(msg.id, msg.type, msg.timestamp, msg.username, msg.text, msg.icon)
|
console.log("Message", msg)
|
||||||
|
gooseAppendMessage(msg.id, msg.native_id, msg.author_id, msg.type, msg.timestamp, msg.username, msg.text, msg.icon)
|
||||||
}
|
}
|
||||||
}, false)
|
}, false)
|
||||||
|
|||||||
Reference in New Issue
Block a user