├── README.md
├── index.html
├── license.txt
├── main.css
└── main.js
/README.md:
--------------------------------------------------------------------------------
1 | # Overview:
2 |
3 | This project is an overlay that shows emote streaks on the bottom left of the page.
4 | It can also show emotes randomly on screen if a chatter does !showemote (*emote_name*)
5 | *This overlay can be used in streaming software like OBS*
6 | The emotes are taken from Twitch, FFZ, BTTV, and 7TV.
7 |
8 | This project took direct inspiration from pajlada's pajbot, although I believe my version is easier to setup and use.
9 |
10 | ---
11 |
12 | # Live Version:
13 | You can put this URL into your streaming software and use it! \
14 | Please scroll further down to see all the settings that you can tweak.
15 |
16 | ### https://api.roaringiron.com/emoteoverlay?channel=forsen
17 |
18 | ---
19 |
20 | # Examples:
21 |
22 | ## Emote Combo:
23 |
24 | 
25 |
26 | ## Show Emote:
27 |
28 | 
29 |
30 | ---
31 |
32 | # Usage & Available Parameters/Settings:
33 |
34 | To use these parameters, add them after the url with this format: "&(parameter)=(value)"
35 | For example, if I wanted to add the "minStreak" and the "showEmoteSizeMultiplier" parameter, my new URL would be "https://api.roaringiron.com/emoteoverlay?channel=forsen&minStreak=10&showEmoteSizeMultiplier=3"
36 |
37 | #### REQUIRED PARAMETERS:
38 | - channel=(channel name)
39 |
40 | #### OPTIONAL PARAMETERS:
41 | - minStreak=*(number)*
42 | - Minimum emote streak needed to show up in overlay
43 | - Defaults to 5 - Minimum value allowed is 3
44 | - showEmoteEnabled=*(1 for enabled, 0 for disabled)*
45 | - Enable or disable the show emote module
46 | - Defaults to 1 (enabled)
47 | - streakEnabled=*(1 for enabled, 0 for disabled)*
48 | - Enable or disable the emote streak module
49 | - Defaults to 1 (enabled)
50 | - showEmoteSizeMultiplier=*(multipler)*
51 | - Changes the size of the show emotes by the number provided
52 | - Defaults to 2
53 | - showEmoteCooldown=*(seconds)*
54 | - Cooldown in seconds between usage of !showemote command
55 | - Defaults to 5
56 | - emoteStreakText=*(text (without quotes))*
57 | - Sets the ending text for the emote streak
58 | - For no text, add an empty `emoteStreakText=` to the end of the URL
59 | - Defaults to `Streak!`
60 | - emoteLocation=*(1 for bottom left, 2 for top left, 3 for top right, 4 for bottom right)*
61 | - Sets the emoteStreak position
62 | - Defaults to 1
63 | ---
64 |
65 | ## Development
66 |
67 | I welcome all developers to open a pull request or ticket with any new features or changes you'd like to see!
68 |
--------------------------------------------------------------------------------
/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
11 |
14 |
--------------------------------------------------------------------------------
/license.txt:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2020 Jahaan Jain
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
--------------------------------------------------------------------------------
/main.css:
--------------------------------------------------------------------------------
1 | body {
2 | margin: 0;
3 | }
4 | #world {
5 | color: #ffffff;
6 | font-size: 25px;
7 | font-family: "Comic Sans MS", cursive, sans-serif;
8 | text-shadow: -1.5px -1.5px 0 #000, 1.5px -1.5px 0 #000, -1.5px 1.5px 0 #000, 1.5px 1.5px 0 #000;
9 | }
10 | #showEmote {
11 | position: absolute;
12 | width: 100%;
13 | height: 100%;
14 |
15 | display: grid;
16 | grid-template-areas: ". . ." ". img ." ". . .";
17 | }
18 | #showEmote img {
19 | grid-area: img;
20 | }
21 |
--------------------------------------------------------------------------------
/main.js:
--------------------------------------------------------------------------------
1 | const url = new URL(window.location.href);
2 |
3 | const config = {
4 | channel: url.searchParams.get("channel"),
5 | currentStreak: { streak: 1, emote: "", url: "" },
6 | streakEnabled: !!Number(url.searchParams.get("streakEnabled") || 1),
7 | showEmoteEnabled: !!Number(url.searchParams.get("showEmoteEnabled") || 1),
8 | showEmoteCooldown: Number(url.searchParams.get("showEmoteCooldown") || 6),
9 | showEmoteSizeMultiplier: Number(
10 | url.searchParams.get("showEmoteSizeMultiplier") || 1
11 | ),
12 | minStreak: Number(url.searchParams.get("minStreak") || 5),
13 | emoteLocation: Number(url.searchParams.get("emoteLocation") || 1),
14 | emoteStreakEndingText:
15 | url.searchParams.get("emoteStreakText")?.replace(/(<([^>]+)>)/gi, "") ??
16 | "streak!",
17 | showEmoteCooldownRef: new Date(),
18 | streakCooldown: new Date().getTime(),
19 | emotes: [],
20 | };
21 |
22 | const getEmotes = async () => {
23 | // const proxy = "https://tpbcors.herokuapp.com/";
24 | const proxy = "https://api.roaringiron.com/proxy/";
25 | console.log(config);
26 |
27 | if (!config.channel)
28 | return $("#errors").html(
29 | `Invalid channel. Please enter a channel name in the URL. Example: https://api.roaringiron.com/emoteoverlay?channel=forsen`
30 | );
31 |
32 | const twitchId = (
33 | await (
34 | await fetch(
35 | proxy + "https://api.ivr.fi/v2/twitch/user?login=" + config.channel,
36 | {
37 | headers: { "User-Agent": "api.roaringiron.com/emoteoverlay" },
38 | }
39 | )
40 | ).json()
41 | )?.[0].id;
42 |
43 | await (
44 | await fetch(
45 | proxy + "https://api.frankerfacez.com/v1/room/" + config.channel
46 | )
47 | )
48 | .json()
49 | .then((data) => {
50 | const emoteNames = Object.keys(data.sets);
51 | for (let i = 0; i < emoteNames.length; i++) {
52 | for (let j = 0; j < data.sets[emoteNames[i]].emoticons.length; j++) {
53 | const emote = data.sets[emoteNames[i]].emoticons[j];
54 | config.emotes.push({
55 | name: emote.name,
56 | url:
57 | "https://" +
58 | (emote.urls["2"] || emote.urls["1"]).split("//").pop(),
59 | });
60 | }
61 | }
62 | })
63 | .catch(console.error);
64 |
65 | await (
66 | await fetch(proxy + "https://api.frankerfacez.com/v1/set/global")
67 | )
68 | .json()
69 | .then((data) => {
70 | const emoteNames = Object.keys(data.sets);
71 | for (let i = 0; i < emoteNames.length; i++) {
72 | for (let j = 0; j < data.sets[emoteNames[i]].emoticons.length; j++) {
73 | const emote = data.sets[emoteNames[i]].emoticons[j];
74 | config.emotes.push({
75 | name: emote.name,
76 | url:
77 | "https://" +
78 | (emote.urls["2"] || emote.urls["1"]).split("//").pop(),
79 | });
80 | }
81 | }
82 | })
83 | .catch(console.error);
84 |
85 | await (
86 | await fetch(
87 | proxy + "https://api.betterttv.net/3/cached/users/twitch/" + twitchId
88 | )
89 | )
90 | .json()
91 | .then((data) => {
92 | for (let i = 0; i < data.channelEmotes.length; i++) {
93 | config.emotes.push({
94 | name: data.channelEmotes[i].code,
95 | url: `https://cdn.betterttv.net/emote/${data.channelEmotes[i].id}/2x`,
96 | });
97 | }
98 | for (let i = 0; i < data.sharedEmotes.length; i++) {
99 | config.emotes.push({
100 | name: data.sharedEmotes[i].code,
101 | url: `https://cdn.betterttv.net/emote/${data.sharedEmotes[i].id}/2x`,
102 | });
103 | }
104 | })
105 | .catch(console.error);
106 |
107 | await (
108 | await fetch(proxy + "https://api.betterttv.net/3/cached/emotes/global")
109 | )
110 | .json()
111 | .then((data) => {
112 | for (let i = 0; i < data.length; i++) {
113 | config.emotes.push({
114 | name: data[i].code,
115 | url: `https://cdn.betterttv.net/emote/${data[i].id}/2x`,
116 | });
117 | }
118 | })
119 | .catch(console.error);
120 |
121 | await (
122 | await fetch(proxy + "https://7tv.io/v3/emote-sets/global")
123 | )
124 | .json()
125 | .then((data) => {
126 | for (let i = 0; i < data.emotes.length; i++) {
127 | config.emotes.push({
128 | name: data.emotes[i].name,
129 | url: `https://cdn.7tv.app/emote/${data.emotes[i].id}/2x.webp`,
130 | });
131 | }
132 | })
133 | .catch(console.error);
134 |
135 | await (
136 | await fetch(proxy + "https://7tv.io/v3/users/twitch/" + twitchId)
137 | )
138 | .json()
139 | .then((data) => {
140 | const emoteSet = data["emote_set"];
141 | if (emoteSet === null) return;
142 | const emotes = emoteSet["emotes"];
143 | for (let i = 0; i < emotes.length; i++) {
144 | config.emotes.push({
145 | name: emotes[i].name,
146 | url:
147 | "https:" +
148 | emotes[i].data.host.url +
149 | "/" +
150 | emotes[i].data.host.files[2].name,
151 | });
152 | }
153 | })
154 | .catch(console.error);
155 |
156 | const successMessage = `Successfully loaded ${config.emotes.length} emotes for channel ${config.channel}`;
157 |
158 | $("#errors").html(successMessage).delay(2000).fadeOut(300);
159 | console.log(successMessage, config.emotes);
160 | };
161 |
162 | const findEmoteInMessage = (message) => {
163 | for (const emote of config.emotes.map((a) => a.name)) {
164 | if (message.includes(emote)) {
165 | return emote;
166 | }
167 | }
168 | return null;
169 | };
170 |
171 | const findUrlInEmotes = (emote) => {
172 | for (const emoteObj of config.emotes) {
173 | if (emoteObj.name === emote) {
174 | return emoteObj.url;
175 | }
176 | }
177 | return null;
178 | };
179 |
180 | const showEmote = (message, rawMessage) => {
181 | if (config.showEmoteEnabled) {
182 | const emoteUsedPos = rawMessage[4].startsWith("emotes=") ? 4 : 5;
183 | const emoteUsed = rawMessage[emoteUsedPos].split("emotes=").pop();
184 | const splitMessage = message.split(" ");
185 |
186 | if (emoteUsed.length === 0) {
187 | const url = findUrlInEmotes(findEmoteInMessage(splitMessage));
188 | if (url) return showEmoteEvent(url);
189 | } else {
190 | const url = `https://static-cdn.jtvnw.net/emoticons/v2/${
191 | emoteUsed.split(":")[0]
192 | }/default/dark/2.0`;
193 | return showEmoteEvent(url);
194 | }
195 | }
196 | };
197 |
198 | const findEmotes = (message, rawMessage) => {
199 | if (config.emotes.length === 0) return;
200 |
201 | const emoteUsedPos = rawMessage[4].startsWith("emotes=")
202 | ? 4
203 | : rawMessage[5].startsWith("emote-only=")
204 | ? 6
205 | : 5;
206 | const emoteUsed = rawMessage[emoteUsedPos].split("emotes=").pop();
207 | const splitMessage = message.split(" ").filter((a) => !!a.length);
208 |
209 | if (splitMessage.includes(config.currentStreak.emote))
210 | config.currentStreak.streak++;
211 | else if (
212 | rawMessage[emoteUsedPos].startsWith("emotes=") &&
213 | emoteUsed.length > 1
214 | ) {
215 | config.currentStreak.streak = 1;
216 | config.currentStreak.emote = message.substring(
217 | parseInt(emoteUsed.split(":")[1].split("-")[0]),
218 | parseInt(emoteUsed.split(":")[1].split("-")[1]) + 1
219 | );
220 | config.currentStreak.url = `https://static-cdn.jtvnw.net/emoticons/v2/${
221 | emoteUsed.split(":")[0]
222 | }/default/dark/2.0`;
223 | } else {
224 | config.currentStreak.streak = 1;
225 | config.currentStreak.emote = findEmoteInMessage(splitMessage);
226 | config.currentStreak.url = findUrlInEmotes(config.currentStreak.emote);
227 | }
228 |
229 | streakEvent();
230 | };
231 |
232 | const streakEvent = () => {
233 | if (config.currentStreak.streak >= config.minStreak && config.streakEnabled) {
234 | $("#main").empty();
235 | $("#main").css("position", "absolute");
236 |
237 | switch (config.emoteLocation) {
238 | default:
239 | case 1:
240 | $("#main").css("top", "600");
241 | $("#main").css("left", "35");
242 | break;
243 | case 2:
244 | $("#main").css("bottom", "600");
245 | $("#main").css("left", "35");
246 | break;
247 | case 3:
248 | $("#main").css("bottom", "600");
249 | $("#main").css("right", "35");
250 | break;
251 | case 4:
252 | $("#main").css("top", "600");
253 | $("#main").css("right", "35");
254 | break;
255 | }
256 |
257 | $(" ", { src: config.currentStreak.url }).appendTo("#main");
258 | $("#main")
259 | .append(
260 | " x" +
261 | config.currentStreak.streak +
262 | " " +
263 | config.emoteStreakEndingText
264 | )
265 | .appendTo("#main");
266 |
267 | gsap.to("#main", 0.15, {
268 | scaleX: 1.2,
269 | scaleY: 1.2,
270 | onComplete: () => gsap.to("#main", 0.15, { scaleX: 1, scaleY: 1 }),
271 | });
272 |
273 | config.streakCooldown = new Date().getTime();
274 | setInterval(() => {
275 | if ((new Date().getTime() - config.streakCooldown) / 1000 > 4) {
276 | config.streakCooldown = new Date().getTime();
277 | gsap.to("#main", 0.2, {
278 | scaleX: 0,
279 | scaleY: 0,
280 | delay: 0.5,
281 | onComplete: () => (config.streakCooldown = new Date().getTime()),
282 | });
283 | }
284 | }, 1000);
285 | }
286 | };
287 |
288 | const getRandomPosPercent = () => [
289 | Math.floor(Math.random() * 100),
290 | Math.floor(Math.random() * 100),
291 | ];
292 |
293 | const showEmoteEvent = (url) => {
294 | const secondsDiff =
295 | (new Date().getTime() - new Date(config.showEmoteCooldownRef).getTime()) /
296 | 1000;
297 |
298 | if (secondsDiff > config.showEmoteCooldown) {
299 | config.showEmoteCooldownRef = new Date();
300 |
301 | $("#showEmote").empty();
302 | const [x, y] = getRandomPosPercent();
303 | const emoteEl = $("#showEmote");
304 |
305 | emoteEl.css({
306 | position: "absolute", // Ensure the parent container has position: relative
307 | left: `${x}%`,
308 | top: `${y}%`,
309 | transform: `translate(-50%, -50%)`, // Center the emote based on its own dimensions
310 | });
311 |
312 | $(" ", {
313 | src: url,
314 | style: `transform: scale(${config.showEmoteSizeMultiplier});`,
315 | }).appendTo(emoteEl);
316 |
317 | gsap.to("#showEmote", 1, {
318 | autoAlpha: 1,
319 | onComplete: () =>
320 | gsap.to("#showEmote", 1, {
321 | autoAlpha: 0,
322 | delay: 4,
323 | onComplete: () => $("#showEmote").empty(),
324 | }),
325 | });
326 | }
327 | };
328 |
329 | const connect = () => {
330 | const chat = new WebSocket("wss://irc-ws.chat.twitch.tv");
331 | const timeout = setTimeout(() => {
332 | chat.close();
333 | chat.connect();
334 | }, 10000);
335 |
336 | chat.onopen = function () {
337 | clearInterval(timeout);
338 | chat.send(
339 | "CAP REQ :twitch.tv/tags twitch.tv/commands twitch.tv/membership"
340 | );
341 | chat.send("PASS oauth:xd123");
342 | chat.send("NICK justinfan123");
343 | chat.send("JOIN #" + config.channel);
344 | console.log("Connected to Twitch IRC");
345 | getEmotes();
346 | };
347 |
348 | chat.onerror = function () {
349 | console.error("There was an error.. disconnected from the IRC");
350 | chat.close();
351 | chat.connect();
352 | };
353 |
354 | chat.onmessage = function (event) {
355 | const usedMessage = event.data.split(/\r\n/)[0];
356 | const textStart = usedMessage.indexOf(` `); // tag part ends at the first space
357 | const fullMessage = usedMessage.slice(0, textStart).split(`;`); // gets the tag part and splits the tags
358 | fullMessage.push(usedMessage.slice(textStart + 1));
359 |
360 | if (fullMessage.length > 13) {
361 | const parsedMessage = fullMessage[fullMessage.length - 1]
362 | .split(`${config.channel} :`)
363 | .pop(); // gets the raw message
364 | let message = parsedMessage.split(" ").includes("ACTION")
365 | ? parsedMessage.split("ACTION ").pop().split("")[0]
366 | : parsedMessage; // checks for the /me ACTION usage and gets the specific message
367 | if (
368 | message.toLowerCase().startsWith("!showemote") ||
369 | message.toLowerCase().startsWith("!#showemote")
370 | ) {
371 | showEmote(message, fullMessage);
372 | }
373 | findEmotes(message, fullMessage);
374 | }
375 | if (fullMessage.length == 2 && fullMessage[0].startsWith("PING")) {
376 | console.log("sending pong");
377 | chat.send("PONG");
378 | }
379 | };
380 | };
381 |
--------------------------------------------------------------------------------