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() {
17 $this->logger = new Logger('TwitchBot');
18 $this->logger->pushHandler(new StreamHandler('php://stdout', Logger::INFO));
20 $this->token = TwitchToken::firstWhere('nick', 'localhorsttv');
22 throw new \Exception('unable to find access token');
25 $this->connector = new Connector();
29 public function getLogger() {
33 public function getLoop() {
37 public function run() {
38 $this->shutting_down = false;
39 $this->getLoop()->run();
42 public function stop() {
43 $this->logger->info('shutting down');
44 $this->shutting_down = true;
46 $this->getLoop()->stop();
50 public function connect() {
51 ($this->connector)('wss://irc-ws.chat.twitch.tv:443')->done(
52 [$this, 'handleWsConnect'],
53 [$this, 'handleWsConnectError'],
57 public function disconnect() {
61 public function handleWsConnect(WebSocket $ws) {
62 $this->logger->info('websocket connection established');
64 $ws->on('message', [$this, 'handleWsMessage']);
65 $ws->on('close', [$this, 'handleWsClose']);
66 $ws->on('error', [$this, 'handleWsError']);
67 $ws->send('CAP REQ :twitch.tv/tags twitch.tv/commands');
71 public function handleWsConnectError(WebSocket $ws) {
72 $this->logger->error('failed to establish websocket connection');
75 public function handleWsMessage(Message $message, WebSocket $ws) {
76 $irc_messages = explode("\r\n", rtrim($message->getPayload(), "\r\n"));
77 foreach ($irc_messages as $irc_message) {
78 $this->logger->debug('received IRC message '.$irc_message);
79 $this->handleIRCMessage(IRCMessage::fromString($irc_message));
83 public function handleWsClose(int $op, string $reason) {
84 $this->logger->info('websocket connection closed: '.$reason.' ['.$op.']');
85 if (!$this->shutting_down) {
86 $this->logger->info('reconnecting in 10 seconds');
87 Loop::addTimer(10, [$this, 'connect']);
91 public function handleWsError(\Exception $e, WebSocket $ws) {
92 $this->logger->error('websocket error '.$e->getMessage());
96 public function handleIRCMessage(IRCMessage $msg) {
97 if ($msg->isPrivMsg()) {
98 $this->handlePrivMsg($msg);
101 if ($msg->isPing()) {
102 $this->sendIRCMessage($msg->makePong());
105 if ($msg->isNotice() && $msg->getText() == 'Login authentication failed') {
106 $this->logger->notice('login failed, refreshing access token');
107 $this->token->refresh();
111 if ($msg->command == '001') {
113 $this->joinChannels();
118 public function handlePrivMsg(IRCMessage $msg) {
119 $target = $msg->getPrivMsgTarget();
120 if ($target[0] != '#') return;
121 $text = $msg->getText();
122 if ($text[0] != '!') return;
123 $channel = Channel::firstWhere('twitch_chat', '=', $target);
124 if (!$channel) return;
125 $this->handleChatCommand($channel, $msg);
128 public function handleChatCommand(Channel $channel, IRCMessage $msg) {
129 $cmd = explode(' ', ltrim($msg->getText(), '!'), 2);
130 if (!isset($channel->chat_commands[$cmd[0]])) return;
131 $config = $channel->chat_commands[$cmd[0]];
132 $this->logger->info('got command '.$cmd[0].' on channel '.$channel->title);
134 $command = ChatCommand::create($this, $channel, $config);
135 $command->execute($cmd[1] ?? '');
136 } catch (\Exception $e) {
137 $this->logger->warning('error executing command '.$cmd[0].' on channel '.$channel->title.': '.$e->getMessage());
141 public function login() {
142 $this->ws->send('PASS oauth:'.$this->token->access);
143 $this->ws->send('NICK localhorsttv');
146 public function joinChannels() {
147 $this->logger->info('joining channels');
148 $channels = Channel::where('twitch_chat', '!=', '')->get();
150 foreach ($channels as $channel) {
151 $names[] = $channel->twitch_chat;
153 $chunks = array_chunk($names, 10);
154 foreach ($chunks as $chunk) {
155 $this->sendIRCMessage(IRCMessage::join($chunk));
159 public function sendIRCMessage(IRCMessage $msg) {
160 $irc_message = $msg->encode();
161 $this->logger->debug('sending IRC message '.$irc_message);
162 $this->ws->send($irc_message);
172 private $shutting_down = false;