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 handleWhisper(IRCMessage $msg) {
50 $text = $this->chatlib->generate($msg->getText());
51 $this->sendWhisper($msg->tags['user-id'], $text);
54 public function getChatlibDatabase(Channel $channel) {
55 return $this->chatlib;
59 private function startTimer() {
60 $this->getLoop()->addPeriodicTimer(1, function () {
61 if (!$this->isReady()) return;
62 foreach ($this->channels as $channel) {
63 $this->decideSend($channel);
66 $this->getLoop()->addPeriodicTimer(60, function () {
67 $this->updateChannels();
71 private function updateChannels() {
72 $this->channels = Channel::where('twitch_chat', '!=', '')->where('chat', '=', true)->get();
75 private function decideSend(Channel $channel) {
76 $notes = $this->getNotes($channel);
77 if ($notes['read_since_last_write'] < $notes['wait_msgs']) {
80 if (time() - $notes['last_write'] < $notes['wait_time']) {
83 if ($notes['read_since_last_write'] == $notes['wait_msgs'] && time() - $notes['last_read'] < 3) {
84 // don't immediately respond if we crossed the msg threshold last
87 $text = $this->contextualMsg($channel);
88 if (!$text && $this->shouldAdlib($channel)) {
89 $this->performAdlib($channel);
92 if (!$text) $text = $this->randomChat($channel);
94 $actual_text = is_object($text) ? $text->text_content : $text;
95 $this->tagChannelWrite($channel);
96 $this->sendIRCMessage(IRCMessage::privmsg($channel->twitch_chat, $actual_text));
97 $log = new ChatBotLog();
98 $log->channel()->associate($channel);
99 if (is_object($text)) {
100 $log->origin()->associate($text);
101 $log->category = $text->classification;
103 $log->category = $this->getLastSpecialSent($channel);
105 $log->text = $actual_text;
109 private function getNotes(Channel $channel) {
110 if (!isset($this->notes[$channel->id])) {
111 $this->notes[$channel->id] = [
113 'last_special' => [],
114 'last_write' => time(),
116 'queued_special' => false,
117 'read_since_last_write' => 0,
118 'wait_msgs' => $this->randomWaitMsgs($channel),
119 'wait_time' => $this->randomWaitTime($channel),
122 return $this->notes[$channel->id];
125 private function getNote(Channel $channel, $name, $default = null) {
126 $notes = $this->getNotes($channel);
127 if (array_key_exists($name, $notes)) {
128 return $notes[$name];
133 private function setNote(Channel $channel, $name, $value) {
134 $this->getNotes($channel);
135 $this->notes[$channel->id][$name] = $value;
138 private function collectClassifications(Channel $channel) {
139 $classifications = [];
140 $notes = $this->getNotes($channel);
141 foreach ($notes['latest_msgs'] as $msg) {
142 $classification = $msg->classify();
143 if ($classification == 'unclassified') continue;
144 if (isset($classifications[$classification])) {
145 ++$classifications[$classification];
147 $classifications[$classification] = 1;
150 arsort($classifications);
151 return $classifications;
154 private function contextualMsg(Channel $channel) {
155 if ($this->hasQueuedSpecial($channel)) {
156 $classification = $this->getQueuedSpecial($channel);
157 if (is_string($classification)) {
158 $this->tagChannelSpecialSent($channel, $classification);
160 $this->clearQueuedSpecial($channel);
161 return $this->getRandomOfClass($channel, $classification);
163 $latest_msg = $this->getLatestMessage($channel);
164 if ($latest_msg->classify() == 'question') {
165 $response = $latest_msg->getResponseCategory();
166 return $this->getRandomOfClass($channel, $response);
168 $last = $this->getLastSpecialSent($channel);
169 $classifications = $this->collectClassifications($channel);
194 foreach ($classifications as $classification => $count) {
195 if ($classification == $last) continue;
196 if (!isset($count_quotas[$classification]) || $count < $count_quotas[$classification]) continue;
197 if (!isset($time_quotas[$classification]) || $this->getTimeSinceSpecial($channel, $classification) < $time_quotas[$classification]) continue;
198 $this->tagChannelSpecialSent($channel, $classification);
199 $reaction = $this->getChimeInReaction($channel, $classification);
200 return $this->getRandomOfClass($channel, $reaction);
205 private function randomChat(Channel $channel) {
206 return $channel->queryChatlog()
207 ->whereNotIn('classification', ['gg', 'gl', 'number', 'o7'])
211 private function randomContextualNumber(Channel $channel) {
212 $notes = $this->getNotes($channel);
215 foreach ($notes['latest_msgs'] as $msg) {
216 if ($msg->classify() == 'number') {
217 $number = $msg->getNumericValue();
218 $min = min($min, $number);
219 $max = max($max, $number);
222 return random_int($min, $max);
225 private function randomLaughter(Channel $channel) {
226 if (!random_int(0, 2)) {
227 return $channel->randomOfClass('lol');
260 private function randomMsg(Channel $channel) {
261 return $channel->queryChatlog()->first();
264 private function performAdlib(Channel $channel) {
265 $db = $this->getChatlibDatabase($channel);
266 $latest_msg = $this->getLatestMessage($channel);
267 $text = $db->generate($latest_msg->getText());
268 $this->tagChannelWrite($channel);
269 $this->sendIRCMessage(IRCMessage::privmsg($channel->twitch_chat, $text));
270 $log = new ChatBotLog();
271 $log->channel()->associate($channel);
272 $log->category = 'adlib';
277 private function randomWaitMsgs(Channel $channel) {
278 $min = $channel->getChatSetting('wait_msgs_min', 1);
279 $max = $channel->getChatSetting('wait_msgs_max', 10);
280 return random_int($min, $max);
283 private function randomWaitTime(Channel $channel) {
284 $min = $channel->getChatSetting('wait_time_min', 1);
285 $max = $channel->getChatSetting('wait_time_max', 900);
286 return random_int($min, $max);
289 private function queueSpecial(Channel $channel, $classification) {
290 $this->getNotes($channel);
291 $this->notes[$channel->id]['queued_special'] = $classification;
294 private function hasQueuedSpecial(Channel $channel) {
295 return !!$this->getQueuedSpecial($channel);
298 private function getQueuedSpecial(Channel $channel) {
299 $notes = $this->getNotes($channel);
300 return $notes['queued_special'];
303 private function clearQueuedSpecial(Channel $channel) {
304 $this->getNotes($channel);
305 $this->notes[$channel->id]['queued_special'] = false;
308 private function tagChannelRead(Channel $channel, IRCMessage $msg) {
309 $this->getNotes($channel);
310 $this->notes[$channel->id]['last_read'] = time();
311 ++$this->notes[$channel->id]['read_since_last_write'];
313 $tokenized = $msg->tokenize();
314 if (!ChatLog::isKnownBot($msg->nick) && !$tokenized->isSpammy()) {
315 $this->noteChannelMessage($channel, $tokenized);
317 if ($this->isDirectedAtMe($msg->getText()) && $this->shouldRespond($channel)) {
318 $this->noteChannelMessage($channel, $tokenized);
319 $this->notes[$channel->id]['wait_msgs'] = 0;
320 $this->notes[$channel->id]['wait_time'] = 0;
321 $response = $tokenized->getResponseCategory();
323 $this->queueSpecial($channel, $response);
328 private function noteChannelMessage(Channel $channel, TokenizedMessage $tokenized) {
329 $this->notes[$channel->id]['latest_msgs'][] = $tokenized;
330 if (count($this->notes[$channel->id]['latest_msgs']) > 10) {
331 array_shift($this->notes[$channel->id]['latest_msgs']);
335 private function tagChannelWrite(Channel $channel) {
336 $this->getNotes($channel);
337 $this->notes[$channel->id]['last_write'] = time();
338 $this->notes[$channel->id]['read_since_last_write'] = 0;
339 $this->notes[$channel->id]['wait_msgs'] = $this->randomWaitMsgs($channel);
340 $this->notes[$channel->id]['wait_time'] = $this->randomWaitTime($channel);
343 private function tagChannelSpecialSent(Channel $channel, $classification) {
344 $this->getNotes($channel);
345 $this->notes[$channel->id]['last_special'][$classification] = time();
348 private function getLatestMessage(Channel $channel) {
349 $notes = $this->getNotes($channel);
350 if (!empty($notes['latest_msgs'])) {
351 return $notes['latest_msgs'][count($notes['latest_msgs']) - 1];
353 return TokenizedMessage::fromString('');
356 private function getLastSpecialSent(Channel $channel) {
357 $notes = $this->getNotes($channel);
359 $max_classification = '';
360 foreach ($notes['last_special'] as $classification => $time) {
361 if ($time > $max_time) {
363 $max_classification = $classification;
366 return $max_classification;
369 private function getTimeSinceSpecial(Channel $channel, $classification) {
370 $notes = $this->getNotes($channel);
371 if (isset($notes['last_special'][$classification])) {
372 return time() - $notes['last_special'][$classification];
377 private function isDirectedAtMe($raw_text) {
378 $text = strtolower($raw_text);
379 if (strpos($text, 'horsti') !== false) {
385 private function shouldAdlib(Channel $channel) {
386 $setting = $channel->getChatSetting('adlib', 50);
390 if ($setting == 100) {
393 return random_int(0, 100) <= $setting;
396 private function shouldRespond(Channel $channel) {
397 $setting = $channel->getChatSetting('respond', 'yes');
398 if ($setting == 'yes') {
401 if ($setting == '50') {
402 return random_int(0, 1);
407 private function getRandomOfClass(Channel $channel, $classification) {
408 if ($classification == 'number') {
409 return $this->randomContextualNumber($channel);
411 if ($classification == 'lol') {
412 return $this->randomLaughter($channel);
414 return $channel->randomOfClass($classification);
417 private function getChimeInReaction(Channel $channel, $classification) {
418 switch ($classification) {
420 return ['hi', 'love'];
422 return ['hype', 'love', 'pog'];
424 return ['kappa', 'lol'];
426 return ['hype', 'pog'];
428 return ['lol', 'wtf'];
430 return $classification;