Twitch Chat Line Bot (-ish)
--------------------------------------------------------------------------------
/projects/line-bot/v1/index.pug:
--------------------------------------------------------------------------------
1 | doctype html
2 | html
3 | head
4 | meta(charset="utf-8")
5 | link(rel="icon", href="data:;base64,iVBORw0KGgo=")
6 | title Line Bot
7 | link(href="https://fonts.googleapis.com/css?family=Roboto+Condensed:400,700", rel="stylesheet")
8 | link(href="css/main.min.css", rel="stylesheet")
9 | script(src="https://cdn.tmijs.org/js/1.1.1/tmi.min.js")
10 | script(src="https://rubaxa.github.io/Sortable/Sortable.js")
11 | body
12 | #body
13 | #title.underlined Twitch Chat Line Bot (-ish)
14 | #options.disconnected.underlined
15 | #connection.option-group
16 | label
17 | b Channel:
18 | input#channel(type="text", placeholder="Channel")
19 | input#connect(type="button", value="Connect")
20 | input#disconnect(type="button", value="Disconnect", disabled)
21 | #status.option-group.underlined
22 | #connection-status
23 | div
24 | span Disconnected
25 | #join-status Parted
26 | #commands.option-group
27 | .header-2 Commands: (case-insensitive)
28 | .indented
29 | label
30 | b Join:
31 | input#command-add(type="text", placeholder="joinline", value="joinline")
32 | label
33 | b Leave:
34 | input#command-remove(type="text", placeholder="leaveline", value="leaveline")
35 | #line
36 | .header-1 Line:
37 | input#clear-line(type="button", value="Clear all")
38 | #list
39 | script(src="js/module.js")
40 | script(src="js/variables.js")
41 | script(src="js/settings.js")
42 | script(src="js/line.js")
43 | script(src="js/commands.js")
44 | script(src="js/chat.js")
45 | script(src="js/main.js")
46 |
--------------------------------------------------------------------------------
/projects/line-bot/v1/js/chat.js:
--------------------------------------------------------------------------------
1 | (function(module) {
2 |
3 | function updateConnStatus(state) {
4 | let colors = {
5 | red: 'hsl(0, 100%, 50%)',
6 | green: 'hsl(120, 100%, 50%)',
7 | blue: 'hsl(210, 100%, 50%)',
8 | grey: 'hsl(0, 0%, 50%)'
9 | },
10 | stateColors = {
11 | CONNECTING: colors.blue,
12 | OPEN: colors.green,
13 | CLOSING: colors.blue,
14 | CLOSED: colors.grey
15 | },
16 | stateMessages = {
17 | CONNECTING: 'Connecting',
18 | OPEN: 'Connected',
19 | CLOSING: 'Disconnecting',
20 | CLOSED: 'Disconnected'
21 | },
22 | connStatusEle = outputs.status.connection,
23 | stateColor,
24 | stateMessage;
25 |
26 | if(state === undefined || Array.isArray(state) ||
27 | (typeof state !== 'object' && typeof state !== 'string')) {
28 | state = client.readyState();
29 | }
30 |
31 | stateColor = state.color || stateColors[state] || colors.grey;
32 | stateMessage = state.message || stateMessages[state];
33 |
34 | connStatusEle.firstElementChild.style.backgroundColor = stateColor;
35 | connStatusEle.lastElementChild.innerText = stateMessage;
36 | }
37 |
38 | function createClient() {
39 | client = new tmi.client({
40 | options: {
41 | debug: true
42 | },
43 | connection: {
44 | secure: true,
45 | reconnect: true
46 | }
47 | });
48 | }
49 |
50 | function connectClient() {
51 | let state = client.readyState(),
52 | channel = inputs.channel.value.toLowerCase();
53 | if(state !== 'CLOSED') {
54 | return Promise.reject(new Error(errors.TMI_ALREADY_CONNECTED));
55 | }
56 | if(typeof channel !== 'string') {
57 | return Promise.reject(new TypeError(errors.CHANNEL_WRONG_TYPE));
58 | }
59 | if(channel === '') {
60 | return Promise.reject(new Error(errors.CHANNEL_EMPTY));
61 | }
62 | let prom = client.connect()
63 | .then(updateConnStatus)
64 | .then(() => client.join(channel))
65 | .then(() => _channel = channel);
66 | updateConnStatus();
67 | return prom;
68 | }
69 |
70 | function disconnectClient() {
71 | let state = client.readyState();
72 | if(state !== 'OPEN') {
73 | return Promise.reject(new Error(errors.TMI_ALREADY_DISCONNECTED));
74 | }
75 | let prom = client.disconnect()
76 | .then(updateConnStatus);
77 | updateConnStatus();
78 | return prom;
79 | }
80 |
81 | function tryingToConnect(bool) {
82 | if(bool) {
83 | inputs.channel.disabled = true;
84 | inputs.connect.disabled = true;
85 | inputs.disconnect.disabled = false;
86 | }
87 | else {
88 | inputs.channel.disabled = false;
89 | inputs.connect.disabled = false;
90 | inputs.disconnect.disabled = true;
91 | }
92 | }
93 |
94 | function clickedConnect(e) {
95 | if(e instanceof KeyboardEvent && e.keyCode !== 13) {
96 | return false;
97 | }
98 | tryingToConnect(true);
99 | settings.save();
100 | return connectClient()
101 | .catch(e => {
102 | if(e.message === errors.CHANNEL_EMPTY) {
103 | tryingToConnect(false);
104 | inputs.channel.className = 'alert';
105 | setTimeout(() => inputs.channel.className = '', 10);
106 | inputs.channel.focus();
107 | }
108 | else {
109 | logError(e);
110 | }
111 | });
112 | }
113 |
114 | function clickedDisconnect() {
115 | tryingToConnect(false);
116 | settings.save();
117 | return disconnectClient()
118 | .catch(e => {
119 | if(e.message !== errors.TMI_ALREADY_DISCONNECTED) {
120 | tryingToConnect(true);
121 | logError(e);
122 | }
123 | });
124 | }
125 |
126 | function handleChat(channel, user, message, fromSelf) {
127 | let chan = channel.replace('#', '');
128 | if(fromSelf || chan !== _channel) {
129 | return false;
130 | }
131 |
132 | message = message.trim();
133 |
134 | if(!message.startsWith('!')) {
135 | return false;
136 | }
137 |
138 | let params = message.split(' '),
139 | command = params.shift().toLowerCase().substr(1),
140 |
141 | isBroadcaster = user.username === chan,
142 | isSub = user.subscriber,
143 |
144 | modUp = user.mod || isBroadcaster,
145 | subUp = isSub || modUp;
146 |
147 | if(subUp) {
148 | let action = false,
149 | name = user,
150 | changedByOther = false;
151 |
152 | switch(command) {
153 | case line.commands.add:
154 | action = line.add;
155 | break;
156 | case line.commands.remove:
157 | action = line.remove;
158 | break;
159 | default:
160 | return false;
161 | }
162 |
163 | if(modUp && params.length > 0) {
164 | let param1 = params[0].replace(/\W/g, '');
165 | if(name !== user.username) {
166 | changedByOther = true;
167 | name = {
168 | 'display-name': param1,
169 | username: param1.toLowerCase()
170 | };
171 | }
172 | }
173 |
174 | if(action) {
175 | return action(name, changedByOther);
176 | }
177 | }
178 | }
179 |
180 | function attachListeners() {
181 | client.on('join', (channel, username, self) => {
182 | if(self) {
183 | outputs.status.join.innerText = `Joined ${channel.substr(1)}`;
184 | }
185 | })
186 | .on('part', (channel, username, self) => {
187 | if(self) {
188 | outputs.status.join.innerText = `Parted ${channel.substr(1)}`;
189 | }
190 | })
191 | .on('connected', () => {
192 | tryingToConnect(true);
193 | updateConnStatus();
194 | inputs.options.classList.remove('disconnected');
195 | })
196 | .on('disconnected', reason => {
197 | tryingToConnect(false);
198 | updateConnStatus();
199 | inputs.options.classList.add('disconnected');
200 | })
201 | .on('reconnect', () => {
202 | tryingToConnect(true);
203 | updateConnStatus({ message: 'Reconnecting' });
204 | })
205 | .on('message', handleChat);
206 | }
207 |
208 | module.exports({
209 | attachListeners,
210 | clickedConnect,
211 | clickedDisconnect,
212 | connect: connectClient,
213 | create: createClient,
214 | disconnect: disconnectClient,
215 | handle: handleChat,
216 | tryingToConnect,
217 | updateConnStatus,
218 | });
219 |
220 | })(new Module('chat'));
221 |
--------------------------------------------------------------------------------
/projects/line-bot/v1/js/commands.js:
--------------------------------------------------------------------------------
1 | (function(module) {
2 |
3 | const identifiers = /^[!@#$%^&*()_+]+/g;
4 |
5 | function test(str) {
6 | return identifiers.test(str.trim());
7 | }
8 |
9 | function normalize(name) {
10 | return name.replace(identifiers, '') || name;
11 | }
12 |
13 | function parse(chan, user, message) {
14 | if(!test(message)) {
15 | return false;
16 | }
17 | message = normalize(message);
18 | }
19 |
20 | module.exports({
21 | normalize,
22 | parse,
23 | test
24 | });
25 |
26 | })(new Module('commands'));
27 |
--------------------------------------------------------------------------------
/projects/line-bot/v1/js/line.js:
--------------------------------------------------------------------------------
1 | (function(module) {
2 |
3 | function addToLine(user, addedByOther) {
4 | let name = (user.username || user).replace(/\W/g, '');
5 | if(name === '') {
6 | return false;
7 | }
8 | else if(line.list.find(n => name.toLowerCase() === n.name)) {
9 | return false;
10 | }
11 |
12 | let item = {
13 | name: name.toLowerCase(),
14 | display: user['display-name'] || name,
15 | added: Date.now(),
16 | removed: false,
17 | removedByOther: false,
18 | ele: null,
19 | addedByOther
20 | };
21 |
22 | if(item.name.indexOf('"') > -1) {
23 | item.name = item.name.replace(new RegExp('"', 'g'), '\\\\"');
24 | }
25 |
26 | let itemEle = document.createElement('div'),
27 | itemNameEle = document.createElement('div'),
28 | itemRemoveEle = document.createElement('div');
29 |
30 | itemEle.className = 'list-item';
31 | itemNameEle.className = 'item-name';
32 | itemRemoveEle.className = 'item-remove';
33 |
34 | itemEle.setAttribute('name', name.toLowerCase());
35 | itemNameEle.innerText = item.display;
36 | itemRemoveEle.addEventListener('click', () => removeFromLine(user), false);
37 |
38 | itemEle.appendChild(itemRemoveEle);
39 | itemEle.appendChild(itemNameEle);
40 | list.appendChild(itemEle);
41 |
42 | item.ele = itemEle;
43 |
44 | line.list.push(item);
45 |
46 | return item;
47 | }
48 |
49 | function removeFromLine(user, removedByOther) {
50 | let name = (user.username || user.name || user).toLowerCase(),
51 | ele = user.ele || document.querySelector(`[name="${name}"]`),
52 | itemIndex = line.list.findIndex(n => name === n.name),
53 | item = line.list[itemIndex];
54 | if(itemIndex < 0) {
55 | return false;
56 | }
57 | ele.parentNode.removeChild(ele);
58 | line.list.splice(itemIndex, 1);
59 |
60 | item.removed = Date.now();
61 | item.removedByOther = removedByOther || false;
62 |
63 | return item;
64 | }
65 |
66 | function clearLine() {
67 | return line.list.slice(0).map(removeFromLine);
68 | }
69 |
70 | function sortUpdate(event) {
71 | let item = line.list.splice(event.oldIndex, 1)[0];
72 | line.list.splice(event.newIndex, 0, item);
73 | }
74 |
75 |
76 | module.exports({
77 | add: addToLine,
78 | remove: removeFromLine,
79 | clear: clearLine,
80 | list: [],
81 | commands: {
82 | add: '',
83 | remove: ''
84 | },
85 | sortUpdate
86 | });
87 |
88 | })(new Module('line'));
89 |
--------------------------------------------------------------------------------
/projects/line-bot/v1/js/main.js:
--------------------------------------------------------------------------------
1 | function logError(e) {
2 | if(e) {
3 | if(e instanceof Error) {
4 | console.log(e.message);
5 | }
6 | else {
7 | console.log(e);
8 | }
9 | }
10 | }
11 |
12 | function attachListeners() {
13 | [
14 | [ inputs.connect, 'click', chat.clickedConnect ],
15 | [ inputs.disconnect, 'click', chat.clickedDisconnect ],
16 | [ inputs.channel, 'keydown', chat.clickedConnect ],
17 | [ inputs.channel, 'keydown', chat.clickedConnect ],
18 | [ inputs.channel, 'input', settings.save ],
19 | [ inputs.addline, 'input', settings.save ],
20 | [ inputs.removeline, 'input', settings.save ],
21 | [ inputs.clearBtn, 'click', line.clear ]
22 | ].forEach((n,i) => {
23 | n[0].addEventListener(n[1], n[2], false);
24 | });
25 |
26 | chat.attachListeners();
27 | }
28 |
29 | void function init() {
30 | Sortable.create(outputs.list, {
31 | animation: 100,
32 | ghostClass: 'sortable-ghost',
33 | chosenClass: 'sortable-chosen',
34 | onUpdate: line.sortUpdate
35 | });
36 | chat.create();
37 | settings.load();
38 | attachListeners();
39 | }();
40 |
--------------------------------------------------------------------------------
/projects/line-bot/v1/js/module.js:
--------------------------------------------------------------------------------
1 | (function(global) {
2 |
3 | class Module {
4 | constructor(name) {
5 | this.name = name || null;
6 | }
7 |
8 | exports(value) {
9 | if(typeof value === 'undefined') {
10 | throw new TypeError('Nothing passed');
11 | }
12 | else if(this.name === null) {
13 | throw new Error('Not ready');
14 | }
15 | global[this.name] = value;
16 | return this;
17 | }
18 | }
19 |
20 | global.Module = Module;
21 |
22 | })(window);
23 |
--------------------------------------------------------------------------------
/projects/line-bot/v1/js/settings.js:
--------------------------------------------------------------------------------
1 | (function(module) {
2 |
3 | let _channel = '';
4 |
5 | function loadFromStorage() {
6 | line = JSON.parse(localStorage.twitchchatlinebot || '{}');
7 | line.list = line.list || [];
8 | }
9 |
10 | function saveToStorage() {
11 | localStorage.twitchchatlinebot = JSON.stringify(line);
12 | }
13 |
14 | function parseHash() {
15 | return location.hash
16 | .replace(/^#/, '')
17 | .split('&')
18 | .map(n => n.split('='))
19 | .reduce((p, n) => {
20 | p[decodeURIComponent(n[0])] = decodeURIComponent(n[1] || '') || true;
21 | return p;
22 | }, {});
23 | }
24 |
25 | function stringifyHash(obj) {
26 | return location.hash = Object.keys(obj)
27 | .map(key =>
28 | `${encodeURIComponent(key)}=${encodeURIComponent(obj[key])}`
29 | )
30 | .join('&');
31 | }
32 |
33 | function restoreFromHash() {
34 | let hash = parseHash(),
35 | cA = 'command.add',
36 | cR = 'command.remove';
37 |
38 | if(hash.hasOwnProperty('channel') && typeof hash.channel === 'string') {
39 | let channel = hash.channel.toLowerCase();
40 | _channel = channel;
41 | inputs.channel.value = channel;
42 | }
43 | if(hash.hasOwnProperty(cA) && typeof hash[cA] === 'string') {
44 | let add = hash[cA].toLowerCase();
45 | line.commands.add = add;
46 | inputs.addline.value = add;
47 | }
48 | if(hash.hasOwnProperty(cR) && typeof hash[cR] === 'string') {
49 | let remove = hash[cR].toLowerCase();
50 | line.commands.remove = remove;
51 | inputs.removeline.value = remove;
52 | }
53 | }
54 |
55 | function saveToHash() {
56 | let values = {
57 | 'channel': inputs.channel.value.toLowerCase(),
58 | 'command.add': inputs.addline.value.toLowerCase(),
59 | 'command.remove': inputs.removeline.value.toLowerCase(),
60 | };
61 | line.commands.add = values['command.add'];
62 | line.commands.remove = values['command.remove'];
63 | return stringifyHash(values);
64 | }
65 |
66 | module.exports({
67 | save: saveToHash,
68 | load: restoreFromHash,
69 | channel
70 | });
71 |
72 | })(new Module('settings'));
73 |
--------------------------------------------------------------------------------
/projects/line-bot/v1/js/variables.js:
--------------------------------------------------------------------------------
1 | const errors = {
2 | CHANNEL_EMPTY: 'Channel is an empty string.',
3 | CHANNEL_WRONG_TYPE: 'Channel is not a string.',
4 | TMI_ALREADY_CONNECTED: 'Client might still be connected.',
5 | TMI_ALREADY_DISCONNECTED: 'Client is already disconnected.'
6 | },
7 | inputs = {
8 | options: document.getElementById('options'),
9 | channel: document.getElementById('channel'),
10 | connect: document.getElementById('connect'),
11 | disconnect: document.getElementById('disconnect'),
12 | addline: document.getElementById('command-add'),
13 | removeline: document.getElementById('command-remove'),
14 | clearBtn: document.getElementById('clear-line')
15 | },
16 | outputs = {
17 | status: {
18 | connection: document.getElementById('connection-status'),
19 | join: document.getElementById('join-status')
20 | },
21 | line: document.getElementById('line'),
22 | list: document.getElementById('list')
23 | };
24 |
25 | let client;
26 |
--------------------------------------------------------------------------------
/projects/live-youtube-subs/v1/README.MD:
--------------------------------------------------------------------------------
1 | # Live YouTube Subscribers
2 |
3 | ## How to use
4 |
5 | In your streaming software, create a browser source and use the URL
6 |
7 | > `http://alcadesign.github.io/Twitch/projects/live-youtube-subs/v1/`
8 |
9 | You can append these parameters to the url to configure the behavior:
10 |
11 | ### Parameters
12 |
13 | | Name | Functionality | Example |
14 | |-----------|-------------------------|-----------------------|
15 | | username | YouTube username | pewdiepie |
16 | | color | A valid css color | white |
17 |
--------------------------------------------------------------------------------
/projects/live-youtube-subs/v1/css/style.css:
--------------------------------------------------------------------------------
1 | html,
2 | body {
3 | font-family: 'Arimo', cursive;
4 | }
5 |
6 | #subcount {
7 | font-size: 42px;
8 | }
9 |
10 | #subcount div {
11 | width: 24px;
12 | display: inline-block;
13 | }
14 |
15 | #subcount div:nth-last-child(4n) {
16 | width: 12px;
17 | }
18 |
--------------------------------------------------------------------------------
/projects/live-youtube-subs/v1/css/style.min.css:
--------------------------------------------------------------------------------
1 | html,body{font-family:'Arimo', cursive}#subcount{font-size:42px}#subcount div{width:24px;display:inline-block}#subcount div:nth-last-child(4n){width:12px}
2 |
--------------------------------------------------------------------------------
/projects/live-youtube-subs/v1/css/style.scss:
--------------------------------------------------------------------------------
1 | html,
2 | body {
3 | font-family: 'Arimo', cursive;
4 | }
5 |
6 | #subcount {
7 | font-size: 42px;
8 |
9 | div {
10 | width: 24px;
11 | display: inline-block;
12 |
13 | &:nth-last-child(4n) {
14 | width: 12px;
15 | }
16 | }
17 | }
18 |
--------------------------------------------------------------------------------
/projects/live-youtube-subs/v1/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |