├── 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 | ![Emote Combo](https://i.imgur.com/gOETm6Z.gif) 25 | 26 | ## Show Emote: 27 | 28 | ![Show Emote](https://i.imgur.com/987NJzD.gif) 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 |
8 |
9 |
10 |
11 |
12 |
13 |
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 | --------------------------------------------------------------------------------