updateChannels(); $this->startTimer(); $this->listenCommands(); $this->chatlib = new ChatLib(); $this->chatlib->loadFrom('de'); } public function joinChannels() { $this->getLogger()->info('joining channels'); $names = []; foreach ($this->channels as $channel) { $names[] = $channel->twitch_chat; } $chunks = array_chunk($names, 10); foreach ($chunks as $chunk) { $this->sendIRCMessage(IRCMessage::join($chunk)); } } public function logMessage(IRCMessage $msg) { $channel = $this->getMessageChannel($msg); if ($channel && !$channel->join) { $msg->log(); } } public function handlePrivMsg(IRCMessage $msg) { if ($msg->nick == 'horstiebot') return; $channel = $this->getMessageChannel($msg); if (!$channel) return; $this->tagChannelRead($channel, $msg); } public function getChatlibDatabase(Channel $channel) { return $this->chatlib; } private function startTimer() { $this->getLoop()->addPeriodicTimer(1, function () { if (!$this->isReady()) return; foreach ($this->channels as $channel) { $this->decideSend($channel); } }); $this->getLoop()->addPeriodicTimer(60, function () { $this->updateChannels(); }); } private function updateChannels() { $this->channels = Channel::where('twitch_chat', '!=', '')->where('chat', '=', true)->get(); } private function decideSend(Channel $channel) { $notes = $this->getNotes($channel); if ($notes['read_since_last_write'] < $notes['wait_msgs']) { return; } if (time() - $notes['last_write'] < $notes['wait_time']) { return; } if ($notes['read_since_last_write'] == $notes['wait_msgs'] && time() - $notes['last_read'] < 3) { // don't immediately respond if we crossed the msg threshold last return; } $text = $this->contextualMsg($channel); if ($this->shouldAdlib($channel)) { $this->performAdlib($channel); return; } if (!$text) $text = $this->randomChat($channel); if (!$text) return; $actual_text = is_object($text) ? $text->text_content : $text; $this->tagChannelWrite($channel); $this->sendIRCMessage(IRCMessage::privmsg($channel->twitch_chat, $actual_text)); $log = new ChatBotLog(); $log->channel()->associate($channel); if (is_object($text)) { $log->origin()->associate($text); $log->category = $text->classification; } else { $log->category = $this->getLastSpecialSent($channel); } $log->text = $actual_text; $log->save(); } private function getNotes(Channel $channel) { if (!isset($this->notes[$channel->id])) { $this->notes[$channel->id] = [ 'last_read' => 0, 'last_special' => [], 'last_write' => time(), 'latest_msgs' => [], 'queued_special' => false, 'read_since_last_write' => 0, 'wait_msgs' => $this->randomWaitMsgs($channel), 'wait_time' => $this->randomWaitTime($channel), ]; } return $this->notes[$channel->id]; } private function getNote(Channel $channel, $name, $default = null) { $notes = $this->getNotes($channel); if (array_key_exists($name, $notes)) { return $notes[$name]; } return $default; } private function setNote(Channel $channel, $name, $value) { $this->getNotes($channel); $this->notes[$channel->id][$name] = $value; } private function collectClassifications(Channel $channel) { $classifications = []; $notes = $this->getNotes($channel); foreach ($notes['latest_msgs'] as $msg) { $classification = $msg->classify(); if ($classification == 'unclassified') continue; if (isset($classifications[$classification])) { ++$classifications[$classification]; } else { $classifications[$classification] = 1; } } arsort($classifications); return $classifications; } private function contextualMsg(Channel $channel) { if ($this->hasQueuedSpecial($channel)) { $classification = $this->getQueuedSpecial($channel); if (is_string($classification)) { $this->tagChannelSpecialSent($channel, $classification); } $this->clearQueuedSpecial($channel); return $this->getRandomOfClass($channel, $classification); } $latest_msg = $this->getLatestMessage($channel); if ($latest_msg->classify() == 'question') { $response = $latest_msg->getResponseCategory(); return $this->getRandomOfClass($channel, $response); } $last = $this->getLastSpecialSent($channel); $classifications = $this->collectClassifications($channel); $count_quotas = [ 'gg' => 2, 'gl' => 2, 'hi' => 2, 'hype' => 2, 'lol' => 2, 'love' => 2, 'number' => 2, 'pog' => 2, 'o7' => 2, 'wtf' => 2, ]; $time_quotas = [ 'gg' => 600, 'gl' => 900, 'hi' => 60, 'hype' => 60, 'lol' => 60, 'love' => 60, 'number' => 300, 'pog' => 60, 'o7' => 300, 'wtf' => 60, ]; foreach ($classifications as $classification => $count) { if ($classification == $last) continue; if (!isset($count_quotas[$classification]) || $count < $count_quotas[$classification]) continue; if (!isset($time_quotas[$classification]) || $this->getTimeSinceSpecial($channel, $classification) < $time_quotas[$classification]) continue; $this->tagChannelSpecialSent($channel, $classification); $reaction = $this->getChimeInReaction($channel, $classification); return $this->getRandomOfClass($channel, $reaction); } return false; } private function randomChat(Channel $channel) { return $channel->queryChatlog() ->whereNotIn('classification', ['gg', 'gl', 'number', 'o7']) ->first(); } private function randomContextualNumber(Channel $channel) { $notes = $this->getNotes($channel); $min = 100000; $max = 0; foreach ($notes['latest_msgs'] as $msg) { if ($msg->classify() == 'number') { $number = $msg->getNumericValue(); $min = min($min, $number); $max = max($max, $number); } } return random_int($min, $max); } private function randomLaughter(Channel $channel) { if (!random_int(0, 2)) { return $channel->randomOfClass('lol'); } return Arr::random([ ':tf:', '4Head', 'CarlSmile', 'CruW', 'DendiFace', 'EleGiggle', 'GunRun', 'heh', 'Hhhehehe', 'Jebaited', 'Jebasted', 'KEKW', 'KEKHeim', 'KKona', 'KomodoHype', 'MaxLOL', 'MingLee', 'lol', 'LOL!', 'LUL', 'OneHand', 'SeemsGood', 'ShadyLulu', 'SoonerLater', 'SUBprise', 'xD', 'YouDontSay', ]); } private function randomMsg(Channel $channel) { return $channel->queryChatlog()->first(); } private function performAdlib(Channel $channel) { $db = $this->getChatlibDatabase($channel); $text = $db->generate(); $this->tagChannelWrite($channel); $this->sendIRCMessage(IRCMessage::privmsg($channel->twitch_chat, $text)); $log = new ChatBotLog(); $log->channel()->associate($channel); $log->category = 'adlib'; $log->text = $text; $log->save(); } private function randomWaitMsgs(Channel $channel) { $min = $channel->getChatSetting('wait_msgs_min', 1); $max = $channel->getChatSetting('wait_msgs_max', 10); return random_int($min, $max); } private function randomWaitTime(Channel $channel) { $min = $channel->getChatSetting('wait_time_min', 1); $max = $channel->getChatSetting('wait_time_max', 900); return random_int($min, $max); } private function queueSpecial(Channel $channel, $classification) { $this->getNotes($channel); $this->notes[$channel->id]['queued_special'] = $classification; } private function hasQueuedSpecial(Channel $channel) { return !!$this->getQueuedSpecial($channel); } private function getQueuedSpecial(Channel $channel) { $notes = $this->getNotes($channel); return $notes['queued_special']; } private function clearQueuedSpecial(Channel $channel) { $this->getNotes($channel); $this->notes[$channel->id]['queued_special'] = false; } private function tagChannelRead(Channel $channel, IRCMessage $msg) { $this->getNotes($channel); $this->notes[$channel->id]['last_read'] = time(); ++$this->notes[$channel->id]['read_since_last_write']; $tokenized = $msg->tokenize(); if (!ChatLog::isKnownBot($msg->nick) && !$tokenized->isSpammy()) { $this->notes[$channel->id]['latest_msgs'][] = $tokenized; if (count($this->notes[$channel->id]['latest_msgs']) > 10) { array_shift($this->notes[$channel->id]['latest_msgs']); } } if ($this->isDirectedAtMe($msg->getText()) && $this->shouldRespond($channel)) { $this->notes[$channel->id]['wait_msgs'] = 0; $this->notes[$channel->id]['wait_time'] = 0; $response = $tokenized->getResponseCategory(); if ($response) { $this->queueSpecial($channel, $response); } } } private function tagChannelWrite(Channel $channel) { $this->getNotes($channel); $this->notes[$channel->id]['last_write'] = time(); $this->notes[$channel->id]['read_since_last_write'] = 0; $this->notes[$channel->id]['wait_msgs'] = $this->randomWaitMsgs($channel); $this->notes[$channel->id]['wait_time'] = $this->randomWaitTime($channel); } private function tagChannelSpecialSent(Channel $channel, $classification) { $this->getNotes($channel); $this->notes[$channel->id]['last_special'][$classification] = time(); } private function getLatestMessage(Channel $channel) { $this->getNotes($channel); if (!empty($notes['latest_msgs'])) { return $notes['latest_msgs'][count($notes['latest_msgs']) - 1]; } return TokenizedMessage::fromString(''); } private function getLastSpecialSent(Channel $channel) { $notes = $this->getNotes($channel); $max_time = 0; $max_classification = ''; foreach ($notes['last_special'] as $classification => $time) { if ($time > $max_time) { $max_time = $time; $max_classification = $classification; } } return $max_classification; } private function getTimeSinceSpecial(Channel $channel, $classification) { $notes = $this->getNotes($channel); if (isset($notes['last_special'][$classification])) { return time() - $notes['last_special'][$classification]; } return 999999; } private function isDirectedAtMe($raw_text) { $text = strtolower($raw_text); if (strpos($text, 'horsti') !== false) { return true; } return false; } private function shouldAdlib(Channel $channel) { $setting = $channel->getChatSetting('adlib', 50); if ($setting == 0) { return false; } if ($setting == 100) { return true; } return random_int(0, 100) <= $setting; } private function shouldRespond(Channel $channel) { $setting = $channel->getChatSetting('respond', 'yes'); if ($setting == 'yes') { return true; } if ($setting == '50') { return random_int(0, 1); } return false; } private function getRandomOfClass(Channel $channel, $classification) { if ($classification == 'number') { return $this->randomContextualNumber($channel); } if ($classification == 'lol') { return $this->randomLaughter($channel); } return $channel->randomOfClass($classification); } private function getChimeInReaction(Channel $channel, $classification) { switch ($classification) { case 'hi': return ['hi', 'love']; case 'hype': return ['hype', 'love', 'pog']; case 'lol': return ['kappa', 'lol']; case 'pog': return ['hype', 'pog']; case 'wtf': return ['lol', 'wtf']; } return $classification; } private $channels; private $notes = []; private $chatlib; }