]> git.localhorst.tv Git - alttp.git/blob - app/TwitchBot/TwitchBot.php
e5a5aa6e442307b06bbae8b68635a07f4255b463
[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
27                 $this->connector = new Connector();
28                 $this->connect();
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 isReady() {
41                 return $this->ready;
42         }
43
44         public function run() {
45                 $this->shutting_down = false;
46                 $this->getLoop()->run();
47         }
48
49         public function stop() {
50                 $this->logger->info('shutting down');
51                 $this->shutting_down = true;
52                 $this->disconnect();
53                 $this->getLoop()->stop();
54         }
55
56
57         public function connect() {
58                 ($this->connector)('wss://irc-ws.chat.twitch.tv:443')->done(
59                         [$this, 'handleWsConnect'],
60                         [$this, 'handleWsConnectError'],
61                 );
62         }
63
64         public function disconnect() {
65                 $this->ws->close();
66         }
67
68         public function handleWsConnect(WebSocket $ws) {
69                 $this->logger->info('websocket connection established');
70                 $this->ws = $ws;
71                 $ws->on('message', [$this, 'handleWsMessage']);
72                 $ws->on('close', [$this, 'handleWsClose']);
73                 $ws->on('error', [$this, 'handleWsError']);
74                 $ws->send('CAP REQ :twitch.tv/tags twitch.tv/commands');
75                 $this->login();
76         }
77
78         public function handleWsConnectError(WebSocket $ws) {
79                 $this->logger->error('failed to establish websocket connection');
80         }
81
82         public function handleWsMessage(Message $message, WebSocket $ws) {
83                 $irc_messages = explode("\r\n", rtrim($message->getPayload(), "\r\n"));
84                 foreach ($irc_messages as $irc_message) {
85                         $this->logger->info('received IRC message '.$irc_message);
86                         $this->handleIRCMessage(IRCMessage::fromString($irc_message));
87                 }
88         }
89
90         public function handleWsClose(int $op, string $reason) {
91                 $this->ready = false;
92                 $this->logger->info('websocket connection closed: '.$reason.' ['.$op.']');
93                 if (!$this->shutting_down) {
94                         $this->logger->info('reconnecting in 5 seconds');
95                         Loop::addTimer(5, [$this, 'connect']);
96                 }
97         }
98
99         public function handleWsError(\Exception $e, WebSocket $ws) {
100                 $this->logger->error('websocket error '.$e->getMessage());
101         }
102
103
104         public function handleIRCMessage(IRCMessage $msg) {
105                 $this->last_contact = time();
106                 if ($msg->isPing()) {
107                         $this->sendIRCMessage($msg->makePong());
108                         return;
109                 }
110                 if ($msg->isPong()) {
111                         return;
112                 }
113                 $this->logMessage($msg);
114                 if ($msg->isPrivMsg()) {
115                         $this->handlePrivMsg($msg);
116                         return;
117                 }
118                 if ($msg->isNotice() && $msg->getText() == 'Login authentication failed') {
119                         $this->logger->notice('login failed, refreshing access token');
120                         $this->token->refresh();
121                         $this->login();
122                         return;
123                 }
124                 if ($msg->command == '001') {
125                         // successful login
126                         $this->joinChannels();
127                         $this->ready = true;
128                         return;
129                 }
130         }
131
132         public function getMessageChannel(IRCMessage $msg) {
133                 $target = $msg->getPrivMsgTarget();
134                 if (substr($target, 0, 1) !== '#') {
135                         $target = '#'.$target;
136                 }
137                 return Channel::firstWhere('twitch_chat', '=', $target);
138         }
139
140         public function logMessage(IRCMessage $msg) {
141         }
142
143         public function handlePrivMsg(IRCMessage $msg) {
144         }
145
146         public function login() {
147                 $this->ws->send('PASS oauth:'.$this->token->access);
148                 $this->ws->send('NICK '.$this->nick);
149         }
150
151         public function joinChannels() {
152         }
153
154         private function startPinger() {
155                 $this->getLoop()->addPeriodicTimer(15, function () {
156                         if (!$this->ready) return;
157                         if (time() - $this->last_contact < 60) return;
158                         try {
159                                 $this->sendIRCMessage(IRCMessage::ping($this->nick));
160                         } catch (\Exception $e) {
161                         }
162                 });
163         }
164
165         public function sendIRCMessage(IRCMessage $msg) {
166                 $irc_message = $msg->encode();
167                 $this->logger->info('sending IRC message '.$irc_message);
168                 $this->ws->send($irc_message);
169                 $this->last_contact = time();
170         }
171
172
173         protected function listenCommands() {
174                 $this->getLoop()->addPeriodicTimer(1, function () {
175                         if (!$this->isReady()) return;
176                         $command = TwitchBotCommand::where('bot_nick', '=', $this->nick)->where('status', '=', 'pending')->oldest()->first();
177                         if ($command) {
178                                 try {
179                                         $command->execute($this);
180                                 } catch (\Exception $e) {
181                                 }
182                         }
183                 });
184         }
185
186
187         private $logger;
188
189         private $nick;
190         private $token;
191
192         private $connector;
193         private $ws;
194         private $ready = false;
195         private $shutting_down = false;
196
197         private $last_contact;
198
199 }
200
201 ?>