Skip to content

Commit abb17c7

Browse files
steveseguinactions-user
authored andcommitted
feat(webhooks): Add generic incoming webhook relay
Introduces a new feature to relay raw incoming webhook payloads from various platforms to a user-defined endpoint, enhancing integration flexibility. - Implemented `relayIncomingWebhook` in `background.js` to forward event data. - Configured relay for `Stripe`, `Ko-fi`, `Buy Me a Coffee (BMAC)`, and `Fourthwall` events within the `socketserver` message listener. - Added a new UI setting in `popup.html` (checkbox and URL input) to enable and configure the custom webhook relay endpoint. - Refined `hype.html` centering logic, ensuring the `parentHolder` element correctly occupies 100% width when `textAlign` is `center` for improved display. - Updated `manifest.json` version to `3.32.9`. [auto-enhanced]
1 parent f0513bf commit abb17c7

4 files changed

Lines changed: 202 additions & 116 deletions

File tree

background.js

Lines changed: 173 additions & 98 deletions
Original file line numberDiff line numberDiff line change
@@ -4284,70 +4284,138 @@ function escapeHtml(unsafe) {
42844284
}
42854285
}
42864286

4287-
function sendToH2R(data) {
4288-
if (settings.h2r && settings.h2rserver && settings.h2rserver.textsetting) {
4289-
try {
4290-
var postServer = "http://127.0.0.1:4001/data/";
4291-
if (settings.h2rserver.textsetting.startsWith("http")) {
4292-
// full URL provided
4293-
postServer = settings.h2rserver.textsetting;
4294-
} else if (settings.h2rserver.textsetting.startsWith("127.0.0.1")) {
4295-
// missing the HTTP, so assume what they mean
4296-
postServer = "http://" + settings.h2rserver.textsetting;
4297-
} else {
4298-
postServer += settings.h2rserver.textsetting; // Just going to assume they gave the token
4299-
}
4300-
var msg = {};
4301-
if ("id" in data) {
4302-
msg.id = data.id;
4303-
}
4304-
if (data.timestamp) {
4305-
msg.timestamp = data.timestamp;
4306-
}
4307-
if (!data.textonly) {
4308-
data.chatmessage = unescapeHtml(data.chatmessage);
4309-
}
4310-
msg.snippet = {};
4311-
msg.snippet.displayMessage = sanitizeRelay(data.chatmessage, data.textonly) || "";
4312-
if (!msg.snippet.displayMessage) {
4313-
return;
4314-
}
4315-
msg.authorDetails = {};
4316-
msg.authorDetails.displayName = data.chatname || "";
4317-
if (data.type && (data.type == "twitch") && !data.chatimg && data.chatname) {
4318-
msg.authorDetails.profileImageUrl = "https://api.socialstream.ninja/twitch/large?username=" + encodeURIComponent(data.chatname); // 150x150
4319-
} else if (data.type && ((data.type == "youtube") || (data.type == "youtubeshorts")) && data.chatimg) {
4320-
let chatimg = data.chatimg.replace("=s32-", "=s256-");
4321-
msg.authorDetails.profileImageUrl = chatimg.replace("=s64-", "=s256-");
4322-
} else {
4323-
msg.authorDetails.profileImageUrl = data.chatimg || "https://socialstream.ninja/sources/images/unknown.png";
4324-
}
4325-
if (data.type && data.sourceImg && data.type == "restream") {
4326-
msg.platform = {};
4327-
msg.platform.name = data.type || "";
4328-
if (data.sourceImg === "restream.png") {
4329-
msg.platform.logoUrl = "https://socialstream.ninja/sources/images/" + data.sourceImg;
4330-
} else {
4331-
msg.platform.logoUrl = data.sourceImg;
4332-
}
4333-
} else if (data.type) {
4334-
msg.platform = {};
4335-
msg.platform.name = data.type || "";
4336-
msg.platform.logoUrl = "https://socialstream.ninja/sources/images/" + data.type + ".png";
4337-
}
4338-
var h2r = {};
4339-
h2r.messages = [];
4340-
h2r.messages.push(msg);
4341-
ajax(h2r, postServer, "POST");
4342-
} catch (e) {
4343-
console.warn(e);
4344-
}
4345-
}
4346-
}
4347-
function sanitizeRelay(text, textonly=false, alt = false) {
4348-
if (!text || !text.trim()) {
4349-
return alt || text;
4350-
}
4287+
function sendToH2R(data) {
4288+
if (settings.h2r && settings.h2rserver && settings.h2rserver.textsetting) {
4289+
try {
4290+
var postServer = "http://127.0.0.1:4001/data/";
4291+
if (settings.h2rserver.textsetting.startsWith("http")) {
4292+
// full URL provided
4293+
postServer = settings.h2rserver.textsetting;
4294+
} else if (settings.h2rserver.textsetting.startsWith("127.0.0.1")) {
4295+
// missing the HTTP, so assume what they mean
4296+
postServer = "http://" + settings.h2rserver.textsetting;
4297+
} else {
4298+
postServer += settings.h2rserver.textsetting; // Just going to assume they gave the token
4299+
}
4300+
var msg = {};
4301+
if ("id" in data) {
4302+
msg.id = data.id;
4303+
}
4304+
if (data.timestamp) {
4305+
msg.timestamp = data.timestamp;
4306+
}
4307+
if (!data.textonly) {
4308+
data.chatmessage = unescapeHtml(data.chatmessage);
4309+
}
4310+
msg.snippet = {};
4311+
msg.snippet.displayMessage = sanitizeRelay(data.chatmessage, data.textonly) || "";
4312+
if (!msg.snippet.displayMessage) {
4313+
return;
4314+
}
4315+
msg.authorDetails = {};
4316+
msg.authorDetails.displayName = data.chatname || "";
4317+
if (data.type && (data.type == "twitch") && !data.chatimg && data.chatname) {
4318+
msg.authorDetails.profileImageUrl = "https://api.socialstream.ninja/twitch/large?username=" + encodeURIComponent(data.chatname); // 150x150
4319+
} else if (data.type && ((data.type == "youtube") || (data.type == "youtubeshorts")) && data.chatimg) {
4320+
let chatimg = data.chatimg.replace("=s32-", "=s256-");
4321+
msg.authorDetails.profileImageUrl = chatimg.replace("=s64-", "=s256-");
4322+
} else {
4323+
msg.authorDetails.profileImageUrl = data.chatimg || "https://socialstream.ninja/sources/images/unknown.png";
4324+
}
4325+
if (data.type && data.sourceImg && data.type == "restream") {
4326+
msg.platform = {};
4327+
msg.platform.name = data.type || "";
4328+
if (data.sourceImg === "restream.png") {
4329+
msg.platform.logoUrl = "https://socialstream.ninja/sources/images/" + data.sourceImg;
4330+
} else {
4331+
msg.platform.logoUrl = data.sourceImg;
4332+
}
4333+
} else if (data.type) {
4334+
msg.platform = {};
4335+
msg.platform.name = data.type || "";
4336+
msg.platform.logoUrl = "https://socialstream.ninja/sources/images/" + data.type + ".png";
4337+
}
4338+
var h2r = {};
4339+
h2r.messages = [];
4340+
h2r.messages.push(msg);
4341+
ajax(h2r, postServer, "POST");
4342+
} catch (e) {
4343+
console.warn(e);
4344+
}
4345+
}
4346+
}
4347+
4348+
const WEBHOOK_RELAY_SOURCES = new Set(["stripe", "kofi", "bmac", "fourthwall"]);
4349+
4350+
function normalizeWebhookRelayUrl(rawUrl) {
4351+
if (!rawUrl) {
4352+
return null;
4353+
}
4354+
4355+
let url = String(rawUrl).trim();
4356+
if (!url) {
4357+
return null;
4358+
}
4359+
4360+
if (url.startsWith("http://") || url.startsWith("https://")) {
4361+
return url;
4362+
}
4363+
4364+
if (url.startsWith("127.0.0.1") || url.startsWith("localhost")) {
4365+
return "http://" + url;
4366+
}
4367+
4368+
return "https://" + url;
4369+
}
4370+
4371+
function relayIncomingWebhook(source, payload) {
4372+
if (!WEBHOOK_RELAY_SOURCES.has(source)) {
4373+
return;
4374+
}
4375+
if (!settings.webhookrelay || !settings.webhookrelayurl || !settings.webhookrelayurl.textsetting) {
4376+
return;
4377+
}
4378+
if (!payload) {
4379+
return;
4380+
}
4381+
4382+
const endpoint = normalizeWebhookRelayUrl(settings.webhookrelayurl.textsetting);
4383+
if (!endpoint) {
4384+
return;
4385+
}
4386+
4387+
let body;
4388+
let contentType = "application/json";
4389+
if (typeof payload === "string") {
4390+
body = payload;
4391+
contentType = "text/plain";
4392+
} else {
4393+
try {
4394+
body = JSON.stringify(payload);
4395+
} catch (e) {
4396+
console.warn(`[WebhookRelay] Failed to serialize payload for ${source}:`, e);
4397+
return;
4398+
}
4399+
}
4400+
4401+
try {
4402+
fetch(endpoint, {
4403+
method: "POST",
4404+
headers: {
4405+
"Content-Type": contentType,
4406+
"X-SSN-Webhook-Source": source
4407+
},
4408+
body
4409+
}).catch(err => console.warn(`[WebhookRelay] Request failed for ${source}:`, err));
4410+
} catch (err) {
4411+
console.warn(`[WebhookRelay] Unexpected error relaying ${source}:`, err);
4412+
}
4413+
}
4414+
4415+
function sanitizeRelay(text, textonly=false, alt = false) {
4416+
if (!text || !text.trim()) {
4417+
return alt || text;
4418+
}
43514419

43524420
// Extract all emojis from image alt attributes before stripping HTML
43534421
const emojiMap = new Map();
@@ -5897,15 +5965,17 @@ socketserver.addEventListener("message", async function (event) {
58975965
} catch (e) {
58985966
console.error(e);
58995967
}
5900-
} else if ("stripe" in data) {
5901-
try {
5902-
if (data.stripe.type !== "checkout.session.completed") {
5903-
return false;
5904-
}
5905-
5906-
console.log(data.stripe);
5907-
5908-
var message = {};
5968+
} else if ("stripe" in data) {
5969+
try {
5970+
if (data.stripe.type !== "checkout.session.completed") {
5971+
return false;
5972+
}
5973+
5974+
console.log(data.stripe);
5975+
5976+
relayIncomingWebhook("stripe", data.stripe);
5977+
5978+
var message = {};
59095979
message.chatname = "";
59105980
message.chatmessage = "";
59115981

@@ -6025,13 +6095,15 @@ socketserver.addEventListener("message", async function (event) {
60256095
return;
60266096
}
60276097
} else if ("kofi" in data) {
6028-
try {
6029-
6030-
if (!data.kofi.data) {
6031-
return false;
6032-
}
6033-
try {
6034-
var kofi = JSON.parse(decodeURIComponent(data.kofi.data).replace(/\+/g, " "));
6098+
try {
6099+
6100+
if (!data.kofi.data) {
6101+
return false;
6102+
}
6103+
6104+
relayIncomingWebhook("kofi", data.kofi);
6105+
try {
6106+
var kofi = JSON.parse(decodeURIComponent(data.kofi.data).replace(/\+/g, " "));
60356107
} catch (e) {
60366108
console.error(e);
60376109
return;
@@ -6097,15 +6169,16 @@ socketserver.addEventListener("message", async function (event) {
60976169
}
60986170

60996171

6100-
} else if ("bmac" in data) { // Buy Me a Coffe New Membership and Donation detection
6101-
try {
6102-
if (!data.bmac) {
6103-
return false;
6104-
}
6105-
else {
6106-
var bmac = data.bmac;
6107-
var message = {};
6108-
if (bmac.type === "membership.started") {
6172+
} else if ("bmac" in data) { // Buy Me a Coffe New Membership and Donation detection
6173+
try {
6174+
if (!data.bmac) {
6175+
return false;
6176+
}
6177+
else {
6178+
var bmac = data.bmac;
6179+
relayIncomingWebhook("bmac", data.bmac);
6180+
var message = {};
6181+
if (bmac.type === "membership.started") {
61096182
message.chatname = bmac.data.supporter_name || "Anonymous";
61106183
message.chatmessage = bmac.data.support_note.trim();
61116184
//We use the donation badge from Kofi to feature the membership level name
@@ -6159,15 +6232,17 @@ socketserver.addEventListener("message", async function (event) {
61596232
} catch (e) {
61606233
return;
61616234
}
6162-
} else if ("fourthwall" in data) { // Dorthwall
6163-
try {
6164-
if (!data.fourthwall.data || data.fourthwall.type !== "ORDER_PLACED") {
6165-
return false;
6166-
}
6167-
6168-
const fourthwallData = data.fourthwall.data;
6169-
6170-
var message = {};
6235+
} else if ("fourthwall" in data) { // Dorthwall
6236+
try {
6237+
if (!data.fourthwall.data || data.fourthwall.type !== "ORDER_PLACED") {
6238+
return false;
6239+
}
6240+
6241+
relayIncomingWebhook("fourthwall", data.fourthwall);
6242+
6243+
const fourthwallData = data.fourthwall.data;
6244+
6245+
var message = {};
61716246
message.chatname = fourthwallData.username ||
61726247
(fourthwallData.billing?.address?.name || "Anonymous");
61736248
message.chatmessage = fourthwallData.message || "";

hype.html

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -877,7 +877,8 @@
877877
} else {
878878
parentHolder.style.marginLeft = "auto";
879879
parentHolder.style.marginRight = "auto";
880-
parentHolder.style.textAlign = "center";
880+
parentHolder.style.textAlign = "center";
881+
parentHolder.style.width = "100%";
881882
}
882883
if (alignRightLegacy) {
883884
parentHolder.style.position = "absolute";

manifest.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
"name": "Social Stream Ninja",
33
"description": "Powerful tooling to engage live chat on Youtube, Twitch, Zoom, and more.",
44
"manifest_version": 3,
5-
"version": "3.32.8",
5+
"version": "3.32.9",
66
"homepage_url": "http://socialstream.ninja/",
77
"icons": {
88
"128": "icons/icon-128.png"

popup.html

Lines changed: 26 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1453,7 +1453,7 @@ <h2 class="streaming_chat_title">
14531453
<div id="dock" data-raw="" class="streaming_chat"></div>
14541454

14551455
<!-- Stream Overlay Preset Selector -->
1456-
<div class="overlay-selector" style="margin: 10px 0; padding: 10px; background: rgba(255,255,255,0.05); border-radius: 8px;">
1456+
<div class="overlay-selector" style="margin: 10px 0; padding: 10px; background: rgba(255,255,255,0.05); border-radius: 8px;overflow:hidden;">
14571457
<label for="overlay-preset-select" style="display: inline-block; margin-right: 10px;">
14581458
<span data-translate="select-overlay">🎨 Select Preset Overlay Theme:</span>
14591459
</label>
@@ -8029,21 +8029,31 @@ <h3>Printer Control</h3>
80298029
</span>
80308030
</p>
80318031
<hr>
8032-
<div>
8033-
<label class="switch">
8034-
<input type="checkbox" data-setting="h2r" />
8035-
<span class="slider round"></span>
8036-
</label>
8037-
<div class="textInputContainer" style="display: inline-block; width: 250px;">
8038-
<input type="text" id="h2rserver" class="textInput" autocomplete="off" placeholder="Provide ID or full URL" data-textsetting="h2rserver" />
8039-
<label for="h2rserver"><span data-translate="send-all-to-h2r-server">&gt; Send all to H2R server</span></label>
8040-
</div>
8041-
</div>
8042-
<div>
8043-
<label class="switch">
8044-
<input type="checkbox" data-setting="post" />
8045-
<span class="slider round"></span>
8046-
</label>
8032+
<div>
8033+
<label class="switch">
8034+
<input type="checkbox" data-setting="h2r" />
8035+
<span class="slider round"></span>
8036+
</label>
8037+
<div class="textInputContainer" style="display: inline-block; width: 250px;">
8038+
<input type="text" id="h2rserver" class="textInput" autocomplete="off" placeholder="Provide ID or full URL" data-textsetting="h2rserver" />
8039+
<label for="h2rserver"><span data-translate="send-all-to-h2r-server">&gt; Send all to H2R server</span></label>
8040+
</div>
8041+
</div>
8042+
<div title="Relay Ko-fi, Stripe, Buy Me a Coffee, and Fourthwall webhook payloads to your custom endpoint">
8043+
<label class="switch">
8044+
<input type="checkbox" data-setting="webhookrelay" />
8045+
<span class="slider round"></span>
8046+
</label>
8047+
<div class="textInputContainer" style="display: inline-block; width: 250px;">
8048+
<input type="text" id="webhookrelayurl" class="textInput" autocomplete="off" placeholder="https://thirdparty.com/api" data-textsetting="webhookrelayurl" />
8049+
<label for="webhookrelayurl"><span>&gt; Relay incoming webhooks</span></label>
8050+
</div>
8051+
</div>
8052+
<div>
8053+
<label class="switch">
8054+
<input type="checkbox" data-setting="post" />
8055+
<span class="slider round"></span>
8056+
</label>
80478057
<div class="textInputContainer" style="display: inline-block; width: 250px;">
80488058
<input type="text" id="postserver" class="textInput" autocomplete="off" placeholder="Provide ID or full URL" data-textsetting="postserver" />
80498059
<label for="postserver"><span data-translate="send-all-to-post-server">&gt; Send all to POST server</span></label>

0 commit comments

Comments
 (0)