Add files via upload

This commit is contained in:
Rodrigo Emanuel
2025-05-06 20:32:33 -03:00
committed by GitHub
parent 4db1f94f91
commit 43ac28bb47
12 changed files with 120 additions and 33 deletions

View File

@@ -18,9 +18,9 @@ ChatRD is a chat overlay widget for OBS that unifies messages and events from **
## 🛠️ Usage
1. Make sure your **Twitch** and **YouTube** accounts are connected on **Streamer.bot** and you have **TikFinity Desktop App** installed and set up to your account on **TikTok**. **BOTH APPS NEED TO RUN ON THE SAME PC**.
2. In **Streamer.bot**, go to **Server/Clients → WebSocket Server** and make sure it is running
3. Import the string inside the file [streamerbot-import.vortisrd](https://github.com/vortisrd/chatrd/blob/main/streamerbot-import.vortisrd) to your **Streamer.bot** using the **Import** button at the top.
1. Make sure your **Twitch** and **YouTube** accounts are connected on **Streamer.Bot** and you have **TikFinity Desktop App** installed and set up to your account on **TikTok**. **BOTH APPS NEED TO RUN ON THE SAME PC**.
2. In **Streamer.Bot**, go to **Server/Clients → WebSocket Server** and make sure it is running
3. Import the string inside the file [streamerbot-import.vortisrd](https://github.com/vortisrd/chatrd/blob/main/streamerbot-import.vortisrd) to your **Streamer.Bot** using the **Import** button at the top.
4. Open the [Settings Page](https://vortisrd.github.io/chatrd) in your browser
5. Choose your desired options
6. Click **"Copy URL"**
@@ -30,21 +30,34 @@ ChatRD is a chat overlay widget for OBS that unifies messages and events from **
## 🛠️ How to Use it in a Local Network
1. On **Streamer.bot**, go to **Server/Clients → WebSocket Server** and make sure the Address is set to the Local Network IP from the PC, for example **192.168.0.10** ... or, if you prefer, use 0.0.0.0.
2. On the [Settings Page](https://vortisrd.github.io/chatrd), make sure you set that IP on **Streamer.bot WebSocket Server**.
1. On **Streamer.Bot**, go to **Server/Clients → WebSocket Server** and make sure the Address is set to the Local Network IP from the PC, for example **192.168.0.10** ... or, if you prefer, use 0.0.0.0.
2. On the [Settings Page](https://vortisrd.github.io/chatrd), make sure you set that IP on **Streamer.Bot WebSocket Server**.
3. Turn the **"Run Locally"** switch on.
4. Click **"Copy URL"**
5. [Download ChatRD](https://github.com/vortisrd/chatrd/archive/refs/heads/main.zip) on the machine you want to see the chat on or want to set up OBS in, unzipping the file.
6. Open it on your browser or add it in OBS as Browser Source (don't tick **Local File**)following the file trail, for example: **file:///C:/PATH_TO_THE_FILE/chat.html**.
7. After **chat.html**, paste the URL copied from the configurator. The full link should be like this: **file:///C:/PATH_TO_THE_FILE/chat.html?language=ptbr&showPlatform=true&showAvatar=true&showTimestamps=false&showBadges=true&showPlatformStatistics=false&excludeCommands=true&showTwitchMessages=true&showTwitchFollows=true&showTwitchBits=true&showTwitchAnnouncements=true&showTwitchSubs=true&showTwitchGiftedSubs=true&showTwitchMassGiftedSubs=true&showTwitchGiftedSubsUserTrain=true&showTwitchRewardRedemptions=true&showTwitchRaids=true&showTwitchSharedChat=true&showTwitchViewers=true&showYouTubeMessages=true&showYouTubeSuperChats=true&showYouTubeSuperStickers=false&showYouTubeMemberships=true&showYouTubeGiftMemberships=true&showYouTubeMembershipsTrain=true&showYouTubeStatistics=true&showTikTokMessages=true&showTikTokFollows=true&showTikTokGifts=true&showTikTokSubs=true&showTikTokStatistics=true&showStreamlabsDonations=true&showStreamElementsTips=true&ignoreChatters=&streamerBotServerAddress=127.0.0.1&streamerBotServerPort=8080&=&hideAfter=0**
---
## 🔊 How to set TTS with Speaker.Bot
### Speaker.Bot Setup
1. Go to **Settings → WebSocket Server**, click on *Start Server*. Make sure to also tick the *Auto-Start* checkbox.
2. Go to **Settings → Speech Engine** and add the TTS Service of your preference. (Sapi5 is the Windows default).
3. Go to **Settings → Voice Aliases**, name the voice *SpeakerBot* and click **Add** right next to it.
4. In the Left Column, click on the **SpeakerBot** you just added and on the **Speak!** section, select the voice you want to use and click **Add**. (If you're using Sapi5, I recommend using *Microsoft Zira Desktop* as a voice).
### Streamer.Bot Setup
1. Import the [streamerbot-import.vortisrd](https://github.com/vortisrd/chatrd/blob/main/streamerbot-import.vortisrd) file to your **Streamer.Bot**. There's a new action that will handle the **Speaker.Bot** integration.
2. Go to **Integrations → Speaker.Bot**, click on *Connect*. Make sure to also tick the *Auto-Start* and *Auto-Connect* checkboxes.
---
## 🧩 Integrations
- 🟣 **Twitch** (via Streamer.bot)
- 🔴 **YouTube** (via Streamer.bot)
- 🟣 **Twitch** (via Streamer.Bot)
- 🔴 **YouTube** (via Streamer.Bot)
-**TikTok** (via TikFinity Desktop App)
- 💸 **Streamlabs / StreamElements**
@@ -52,8 +65,9 @@ ChatRD is a chat overlay widget for OBS that unifies messages and events from **
## 📦 Dependencies
- [Streamer.bot](https://streamer.bot)
- [Streamer.bot Client JS](https://streamerbot.github.io/client/)
- [Streamer.Bot](https://streamer.bot)
- [Speaker.Bot](http://speaker.bot/)
- [Streamer.Bot Client JS](https://streamerbot.github.io/client/)
- [TikFinity Desktop App](https://tikfinity.zerody.one/)
- [Font Awesome](https://fontawesome.com/)
- [Animate.css](https://animate.style/)
@@ -78,9 +92,9 @@ ChatRD is a chat overlay widget for OBS that unifies messages and events from **
## **⚠️ DISCLAIMERS ⚠️**
### About YouTube Membership Emojis
I tried to add member emotes but **that is currently impossible due to YouTube's API not exposing Members Emotes and with that, Streamer.bot won't be able to show them.**. So I've added a way for the users to add them manually at the overlay, with the data saved as a Streamer.Bot Global Variable.
I tried to add member emotes but **that is currently impossible due to YouTube's API not exposing Members Emotes and with that, Streamer.Bot won't be able to show them.**. So I've added a way for the users to add them manually at the overlay, with the data saved as a Streamer.Bot Global Variable.
What Casterlabs Caffeinated, Social Stream Ninja and Onecomme do to scrape the emotes won't work with the current way Streamer.bot and my code works, so I had to choose between **making the user add them manually** or build a **server-sided executable (using NodeJS, Python or whatever) to read the chat as it's going or scrape the HTML code**. I don't want to add another executable on top of the user's flow, so it would be easier to use what it's currently available. **And no, I won't do any research based on what other tools do.** Tried to do that and wasted 1 week of my life doing it.
What Casterlabs Caffeinated, Social Stream Ninja and Onecomme do to scrape the emotes won't work with the current way Streamer.Bot and my code works, so I had to choose between **making the user add them manually** or build a **server-sided executable (using NodeJS, Python or whatever) to read the chat as it's going or scrape the HTML code**. I don't want to add another executable on top of the user's flow, so it would be easier to use what it's currently available. **And no, I won't do any research based on what other tools do.** Tried to do that and wasted 1 week of my life doing it.
When YouTube decide to expose their Partner Emotes on their API, I'll come back to this.

View File

@@ -45,13 +45,10 @@
</div>
<script src="https://cdn.jsdelivr.net/npm/dompurify@3.1.5/dist/purify.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/simple-notify@1.0.4/dist/simple-notify.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/@streamerbot/client@1.9.5/dist/streamerbot-client.min.js"></script>
<script src="js/lang/ptbr.js"></script>
<script src="js/lang/en.js"></script>
<script src="js/lang/es.js"></script>
@@ -69,4 +66,4 @@
<script src="js/streamelements/module.js"></script>
</body>
</html>
</html>

View File

@@ -422,3 +422,13 @@ footer a {
justify-content: flex-end;
gap: 1em;
}
#memberemotesbstatus.online i {
color: #00dd63;
text-shadow: 0 0 5px #00dd63;
}
#memberemotesbstatus.offline i {
color: #ff0000;
text-shadow: 0 0 5px #ff0000;
}

View File

@@ -2,7 +2,7 @@
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta name="viewport" c\ontent="width=device-width, initial-scale=1.0">
<title>ChatRD - Config</title>
<link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Inter:wght@100..900">
@@ -22,9 +22,23 @@
<div class="tab-content" id="config">
<h2><i class="fa-solid fa-screwdriver-wrench"></i> Config</h2>
<div class="setting">
<small id="memberemotesbstatus" class="offline" style="display: inline-block; padding: 10px 20px 10px 10px; background-color: #232323; color: #FFF; border-radius: 10px;">
<i class="fa-solid fa-power-off"></i>
<span>Streamer.Bot is Offline!</span>
</small>
</div>
<div class="setting field"><label>Streamer.bot WebSocket Server</label><input type="text" name="streamerBotServerAddress" value="127.0.0.1"></div>
<div class="setting field"><label>Streamer.bot WebSocket Port</label><input type="text" name="streamerBotServerPort" value="8080"></div>
<div class="setting"><label>Run Locally<br><small>In case you want to download ChatRD to run locally or over the network.<br><a target="_blank" href="https://github.com/vortisrd/chatrd?tab=readme-ov-file#%EF%B8%8F-how-to-use-in-a-local-network--como-usar-em-rede-local">Follow the tutorial.</a></small></label><label class="switch"><input type="checkbox" name="runThisLocally"><span class="slider"></span></label></div>
<div class="setting"><label>Text-to-Speech Chat<br><small>Enables TTS for Chat.</small></label><label class="switch"><input type="checkbox" name="ttsSpeakerBotChat"><span class="slider"></span></label></div>
<div class="setting"><label>Text-to-Speech Events<br><small>Enables TTS for Events.</small></label><label class="switch"><input type="checkbox" name="ttsSpeakerBotEvents"><span class="slider"></span></label></div>
<div class="setting"><small style="display: inline-block; padding: 10px 20px 10px 10px; background-color: #232323; color: #FFF; border-radius: 10px;"><i class="fa-solid fa-volume-high"></i> Speaker.Bot is required for Text-to-Speech! <strong><a target="_blank" href="https://github.com/vortisrd/chatrd?tab=readme-ov-file#%EF%B8%8F-how-to-use-in-a-local-network--como-usar-em-rede-local">Follow the tutorial</a>.</strong></small></label></div>
<div class="setting"><label>Run Locally<br><small>In case you want to download ChatRD to run locally or over the network.<br><strong><a target="_blank" href="https://github.com/vortisrd/chatrd?tab=readme-ov-file#%EF%B8%8F-how-to-use-in-a-local-network--como-usar-em-rede-local">Follow the tutorial</a></strong>.</small></label><label class="switch"><input type="checkbox" name="runThisLocally"><span class="slider"></span></label></div>
</div>
<div class="tab-content" id="geral">
@@ -89,8 +103,16 @@
<div class="setting"><label><i class="fa-solid fa-arrow-turn-up"></i> Gifted Membership<br><small>Shows the person gifting members.</small></label><label class="switch"><input type="checkbox" name="showYouTubeGiftMemberships" checked><span class="slider"></span></label></div>
<div class="setting"><label><i class="fa-solid fa-arrow-turn-up"></i> Gifted Membership Train<br><small>Shows all users that were gifted a membership on a massive gifting membership.</small></label><label class="switch"><input type="checkbox" name="showYouTubeMembershipsTrain" checked><span class="slider"></span></label></div>
<div class="setting"><label>Statistics<br><small>Shows viewers and likes.</small></label><label class="switch"><input type="checkbox" name="showYouTubeStatistics" checked><span class="slider"></span></label></div>
<div class="setting column">
<label>Members Only Emotes<br><small>Because YouTube's API doesn't expose Partner Custom Emotes. <em>Really YouTube!?</em> 🤨<br><em id="memberemotesbstatus" style="font-style: normal; font-weight: bold; color: #ff0000;">Streamer.Bot Needs to be Online!</em></small></label>
<label>
Members Only Emotes
<br>
<small>Because YouTube's API doesn't expose Partner Custom Emotes. <em>Really YouTube!?</em> 🤨
</small>
</label>
<div class="emote-list">
<span class="emote-item">

View File

@@ -4,6 +4,10 @@
const streamerBotServerAddress = getURLParam("streamerBotServerAddress", "127.0.0.1");
const streamerBotServerPort = getURLParam("streamerBotServerPort", "8080");
const ttsSpeakerBotChat = getURLParam("ttsSpeakerBotChat", false);
const ttsSpeakerBotEvents = getURLParam("ttsSpeakerBotEvents", false);
let streamerBotConnected = false;
const chatThreshhold = 50;
@@ -75,6 +79,8 @@ const streamerBotClient = new StreamerbotClient({
async function addMessageToChat(userID, messageID, platform, data) {
if (ttsSpeakerBotChat == true) { ttsSpeakerBotSays(data.userName, currentLang.ttschat, data.message); }
const html = DOMPurify.sanitize(`
<div id="${messageID}" data-user="${userID}" class="${platform} ${data.classes} message" style="">
@@ -121,6 +127,8 @@ async function addMessageToChat(userID, messageID, platform, data) {
async function addEventToChat(userID, messageID, platform, data) {
if (ttsSpeakerBotEvents == true) { ttsSpeakerBotSays(data.userName, '', data.message); }
const html = DOMPurify.sanitize(`
<div id="${messageID}" data-user="${userID}" class="${platform} ${data.classes} message event" style="">
<div class="animate__animated ${chatHorizontal == true ? 'animate__fadeInRight' : 'animate__fadeInUp'} animate__faster">
@@ -149,7 +157,7 @@ async function addEventToChat(userID, messageID, platform, data) {
}, 1000);
}, Math.floor(hideAfter * 1000));
}
removeExtraChatMessages();
}
@@ -298,7 +306,42 @@ const notifySuccess = (success) => {
}
function escapeRegex(string) {
return string.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
}
async function ttsSpeakerBotSays(user, action, message) {
if (streamerBotConnected == false) return;
const ttstext = await cleanStringOfHTMLButEmotes(message);
const ttsmessage = user+' '+action+' '+ttstext;
streamerBotClient.doAction(
{ name : "TTS Event" },
{
"ttsmessage": ttsmessage,
}
).then( (ttsstuff) => {
console.debug('Sending TTS to Streamer.Bot', ttsstuff);
});
}
async function cleanStringOfHTMLButEmotes(string) {
// Cria um elemento DOM temporário
const container = document.createElement('div');
container.innerHTML = string;
// Substitui <img class="emote" alt="..."> por texto do alt
const emotes = container.querySelectorAll('img.emote[alt]');
emotes.forEach(img => {
const altText = img.getAttribute('alt');
const textNode = document.createTextNode(altText);
img.replaceWith(textNode);
});
// Remove todo o restante do HTML
return container.textContent || "";
}

View File

@@ -1,6 +1,7 @@
const en = {
streamerbotconnected: 'Streamer.bot Online!',
streamerbotdisconnected: 'Streamer.bot Disconnected!',
ttschat: 'said',
twitch : {
firstMessage : () => `First chatter`,

View File

@@ -1,6 +1,7 @@
const es = {
streamerbotconnected: '¡Streamer.bot en línea!',
streamerbotdisconnected: '¡Streamer.bot desconectado!',
ttschat: 'dijo',
twitch : {
firstMessage : () => `Primeira mensaje`,

View File

@@ -1,6 +1,7 @@
const ptbr = {
streamerbotconnected: 'Streamer.bot Conectado!',
streamerbotdisconnected: 'Streamer.bot Desconectado!',
ttschat: 'disse',
twitch : {
firstMessage : () => `Primeira mensagem`,

View File

@@ -77,10 +77,9 @@ async function loadSettingsFromLocalStorage() {
onConnect: (data) => {
streamerBotConnected = true;
var sbstatus = document.getElementById('memberemotesbstatus');
sbstatus.style.color = '#00dd63';
sbstatus.textContent = 'Streamer.Bot is Online!';
document.querySelector('#memberemotesbstatus').classList.remove('offline');
document.querySelector('#memberemotesbstatus').classList.add('online');
document.querySelector('#memberemotesbstatus span').textContent = 'Streamer.Bot is Online!';
streamerBotClient.getGlobals().then( (getglobals) => {
const settings = JSON.parse(getglobals.variables.chatrdytcustomemotes.value);
@@ -96,10 +95,10 @@ async function loadSettingsFromLocalStorage() {
console.error('Streamer.bot Disconnected!');
streamerBotConnected = false;
var sbstatus = document.getElementById('memberemotesbstatus');
sbstatus.style.color = '#ff0000';
sbstatus.textContent = 'Streamer.Bot Needs to be Online!';
document.querySelector('#memberemotesbstatus').classList.remove('online');
document.querySelector('#memberemotesbstatus').classList.add('offline');
document.querySelector('#memberemotesbstatus span').textContent = 'Streamer.Bot is Offline!';
}
});

View File

@@ -515,7 +515,6 @@ async function twitchChatClearMessages() {
async function twitchUpdateStatistics(data) {
if (showPlatformStatistics == false || showTwitchViewers == false) return;
const viewers = DOMPurify.sanitize(data.viewerCount);

View File

@@ -353,7 +353,7 @@ async function getYouTubeEmotes(data) {
const emoteRegex = new RegExp(`(?<!\\S)${escapedCode}(?!\\S)`, 'g');
const imageUrl = `https://cdn.betterttv.net/emote/${emote.id}/1x`;
const emoteElement = `<img src="${imageUrl}" class="emote">`;
const emoteElement = `<img src="${imageUrl}" class="emote" alt="${escapedCode}">`;
message = message.replace(emoteRegex, emoteElement);
}
@@ -362,7 +362,7 @@ async function getYouTubeEmotes(data) {
if (data.emotes) {
for (const emote of data.emotes) {
const emoteRegex = new RegExp(escapeRegex(emote.name), 'g');
const emoteElement = `<img src="${emote.imageUrl}" class="emote">`;
const emoteElement = `<img src="${emote.imageUrl}" class="emote" alt="${emote.name}">`;
message = message.replace(emoteRegex, emoteElement);
}
}
@@ -372,7 +372,7 @@ async function getYouTubeEmotes(data) {
if (data.user.isSponsor == true || data.user.isOwner == true) {
message = message.replace(/:([a-zA-Z0-9_]+):/g, (match, emoteName) => {
if (youTubeCustomEmotes[emoteName]) {
return `<img src="${youTubeCustomEmotes[emoteName]}" class="emote">`;
return `<img src="${youTubeCustomEmotes[emoteName]}" class="emote" alt="${emoteName}">`;
}
return match;
});

View File

@@ -1 +1 @@
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