]> git.localhorst.tv Git - alttp.git/blob - app/TwitchBot/TwitchChatBot.php
add context to adlibs
[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 (!$text && $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                 $latest_msg = $this->getLatestMessage($channel);
262                 $text = $db->generate($latest_msg->getText());
263                 $this->tagChannelWrite($channel);
264                 $this->sendIRCMessage(IRCMessage::privmsg($channel->twitch_chat, $text));
265                 $log = new ChatBotLog();
266                 $log->channel()->associate($channel);
267                 $log->category = 'adlib';
268                 $log->text = $text;
269                 $log->save();
270         }
271
272         private function randomWaitMsgs(Channel $channel) {
273                 $min = $channel->getChatSetting('wait_msgs_min', 1);
274                 $max = $channel->getChatSetting('wait_msgs_max', 10);
275                 return random_int($min, $max);
276         }
277
278         private function randomWaitTime(Channel $channel) {
279                 $min = $channel->getChatSetting('wait_time_min', 1);
280                 $max = $channel->getChatSetting('wait_time_max', 900);
281                 return random_int($min, $max);
282         }
283
284         private function queueSpecial(Channel $channel, $classification) {
285                 $this->getNotes($channel);
286                 $this->notes[$channel->id]['queued_special'] = $classification;
287         }
288
289         private function hasQueuedSpecial(Channel $channel) {
290                 return !!$this->getQueuedSpecial($channel);
291         }
292
293         private function getQueuedSpecial(Channel $channel) {
294                 $notes = $this->getNotes($channel);
295                 return $notes['queued_special'];
296         }
297
298         private function clearQueuedSpecial(Channel $channel) {
299                 $this->getNotes($channel);
300                 $this->notes[$channel->id]['queued_special'] = false;
301         }
302
303         private function tagChannelRead(Channel $channel, IRCMessage $msg) {
304                 $this->getNotes($channel);
305                 $this->notes[$channel->id]['last_read'] = time();
306                 ++$this->notes[$channel->id]['read_since_last_write'];
307
308                 $tokenized = $msg->tokenize();
309                 if (!ChatLog::isKnownBot($msg->nick) && !$tokenized->isSpammy()) {
310                         $this->noteChannelMessage($channel, $tokenized);
311                 }
312                 if ($this->isDirectedAtMe($msg->getText()) && $this->shouldRespond($channel)) {
313                         $this->noteChannelMessage($channel, $tokenized);
314                         $this->notes[$channel->id]['wait_msgs'] = 0;
315                         $this->notes[$channel->id]['wait_time'] = 0;
316                         $response = $tokenized->getResponseCategory();
317                         if ($response) {
318                                 $this->queueSpecial($channel, $response);
319                         }
320                 }
321         }
322
323         private function noteChannelMessage(Channel $channel, TokenizedMessage $tokenized) {
324                 $this->notes[$channel->id]['latest_msgs'][] = $tokenized;
325                 if (count($this->notes[$channel->id]['latest_msgs']) > 10) {
326                         array_shift($this->notes[$channel->id]['latest_msgs']);
327                 }
328         }
329
330         private function tagChannelWrite(Channel $channel) {
331                 $this->getNotes($channel);
332                 $this->notes[$channel->id]['last_write'] = time();
333                 $this->notes[$channel->id]['read_since_last_write'] = 0;
334                 $this->notes[$channel->id]['wait_msgs'] = $this->randomWaitMsgs($channel);
335                 $this->notes[$channel->id]['wait_time'] = $this->randomWaitTime($channel);
336         }
337
338         private function tagChannelSpecialSent(Channel $channel, $classification) {
339                 $this->getNotes($channel);
340                 $this->notes[$channel->id]['last_special'][$classification] = time();
341         }
342
343         private function getLatestMessage(Channel $channel) {
344                 $notes = $this->getNotes($channel);
345                 if (!empty($notes['latest_msgs'])) {
346                         return $notes['latest_msgs'][count($notes['latest_msgs']) - 1];
347                 }
348                 return TokenizedMessage::fromString('');
349         }
350
351         private function getLastSpecialSent(Channel $channel) {
352                 $notes = $this->getNotes($channel);
353                 $max_time = 0;
354                 $max_classification = '';
355                 foreach ($notes['last_special'] as $classification => $time) {
356                         if ($time > $max_time) {
357                                 $max_time = $time;
358                                 $max_classification = $classification;
359                         }
360                 }
361                 return $max_classification;
362         }
363
364         private function getTimeSinceSpecial(Channel $channel, $classification) {
365                 $notes = $this->getNotes($channel);
366                 if (isset($notes['last_special'][$classification])) {
367                         return time() - $notes['last_special'][$classification];
368                 }
369                 return 999999;
370         }
371
372         private function isDirectedAtMe($raw_text) {
373                 $text = strtolower($raw_text);
374                 if (strpos($text, 'horsti') !== false) {
375                         return true;
376                 }
377                 return false;
378         }
379
380         private function shouldAdlib(Channel $channel) {
381                 $setting = $channel->getChatSetting('adlib', 50);
382                 if ($setting == 0) {
383                         return false;
384                 }
385                 if ($setting == 100) {
386                         return true;
387                 }
388                 return random_int(0, 100) <= $setting;
389         }
390
391         private function shouldRespond(Channel $channel) {
392                 $setting = $channel->getChatSetting('respond', 'yes');
393                 if ($setting == 'yes') {
394                         return true;
395                 }
396                 if ($setting == '50') {
397                         return random_int(0, 1);
398                 }
399                 return false;
400         }
401
402         private function getRandomOfClass(Channel $channel, $classification) {
403                 if ($classification == 'number') {
404                         return $this->randomContextualNumber($channel);
405                 }
406                 if ($classification == 'lol') {
407                         return $this->randomLaughter($channel);
408                 }
409                 return $channel->randomOfClass($classification);
410         }
411
412         private function getChimeInReaction(Channel $channel, $classification) {
413                 switch ($classification) {
414                         case 'hi':
415                                 return ['hi', 'love'];
416                         case 'hype':
417                                 return ['hype', 'love', 'pog'];
418                         case 'lol':
419                                 return ['kappa', 'lol'];
420                         case 'pog':
421                                 return ['hype', 'pog'];
422                         case 'wtf':
423                                 return ['lol', 'wtf'];
424                 }
425                 return $classification;
426         }
427
428         private $channels;
429         private $notes = [];
430         private $chatlib;
431
432 }