3 namespace App\TwitchBot;
5 use App\Models\Channel;
6 use App\Models\TwitchToken;
7 use Monolog\Handler\StreamHandler;
9 use Ratchet\Client\Connector;
10 use Ratchet\Client\WebSocket;
11 use Ratchet\RFC6455\Messaging\Message;
12 use React\EventLoop\Loop;
16 public function __construct($nick) {
18 $this->logger = new Logger('TwitchBot');
19 $this->logger->pushHandler(new StreamHandler('php://stdout', Logger::INFO));
21 $this->token = TwitchToken::firstWhere('nick', $nick);
23 throw new \Exception('unable to find access token');
26 $this->connector = new Connector();
31 public function getLogger() {
35 public function getLoop() {
39 public function isReady() {
43 public function run() {
44 $this->shutting_down = false;
45 $this->getLoop()->run();
48 public function stop() {
49 $this->logger->info('shutting down');
50 $this->shutting_down = true;
52 $this->getLoop()->stop();
56 public function connect() {
57 ($this->connector)('wss://irc-ws.chat.twitch.tv:443')->done(
58 [$this, 'handleWsConnect'],
59 [$this, 'handleWsConnectError'],
63 public function disconnect() {
67 public function handleWsConnect(WebSocket $ws) {
68 $this->logger->info('websocket connection established');
70 $ws->on('message', [$this, 'handleWsMessage']);
71 $ws->on('close', [$this, 'handleWsClose']);
72 $ws->on('error', [$this, 'handleWsError']);
73 $ws->send('CAP REQ :twitch.tv/tags twitch.tv/commands');
77 public function handleWsConnectError(WebSocket $ws) {
78 $this->logger->error('failed to establish websocket connection');
81 public function handleWsMessage(Message $message, WebSocket $ws) {
82 $irc_messages = explode("\r\n", rtrim($message->getPayload(), "\r\n"));
83 foreach ($irc_messages as $irc_message) {
84 $this->logger->info('received IRC message '.$irc_message);
85 $this->handleIRCMessage(IRCMessage::fromString($irc_message));
89 public function handleWsClose(int $op, string $reason) {
91 $this->logger->info('websocket connection closed: '.$reason.' ['.$op.']');
92 if (!$this->shutting_down) {
93 $this->logger->info('reconnecting in 5 seconds');
94 Loop::addTimer(5, [$this, 'connect']);
98 public function handleWsError(\Exception $e, WebSocket $ws) {
99 $this->logger->error('websocket error '.$e->getMessage());
103 public function handleIRCMessage(IRCMessage $msg) {
104 $this->last_contact = time();
105 if ($msg->isPing()) {
106 $this->sendIRCMessage($msg->makePong());
109 if ($msg->isPong()) {
112 $this->logMessage($msg);
113 if ($msg->isPrivMsg()) {
114 $this->handlePrivMsg($msg);
117 if ($msg->isNotice() && $msg->getText() == 'Login authentication failed') {
118 $this->logger->notice('login failed, refreshing access token');
119 $this->token->refresh();
123 if ($msg->command == '001') {
125 $this->joinChannels();
131 public function getMessageChannel(IRCMessage $msg) {
132 $target = $msg->getPrivMsgTarget();
133 if (substr($target, 0, 1) !== '#') {
134 $target = '#'.$target;
136 return Channel::firstWhere('twitch_chat', '=', $target);
139 public function logMessage(IRCMessage $msg) {
142 public function handlePrivMsg(IRCMessage $msg) {
145 public function login() {
146 $this->ws->send('PASS oauth:'.$this->token->access);
147 $this->ws->send('NICK '.$this->nick);
150 public function joinChannels() {
153 private function startPinger() {
154 $this->getLoop()->addPeriodicTimer(15, function () {
155 if (!$this->ready) return;
156 if (time() - $this->last_contact < 60) return;
158 $this->sendIRCMessage(IRCMessage::ping());
159 } catch (\Exception $e) {
164 public function sendIRCMessage(IRCMessage $msg) {
165 $irc_message = $msg->encode();
166 $this->logger->info('sending IRC message '.$irc_message);
167 $this->ws->send($irc_message);
168 $this->last_contact = time();
179 private $ready = false;
180 private $shutting_down = false;
182 private $last_contact;