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 ($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 $text = $db->generate();
262 $this->tagChannelWrite($channel);
263 $this->sendIRCMessage(IRCMessage::privmsg($channel->twitch_chat, $text));
264 $log = new ChatBotLog();
265 $log->channel()->associate($channel);
266 $log->category = 'adlib';
271 private function randomWaitMsgs(Channel $channel) {
272 $min = $channel->getChatSetting('wait_msgs_min', 1);
273 $max = $channel->getChatSetting('wait_msgs_max', 10);
274 return random_int($min, $max);
277 private function randomWaitTime(Channel $channel) {
278 $min = $channel->getChatSetting('wait_time_min', 1);
279 $max = $channel->getChatSetting('wait_time_max', 900);
280 return random_int($min, $max);
283 private function queueSpecial(Channel $channel, $classification) {
284 $this->getNotes($channel);
285 $this->notes[$channel->id]['queued_special'] = $classification;
288 private function hasQueuedSpecial(Channel $channel) {
289 return !!$this->getQueuedSpecial($channel);
292 private function getQueuedSpecial(Channel $channel) {
293 $notes = $this->getNotes($channel);
294 return $notes['queued_special'];
297 private function clearQueuedSpecial(Channel $channel) {
298 $this->getNotes($channel);
299 $this->notes[$channel->id]['queued_special'] = false;
302 private function tagChannelRead(Channel $channel, IRCMessage $msg) {
303 $this->getNotes($channel);
304 $this->notes[$channel->id]['last_read'] = time();
305 ++$this->notes[$channel->id]['read_since_last_write'];
307 $tokenized = $msg->tokenize();
308 if (!ChatLog::isKnownBot($msg->nick) && !$tokenized->isSpammy()) {
309 $this->notes[$channel->id]['latest_msgs'][] = $tokenized;
310 if (count($this->notes[$channel->id]['latest_msgs']) > 10) {
311 array_shift($this->notes[$channel->id]['latest_msgs']);
314 if ($this->isDirectedAtMe($msg->getText()) && $this->shouldRespond($channel)) {
315 $this->notes[$channel->id]['wait_msgs'] = 0;
316 $this->notes[$channel->id]['wait_time'] = 0;
317 $response = $tokenized->getResponseCategory();
319 $this->queueSpecial($channel, $response);
324 private function tagChannelWrite(Channel $channel) {
325 $this->getNotes($channel);
326 $this->notes[$channel->id]['last_write'] = time();
327 $this->notes[$channel->id]['read_since_last_write'] = 0;
328 $this->notes[$channel->id]['wait_msgs'] = $this->randomWaitMsgs($channel);
329 $this->notes[$channel->id]['wait_time'] = $this->randomWaitTime($channel);
332 private function tagChannelSpecialSent(Channel $channel, $classification) {
333 $this->getNotes($channel);
334 $this->notes[$channel->id]['last_special'][$classification] = time();
337 private function getLatestMessage(Channel $channel) {
338 $this->getNotes($channel);
339 if (!empty($notes['latest_msgs'])) {
340 return $notes['latest_msgs'][count($notes['latest_msgs']) - 1];
342 return TokenizedMessage::fromString('');
345 private function getLastSpecialSent(Channel $channel) {
346 $notes = $this->getNotes($channel);
348 $max_classification = '';
349 foreach ($notes['last_special'] as $classification => $time) {
350 if ($time > $max_time) {
352 $max_classification = $classification;
355 return $max_classification;
358 private function getTimeSinceSpecial(Channel $channel, $classification) {
359 $notes = $this->getNotes($channel);
360 if (isset($notes['last_special'][$classification])) {
361 return time() - $notes['last_special'][$classification];
366 private function isDirectedAtMe($raw_text) {
367 $text = strtolower($raw_text);
368 if (strpos($text, 'horsti') !== false) {
374 private function shouldAdlib(Channel $channel) {
375 $setting = $channel->getChatSetting('adlib', 50);
379 if ($setting == 100) {
382 return random_int(0, 100) <= $setting;
385 private function shouldRespond(Channel $channel) {
386 $setting = $channel->getChatSetting('respond', 'yes');
387 if ($setting == 'yes') {
390 if ($setting == '50') {
391 return random_int(0, 1);
396 private function getRandomOfClass(Channel $channel, $classification) {
397 if ($classification == 'number') {
398 return $this->randomContextualNumber($channel);
400 if ($classification == 'lol') {
401 return $this->randomLaughter($channel);
403 return $channel->randomOfClass($classification);
406 private function getChimeInReaction(Channel $channel, $classification) {
407 switch ($classification) {
409 return ['hi', 'love'];
411 return ['hype', 'love', 'pog'];
413 return ['kappa', 'lol'];
415 return ['hype', 'pog'];
417 return ['lol', 'wtf'];
419 return $classification;