· 6 years ago · Aug 13, 2019, 01:02 AM
1// Installation:
2// npm install lodash tmi.js
3
4const tmi = require('tmi.js');
5const _ = require('lodash');
6
7// Define configuration options
8const opts = {
9 identity: {
10 username: "cheerupbot",
11 password: "<OAUTH_TOKEN>"
12 },
13 channels: [
14 "cheerupbeer"
15 ]
16};
17
18// These map be edited
19let shorthands = {
20 Australia: ['aus', 'straya'],
21 Bangladesh: ['bangla'],
22 Bhutan: ['bootan'],
23 Botswana: ['bots', 'wana'],
24 Brazil: ['brazzers'],
25 'Brunei Darussalam': ['brunei'],
26 Cambodia: ['khmer'],
27 Estonia: ['eesti', 'green'],
28 Germany: ['blurmany'],
29 Guatemala: ['guat'],
30 Indonesia: ['indo'],
31 Israel: ['palestine', 'israel/palestine', 'west bank', 'ps'],
32 Japan: ['nihon', 'nippon'],
33 Kazakhstan: ['kazakh'],
34 'South Korea': ['korea'],
35 Kyrgyzstan: ['kyrgyz'],
36 Laos: ['lao', 'lao pdr'],
37 Latvia: ['lat'],
38 Lithuania: ['lith'],
39 Luxembourg: ['lux'],
40 'North Macedonia': ['macedonia'],
41 Malaysia: ['malay'],
42 Malta: ['rocks'],
43 Mexico: ['mex'],
44 Netherlands: ['holland'],
45 'Papua New Guinea': ['png'],
46 Philippines: ['phils'],
47 Poland: ['poleland'],
48 Romania: ['rom'],
49 Singapore: ['singapura'],
50 'Sri Lanka': ['sri', 'lanka'],
51 Taiwan: ['numba1'],
52 Thailand: ['thai'],
53 Turkey: ['turk'],
54 'United Arab Emirates': ['uae'],
55 'United Kingdom': ['uk'],
56 'United States': ['usa'],
57 'Vietnam': ['viet'],
58};
59
60// autogenerated, don't edit this
61let countryList = [
62 ["Afghanistan", "AF", 0],
63 ["Albania", "AL", 0],
64 ["Algeria", "DZ", 0],
65 ["Andorra", "AD", 0],
66 ["Angola", "AO", 0],
67 ["Antigua and Barbuda", "AG", 0],
68 ["Argentina", "AR", 0],
69 ["Armenia", "AM", 0],
70 ["Australia", "AU", ["Christmas Island", "CX", "Cocos Islands", "CC", "Heard Island and McDonald Islands", "HM", "Norfolk Island", "NF"]],
71 ["Austria", "AT", 0],
72 ["Azerbaijan", "AZ", 0],
73 ["Bahamas", "BS", 0],
74 ["Bahrain", "BH", 0],
75 ["Bangladesh", "BD", 0],
76 ["Barbados", "BB", 0],
77 ["Belarus", "BY", 0],
78 ["Belgium", "BE", 0],
79 ["Belize", "BZ", 0],
80 ["Benin", "BJ", 0],
81 ["Bhutan", "BT", 0],
82 ["Bolivia", "BO", 0],
83 ["Bosnia and Herzegovina", "BA", 0],
84 ["Botswana", "BW", 0],
85 ["Brazil", "BR", 0],
86 ["Brunei Darussalam", "BN", 0],
87 ["Bulgaria", "BG", 0],
88 ["Burkina Faso", "BF", 0],
89 ["Burundi", "BI", 0],
90 ["Cabo Verde", "CV", 0],
91 ["Cambodia", "KH", 0],
92 ["Cameroon", "CM", 0],
93 ["Canada", "CA", 0],
94 ["Central African Republic", "CF", 0],
95 ["Chad", "TD", 0],
96 ["Chile", "CL", 0],
97 ["China", "CN", 0],
98 ["Colombia", "CO", 0],
99 ["Comoros", "KM", 0],
100 ["Congo", "CG", 0],
101 ["Costa Rica", "CR", 0],
102 ["C\u00f4te d'Ivoire", "CI", 0],
103 ["Croatia", "HR", 0],
104 ["Cuba", "CU", 0],
105 ["Cyprus", "CY", 0],
106 ["Czechia", "CZ", 0],
107 ["Denmark", "DK", ["Faroe Islands", "FO", "Greenland", "GL"]],
108 ["Djibouti", "DJ", 0],
109 ["Dominica", "DM", 0],
110 ["Dominican Republic", "DO", 0],
111 ["Ecuador", "EC", 0],
112 ["Egypt", "EG", 0],
113 ["El Salvador", "SV", 0],
114 ["Equatorial Guinea", "GQ", 0],
115 ["Eritrea", "ER", 0],
116 ["Estonia", "EE", 0],
117 ["Eswatini", "SZ", 0],
118 ["Ethiopia", "ET", 0],
119 ["Fiji", "FJ", 0],
120 ["Finland", "FI", ["\u00c5land Islands", "AX"]],
121 ["France", "FR", ["French Guiana", "GF", "French Polynesia", "PF", "French Southern Territories", "TF", "Guadeloupe", "GP", "Martinique", "MQ", "Mayotte", "YT", "New Caledonia", "NC", "R\u00e9union", "RE", "Saint Barth\u00e9lemy", "BL", "Saint Martin", "MF", "Saint Pierre and Miquelon", "PM", "Wallis and Futuna", "WF"]],
122 ["Gabon", "GA", 0],
123 ["Gambia", "GM", 0],
124 ["Georgia", "GE", 0],
125 ["Germany", "DE", 0],
126 ["Ghana", "GH", 0],
127 ["Greece", "GR", 0],
128 ["Grenada", "GD", 0],
129 ["Guatemala", "GT", 0],
130 ["Guinea", "GN", 0],
131 ["Guinea-Bissau", "GW", 0],
132 ["Guyana", "GY", 0],
133 ["Haiti", "HT", 0],
134 ["Vatican City", "VA", 0],
135 ["Honduras", "HN", 0],
136 ["Hong Kong", "HK", 0],
137 ["Hungary", "HU", 0],
138 ["Iceland", "IS", 0],
139 ["India", "IN", 0],
140 ["Indonesia", "ID", 0],
141 ["Iran", "IR", 0],
142 ["Iraq", "IQ", 0],
143 ["Ireland", "IE", 0],
144 ["Israel", "IL", 0],
145 ["Italy", "IT", 0],
146 ["Jamaica", "JM", 0],
147 ["Japan", "JP", 0],
148 ["Jordan", "JO", 0],
149 ["Kazakhstan", "KZ", 0],
150 ["Kenya", "KE", 0],
151 ["Kiribati", "KI", 0],
152 ["North Korea", "KP", 0],
153 ["South Korea", "KR", 0],
154 ["Kuwait", "KW", 0],
155 ["Kyrgyzstan", "KG", 0],
156 ["Laos", "LA", 0],
157 ["Latvia", "LV", 0],
158 ["Lebanon", "LB", 0],
159 ["Lesotho", "LS", 0],
160 ["Liberia", "LR", 0],
161 ["Libya", "LY", 0],
162 ["Liechtenstein", "LI", 0],
163 ["Lithuania", "LT", 0],
164 ["Luxembourg", "LU", 0],
165 ["Macao", "MO", 0],
166 ["North Macedonia", "MK", 0],
167 ["Madagascar", "MG", 0],
168 ["Malawi", "MW", 0],
169 ["Malaysia", "MY", 0],
170 ["Maldives", "MV", 0],
171 ["Mali", "ML", 0],
172 ["Malta", "MT", 0],
173 ["Marshall Islands", "MH", 0],
174 ["Mauritania", "MR", 0],
175 ["Mauritius", "MU", 0],
176 ["Mexico", "MX", 0],
177 ["Micronesia", "FM", 0],
178 ["South Korea", "MD", 0],
179 ["Monaco", "MC", 0],
180 ["Mongolia", "MN", 0],
181 ["Montenegro", "ME", 0],
182 ["Morocco", "MA", 0],
183 ["Mozambique", "MZ", 0],
184 ["Myanmar", "MM", 0],
185 ["Namibia", "NA", 0],
186 ["Nauru", "NR", 0],
187 ["Nepal", "NP", 0],
188 ["Netherlands", "NL", ["Aruba", "AW", "Bonaire Sint Eustatius Saba", "BQ", "Cura\u00e7ao", "CW", "Sint Maarten", "SX"]],
189 ["New Zealand", "NZ", ["Cook Islands", "CK", "Niue", "NU", "Tokelau", "TK"]],
190 ["Nicaragua", "NI", 0],
191 ["Niger", "NE", 0],
192 ["Nigeria", "NG", 0],
193 ["Norway", "NO", ["Bouvet Island", "BV", "Svalbard Jan Mayen", "SJ"]],
194 ["Oman", "OM", 0],
195 ["Pakistan", "PK", 0],
196 ["Palau", "PW", 0],
197 ["Palestine, State of", "PS", 0],
198 ["Panama", "PA", 0],
199 ["Papua New Guinea", "PG", 0],
200 ["Paraguay", "PY", 0],
201 ["Peru", "PE", 0],
202 ["Philippines", "PH", 0],
203 ["Poland", "PL", 0],
204 ["Portugal", "PT", 0],
205 ["Qatar", "QA", 0],
206 ["Romania", "RO", 0],
207 ["Russia", "RU", 0],
208 ["Rwanda", "RW", 0],
209 ["Saint Kitts and Nevis", "KN", 0],
210 ["Saint Lucia", "LC", 0],
211 ["Saint Vincent and the Grenadines", "VC", 0],
212 ["Samoa", "WS", 0],
213 ["San Marino", "SM", 0],
214 ["Sao Tome and Principe", "ST", 0],
215 ["Saudi Arabia", "SA", 0],
216 ["Senegal", "SN", 0],
217 ["Serbia", "RS", 0],
218 ["Seychelles", "SC", 0],
219 ["Sierra Leone", "SL", 0],
220 ["Singapore", "SG", 0],
221 ["Slovakia", "SK", 0],
222 ["Slovenia", "SI", 0],
223 ["Solomon Islands", "SB", 0],
224 ["Somalia", "SO", 0],
225 ["South Africa", "ZA", 0],
226 ["South Sudan", "SS", 0],
227 ["Spain", "ES", 0],
228 ["Sri Lanka", "LK", 0],
229 ["Sudan", "SD", 0],
230 ["Suriname", "SR", 0],
231 ["Sweden", "SE", 0],
232 ["Switzerland", "CH", 0],
233 ["Syria", "SY", 0],
234 ["Taiwan", "TW", 0],
235 ["Tajikistan", "TJ", 0],
236 ["Tanzania", "TZ", 0],
237 ["Thailand", "TH", 0],
238 ["Timor-Leste", "TL", 0],
239 ["Togo", "TG", 0],
240 ["Tonga", "TO", 0],
241 ["Trinidad and Tobago", "TT", 0],
242 ["Tunisia", "TN", 0],
243 ["Turkey", "TR", 0],
244 ["Turkmenistan", "TM", 0],
245 ["Tuvalu", "TV", 0],
246 ["Uganda", "UG", 0],
247 ["Ukraine", "UA", 0],
248 ["United Arab Emirates", "AE", 0],
249 ["United Kingdom", "GB", ["Anguilla", "AI", "Bermuda", "BM", "British Indian Ocean Territory", "IO", "Cayman Islands", "KY", "Falkland Islands", "FK", "Gibraltar", "GI", "Guernsey", "GG", "Isle of Man", "IM", "Jersey", "JE", "Montserrat", "MS", "Pitcairn", "PN", "Saint Helena Ascension Island Tristan da Cunha", "SH", "South Georgia and the South Sandwich Islands", "GS", "Turks and Caicos Islands", "TC"]],
250 ["United States", "US", ["American Samoa", "AS", "Guam", "GU", "Northern Mariana Islands", "MP", "Puerto Rico", "PR"]],
251 ["Uruguay", "UY", 0],
252 ["Uzbekistan", "UZ", 0],
253 ["Vanuatu", "VU", 0],
254 ["Venezuela", "VE", 0],
255 ["Vietnam", "VN", 0],
256 ["Yemen", "YE", 0],
257 ["Zambia", "ZM", 0],
258 ["Zimbabwe", "ZW", 0],
259]; // autogenerated, don't edit this
260
261
262let namesToCountries = {};
263for (let [name, iso, alts] in countryList) {
264 namesToCountries[name.toLowerCase()] = name;
265 namesToCountries[iso.toLowerCase()] = name;
266 for (let alt of alts) {
267 namesToCountries[alt.toLowerCase()] = name;
268 }
269}
270for (let key in shorthands) {
271 for (let value in shorthands[key]) {
272 namesToCountries[value.toLowerCase()] = name;
273 }
274}
275
276function countryMatches(a, b) {
277 a = a.toLowerCase().trim();
278 b = b.toLowerCase().trim();
279
280 // TODO notify users in chat if their country isn't recognized
281
282 if (a === b && !!a) return true;
283
284 let a1 = namesToCountries[a];
285 let b1 = namesToCountries[b];
286
287 return a1 === b1 && !!a1;
288}
289
290function countryExists(a) {
291 return !!namesToCountries(a.toLowerCase().trim());
292}
293
294class Round {
295 constructor(parent) {
296 this.parent = parent;
297 this.guesses = new Map();
298 }
299 guess(player, country) {
300 this.guesses.set(player, country);
301 }
302 playersWhoSelected(answer) {
303 let result = [];
304 for (let [player, options] of this.guesses) {
305 if (countryMatches(options.answer, guess)) {
306 result.push(options.displayname);
307 }
308 }
309 return result;
310 }
311}
312
313class Game {
314 constructor(parent) {
315 this.parent = parent;
316 this.streaks = new Map();
317 }
318 addWinner(displayname) {
319 let obj = this.streaks.get(displayname);
320 if (!obj) {
321 obj = { streak: 0 };
322 this.streaks.set(displayname, obj);
323 }
324 obj.streak += 1;
325 }
326 sortedWinners() {
327 let winners = [];
328 for (let [player, obj] of this.streaks) {
329 winner.push([player, obj.streak]);
330 }
331 winner = _.orderBy(winner, x => x[1], 'desc');
332 return winner;
333 }
334}
335
336class Controller {
337 constructor(client) {
338 this.client = client;
339 this.newRound();
340 this.newGame();
341 }
342 newRound() {
343 this.round = new Round(this);
344 }
345 newGame() {
346 this.game = new Game(this);
347 }
348
349 recv(channel, userstate, msg, self, isWhisper) {
350 const displayname = userstate['display-name'];
351 const username = userstate['username'];
352 const userkey = username['user-id'];
353
354 if (username === 'cheerupbeer') {
355 if (/^!answer\b/i.test(msg)) {
356 let answer = /^![a-z]+\s([^\n]+)/i.exec(msg)[1];
357 if (answer) {
358 answer = answer.trim();
359 let winners = this.round.playersWhoSelected(answer);
360 for (let winner of winners) {
361 this.game.addWinner(winner);
362 }
363 this.client.say(target, `The winners were ${winners.map(w => '@'+w).join(', ')}`);
364 }
365 }
366 }
367 if (isWhisper && /^!g(?:uess)?\b/i.test(msg)) {
368 let answer = /^![a-z]+\s([^\n]+)/i.exec(msg)[1];
369 if (answer) {
370 answer = answer.trim();
371 if (countryExists(answer)) {
372 this.round.set(userkey, {
373 displayname, answer
374 });
375 } else {
376 this.client.say(target, `@${displayname} I'm sorry, I don't know the country you guessed!`);
377 }
378 }
379 }
380 if (/^!leaderboard\b/i.test(msg)) {
381 let sortedWinners = this.game.sortedWinners();
382 let strs = [''];
383 for (let [displayname, streak] of sortedWinners) {
384 let str1 = strs[strs.length - 1];
385 let str2 = `${streak} ${displayname}`;
386 let suffix = ' ; ' + str2;
387 // Limit message lengths to 500
388 if ((str1 + suffix).length >= 500) {
389 strs.push(str2);
390 } else {
391 strs[strs.length - 1] += suffix;
392 }
393 }
394 for (let str of strs) {
395 this.client.say(str);
396 }
397 }
398 }
399}
400
401// Create a client with our options
402const client = new tmi.client(opts);
403
404// Register our event handlers (defined below)
405client.on('message', onMessageHandler);
406client.on('whisper', onWhisperHandler);
407client.on('connected', onConnectedHandler);
408
409// Connect to Twitch:
410client.connect();
411
412const controller = new Controller(client);
413
414// https://github.com/tmijs/docs/blob/gh-pages/_posts/v1.4.2/2019-03-03-Events.md#message
415
416// Called every time a message comes in
417function onMessageHandler (channel, userstate, message, self) {
418 if (self) { return; } // Ignore messages from the bot
419
420 // If the command is known, let's execute it
421 controller.recv(channel, userstate, msg.trim(), self, false);
422}
423
424function onWhisperHandler (channel, userstate, message, self) {
425 if (self) { return; } // Ignore messages from the bot
426
427 // If the command is known, let's execute it
428 controller.recv(channel, userstate, msg.trim(), self, true);
429}
430
431// Called every time the bot connects to Twitch chat
432function onConnectedHandler (addr, port) {
433 console.log(`* Connected to ${addr}:${port}`);
434}