]> git.localhorst.tv Git - alttp.git/blob - app/TwitchBot/TwitchChatBot.php
better quota system for contextual messages
[alttp.git] / app / TwitchBot / TwitchChatBot.php
1 <?php
2
3 namespace App\TwitchBot;
4
5 use App\Models\Channel;
6 use App\Models\ChatLog;
7 use Illuminate\Support\Arr;
8 use Illuminate\Support\Str;
9
10 class TwitchChatBot extends TwitchBot {
11
12         public function __construct() {
13                 parent::__construct('horstiebot');
14                 $this->updateChannels();
15                 $this->startTimer();
16                 $this->listenCommands();
17         }
18
19         public function joinChannels() {
20                 $this->getLogger()->info('joining channels');
21                 $names = [];
22                 foreach ($this->channels as $channel) {
23                         $names[] = $channel->twitch_chat;
24                 }
25                 $chunks = array_chunk($names, 10);
26                 foreach ($chunks as $chunk) {
27                         $this->sendIRCMessage(IRCMessage::join($chunk));
28                 }
29         }
30
31         public function logMessage(IRCMessage $msg) {
32                 $channel = $this->getMessageChannel($msg);
33                 if ($channel && !$channel->join) {
34                         $msg->log();
35                 }
36         }
37
38         public function handlePrivMsg(IRCMessage $msg) {
39                 if ($msg->nick == 'horstiebot') return;
40                 $channel = $this->getMessageChannel($msg);
41                 if (!$channel) return;
42                 $this->tagChannelRead($channel, $msg);
43         }
44
45
46         private function startTimer() {
47                 $this->getLoop()->addPeriodicTimer(1, function () {
48                         if (!$this->isReady()) return;
49                         foreach ($this->channels as $channel) {
50                                 $this->decideSend($channel);
51                         }
52                 });
53                 $this->getLoop()->addPeriodicTimer(60, function () {
54                         $this->updateChannels();
55                 });
56         }
57
58         private function updateChannels() {
59                 $this->channels = Channel::where('twitch_chat', '!=', '')->where('chat', '=', true)->get();
60         }
61
62         private function decideSend(Channel $channel) {
63                 $notes = $this->getNotes($channel);
64                 if ($notes['read_since_last_write'] < $notes['wait_msgs']) {
65                         return;
66                 }
67                 if (time() - $notes['last_write'] < $notes['wait_time']) {
68                         return;
69                 }
70                 if ($notes['read_since_last_write'] == $notes['wait_msgs'] && time() - $notes['last_read'] < 3) {
71                         // don't immediately respond if we crossed the msg threshold last
72                         return;
73                 }
74                 $text = $this->contextualMsg($channel);
75                 if (!$text) $text = $this->randomChat($channel);
76                 if (!$text) return;
77                 $this->tagChannelWrite($channel);
78                 $this->sendIRCMessage(IRCMessage::privmsg($channel->twitch_chat, $text));
79         }
80
81         private function getNotes(Channel $channel) {
82                 if (!isset($this->notes[$channel->id])) {
83                         $this->notes[$channel->id] = [
84                                 'last_read' => 0,
85                                 'last_special' => [],
86                                 'last_write' => time(),
87                                 'latest_msgs' => [],
88                                 'read_since_last_write' => 0,
89                                 'wait_msgs' => $this->randomWaitMsgs($channel),
90                                 'wait_time' => $this->randomWaitTime($channel),
91                         ];
92                 }
93                 return $this->notes[$channel->id];
94         }
95
96         private function getNote(Channel $channel, $name, $default = null) {
97                 $notes = $this->getNotes($channel);
98                 if (array_key_exists($name, $notes)) {
99                         return $notes[$name];
100                 }
101                 return $default;
102         }
103
104         private function setNote(Channel $channel, $name, $value) {
105                 $this->getNotes($channel);
106                 $this->notes[$channel->id][$name] = $value;
107         }
108
109         private function collectClassifications(Channel $channel) {
110                 $classifications = [];
111                 $notes = $this->getNotes($channel);
112                 foreach ($notes['latest_msgs'] as $msg) {
113                         $classification = $msg->classify();
114                         if ($classification == 'unclassified') continue;
115                         if (isset($classifications[$classification])) {
116                                 ++$classifications[$classification];
117                         } else {
118                                 $classifications[$classification] = 1;
119                         }
120                 }
121                 arsort($classifications);
122                 return $classifications;
123         }
124
125         private function contextualMsg(Channel $channel) {
126                 $last = $this->getNote($channel, 'last_special');
127                 $classifications = $this->collectClassifications($channel);
128                 $count_quotas = [
129                         'gg' => 2,
130                         'gl' => 2,
131                         'hi' => 2,
132                         'hype' => 2,
133                         'lol' => 2,
134                         'number' => 2,
135                         'pog' => 2,
136                         'o7' => 2,
137                 ];
138                 $time_quotas = [
139                         'gg' => 300,
140                         'gl' => 900,
141                         'hi' => 60,
142                         'hype' => 60,
143                         'lol' => 60,
144                         'number' => 300,
145                         'pog' => 60,
146                         'o7' => 300,
147                 ];
148                 foreach ($classifications as $classification => $count) {
149                         if ($classification == $last) continue;
150                         if (!isset($count_quotas[$classification]) || $count < $count_quotas[$classification]) continue;
151                         if (!isset($time_quotas[$classification]) || $this->getTimeSinceSpecial($channel, $classification) < $time_quotas[$classification]) continue;
152                         $this->tagChannelSpecialSent($channel, $classification);
153                         if ($classification == 'number') {
154                                 return $this->randomContextualNumber($channel);
155                         }
156                         if ($classification == 'lol') {
157                                 return $this->randomLaughter($channel);
158                         }
159                         return $channel->randomOfClass($classification);
160                 }
161                 return false;
162         }
163
164         private function randomChat(Channel $channel) {
165                 $line = $channel->queryChatlog()
166                         ->whereIn('classification', ['hi', 'hype', 'lol', 'pog', 'unclassified'])
167                         ->first();
168                 return $line->text_content;
169         }
170
171         private function randomContextualNumber(Channel $channel) {
172                 $notes = $this->getNotes($channel);
173                 $min = 100000;
174                 $max = 0;
175                 foreach ($notes['latest_msgs'] as $msg) {
176                         if ($msg->classify() == 'number') {
177                                 $number = $msg->getNumericValue();
178                                 $min = min($min, $number);
179                                 $max = max($max, $number);
180                         }
181                 }
182                 return random_int($min, $max);
183         }
184
185         private function randomLaughter(Channel $channel) {
186                 return Arr::random([
187                         ':tf:',
188                         '4Head',
189                         'CarlSmile',
190                         'CruW',
191                         'DendiFace',
192                         'EleGiggle',
193                         'GunRun',
194                         'heh',
195                         'Hhhehehe',
196                         'HypeLUL',
197                         'Jebaited',
198                         'Jebasted',
199                         'KEKW',
200                         'KEKHeim',
201                         'KKona',
202                         'KomodoHype',
203                         'MaxLOL',
204                         'MingLee',
205                         'lol',
206                         'LOL!',
207                         'LUL',
208                         'OneHand',
209                         'SeemsGood',
210                         'ShadyLulu',
211                         'SoonerLater',
212                         'SUBprise',
213                         'xD',
214                         'YouDontSay',
215                         $channel->randomOfClass('lol'),
216                 ]);
217         }
218
219         private function randomMsg(Channel $channel) {
220                 $line = $channel->queryChatlog()->first();
221                 return $line->text_content;
222         }
223
224         private function randomWaitMsgs(Channel $channel) {
225                 $min = $channel->getChatSetting('wait_msgs_min', 1);
226                 $max = $channel->getChatSetting('wait_msgs_max', 10);
227                 return random_int($min, $max);
228         }
229
230         private function randomWaitTime(Channel $channel) {
231                 $min = $channel->getChatSetting('wait_time_min', 1);
232                 $max = $channel->getChatSetting('wait_time_max', 900);
233                 return random_int($min, $max);
234         }
235
236         private function tagChannelRead(Channel $channel, IRCMessage $msg) {
237                 $this->getNotes($channel);
238                 $this->notes[$channel->id]['last_read'] = time();
239                 ++$this->notes[$channel->id]['read_since_last_write'];
240
241                 $tokenized = $msg->tokenize();
242                 if (!ChatLog::isKnownBot($msg->nick) && !$tokenized->isSpammy()) {
243                         $this->notes[$channel->id]['latest_msgs'][] = $tokenized;
244                         if (count($this->notes[$channel->id]['latest_msgs']) > 10) {
245                                 array_shift($this->notes[$channel->id]['latest_msgs']);
246                         }
247                 }
248                 if ($this->isDirectedAtMe($msg->getText()) && $this->shouldRespond($channel)) {
249                         $this->notes[$channel->id]['wait_msgs'] = 0;
250                         $this->notes[$channel->id]['wait_time'] = 0;
251                 }
252         }
253
254         private function tagChannelWrite(Channel $channel) {
255                 $this->getNotes($channel);
256                 $this->notes[$channel->id]['last_write'] = time();
257                 $this->notes[$channel->id]['read_since_last_write'] = 0;
258                 $this->notes[$channel->id]['wait_msgs'] = $this->randomWaitMsgs($channel);
259                 $this->notes[$channel->id]['wait_time'] = $this->randomWaitTime($channel);
260         }
261
262         private function tagChannelSpecialSent(Channel $channel, $classification) {
263                 $this->getNotes($channel);
264                 $this->notes[$channel->id]['last_special'][$classification] = time();
265         }
266
267         private function getTimeSinceSpecial(Channel $channel, $classification) {
268                 $notes = $this->getNotes($channel);
269                 if (isset($notes['last_special'][$classification])) {
270                         return time() - $notes['last_special'][$classification];
271                 }
272                 return 999999;
273         }
274
275         private function isDirectedAtMe($raw_text) {
276                 $text = strtolower($raw_text);
277                 if (strpos($text, 'horsti') !== false) {
278                         return true;
279                 }
280                 return false;
281         }
282
283         private function shouldRespond(Channel $channel) {
284                 $setting = $channel->getChatSetting('respond', 'yes');
285                 if ($setting == 'yes') {
286                         return true;
287                 }
288                 if ($setting == '50') {
289                         return random_int(0, 1);
290                 }
291                 return false;
292         }
293
294         private $channels;
295         private $notes = [];
296
297 }