3 namespace App\TwitchBot;
5 use App\Models\Channel;
6 use App\Models\ChatBotLog;
7 use App\Models\ChatLib;
8 use App\Models\ChatLog;
9 use Illuminate\Support\Arr;
10 use Illuminate\Support\Str;
12 class TwitchChatBot extends TwitchBot {
14 public function __construct() {
15 parent::__construct('horstiebot');
16 $this->updateChannels();
18 $this->listenCommands();
19 $this->chatlib = new ChatLib();
20 $this->chatlib->loadFrom('de');
23 public function joinChannels() {
24 $this->getLogger()->info('joining channels');
26 foreach ($this->channels as $channel) {
27 $names[] = $channel->twitch_chat;
29 $chunks = array_chunk($names, 10);
30 foreach ($chunks as $chunk) {
31 $this->sendIRCMessage(IRCMessage::join($chunk));
35 public function logMessage(IRCMessage $msg) {
36 $channel = $this->getMessageChannel($msg);
37 if ($channel && !$channel->join) {
42 public function handlePrivMsg(IRCMessage $msg) {
43 if ($msg->nick == 'horstiebot') return;
44 $channel = $this->getMessageChannel($msg);
45 if (!$channel) return;
46 $this->tagChannelRead($channel, $msg);
49 public function getChatlibDatabase(Channel $channel) {
50 return $this->chatlib;
54 private function startTimer() {
55 $this->getLoop()->addPeriodicTimer(1, function () {
56 if (!$this->isReady()) return;
57 foreach ($this->channels as $channel) {
58 $this->decideSend($channel);
61 $this->getLoop()->addPeriodicTimer(60, function () {
62 $this->updateChannels();
66 private function updateChannels() {
67 $this->channels = Channel::where('twitch_chat', '!=', '')->where('chat', '=', true)->get();
70 private function decideSend(Channel $channel) {
71 $notes = $this->getNotes($channel);
72 if ($notes['read_since_last_write'] < $notes['wait_msgs']) {
75 if (time() - $notes['last_write'] < $notes['wait_time']) {
78 if ($notes['read_since_last_write'] == $notes['wait_msgs'] && time() - $notes['last_read'] < 3) {
79 // don't immediately respond if we crossed the msg threshold last
82 $text = $this->contextualMsg($channel);
83 if (!$text && $this->shouldAdlib($channel)) {
84 $this->performAdlib($channel);
87 if (!$text) $text = $this->randomChat($channel);
89 $actual_text = is_object($text) ? $text->text_content : $text;
90 $this->tagChannelWrite($channel);
91 $this->sendIRCMessage(IRCMessage::privmsg($channel->twitch_chat, $actual_text));
92 $log = new ChatBotLog();
93 $log->channel()->associate($channel);
94 if (is_object($text)) {
95 $log->origin()->associate($text);
96 $log->category = $text->classification;
98 $log->category = $this->getLastSpecialSent($channel);
100 $log->text = $actual_text;
104 private function getNotes(Channel $channel) {
105 if (!isset($this->notes[$channel->id])) {
106 $this->notes[$channel->id] = [
108 'last_special' => [],
109 'last_write' => time(),
111 'queued_special' => false,
112 'read_since_last_write' => 0,
113 'wait_msgs' => $this->randomWaitMsgs($channel),
114 'wait_time' => $this->randomWaitTime($channel),
117 return $this->notes[$channel->id];
120 private function getNote(Channel $channel, $name, $default = null) {
121 $notes = $this->getNotes($channel);
122 if (array_key_exists($name, $notes)) {
123 return $notes[$name];
128 private function setNote(Channel $channel, $name, $value) {
129 $this->getNotes($channel);
130 $this->notes[$channel->id][$name] = $value;
133 private function collectClassifications(Channel $channel) {
134 $classifications = [];
135 $notes = $this->getNotes($channel);
136 foreach ($notes['latest_msgs'] as $msg) {
137 $classification = $msg->classify();
138 if ($classification == 'unclassified') continue;
139 if (isset($classifications[$classification])) {
140 ++$classifications[$classification];
142 $classifications[$classification] = 1;
145 arsort($classifications);
146 return $classifications;
149 private function contextualMsg(Channel $channel) {
150 if ($this->hasQueuedSpecial($channel)) {
151 $classification = $this->getQueuedSpecial($channel);
152 if (is_string($classification)) {
153 $this->tagChannelSpecialSent($channel, $classification);
155 $this->clearQueuedSpecial($channel);
156 return $this->getRandomOfClass($channel, $classification);
158 $latest_msg = $this->getLatestMessage($channel);
159 if ($latest_msg->classify() == 'question') {
160 $response = $latest_msg->getResponseCategory();
161 return $this->getRandomOfClass($channel, $response);
163 $last = $this->getLastSpecialSent($channel);
164 $classifications = $this->collectClassifications($channel);
189 foreach ($classifications as $classification => $count) {
190 if ($classification == $last) continue;
191 if (!isset($count_quotas[$classification]) || $count < $count_quotas[$classification]) continue;
192 if (!isset($time_quotas[$classification]) || $this->getTimeSinceSpecial($channel, $classification) < $time_quotas[$classification]) continue;
193 $this->tagChannelSpecialSent($channel, $classification);
194 $reaction = $this->getChimeInReaction($channel, $classification);
195 return $this->getRandomOfClass($channel, $reaction);
200 private function randomChat(Channel $channel) {
201 return $channel->queryChatlog()
202 ->whereNotIn('classification', ['gg', 'gl', 'number', 'o7'])
206 private function randomContextualNumber(Channel $channel) {
207 $notes = $this->getNotes($channel);
210 foreach ($notes['latest_msgs'] as $msg) {
211 if ($msg->classify() == 'number') {
212 $number = $msg->getNumericValue();
213 $min = min($min, $number);
214 $max = max($max, $number);
217 return random_int($min, $max);
220 private function randomLaughter(Channel $channel) {
221 if (!random_int(0, 2)) {
222 return $channel->randomOfClass('lol');
255 private function randomMsg(Channel $channel) {
256 return $channel->queryChatlog()->first();
259 private function performAdlib(Channel $channel) {
260 $db = $this->getChatlibDatabase($channel);
261 $latest_msg = $this->getLatestMessage($channel);
262 $text = $db->generate($latest_msg->getText());
263 $this->tagChannelWrite($channel);
264 $this->sendIRCMessage(IRCMessage::privmsg($channel->twitch_chat, $text));
265 $log = new ChatBotLog();
266 $log->channel()->associate($channel);
267 $log->category = 'adlib';
272 private function randomWaitMsgs(Channel $channel) {
273 $min = $channel->getChatSetting('wait_msgs_min', 1);
274 $max = $channel->getChatSetting('wait_msgs_max', 10);
275 return random_int($min, $max);
278 private function randomWaitTime(Channel $channel) {
279 $min = $channel->getChatSetting('wait_time_min', 1);
280 $max = $channel->getChatSetting('wait_time_max', 900);
281 return random_int($min, $max);
284 private function queueSpecial(Channel $channel, $classification) {
285 $this->getNotes($channel);
286 $this->notes[$channel->id]['queued_special'] = $classification;
289 private function hasQueuedSpecial(Channel $channel) {
290 return !!$this->getQueuedSpecial($channel);
293 private function getQueuedSpecial(Channel $channel) {
294 $notes = $this->getNotes($channel);
295 return $notes['queued_special'];
298 private function clearQueuedSpecial(Channel $channel) {
299 $this->getNotes($channel);
300 $this->notes[$channel->id]['queued_special'] = false;
303 private function tagChannelRead(Channel $channel, IRCMessage $msg) {
304 $this->getNotes($channel);
305 $this->notes[$channel->id]['last_read'] = time();
306 ++$this->notes[$channel->id]['read_since_last_write'];
308 $tokenized = $msg->tokenize();
309 if (!ChatLog::isKnownBot($msg->nick) && !$tokenized->isSpammy()) {
310 $this->noteChannelMessage($channel, $tokenized);
312 if ($this->isDirectedAtMe($msg->getText()) && $this->shouldRespond($channel)) {
313 $this->noteChannelMessage($channel, $tokenized);
314 $this->notes[$channel->id]['wait_msgs'] = 0;
315 $this->notes[$channel->id]['wait_time'] = 0;
316 $response = $tokenized->getResponseCategory();
318 $this->queueSpecial($channel, $response);
323 private function noteChannelMessage(Channel $channel, TokenizedMessage $tokenized) {
324 $this->notes[$channel->id]['latest_msgs'][] = $tokenized;
325 if (count($this->notes[$channel->id]['latest_msgs']) > 10) {
326 array_shift($this->notes[$channel->id]['latest_msgs']);
330 private function tagChannelWrite(Channel $channel) {
331 $this->getNotes($channel);
332 $this->notes[$channel->id]['last_write'] = time();
333 $this->notes[$channel->id]['read_since_last_write'] = 0;
334 $this->notes[$channel->id]['wait_msgs'] = $this->randomWaitMsgs($channel);
335 $this->notes[$channel->id]['wait_time'] = $this->randomWaitTime($channel);
338 private function tagChannelSpecialSent(Channel $channel, $classification) {
339 $this->getNotes($channel);
340 $this->notes[$channel->id]['last_special'][$classification] = time();
343 private function getLatestMessage(Channel $channel) {
344 $notes = $this->getNotes($channel);
345 if (!empty($notes['latest_msgs'])) {
346 return $notes['latest_msgs'][count($notes['latest_msgs']) - 1];
348 return TokenizedMessage::fromString('');
351 private function getLastSpecialSent(Channel $channel) {
352 $notes = $this->getNotes($channel);
354 $max_classification = '';
355 foreach ($notes['last_special'] as $classification => $time) {
356 if ($time > $max_time) {
358 $max_classification = $classification;
361 return $max_classification;
364 private function getTimeSinceSpecial(Channel $channel, $classification) {
365 $notes = $this->getNotes($channel);
366 if (isset($notes['last_special'][$classification])) {
367 return time() - $notes['last_special'][$classification];
372 private function isDirectedAtMe($raw_text) {
373 $text = strtolower($raw_text);
374 if (strpos($text, 'horsti') !== false) {
380 private function shouldAdlib(Channel $channel) {
381 $setting = $channel->getChatSetting('adlib', 50);
385 if ($setting == 100) {
388 return random_int(0, 100) <= $setting;
391 private function shouldRespond(Channel $channel) {
392 $setting = $channel->getChatSetting('respond', 'yes');
393 if ($setting == 'yes') {
396 if ($setting == '50') {
397 return random_int(0, 1);
402 private function getRandomOfClass(Channel $channel, $classification) {
403 if ($classification == 'number') {
404 return $this->randomContextualNumber($channel);
406 if ($classification == 'lol') {
407 return $this->randomLaughter($channel);
409 return $channel->randomOfClass($classification);
412 private function getChimeInReaction(Channel $channel, $classification) {
413 switch ($classification) {
415 return ['hi', 'love'];
417 return ['hype', 'love', 'pog'];
419 return ['kappa', 'lol'];
421 return ['hype', 'pog'];
423 return ['lol', 'wtf'];
425 return $classification;