3 namespace App\TwitchBot;
5 use App\Models\Channel;
6 use App\Models\ChatLog;
7 use Illuminate\Support\Arr;
8 use Illuminate\Support\Str;
10 class TwitchChatBot extends TwitchBot {
12 public function __construct() {
13 parent::__construct('horstiebot');
14 $this->updateChannels();
16 $this->listenCommands();
19 public function joinChannels() {
20 $this->getLogger()->info('joining channels');
22 foreach ($this->channels as $channel) {
23 $names[] = $channel->twitch_chat;
25 $chunks = array_chunk($names, 10);
26 foreach ($chunks as $chunk) {
27 $this->sendIRCMessage(IRCMessage::join($chunk));
31 public function logMessage(IRCMessage $msg) {
32 $channel = $this->getMessageChannel($msg);
33 if ($channel && !$channel->join) {
38 public function handlePrivMsg(IRCMessage $msg) {
39 if ($msg->nick == 'horstiebot') return;
40 $channel = $this->getMessageChannel($msg);
41 if (!$channel) return;
42 $this->tagChannelRead($channel, $msg);
46 private function startTimer() {
47 $this->getLoop()->addPeriodicTimer(1, function () {
48 if (!$this->isReady()) return;
49 foreach ($this->channels as $channel) {
50 $this->decideSend($channel);
53 $this->getLoop()->addPeriodicTimer(60, function () {
54 $this->updateChannels();
58 private function updateChannels() {
59 $this->channels = Channel::where('twitch_chat', '!=', '')->where('chat', '=', true)->get();
62 private function decideSend(Channel $channel) {
63 $notes = $this->getNotes($channel);
64 if ($notes['read_since_last_write'] < $notes['wait_msgs']) {
67 if (time() - $notes['last_write'] < $notes['wait_time']) {
70 if ($notes['read_since_last_write'] == $notes['wait_msgs'] && time() - $notes['last_read'] < 3) {
71 // don't immediately respond if we crossed the msg threshold last
74 $text = $this->contextualMsg($channel);
75 if (!$text) $text = $this->randomChat($channel);
77 $this->tagChannelWrite($channel);
78 $this->sendIRCMessage(IRCMessage::privmsg($channel->twitch_chat, $text));
81 private function getNotes(Channel $channel) {
82 if (!isset($this->notes[$channel->id])) {
83 $this->notes[$channel->id] = [
86 'last_write' => time(),
88 'read_since_last_write' => 0,
89 'wait_msgs' => $this->randomWaitMsgs($channel),
90 'wait_time' => $this->randomWaitTime($channel),
93 return $this->notes[$channel->id];
96 private function getNote(Channel $channel, $name, $default = null) {
97 $notes = $this->getNotes($channel);
98 if (array_key_exists($name, $notes)) {
104 private function setNote(Channel $channel, $name, $value) {
105 $this->getNotes($channel);
106 $this->notes[$channel->id][$name] = $value;
109 private function collectClassifications(Channel $channel) {
110 $classifications = [];
111 $notes = $this->getNotes($channel);
112 foreach ($notes['latest_msgs'] as $msg) {
113 $classification = $msg->classify();
114 if ($classification == 'unclassified') continue;
115 if (isset($classifications[$classification])) {
116 ++$classifications[$classification];
118 $classifications[$classification] = 1;
121 arsort($classifications);
122 return $classifications;
125 private function contextualMsg(Channel $channel) {
126 $last = $this->getNote($channel, 'last_special');
127 $classifications = $this->collectClassifications($channel);
148 foreach ($classifications as $classification => $count) {
149 if ($classification == $last) continue;
150 if (!isset($count_quotas[$classification]) || $count < $count_quotas[$classification]) continue;
151 if (!isset($time_quotas[$classification]) || $this->getTimeSinceSpecial($channel, $classification) < $time_quotas[$classification]) continue;
152 $this->tagChannelSpecialSent($channel, $classification);
153 if ($classification == 'number') {
154 return $this->randomContextualNumber($channel);
156 if ($classification == 'lol') {
157 return $this->randomLaughter($channel);
159 return $channel->randomOfClass($classification);
164 private function randomChat(Channel $channel) {
165 $line = $channel->queryChatlog()
166 ->whereIn('classification', ['hi', 'hype', 'lol', 'pog', 'unclassified'])
168 return $line->text_content;
171 private function randomContextualNumber(Channel $channel) {
172 $notes = $this->getNotes($channel);
175 foreach ($notes['latest_msgs'] as $msg) {
176 if ($msg->classify() == 'number') {
177 $number = $msg->getNumericValue();
178 $min = min($min, $number);
179 $max = max($max, $number);
182 return random_int($min, $max);
185 private function randomLaughter(Channel $channel) {
215 $channel->randomOfClass('lol'),
219 private function randomMsg(Channel $channel) {
220 $line = $channel->queryChatlog()->first();
221 return $line->text_content;
224 private function randomWaitMsgs(Channel $channel) {
225 $min = $channel->getChatSetting('wait_msgs_min', 1);
226 $max = $channel->getChatSetting('wait_msgs_max', 10);
227 return random_int($min, $max);
230 private function randomWaitTime(Channel $channel) {
231 $min = $channel->getChatSetting('wait_time_min', 1);
232 $max = $channel->getChatSetting('wait_time_max', 900);
233 return random_int($min, $max);
236 private function tagChannelRead(Channel $channel, IRCMessage $msg) {
237 $this->getNotes($channel);
238 $this->notes[$channel->id]['last_read'] = time();
239 ++$this->notes[$channel->id]['read_since_last_write'];
241 $tokenized = $msg->tokenize();
242 if (!ChatLog::isKnownBot($msg->nick) && !$tokenized->isSpammy()) {
243 $this->notes[$channel->id]['latest_msgs'][] = $tokenized;
244 if (count($this->notes[$channel->id]['latest_msgs']) > 10) {
245 array_shift($this->notes[$channel->id]['latest_msgs']);
248 if ($this->isDirectedAtMe($msg->getText()) && $this->shouldRespond($channel)) {
249 $this->notes[$channel->id]['wait_msgs'] = 0;
250 $this->notes[$channel->id]['wait_time'] = 0;
254 private function tagChannelWrite(Channel $channel) {
255 $this->getNotes($channel);
256 $this->notes[$channel->id]['last_write'] = time();
257 $this->notes[$channel->id]['read_since_last_write'] = 0;
258 $this->notes[$channel->id]['wait_msgs'] = $this->randomWaitMsgs($channel);
259 $this->notes[$channel->id]['wait_time'] = $this->randomWaitTime($channel);
262 private function tagChannelSpecialSent(Channel $channel, $classification) {
263 $this->getNotes($channel);
264 $this->notes[$channel->id]['last_special'][$classification] = time();
267 private function getTimeSinceSpecial(Channel $channel, $classification) {
268 $notes = $this->getNotes($channel);
269 if (isset($notes['last_special'][$classification])) {
270 return time() - $notes['last_special'][$classification];
275 private function isDirectedAtMe($raw_text) {
276 $text = strtolower($raw_text);
277 if (strpos($text, 'horsti') !== false) {
283 private function shouldRespond(Channel $channel) {
284 $setting = $channel->getChatSetting('respond', 'yes');
285 if ($setting == 'yes') {
288 if ($setting == '50') {
289 return random_int(0, 1);