namespace App\Console\Commands;
-use App\TwitchBot\TwitchBot;
+use App\TwitchBot\TwitchAppBot;
use Illuminate\Console\Command;
class TwitchBotCommand extends Command {
* @return int
*/
public function handle() {
- $bot = new TwitchBot();
+ $bot = new TwitchAppBot();
$bot->getLoop()->addSignal(SIGINT, function() use ($bot) {
$bot->stop();
--- /dev/null
+<?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;
+ }
+
+}
+
+?>
}
protected $casts = [
+ 'chat' => 'boolean',
'chat_commands' => 'array',
'languages' => 'array',
'join' => 'boolean',
$this->type = 'system';
return;
}
- if ($this->nick == 'localhorsttv') {
+ if (in_array($this->nick, ['horstiebot', 'localhorsttv'])) {
$this->type = 'self';
return;
}
--- /dev/null
+<?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) {
+ }
+ }
+ });
+ }
+
+}
namespace App\TwitchBot;
use App\Models\Channel;
-use App\Models\TwitchBotCommand;
use App\Models\TwitchToken;
use Monolog\Handler\StreamHandler;
use Monolog\Logger;
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();
}
return Loop::get();
}
+ public function isReady() {
+ return $this->ready;
+ }
+
public function run() {
$this->shutting_down = false;
$this->getLoop()->run();
if ($msg->isPong()) {
return;
}
- $msg->log();
+ $this->logMessage($msg);
if ($msg->isPrivMsg()) {
$this->handlePrivMsg($msg);
return;
}
}
- 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() {
private $logger;
+ private $nick;
private $token;
private $connector;
--- /dev/null
+<?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 = [];
+
+}
--- /dev/null
+<?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');
+ });
+ }
+};