3 namespace App\TwitchBot;
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;
11 class TwitchChatBot extends TwitchBot {
13 public function __construct() {
14 parent::__construct('horstiebot');
15 $this->updateChannels();
17 $this->listenCommands();
20 public function joinChannels() {
21 $this->getLogger()->info('joining channels');
23 foreach ($this->channels as $channel) {
24 $names[] = $channel->twitch_chat;
26 $chunks = array_chunk($names, 10);
27 foreach ($chunks as $chunk) {
28 $this->sendIRCMessage(IRCMessage::join($chunk));
32 public function logMessage(IRCMessage $msg) {
33 $channel = $this->getMessageChannel($msg);
34 if ($channel && !$channel->join) {
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);
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);
54 $this->getLoop()->addPeriodicTimer(60, function () {
55 $this->updateChannels();
59 private function updateChannels() {
60 $this->channels = Channel::where('twitch_chat', '!=', '')->where('chat', '=', true)->get();
63 private function decideSend(Channel $channel) {
64 $notes = $this->getNotes($channel);
65 if ($notes['read_since_last_write'] < $notes['wait_msgs']) {
68 if (time() - $notes['last_write'] < $notes['wait_time']) {
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
75 $text = $this->contextualMsg($channel);
76 if (!$text) $text = $this->randomChat($channel);
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 $log->category = $text->classification;
87 $log->category = $this->getLastSpecialSent($channel);
89 $log->text = $actual_text;
93 private function getNotes(Channel $channel) {
94 if (!isset($this->notes[$channel->id])) {
95 $this->notes[$channel->id] = [
98 'last_write' => time(),
100 'queued_special' => false,
101 'read_since_last_write' => 0,
102 'wait_msgs' => $this->randomWaitMsgs($channel),
103 'wait_time' => $this->randomWaitTime($channel),
106 return $this->notes[$channel->id];
109 private function getNote(Channel $channel, $name, $default = null) {
110 $notes = $this->getNotes($channel);
111 if (array_key_exists($name, $notes)) {
112 return $notes[$name];
117 private function setNote(Channel $channel, $name, $value) {
118 $this->getNotes($channel);
119 $this->notes[$channel->id][$name] = $value;
122 private function collectClassifications(Channel $channel) {
123 $classifications = [];
124 $notes = $this->getNotes($channel);
125 foreach ($notes['latest_msgs'] as $msg) {
126 $classification = $msg->classify();
127 if ($classification == 'unclassified') continue;
128 if (isset($classifications[$classification])) {
129 ++$classifications[$classification];
131 $classifications[$classification] = 1;
134 arsort($classifications);
135 return $classifications;
138 private function contextualMsg(Channel $channel) {
139 if ($this->hasQueuedSpecial($channel)) {
140 $classification = $this->getQueuedSpecial($channel);
141 if (is_string($classification)) {
142 $this->tagChannelSpecialSent($channel, $classification);
144 $this->clearQueuedSpecial($channel);
145 return $this->getRandomOfClass($channel, $classification);
147 $latest_msg = $this->getLatestMessage($channel);
148 if ($latest_msg->classify() == 'question') {
149 $response = $latest_msg->getResponseCategory();
150 return $this->getRandomOfClass($channel, $response);
152 $last = $this->getLastSpecialSent($channel);
153 $classifications = $this->collectClassifications($channel);
178 foreach ($classifications as $classification => $count) {
179 if ($classification == $last) continue;
180 if (!isset($count_quotas[$classification]) || $count < $count_quotas[$classification]) continue;
181 if (!isset($time_quotas[$classification]) || $this->getTimeSinceSpecial($channel, $classification) < $time_quotas[$classification]) continue;
182 $this->tagChannelSpecialSent($channel, $classification);
183 $reaction = $this->getChimeInReaction($channel, $classification);
184 return $this->getRandomOfClass($channel, $reaction);
189 private function randomChat(Channel $channel) {
190 return $channel->queryChatlog()
191 ->whereNotIn('classification', ['gg', 'gl', 'number', 'o7'])
195 private function randomContextualNumber(Channel $channel) {
196 $notes = $this->getNotes($channel);
199 foreach ($notes['latest_msgs'] as $msg) {
200 if ($msg->classify() == 'number') {
201 $number = $msg->getNumericValue();
202 $min = min($min, $number);
203 $max = max($max, $number);
206 return random_int($min, $max);
209 private function randomLaughter(Channel $channel) {
210 if (!random_int(0, 2)) {
211 return $channel->randomOfClass('lol');
244 private function randomMsg(Channel $channel) {
245 return $channel->queryChatlog()->first();
248 private function randomWaitMsgs(Channel $channel) {
249 $min = $channel->getChatSetting('wait_msgs_min', 1);
250 $max = $channel->getChatSetting('wait_msgs_max', 10);
251 return random_int($min, $max);
254 private function randomWaitTime(Channel $channel) {
255 $min = $channel->getChatSetting('wait_time_min', 1);
256 $max = $channel->getChatSetting('wait_time_max', 900);
257 return random_int($min, $max);
260 private function queueSpecial(Channel $channel, $classification) {
261 $this->getNotes($channel);
262 $this->notes[$channel->id]['queued_special'] = $classification;
265 private function hasQueuedSpecial(Channel $channel) {
266 return !!$this->getQueuedSpecial($channel);
269 private function getQueuedSpecial(Channel $channel) {
270 $notes = $this->getNotes($channel);
271 return $notes['queued_special'];
274 private function clearQueuedSpecial(Channel $channel) {
275 $this->getNotes($channel);
276 $this->notes[$channel->id]['queued_special'] = false;
279 private function tagChannelRead(Channel $channel, IRCMessage $msg) {
280 $this->getNotes($channel);
281 $this->notes[$channel->id]['last_read'] = time();
282 ++$this->notes[$channel->id]['read_since_last_write'];
284 $tokenized = $msg->tokenize();
285 if (!ChatLog::isKnownBot($msg->nick) && !$tokenized->isSpammy()) {
286 $this->notes[$channel->id]['latest_msgs'][] = $tokenized;
287 if (count($this->notes[$channel->id]['latest_msgs']) > 10) {
288 array_shift($this->notes[$channel->id]['latest_msgs']);
291 if ($this->isDirectedAtMe($msg->getText()) && $this->shouldRespond($channel)) {
292 $this->notes[$channel->id]['wait_msgs'] = 0;
293 $this->notes[$channel->id]['wait_time'] = 0;
294 $response = $tokenized->getResponseCategory();
296 $this->queueSpecial($channel, $response);
301 private function tagChannelWrite(Channel $channel) {
302 $this->getNotes($channel);
303 $this->notes[$channel->id]['last_write'] = time();
304 $this->notes[$channel->id]['read_since_last_write'] = 0;
305 $this->notes[$channel->id]['wait_msgs'] = $this->randomWaitMsgs($channel);
306 $this->notes[$channel->id]['wait_time'] = $this->randomWaitTime($channel);
309 private function tagChannelSpecialSent(Channel $channel, $classification) {
310 $this->getNotes($channel);
311 $this->notes[$channel->id]['last_special'][$classification] = time();
314 private function getLatestMessage(Channel $channel) {
315 $this->getNotes($channel);
316 if (!empty($notes['latest_msgs'])) {
317 return $notes['latest_msgs'][count($notes['latest_msgs']) - 1];
319 return TokenizedMessage::fromString('');
322 private function getLastSpecialSent(Channel $channel) {
323 $notes = $this->getNotes($channel);
325 $max_classification = '';
326 foreach ($notes['last_special'] as $classification => $time) {
327 if ($time > $max_time) {
329 $max_classification = $classification;
332 return $max_classification;
335 private function getTimeSinceSpecial(Channel $channel, $classification) {
336 $notes = $this->getNotes($channel);
337 if (isset($notes['last_special'][$classification])) {
338 return time() - $notes['last_special'][$classification];
343 private function isDirectedAtMe($raw_text) {
344 $text = strtolower($raw_text);
345 if (strpos($text, 'horsti') !== false) {
351 private function shouldRespond(Channel $channel) {
352 $setting = $channel->getChatSetting('respond', 'yes');
353 if ($setting == 'yes') {
356 if ($setting == '50') {
357 return random_int(0, 1);
362 private function getRandomOfClass(Channel $channel, $classification) {
363 if ($classification == 'number') {
364 return $this->randomContextualNumber($channel);
366 if ($classification == 'lol') {
367 return $this->randomLaughter($channel);
369 return $channel->randomOfClass($classification);
372 private function getChimeInReaction(Channel $channel, $classification) {
373 switch ($classification) {
375 return ['hi', 'love'];
377 return ['hype', 'love', 'pog'];
379 return ['kappa', 'lol'];
381 return ['hype', 'pog'];
383 return ['lol', 'wtf'];
385 return $classification;