]> git.localhorst.tv Git - alttp.git/commitdiff
add chat bot
authorDaniel Karbach <daniel.karbach@localhorst.tv>
Fri, 19 Jan 2024 16:35:28 +0000 (17:35 +0100)
committerDaniel Karbach <daniel.karbach@localhorst.tv>
Fri, 19 Jan 2024 16:35:28 +0000 (17:35 +0100)
app/Console/Commands/TwitchBotCommand.php
app/Console/Commands/TwitchChatBotCommand.php [new file with mode: 0644]
app/Models/Channel.php
app/Models/ChatLog.php
app/TwitchBot/TwitchAppBot.php [new file with mode: 0644]
app/TwitchBot/TwitchBot.php
app/TwitchBot/TwitchChatBot.php [new file with mode: 0644]
database/migrations/2024_01_19_140728_channel_chat_flag.php [new file with mode: 0644]

index 55ae34b8f7b3dc9c4534f1ba7d6f0b42a8104627..241a8854d9febd00c5f994f23c6e342ca7e7f260 100644 (file)
@@ -2,7 +2,7 @@
 
 namespace App\Console\Commands;
 
-use App\TwitchBot\TwitchBot;
+use App\TwitchBot\TwitchAppBot;
 use Illuminate\Console\Command;
 
 class TwitchBotCommand extends Command {
@@ -27,7 +27,7 @@ class TwitchBotCommand extends Command {
         * @return int
         */
        public function handle() {
-               $bot = new TwitchBot();
+               $bot = new TwitchAppBot();
 
                $bot->getLoop()->addSignal(SIGINT, function() use ($bot) {
                        $bot->stop();
diff --git a/app/Console/Commands/TwitchChatBotCommand.php b/app/Console/Commands/TwitchChatBotCommand.php
new file mode 100644 (file)
index 0000000..753a393
--- /dev/null
@@ -0,0 +1,43 @@
+<?php
+
+namespace App\Console\Commands;
+
+use App\TwitchBot\TwitchChatBot;
+use Illuminate\Console\Command;
+
+class TwitchChatBotCommand extends Command {
+
+       /**
+        * The name and signature of the console command.
+        *
+        * @var string
+        */
+       protected $signature = 'twitch:chatbot';
+
+       /**
+        * The console command description.
+        *
+        * @var string
+        */
+       protected $description = 'Runs the chat twitch bot';
+
+       /**
+        * Execute the console command.
+        *
+        * @return int
+        */
+       public function handle() {
+               $bot = new TwitchChatBot();
+
+               $bot->getLoop()->addSignal(SIGINT, function() use ($bot) {
+                       $bot->stop();
+               });
+
+               $bot->run();
+
+               return 0;
+       }
+
+}
+
+?>
index 025bb34151cdd9ab56fa32b59c6f89412fd05616..255b52cef3537ceecfb14ac8a354b5f37af81fee 100644 (file)
@@ -31,6 +31,7 @@ class Channel extends Model
        }
 
        protected $casts = [
+               'chat' => 'boolean',
                'chat_commands' => 'array',
                'languages' => 'array',
                'join' => 'boolean',
index aa4e7aeef913185a1120043b2e8fcfbf66c9e201..6f72c352b42ff356123ad3e1bdb29b8d8407e487 100644 (file)
@@ -25,7 +25,7 @@ class ChatLog extends Model {
                        $this->type = 'system';
                        return;
                }
-               if ($this->nick == 'localhorsttv') {
+               if (in_array($this->nick, ['horstiebot', 'localhorsttv'])) {
                        $this->type = 'self';
                        return;
                }
diff --git a/app/TwitchBot/TwitchAppBot.php b/app/TwitchBot/TwitchAppBot.php
new file mode 100644 (file)
index 0000000..4e2c7d4
--- /dev/null
@@ -0,0 +1,69 @@
+<?php
+
+namespace App\TwitchBot;
+
+use App\Models\Channel;
+use App\Models\TwitchBotCommand;
+
+class TwitchAppBot extends TwitchBot {
+
+       public function __construct() {
+               parent::__construct('localhorsttv');
+               $this->listenCommands();
+       }
+
+       public function logMessage(IRCMessage $msg) {
+               $msg->log();
+       }
+
+       public function handlePrivMsg(IRCMessage $msg) {
+               $target = $msg->getPrivMsgTarget();
+               if ($target[0] != '#') return; // direct message
+               $text = $msg->getText();
+               if ($text[0] != '!') return;
+               $channel = $this->getMessageChannel($msg);
+               if (!$channel) return;
+               $this->handleChatCommand($channel, $msg);
+       }
+
+       public function handleChatCommand(Channel $channel, IRCMessage $msg) {
+               $cmd = explode(' ', ltrim($msg->getText(), '!'), 2);
+               if (!isset($channel->chat_commands[$cmd[0]])) return;
+               $config = $channel->chat_commands[$cmd[0]];
+               $this->getLogger()->info('got command '.$cmd[0].' on channel '.$channel->title);
+               try {
+                       $command = ChatCommand::create($this, $channel, $config);
+                       $command->execute($cmd[1] ?? '');
+               } catch (\Exception $e) {
+                       $this->getLogger()->warning('error executing command '.$cmd[0].' on channel '.$channel->title.': '.$e->getMessage());
+               }
+       }
+
+       public function joinChannels() {
+               $this->getLogger()->info('joining channels');
+               $channels = Channel::where('twitch_chat', '!=', '')->where('join', '=', true)->get();
+               $names = [];
+               foreach ($channels as $channel) {
+                       $names[] = $channel->twitch_chat;
+               }
+               $chunks = array_chunk($names, 10);
+               foreach ($chunks as $chunk) {
+                       $this->sendIRCMessage(IRCMessage::join($chunk));
+               }
+       }
+
+
+       private function listenCommands() {
+               $this->getLoop()->addPeriodicTimer(1, function () {
+                       if (!$this->isReady()) return;
+                       $command = TwitchBotCommand::where('status', '=', 'pending')->oldest()->first();
+                       if ($command) {
+                               try {
+                                       $command->execute($this);
+                               } catch (\Exception $e) {
+                               }
+                       }
+               });
+       }
+
+}
index c8d77a36d6f656007a0b3b50c009689b55f3adbe..79b6bf13f1861af5bde63e400992cbc060e08ae0 100644 (file)
@@ -3,7 +3,6 @@
 namespace App\TwitchBot;
 
 use App\Models\Channel;
-use App\Models\TwitchBotCommand;
 use App\Models\TwitchToken;
 use Monolog\Handler\StreamHandler;
 use Monolog\Logger;
@@ -14,18 +13,18 @@ use React\EventLoop\Loop;
 
 class TwitchBot {
 
-       public function __construct() {
+       public function __construct($nick) {
+               $this->nick = $nick;
                $this->logger = new Logger('TwitchBot');
                $this->logger->pushHandler(new StreamHandler('php://stdout', Logger::INFO));
 
-               $this->token = TwitchToken::firstWhere('nick', 'localhorsttv');
+               $this->token = TwitchToken::firstWhere('nick', $nick);
                if (!$this->token) {
                        throw new \Exception('unable to find access token');
                }
 
                $this->connector = new Connector();
                $this->connect();
-               $this->listenCommands();
                $this->startPinger();
        }
 
@@ -37,6 +36,10 @@ class TwitchBot {
                return Loop::get();
        }
 
+       public function isReady() {
+               return $this->ready;
+       }
+
        public function run() {
                $this->shutting_down = false;
                $this->getLoop()->run();
@@ -106,7 +109,7 @@ class TwitchBot {
                if ($msg->isPong()) {
                        return;
                }
-               $msg->log();
+               $this->logMessage($msg);
                if ($msg->isPrivMsg()) {
                        $this->handlePrivMsg($msg);
                        return;
@@ -125,58 +128,26 @@ class TwitchBot {
                }
        }
 
-       public function handlePrivMsg(IRCMessage $msg) {
+       public function getMessageChannel(IRCMessage $msg) {
                $target = $msg->getPrivMsgTarget();
-               if ($target[0] != '#') return; // direct message
-               $text = $msg->getText();
-               if ($text[0] != '!') return;
-               $channel = Channel::firstWhere('twitch_chat', '=', $target);
-               if (!$channel) return;
-               $this->handleChatCommand($channel, $msg);
-       }
-
-       public function handleChatCommand(Channel $channel, IRCMessage $msg) {
-               $cmd = explode(' ', ltrim($msg->getText(), '!'), 2);
-               if (!isset($channel->chat_commands[$cmd[0]])) return;
-               $config = $channel->chat_commands[$cmd[0]];
-               $this->logger->info('got command '.$cmd[0].' on channel '.$channel->title);
-               try {
-                       $command = ChatCommand::create($this, $channel, $config);
-                       $command->execute($cmd[1] ?? '');
-               } catch (\Exception $e) {
-                       $this->logger->warning('error executing command '.$cmd[0].' on channel '.$channel->title.': '.$e->getMessage());
+               if (substr($target, 0, 1) !== '#') {
+                       $target = '#'.$target;
                }
+               return Channel::firstWhere('twitch_chat', '=', $target);
+       }
+
+       public function logMessage(IRCMessage $msg) {
+       }
+
+       public function handlePrivMsg(IRCMessage $msg) {
        }
 
        public function login() {
                $this->ws->send('PASS oauth:'.$this->token->access);
-               $this->ws->send('NICK localhorsttv');
+               $this->ws->send('NICK '.$this->nick);
        }
 
        public function joinChannels() {
-               $this->logger->info('joining channels');
-               $channels = Channel::where('twitch_chat', '!=', '')->where('join', '=', true)->get();
-               $names = [];
-               foreach ($channels as $channel) {
-                       $names[] = $channel->twitch_chat;
-               }
-               $chunks = array_chunk($names, 10);
-               foreach ($chunks as $chunk) {
-                       $this->sendIRCMessage(IRCMessage::join($chunk));
-               }
-       }
-
-       private function listenCommands() {
-               $this->getLoop()->addPeriodicTimer(1, function () {
-                       if (!$this->ready) return;
-                       $command = TwitchBotCommand::where('status', '=', 'pending')->oldest()->first();
-                       if ($command) {
-                               try {
-                                       $command->execute($this);
-                               } catch (\Exception $e) {
-                               }
-                       }
-               });
        }
 
        private function startPinger() {
@@ -200,6 +171,7 @@ class TwitchBot {
 
        private $logger;
 
+       private $nick;
        private $token;
 
        private $connector;
diff --git a/app/TwitchBot/TwitchChatBot.php b/app/TwitchBot/TwitchChatBot.php
new file mode 100644 (file)
index 0000000..0469fbc
--- /dev/null
@@ -0,0 +1,107 @@
+<?php
+
+namespace App\TwitchBot;
+
+use App\Models\Channel;
+use App\Models\ChatLog;
+
+class TwitchChatBot extends TwitchBot {
+
+       public function __construct() {
+               parent::__construct('horstiebot');
+               $this->channels = Channel::where('twitch_chat', '!=', '')->where('chat', '=', true)->get();
+               foreach ($this->channels as $channel) {
+                       $this->notes[$channel->id] = [
+                               'last_read' => 0,
+                               'last_write' => time(),
+                               'read_since_last_write' => 0,
+                               'wait_msgs' => $this->randomWaitMsgs($channel),
+                               'wait_time' => $this->randomWaitTime($channel),
+                       ];
+               }
+               $this->startTimer();
+       }
+
+       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);
+       }
+
+
+       private function startTimer() {
+               $this->getLoop()->addPeriodicTimer(1, function () {
+                       if (!$this->isReady()) return;
+                       foreach ($this->channels as $channel) {
+                               $this->decideSend($channel);
+                       }
+               });
+       }
+
+       private function decideSend(Channel $channel) {
+               $notes = $this->notes[$channel->id];
+               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->randomMsg($channel);
+               if (!$text) return;
+               $this->tagChannelWrite($channel);
+               $this->sendIRCMessage(IRCMessage::privmsg($channel->twitch_chat, $text));
+       }
+
+       private function randomMsg(Channel $channel) {
+               $line = ChatLog::where('type', '=', 'chat')->where('banned', '=', false)->inRandomOrder()->first();
+               return $line->text_content;
+       }
+
+       private function randomWaitMsgs(Channel $channel) {
+               return random_int(1, 10);
+       }
+
+       private function randomWaitTime(Channel $channel) {
+               return random_int(1, 1800);
+       }
+
+       private function tagChannelRead(Channel $channel) {
+               $this->notes[$channel->id]['last_read'] = time();
+               ++$this->notes[$channel->id]['read_since_last_write'];
+       }
+
+       private function tagChannelWrite(Channel $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 $channels;
+       private $notes = [];
+
+}
diff --git a/database/migrations/2024_01_19_140728_channel_chat_flag.php b/database/migrations/2024_01_19_140728_channel_chat_flag.php
new file mode 100644 (file)
index 0000000..8af1700
--- /dev/null
@@ -0,0 +1,32 @@
+<?php
+
+use Illuminate\Database\Migrations\Migration;
+use Illuminate\Database\Schema\Blueprint;
+use Illuminate\Support\Facades\Schema;
+
+return new class extends Migration
+{
+       /**
+        * Run the migrations.
+        *
+        * @return void
+        */
+       public function up()
+       {
+               Schema::table('channels', function (Blueprint $table) {
+                       $table->boolean('chat')->default(false);
+               });
+       }
+
+       /**
+        * Reverse the migrations.
+        *
+        * @return void
+        */
+       public function down()
+       {
+               Schema::table('channels', function (Blueprint $table) {
+                       $table->dropColumn('chat');
+               });
+       }
+};