Skip to content
This repository was archived by the owner on Feb 18, 2023. It is now read-only.

Commit d815b51

Browse files
committed
Added ability to host behind PHP-CGI/FPM
1 parent f7aea12 commit d815b51

File tree

3 files changed

+70
-11
lines changed

3 files changed

+70
-11
lines changed

README.md

Lines changed: 33 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -70,8 +70,8 @@ Now that you have registered commands, you can set up an HTTP server to listen f
7070

7171
There are a few ways to set up an HTTP server to listen for requests:
7272
- The built-in ReactPHP HTTP server.
73-
- Using an external HTTP server such as Apache or nginx (documentation to come).
74-
- Using the built-in ReactPHP HTTP server without HTTPS and using Apache or nginx as a reverse proxy.
73+
- Using an external HTTP server such as Apache or nginx.
74+
- Using the built-in ReactPHP HTTP server without HTTPS and using Apache or nginx as a reverse proxy (recommended).
7575

7676
Whatever path you choose, the server **must** be protected with HTTPS - Discord will not accept regular HTTP.
7777

@@ -157,6 +157,37 @@ $client->registerCommand('my_cool_command', function (Interaction $interaction,
157157
$discord->run();
158158
```
159159

160+
### Running behing PHP-CGI/PHP-FPM
161+
162+
To run behind CGI/FPM and a webserver, the `kambo/httpmessage` package is required:
163+
164+
```sh
165+
$ composer require kambo/httpmessage
166+
```
167+
168+
The syntax is then exactly the same as if you were running with the ReactPHP http server, except for the last line:
169+
170+
```php
171+
<?php
172+
173+
include 'vendor/autoload.php';
174+
175+
use Discord\Slash\Client;
176+
177+
$client = new Client([
178+
'public_key' => '???????',
179+
'uri' => null, // note the null uri - signifies to not register the socket
180+
]);
181+
182+
// register your commands like normal
183+
$client->registerCommand(...);
184+
185+
// note the difference here - runCgi instead of run
186+
$client->runCgi();
187+
```
188+
189+
Do note that the regular DiscordPHP client will not run on CGI or FPM, so your mileage may vary.
190+
160191
### Setting up Apache2 as a reverse proxy
161192

162193
Assuming you already have Apache2 installed and the SSL certificates on your server:

composer.json

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,5 +24,8 @@
2424
},
2525
"require-dev": {
2626
"symfony/var-dumper": "^5.2"
27+
},
28+
"suggest": {
29+
"kambo/httpmessage": "Required for hosting the command server behind a CGI/FPM server."
2730
}
2831
}

src/Discord/Client.php

Lines changed: 34 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -7,8 +7,11 @@
77
use Discord\InteractionType;
88
use Discord\Slash\Parts\Interaction;
99
use Discord\Slash\Parts\RegisteredCommand;
10+
use Exception;
1011
use InvalidArgumentException;
11-
use React\Http\MEssage\Response;
12+
use Kambo\Http\Message\Environment\Environment;
13+
use Kambo\Http\Message\Factories\Environment\ServerRequestFactory;
14+
use React\Http\Message\Response;
1215
use Monolog\Handler\StreamHandler;
1316
use Monolog\Logger;
1417
use Psr\Http\Message\ServerRequestInterface;
@@ -116,14 +119,14 @@ private function resolveOptions(array $options): array
116119
*/
117120
private function registerServer()
118121
{
122+
// no uri => cgi/fpm
123+
if (is_null($this->options['uri'])) {
124+
return;
125+
}
126+
119127
$this->server = new HttpServer($this->getLoop(), [$this, 'handleRequest']);
120128
$this->socket = new SocketServer($this->options['uri'], $this->getLoop(), $this->options['socket_options']);
121-
122-
// future tick so that the socket won't listen
123-
// when running in CGI mode
124-
$this->getLoop()->futureTick(function () {
125-
$this->server->listen($this->socket);
126-
});
129+
$this->server->listen($this->socket);
127130
}
128131

129132
/**
@@ -138,7 +141,7 @@ public function handleRequest(ServerRequestInterface $request)
138141
$timestamp = $request->getHeaderLine('X-Signature-Timestamp');
139142

140143
if (empty($signature) || empty($timestamp) || ! DiscordInteraction::verifyKey((string) $request->getBody(), $signature, $timestamp, $this->options['public_key'])) {
141-
return new Response(401, [0], 'Not verified');
144+
return \React\Promise\Resolve(new Response(401, [0], 'Not verified'));
142145
}
143146

144147
$interaction = new Interaction(json_decode($request->getBody(), true));
@@ -182,7 +185,7 @@ private function handleApplicationCommand(Interaction $interaction): void
182185
{
183186
$checkCommand = function ($command) use ($interaction, &$checkCommand) {
184187
if (isset($this->commands[$command['name']])) {
185-
if ($this->commands[$command['name']]->execute($command['options'], $interaction)) return true;
188+
if ($this->commands[$command['name']]->execute($command['options'] ?? [], $interaction)) return true;
186189
}
187190

188191
foreach ($command['options'] ?? [] as $option) {
@@ -221,6 +224,28 @@ public function registerCommand($name, callable $callback = null): RegisteredCom
221224
return $this->commands[$baseCommand]->addSubCommand($name, $callback);
222225
}
223226

227+
/**
228+
* Runs the client on a CGI/FPM server.
229+
*/
230+
public function runCgi()
231+
{
232+
if (empty($_SERVER['REMOTE_ADDR'])) {
233+
throw new Exception('The `runCgi()` method must only be called from PHP-CGI/FPM.');
234+
}
235+
236+
if (! class_exists(Environment::class)) {
237+
throw new Exception('The `kambo/httpmessage` package must be installed to handle slash command interactions with a CGI/FPM server.');
238+
}
239+
240+
$environment = new Environment($_SERVER, fopen('php://input', 'w+'), $_POST, $_COOKIE, $_FILES);
241+
$serverRequest = (new ServerRequestFactory())->create($environment);
242+
243+
$this->handleRequest($serverRequest)->then(function (Response $response) {
244+
http_response_code($response->getStatusCode());
245+
echo (string) $response->getBody();
246+
});
247+
}
248+
224249
/**
225250
* Starts the ReactPHP event loop.
226251
*/

0 commit comments

Comments
 (0)