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