]> git.localhorst.tv Git - alttp.git/blob - app/TwitchBot/TwitchBot.php
update muffins tracker
[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($nick) {
18                 $this->nick = $nick;
19                 $this->logger = new Logger('TwitchBot');
20                 $this->logger->pushHandler(new StreamHandler('php://stdout', Logger::INFO));
21
22                 $this->token = TwitchToken::firstWhere('nick', $nick);
23                 if (!$this->token) {
24                         throw new \Exception('unable to find access token');
25                 }
26                 if ($this->token->hasExpired()) {
27                         $this->token->refresh();
28                 }
29
30                 $this->connector = new Connector();
31                 $this->connect();
32                 $this->startPinger();
33         }
34
35         public function getLogger() {
36                 return $this->logger;
37         }
38
39         public function getLoop() {
40                 return Loop::get();
41         }
42
43         public function isReady() {
44                 return $this->ready;
45         }
46
47         public function run() {
48                 $this->shutting_down = false;
49                 $this->getLoop()->run();
50         }
51
52         public function stop() {
53                 $this->logger->info('shutting down');
54                 $this->shutting_down = true;
55                 $this->disconnect();
56                 $this->getLoop()->stop();
57         }
58
59
60         public function connect() {
61                 ($this->connector)('wss://irc-ws.chat.twitch.tv:443')->done(
62                         [$this, 'handleWsConnect'],
63                         [$this, 'handleWsConnectError'],
64                 );
65         }
66
67         public function disconnect() {
68                 $this->ws->close();
69         }
70
71         public function handleWsConnect(WebSocket $ws) {
72                 $this->logger->info('websocket connection established');
73                 $this->ws = $ws;
74                 $ws->on('message', [$this, 'handleWsMessage']);
75                 $ws->on('close', [$this, 'handleWsClose']);
76                 $ws->on('error', [$this, 'handleWsError']);
77                 $ws->send('CAP REQ :twitch.tv/tags twitch.tv/commands');
78                 $this->login();
79         }
80
81         public function handleWsConnectError(WebSocket $ws) {
82                 $this->logger->error('failed to establish websocket connection');
83         }
84
85         public function handleWsMessage(Message $message, WebSocket $ws) {
86                 $irc_messages = explode("\r\n", rtrim($message->getPayload(), "\r\n"));
87                 foreach ($irc_messages as $irc_message) {
88                         $this->logger->info('received IRC message '.$irc_message);
89                         $this->handleIRCMessage(IRCMessage::fromString($irc_message));
90                 }
91         }
92
93         public function handleWsClose(int $op, string $reason) {
94                 $this->ready = false;
95                 $this->logger->info('websocket connection closed: '.$reason.' ['.$op.']');
96                 if (!$this->shutting_down) {
97                         $this->logger->info('reconnecting in 5 seconds');
98                         Loop::addTimer(5, [$this, 'connect']);
99                 }
100         }
101
102         public function handleWsError(\Exception $e, WebSocket $ws) {
103                 $this->logger->error('websocket error '.$e->getMessage());
104         }
105
106
107         public function handleIRCMessage(IRCMessage $msg) {
108                 $this->last_contact = time();
109                 if ($msg->isPing()) {
110                         $this->sendIRCMessage($msg->makePong());
111                         return;
112                 }
113                 if ($msg->isPong()) {
114                         return;
115                 }
116                 $this->logMessage($msg);
117                 if ($msg->isPrivMsg()) {
118                         $this->handlePrivMsg($msg);
119                         return;
120                 }
121                 if ($msg->isWhisper()) {
122                         $this->handleWhisper($msg);
123                         return;
124                 }
125                 if ($msg->isRoomstate()) {
126                         $this->handleRoomstate($msg);
127                         return;
128                 }
129                 if ($msg->isNotice() && $msg->getText() == 'Login authentication failed') {
130                         $this->logger->notice('login failed, refreshing access token');
131                         $this->token->refresh();
132                         $this->login();
133                         return;
134                 }
135                 if ($msg->command == '001') {
136                         // successful login
137                         $this->joinChannels();
138                         $this->ready = true;
139                         return;
140                 }
141                 if ($msg->command == 'GLOBALUSERSTATE') {
142                         // receive own user metadata
143                         $this->handleUserState($msg);
144                         return;
145                 }
146         }
147
148         public function getMessageChannel(IRCMessage $msg) {
149                 $target = $msg->getPrivMsgTarget();
150                 if (substr($target, 0, 1) !== '#') {
151                         $target = '#'.$target;
152                 }
153                 return Channel::firstWhere('twitch_chat', '=', $target);
154         }
155
156         public function logMessage(IRCMessage $msg) {
157         }
158
159         public function handlePrivMsg(IRCMessage $msg) {
160         }
161
162         public function handleRoomstate(IRCMessage $msg) {
163         }
164
165         public function handleUserState(IRCMessage $msg) {
166                 if (isset($msg->tags['user-id'])) {
167                         $this->user_id = $msg->tags['user-id'];
168                 }
169         }
170
171         public function handleWhisper(IRCMessage $msg) {
172         }
173
174         public function login() {
175                 $this->ws->send('PASS oauth:'.$this->token->access);
176                 $this->ws->send('NICK '.$this->nick);
177         }
178
179         public function joinChannels() {
180         }
181
182         private function startPinger() {
183                 $this->getLoop()->addPeriodicTimer(15, function () {
184                         if (!$this->ready) return;
185                         if (time() - $this->last_contact < 60) return;
186                         try {
187                                 $this->sendIRCMessage(IRCMessage::ping($this->nick));
188                         } catch (\Exception $e) {
189                         }
190                 });
191         }
192
193         public function sendIRCMessage(IRCMessage $msg) {
194                 $irc_message = $msg->encode();
195                 $this->logger->info('sending IRC message '.$irc_message);
196                 $this->ws->send($irc_message);
197                 $this->last_contact = time();
198         }
199
200         public function sendWhisper($to, $msg) {
201                 $this->logger->info('sending whisper to '.$to.': '.$msg);
202                 try {
203                         $response = $this->token->request()->post('/whispers?from_user_id='.$this->user_id.'&to_user_id='.$to, [
204                                 'message' => $msg,
205                         ]);
206                         if (!$response->successful()) {
207                                 $this->logger->error('sending whisper to '.$to.': '.$response->status());
208                         }
209                 } catch (\Exception $e) {
210                         $this->logger->error('sending whisper to '.$to.': '.$e->getMessage());
211                 }
212         }
213
214
215         protected function listenCommands() {
216                 $this->getLoop()->addPeriodicTimer(1, function () {
217                         if (!$this->isReady()) return;
218                         $command = TwitchBotCommand::where('bot_nick', '=', $this->nick)->where('status', '=', 'pending')->oldest()->first();
219                         if ($command) {
220                                 try {
221                                         $command->execute($this);
222                                 } catch (\Exception $e) {
223                                 }
224                         }
225                 });
226         }
227
228
229         private $logger;
230
231         private $nick;
232         private $token;
233         private $user_id = '';
234
235         private $connector;
236         private $ws;
237         private $ready = false;
238         private $shutting_down = false;
239
240         private $last_contact;
241
242 }
243
244 ?>