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