<?php
namespace App\TwitchBot;
-use Illuminate\Support\Facades\Http;
+
+use App\Models\Channel;
+use App\Models\TwitchBotCommand;
+use App\Models\TwitchToken;
use Monolog\Handler\StreamHandler;
use Monolog\Logger;
use Ratchet\Client\Connector;
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->fetchToken();
+ $this->token = TwitchToken::firstWhere('nick', $nick);
+ if (!$this->token) {
+ throw new \Exception('unable to find access token');
+ }
+ if ($this->token->hasExpired()) {
+ $this->token->refresh();
+ }
$this->connector = new Connector();
$this->connect();
+ $this->startPinger();
}
public function getLogger() {
return Loop::get();
}
+ public function isReady() {
+ return $this->ready;
+ }
+
public function run() {
+ $this->shutting_down = false;
$this->getLoop()->run();
}
public function stop() {
+ $this->logger->info('shutting down');
+ $this->shutting_down = true;
$this->disconnect();
$this->getLoop()->stop();
}
- public function fetchToken() {
- $this->logger->info('acquiring token');
- $rsp = Http::post('https://id.twitch.tv/oauth2/token', [
- 'client_id' => config('twitch.client_id'),
- 'client_secret' => config('twitch.client_secret'),
- 'code' => config('twitch.code'),
- 'grant_type' => 'authorization_code',
- 'redirect_uri' => config('twitch.redirect_uri'),
- ]);
- var_dump($rsp);
- var_dump($rsp->body());
- $this->token = $rsp->json();
- }
-
-
public function connect() {
($this->connector)('wss://irc-ws.chat.twitch.tv:443')->done(
[$this, 'handleWsConnect'],
}
public function handleWsConnect(WebSocket $ws) {
- $this->logger->info('websocket connection estblished');
+ $this->logger->info('websocket connection established');
$this->ws = $ws;
- $ws->on('message', [$this, 'handleWsMessage']);
- $ws->on('close', [$this, 'handleWsClose']);
- $ws->on('error', [$this, 'handleWsError']);
+ $ws->on('message', [$this, 'handleWsMessage']);
+ $ws->on('close', [$this, 'handleWsClose']);
+ $ws->on('error', [$this, 'handleWsError']);
$ws->send('CAP REQ :twitch.tv/tags twitch.tv/commands');
- $ws->send('PASS oauth:'.$this->token->access_token);
- $ws->send('NICK localhorsttv');
+ $this->login();
}
public function handleWsConnectError(WebSocket $ws) {
}
public function handleWsMessage(Message $message, WebSocket $ws) {
- $this->logger->info('websocket message received');
- var_dump($message->getPayload());
+ $irc_messages = explode("\r\n", rtrim($message->getPayload(), "\r\n"));
+ foreach ($irc_messages as $irc_message) {
+ $this->logger->info('received IRC message '.$irc_message);
+ $this->handleIRCMessage(IRCMessage::fromString($irc_message));
+ }
}
- public function handleWsClose(int $op, string $reason) {
+ public function handleWsClose(int $op, string $reason) {
+ $this->ready = false;
$this->logger->info('websocket connection closed: '.$reason.' ['.$op.']');
+ if (!$this->shutting_down) {
+ $this->logger->info('reconnecting in 5 seconds');
+ Loop::addTimer(5, [$this, 'connect']);
+ }
}
- public function handleWsError(\Exception $e, WebSocket $ws) {
+ public function handleWsError(\Exception $e, WebSocket $ws) {
$this->logger->error('websocket error '.$e->getMessage());
}
+ public function handleIRCMessage(IRCMessage $msg) {
+ $this->last_contact = time();
+ if ($msg->isPing()) {
+ $this->sendIRCMessage($msg->makePong());
+ return;
+ }
+ if ($msg->isPong()) {
+ return;
+ }
+ $this->logMessage($msg);
+ if ($msg->isPrivMsg()) {
+ $this->handlePrivMsg($msg);
+ return;
+ }
+ if ($msg->isNotice() && $msg->getText() == 'Login authentication failed') {
+ $this->logger->notice('login failed, refreshing access token');
+ $this->token->refresh();
+ $this->login();
+ return;
+ }
+ if ($msg->command == '001') {
+ // successful login
+ $this->joinChannels();
+ $this->ready = true;
+ return;
+ }
+ }
+
+ public function getMessageChannel(IRCMessage $msg) {
+ $target = $msg->getPrivMsgTarget();
+ 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 '.$this->nick);
+ }
+
+ public function joinChannels() {
+ }
+
+ private function startPinger() {
+ $this->getLoop()->addPeriodicTimer(15, function () {
+ if (!$this->ready) return;
+ if (time() - $this->last_contact < 60) return;
+ try {
+ $this->sendIRCMessage(IRCMessage::ping($this->nick));
+ } catch (\Exception $e) {
+ }
+ });
+ }
+
+ public function sendIRCMessage(IRCMessage $msg) {
+ $irc_message = $msg->encode();
+ $this->logger->info('sending IRC message '.$irc_message);
+ $this->ws->send($irc_message);
+ $this->last_contact = time();
+ }
+
+
+ protected function listenCommands() {
+ $this->getLoop()->addPeriodicTimer(1, function () {
+ if (!$this->isReady()) return;
+ $command = TwitchBotCommand::where('bot_nick', '=', $this->nick)->where('status', '=', 'pending')->oldest()->first();
+ if ($command) {
+ try {
+ $command->execute($this);
+ } catch (\Exception $e) {
+ }
+ }
+ });
+ }
+
+
private $logger;
- private $loop;
+ private $nick;
private $token;
private $connector;
private $ws;
+ private $ready = false;
+ private $shutting_down = false;
+
+ private $last_contact;
}