]> git.localhorst.tv Git - alttp.git/blob - app/TwitchBot/TwitchChatBot.php
90a718d25b36875591e44b7ea9baf8d33fba69f6
[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                                 'read_since_last_write' => 0,
98                                 'wait_msgs' => $this->randomWaitMsgs($channel),
99                                 'wait_time' => $this->randomWaitTime($channel),
100                         ];
101                 }
102                 return $this->notes[$channel->id];
103         }
104
105         private function getNote(Channel $channel, $name, $default = null) {
106                 $notes = $this->getNotes($channel);
107                 if (array_key_exists($name, $notes)) {
108                         return $notes[$name];
109                 }
110                 return $default;
111         }
112
113         private function setNote(Channel $channel, $name, $value) {
114                 $this->getNotes($channel);
115                 $this->notes[$channel->id][$name] = $value;
116         }
117
118         private function collectClassifications(Channel $channel) {
119                 $classifications = [];
120                 $notes = $this->getNotes($channel);
121                 foreach ($notes['latest_msgs'] as $msg) {
122                         $classification = $msg->classify();
123                         if ($classification == 'unclassified') continue;
124                         if (isset($classifications[$classification])) {
125                                 ++$classifications[$classification];
126                         } else {
127                                 $classifications[$classification] = 1;
128                         }
129                 }
130                 arsort($classifications);
131                 return $classifications;
132         }
133
134         private function contextualMsg(Channel $channel) {
135                 $last = $this->getNote($channel, 'last_special');
136                 $classifications = $this->collectClassifications($channel);
137                 $count_quotas = [
138                         'gg' => 2,
139                         'gl' => 2,
140                         'hi' => 2,
141                         'hype' => 2,
142                         'lol' => 2,
143                         'number' => 2,
144                         'pog' => 2,
145                         'o7' => 2,
146                 ];
147                 $time_quotas = [
148                         'gg' => 300,
149                         'gl' => 900,
150                         'hi' => 60,
151                         'hype' => 60,
152                         'lol' => 60,
153                         'number' => 300,
154                         'pog' => 60,
155                         'o7' => 300,
156                 ];
157                 foreach ($classifications as $classification => $count) {
158                         if ($classification == $last) continue;
159                         if (!isset($count_quotas[$classification]) || $count < $count_quotas[$classification]) continue;
160                         if (!isset($time_quotas[$classification]) || $this->getTimeSinceSpecial($channel, $classification) < $time_quotas[$classification]) continue;
161                         $this->tagChannelSpecialSent($channel, $classification);
162                         if ($classification == 'number') {
163                                 return $this->randomContextualNumber($channel);
164                         }
165                         if ($classification == 'lol') {
166                                 return $this->randomLaughter($channel);
167                         }
168                         return $channel->randomOfClass($classification);
169                 }
170                 return false;
171         }
172
173         private function randomChat(Channel $channel) {
174                 return $channel->queryChatlog()
175                         ->whereIn('classification', ['hi', 'hype', 'lol', 'pog', 'unclassified'])
176                         ->first();
177         }
178
179         private function randomContextualNumber(Channel $channel) {
180                 $notes = $this->getNotes($channel);
181                 $min = 100000;
182                 $max = 0;
183                 foreach ($notes['latest_msgs'] as $msg) {
184                         if ($msg->classify() == 'number') {
185                                 $number = $msg->getNumericValue();
186                                 $min = min($min, $number);
187                                 $max = max($max, $number);
188                         }
189                 }
190                 return random_int($min, $max);
191         }
192
193         private function randomLaughter(Channel $channel) {
194                 return Arr::random([
195                         ':tf:',
196                         '4Head',
197                         'CarlSmile',
198                         'CruW',
199                         'DendiFace',
200                         'EleGiggle',
201                         'GunRun',
202                         'heh',
203                         'Hhhehehe',
204                         'HypeLUL',
205                         'Jebaited',
206                         'Jebasted',
207                         'KEKW',
208                         'KEKHeim',
209                         'KKona',
210                         'KomodoHype',
211                         'MaxLOL',
212                         'MingLee',
213                         'lol',
214                         'LOL!',
215                         'LUL',
216                         'OneHand',
217                         'SeemsGood',
218                         'ShadyLulu',
219                         'SoonerLater',
220                         'SUBprise',
221                         'xD',
222                         'YouDontSay',
223                         $channel->randomOfClass('lol'),
224                 ]);
225         }
226
227         private function randomMsg(Channel $channel) {
228                 return $channel->queryChatlog()->first();
229         }
230
231         private function randomWaitMsgs(Channel $channel) {
232                 $min = $channel->getChatSetting('wait_msgs_min', 1);
233                 $max = $channel->getChatSetting('wait_msgs_max', 10);
234                 return random_int($min, $max);
235         }
236
237         private function randomWaitTime(Channel $channel) {
238                 $min = $channel->getChatSetting('wait_time_min', 1);
239                 $max = $channel->getChatSetting('wait_time_max', 900);
240                 return random_int($min, $max);
241         }
242
243         private function tagChannelRead(Channel $channel, IRCMessage $msg) {
244                 $this->getNotes($channel);
245                 $this->notes[$channel->id]['last_read'] = time();
246                 ++$this->notes[$channel->id]['read_since_last_write'];
247
248                 $tokenized = $msg->tokenize();
249                 if (!ChatLog::isKnownBot($msg->nick) && !$tokenized->isSpammy()) {
250                         $this->notes[$channel->id]['latest_msgs'][] = $tokenized;
251                         if (count($this->notes[$channel->id]['latest_msgs']) > 10) {
252                                 array_shift($this->notes[$channel->id]['latest_msgs']);
253                         }
254                 }
255                 if ($this->isDirectedAtMe($msg->getText()) && $this->shouldRespond($channel)) {
256                         $this->notes[$channel->id]['wait_msgs'] = 0;
257                         $this->notes[$channel->id]['wait_time'] = 0;
258                 }
259         }
260
261         private function tagChannelWrite(Channel $channel) {
262                 $this->getNotes($channel);
263                 $this->notes[$channel->id]['last_write'] = time();
264                 $this->notes[$channel->id]['read_since_last_write'] = 0;
265                 $this->notes[$channel->id]['wait_msgs'] = $this->randomWaitMsgs($channel);
266                 $this->notes[$channel->id]['wait_time'] = $this->randomWaitTime($channel);
267         }
268
269         private function tagChannelSpecialSent(Channel $channel, $classification) {
270                 $this->getNotes($channel);
271                 $this->notes[$channel->id]['last_special'][$classification] = time();
272         }
273
274         private function getTimeSinceSpecial(Channel $channel, $classification) {
275                 $notes = $this->getNotes($channel);
276                 if (isset($notes['last_special'][$classification])) {
277                         return time() - $notes['last_special'][$classification];
278                 }
279                 return 999999;
280         }
281
282         private function isDirectedAtMe($raw_text) {
283                 $text = strtolower($raw_text);
284                 if (strpos($text, 'horsti') !== false) {
285                         return true;
286                 }
287                 return false;
288         }
289
290         private function shouldRespond(Channel $channel) {
291                 $setting = $channel->getChatSetting('respond', 'yes');
292                 if ($setting == 'yes') {
293                         return true;
294                 }
295                 if ($setting == '50') {
296                         return random_int(0, 1);
297                 }
298                 return false;
299         }
300
301         private $channels;
302         private $notes = [];
303
304 }