]> git.localhorst.tv Git - alttp.git/blob - app/TwitchBot/TwitchChatBot.php
a403b0b57b716ae488c1056e8f3e6d8be53573f8
[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                         if ($classification == 'number') {
143                                 return $this->randomContextualNumber($channel);
144                         }
145                         if ($classification == 'lol') {
146                                 return $this->randomLaughter($channel);
147                         }
148                         return $channel->randomOfClass($classification);
149                 }
150                 $last = $this->getLastSpecialSent($channel);
151                 $classifications = $this->collectClassifications($channel);
152                 $count_quotas = [
153                         'gg' => 2,
154                         'gl' => 2,
155                         'hi' => 2,
156                         'hype' => 2,
157                         'lol' => 2,
158                         'love' => 2,
159                         'number' => 2,
160                         'pog' => 2,
161                         'o7' => 2,
162                         'wtf' => 2,
163                 ];
164                 $time_quotas = [
165                         'gg' => 600,
166                         'gl' => 900,
167                         'hi' => 60,
168                         'hype' => 60,
169                         'lol' => 60,
170                         'love' => 60,
171                         'number' => 300,
172                         'pog' => 60,
173                         'o7' => 300,
174                         'wtf' => 60,
175                 ];
176                 foreach ($classifications as $classification => $count) {
177                         if ($classification == $last) continue;
178                         if (!isset($count_quotas[$classification]) || $count < $count_quotas[$classification]) continue;
179                         if (!isset($time_quotas[$classification]) || $this->getTimeSinceSpecial($channel, $classification) < $time_quotas[$classification]) continue;
180                         $this->tagChannelSpecialSent($channel, $classification);
181                         if ($classification == 'number') {
182                                 return $this->randomContextualNumber($channel);
183                         }
184                         if ($classification == 'lol') {
185                                 return $this->randomLaughter($channel);
186                         }
187                         return $channel->randomOfClass($classification);
188                 }
189                 return false;
190         }
191
192         private function randomChat(Channel $channel) {
193                 return $channel->queryChatlog()
194                         ->whereNotIn('classification', ['gg', 'gl', 'number', 'o7'])
195                         ->first();
196         }
197
198         private function randomContextualNumber(Channel $channel) {
199                 $notes = $this->getNotes($channel);
200                 $min = 100000;
201                 $max = 0;
202                 foreach ($notes['latest_msgs'] as $msg) {
203                         if ($msg->classify() == 'number') {
204                                 $number = $msg->getNumericValue();
205                                 $min = min($min, $number);
206                                 $max = max($max, $number);
207                         }
208                 }
209                 return random_int($min, $max);
210         }
211
212         private function randomLaughter(Channel $channel) {
213                 if (!random_int(0, 2)) {
214                         return $channel->randomOfClass('lol');
215                 }
216                 return Arr::random([
217                         ':tf:',
218                         '4Head',
219                         'CarlSmile',
220                         'CruW',
221                         'DendiFace',
222                         'EleGiggle',
223                         'GunRun',
224                         'heh',
225                         'Hhhehehe',
226                         'Jebaited',
227                         'Jebasted',
228                         'KEKW',
229                         'KEKHeim',
230                         'KKona',
231                         'KomodoHype',
232                         'MaxLOL',
233                         'MingLee',
234                         'lol',
235                         'LOL!',
236                         'LUL',
237                         'OneHand',
238                         'SeemsGood',
239                         'ShadyLulu',
240                         'SoonerLater',
241                         'SUBprise',
242                         'xD',
243                         'YouDontSay',
244                 ]);
245         }
246
247         private function randomMsg(Channel $channel) {
248                 return $channel->queryChatlog()->first();
249         }
250
251         private function randomWaitMsgs(Channel $channel) {
252                 $min = $channel->getChatSetting('wait_msgs_min', 1);
253                 $max = $channel->getChatSetting('wait_msgs_max', 10);
254                 return random_int($min, $max);
255         }
256
257         private function randomWaitTime(Channel $channel) {
258                 $min = $channel->getChatSetting('wait_time_min', 1);
259                 $max = $channel->getChatSetting('wait_time_max', 900);
260                 return random_int($min, $max);
261         }
262
263         private function queueSpecial(Channel $channel, $classification) {
264                 $this->getNotes($channel);
265                 $this->notes[$channel->id]['queued_special'] = $classification;
266         }
267
268         private function hasQueuedSpecial(Channel $channel) {
269                 return !!$this->getQueuedSpecial($channel);
270         }
271
272         private function getQueuedSpecial(Channel $channel) {
273                 $notes = $this->getNotes($channel);
274                 return $notes['queued_special'];
275         }
276
277         private function clearQueuedSpecial(Channel $channel) {
278                 $this->getNotes($channel);
279                 $this->notes[$channel->id]['queued_special'] = false;
280         }
281
282         private function tagChannelRead(Channel $channel, IRCMessage $msg) {
283                 $this->getNotes($channel);
284                 $this->notes[$channel->id]['last_read'] = time();
285                 ++$this->notes[$channel->id]['read_since_last_write'];
286
287                 $tokenized = $msg->tokenize();
288                 if (!ChatLog::isKnownBot($msg->nick) && !$tokenized->isSpammy()) {
289                         $this->notes[$channel->id]['latest_msgs'][] = $tokenized;
290                         if (count($this->notes[$channel->id]['latest_msgs']) > 10) {
291                                 array_shift($this->notes[$channel->id]['latest_msgs']);
292                         }
293                 }
294                 if ($this->isDirectedAtMe($msg->getText()) && $this->shouldRespond($channel)) {
295                         $this->notes[$channel->id]['wait_msgs'] = 0;
296                         $this->notes[$channel->id]['wait_time'] = 0;
297                         $response = $this->getResponseTo($tokenized);
298                         if ($response) {
299                                 $this->queueSpecial($channel, $response);
300                         }
301                 }
302         }
303
304         private function tagChannelWrite(Channel $channel) {
305                 $this->getNotes($channel);
306                 $this->notes[$channel->id]['last_write'] = time();
307                 $this->notes[$channel->id]['read_since_last_write'] = 0;
308                 $this->notes[$channel->id]['wait_msgs'] = $this->randomWaitMsgs($channel);
309                 $this->notes[$channel->id]['wait_time'] = $this->randomWaitTime($channel);
310         }
311
312         private function tagChannelSpecialSent(Channel $channel, $classification) {
313                 $this->getNotes($channel);
314                 $this->notes[$channel->id]['last_special'][$classification] = time();
315         }
316
317         private function getLastSpecialSent(Channel $channel) {
318                 $notes = $this->getNotes($channel);
319                 $max_time = 0;
320                 $max_classification = '';
321                 foreach ($notes['last_special'] as $classification => $time) {
322                         if ($time > $max_time) {
323                                 $max_time = $time;
324                                 $max_classification = $classification;
325                         }
326                 }
327                 return $max_classification;
328         }
329
330         private function getTimeSinceSpecial(Channel $channel, $classification) {
331                 $notes = $this->getNotes($channel);
332                 if (isset($notes['last_special'][$classification])) {
333                         return time() - $notes['last_special'][$classification];
334                 }
335                 return 999999;
336         }
337
338         private function isDirectedAtMe($raw_text) {
339                 $text = strtolower($raw_text);
340                 if (strpos($text, 'horsti') !== false) {
341                         return true;
342                 }
343                 return false;
344         }
345
346         private function shouldRespond(Channel $channel) {
347                 $setting = $channel->getChatSetting('respond', 'yes');
348                 if ($setting == 'yes') {
349                         return true;
350                 }
351                 if ($setting == '50') {
352                         return random_int(0, 1);
353                 }
354                 return false;
355         }
356
357         private function getResponseTo(TokenizedMessage $msg) {
358                 switch ($msg->classify()) {
359                         case 'gg':
360                                 return ['love', 'eyes', 'thx', 'pog', 'kappa'];
361                         case 'gl':
362                                 return ['love', 'eyes', 'thx'];
363                         case 'hi':
364                                 return ['hi', 'love', 'eyes', 'hype', 'pog'];
365                         case 'kappa':
366                                 return ['kappa', 'lol', 'eyes'];
367                         case 'love':
368                                 return ['hi', 'love', 'eyes', 'thx'];
369                         case 'question':
370                                 return ['yes', 'no', 'kappa', 'lol', 'wtf', 'number'];
371                         case 'rage':
372                                 return ['kappa', 'lol', 'rage'];
373                         case 'wtf':
374                                 return ['kappa', 'lol', 'rage'];
375                 }
376                 return false;
377         }
378
379         private $channels;
380         private $notes = [];
381
382 }