]> git.localhorst.tv Git - alttp.git/blob - app/TwitchBot/TwitchChatBot.php
prevent repeat special chat
[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->getLastSpecialSent($channel);
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' => 600,
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 getLastSpecialSent(Channel $channel) {
275                 $notes = $this->getNotes($channel);
276                 $max_time = 0;
277                 $max_classification = '';
278                 foreach ($notes['last_special'] as $classification => $time) {
279                         if ($time > $max_time) {
280                                 $max_time = $time;
281                                 $max_classification = $classification;
282                         }
283                 }
284                 return $max_classification;
285         }
286
287         private function getTimeSinceSpecial(Channel $channel, $classification) {
288                 $notes = $this->getNotes($channel);
289                 if (isset($notes['last_special'][$classification])) {
290                         return time() - $notes['last_special'][$classification];
291                 }
292                 return 999999;
293         }
294
295         private function isDirectedAtMe($raw_text) {
296                 $text = strtolower($raw_text);
297                 if (strpos($text, 'horsti') !== false) {
298                         return true;
299                 }
300                 return false;
301         }
302
303         private function shouldRespond(Channel $channel) {
304                 $setting = $channel->getChatSetting('respond', 'yes');
305                 if ($setting == 'yes') {
306                         return true;
307                 }
308                 if ($setting == '50') {
309                         return random_int(0, 1);
310                 }
311                 return false;
312         }
313
314         private $channels;
315         private $notes = [];
316
317 }