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