]> git.localhorst.tv Git - alttp.git/blob - app/TwitchBot/TwitchBot.php
a45de87fcce69a36b32e06cdb2fc567b222144a0
[alttp.git] / app / TwitchBot / TwitchBot.php
1 <?php
2
3 namespace App\TwitchBot;
4
5 use App\Models\Channel;
6 use App\Models\TwitchBotCommand;
7 use App\Models\TwitchToken;
8 use Monolog\Handler\StreamHandler;
9 use Monolog\Logger;
10 use Ratchet\Client\Connector;
11 use Ratchet\Client\WebSocket;
12 use Ratchet\RFC6455\Messaging\Message;
13 use React\EventLoop\Loop;
14
15 class TwitchBot {
16
17         public function __construct() {
18                 $this->logger = new Logger('TwitchBot');
19                 $this->logger->pushHandler(new StreamHandler('php://stdout', Logger::INFO));
20
21                 $this->token = TwitchToken::firstWhere('nick', 'localhorsttv');
22                 if (!$this->token) {
23                         throw new \Exception('unable to find access token');
24                 }
25
26                 $this->connector = new Connector();
27                 $this->connect();
28                 $this->listenCommands();
29                 $this->startPinger();
30         }
31
32         public function getLogger() {
33                 return $this->logger;
34         }
35
36         public function getLoop() {
37                 return Loop::get();
38         }
39
40         public function run() {
41                 $this->shutting_down = false;
42                 $this->getLoop()->run();
43         }
44
45         public function stop() {
46                 $this->logger->info('shutting down');
47                 $this->shutting_down = true;
48                 $this->disconnect();
49                 $this->getLoop()->stop();
50         }
51
52
53         public function connect() {
54                 ($this->connector)('wss://irc-ws.chat.twitch.tv:443')->done(
55                         [$this, 'handleWsConnect'],
56                         [$this, 'handleWsConnectError'],
57                 );
58         }
59
60         public function disconnect() {
61                 $this->ws->close();
62         }
63
64         public function handleWsConnect(WebSocket $ws) {
65                 $this->logger->info('websocket connection established');
66                 $this->ws = $ws;
67                 $ws->on('message', [$this, 'handleWsMessage']);
68                 $ws->on('close', [$this, 'handleWsClose']);
69                 $ws->on('error', [$this, 'handleWsError']);
70                 $ws->send('CAP REQ :twitch.tv/tags twitch.tv/commands');
71                 $this->login();
72         }
73
74         public function handleWsConnectError(WebSocket $ws) {
75                 $this->logger->error('failed to establish websocket connection');
76         }
77
78         public function handleWsMessage(Message $message, WebSocket $ws) {
79                 $irc_messages = explode("\r\n", rtrim($message->getPayload(), "\r\n"));
80                 foreach ($irc_messages as $irc_message) {
81                         $this->logger->info('received IRC message '.$irc_message);
82                         $this->handleIRCMessage(IRCMessage::fromString($irc_message));
83                 }
84         }
85
86         public function handleWsClose(int $op, string $reason) {
87                 $this->ready = false;
88                 $this->logger->info('websocket connection closed: '.$reason.' ['.$op.']');
89                 if (!$this->shutting_down) {
90                         $this->logger->info('reconnecting in 5 seconds');
91                         Loop::addTimer(5, [$this, 'connect']);
92                 }
93         }
94
95         public function handleWsError(\Exception $e, WebSocket $ws) {
96                 $this->logger->error('websocket error '.$e->getMessage());
97         }
98
99
100         public function handleIRCMessage(IRCMessage $msg) {
101                 $this->last_contact = time();
102                 if ($msg->isPrivMsg()) {
103                         $this->handlePrivMsg($msg);
104                         return;
105                 }
106                 if ($msg->isPing()) {
107                         $this->sendIRCMessage($msg->makePong());
108                         return;
109                 }
110                 if ($msg->isPong()) {
111                         return;
112                 }
113                 if ($msg->isNotice() && $msg->getText() == 'Login authentication failed') {
114                         $this->logger->notice('login failed, refreshing access token');
115                         $this->token->refresh();
116                         $this->login();
117                         return;
118                 }
119                 if ($msg->command == '001') {
120                         // successful login
121                         $this->joinChannels();
122                         $this->ready = true;
123                         return;
124                 }
125         }
126
127         public function handlePrivMsg(IRCMessage $msg) {
128                 $target = $msg->getPrivMsgTarget();
129                 if ($target[0] != '#') return;
130                 $text = $msg->getText();
131                 if ($text[0] != '!') return;
132                 $channel = Channel::firstWhere('twitch_chat', '=', $target);
133                 if (!$channel) return;
134                 $this->handleChatCommand($channel, $msg);
135         }
136
137         public function handleChatCommand(Channel $channel, IRCMessage $msg) {
138                 $cmd = explode(' ', ltrim($msg->getText(), '!'), 2);
139                 if (!isset($channel->chat_commands[$cmd[0]])) return;
140                 $config = $channel->chat_commands[$cmd[0]];
141                 $this->logger->info('got command '.$cmd[0].' on channel '.$channel->title);
142                 try {
143                         $command = ChatCommand::create($this, $channel, $config);
144                         $command->execute($cmd[1] ?? '');
145                 } catch (\Exception $e) {
146                         $this->logger->warning('error executing command '.$cmd[0].' on channel '.$channel->title.': '.$e->getMessage());
147                 }
148         }
149
150         public function login() {
151                 $this->ws->send('PASS oauth:'.$this->token->access);
152                 $this->ws->send('NICK localhorsttv');
153         }
154
155         public function joinChannels() {
156                 $this->logger->info('joining channels');
157                 $channels = Channel::where('twitch_chat', '!=', '')->where('join', '=', true)->get();
158                 $names = [];
159                 foreach ($channels as $channel) {
160                         $names[] = $channel->twitch_chat;
161                 }
162                 $chunks = array_chunk($names, 10);
163                 foreach ($chunks as $chunk) {
164                         $this->sendIRCMessage(IRCMessage::join($chunk));
165                 }
166         }
167
168         private function listenCommands() {
169                 $this->getLoop()->addPeriodicTimer(1, function () {
170                         if (!$this->ready) return;
171                         $command = TwitchBotCommand::where('status', '=', 'pending')->oldest()->first();
172                         if ($command) {
173                                 try {
174                                         $command->execute($this);
175                                 } catch (\Exception $e) {
176                                 }
177                         }
178                 });
179         }
180
181         private function startPinger() {
182                 $this->getLoop()->addPeriodicTimer(15, function () {
183                         if (!$this->ready) return;
184                         if (time() - $this->last_contact < 60) return;
185                         try {
186                                 $this->sendIRCMessage(IRCMessage::ping());
187                         } catch (\Exception $e) {
188                         }
189                 });
190         }
191
192         public function sendIRCMessage(IRCMessage $msg) {
193                 $irc_message = $msg->encode();
194                 $this->logger->info('sending IRC message '.$irc_message);
195                 $this->ws->send($irc_message);
196                 $this->last_contact = time();
197         }
198
199
200         private $logger;
201
202         private $token;
203
204         private $connector;
205         private $ws;
206         private $ready = false;
207         private $shutting_down = false;
208
209         private $last_contact;
210
211 }
212
213 ?>