├── .gitignore ├── channels.txt ├── package.json ├── LICENCE ├── config.example.js ├── color.js ├── README.md └── util.js /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | config.js 3 | color-channels.json 4 | -------------------------------------------------------------------------------- /channels.txt: -------------------------------------------------------------------------------- 1 | # Write your channel names here 2 | # Lines starting with a '#' are ignored, as well as blank lines. 3 | # The script will also add any channels here, 4 | # when you use the 'addColor' command. 5 | 6 | # The script will always join your channel. 7 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "twitch-chat-colorchanger", 3 | "version": "1.0.0", 4 | "description": "Changes your chat color on twitch.tv to random colors, every so often.", 5 | "main": "color.js", 6 | "scripts": { 7 | "test": "echo \"Error: no test specified\" && exit 1", 8 | "start": "node ." 9 | }, 10 | "repository": { 11 | "type": "git", 12 | "url": "git+https://github.com/notnotquinn/twitch-chat-colorchanger.git" 13 | }, 14 | "keywords": [ 15 | "twitch", 16 | "bot", 17 | "irc" 18 | ], 19 | "author": "Quinn T", 20 | "license": "MIT", 21 | "bugs": { 22 | "url": "https://github.com/notnotquinn/twitch-chat-colorchanger/issues" 23 | }, 24 | "homepage": "https://github.com/notnotquinn/twitch-chat-colorchanger#readme", 25 | "dependencies": { 26 | "color": "^3.1.3", 27 | "dank-twitch-irc": "^4.3.0", 28 | "pm2": "^5.1.0" 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /LICENCE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright © 2021 Quinn T 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. -------------------------------------------------------------------------------- /config.example.js: -------------------------------------------------------------------------------- 1 | // configuration 2 | 3 | // EDITING THIS FILE DOES NOTHING, 4 | // copy this file to a new file called "config.js" 5 | // and then the changes you make there will take effect. 6 | 7 | module.exports = { 8 | 9 | // this is your username, it must be in lowercase 10 | username: "example_user", 11 | 12 | // this is an oauth token to login to chat, you can get one here: https://twitchapps.com/tmi/ (you dont need the "oauth:" part) 13 | oauth: "xxxxxxxxxxxxxxxxxxxxxx", 14 | 15 | // this is how often the script will send a new color, you have to keep this in twitch ratelimits. 16 | // See https://dev.twitch.tv/docs/irc/guide#command--message-limits for more information 17 | // just dont set it super low basicly, 10 seconds is as low as I would go. 18 | // just keep in mind that if you plan on spamming, you can only spam up to 20 messages per 30 seconds without mod/vip 19 | // and this will be part of that. (if you go over you wont be able to send any messages for about 30 mins) 20 | seconds: 15, 21 | 22 | // change to true to have ANY color (needs twitch prime or turbo) 23 | usePrimeColors: false, 24 | 25 | // This will only work if hasPrime is true 26 | // it will go through all colors in the rainbow, in order. 27 | useRainbow: false, 28 | 29 | // this is by how much the hue will change every color 30 | // negative numbers will go through the rainbow backwords. 31 | // at this speed, it will be very slow, almost unnoticable. 32 | // but over a few mins it will look cool. 33 | rainbowSpeed: 1, 34 | 35 | // This is for if you want to start the rainbow in the middle or something. 36 | // it will increace the hue by this ammount before the first color 37 | rainbowStartHue: 0, 38 | 39 | // Works really well with rainbow 40 | // If this is true it will only change your color when you send a message 41 | // it only checks in the channels you have added though, so dont expect it to work site wide. 42 | // this is not something we can improve. 43 | onlyChangeColorOnMessageSent: false, 44 | 45 | 46 | // Transitions between each color by converting each to HSL (hue, saturation, lightness) 47 | // And finding the target color's HSL, then slowly transitioning all 3. 48 | // (active when not empty and using rainbow) 49 | colorList: [ 50 | // "#B1FCDF", 51 | // "#8C7F7F", 52 | // "#FFFF00", 53 | ] 54 | } 55 | -------------------------------------------------------------------------------- /color.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | // Imports 3 | const config = require("./config"); 4 | const util = require("./util") 5 | const Color = require('color'); 6 | 7 | let channels = util.getChannels(config) 8 | 9 | // Initilization 10 | let rainbowColor = Color('hsl(0, 60%, 50%)'); 11 | rainbowColor = rainbowColor.rotate(config.rainbowStartHue); 12 | const client = util.getClient({ 13 | username: config.username, 14 | password: config.oauth 15 | }, () => util.showInfo(config)); 16 | let colors_sent = 0; 17 | const nextTransitionalColor = util.getTransitionColorGetter(config) 18 | 19 | // 2021-07-20: QuinnDT: This is a mess......... 20 | // This whole code is so bad. 21 | // But it works eShrug 22 | 23 | // 2021-07-24: QuinnDT: Much better, but still sorta a mess. 24 | // Glad I made './util.js' tho, made it a lot easier. 25 | 26 | function randomHex() { 27 | let out = "#"; 28 | const hexLetters = "0123456789abcdef" 29 | 30 | for(i = 0;i < 6;i++) { 31 | // chooses one random letter 6 times 32 | out += hexLetters[util.randInt(hexLetters.length)] 33 | } 34 | 35 | return out; 36 | } 37 | 38 | function rainbowHex() { 39 | rainbowColor = rainbowColor.rotate(config.rainbowSpeed); // increaces the hue by the speed, think of it like rotating it on the color wheel. 40 | 41 | return rainbowColor.hex() 42 | } 43 | 44 | 45 | function nonPrimeColor() { 46 | let out = ""; 47 | const primeColors = ["Blue", "BlueViolet", "CadetBlue", "Chocolate", "Coral", "DodgerBlue", "Firebrick", "GoldenRod", "Green", "HotPink", "OrangeRed", "Red", "SeaGreen", "SpringGreen", "YellowGreen"]; 48 | 49 | // just pick one at random 50 | out += primeColors[util.randInt(primeColors.length)]; 51 | 52 | return out; 53 | } 54 | 55 | 56 | function updateColor() { 57 | if(colors_sent % 10 == 0) { 58 | // every 10th color. 59 | util.showInfo(config) 60 | } 61 | colors_sent++; 62 | let color = ""; 63 | if(config.usePrimeColors) { 64 | if ( config.useRainbow ) { 65 | if ((config.colorList?.length ?? 0) > 0) { 66 | color = nextTransitionalColor(); 67 | if (!color) throw new Error(`got invalid value as a color: ${color}`) 68 | } else color = rainbowHex(); 69 | } else { 70 | color = randomHex() 71 | } 72 | } else { 73 | color = nonPrimeColor() 74 | } 75 | if (client.connections.length > 0) { 76 | util.log('color', color) 77 | client.privmsg(config.username, `/color ${color}`); 78 | } else { 79 | util.log('color', 'Did not update color because client not connected.') 80 | } 81 | } 82 | 83 | // only do it every ammount of seconds 84 | if (!config.onlyChangeColorOnMessageSent) { 85 | setInterval(updateColor, config.seconds * 1000); 86 | } 87 | 88 | util.log('THANKS', `Thanks for using my colorchanger, inspired by turtoise's version.`) 89 | util.log('INFO', `Connecting...`) 90 | client.connect() 91 | if (config.onlyChangeColorOnMessageSent) { 92 | 93 | let anonClient = util.getAnonClient(client, config, channels, updateColor) 94 | 95 | // only join if were going to use them. 96 | // because the anon client will just check every message if its you. 97 | anonClient.connect() 98 | anonClient.joinAll(channels); 99 | } 100 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # This script won't work after 24 Feb 2023, because *twitch*. 2 | 3 | I won't be updating it to the new API, see [my comment on issue #3](https://github.com/NotNotQuinn/twitch-chat-colorchanger/issues/3#issuecomment-1435914185). 4 | 5 | --- 6 | 7 | Original description: 8 | 9 | > # twitch-chat-colorchanger 10 | > Changes your chat color on twitch.tv to random colors, every so often, or optionally in a rainbow! 11 | > 12 | > ## Installation 13 | > 1. Make sure you have NodeJS installed, you can install it here: https://nodejs.org 14 | > 2. Either clone the repository if you have git installed, or download the zip file of the code. 15 | > ![Image showing download button](https://i.imgur.com/ztyR5Mb.png) 16 | > 2. Open a terminal in the directory the code is in. 17 | > 3. Run `npm i` in the command line to install dependencies. 18 | > 4. Copy the `config.example.js` file to a file called `config.js` 19 | > 5. Edit the contents of the config file to hold your information, there is instructions in the file. 20 | > 5. Register a chat bot application on https://dev.twitch.tv (top right) 21 | > - For the redirect url, use https://localhost/ 22 | > - ![Image showing application registration screen](https://i.imgur.com/yjnI23y.png ) 23 | > 6. Run `node .` in the command line to start the program. 24 | > 25 | > ## Configuration 26 | > - `username` 27 | > - This is your username, it must be in lowercase 28 | > - This must be a `string` 29 | > - `oauth` 30 | > - This is an oauth token to login to chat, you can get one here: https://twitchapps.com/tmi/ (you dont need the "oauth:" part) 31 | > - This must be a `string` 32 | > - `seconds` 33 | > - This is how often the script will send a new color, you have to keep this in twitch ratelimits. See https://dev.twitch.tv/docs/irc/guide#command--message-limits for more information. 34 | > - Just dont set it super low basicly, 10 seconds is as low as I would go. Its good to keep in mind that if you plan on spamming, you can only spam up to 20 messages per 30 seconds without mod/vip and this will be part of that. (if you go over you wont be able to send any messages in channels you arent a sub/mod/vip in for about 30 mins) 35 | > - This must be a `number` 36 | > - `usePrimeColors` 37 | > - If this is set to `true`, it will use prime colors, otherwise it uses the basic twitch colors. 38 | > - Requires Prime Gaming, or Twitch Turbo, they both give access to extended colors. 39 | > - This must be a `bool` (`true` or `false`) 40 | > - `useRainbow` 41 | > - Does NOTHING if `usePrimeColors` is `false` 42 | > - If this is set to `true`, it will send a color from the rainbow and go through it. 43 | > - This must be a `bool` (`true` or `false`) 44 | > - `rainbowSpeed` 45 | > - This is how much the hue of the rainbow changes every color. 46 | > - Negative numbers will go through the rainbow backwords. 47 | > - This must be a `number` 48 | > - `rainbowStartHue` 49 | > - This is an offset added to the color's hue before the first color is sent. 50 | > - This must be a `number` 51 | > - `onlyChangeColorOnMessageSent` 52 | > - If this is `true` your color will only change when you send a message, but the script will only see messages in the list of channels to check, seen below. 53 | > - This must be a `bool` (`true` or `false`) 54 | > 55 | > Also to note: 56 | > - `channels.txt` 57 | > - every line in this file will be a channel that the script joins (if you have it set up that way.) Lines starting with a '#' are ignored, and so are empty lines. 58 | > 59 | > ## Use 60 | > 61 | > ### Chat commands 62 | > 63 | > This script has a few chat commands. They will only respond in your channel to avoid "adding bots" to peoples chats that they dont want. Only you can use these commands. The commands: 64 | > 65 | > - `"toggleColor"` 66 | > - Turn color on/off 67 | > - `"addColor"` 68 | > - adds the channel to the channels that change color 69 | > for example: "`addColor justin`" will add `justin` to the channels text file and join it. 70 | > 71 | -------------------------------------------------------------------------------- /util.js: -------------------------------------------------------------------------------- 1 | // Imports 2 | const Color = require("color"); 3 | const DankTwitch = require("dank-twitch-irc"); 4 | const fs = require("fs"); 5 | 6 | 7 | // Util functions 8 | /** 9 | * Logs things. 10 | * @param {string} level The level to show on screen 11 | * @param {any} thing Thing to log 12 | */ 13 | const log = (level, thing) => { 14 | console.log(`${new Date().toLocaleTimeString()} | ${level.toUpperCase()} -`, thing) 15 | } 16 | 17 | /** 18 | * Gets twitch client. 19 | * @param {object} config Config to pass to client. 20 | * @param {() => void} ShowSomeInfo Function to show info. (??) 21 | */ 22 | const getClient = (config, ShowSomeInfo) => { 23 | let client = new DankTwitch.ChatClient(config) 24 | 25 | client.on("ready", () => { 26 | log("info", "Connected to chat.") 27 | ShowSomeInfo() 28 | }) 29 | return client 30 | } 31 | 32 | /** 33 | * Get channels from the file. 34 | * @param {any} config The config. 35 | * @returns {Array} channels 36 | */ 37 | const getChannels = (config) => { 38 | return fs.readFileSync("channels.txt") 39 | .toString() 40 | .split(/\r?\n/i) 41 | .map(i => i.toLowerCase()) 42 | .filter(Boolean) 43 | .filter(i => !i.startsWith("#")) 44 | .concat(config.username) 45 | } 46 | 47 | /** 48 | * Writes to the channels file attempting to keep newlines and comments. 49 | * @param {Array} arr 50 | */ 51 | const setChannels = (arr, config) => { 52 | let lines = fs.readFileSync("channels.txt").toString().split(/\r?\n/i).filter(i => { 53 | if (i.startsWith("#")) return true; 54 | if (i === "") return true; 55 | return false; 56 | }) 57 | 58 | // Always 1 empty line at the end. 59 | if (lines[lines.length-1] != "") lines.push("") 60 | lines = lines.concat(arr) 61 | fs.writeFileSync("channels.txt", lines.filter(i => i !== config.username).join('\r\n')) 62 | } 63 | 64 | /** 65 | * Shows info. 66 | * @param {any} config The config 67 | */ 68 | const showInfo = (config) => { 69 | let primeMessage = "Prime colors are off, if you would like to have more colors, turn it on. (Requires Prime/Turbo)" 70 | if(config.usePrimeColors) { 71 | primeMessage = "Prime colors are on. If its not doing anything, try turning them off." 72 | } 73 | if(config.useRainbow) { 74 | let rainbowMessage = `Rainbow is on. Speed: ${config.rainbowSpeed}`; 75 | if (!config.usePrimeColors) { 76 | rainbowMessage = "Rainbow is on, but prime colors are off. Using random default color." 77 | } 78 | log(`info`, rainbowMessage) 79 | } 80 | log('info', `All commands are only sent to YOUR chat (#${config.username})`) 81 | log('info', primeMessage) 82 | } 83 | 84 | 85 | /** 86 | * Generate a random number. 87 | */ 88 | const randInt = (limit) => { 89 | return Math.floor(Math.random() * Math.floor(limit)); 90 | } 91 | 92 | /** 93 | * Gets the anon client 94 | * @param {DankTwitch.ChatClient} client The non-anon client. 95 | * @param {any} config The config. 96 | * @param {Array} channels The list of channels. 97 | * @param {() => void} UpdateColorMethod A function that when called will update the color. 98 | * @returns {DankTwitch.ChatClient} The anon client. 99 | */ 100 | const getAnonClient = (client, config, channels, UpdateColorMethod) => { 101 | let useColor = true; 102 | 103 | const anonClient = new DankTwitch.ChatClient(); 104 | 105 | anonClient.on("PRIVMSG", (msg) => { 106 | 107 | // whenever the anon client sees a message, it just checks if the sender is you, 108 | // then it will update the color if it is. 109 | if(msg.messageText == "toggleColor" && msg.senderUsername == config.username){ 110 | if(useColor){ 111 | useColor = false; 112 | client.privmsg(config.username, `Color is now off Kappa`) 113 | log('info', "Color is now off") 114 | }else{ 115 | useColor = true; 116 | client.privmsg(config.username, `Color is now on KappaPride`) 117 | log('info', "Color is now on") 118 | } 119 | 120 | } 121 | 122 | if (msg.messageText.startsWith("addColor") && msg.senderUsername == config.username.toLowerCase()) { 123 | let channel = msg.messageText.split(" ")[1].toLowerCase() 124 | if (channels.indexOf(channel) == -1) { 125 | anonClient.join(channel); 126 | channels.push(channel) 127 | setChannels(channels, config) 128 | client.privmsg(config.username, "channel added") 129 | } else { 130 | client.privmsg(config.username, "Channel already on the list") 131 | } 132 | }; 133 | if (msg.senderUsername == config.username && useColor == true) { 134 | log('INFO', `[${msg.channelName}] ${msg.senderUsername}: ${msg.messageText}`) 135 | UpdateColorMethod() 136 | } 137 | }) 138 | 139 | anonClient.on("ready", () => { 140 | log('INFO', `Anonymous client connected.`) 141 | }) 142 | 143 | anonClient.on("JOIN", (msg) => { 144 | log('INFO', `Anonymous client joined #${msg.channelName}.`) 145 | }) 146 | 147 | return anonClient 148 | } 149 | 150 | /** 151 | * Get a transition function that will return a new transition between colors using HSL. 152 | * 153 | * The transition will not include the ending color, but will include the starting color. 154 | * @param {string} startColor The starting color 155 | * @param {string} endColor The finishing color 156 | * @returns {() => string|null} Transition function. 157 | */ 158 | const getTransitioner = (startColor, endColor, speedMultiplier) => { 159 | let start = Color(startColor).hsl(); 160 | let current = Color(startColor).hsl(); 161 | let end = Color(endColor).hsl(); 162 | 163 | // Figure out how far we need to go for each. 164 | let Hdistance = end.hue() - start.hue() 165 | let Sdistance = end.saturationl() - start.saturationl() 166 | let Ldistance = end.lightness() - start.lightness() 167 | 168 | // This ensures that the speed will move the one that has the largest distance 169 | // one point at a time, and the others less than that. 170 | let numSteps = Math.ceil(Math.max( 171 | Math.abs(Hdistance), 172 | Math.abs(Sdistance), 173 | Math.abs(Ldistance) 174 | )); 175 | 176 | // The speed for the largest distance will be close to but smaller than 1 177 | // The others will be smaller than 1 178 | let Hspeed = Hdistance/numSteps 179 | let Sspeed = Sdistance/numSteps 180 | let Lspeed = Ldistance/numSteps 181 | 182 | let done = false; 183 | return () => { 184 | if (done || current.hex() == end.hex()) { 185 | done = true 186 | return null 187 | } 188 | 189 | let hex = current.hex() 190 | 191 | current = Color([ 192 | current.hue() + Hspeed * speedMultiplier, 193 | current.saturationl() + Sspeed * speedMultiplier, 194 | current.lightness() + Lspeed * speedMultiplier 195 | ], 'hsl') 196 | 197 | // check here to see if we have essentially 'skipped over' the target color. 198 | // If we have, we are finished. 199 | if ((() => { 200 | if (Hspeed <= 0 ) { 201 | // Hue speed is negative, going downwards. 202 | if (current.hue() < end.hue()) return true; 203 | } else { 204 | if (current.hue() > end.hue()) return true; 205 | } 206 | if (Sspeed <= 0 ) { 207 | // Saturation speed is negative, going downwards. 208 | if (current.saturationl() < end.saturationl()) return true; 209 | } else { 210 | if (current.saturationl() > end.saturationl()) return true; 211 | } 212 | if (Lspeed <= 0 ) { 213 | // Saturation speed is negative, going downwards. 214 | if (current.lightness() < end.lightness()) return true; 215 | } else { 216 | if (current.lightness() > end.lightness()) return true; 217 | } 218 | return false 219 | })()) { 220 | console.log("Color overshot!") 221 | done = true 222 | } 223 | 224 | return hex 225 | } 226 | } 227 | 228 | 229 | /** 230 | * Gets a function that will transition the colours 231 | * @param {object} config The config. 232 | * @returns {() => string|null} The transition color getter. 233 | */ 234 | const getTransitionColorGetter = (config) => { 235 | // If there is 0 or 1 colors, always return the only color or null 236 | if (Array.isArray(config.colorList) && config.colorList.length < 2) { 237 | const item = 238 | config.colorList[0] 239 | ? Color(config.colorList[0]).hex() 240 | : null 241 | return () => item 242 | } 243 | 244 | /** @type {Array} */ 245 | let colors = config.colorList; 246 | /** The index we are transitioning to. */ 247 | let target = 1; 248 | /** The index we are transitioning from. */ 249 | let last = 0; 250 | /** @type {() => string|null} */ 251 | let nextColor = getTransitioner(colors[0], colors[1], config.rainbowSpeed); 252 | return () => { 253 | let color = nextColor() 254 | if (!color) { 255 | // We have just reached the target, increament or loop over. 256 | last = target; 257 | if (++target >= colors.length) { 258 | target = 0; 259 | }; 260 | // Get the next transition and return the first item in it. 261 | nextColor = getTransitioner(colors[last], colors[target], config.rainbowSpeed); 262 | 263 | color = nextColor() 264 | } 265 | return color; 266 | } 267 | } 268 | 269 | 270 | // Exporting 271 | module.exports = { 272 | getAnonClient, 273 | getChannels, 274 | getClient, 275 | getTransitionColorGetter, 276 | getTransitioner, 277 | log, 278 | setChannels, 279 | showInfo, 280 | randInt, 281 | } --------------------------------------------------------------------------------