]> git.localhorst.tv Git - alttp.git/blob - app/TwitchBot/TwitchChatBot.php
add up to 4 player support for zsr sync
[alttp.git] / app / TwitchBot / TwitchChatBot.php
1 <?php
2
3 namespace App\TwitchBot;
4
5 use App\Models\Channel;
6 use App\Models\ChatBotLog;
7 use App\Models\ChatLib;
8 use App\Models\ChatLog;
9 use Illuminate\Support\Arr;
10 use Illuminate\Support\Str;
11
12 class TwitchChatBot extends TwitchBot {
13
14         public function __construct() {
15                 parent::__construct('horstiebot');
16                 $this->updateChannels();
17                 $this->startTimer();
18                 $this->listenCommands();
19                 $this->chatlib = new ChatLib();
20                 $this->chatlib->loadFrom('de');
21         }
22
23         public function joinChannels() {
24                 $this->getLogger()->info('joining channels');
25                 $names = [];
26                 foreach ($this->channels as $channel) {
27                         $names[] = $channel->twitch_chat;
28                 }
29                 $chunks = array_chunk($names, 10);
30                 foreach ($chunks as $chunk) {
31                         $this->sendIRCMessage(IRCMessage::join($chunk));
32                 }
33         }
34
35         public function logMessage(IRCMessage $msg) {
36                 $channel = $this->getMessageChannel($msg);
37                 if ($channel && !$channel->join) {
38                         $msg->log();
39                 }
40         }
41
42         public function handlePrivMsg(IRCMessage $msg) {
43                 if ($msg->nick == 'horstiebot') return;
44                 $channel = $this->getMessageChannel($msg);
45                 if (!$channel) return;
46                 $this->tagChannelRead($channel, $msg);
47         }
48
49         public function getChatlibDatabase(Channel $channel) {
50                 return $this->chatlib;
51         }
52
53
54         private function startTimer() {
55                 $this->getLoop()->addPeriodicTimer(1, function () {
56                         if (!$this->isReady()) return;
57                         foreach ($this->channels as $channel) {
58                                 $this->decideSend($channel);
59                         }
60                 });
61                 $this->getLoop()->addPeriodicTimer(60, function () {
62                         $this->updateChannels();
63                 });
64         }
65
66         private function updateChannels() {
67                 $this->channels = Channel::where('twitch_chat', '!=', '')->where('chat', '=', true)->get();
68         }
69
70         private function decideSend(Channel $channel) {
71                 $notes = $this->getNotes($channel);
72                 if ($notes['read_since_last_write'] < $notes['wait_msgs']) {
73                         return;
74                 }
75                 if (time() - $notes['last_write'] < $notes['wait_time']) {
76                         return;
77                 }
78                 if ($notes['read_since_last_write'] == $notes['wait_msgs'] && time() - $notes['last_read'] < 3) {
79                         // don't immediately respond if we crossed the msg threshold last
80                         return;
81                 }
82                 $text = $this->contextualMsg($channel);
83                 if ($this->shouldAdlib($channel)) {
84                         $this->performAdlib($channel);
85                         return;
86                 }
87                 if (!$text) $text = $this->randomChat($channel);
88                 if (!$text) return;
89                 $actual_text = is_object($text) ? $text->text_content : $text;
90                 $this->tagChannelWrite($channel);
91                 $this->sendIRCMessage(IRCMessage::privmsg($channel->twitch_chat, $actual_text));
92                 $log = new ChatBotLog();
93                 $log->channel()->associate($channel);
94                 if (is_object($text)) {
95                         $log->origin()->associate($text);
96                         $log->category = $text->classification;
97                 } else {
98                         $log->category = $this->getLastSpecialSent($channel);
99                 }
100                 $log->text = $actual_text;
101                 $log->save();
102         }
103
104         private function getNotes(Channel $channel) {
105                 if (!isset($this->notes[$channel->id])) {
106                         $this->notes[$channel->id] = [
107                                 'last_read' => 0,
108                                 'last_special' => [],
109                                 'last_write' => time(),
110                                 'latest_msgs' => [],
111                                 'queued_special' => false,
112                                 'read_since_last_write' => 0,
113                                 'wait_msgs' => $this->randomWaitMsgs($channel),
114                                 'wait_time' => $this->randomWaitTime($channel),
115                         ];
116                 }
117                 return $this->notes[$channel->id];
118         }
119
120         private function getNote(Channel $channel, $name, $default = null) {
121                 $notes = $this->getNotes($channel);
122                 if (array_key_exists($name, $notes)) {
123                         return $notes[$name];
124                 }
125                 return $default;
126         }
127
128         private function setNote(Channel $channel, $name, $value) {
129                 $this->getNotes($channel);
130                 $this->notes[$channel->id][$name] = $value;
131         }
132
133         private function collectClassifications(Channel $channel) {
134                 $classifications = [];
135                 $notes = $this->getNotes($channel);
136                 foreach ($notes['latest_msgs'] as $msg) {
137                         $classification = $msg->classify();
138                         if ($classification == 'unclassified') continue;
139                         if (isset($classifications[$classification])) {
140                                 ++$classifications[$classification];
141                         } else {
142                                 $classifications[$classification] = 1;
143                         }
144                 }
145                 arsort($classifications);
146                 return $classifications;
147         }
148
149         private function contextualMsg(Channel $channel) {
150                 if ($this->hasQueuedSpecial($channel)) {
151                         $classification = $this->getQueuedSpecial($channel);
152                         if (is_string($classification)) {
153                                 $this->tagChannelSpecialSent($channel, $classification);
154                         }
155                         $this->clearQueuedSpecial($channel);
156                         return $this->getRandomOfClass($channel, $classification);
157                 }
158                 $latest_msg = $this->getLatestMessage($channel);
159                 if ($latest_msg->classify() == 'question') {
160                         $response = $latest_msg->getResponseCategory();
161                         return $this->getRandomOfClass($channel, $response);
162                 }
163                 $last = $this->getLastSpecialSent($channel);
164                 $classifications = $this->collectClassifications($channel);
165                 $count_quotas = [
166                         'gg' => 2,
167                         'gl' => 2,
168                         'hi' => 2,
169                         'hype' => 2,
170                         'lol' => 2,
171                         'love' => 2,
172                         'number' => 2,
173                         'pog' => 2,
174                         'o7' => 2,
175                         'wtf' => 2,
176                 ];
177                 $time_quotas = [
178                         'gg' => 600,
179                         'gl' => 900,
180                         'hi' => 60,
181                         'hype' => 60,
182                         'lol' => 60,
183                         'love' => 60,
184                         'number' => 300,
185                         'pog' => 60,
186                         'o7' => 300,
187                         'wtf' => 60,
188                 ];
189                 foreach ($classifications as $classification => $count) {
190                         if ($classification == $last) continue;
191                         if (!isset($count_quotas[$classification]) || $count < $count_quotas[$classification]) continue;
192                         if (!isset($time_quotas[$classification]) || $this->getTimeSinceSpecial($channel, $classification) < $time_quotas[$classification]) continue;
193                         $this->tagChannelSpecialSent($channel, $classification);
194                         $reaction = $this->getChimeInReaction($channel, $classification);
195                         return $this->getRandomOfClass($channel, $reaction);
196                 }
197                 return false;
198         }
199
200         private function randomChat(Channel $channel) {
201                 return $channel->queryChatlog()
202                         ->whereNotIn('classification', ['gg', 'gl', 'number', 'o7'])
203                         ->first();
204         }
205
206         private function randomContextualNumber(Channel $channel) {
207                 $notes = $this->getNotes($channel);
208                 $min = 100000;
209                 $max = 0;
210                 foreach ($notes['latest_msgs'] as $msg) {
211                         if ($msg->classify() == 'number') {
212                                 $number = $msg->getNumericValue();
213                                 $min = min($min, $number);
214                                 $max = max($max, $number);
215                         }
216                 }
217                 return random_int($min, $max);
218         }
219
220         private function randomLaughter(Channel $channel) {
221                 if (!random_int(0, 2)) {
222                         return $channel->randomOfClass('lol');
223                 }
224                 return Arr::random([
225                         ':tf:',
226                         '4Head',
227                         'CarlSmile',
228                         'CruW',
229                         'DendiFace',
230                         'EleGiggle',
231                         'GunRun',
232                         'heh',
233                         'Hhhehehe',
234                         'Jebaited',
235                         'Jebasted',
236                         'KEKW',
237                         'KEKHeim',
238                         'KKona',
239                         'KomodoHype',
240                         'MaxLOL',
241                         'MingLee',
242                         'lol',
243                         'LOL!',
244                         'LUL',
245                         'OneHand',
246                         'SeemsGood',
247                         'ShadyLulu',
248                         'SoonerLater',
249                         'SUBprise',
250                         'xD',
251                         'YouDontSay',
252                 ]);
253         }
254
255         private function randomMsg(Channel $channel) {
256                 return $channel->queryChatlog()->first();
257         }
258
259         private function performAdlib(Channel $channel) {
260                 $db = $this->getChatlibDatabase($channel);
261                 $text = $db->generate();
262                 $this->tagChannelWrite($channel);
263                 $this->sendIRCMessage(IRCMessage::privmsg($channel->twitch_chat, $text));
264                 $log = new ChatBotLog();
265                 $log->channel()->associate($channel);
266                 $log->category = 'adlib';
267                 $log->text = $text;
268                 $log->save();
269         }
270
271         private function randomWaitMsgs(Channel $channel) {
272                 $min = $channel->getChatSetting('wait_msgs_min', 1);
273                 $max = $channel->getChatSetting('wait_msgs_max', 10);
274                 return random_int($min, $max);
275         }
276
277         private function randomWaitTime(Channel $channel) {
278                 $min = $channel->getChatSetting('wait_time_min', 1);
279                 $max = $channel->getChatSetting('wait_time_max', 900);
280                 return random_int($min, $max);
281         }
282
283         private function queueSpecial(Channel $channel, $classification) {
284                 $this->getNotes($channel);
285                 $this->notes[$channel->id]['queued_special'] = $classification;
286         }
287
288         private function hasQueuedSpecial(Channel $channel) {
289                 return !!$this->getQueuedSpecial($channel);
290         }
291
292         private function getQueuedSpecial(Channel $channel) {
293                 $notes = $this->getNotes($channel);
294                 return $notes['queued_special'];
295         }
296
297         private function clearQueuedSpecial(Channel $channel) {
298                 $this->getNotes($channel);
299                 $this->notes[$channel->id]['queued_special'] = false;
300         }
301
302         private function tagChannelRead(Channel $channel, IRCMessage $msg) {
303                 $this->getNotes($channel);
304                 $this->notes[$channel->id]['last_read'] = time();
305                 ++$this->notes[$channel->id]['read_since_last_write'];
306
307                 $tokenized = $msg->tokenize();
308                 if (!ChatLog::isKnownBot($msg->nick) && !$tokenized->isSpammy()) {
309                         $this->notes[$channel->id]['latest_msgs'][] = $tokenized;
310                         if (count($this->notes[$channel->id]['latest_msgs']) > 10) {
311                                 array_shift($this->notes[$channel->id]['latest_msgs']);
312                         }
313                 }
314                 if ($this->isDirectedAtMe($msg->getText()) && $this->shouldRespond($channel)) {
315                         $this->notes[$channel->id]['wait_msgs'] = 0;
316                         $this->notes[$channel->id]['wait_time'] = 0;
317                         $response = $tokenized->getResponseCategory();
318                         if ($response) {
319                                 $this->queueSpecial($channel, $response);
320                         }
321                 }
322         }
323
324         private function tagChannelWrite(Channel $channel) {
325                 $this->getNotes($channel);
326                 $this->notes[$channel->id]['last_write'] = time();
327                 $this->notes[$channel->id]['read_since_last_write'] = 0;
328                 $this->notes[$channel->id]['wait_msgs'] = $this->randomWaitMsgs($channel);
329                 $this->notes[$channel->id]['wait_time'] = $this->randomWaitTime($channel);
330         }
331
332         private function tagChannelSpecialSent(Channel $channel, $classification) {
333                 $this->getNotes($channel);
334                 $this->notes[$channel->id]['last_special'][$classification] = time();
335         }
336
337         private function getLatestMessage(Channel $channel) {
338                 $this->getNotes($channel);
339                 if (!empty($notes['latest_msgs'])) {
340                         return $notes['latest_msgs'][count($notes['latest_msgs']) - 1];
341                 }
342                 return TokenizedMessage::fromString('');
343         }
344
345         private function getLastSpecialSent(Channel $channel) {
346                 $notes = $this->getNotes($channel);
347                 $max_time = 0;
348                 $max_classification = '';
349                 foreach ($notes['last_special'] as $classification => $time) {
350                         if ($time > $max_time) {
351                                 $max_time = $time;
352                                 $max_classification = $classification;
353                         }
354                 }
355                 return $max_classification;
356         }
357
358         private function getTimeSinceSpecial(Channel $channel, $classification) {
359                 $notes = $this->getNotes($channel);
360                 if (isset($notes['last_special'][$classification])) {
361                         return time() - $notes['last_special'][$classification];
362                 }
363                 return 999999;
364         }
365
366         private function isDirectedAtMe($raw_text) {
367                 $text = strtolower($raw_text);
368                 if (strpos($text, 'horsti') !== false) {
369                         return true;
370                 }
371                 return false;
372         }
373
374         private function shouldAdlib(Channel $channel) {
375                 $setting = $channel->getChatSetting('adlib', 50);
376                 if ($setting == 0) {
377                         return false;
378                 }
379                 if ($setting == 100) {
380                         return true;
381                 }
382                 return random_int(0, 100) <= $setting;
383         }
384
385         private function shouldRespond(Channel $channel) {
386                 $setting = $channel->getChatSetting('respond', 'yes');
387                 if ($setting == 'yes') {
388                         return true;
389                 }
390                 if ($setting == '50') {
391                         return random_int(0, 1);
392                 }
393                 return false;
394         }
395
396         private function getRandomOfClass(Channel $channel, $classification) {
397                 if ($classification == 'number') {
398                         return $this->randomContextualNumber($channel);
399                 }
400                 if ($classification == 'lol') {
401                         return $this->randomLaughter($channel);
402                 }
403                 return $channel->randomOfClass($classification);
404         }
405
406         private function getChimeInReaction(Channel $channel, $classification) {
407                 switch ($classification) {
408                         case 'hi':
409                                 return ['hi', 'love'];
410                         case 'hype':
411                                 return ['hype', 'love', 'pog'];
412                         case 'lol':
413                                 return ['kappa', 'lol'];
414                         case 'pog':
415                                 return ['hype', 'pog'];
416                         case 'wtf':
417                                 return ['lol', 'wtf'];
418                 }
419                 return $classification;
420         }
421
422         private $channels;
423         private $notes = [];
424         private $chatlib;
425
426 }